11import { detectTerminalImagePaths } from "./terminal-image-paths.js"
2+ import type { TerminalInlineImageEntry } from "./terminal-inline-images.js"
23
34export type TerminalInlineImageOutputSegment = {
45 readonly endedWithLineBreak : boolean
@@ -9,8 +10,7 @@ export type TerminalInlineImageOutputSegment = {
910export type TerminalInlineImagePreviewsEnabledRef = { readonly current : boolean }
1011
1112export type TerminalOutputSegmentWriter = {
12- readonly writePreviewLineBreak : ( segment : TerminalInlineImageOutputSegment , onComplete : ( ) => void ) => void
13- readonly writePreviews : ( paths : ReadonlyArray < string > , onComplete : ( ) => void ) => void
13+ readonly writePreviews : ( segment : TerminalInlineImageOutputSegment , onComplete : ( ) => void ) => void
1414 readonly writeText : ( text : string , onComplete : ( ) => void ) => void
1515}
1616
@@ -61,12 +61,11 @@ export const splitTerminalInlineImageOutput = (
6161 * effects belong to the writer implementation.
6262 *
6363 * @pure false - invokes effectful writer callbacks.
64- * @effect writer callbacks: writeText, writePreviewLineBreak, writePreviews.
64+ * @effect writer callbacks: writeText, writePreviews.
6565 * @precondition segment is the next queued terminal output segment and
6666 * onComplete belongs to the caller's active output queue drain.
6767 * @postcondition writeText is requested exactly once; when previews are enabled
68- * and imagePaths is non-empty, the preview line break and preview writes are
69- * requested in order before onComplete.
68+ * and imagePaths is non-empty, the preview write is requested before onComplete.
7069 * @invariant segment.text is emitted before any preview callback, and preview
7170 * callbacks never run when imagePaths is empty or previews are disabled.
7271 * @complexity O(1) plus writer callback complexity; image paths are forwarded
@@ -83,8 +82,92 @@ export const writeTerminalOutputSegment = (
8382 onComplete ( )
8483 return
8584 }
86- writer . writePreviewLineBreak ( segment , ( ) => {
87- writer . writePreviews ( segment . imagePaths , onComplete )
88- } )
85+ writer . writePreviews ( segment , onComplete )
8986 } )
9087}
88+
89+ export type TerminalInlineImagePreviewWriter = {
90+ readonly loadEntry : ( path : string , onComplete : ( entry : TerminalInlineImageEntry | null ) => void ) => void
91+ readonly renderPreview : ( entry : TerminalInlineImageEntry , onComplete : ( ) => void ) => void
92+ readonly writeLineBreak : ( onComplete : ( ) => void ) => void
93+ }
94+
95+ export type TerminalInlineImagePreviewWriteArgs = {
96+ readonly needsLeadingLineBreak : boolean
97+ readonly paths : ReadonlyArray < string >
98+ readonly renderedPaths : Set < string >
99+ readonly writer : TerminalInlineImagePreviewWriter
100+ }
101+
102+ /**
103+ * Sequences inline image preview writes for the image paths of one segment.
104+ *
105+ * Skips paths whose preview was already rendered in this terminal session and
106+ * paths whose entry loads as `null` (unavailable image), so unavailable links
107+ * never produce a placeholder and the same image is rendered at most once.
108+ * The leading line break is written lazily before the first rendered preview
109+ * only, so segments whose previews are all skipped leave the output untouched.
110+ *
111+ * @param args - Preview paths, session-rendered path set, and effectful writer callbacks.
112+ * @param onComplete - Invoked exactly once after every path is processed.
113+ * @returns Nothing; results are delivered through the writer callbacks.
114+ * @pure false - invokes effectful writer callbacks and mutates renderedPaths.
115+ * @effect writer callbacks: loadEntry, renderPreview, writeLineBreak.
116+ * @precondition paths contains no duplicates (segment paths are deduplicated at detection).
117+ * @postcondition ∀ path ∈ paths: rendered(path) → path ∈ renderedPaths, and
118+ * renderPreview runs only for freshly loaded, previously unrendered entries.
119+ * @invariant renderPreview is never invoked for a null entry or a path already
120+ * in renderedPaths; writeLineBreak runs at most once and only before the first render.
121+ * @complexity O(n) where n = |paths|.
122+ * @throws Through writer callbacks or onComplete only.
123+ */
124+ // CHANGE: skip unavailable inline images and deduplicate previews per terminal session
125+ // WHY: rendering placeholders for unresolvable paths and repeating previews for the
126+ // same image adds noise to terminal output without conveying information
127+ // QUOTE(ТЗ): "не пытаться рендерить unavailable ссылки + сделать что бы одна и таже
128+ // картинка не рендерилась несколько раз"
129+ // REF: issue-445
130+ // SOURCE: n/a
131+ // FORMAT THEOREM: ∀ path: renders(path) ≤ 1 ∧ (entry(path) = null → renders(path) = 0)
132+ // PURITY: CORE sequencing over injected SHELL callbacks
133+ // EFFECT: mutates renderedPaths and drives writer callbacks
134+ // INVARIANT: onComplete fires exactly once after all paths are processed in input order
135+ // COMPLEXITY: O(n) where n = |paths|
136+ export const writeTerminalInlineImagePreviews = (
137+ { needsLeadingLineBreak, paths, renderedPaths, writer } : TerminalInlineImagePreviewWriteArgs ,
138+ onComplete : ( ) => void
139+ ) : void => {
140+ let lineBreakPending = needsLeadingLineBreak
141+ let index = 0
142+ const renderNext = ( entry : TerminalInlineImageEntry , onRendered : ( ) => void ) : void => {
143+ if ( ! lineBreakPending ) {
144+ writer . renderPreview ( entry , onRendered )
145+ return
146+ }
147+ lineBreakPending = false
148+ writer . writeLineBreak ( ( ) => {
149+ writer . renderPreview ( entry , onRendered )
150+ } )
151+ }
152+ const writeNext = ( ) : void => {
153+ const path = paths [ index ]
154+ if ( path === undefined ) {
155+ onComplete ( )
156+ return
157+ }
158+ index += 1
159+ if ( renderedPaths . has ( path ) ) {
160+ writeNext ( )
161+ return
162+ }
163+ writer . loadEntry ( path , ( entry ) => {
164+ if ( entry === null ) {
165+ writeNext ( )
166+ return
167+ }
168+ renderedPaths . add ( path )
169+ renderNext ( entry , writeNext )
170+ } )
171+ }
172+ writeNext ( )
173+ }
0 commit comments