From 6b7bbc711c0025a3a39bd7d1fcb1326fd39efe26 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Thu, 30 Jul 2026 18:21:17 +0200 Subject: [PATCH] tui: bind leader+i to image viewer Default i opens the prompt image attachment viewer from the keyboard. Drop thumbnail captions so previews stay compact and fill their allotted height. --- packages/tui/src/component/prompt/index.tsx | 36 ++++++++++++--------- packages/tui/src/config/v1/keybind.ts | 2 ++ packages/tui/test/config.test.tsx | 8 +++++ packages/tui/test/prompt/clipboard.test.ts | 15 +++++++-- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index fafc4e64f2..199ecc269b 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -640,6 +640,7 @@ export function Prompt(props: PromptProps) { "prompt.submit", "prompt.editor", "prompt.editor_context.clear", + "prompt.images.view", "prompt.stash", "prompt.stash.pop", "prompt.stash.list", @@ -1510,7 +1511,7 @@ export function Prompt(props: PromptProps) { - setFailed(true)} - /> - - {failed() ? "No preview" : (file.mention?.text ?? `Image ${index() + 1}`)} - + + No preview + + } + > + setFailed(true)} + /> + ) }} diff --git a/packages/tui/src/config/v1/keybind.ts b/packages/tui/src/config/v1/keybind.ts index 9787f9ad5d..b3ed88ced2 100644 --- a/packages/tui/src/config/v1/keybind.ts +++ b/packages/tui/src/config/v1/keybind.ts @@ -164,6 +164,7 @@ export const Definitions = { prompt_submit: keybind("none", "Submit prompt"), prompt_editor_context_clear: keybind("none", "Clear editor context"), + prompt_images_view: keybind("i", "View image attachments"), prompt_skills: keybind("none", "Open skill selector"), prompt_stash: keybind("none", "Stash prompt"), prompt_stash_pop: keybind("none", "Pop stashed prompt"), @@ -364,6 +365,7 @@ export const CommandMap = { display_thinking: "session.toggle.thinking", prompt_submit: "prompt.submit", prompt_editor_context_clear: "prompt.editor_context.clear", + prompt_images_view: "prompt.images.view", prompt_skills: "prompt.skills", prompt_stash: "prompt.stash", prompt_stash_pop: "prompt.stash.pop", diff --git a/packages/tui/test/config.test.tsx b/packages/tui/test/config.test.tsx index a9d5a007fe..20de66ce65 100644 --- a/packages/tui/test/config.test.tsx +++ b/packages/tui/test/config.test.tsx @@ -86,6 +86,14 @@ test("resolves a session move keybind", () => { expect(config.keybinds.get("session.move")).toMatchObject([{ key: "ctrl+o" }]) }) +test("resolves the image viewer keybind", () => { + const defaults = resolve({}, { terminalSuspend: true }) + const overridden = resolve({ keybinds: { prompt_images_view: "ctrl+shift+i" } }, { terminalSuspend: true }) + + expect(defaults.keybinds.get("prompt.images.view")).toMatchObject([{ key: "i" }]) + expect(overridden.keybinds.get("prompt.images.view")).toMatchObject([{ key: "ctrl+shift+i" }]) +}) + test("resolves message navigation defaults", () => { const config = resolve({}, { terminalSuspend: true }) diff --git a/packages/tui/test/prompt/clipboard.test.ts b/packages/tui/test/prompt/clipboard.test.ts index f7e772ffc2..41f315b366 100644 --- a/packages/tui/test/prompt/clipboard.test.ts +++ b/packages/tui/test/prompt/clipboard.test.ts @@ -318,6 +318,9 @@ test("renders at most three left-aligned cropped thumbnails", async () => { expect(prompt.setup.renderer.root.findDescendantById("prompt-image-preview-3")).toBeUndefined() const frame = await prompt.setup.waitForFrame((frame) => frame.includes("+1 more")) expect(frame).toMatch(/^┃ █/m) + expect(frame.match(/\[Image 1\]/g)).toHaveLength(1) + expect(frame.match(/\[Image 2\]/g)).toHaveLength(1) + expect(frame.match(/\[Image 3\]/g)).toHaveLength(1) await prompt.setup.mockMouse.click(49, 1, MouseButtons.LEFT) await prompt.setup.waitForFrame((frame) => frame.includes("Image 4 of 4")) @@ -331,13 +334,20 @@ test("renders at most three left-aligned cropped thumbnails", async () => { } }) -test("opens thumbnails by their exact mouse bounds and from the command palette", async () => { +test("opens image attachments by keyboard, mouse, and command palette", async () => { const prompt = await mountPrompt(readPngClipboard, true) try { await pasteImages(prompt, 2) const thumbnail = prompt.setup.renderer.root.findDescendantById("prompt-image-preview-1") if (!(thumbnail instanceof ImageRenderable)) throw new Error("Second image thumbnail did not render") + + prompt.setup.mockInput.pressKey("x", { ctrl: true }) + prompt.setup.mockInput.pressKey("i") + await prompt.setup.waitForFrame((frame) => frame.includes("Image 1 of 2")) + prompt.setup.mockInput.pressCtrlC() + await prompt.setup.waitForFrame((frame) => !frame.includes("Image 1 of 2")) + await prompt.setup.mockMouse.click(14, 1, MouseButtons.LEFT) await prompt.setup.waitForFrame((frame) => frame.includes("Image 1 of 2")) @@ -369,7 +379,8 @@ test("opens thumbnails by their exact mouse bounds and from the command palette" prompt.setup.mockInput.pressKey("p", { ctrl: true }) await prompt.setup.waitForFrame((frame) => frame.includes("Commands")) for (const key of "view image attachments") prompt.setup.mockInput.pressKey(key) - await prompt.setup.waitForFrame((frame) => frame.includes("View image attachments")) + const palette = await prompt.setup.waitForFrame((frame) => frame.includes("View image attachments")) + expect(palette).toContain("ctrl+x i") prompt.setup.mockInput.pressEnter() await prompt.setup.waitForFrame((frame) => frame.includes("Image 1 of 2")) } finally {