diff --git a/extensions/shared/agent-session-page.test.ts b/extensions/shared/agent-session-page.test.ts index 36c4b4b4..93214b37 100644 --- a/extensions/shared/agent-session-page.test.ts +++ b/extensions/shared/agent-session-page.test.ts @@ -47,11 +47,10 @@ function state(): AgentSessionPageState { }; } -test("writable and read-only children use one full-terminal page", () => { +test("Direct and Workflow children use one read-only full-terminal page", () => { const direct = new AgentSessionPage(tui(18), theme, keybindings, { getState: state, close() {}, - send() {}, }); const workflow = new AgentSessionPage(tui(18), theme, keybindings, { getState: state, diff --git a/extensions/shared/agent-session-page.ts b/extensions/shared/agent-session-page.ts index bc0b8b0b..5f04e726 100644 --- a/extensions/shared/agent-session-page.ts +++ b/extensions/shared/agent-session-page.ts @@ -4,7 +4,6 @@ import type { } from "@earendil-works/pi-coding-agent"; import type { Component, Focusable, TUI } from "@earendil-works/pi-tui"; import { - Input, Key, matchesKey, truncateToWidth, @@ -36,7 +35,6 @@ export interface AgentSessionPageState { export interface AgentSessionPageSource { getState(): AgentSessionPageState | undefined; close(): void; - send?(text: string): void; abort?(): void; } @@ -67,14 +65,13 @@ function stateGlyph(state: AgentSessionPageState, theme: Theme, now: number) { /** * One full-screen child-session page shared by Direct and Workflow adapters. * The source owns lifecycle facts; this module owns only page rendering, - * reading position, optional input, and navigation back to the parent view. + * reading position, and navigation back to the parent view. */ export class AgentSessionPage implements Component, Focusable { private tui: TUI; private theme: Theme; private keybindings: KeybindingsManager; private source: AgentSessionPageSource; - private input = new Input(); private renderer = new AgentTranscriptRenderer(); private viewport = new TranscriptViewport(); private rowCount = 0; @@ -86,7 +83,6 @@ export class AgentSessionPage implements Component, Focusable { } set focused(value: boolean) { this._focused = value; - this.input.focused = value; } constructor( @@ -99,14 +95,6 @@ export class AgentSessionPage implements Component, Focusable { this.theme = theme; this.keybindings = keybindings; this.source = source; - this.input.onSubmit = (value: string) => { - const text = value.trim(); - if (!text || !this.source.send) return; - this.input.setValue(""); - this.source.send(text); - this.viewport.scrollToEnd(this.rowCount, this.viewportSize); - this.tui.requestRender(); - }; } handleInput(data: string) { @@ -127,27 +115,21 @@ export class AgentSessionPage implements Component, Focusable { return; } - const writable = Boolean(this.source.send); - const inputEmpty = !writable || this.input.getValue().length === 0; if ( - inputEmpty && - (this.keybindings.matches(data, "tui.editor.cursorLeft") || - (!writable && data === "h")) + this.keybindings.matches(data, "tui.editor.cursorLeft") || + data === "h" ) { this.source.close(); return; } - if ( - this.keybindings.matches(data, "tui.editor.cursorUp") || - (!writable && data === "k") - ) { + if (this.keybindings.matches(data, "tui.editor.cursorUp") || data === "k") { this.viewport.scrollBy(-SCROLL_STEP, this.rowCount, this.viewportSize); this.tui.requestRender(); return; } if ( this.keybindings.matches(data, "tui.editor.cursorDown") || - (!writable && data === "j") + data === "j" ) { this.viewport.scrollBy(SCROLL_STEP, this.rowCount, this.viewportSize); this.tui.requestRender(); @@ -177,20 +159,16 @@ export class AgentSessionPage implements Component, Focusable { this.tui.requestRender(); return; } - if (inputEmpty && (matchesKey(data, Key.home) || data === "g")) { + if (matchesKey(data, Key.home) || data === "g") { this.viewport.scrollToTop(this.rowCount, this.viewportSize); this.tui.requestRender(); return; } - if (inputEmpty && (matchesKey(data, Key.end) || data === "G")) { + if (matchesKey(data, Key.end) || data === "G") { this.viewport.scrollToEnd(this.rowCount, this.viewportSize); this.tui.requestRender(); return; } - if (writable) { - this.input.handleInput(data); - this.tui.requestRender(); - } } private rule(width: number, left = "", right = "") { @@ -245,8 +223,7 @@ export class AgentSessionPage implements Component, Focusable { metadata.shift(); } - const writable = Boolean(this.source.send); - const chromeRows = writable ? 5 : 4; + const chromeRows = 4; const errorRows = state.errorText ? 1 : 0; const bodyHeight = Math.max(1, height - chromeRows); const transcriptCapacity = Math.max(1, bodyHeight - errorRows); @@ -300,12 +277,9 @@ export class AgentSessionPage implements Component, Focusable { ), ), ); - if (writable) lines.push(...this.input.render(width)); - const keys = (binding: Parameters[0]) => configuredKeys(this.keybindings, binding); const hints: ScreenHint[] = []; - if (writable) hints.push([keys("tui.input.submit"), "send"]); hints.push([keys("app.interrupt"), "back"]); if (this.source.abort) hints.push([keys("app.clear"), "abort run"]); hints.push( @@ -322,7 +296,6 @@ export class AgentSessionPage implements Component, Focusable { } invalidate() { - this.input.invalidate(); this.renderer.invalidate(); } } diff --git a/extensions/subagents/src/ui/takeover.ts b/extensions/subagents/src/ui/takeover.ts index d31120e9..1452f2b6 100644 --- a/extensions/subagents/src/ui/takeover.ts +++ b/extensions/subagents/src/ui/takeover.ts @@ -2,8 +2,8 @@ * Takeover UI for subagents (ported from v1, rendering from the synchronous * SubagentReadModel instead of live pi sessions): * - SubagentDashboard: compact picker docked above the input, listing all subagents. - * - TakeoverView: full interactive view of one subagent with an input line - * to steer/continue it. + * - TakeoverView: full read-only view of one subagent; steering remains owned + * by the parent model through the subagent tools. */ import type { @@ -432,7 +432,6 @@ export class TakeoverView implements Component, Focusable { }; }, close: () => this.close(done), - send: (text) => view.requestSend(id, text), abort: () => view.requestAbort(id), }); this.unsubscribe = view.subscribeTo(id, () => { diff --git a/extensions/subagents/takeover.test.ts b/extensions/subagents/takeover.test.ts index 33749a2c..b7746770 100644 --- a/extensions/subagents/takeover.test.ts +++ b/extensions/subagents/takeover.test.ts @@ -1,6 +1,5 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { stripVTControlCharacters } from "node:util"; import type { KeybindingsManager, Theme, @@ -201,6 +200,28 @@ test("chrome rows stay width-bounded and takeover uses three rules", () => { } }); +test("takeover is a read-only child page without a message editor", () => { + const running = snap("run"); + let sends = 0; + const list: SubagentReadModel = { + ...model([running]), + requestSend() { + sends += 1; + }, + }; + const view = new TakeoverView(tui(20), theme, keys, "run", list, () => {}); + try { + assert.doesNotMatch(view.render(80).join("\n"), /\bsend\b/); + view.handleInput("1"); + view.handleInput("2"); + view.handleInput("3"); + view.handleInput("\r"); + assert.equal(sends, 0); + } finally { + view.dispose(); + } +}); + test("takeover scroll indicator lives in its rule without changing overlay height", () => { const transcript = Array.from({ length: 40 }, (_, index) => ({ kind: "assistant" as const, @@ -277,26 +298,3 @@ test("takeover pauses on an absolute reading anchor and resumes at the end", () view.dispose(); } }); - -test("Home and End edit non-empty input instead of moving the transcript", () => { - const view = new TakeoverView( - tui(20), - theme, - keys, - "run", - model([snap("run")]), - () => {}, - ); - try { - view.handleInput("abc"); - view.handleInput("\u001b[H"); - view.handleInput("X"); - assert.match(stripVTControlCharacters(view.render(80).join("\n")), /Xabc/); - - view.handleInput("\u001b[F"); - view.handleInput("Y"); - assert.match(stripVTControlCharacters(view.render(80).join("\n")), /XabcY/); - } finally { - view.dispose(); - } -});