From 9481167b4b0dc6e4c5b5b37e238c90d999b99042 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 5 Jul 2026 08:15:04 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/445 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..de5c5e27 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-07-05T08:15:04.140Z for PR creation at branch issue-445-9a1f5e5f81b6 for issue https://github.com/ProverCoderAI/docker-git/issues/445 \ No newline at end of file From 52d0d105efb676e386456917bfc7225d8a5a04af Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 5 Jul 2026 08:28:53 +0000 Subject: [PATCH 2/3] fix(terminal): skip unavailable inline image previews and dedupe repeated renders Failed inline image fetches rendered an "unavailable" placeholder box plus four spacer rows, and every re-mention or redraw of the same image path rendered another preview, flooding the web terminal with duplicate boxes. Fetch failures now resolve to null and the preview loop skips them entirely (the clickable path link is unaffected, and failures are not cached so a path printed again after the file appears can still preview). Successfully rendered paths are tracked in a session-level set so each image renders at most once, and the leading line break before previews is written lazily only before the first preview that actually renders, so fully skipped segments leave the output untouched. Refs #445 --- ...erminal-inline-image-unavailable-dedupe.md | 14 ++ .../src/web/terminal-inline-images-core.ts | 99 +++++++++- .../src/web/terminal-inline-images.ts | 68 ++----- .../src/web/terminal-panel-cleanup-runtime.ts | 1 + .../terminal-panel-inline-images-runtime.ts | 79 +++----- .../src/web/terminal-panel-runtime-core.ts | 1 + .../src/web/terminal-panel-runtime-types.ts | 1 + .../web/terminal-inline-images-core.test.ts | 181 ++++++++++++++++-- 8 files changed, 307 insertions(+), 137 deletions(-) create mode 100644 .changeset/terminal-inline-image-unavailable-dedupe.md diff --git a/.changeset/terminal-inline-image-unavailable-dedupe.md b/.changeset/terminal-inline-image-unavailable-dedupe.md new file mode 100644 index 00000000..bef687f3 --- /dev/null +++ b/.changeset/terminal-inline-image-unavailable-dedupe.md @@ -0,0 +1,14 @@ +--- +"@prover-coder-ai/docker-git-terminal": patch +--- + +Stop rendering "unavailable" placeholder boxes for inline image paths that +cannot be fetched, and render each image preview at most once per terminal +session. + +Failed image fetches previously produced an "unavailable" placeholder plus +four spacer rows, and every re-mention or redraw of the same path rendered +another preview. Unavailable paths are now skipped entirely (the clickable +path link remains), rendered paths are tracked per session so duplicates are +suppressed, and the leading line break before previews is written lazily so +fully skipped segments leave the terminal output untouched. diff --git a/packages/terminal/src/web/terminal-inline-images-core.ts b/packages/terminal/src/web/terminal-inline-images-core.ts index 39b76f05..56cf9139 100644 --- a/packages/terminal/src/web/terminal-inline-images-core.ts +++ b/packages/terminal/src/web/terminal-inline-images-core.ts @@ -1,4 +1,5 @@ import { detectTerminalImagePaths } from "./terminal-image-paths.js" +import type { TerminalInlineImageEntry } from "./terminal-inline-images.js" export type TerminalInlineImageOutputSegment = { readonly endedWithLineBreak: boolean @@ -9,8 +10,7 @@ export type TerminalInlineImageOutputSegment = { export type TerminalInlineImagePreviewsEnabledRef = { readonly current: boolean } export type TerminalOutputSegmentWriter = { - readonly writePreviewLineBreak: (segment: TerminalInlineImageOutputSegment, onComplete: () => void) => void - readonly writePreviews: (paths: ReadonlyArray, onComplete: () => void) => void + readonly writePreviews: (segment: TerminalInlineImageOutputSegment, onComplete: () => void) => void readonly writeText: (text: string, onComplete: () => void) => void } @@ -61,12 +61,11 @@ export const splitTerminalInlineImageOutput = ( * effects belong to the writer implementation. * * @pure false - invokes effectful writer callbacks. - * @effect writer callbacks: writeText, writePreviewLineBreak, writePreviews. + * @effect writer callbacks: writeText, writePreviews. * @precondition segment is the next queued terminal output segment and * onComplete belongs to the caller's active output queue drain. * @postcondition writeText is requested exactly once; when previews are enabled - * and imagePaths is non-empty, the preview line break and preview writes are - * requested in order before onComplete. + * and imagePaths is non-empty, the preview write is requested before onComplete. * @invariant segment.text is emitted before any preview callback, and preview * callbacks never run when imagePaths is empty or previews are disabled. * @complexity O(1) plus writer callback complexity; image paths are forwarded @@ -83,8 +82,92 @@ export const writeTerminalOutputSegment = ( onComplete() return } - writer.writePreviewLineBreak(segment, () => { - writer.writePreviews(segment.imagePaths, onComplete) - }) + writer.writePreviews(segment, onComplete) }) } + +export type TerminalInlineImagePreviewWriter = { + readonly loadEntry: (path: string, onComplete: (entry: TerminalInlineImageEntry | null) => void) => void + readonly renderPreview: (entry: TerminalInlineImageEntry, onComplete: () => void) => void + readonly writeLineBreak: (onComplete: () => void) => void +} + +export type TerminalInlineImagePreviewWriteArgs = { + readonly needsLeadingLineBreak: boolean + readonly paths: ReadonlyArray + readonly renderedPaths: Set + readonly writer: TerminalInlineImagePreviewWriter +} + +/** + * Sequences inline image preview writes for the image paths of one segment. + * + * Skips paths whose preview was already rendered in this terminal session and + * paths whose entry loads as `null` (unavailable image), so unavailable links + * never produce a placeholder and the same image is rendered at most once. + * The leading line break is written lazily before the first rendered preview + * only, so segments whose previews are all skipped leave the output untouched. + * + * @param args - Preview paths, session-rendered path set, and effectful writer callbacks. + * @param onComplete - Invoked exactly once after every path is processed. + * @returns Nothing; results are delivered through the writer callbacks. + * @pure false - invokes effectful writer callbacks and mutates renderedPaths. + * @effect writer callbacks: loadEntry, renderPreview, writeLineBreak. + * @precondition paths contains no duplicates (segment paths are deduplicated at detection). + * @postcondition ∀ path ∈ paths: rendered(path) → path ∈ renderedPaths, and + * renderPreview runs only for freshly loaded, previously unrendered entries. + * @invariant renderPreview is never invoked for a null entry or a path already + * in renderedPaths; writeLineBreak runs at most once and only before the first render. + * @complexity O(n) where n = |paths|. + * @throws Through writer callbacks or onComplete only. + */ +// CHANGE: skip unavailable inline images and deduplicate previews per terminal session +// WHY: rendering placeholders for unresolvable paths and repeating previews for the +// same image adds noise to terminal output without conveying information +// QUOTE(ТЗ): "не пытаться рендерить unavailable ссылки + сделать что бы одна и таже +// картинка не рендерилась несколько раз" +// REF: issue-445 +// SOURCE: n/a +// FORMAT THEOREM: ∀ path: renders(path) ≤ 1 ∧ (entry(path) = null → renders(path) = 0) +// PURITY: CORE sequencing over injected SHELL callbacks +// EFFECT: mutates renderedPaths and drives writer callbacks +// INVARIANT: onComplete fires exactly once after all paths are processed in input order +// COMPLEXITY: O(n) where n = |paths| +export const writeTerminalInlineImagePreviews = ( + { needsLeadingLineBreak, paths, renderedPaths, writer }: TerminalInlineImagePreviewWriteArgs, + onComplete: () => void +): void => { + let lineBreakPending = needsLeadingLineBreak + let index = 0 + const renderNext = (entry: TerminalInlineImageEntry, onRendered: () => void): void => { + if (!lineBreakPending) { + writer.renderPreview(entry, onRendered) + return + } + lineBreakPending = false + writer.writeLineBreak(() => { + writer.renderPreview(entry, onRendered) + }) + } + const writeNext = (): void => { + const path = paths[index] + if (path === undefined) { + onComplete() + return + } + index += 1 + if (renderedPaths.has(path)) { + writeNext() + return + } + writer.loadEntry(path, (entry) => { + if (entry === null) { + writeNext() + return + } + renderedPaths.add(path) + renderNext(entry, writeNext) + }) + } + writeNext() +} diff --git a/packages/terminal/src/web/terminal-inline-images.ts b/packages/terminal/src/web/terminal-inline-images.ts index 48c5d245..777e3a34 100644 --- a/packages/terminal/src/web/terminal-inline-images.ts +++ b/packages/terminal/src/web/terminal-inline-images.ts @@ -14,18 +14,12 @@ const terminalInlineImagePreviewColumns = 16 const terminalInlineImagePreviewHeightPx = 56 const terminalInlineImagePreviewWidthPx = 96 -export type TerminalInlineImageEntry = - | { - readonly _tag: "AvailableTerminalInlineImage" - readonly displayUrl: string - readonly fetchUrl: string - readonly path: string - } - | { - readonly _tag: "UnavailableTerminalInlineImage" - readonly fetchUrl: string - readonly path: string - } +export type TerminalInlineImageEntry = { + readonly _tag: "AvailableTerminalInlineImage" + readonly displayUrl: string + readonly fetchUrl: string + readonly path: string +} type TerminalInlineImageObjectUrlCache = Map @@ -40,15 +34,6 @@ const availableTerminalInlineImageEntry = ( path }) -export const unavailableTerminalInlineImageEntry = ( - path: string, - fetchUrl: string -): TerminalInlineImageEntry => ({ - _tag: "UnavailableTerminalInlineImage", - fetchUrl, - path -}) - export const cachedTerminalInlineImageEntry = ( cache: TerminalInlineImageObjectUrlCache, path: string, @@ -101,18 +86,12 @@ export const revokeTerminalInlineImageObjectUrlCache = ( cache.clear() } -const terminalInlineImageLinkUrl = (entry: TerminalInlineImageEntry): string => - entry._tag === "AvailableTerminalInlineImage" ? entry.displayUrl : entry.fetchUrl - -const terminalInlineImageTitle = (entry: TerminalInlineImageEntry): string => - entry._tag === "AvailableTerminalInlineImage" ? entry.path : `${entry.path} unavailable` - const createTerminalInlineImageLink = (entry: TerminalInlineImageEntry): HTMLAnchorElement => { const link = document.createElement("a") - link.href = terminalInlineImageLinkUrl(entry) + link.href = entry.displayUrl link.rel = "noreferrer" link.target = "_blank" - link.title = terminalInlineImageTitle(entry) + link.title = entry.path link.style.alignItems = "center" link.style.background = "#0d1218" link.style.border = "1px solid #3a4652" @@ -129,9 +108,9 @@ const createTerminalInlineImageLink = (entry: TerminalInlineImageEntry): HTMLAnc return link } -const appendAvailableTerminalInlineImage = ( +const appendTerminalInlineImageContent = ( link: HTMLAnchorElement, - entry: Extract + entry: TerminalInlineImageEntry ): void => { const image = document.createElement("img") image.alt = entry.path @@ -144,30 +123,6 @@ const appendAvailableTerminalInlineImage = ( link.append(image) } -const appendUnavailableTerminalInlineImage = (link: HTMLAnchorElement): void => { - const label = document.createElement("span") - label.textContent = "unavailable" - label.style.color = "#9aa8b6" - label.style.fontFamily = "'IBM Plex Mono', ui-monospace, monospace" - label.style.fontSize = "11px" - label.style.lineHeight = "1" - label.style.overflow = "hidden" - label.style.textOverflow = "ellipsis" - label.style.whiteSpace = "nowrap" - link.append(label) -} - -const appendTerminalInlineImageContent = ( - link: HTMLAnchorElement, - entry: TerminalInlineImageEntry -): void => { - if (entry._tag === "AvailableTerminalInlineImage") { - appendAvailableTerminalInlineImage(link, entry) - return - } - appendUnavailableTerminalInlineImage(link) -} - const openImage = (fetchUrl: string): void => { const imageWindow = window.open(fetchUrl, "_blank", "noopener,noreferrer") if (imageWindow === null) { @@ -191,7 +146,7 @@ const renderInlineImageElement = ( element: HTMLElement, entry: TerminalInlineImageEntry ): void => { - if (element.dataset["path"] === entry.path && element.dataset["tag"] === entry._tag) { + if (element.dataset["path"] === entry.path) { return } @@ -199,7 +154,6 @@ const renderInlineImageElement = ( appendTerminalInlineImageContent(link, entry) element.dataset["path"] = entry.path - element.dataset["tag"] = entry._tag element.style.pointerEvents = "none" element.replaceChildren(link) } diff --git a/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts b/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts index 8677d49e..1c4c9e9b 100644 --- a/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts +++ b/packages/terminal/src/web/terminal-panel-cleanup-runtime.ts @@ -32,6 +32,7 @@ export const cleanupTerminalResources = ( } args.lifecycle.inlineImageDisposables = [] revokeTerminalInlineImageObjectUrlCache(args.lifecycle.inlineImageObjectUrls) + args.lifecycle.inlineImageRenderedPaths.clear() args.lifecycle.outputQueue = [] args.lifecycle.outputWriting = false args.removeImageLinks() diff --git a/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts b/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts index 7a464bb7..a8a3b6ae 100644 --- a/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts +++ b/packages/terminal/src/web/terminal-panel-inline-images-runtime.ts @@ -6,14 +6,14 @@ import { resolveTerminalImageFetchUrl } from "./terminal-image-url.js" import { splitTerminalInlineImageOutput, type TerminalInlineImageOutputSegment, + writeTerminalInlineImagePreviews, writeTerminalOutputSegment } from "./terminal-inline-images-core.js" import { cachedTerminalInlineImageEntry, cacheTerminalInlineImageBlob, didAppendTerminalInlineImagePreview, - terminalInlineImageSpacer, - unavailableTerminalInlineImageEntry + terminalInlineImageSpacer } from "./terminal-inline-images.js" import type { TerminalInlineImageEntry } from "./terminal-inline-images.js" import type { TerminalMessageHandlers } from "./terminal-panel-runtime-types.js" @@ -36,14 +36,6 @@ const emptyTerminalInlineImageBufferState: TerminalInlineImageBufferState = { size: 0 } -const terminalImageEntry = ( - handlers: TerminalMessageHandlers, - path: string -): TerminalInlineImageEntry | null => { - const fetchUrl = resolveTerminalImageFetchUrl(handlers.session.websocketPath, path) - return cachedTerminalInlineImageEntry(handlers.lifecycle.inlineImageObjectUrls, path, fetchUrl) -} - const terminalInlineImageFetchError = (message: string): TerminalInlineImageFetchError => ({ _tag: "TerminalInlineImageFetchError", message @@ -136,7 +128,7 @@ const fetchTerminalInlineImageBlob = ( const loadTerminalImageEntry = ( handlers: TerminalMessageHandlers, path: string, - onComplete: (entry: TerminalInlineImageEntry) => void + onComplete: (entry: TerminalInlineImageEntry | null) => void ): void => { const fetchUrl = resolveTerminalImageFetchUrl(handlers.session.websocketPath, path) const cached = cachedTerminalInlineImageEntry(handlers.lifecycle.inlineImageObjectUrls, path, fetchUrl) @@ -147,7 +139,7 @@ const loadTerminalImageEntry = ( Effect.runFork( fetchTerminalInlineImageBlob(fetchUrl).pipe( Effect.match({ - onFailure: () => unavailableTerminalInlineImageEntry(path, fetchUrl), + onFailure: () => null, onSuccess: (blob) => handlers.lifecycle.disposed ? null @@ -155,7 +147,7 @@ const loadTerminalImageEntry = ( }), Effect.flatMap((entry) => Effect.sync(() => { - if (entry === null || handlers.lifecycle.disposed) { + if (handlers.lifecycle.disposed) { return } onComplete(entry) @@ -172,21 +164,6 @@ const writePreviewSpacer = ( handlers.terminal.write(terminalInlineImageSpacer, onComplete) } -const writeInlineImagePreview = ( - handlers: TerminalMessageHandlers, - path: string, - onComplete: () => void -): void => { - const cached = terminalImageEntry(handlers, path) - if (cached !== null) { - writeInlineImagePreviewEntry(handlers, cached, onComplete) - return - } - loadTerminalImageEntry(handlers, path, (entry) => { - writeInlineImagePreviewEntry(handlers, entry, onComplete) - }) -} - const writeInlineImagePreviewEntry = ( handlers: TerminalMessageHandlers, entry: TerminalInlineImageEntry, @@ -205,33 +182,26 @@ const writeInlineImagePreviewEntry = ( } const writeInlineImagePreviews = ( - handlers: TerminalMessageHandlers, - paths: ReadonlyArray, - onComplete: () => void -): void => { - let index = 0 - const writeNext = (): void => { - const path = paths[index] - if (path === undefined) { - onComplete() - return - } - index += 1 - writeInlineImagePreview(handlers, path, writeNext) - } - writeNext() -} - -const writeLineBreakBeforePreview = ( handlers: TerminalMessageHandlers, segment: TerminalInlineImageOutputSegment, onComplete: () => void ): void => { - if (segment.endedWithLineBreak) { - onComplete() - return - } - handlers.terminal.write("\r\n", onComplete) + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: !segment.endedWithLineBreak, + paths: segment.imagePaths, + renderedPaths: handlers.lifecycle.inlineImageRenderedPaths, + writer: { + loadEntry: (path, onEntry) => { + loadTerminalImageEntry(handlers, path, onEntry) + }, + renderPreview: (entry, onRendered) => { + writeInlineImagePreviewEntry(handlers, entry, onRendered) + }, + writeLineBreak: (onWritten) => { + handlers.terminal.write("\r\n", onWritten) + } + } + }, onComplete) } const flushTerminalOutputQueue = (handlers: TerminalMessageHandlers): void => { @@ -248,11 +218,8 @@ const flushTerminalOutputQueue = (handlers: TerminalMessageHandlers): void => { inlineImagePreviewsEnabledRef: handlers.inlineImagePreviewsEnabledRef, segment, writer: { - writePreviewLineBreak: (outputSegment, onComplete) => { - writeLineBreakBeforePreview(handlers, outputSegment, onComplete) - }, - writePreviews: (paths, onComplete) => { - writeInlineImagePreviews(handlers, paths, onComplete) + writePreviews: (outputSegment, onComplete) => { + writeInlineImagePreviews(handlers, outputSegment, onComplete) }, writeText: (text, onComplete) => { handlers.terminal.write(text, onComplete) diff --git a/packages/terminal/src/web/terminal-panel-runtime-core.ts b/packages/terminal/src/web/terminal-panel-runtime-core.ts index ab5a6685..0c35258c 100644 --- a/packages/terminal/src/web/terminal-panel-runtime-core.ts +++ b/packages/terminal/src/web/terminal-panel-runtime-core.ts @@ -31,6 +31,7 @@ export const createLifecycleState = (): TerminalLifecycleState => ({ disposed: false, inlineImageDisposables: [], inlineImageObjectUrls: new Map(), + inlineImageRenderedPaths: new Set(), outputQueue: [], outputWriting: false, readyNotified: false, diff --git a/packages/terminal/src/web/terminal-panel-runtime-types.ts b/packages/terminal/src/web/terminal-panel-runtime-types.ts index b36ffd58..1484ba4d 100644 --- a/packages/terminal/src/web/terminal-panel-runtime-types.ts +++ b/packages/terminal/src/web/terminal-panel-runtime-types.ts @@ -28,6 +28,7 @@ export type TerminalLifecycleState = { disposed: boolean inlineImageDisposables: Array inlineImageObjectUrls: Map + inlineImageRenderedPaths: Set outputQueue: Array outputWriting: boolean readyNotified: boolean diff --git a/packages/terminal/tests/web/terminal-inline-images-core.test.ts b/packages/terminal/tests/web/terminal-inline-images-core.test.ts index 8260b197..dea8fde4 100644 --- a/packages/terminal/tests/web/terminal-inline-images-core.test.ts +++ b/packages/terminal/tests/web/terminal-inline-images-core.test.ts @@ -3,15 +3,17 @@ import { vi } from "vitest" import { splitTerminalInlineImageOutput, + type TerminalInlineImageOutputSegment, + writeTerminalInlineImagePreviews, writeTerminalOutputSegment } from "../../src/web/terminal-inline-images-core.js" +import type { TerminalInlineImageEntry } from "../../src/web/terminal-inline-images.js" import { cachedTerminalInlineImageEntry, cacheTerminalInlineImageBlob, revokeTerminalInlineImageObjectUrlCache, terminalInlineImagePreviewRows, - terminalInlineImageSpacer, - unavailableTerminalInlineImageEntry + terminalInlineImageSpacer } from "../../src/web/terminal-inline-images.js" const issue250ImagePath = `/${["t", "mp"].join("")}/phantom-e2e.tuhl98/wallet-step-after-password.png` @@ -71,8 +73,7 @@ describe("terminal inline image output", () => { it("writes detected image paths as plain terminal text when automatic previews are disabled", () => { const textWrites: Array = [] - const previewWrites: Array> = [] - const previewLineBreaks = { count: 0 } + const previewWrites: Array = [] const completions = { count: 0 } writeTerminalOutputSegment({ @@ -83,12 +84,8 @@ describe("terminal inline image output", () => { text: issue250DeleteCommand }, writer: { - writePreviewLineBreak: (_segment, onComplete) => { - previewLineBreaks.count += 1 - onComplete() - }, - writePreviews: (paths, onComplete) => { - previewWrites.push(paths) + writePreviews: (segment, onComplete) => { + previewWrites.push(segment) onComplete() }, writeText: (text, onComplete) => { @@ -101,11 +98,42 @@ describe("terminal inline image output", () => { }) expect(textWrites).toEqual([issue250DeleteCommand]) - expect(previewLineBreaks.count).toBe(0) expect(previewWrites).toEqual([]) expect(completions.count).toBe(1) }) + it("forwards the segment to the preview writer after text when previews are enabled", () => { + const writes: Array = [] + const segment: TerminalInlineImageOutputSegment = { + endedWithLineBreak: true, + imagePaths: [issue250ImagePath], + text: `${issue250ImagePath}\r\n` + } + + writeTerminalOutputSegment({ + inlineImagePreviewsEnabledRef: { current: true }, + segment, + writer: { + writePreviews: (previewSegment, onComplete) => { + writes.push(`previews:${previewSegment.imagePaths.join(",")}`) + onComplete() + }, + writeText: (text, onComplete) => { + writes.push(`text:${text}`) + onComplete() + } + } + }, () => { + writes.push("complete") + }) + + expect(writes).toEqual([ + `text:${issue250ImagePath}\r\n`, + `previews:${issue250ImagePath}`, + "complete" + ]) + }) + it("caches successful image fetch blobs as reusable object urls", () => { const createObjectUrl = vi.spyOn(URL, "createObjectURL").mockReturnValue("blob:terminal-image") const revokeObjectUrl = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => void 0) @@ -130,11 +158,132 @@ describe("terminal inline image output", () => { expect(cache.size).toBe(0) }) - it("represents failed image fetches without using a broken image url", () => { - expect(unavailableTerminalInlineImageEntry("/var/data/missing.png", "https://api/image")).toEqual({ - _tag: "UnavailableTerminalInlineImage", - fetchUrl: "https://api/image", - path: "/var/data/missing.png" +}) + +const availableEntry = (path: string): TerminalInlineImageEntry => ({ + _tag: "AvailableTerminalInlineImage", + displayUrl: `blob:${path}`, + fetchUrl: `https://api${path}`, + path +}) + +type PreviewWriterLog = { + readonly events: Array + readonly loadedEntries: Map +} + +const previewWriter = ({ events, loadedEntries }: PreviewWriterLog) => ({ + loadEntry: (path: string, onComplete: (entry: TerminalInlineImageEntry | null) => void) => { + events.push(`load:${path}`) + onComplete(loadedEntries.get(path) ?? null) + }, + renderPreview: (entry: TerminalInlineImageEntry, onComplete: () => void) => { + events.push(`render:${entry.path}`) + onComplete() + }, + writeLineBreak: (onComplete: () => void) => { + events.push("line-break") + onComplete() + } +}) + +describe("terminal inline image previews (issue 445)", () => { + const missingPath = "/var/data/missing.png" + const presentPath = "/var/data/present.png" + + it("does not render a preview for unavailable images", () => { + const events: Array = [] + const renderedPaths = new Set() + const completions = { count: 0 } + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: true, + paths: [missingPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries: new Map([[missingPath, null]]) }) + }, () => { + completions.count += 1 + }) + + expect(events).toEqual([`load:${missingPath}`]) + expect(renderedPaths.size).toBe(0) + expect(completions.count).toBe(1) + }) + + it("renders the same image path only once across segments", () => { + const events: Array = [] + const renderedPaths = new Set() + const loadedEntries = new Map([[presentPath, availableEntry(presentPath)]]) + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: false, + paths: [presentPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries }) + }, () => { + events.push("complete:first") + }) + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: false, + paths: [presentPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries }) + }, () => { + events.push("complete:second") + }) + + expect(events).toEqual([ + `load:${presentPath}`, + `render:${presentPath}`, + "complete:first", + "complete:second" + ]) + expect(renderedPaths).toEqual(new Set([presentPath])) + }) + + it("writes the leading line break lazily and only before the first rendered preview", () => { + const events: Array = [] + const renderedPaths = new Set() + const otherPath = "/var/data/other.png" + const loadedEntries = new Map([ + [missingPath, null], + [otherPath, availableEntry(otherPath)], + [presentPath, availableEntry(presentPath)] + ]) + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: true, + paths: [missingPath, presentPath, otherPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries }) + }, () => { + events.push("complete") + }) + + expect(events).toEqual([ + `load:${missingPath}`, + `load:${presentPath}`, + "line-break", + `render:${presentPath}`, + `load:${otherPath}`, + `render:${otherPath}`, + "complete" + ]) + }) + + it("leaves output untouched when every preview is skipped", () => { + const events: Array = [] + const renderedPaths = new Set([presentPath]) + + writeTerminalInlineImagePreviews({ + needsLeadingLineBreak: true, + paths: [presentPath], + renderedPaths, + writer: previewWriter({ events, loadedEntries: new Map() }) + }, () => { + events.push("complete") }) + + expect(events).toEqual(["complete"]) }) }) From d18977baac7de9fc132041514d127a68c8f7bad4 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 5 Jul 2026 08:30:31 +0000 Subject: [PATCH 3/3] chore: remove auto-generated .gitkeep scaffold file --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index de5c5e27..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-07-05T08:15:04.140Z for PR creation at branch issue-445-9a1f5e5f81b6 for issue https://github.com/ProverCoderAI/docker-git/issues/445 \ No newline at end of file