Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/tui-opentui/command-surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
66 changes: 58 additions & 8 deletions src/tui-opentui/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}

Expand All @@ -213,7 +263,7 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
!key.meta &&
shell.prompt.value.length === 0
) {
enterSubagentObserve(shell, makeObserveFixture())
enterSubagentObserve(shell, demoObserveSession())
return
}

Expand Down
7 changes: 5 additions & 2 deletions src/tui-opentui/observe-live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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()
}
Expand Down
37 changes: 37 additions & 0 deletions src/tui-opentui/overlay-fixture-fallback.test.ts
Original file line number Diff line number Diff line change
@@ -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 },
)
})
})
31 changes: 24 additions & 7 deletions src/tui-opentui/overlay-paint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import {
openListOverlay,
openMentionsOverlay,
openPalette,
openPluginsOverlay,
openResumeOverlay,
openSettingsOverlay,
setPromptModelLabel,
type AppShell,
Expand Down Expand Up @@ -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.
Expand All @@ -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",
Expand Down
24 changes: 0 additions & 24 deletions src/tui-opentui/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ export type PaletteActionId =
| "copy_active"
| "toggle_mouse"
| "help"
| "settings"
| "plugins"
| "resume"
| "mentions"
| "observe"

Expand All @@ -37,9 +34,6 @@ const RESIDUAL_ACTION_IDS = new Set<string>([
"copy_active",
"toggle_mouse",
"help",
"settings",
"plugins",
"resume",
"mentions",
"observe",
])
Expand Down Expand Up @@ -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",
Expand Down
84 changes: 5 additions & 79 deletions src/tui-opentui/residuals.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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." },
],
}
}
Loading
Loading