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
14 changes: 7 additions & 7 deletions src/tui-opentui/command-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import {
} from "./command-catalog"

describe("commandItemsFromRegistry", () => {
test("maps listCommands-shaped entries to `/`-prefixed labels", () => {
test("maps listCommands-shaped entries to name-only `/` labels", () => {
const items = commandItemsFromRegistry([
{ name: "tasks", description: "Show work list" },
{ name: "clear", description: "Clear screen" },
])
expect(items).toEqual([
{
id: "tasks",
label: "/tasks — Show work list",
keywords: ["tasks", "slash", "command"],
label: "/tasks",
keywords: ["tasks", "Show work list", "slash", "command"],
},
{
id: "clear",
label: "/clear — Clear screen",
keywords: ["clear", "slash", "command"],
label: "/clear",
keywords: ["clear", "Clear screen", "slash", "command"],
},
])
})
Expand All @@ -36,7 +36,7 @@ describe("filterPaletteCommands", () => {
expect(filterPaletteCommands("", catalog)).toEqual(catalog)
})

test("matches by id, label, or keyword substring", () => {
test("matches by id, label, or description keyword substring", () => {
expect(filterPaletteCommands("compact", catalog).map((c) => c.id)).toEqual([
"compact",
])
Expand All @@ -55,6 +55,6 @@ describe("paletteLabels", () => {
const catalog = commandItemsFromRegistry([
{ name: "tasks", description: "Show work list" },
])
expect(paletteLabels(catalog)).toEqual(["/tasks — Show work list"])
expect(paletteLabels(catalog)).toEqual(["/tasks"])
})
})
6 changes: 4 additions & 2 deletions src/tui-opentui/command-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export function commandItemsFromRegistry(
): PaletteCommand[] {
return commands.map((c) => ({
id: c.name,
label: `/${c.name} — ${c.description}`,
keywords: [c.name, "slash", "command"],
// Name-only rows keep the slash popup scannable; description stays in
// keywords so typed filter still finds prose matches.
label: `/${c.name}`,
keywords: [c.name, c.description, "slash", "command"],
}))
}

Expand Down
31 changes: 26 additions & 5 deletions src/tui-opentui/palette-paint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import {
} from "./shell"

const CATALOG: readonly PaletteCommand[] = [
{ id: "help", label: "/help — show keymap help", keywords: ["help"] },
{ id: "model", label: "/model — switch model / provider", keywords: ["model"] },
{ id: "mcp", label: "/mcp — manage MCP servers", keywords: ["mcp"] },
{ id: "help", label: "/help", keywords: ["help", "show keymap help"] },
{ id: "model", label: "/model", keywords: ["model", "switch model / provider"] },
{ id: "mcp", label: "/mcp", keywords: ["mcp", "manage MCP servers"] },
]

async function paletteFrame(width: number): Promise<readonly string[]> {
Expand All @@ -46,15 +46,36 @@ function rowFor(rows: readonly string[], label: string): string | undefined {
}

describe("command list rows", () => {
test("shows the filter prompt with no title rule above it", async () => {
test("slash mode omits the orphan filter row and the title rule", async () => {
const rows = await paletteFrame(100)
expect(rows.some((r) => r.startsWith("─ command palette ─"))).toBe(false)
// Default open is typeToFilter:false — query lives in the prompt.
expect(rows.some((r) => r.trim() === ">")).toBe(false)
})

test("typed filter mode keeps the Amp-style filter prompt", async () => {
const rows = await withTestRenderer(
async (h) => {
const shell = createAppShell(h.renderer, {
terminal: { columns: 100, rows: 32 },
wireKeys: false,
run: "idle",
})
openPalette(shell, { catalog: CATALOG, typeToFilter: true })
await h.renderOnce()
return h
.captureCharFrame()
.split("\n")
.map((line) => line.replace(/^\s*│/, "").replace(/│\s*$/, "").trimEnd())
},
{ width: 100, height: 32 },
)
expect(rows.some((r) => r.trim() === ">")).toBe(true)
})

test("has no leading selection marker or kind column", async () => {
const rows = await paletteFrame(100)
const help = rowFor(rows, "show keymap help")
const help = rowFor(rows, "/help")
expect(help).toBeDefined()
expect(help).not.toContain(">")
expect(help).not.toContain("view")
Expand Down
6 changes: 3 additions & 3 deletions src/tui-opentui/prompt-slash-exit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
} from "./shell"

const CATALOG: readonly PaletteCommand[] = [
{ id: "model", label: "/model — switch model" },
{ id: "mcp", label: "/mcp — manage MCP servers" },
{ id: "compact", label: "/compact — compact history" },
{ id: "model", label: "/model" },
{ id: "mcp", label: "/mcp" },
{ id: "compact", label: "/compact" },
]

type Ctx = {
Expand Down
9 changes: 5 additions & 4 deletions src/tui-opentui/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3541,12 +3541,13 @@ function repaintPalette(shell: AppShell): void {
kind: "palette",
title: state.title,
items: labels,
// Filter query row, Amp-style: the palette always shows what it filtered on.
body: `> ${state.query}`,
// Typed filter row only when the overlay owns keystrokes. The `/` popup
// keeps its query in the prompt, so a body of `>` would be orphan chrome.
...(state.typeToFilter ? { body: `> ${state.query}` } : {}),
frameId: "command-palette",
})
// No title rule row: the box is only ever the palette, and the filter row
// underneath already shows what's typed — a second header said nothing new.
// No title rule row: the box is only ever the palette, and when a filter
// row is present it already shows what's typed.
shell.overlayTitle.visible = false
shell.overlayTitle.content = ""
paintOverlayList(shell)
Expand Down
12 changes: 6 additions & 6 deletions src/tui-opentui/wave6.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import { stringWidth } from "../tui/view/height"
import type { PaletteCommand } from "./command-catalog"

const CATALOG: readonly PaletteCommand[] = [
{ id: "compact", label: "/compact — compact history" },
{ id: "help", label: "/help — show keymap help" },
{ id: "model", label: "/model — switch model" },
{ id: "compact", label: "/compact" },
{ id: "help", label: "/help" },
{ id: "model", label: "/model" },
]

describe("Wave 6: command list", () => {
Expand All @@ -56,9 +56,9 @@ describe("Wave 6: command list", () => {

await h.renderOnce()
const frame = h.captureCharFrame()
// The list drops its title rule row, so identify it on screen by
// its filter prompt and first row rather than the word "palette".
expect(frame).toMatch(/│\s*>\s*│/)
// Slash mode has no title rule and no orphan filter row — identify
// the list by its name-only command labels.
expect(frame).not.toMatch(/│\s*>\s*│/)
expect(frame).toContain("/compact")
// List labels live in overlayItems (frame may clip first row under tight height).
expect(shell.overlayItems[0]).toBe(CATALOG[0]!.label)
Expand Down
Loading