tui: bind leader+i to image viewer

Default <leader>i opens the prompt image attachment
viewer from the keyboard. Drop thumbnail captions so
previews stay compact and fill their allotted height.
This commit is contained in:
Simon Klee
2026-07-30 18:21:17 +02:00
parent 5bba185a77
commit 6b7bbc711c
4 changed files with 44 additions and 17 deletions
+21 -15
View File
@@ -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) {
<Show when={config.prompt?.image_preview && imagePreviewsVisible()}>
<box
width="100%"
height={imagePreviewHeight() + 2}
height={imagePreviewHeight() + 1}
flexDirection="row"
flexShrink={0}
justifyContent="flex-start"
@@ -1523,23 +1524,28 @@ export function Prompt(props: PromptProps) {
return (
<box
width={imagePreviewWidth()}
height="100%"
height={imagePreviewHeight()}
flexBasis={imagePreviewWidth()}
flexShrink={1}
flexDirection="column"
>
<image
id={`prompt-image-preview-${index()}`}
source={file.uri}
fit="cover"
protocol="auto"
width="100%"
height={imagePreviewHeight()}
onError={() => setFailed(true)}
/>
<text fg={theme.text.subdued} wrapMode="none" truncate>
{failed() ? "No preview" : (file.mention?.text ?? `Image ${index() + 1}`)}
</text>
<Show
when={!failed()}
fallback={
<box width="100%" height="100%" alignItems="center" justifyContent="center">
<text fg={theme.text.subdued}>No preview</text>
</box>
}
>
<image
id={`prompt-image-preview-${index()}`}
source={file.uri}
fit="cover"
protocol="auto"
width="100%"
height="100%"
onError={() => setFailed(true)}
/>
</Show>
</box>
)
}}
+2
View File
@@ -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("<leader>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",
+8
View File
@@ -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: "<leader>i" }])
expect(overridden.keybinds.get("prompt.images.view")).toMatchObject([{ key: "ctrl+shift+i" }])
})
test("resolves message navigation defaults", () => {
const config = resolve({}, { terminalSuspend: true })
+13 -2
View File
@@ -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 {