From 3b7f0bad053b4df959c988bf2d8847a34a696c32 Mon Sep 17 00:00:00 2001 From: griffinboris Date: Sun, 24 May 2026 08:50:30 -0400 Subject: [PATCH 1/2] feat(app): restore queued follow-up setting --- .opencode/tool/github-pr-search.ts | 64 -------- .../components/prompt-input/submit.test.ts | 98 +++++++++++- .../app/src/components/settings-general.tsx | 23 +++ packages/app/src/context/settings.tsx | 12 +- packages/app/src/pages/session.tsx | 30 ++-- .../session/session-followup-state.test.ts | 149 ++++++++++++++++++ .../pages/session/session-followup-state.ts | 17 ++ 7 files changed, 306 insertions(+), 87 deletions(-) delete mode 100644 .opencode/tool/github-pr-search.ts create mode 100644 packages/app/src/pages/session/session-followup-state.test.ts create mode 100644 packages/app/src/pages/session/session-followup-state.ts diff --git a/.opencode/tool/github-pr-search.ts b/.opencode/tool/github-pr-search.ts deleted file mode 100644 index 8bc8c554aaee..000000000000 --- a/.opencode/tool/github-pr-search.ts +++ /dev/null @@ -1,64 +0,0 @@ -/// -import { tool } from "@opencode-ai/plugin" -async function githubFetch(endpoint: string, options: RequestInit = {}) { - const response = await fetch(`https://api.github.com${endpoint}`, { - ...options, - headers: { - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - Accept: "application/vnd.github+json", - "Content-Type": "application/json", - ...(options.headers instanceof Headers ? Object.fromEntries(options.headers.entries()) : options.headers), - }, - }) - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) - } - return response.json() -} - -interface PR { - title: string - html_url: string -} - -export default tool({ - description: `Use this tool to search GitHub pull requests by title and description. - -This tool searches PRs in the anomalyco/opencode repository and returns LLM-friendly results including: -- PR number and title -- Author -- State (open/closed/merged) -- Labels -- Description snippet - -Use the query parameter to search for keywords that might appear in PR titles or descriptions.`, - args: { - query: tool.schema.string().describe("Search query for PR titles and descriptions"), - limit: tool.schema.number().describe("Maximum number of results to return").default(10), - offset: tool.schema.number().describe("Number of results to skip for pagination").default(0), - }, - async execute(args) { - const owner = "anomalyco" - const repo = "opencode" - - const page = Math.floor(args.offset / args.limit) + 1 - const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`) - const result = await githubFetch( - `/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`, - ) - - if (result.total_count === 0) { - return `No PRs found matching "${args.query}"` - } - - const prs = result.items as PR[] - - if (prs.length === 0) { - return `No other PRs found matching "${args.query}"` - } - - const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n") - - return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}` - }, -}) diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 83b6212dcc56..e2eb602f9aa2 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -1,7 +1,8 @@ import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" -import type { Prompt } from "@/context/prompt" +import type { ContextItem, Prompt } from "@/context/prompt" let createPromptSubmit: typeof import("./submit").createPromptSubmit +type FollowupDraft = import("./submit").FollowupDraft const createdClients: string[] = [] const createdSessions: string[] = [] @@ -20,10 +21,13 @@ const storedSessions: Record> = {} const promoted: Array<{ directory: string; sessionID: string }> = [] const sentShell: string[] = [] const syncedDirectories: string[] = [] +const removedContextKeys: string[] = [] let params: { id?: string } = {} let selected = "/repo/worktree-a" let variant: string | undefined +let promptContextItems: Array = [] +let promptResetCount = 0 const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] @@ -106,12 +110,17 @@ beforeAll(async () => { mock.module("@/context/prompt", () => ({ usePrompt: () => ({ current: () => promptValue, - reset: () => undefined, + reset: () => { + promptResetCount += 1 + }, set: () => undefined, context: { add: () => undefined, - remove: () => undefined, - items: () => [], + remove: (key: string) => { + removedContextKeys.push(key) + promptContextItems = promptContextItems.filter((item) => item.key !== key) + }, + items: () => promptContextItems, }, }), })) @@ -211,8 +220,11 @@ beforeEach(() => { params = {} sentShell.length = 0 syncedDirectories.length = 0 + removedContextKeys.length = 0 selected = "/repo/worktree-a" variant = undefined + promptContextItems = [] + promptResetCount = 0 for (const key of Object.keys(storedSessions)) delete storedSessions[key] }) @@ -343,3 +355,81 @@ describe("prompt submit worktree selection", () => { expect(optimisticSeeded).toEqual([true]) }) }) + +describe("queued followups", () => { + test("queues followup drafts instead of sending immediately", async () => { + params = { id: "session-1" } + variant = "high" + promptContextItems = [ + { + key: "context-1", + type: "file", + path: "src/index.ts", + selection: undefined, + comment: "Check this", + commentID: "comment-1", + commentOrigin: "file", + preview: "const value = 1", + }, + ] + + const queued: FollowupDraft[] = [] + const modes: Array<"normal" | "shell"> = [] + const popovers: Array<"at" | "slash" | null> = [] + let submitCalls = 0 + + const submit = createPromptSubmit({ + info: () => ({ id: "session-1" }), + imageAttachments: () => [], + commentCount: () => 1, + autoAccept: () => false, + mode: () => "normal", + working: () => true, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: (mode) => modes.push(mode), + setPopover: (popover) => popovers.push(popover), + shouldQueue: () => true, + onQueue: (draft) => queued.push(draft), + onSubmit: () => { + submitCalls += 1 + }, + }) + + const event = { preventDefault: () => undefined } as unknown as Event + + await submit.handleSubmit(event) + + expect(queued).toHaveLength(1) + expect(queued[0]).toMatchObject({ + sessionID: "session-1", + sessionDirectory: "/repo/main", + prompt: promptValue, + context: [ + { + key: "context-1", + type: "file", + path: "src/index.ts", + comment: "Check this", + commentID: "comment-1", + commentOrigin: "file", + preview: "const value = 1", + }, + ], + agent: "agent", + model: { providerID: "provider", modelID: "model" }, + variant: "high", + }) + expect(promptResetCount).toBe(1) + expect(promptContextItems).toEqual([]) + expect(removedContextKeys).toEqual(["context-1"]) + expect(modes).toEqual(["normal"]) + expect(popovers).toEqual([null]) + expect(submitCalls).toBe(0) + expect(optimistic).toEqual([]) + expect(createdSessions).toEqual([]) + }) +}) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index d9462e041f1e..e5895d6d6029 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -258,6 +258,11 @@ export const SettingsGeneral: Component = () => { { value: "dark", label: language.t("theme.scheme.dark") }, ]) + const followupOptions = createMemo((): { value: "queue" | "steer"; label: string }[] => [ + { value: "queue", label: language.t("settings.general.row.followup.option.queue") }, + { value: "steer", label: language.t("settings.general.row.followup.option.steer") }, + ]) + const languageOptions = createMemo(() => language.locales.map((locale) => ({ value: locale, @@ -388,6 +393,24 @@ export const SettingsGeneral: Component = () => { + +