diff --git a/src/tui-opentui/command-surfaces.ts b/src/tui-opentui/command-surfaces.ts index 72e77ccfa..93f754739 100644 --- a/src/tui-opentui/command-surfaces.ts +++ b/src/tui-opentui/command-surfaces.ts @@ -504,7 +504,7 @@ function renderSettingsMenu( export function openSettingsSurface(shell: AppShell, deps: CommandSurfaceDeps): void { const settings = deps.settings if (settings === undefined) { - openSettingsOverlay(shell) + deps.notify("Settings are not available in this session.") return } if (deps.permissions === undefined) { diff --git a/src/tui-opentui/demo.ts b/src/tui-opentui/demo.ts index 4b4ccc947..5903de761 100644 --- a/src/tui-opentui/demo.ts +++ b/src/tui-opentui/demo.ts @@ -20,7 +20,7 @@ import { attachSessionBridge, createRecordingPort, } from "./runtime-bridge.js" -import { makeObserveFixture } from "./residuals.js" +import type { ObserveSession } from "./residuals.js" import { openModelPickerOverlay, openOperatorOverlay, @@ -32,15 +32,55 @@ import { createAppShell, enterSubagentObserve, openHelpOverlay, + openListOverlay, openMentionsOverlay, - openPluginsOverlay, - openResumeOverlay, openSettingsOverlay, paintChrome, setChromeZones, setShellRunState, } from "./shell.js" +/** Demo-only rows: never shipped, just something to look at in `s`/`l`/`e`/`n`. */ +const DEMO_SETTINGS_ITEMS: readonly string[] = [ + "Permissions — revoke remembered approvals", + "Compaction — summarize vs drop", + "Session mode — auto / ask / plan", + "Close settings", +] + +const DEMO_PLUGINS_ITEMS: readonly string[] = [ + "plugin:linear — enabled", + "plugin:github — needs trust", + "Close plugins", +] + +const DEMO_RESUME_ITEMS: readonly string[] = [ + "Fix permissions overflow · 2h ago · idle", + "Wave 6 palette work · yesterday · done", + "Close resume", +] + +const DEMO_MENTION_ITEMS: readonly string[] = [ + "@src/tui-opentui/shell.ts", + "@AGENTS.md", + "Close mentions", +] + +function demoObserveSession(): ObserveSession { + return { + sessionId: "child-1", + agentId: "explore", + description: "map callers of openListOverlay", + lines: [ + { role: "system", text: "— child session explore —" }, + { role: "user", text: "find every openListOverlay caller" }, + { role: "assistant", text: "Searching src/tui-opentui…" }, + { role: "tool", text: "grep openListOverlay → 6 hits", meta: "tool.done" }, + { role: "assistant", text: "Report ready for parent." }, + ], + } +} + if (!process.stdout.isTTY) { console.error("demo requires a TTY (stdout is not a terminal)") process.exit(1) @@ -163,7 +203,7 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => { !key.meta && shell.prompt.value.length === 0 ) { - openSettingsOverlay(shell) + openSettingsOverlay(shell, { items: DEMO_SETTINGS_ITEMS }) return } @@ -183,7 +223,12 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => { !key.meta && shell.prompt.value.length === 0 ) { - openPluginsOverlay(shell) + openListOverlay(shell, { + kind: "plugins", + title: "plugins", + items: DEMO_PLUGINS_ITEMS, + frameId: "overlay-plugins", + }) return } @@ -193,7 +238,12 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => { !key.meta && shell.prompt.value.length === 0 ) { - openResumeOverlay(shell) + openListOverlay(shell, { + kind: "resume", + title: "resume session", + items: DEMO_RESUME_ITEMS, + frameId: "overlay-resume", + }) return } @@ -203,7 +253,7 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => { !key.meta && shell.prompt.value.length === 0 ) { - openMentionsOverlay(shell) + openMentionsOverlay(shell, { items: DEMO_MENTION_ITEMS }) return } @@ -213,7 +263,7 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => { !key.meta && shell.prompt.value.length === 0 ) { - enterSubagentObserve(shell, makeObserveFixture()) + enterSubagentObserve(shell, demoObserveSession()) return } diff --git a/src/tui-opentui/observe-live.test.ts b/src/tui-opentui/observe-live.test.ts index c5131dea7..d54e80f32 100644 --- a/src/tui-opentui/observe-live.test.ts +++ b/src/tui-opentui/observe-live.test.ts @@ -358,7 +358,7 @@ describe("palette observe action asks the host for a live session", () => { ) }) - test("falls back to the fixture when no host handler is set", async () => { + test("stays out of observe with an honest message when no host handler is set", async () => { await withTestRenderer( async (h) => { const shell = createAppShell(h.renderer, { @@ -367,7 +367,10 @@ describe("palette observe action asks the host for a live session", () => { }) try { runPaletteAction(shell, "observe") - expect(shell.observe?.sessionId).toBe("child-1") + expect(shell.observe).toBeNull() + expect( + shell.streamLog.some((r) => r.text === "no subagent session to observe"), + ).toBe(true) } finally { shell.dispose() } diff --git a/src/tui-opentui/overlay-fixture-fallback.test.ts b/src/tui-opentui/overlay-fixture-fallback.test.ts new file mode 100644 index 000000000..814163efb --- /dev/null +++ b/src/tui-opentui/overlay-fixture-fallback.test.ts @@ -0,0 +1,37 @@ +/** + * CL-5596: a missing surface dependency must produce an honest empty state, + * never the hardcoded rows from residuals.ts rendered as if they were real. + */ +import { describe, expect, test } from "bun:test" + +import { openSettingsSurface, type CommandSurfaceDeps } from "./command-surfaces.js" +import { withTestRenderer } from "./harness.js" +import { createAppShell } from "./shell.js" + +describe("overlay dependency gaps never render fixture content", () => { + test("settings surface without a settings dependency shows no fabricated rows", async () => { + await withTestRenderer( + async (h) => { + const shell = createAppShell(h.renderer, { + terminal: { columns: 80, rows: 24 }, + run: "idle", + }) + try { + const notified: string[] = [] + const deps: CommandSurfaceDeps = { notify: (text) => notified.push(text) } + + openSettingsSurface(shell, deps) + + expect(shell.overlayItems).not.toContain( + "Permissions — revoke remembered approvals", + ) + expect(shell.overlayItems).not.toContain("Compaction — summarize vs drop") + expect(notified.length).toBeGreaterThan(0) + } finally { + shell.dispose() + } + }, + { width: 80, height: 24 }, + ) + }) +}) diff --git a/src/tui-opentui/overlay-paint.test.ts b/src/tui-opentui/overlay-paint.test.ts index 8c51a4c78..e5e05ff73 100644 --- a/src/tui-opentui/overlay-paint.test.ts +++ b/src/tui-opentui/overlay-paint.test.ts @@ -16,8 +16,6 @@ import { openListOverlay, openMentionsOverlay, openPalette, - openPluginsOverlay, - openResumeOverlay, openSettingsOverlay, setPromptModelLabel, type AppShell, @@ -153,7 +151,10 @@ describe("overlay host never shares cells with the prompt border", () => { { width: 60, height: 24 }, ] as const) { test(`mention popup with matches clears the prompt border at ${size.width}x${size.height}`, async () => { - const { frame } = await paintOverlay((shell) => openMentionsOverlay(shell), size) + const { frame } = await paintOverlay( + (shell) => openMentionsOverlay(shell, { items: ["@src/file.ts", "@AGENTS.md"] }), + size, + ) // Every border rule stays a border rule: no list text glued onto it, and // the overlay host's own rules never share a row with the prompt box's. @@ -170,11 +171,27 @@ describe("overlay host never shares cells with the prompt border", () => { describe("every overlay kind paints clean rows", () => { const openers: readonly [string, (shell: AppShell) => void][] = [ - ["settings", (s) => openSettingsOverlay(s)], + ["settings", (s) => openSettingsOverlay(s, { items: ["Compaction", "Close settings"] })], ["help", (s) => openHelpOverlay(s)], - ["plugins", (s) => openPluginsOverlay(s)], - ["resume", (s) => openResumeOverlay(s)], - ["mentions", (s) => openMentionsOverlay(s)], + [ + "plugins", + (s) => + openListOverlay(s, { + kind: "plugins", + title: "plugins", + items: ["plugin:linear — enabled", "Close plugins"], + }), + ], + [ + "resume", + (s) => + openListOverlay(s, { + kind: "resume", + title: "resume session", + items: ["Fix permissions overflow · 2h ago · idle", "Close resume"], + }), + ], + ["mentions", (s) => openMentionsOverlay(s, { items: ["@src/file.ts", "Close mentions"] })], ["palette", (s) => openPalette(s)], [ "permissions", diff --git a/src/tui-opentui/palette.ts b/src/tui-opentui/palette.ts index f585ca280..5ffef111a 100644 --- a/src/tui-opentui/palette.ts +++ b/src/tui-opentui/palette.ts @@ -21,9 +21,6 @@ export type PaletteActionId = | "copy_active" | "toggle_mouse" | "help" - | "settings" - | "plugins" - | "resume" | "mentions" | "observe" @@ -37,9 +34,6 @@ const RESIDUAL_ACTION_IDS = new Set([ "copy_active", "toggle_mouse", "help", - "settings", - "plugins", - "resume", "mentions", "observe", ]) @@ -130,24 +124,6 @@ export const DEFAULT_PALETTE_COMMANDS: readonly PaletteCommand[] = [ keywords: ["keys", "bindings", "help"], dispatch: "residual", }, - { - id: "settings", - label: "Open settings", - keywords: ["config", "preferences", "options"], - dispatch: "residual", - }, - { - id: "plugins", - label: "Manage plugins", - keywords: ["mcp", "extension", "plugin"], - dispatch: "residual", - }, - { - id: "resume", - label: "Resume prior session", - keywords: ["history", "session", "picker"], - dispatch: "residual", - }, { id: "mentions", label: "Insert file mention", diff --git a/src/tui-opentui/residuals.ts b/src/tui-opentui/residuals.ts index 0eccae38d..fded57486 100644 --- a/src/tui-opentui/residuals.ts +++ b/src/tui-opentui/residuals.ts @@ -1,13 +1,11 @@ /** - * Wave 7 residual surface fixtures + observe session types (pure). - * Shell openers inject host catalogs via OpenResidualListOpts; fixtures apply - * only when the host omits `items`. - * - * Hosts can also build rows with {@link residualListFromCatalog} and resolve - * accept callbacks via {@link residualIdFromSelection}. + * Residual list-overlay helpers + observe session types (pure, production). + * Hosts build rows with {@link residualListFromCatalog} and resolve accept + * callbacks via {@link residualIdFromSelection}; overlay openers require + * `items` from the caller that owns the data. Demo/fixture data lives in + * demo.ts, not here. */ -import { SHELL_SHORTCUTS } from "./keybindings.js" import type { StreamRow } from "./stream.js" /** Host-owned residual row: stable id + display label. */ @@ -44,81 +42,9 @@ export function residualIdFromSelection( return itemIds[selection.index] } -export function makeSettingsItems(): readonly string[] { - return [ - "Permissions — revoke remembered approvals", - "Compaction — summarize vs drop", - "Session mode — auto / ask / plan", - "Sub-agents — max concurrent", - "Tools — wait-for-approval budget", - "Telemetry — usage opt-in", - "Close settings", - ] -} - -/** Help overlay rows derived from the OpenTUI shell's own keybinding - * catalog, so they cannot drift from what the shell actually implements. */ -export function makeHelpItems(): readonly string[] { - return [ - ...SHELL_SHORTCUTS.map((s) => `${s.keys} — ${s.description}`), - "Close help", - ] -} - -export function makePluginsItems(): readonly string[] { - return [ - "plugin:linear — enabled", - "plugin:github — needs trust", - "plugin:exa — enabled", - "Add plugin from path…", - "Web override: none", - "Close plugins", - ] -} - -export function makeResumeItems(): readonly string[] { - return [ - "Fix permissions overflow · 2h ago · idle", - "Wave 6 palette work · yesterday · done", - "Spike OpenTUI sticky scroll · 3d · done", - "Untitled session · 1w · canceled", - "Close resume", - ] -} - -export function makeMentionItems(): readonly string[] { - return [ - "@src/tui-opentui/shell.ts", - "@src/tui-opentui/residuals.ts", - "@docs/plans/tui-layout-scroll-platform.md", - "@AGENTS.md", - "Close mentions", - ] -} - export type ObserveSession = { readonly sessionId: string readonly agentId: string readonly description: string readonly lines: readonly StreamRow[] } - -/** Fixture child session for tests/demo. */ -export function makeObserveFixture(): ObserveSession { - return { - sessionId: "child-1", - agentId: "explore", - description: "map callers of openListOverlay", - lines: [ - { role: "system", text: "— child session explore —" }, - { role: "user", text: "find every openListOverlay caller" }, - { role: "assistant", text: "Searching src/tui-opentui…" }, - { - role: "tool", - text: "grep openListOverlay → 6 hits", - meta: "tool.done", - }, - { role: "assistant", text: "Report ready for parent." }, - ], - } -} diff --git a/src/tui-opentui/shell.ts b/src/tui-opentui/shell.ts index ae57d08fa..1815e3490 100644 --- a/src/tui-opentui/shell.ts +++ b/src/tui-opentui/shell.ts @@ -124,21 +124,13 @@ import { type PaletteActionId, type PaletteCommand, } from "./palette.js" -import { shortcutForPaletteId } from "./keybindings.js" +import { SHELL_SHORTCUTS, shortcutForPaletteId } from "./keybindings.js" import { destroySubtree } from "./teardown.js" import { filterMentionSuggestions, splitMentionToken, } from "./mention-filter.js" -import { - makeHelpItems, - makeMentionItems, - makeObserveFixture, - makePluginsItems, - makeResumeItems, - makeSettingsItems, - type ObserveSession, -} from "./residuals.js" +import { type ObserveSession } from "./residuals.js" import { buildCopyTargets, createRecordingClipboard, @@ -4022,27 +4014,13 @@ export function runPaletteAction( openHelpOverlay(shell) return } - case "settings": { - openSettingsOverlay(shell) - return - } - case "plugins": { - openPluginsOverlay(shell) - return - } - case "resume": { - openResumeOverlay(shell) - return - } case "mentions": { - openMentionsOverlay(shell) + void openAtMentionSuggestions(shell) return } case "observe": { const onObserveRequest = getPaletteOnObserveRequest(shell) - const session = onObserveRequest - ? onObserveRequest() - : makeObserveFixture() + const session = onObserveRequest ? onObserveRequest() : null if (session) enterSubagentObserve(shell, session) else { appendStreamRow(shell, { @@ -4303,11 +4281,13 @@ export function leaveSubagentObserve(shell: AppShell): void { } /** - * Host-injected residual list open. Fixtures apply only when `items` is omitted. + * Host-injected residual list open. `items` is owned by the caller — there is + * no fallback, so a missing dependency must produce an honest empty state or + * a surfaced error upstream rather than reach this with nothing to show. * Per-open `onAccept` wins over shell-level residual hooks for that open. */ export type OpenResidualListOpts = { - readonly items?: readonly string[] + readonly items: readonly string[] /** Stable ids aligned with `items` (setting keys, session ids, paths). */ readonly itemIds?: readonly string[] /** Plain chosen value aligned with `items`, for the accept echo (see `OpenListOverlayOpts.itemValues`). */ @@ -4323,79 +4303,54 @@ export type OpenResidualListOpts = { export function openSettingsOverlay( shell: AppShell, - opts?: OpenResidualListOpts, + opts: OpenResidualListOpts, ): void { openListOverlay(shell, { kind: "settings", title: "settings", - items: opts?.items ?? makeSettingsItems(), - activeIndex: opts?.activeIndex ?? 0, + items: opts.items, + activeIndex: opts.activeIndex ?? 0, frameId: "overlay-settings", - ...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), - ...(opts?.itemValues !== undefined ? { itemValues: opts.itemValues } : {}), - ...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), - ...(opts?.onCycle !== undefined ? { onCycle: opts.onCycle } : {}), - ...(opts?.describe !== undefined ? { describe: opts.describe } : {}), + ...(opts.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), + ...(opts.itemValues !== undefined ? { itemValues: opts.itemValues } : {}), + ...(opts.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), + ...(opts.onCycle !== undefined ? { onCycle: opts.onCycle } : {}), + ...(opts.describe !== undefined ? { describe: opts.describe } : {}), }) } -export function openHelpOverlay( - shell: AppShell, - opts?: OpenResidualListOpts, -): void { +/** Help rows derived from the shell's own keybinding catalog, so they cannot + * drift from what the shell actually implements — there is no host dependency + * to omit, so this never takes user-supplied items. */ +function helpItems(): readonly string[] { + return [ + ...SHELL_SHORTCUTS.map((s) => `${s.keys} — ${s.description}`), + "Close help", + ] +} + +export function openHelpOverlay(shell: AppShell): void { openListOverlay(shell, { kind: "help", title: "help · keymap", - items: opts?.items ?? makeHelpItems(), - activeIndex: opts?.activeIndex ?? 0, + items: helpItems(), + activeIndex: 0, frameId: "overlay-help", - ...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), - ...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), - }) -} - -export function openPluginsOverlay( - shell: AppShell, - opts?: OpenResidualListOpts, -): void { - openListOverlay(shell, { - kind: "plugins", - title: "plugins", - items: opts?.items ?? makePluginsItems(), - activeIndex: opts?.activeIndex ?? 0, - frameId: "overlay-plugins", - ...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), - ...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), - }) -} - -export function openResumeOverlay( - shell: AppShell, - opts?: OpenResidualListOpts, -): void { - openListOverlay(shell, { - kind: "resume", - title: "resume session", - items: opts?.items ?? makeResumeItems(), - activeIndex: opts?.activeIndex ?? 0, - frameId: "overlay-resume", - ...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), - ...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), }) } export function openMentionsOverlay( shell: AppShell, - opts?: OpenResidualListOpts, + opts: OpenResidualListOpts, ): void { openListOverlay(shell, { kind: "mentions", title: "mentions", - items: opts?.items ?? makeMentionItems(), - activeIndex: opts?.activeIndex ?? 0, + items: opts.items, + activeIndex: opts.activeIndex ?? 0, frameId: "overlay-mentions", - ...(opts?.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), - ...(opts?.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), + ...(opts.itemIds !== undefined ? { itemIds: opts.itemIds } : {}), + ...(opts.onAccept !== undefined ? { onAccept: opts.onAccept } : {}), }) } diff --git a/src/tui-opentui/wave7.test.ts b/src/tui-opentui/wave7.test.ts index ade7a0cc1..e8dfdf1b8 100644 --- a/src/tui-opentui/wave7.test.ts +++ b/src/tui-opentui/wave7.test.ts @@ -1,19 +1,14 @@ /** - * Wave 7 — residual surfaces + subagent observe + readiness smoke. + * Wave 7 — residual list surfaces + subagent observe + readiness smoke. */ import { describe, expect, test } from "bun:test" import { focusOwner } from "./focus/index.js" import { withTestRenderer } from "./harness.js" import { SHELL_SHORTCUTS } from "./keybindings.js" import { - makeHelpItems, - makeMentionItems, - makeObserveFixture, - makePluginsItems, - makeResumeItems, - makeSettingsItems, residualIdFromSelection, residualListFromCatalog, + type ObserveSession, } from "./residuals.js" import { acceptOverlaySelection, @@ -26,14 +21,26 @@ import { moveOverlaySelection, openHelpOverlay, openMentionsOverlay, - openPluginsOverlay, - openResumeOverlay, openSettingsOverlay, setShellOverlayHooks, type OverlaySelection, - type PrimaryOverlayKind, } from "./shell.js" +const SETTINGS_TEST_ITEMS = ["Permissions", "Telemetry", "Close"] as const + +function testObserveSession(): ObserveSession { + return { + sessionId: "child-1", + agentId: "explore", + description: "map callers of openListOverlay", + lines: [ + { role: "system", text: "— child session explore —" }, + { role: "user", text: "find every openListOverlay caller" }, + { role: "assistant", text: "Searching src/tui-opentui…" }, + ], + } +} + describe("Wave 7: residual list surfaces", () => { test("settings open → navigate → Esc restores prompt", async () => { await withTestRenderer( @@ -43,9 +50,9 @@ describe("Wave 7: residual list surfaces", () => { run: "idle", }) try { - openSettingsOverlay(shell) + openSettingsOverlay(shell, { items: [...SETTINGS_TEST_ITEMS] }) expect(shell.overlayKind).toBe("settings") - expect(shell.overlayItems.length).toBe(makeSettingsItems().length) + expect(shell.overlayItems.length).toBe(SETTINGS_TEST_ITEMS.length) expect(focusOwner(shell.focus)).toBe("overlay") expect(shell.prompt.focused).toBe(false) @@ -66,7 +73,7 @@ describe("Wave 7: residual list surfaces", () => { ) }) - test("help / plugins / resume / mentions each open and Esc-restore", async () => { + test("help opens the shell's own keybinding catalog and Esc-restores", async () => { await withTestRenderer( async (h) => { const shell = createAppShell(h.renderer, { @@ -74,42 +81,16 @@ describe("Wave 7: residual list surfaces", () => { run: "idle", }) try { - const cases: Array<{ - open: () => void - kind: PrimaryOverlayKind - count: number - }> = [ - { - open: () => openHelpOverlay(shell), - kind: "help", - count: makeHelpItems().length, - }, - { - open: () => openPluginsOverlay(shell), - kind: "plugins", - count: makePluginsItems().length, - }, - { - open: () => openResumeOverlay(shell), - kind: "resume", - count: makeResumeItems().length, - }, - { - open: () => openMentionsOverlay(shell), - kind: "mentions", - count: makeMentionItems().length, - }, - ] - - for (const c of cases) { - c.open() - expect(shell.overlayKind).toBe(c.kind) - expect(shell.overlayItems.length).toBe(c.count) - expect(focusOwner(shell.focus)).toBe("overlay") - closeInsetOverlay(shell) - expect(shell.overlayList).toBeNull() - expect(focusOwner(shell.focus)).toBe("prompt") - } + openHelpOverlay(shell) + expect(shell.overlayKind).toBe("help") + expect(shell.overlayItems).toEqual([ + ...SHELL_SHORTCUTS.map((s) => `${s.keys} — ${s.description}`), + "Close help", + ]) + expect(focusOwner(shell.focus)).toBe("overlay") + closeInsetOverlay(shell) + expect(shell.overlayList).toBeNull() + expect(focusOwner(shell.focus)).toBe("prompt") } finally { shell.dispose() } @@ -126,7 +107,7 @@ describe("Wave 7: residual list surfaces", () => { run: "idle", }) try { - openSettingsOverlay(shell) + openSettingsOverlay(shell, { items: [...SETTINGS_TEST_ITEMS] }) h.pressKey("escape") await h.renderOnce() if (shell.overlayList) closeInsetOverlay(shell) @@ -157,7 +138,7 @@ describe("Wave 7: subagent observe", () => { }) const parentLen = shell.streamLog.length - const child = makeObserveFixture() + const child = testObserveSession() enterSubagentObserve(shell, child) expect(shell.observe?.agentId).toBe("explore") @@ -197,7 +178,7 @@ describe("Wave 7: subagent observe", () => { }) try { appendStreamRow(shell, { role: "user", text: "stay" }) - enterSubagentObserve(shell, makeObserveFixture()) + enterSubagentObserve(shell, testObserveSession()) expect(shell.observe).not.toBeNull() h.pressKey("escape") @@ -215,36 +196,7 @@ describe("Wave 7: subagent observe", () => { }) }) -describe("Wave 7: residual fixtures", () => { - test("catalogs are non-empty and stable", () => { - expect(makeSettingsItems().length).toBeGreaterThan(3) - expect(makeHelpItems()).toContain( - "Ctrl+O — open the command palette; press again to close it", - ) - expect(makeHelpItems()).toEqual([ - ...SHELL_SHORTCUTS.map((s) => `${s.keys} — ${s.description}`), - "Close help", - ]) - // Rows the interaction contract requires and the shell implements — - // regression guard for the rows that were previously dropped. - for (const keys of ["Enter", "Alt+Enter", "Tab"]) { - expect(SHELL_SHORTCUTS.some((s) => s.keys === keys)).toBe(true) - } - // Ctrl+O must describe the palette, never the Ink renderer's tool-expand - // binding (the two renderers disagree on this chord). - expect(makeHelpItems().some((l) => l.includes("Toggle expand tool output"))).toBe( - false, - ) - expect(makePluginsItems().some((l) => l.includes("plugin:"))).toBe(true) - expect(makeResumeItems().length).toBeGreaterThan(2) - expect( - makeMentionItems().every( - (l) => l.startsWith("@") || l.startsWith("Close"), - ), - ).toBe(true) - expect(makeObserveFixture().lines.length).toBeGreaterThan(2) - }) - +describe("Wave 7: residual catalog helpers", () => { test("residualListFromCatalog + residualIdFromSelection round-trip", () => { const catalog = residualListFromCatalog([ { id: "permissions", label: "Permissions" }, @@ -284,9 +236,6 @@ describe("Wave 7: residual live inject + accept", () => { "Telemetry", "Close", ]) - expect(shell.overlayItems.length).not.toBe( - makeSettingsItems().length, - ) moveOverlaySelection(shell, 1) acceptOverlaySelection(shell) @@ -308,67 +257,6 @@ describe("Wave 7: residual live inject + accept", () => { ) }) - test("resume shell-level onResume when no per-open onAccept", async () => { - await withTestRenderer( - async (h) => { - const shell = createAppShell(h.renderer, { - terminal: { columns: 80, rows: 24 }, - wireKeys: false, - }) - try { - const accepted: OverlaySelection[] = [] - setShellOverlayHooks(shell, { - onResume: (s) => accepted.push(s), - }) - openResumeOverlay(shell, { - items: ["Session A", "Session B"], - itemIds: ["sess-a", "sess-b"], - }) - moveOverlaySelection(shell, 1) - acceptOverlaySelection(shell) - expect(accepted).toEqual([ - { - kind: "resume", - index: 1, - label: "Session B", - id: "sess-b", - }, - ]) - } finally { - clearShellOverlayHooks(shell) - shell.dispose() - } - }, - { width: 80, height: 24 }, - ) - }) - - test("omitted items fall back to fixture catalogs", async () => { - await withTestRenderer( - async (h) => { - const shell = createAppShell(h.renderer, { - terminal: { columns: 80, rows: 24 }, - wireKeys: false, - }) - try { - openPluginsOverlay(shell) - expect(shell.overlayItems).toEqual([...makePluginsItems()]) - closeInsetOverlay(shell) - - openMentionsOverlay(shell) - expect(shell.overlayItems).toEqual([...makeMentionItems()]) - closeInsetOverlay(shell) - - openHelpOverlay(shell) - expect(shell.overlayItems).toEqual([...makeHelpItems()]) - } finally { - shell.dispose() - } - }, - { width: 80, height: 24 }, - ) - }) - test("per-open onAccept wins over shell residual hooks; Esc skips accept", async () => { await withTestRenderer( async (h) => {