diff --git a/src/features/agents/Session.module.css b/src/features/agents/Session.module.css index a28570ef..7df8308d 100644 --- a/src/features/agents/Session.module.css +++ b/src/features/agents/Session.module.css @@ -77,27 +77,41 @@ color: var(--fg-subtle); } -/* Pending permission: an editor notification banner. */ +/* Pending permission: an editor notification banner. A long question (AskUserQuestion) wraps, + scrolling past a third of the height; the answers follow it, wrapping too. */ .banner { flex: none; display: flex; - align-items: center; - gap: 8px; + flex-wrap: wrap; + align-items: flex-start; + gap: 4px 8px; min-height: 36px; - padding: 0 12px; + padding: 9px 12px; background: var(--bg-feedback-warning); border-bottom: 1px solid var(--border-feedback-warning); color: var(--fg-strong); } +.banner > svg { + flex: none; + margin-top: 1px; +} .bannerText { + flex: 1 1 240px; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + max-height: 33vh; + overflow-y: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; } .bannerSpacer { flex: 1; } +.bannerActions { + display: flex; + flex-wrap: wrap; + gap: 4px 12px; + margin-left: auto; +} .bannerLink { flex: none; padding: 0; @@ -430,16 +444,24 @@ .permissionLine { display: flex; - align-items: center; + align-items: flex-start; gap: 6px; min-width: 0; color: var(--fg-secondary); } +/* Up to three lines; the full text is its tooltip. */ .permissionTitle { + display: -webkit-box; min-width: 0; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.permissionLine > svg, +.permissionLine > .muted { + flex: none; } .error { diff --git a/src/features/agents/SessionEditor.tsx b/src/features/agents/SessionEditor.tsx index e6976584..ff57ff3a 100644 --- a/src/features/agents/SessionEditor.tsx +++ b/src/features/agents/SessionEditor.tsx @@ -1151,7 +1151,9 @@ function PermissionLine({ task, entry }: { task: AgentTask; entry: Extract - {shortTitle(task, entry.title)} + + {shortTitle(task, entry.title)} + {entry.chosen ?? Waiting for your answer} ); @@ -1171,15 +1173,14 @@ function PermissionBanner({ task, entries }: { task: AgentTask; entries: AgentEn return (
- - {shortTitle(task, entry.title)} + {shortTitle(task, entry.title)} + + {entry.options.map((o) => ( + + ))} - - {entry.options.map((o) => ( - - ))}
); } diff --git a/src/features/agents/sessionState.test.tsx b/src/features/agents/sessionState.test.tsx index 36c5244d..9348c369 100644 --- a/src/features/agents/sessionState.test.tsx +++ b/src/features/agents/sessionState.test.tsx @@ -10,7 +10,11 @@ import { disconnect, useAgents } from "./store"; import { taskActions } from "./taskActions"; import styles from "./Session.module.css"; -const backend = vi.hoisted(() => ({ entries: new Map(), changes: [] as AgentChange[] })); +const backend = vi.hoisted(() => ({ + entries: new Map(), + changes: [] as AgentChange[], + answers: [] as Array<[string, number, string | null]>, +})); vi.mock("@/platform/ipc", async (original) => ({ ...(await original()), commands: { @@ -20,6 +24,10 @@ vi.mock("@/platform/ipc", async (original) => ({ }), agentChanges: async () => ({ status: "ok", data: backend.changes }), vcsBranchInfo: async () => ({ status: "ok", data: { head: "main" } }), + agentRespond: async (task: string, permission: number, option: string | null) => { + backend.answers.push([task, permission, option]); + return { status: "ok", data: null }; + }, }, })); vi.mock("@tauri-apps/api/webview", () => ({ @@ -93,6 +101,40 @@ beforeEach(() => { }); }); +describe("a pending question", () => { + it("shows the whole question in the banner, with every answer, and sends the chosen one", async () => { + const question = + 'Scenario 4 ("follows the TR") still leaves the "About your request" section empty after two rewrites.\nHow should we continue?'; + const entries: AgentEntry[] = [ + { + type: "permission", + id: 7, + title: question, + options: [ + { id: "a", name: "Ship it as is and record it (Recommended)", kind: "allow_once" }, + { id: "b", name: "Detect the request in code", kind: "allow_once" }, + { id: "c", name: "Skip", kind: "reject_once" }, + ], + chosen: null, + }, + ]; + backend.entries.set("A", entries); + backend.answers = []; + useAgents.setState({ entries: new Map(backend.entries) }); + render(); + const banner = await screen.findByRole("alert", { name: question }); + expect(banner.querySelector(`.${styles.bannerText}`)?.textContent).toBe(question); + const answers = within(banner.querySelector(`.${styles.bannerActions}`)!).getAllByRole("button"); + expect(answers.map((b) => b.textContent)).toEqual([ + "Ship it as is and record it (Recommended)", + "Detect the request in code", + "Skip", + ]); + fireEvent.click(answers[1]!); + await waitFor(() => expect(backend.answers).toEqual([["A", 7, "b"]])); + }); +}); + describe("session state across tasks and hosts", () => { it("returns to the task conversation when the docked task changes after opening a subagent", async () => { const dispose = registerActions(taskActions);