From 5520fbb676651e719536370c8c8e4a08a00c7695 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Mon, 9 Feb 2026 11:25:29 +0000 Subject: [PATCH 1/3] feat: add savoir --- .gitignore | 2 + app/components/issue/CommentForm.vue | 6 + app/components/issue/Issue.vue | 41 +++- app/components/issue/Timeline.vue | 11 +- nuxt.config.ts | 4 + package.json | 1 + pnpm-lock.yaml | 15 ++ .../[name]/issues/[number]/savoir.post.ts | 175 ++++++++++++++++++ 8 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 server/api/repositories/[owner]/[name]/issues/[number]/savoir.post.ts diff --git a/.gitignore b/.gitignore index 5158299..80533e9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ logs # Workflow files .workflow-data/ .swc/ +.vercel +.env*.local diff --git a/app/components/issue/CommentForm.vue b/app/components/issue/CommentForm.vue index 629ef90..cc99347 100644 --- a/app/components/issue/CommentForm.vue +++ b/app/components/issue/CommentForm.vue @@ -15,6 +15,12 @@ const toast = useToast() const newComment = ref('') const isSubmitting = ref(false) +defineExpose({ + setContent(text: string) { + newComment.value = text + } +}) + defineShortcuts({ meta_s: { usingInput: true, diff --git a/app/components/issue/Issue.vue b/app/components/issue/Issue.vue index 60620d8..6430e7b 100644 --- a/app/components/issue/Issue.vue +++ b/app/components/issue/Issue.vue @@ -193,7 +193,7 @@ async function handleRefresh() { } // Handle scrolling to answer comment from Meta -const timelineRef = ref<{ scrollToComment: (commentId: number) => void } | null>(null) +const timelineRef = ref<{ scrollToComment: (commentId: number) => void, setCommentContent: (text: string) => void } | null>(null) function scrollToAnswerComment(commentId: number) { timelineRef.value?.scrollToComment(commentId) @@ -231,6 +231,30 @@ async function handleSync() { } } +// Ask Savoir (doc search) +const savoirLoading = ref(false) + +async function askSavoir() { + if (!issue.value || !issueUrl.value || savoirLoading.value) return + + savoirLoading.value = true + try { + const { response } = await $fetch<{ response: string }>(`${issueUrl.value}/savoir`, { + method: 'POST' + }) + timelineRef.value?.setCommentContent(response) + } catch (err: any) { + toast.add({ + title: 'Ask Savoir failed', + description: err.data?.message || err.message, + color: 'error', + icon: 'i-lucide-x' + }) + } finally { + savoirLoading.value = false + } +} + defineShortcuts({ meta_g: () => { if (props.item.htmlUrl) { @@ -343,6 +367,14 @@ const agentItems = computed(() => { } ] + const savoirItems = [ + { + label: 'Ask Savoir', + icon: 'i-lucide-book-open', + onSelect: () => askSavoir() + } + ] + // Add find duplicates only for issues (not PRs) if (!props.item.pullRequest) { return [ @@ -351,11 +383,12 @@ const agentItems = computed(() => { label: 'Find duplicates', icon: 'i-lucide-copy', onSelect: () => agent.suggest('duplicates') - }] + }], + savoirItems ] } - return [baseItems] + return [baseItems, savoirItems] }) @@ -424,7 +457,7 @@ const agentItems = computed(() => { icon="i-lucide-sparkles" variant="soft" label="Ask AI" - :loading="agent.isLoading.value" + :loading="agent.isLoading.value || savoirLoading" /> diff --git a/app/components/issue/Timeline.vue b/app/components/issue/Timeline.vue index cc32601..7892ee5 100644 --- a/app/components/issue/Timeline.vue +++ b/app/components/issue/Timeline.vue @@ -36,8 +36,14 @@ function scrollToComment(commentId: number) { } } -// Expose scroll function for parent components -defineExpose({ scrollToComment }) +const commentFormRef = ref<{ setContent: (text: string) => void } | null>(null) + +function setCommentContent(text: string) { + commentFormRef.value?.setContent(text) +} + +// Expose scroll function and comment setter for parent components +defineExpose({ scrollToComment, setCommentContent }) const { user } = useUserSession() @@ -226,6 +232,7 @@ const timelineItems = computed(() => {