@crosery/dsh-viewer 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Where a card's durable image bytes come from — pure, so it is testable
3
+ * without a DOM.
4
+ * @module @crosery/dsh-viewer/client/sources
5
+ */
6
+ import type { ModelImage } from '../contract.ts';
7
+ /**
8
+ * The chat's own durable-image loader, as the 0.1.7 tool-view owner supplies
9
+ * it. Its parameter is the harness's branded `ImageAttachmentRef`, which a
10
+ * {@link ModelImage} matches field for field; `never` keeps any train's exact
11
+ * loader type assignable here without importing the brand.
12
+ */
13
+ export type OwnerImageLoader = (attachment: never) => Promise<string>;
14
+ /** How one displayed file reaches its bytes and its Host-side opener. */
15
+ export interface ViewerSources {
16
+ /** Resolve one durable image to a URL. */
17
+ loadImage: (image: ModelImage) => Promise<string>;
18
+ /** Open the file through the Host, when the surrounding view offers that. */
19
+ openFile?: ((path: string) => unknown) | undefined;
20
+ }
21
+ /**
22
+ * The loader a card uses for durable images: the chat's own where the owner
23
+ * supplies one (0.1.7+), the plugin's attachment reader otherwise.
24
+ *
25
+ * The chat's loader is preferred because it shares one read and one browser URL
26
+ * per image across every view of the session and releases them with the
27
+ * session; the plugin's reader keeps its own URLs until the plugin unloads.
28
+ * @param owner - the owner's optional loader.
29
+ * @param fallback - the plugin's reader, by attachment id.
30
+ * @returns a loader over the card's image facts.
31
+ */
32
+ export declare function imageLoaderFor(owner: OwnerImageLoader | undefined, fallback: (attachmentId: string) => Promise<string>): ViewerSources['loadImage'];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The card's own stylesheet, injected for exactly this plugin's lifetime.
3
+ *
4
+ * Plain prefixed class names rather than CSS Modules: the repository's module
5
+ * pipeline is not published, so an out-of-tree package that wants a hashed class
6
+ * map has to reproduce it. Colors come from `--dsw-alias-*` semantic tokens, so
7
+ * the card follows the active palette with no theme branch of its own.
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ /**
11
+ * Mount the card stylesheet for the owning plugin lifetime.
12
+ * @param ctx - owning plugin context.
13
+ */
14
+ export declare function installViewerStyles(ctx: Context): void;
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Keeping displayed files on screen after their turn completes.
3
+ *
4
+ * From 0.1.6 the chat folds a completed turn's work — every tool row, this
5
+ * card included — behind one "used N s" disclosure, and from 0.1.7 that is the
6
+ * default (`transcriptView: standard`). A viewer whose purpose is putting a
7
+ * file in front of the user then hides it the moment the answer lands. The
8
+ * harness's own deliverables solve the same problem through two public
9
+ * surfaces, and this module uses the same two:
10
+ *
11
+ * - a Conversation Definition (`uiConversation.events.register`) that folds a
12
+ * turn's events into Turn-scoped data — here, the turn's successful
13
+ * `display_file` results; and
14
+ * - an entry in the chat's `conversation.chat.turnTail` list, which renders
15
+ * between a completed turn's closing reply and its action row, outside the
16
+ * fold. The component lives in `ViewerTail.tsx`.
17
+ *
18
+ * A `display_file` called from inside `run_code` never appears as a
19
+ * `tool/call`: the code-mode bridge logs it as `tool/ptc-dispatch-start` and
20
+ * `tool/ptc-dispatch`, and neither carries a turn number, so the turn-keyed
21
+ * Definition above cannot be routed one. Two more Definitions cover it, using
22
+ * the engine's own placement instead: each nested display starts a context
23
+ * that records the turn the engine located it in and links to the nested
24
+ * display before it, and each `turn/end` starts a context that walks that
25
+ * chain back through its own turn and publishes what it finds. The engine
26
+ * replays a start whenever the predecessor it read changes, which keeps the
27
+ * chain honest when older history loads. The same turn-end context records the
28
+ * turn's last human input, which the 0.1.7 chat's fold rule needs and which a
29
+ * `user/message` event cannot be routed to a turn to report either.
30
+ *
31
+ * Everything here is pure and structurally typed: the events come from a
32
+ * session log this build may not have written, and the Definition contract is
33
+ * declared by a package (`dsh-client-ui-conversation` 0.1.6+) that older
34
+ * trains do not publish in this shape.
35
+ * @module @crosery/dsh-viewer/client/turn-tail
36
+ */
37
+ import { type DisplayValue } from '../contract.ts';
38
+ /** Definition kind, and the key the turn's data is published under. */
39
+ export declare const VIEWER_TURN_DATA = "crosery-viewer";
40
+ /** Definition kind of one `display_file` dispatched from inside `run_code`. */
41
+ export declare const VIEWER_NESTED = "crosery-viewer-nested";
42
+ /** Definition kind of one human input message. */
43
+ export declare const VIEWER_INPUT = "crosery-viewer-input";
44
+ /** Definition kind, and Turn data key, of what a completed turn collects at its end. */
45
+ export declare const VIEWER_TURN_END = "crosery-viewer-turn-end";
46
+ /** The chat's own Turn data key for its fold boundaries (`dsh-client-ui-chat` 0.1.6+). */
47
+ export declare const CHAT_TURN_PROCESS = "turn-process";
48
+ /** One file a turn displayed. */
49
+ export interface TurnDisplay {
50
+ /** Log sequence of the settled result. */
51
+ seq: number;
52
+ /** The call that displayed it. */
53
+ callId: string;
54
+ /** What the card shows. */
55
+ value: DisplayValue;
56
+ }
57
+ /** Turn-scoped data this Definition publishes. */
58
+ export interface ViewerTurnData {
59
+ displayed: readonly TurnDisplay[];
60
+ }
61
+ /** The Definition's per-turn accumulator. */
62
+ export interface ViewerTurnState {
63
+ turn: number;
64
+ /** `display_file` calls of this turn, by call id. */
65
+ calls: ReadonlySet<string>;
66
+ displayed: readonly TurnDisplay[];
67
+ }
68
+ /** The slice of a session event this module reads. */
69
+ export interface EventLike {
70
+ type?: unknown;
71
+ seq?: unknown;
72
+ data?: unknown;
73
+ /** How a surface event entered the log; replacement copies are model-only. */
74
+ surfaceOp?: unknown;
75
+ }
76
+ /** A Definition match, as far as this module reads one. */
77
+ export interface MatchLike {
78
+ event: EventLike;
79
+ /** Where the engine placed the event: `{ kind: 'turn' | 'step', turn: { turn } }` inside a turn. */
80
+ location?: unknown;
81
+ }
82
+ /** The engine's backward lookup, handed to a Definition's `start`. */
83
+ export interface ReaderLike {
84
+ previous?(kind: string): {
85
+ state?: unknown;
86
+ } | undefined;
87
+ }
88
+ /** One nested display, linked to the nested display before it. */
89
+ export interface NestedDisplay {
90
+ /** The turn the engine located the dispatch in, when it resolved one. */
91
+ turn: number | undefined;
92
+ /** The display, when the dispatch succeeded with a displayable result. */
93
+ display: TurnDisplay | undefined;
94
+ previous: NestedDisplay | undefined;
95
+ }
96
+ /** One human input message, where the engine placed it. */
97
+ export interface HumanInput {
98
+ turn: number | undefined;
99
+ seq: number;
100
+ }
101
+ /** Turn-scoped data published when a turn ends. */
102
+ export interface ViewerTurnEndData {
103
+ /** `display_file` results dispatched from inside `run_code` during the turn, oldest first. */
104
+ nested: readonly TurnDisplay[];
105
+ /** Log sequence of the turn's last human input message, when it had one. */
106
+ lastInputSeq?: number | undefined;
107
+ }
108
+ /** The turn-end Definition's state. */
109
+ export interface ViewerTurnEndState extends ViewerTurnEndData {
110
+ turn: number;
111
+ }
112
+ /**
113
+ * The card payload a settled `display_file` result carries.
114
+ *
115
+ * The same recovery the tool row performs: the presentation metadata first,
116
+ * then the plugin's own envelope for a result written without it.
117
+ * @param data - the `tool/result` event's data.
118
+ * @returns the payload, or `undefined` when the result is not a displayable success.
119
+ */
120
+ export declare function displayedValueOf(data: unknown): DisplayValue | undefined;
121
+ /**
122
+ * The Conversation Definition that collects each turn's displayed files.
123
+ *
124
+ * Shaped after the harness's own deliverables Definition: `turn/start` opens a
125
+ * turn's context, `display_file` calls are remembered by id, and their
126
+ * successful results are appended in log order. Nothing is published as a view
127
+ * node — only Turn data, which the tail entry reads.
128
+ */
129
+ export declare const viewerTurnDefinition: {
130
+ kind: string;
131
+ match(event: EventLike): {
132
+ id: string;
133
+ role: "start" | "update";
134
+ } | null;
135
+ start(_context: unknown, match: MatchLike): ViewerTurnState;
136
+ update(context: {
137
+ state: ViewerTurnState;
138
+ }, match: MatchLike): ViewerTurnState;
139
+ buildLocationData(context: {
140
+ state?: ViewerTurnState | undefined;
141
+ }, scope: string, previous?: {
142
+ kind?: string;
143
+ turn?: number;
144
+ key?: string;
145
+ value?: ViewerTurnData;
146
+ } | null): {
147
+ kind: "turn";
148
+ turn: number;
149
+ key: string;
150
+ value: ViewerTurnData;
151
+ } | null;
152
+ };
153
+ /**
154
+ * Each `display_file` dispatched from inside `run_code`, chained in log order.
155
+ *
156
+ * Keyed by the sub-call id, one context per dispatch, publishing nothing on
157
+ * its own: the turn-end Definition reads the chain. The payload is recovered
158
+ * the way the nested tool row recovers it — a nested dispatch has no
159
+ * presentation metadata, so from the plugin's own envelope.
160
+ */
161
+ export declare const nestedDisplayDefinition: {
162
+ kind: string;
163
+ match(event: EventLike): {
164
+ id: string;
165
+ role: "start";
166
+ } | null;
167
+ start(_context: unknown, match: MatchLike, reader?: ReaderLike): NestedDisplay;
168
+ update(context: {
169
+ state: NestedDisplay;
170
+ }): NestedDisplay;
171
+ publication(): "none";
172
+ };
173
+ /**
174
+ * Each human input message: the prompt that opens a turn, and anything the
175
+ * user sends while it runs (a steer, or a message the turn picks up).
176
+ *
177
+ * The same messages the chat renders as `user` and `steering` rows — an
178
+ * appended `user/message` whose source is the user. Injected context is not
179
+ * one, and neither is the context row that wakes a turn: the chat counts that
180
+ * as input too, but it only ever opens a turn, which the fold rule ignores.
181
+ */
182
+ export declare const humanInputDefinition: {
183
+ kind: string;
184
+ match(event: EventLike): {
185
+ id: string;
186
+ role: "start";
187
+ } | null;
188
+ start(_context: unknown, match: MatchLike): HumanInput;
189
+ update(context: {
190
+ state: HumanInput;
191
+ }): HumanInput;
192
+ publication(): "none";
193
+ };
194
+ /**
195
+ * What a turn collects once it ends, from the Definitions that cannot be
196
+ * routed to it by turn number.
197
+ *
198
+ * Started by `turn/end`, so it reads each chain once, backwards from the end
199
+ * of the turn, and stops at the first entry the engine placed in another turn.
200
+ */
201
+ export declare const turnEndDefinition: {
202
+ kind: string;
203
+ match(event: EventLike): {
204
+ id: string;
205
+ role: "start";
206
+ } | null;
207
+ start(_context: unknown, match: MatchLike, reader?: ReaderLike): ViewerTurnEndState;
208
+ update(context: {
209
+ state: ViewerTurnEndState;
210
+ }): ViewerTurnEndState;
211
+ buildLocationData(context: {
212
+ state?: ViewerTurnEndState | undefined;
213
+ }, scope: string, previous?: {
214
+ kind?: string;
215
+ turn?: number;
216
+ key?: string;
217
+ value?: ViewerTurnEndData;
218
+ } | null): {
219
+ kind: "turn";
220
+ turn: number;
221
+ key: string;
222
+ value: ViewerTurnEndData;
223
+ } | null;
224
+ };
225
+ /** Every Definition the turn tail's data comes from, in registration order. */
226
+ export declare const viewerTurnDefinitions: readonly [{
227
+ kind: string;
228
+ match(event: EventLike): {
229
+ id: string;
230
+ role: "start" | "update";
231
+ } | null;
232
+ start(_context: unknown, match: MatchLike): ViewerTurnState;
233
+ update(context: {
234
+ state: ViewerTurnState;
235
+ }, match: MatchLike): ViewerTurnState;
236
+ buildLocationData(context: {
237
+ state?: ViewerTurnState | undefined;
238
+ }, scope: string, previous?: {
239
+ kind?: string;
240
+ turn?: number;
241
+ key?: string;
242
+ value?: ViewerTurnData;
243
+ } | null): {
244
+ kind: "turn";
245
+ turn: number;
246
+ key: string;
247
+ value: ViewerTurnData;
248
+ } | null;
249
+ }, {
250
+ kind: string;
251
+ match(event: EventLike): {
252
+ id: string;
253
+ role: "start";
254
+ } | null;
255
+ start(_context: unknown, match: MatchLike, reader?: ReaderLike): NestedDisplay;
256
+ update(context: {
257
+ state: NestedDisplay;
258
+ }): NestedDisplay;
259
+ publication(): "none";
260
+ }, {
261
+ kind: string;
262
+ match(event: EventLike): {
263
+ id: string;
264
+ role: "start";
265
+ } | null;
266
+ start(_context: unknown, match: MatchLike): HumanInput;
267
+ update(context: {
268
+ state: HumanInput;
269
+ }): HumanInput;
270
+ publication(): "none";
271
+ }, {
272
+ kind: string;
273
+ match(event: EventLike): {
274
+ id: string;
275
+ role: "start";
276
+ } | null;
277
+ start(_context: unknown, match: MatchLike, reader?: ReaderLike): ViewerTurnEndState;
278
+ update(context: {
279
+ state: ViewerTurnEndState;
280
+ }): ViewerTurnEndState;
281
+ buildLocationData(context: {
282
+ state?: ViewerTurnEndState | undefined;
283
+ }, scope: string, previous?: {
284
+ kind?: string;
285
+ turn?: number;
286
+ key?: string;
287
+ value?: ViewerTurnEndData;
288
+ } | null): {
289
+ kind: "turn";
290
+ turn: number;
291
+ key: string;
292
+ value: ViewerTurnEndData;
293
+ } | null;
294
+ }];
295
+ /** The slice of the tail owner's `turn` this module reads. */
296
+ export interface TurnLike {
297
+ status?: unknown;
298
+ start?: unknown;
299
+ end?: unknown;
300
+ data?: {
301
+ get?: (key: string) => unknown;
302
+ } | undefined;
303
+ }
304
+ /**
305
+ * Whether the user spoke again after the turn got to work: the 0.1.7 chat's
306
+ * `hasInterleavedInput`, reproduced from the data the tail can read.
307
+ *
308
+ * The chat anchors a turn's process at its first assistant or tool activity
309
+ * (`controlAnchorSeq`, published as its `turn-process` data). Input before
310
+ * that opened the turn; input after it was interleaved. With no activity at
311
+ * all the anchor is the turn's own start, and the chat counts every input as
312
+ * opening.
313
+ * @param turn - the tail owner's turn.
314
+ * @returns true when the turn holds human input after its process began.
315
+ */
316
+ export declare function hasInterleavedInput(turn: TurnLike): boolean;
317
+ /**
318
+ * Whether the chat keeps this turn's work open, in which case the tool rows
319
+ * are already on screen and a tail would only repeat them: a turn still
320
+ * running, one that ended aborted or in error, and — in the chat that has the
321
+ * rule (0.1.7) — one the user steered while it ran.
322
+ * @param turn - the tail owner's turn.
323
+ * @param steeredStaysOpen - whether the running chat keeps a steered turn open.
324
+ * @returns true when the turn's process is not foldable.
325
+ */
326
+ export declare function turnStaysOpen(turn: TurnLike, steeredStaysOpen?: boolean): boolean;
327
+ /**
328
+ * The files a completed turn's tail shows.
329
+ *
330
+ * Top-level and `run_code` displays alike; only results logged before the
331
+ * closing reply belong to it, and a file displayed several times appears once,
332
+ * where it was last displayed.
333
+ * @param turn - the tail owner's turn.
334
+ * @param closingSeq - the closing reply's log sequence.
335
+ * @param steeredStaysOpen - whether the running chat keeps a steered turn open.
336
+ * @returns the displays to render, oldest first; empty when there are none.
337
+ */
338
+ export declare function tailDisplays(turn: TurnLike, closingSeq: number, steeredStaysOpen?: boolean): readonly TurnDisplay[];
339
+ /** An observable boolean the tail reads with `useSyncExternalStore`. */
340
+ export interface FoldSource {
341
+ getSnapshot(): boolean;
342
+ subscribe(listener: () => void): () => void;
343
+ /**
344
+ * Whether the running chat keeps a turn the user steered open. Read on each
345
+ * render rather than observed: the answer is fixed by which chat is running.
346
+ */
347
+ keepsSteeredTurnsOpen?(): boolean;
348
+ }
349
+ /**
350
+ * Whether a transcript mode folds completed turns.
351
+ *
352
+ * `verbose` (0.1.7) never folds; every other mode does on the trains that have
353
+ * the turn tail, including the unset value, whose default is `standard` on
354
+ * 0.1.7 and `compact` on 0.1.6.
355
+ * @param mode - the persisted `transcriptView`, verbatim.
356
+ * @returns whether the tail should carry the turn's displays.
357
+ */
358
+ export declare function foldsCompletedTurns(mode: unknown): boolean;
359
+ /**
360
+ * Follow the chat's transcript preference, read from the chat's own settings
361
+ * form (`configForms.get('ui-chat')`).
362
+ *
363
+ * Looked up on every read rather than once: the form service arrives with the
364
+ * settings plugin, which need not have activated before this one. A train with
365
+ * no such form is treated as folding — its default.
366
+ *
367
+ * The form's existence also tells the two chats with a turn tail apart. Every
368
+ * 0.1.7 chat requires `configForms` and keeps a steered turn open; no 0.1.6
369
+ * chat has either, and folds a steered turn like any other.
370
+ * @param forms - reads the optional `configForms` service.
371
+ * @returns the fold preference as an observable.
372
+ */
373
+ export declare function foldSourceOf(forms: () => unknown): FoldSource;
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Viewer taxonomy, field names, and wire shapes shared by both halves.
3
+ *
4
+ * Deliberately free of `@deepseek-ai/schemastery` and of every `node:` import:
5
+ * the browser half imports this module, so a validator or a Node builtin
6
+ * reaching it would be inlined into (or break) the client bundle. The Host
7
+ * schema is built over this module in `settings.ts`.
8
+ */
9
+ /** Wire name of the display tool this plugin registers. */
10
+ export declare const DISPLAY_TOOL = "display_file";
11
+ /**
12
+ * Wire name of the shipped `dsh-tool-fs` image reader. This plugin does not own
13
+ * it — it only registers a card for it, so an image the model pulled in through
14
+ * the shipped tool is shown as a picture too instead of a bare text row.
15
+ */
16
+ export declare const READ_IMAGE_TOOL = "read_image";
17
+ /** Wire name of the shipped text read, whose binary-media calls get redirected. */
18
+ export declare const READ_TOOL = "read";
19
+ /** Settings namespace this plugin owns. */
20
+ export declare const VIEWER_SETTINGS_NAMESPACE = "crosery-viewer";
21
+ /**
22
+ * HTTP route serving one asset's bytes to the browser half.
23
+ *
24
+ * Scoped, because a duplicate exact route fails the WHOLE plugin tree at boot,
25
+ * not just the offender.
26
+ */
27
+ export declare const ASSET_ROUTE = "/crosery/dsh-viewer/asset";
28
+ /**
29
+ * How the browser half renders one file.
30
+ *
31
+ * The axis is "which element plays this", not "what is this file" — `svg` and
32
+ * `png` are both `image` because both go in an `<img>`, and everything with no
33
+ * player at all lands on `file`, which still gets a card with the file's facts
34
+ * and a link. There is no kind that renders nothing.
35
+ */
36
+ export type ViewerKind = 'image' | 'video' | 'audio' | 'pdf' | 'document' | 'html' | 'file';
37
+ /** One extension's classification. */
38
+ interface MediaSpec {
39
+ /** Which element plays it. */
40
+ readonly kind: ViewerKind;
41
+ /** Content type the asset route sends, and the card's declared type. */
42
+ readonly mediaType: string;
43
+ }
44
+ /** Media type of a raster the durable attachment service admits. */
45
+ export type ModelImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
46
+ /** {@link ModelImageMediaType} as a runtime set, for narrowing replayed JSON. */
47
+ export declare const MODEL_IMAGE_MEDIA_TYPES: readonly ModelImageMediaType[];
48
+ /**
49
+ * Lowercased extension (dot included) to its classification.
50
+ *
51
+ * Only formats a mainstream browser actually plays are listed: an entry here is
52
+ * a promise that the card will render something, so a codec the browser would
53
+ * refuse belongs on the `file` fallback instead of in this table.
54
+ */
55
+ export declare const MEDIA_TABLE: Readonly<Record<string, MediaSpec>>;
56
+ /** Classification used for an extension {@link MEDIA_TABLE} does not list. */
57
+ export declare const UNKNOWN_MEDIA: MediaSpec;
58
+ /**
59
+ * The extensions whose bytes the durable attachment service admits, mapped to
60
+ * the media type it verifies them as. A file outside this set can still be
61
+ * displayed — it just cannot enter model context, because there is no durable
62
+ * attachment to reference it by.
63
+ */
64
+ export declare const MODEL_IMAGE_EXTENSIONS: Readonly<Record<string, ModelImageMediaType>>;
65
+ /**
66
+ * Viewer kinds whose bytes are useless as UTF-8 text, so a `read` naming one is
67
+ * a mistake worth correcting rather than a decode that yields replacement
68
+ * characters. `html` is absent on purpose: reading HTML source is a legitimate
69
+ * text read, and displaying it is a different intent.
70
+ */
71
+ export declare const OPAQUE_KINDS: readonly ViewerKind[];
72
+ /**
73
+ * Extract a path's lowercased extension, dot included.
74
+ *
75
+ * Hand-rolled rather than `node:path`'s `extname` because the browser half
76
+ * calls this too. A leading dot is a dotfile, not an empty extension, and both
77
+ * separators are honored so a Windows-style path from a remote workspace still
78
+ * classifies.
79
+ * @param filePath - the raw path as the model wrote it; not yet resolved.
80
+ * @returns the extension including its dot, or `undefined` when there is none.
81
+ */
82
+ export declare function extensionOf(filePath: string): string | undefined;
83
+ /**
84
+ * Classify a path into the element that will render it.
85
+ * @param filePath - the raw path as the model wrote it.
86
+ * @returns the viewer kind and the content type the asset route will send;
87
+ * {@link UNKNOWN_MEDIA} for anything the table does not list.
88
+ */
89
+ export declare function classifyPath(filePath: string): MediaSpec;
90
+ /**
91
+ * The media type the attachment service would verify this path's bytes as.
92
+ * @param filePath - the raw path as the model wrote it.
93
+ * @returns the admissible media type, or `undefined` when the bytes could never
94
+ * become a durable attachment.
95
+ */
96
+ export declare function modelImageMediaTypeForPath(filePath: string): ModelImageMediaType | undefined;
97
+ /**
98
+ * Whether a `read` naming this path is decoding bytes that are not text.
99
+ * @param filePath - the raw path as the model wrote it.
100
+ * @returns true when the path names an opaque binary medium.
101
+ */
102
+ export declare function isOpaqueMediaPath(filePath: string): boolean;
103
+ /**
104
+ * Durable image facts for a raster that also reached the attachment store.
105
+ *
106
+ * Structurally the attachment service's `ImageAttachmentRef` minus the fields a
107
+ * card never reads. Restated instead of imported so the browser half stays free
108
+ * of Host packages; the Host half projects a real reference into this shape.
109
+ */
110
+ export interface ModelImage {
111
+ /** Opaque durable storage id; never a filesystem path or a bearer URL. */
112
+ attachmentId: string;
113
+ /** Media type verified from the stored bytes. */
114
+ mediaType: ModelImageMediaType;
115
+ /** Exact encoded byte length. */
116
+ bytes: number;
117
+ /** Intrinsic encoded width in pixels. */
118
+ width: number;
119
+ /** Intrinsic encoded height in pixels. */
120
+ height: number;
121
+ /** Display name stripped of local path information. */
122
+ name?: string;
123
+ }
124
+ /**
125
+ * Canonical `display_file` outcome — what `execute` returns, what a `run_code`
126
+ * program receives, and (verbatim) what rides `tool/result` as presentation
127
+ * metadata so a reopened session rebuilds the same card.
128
+ */
129
+ export interface DisplayValue {
130
+ /** The backend-resolved path that was displayed. */
131
+ path: string;
132
+ /** Which element the card renders. */
133
+ kind: ViewerKind;
134
+ /** Content type the asset route sends for this file. */
135
+ mediaType: string;
136
+ /** Byte size, or 0 when the backend does not report one. */
137
+ bytes: number;
138
+ /**
139
+ * Same-origin signed URL the card loads. Absent when the filesystem backend
140
+ * exposes no local path for the target (a remote workspace), in which case an
141
+ * admissible raster still reaches the card through {@link ModelImage}.
142
+ */
143
+ assetUrl?: string;
144
+ /** Present only for a raster this call committed to the attachment store. */
145
+ image?: ModelImage;
146
+ /**
147
+ * Why there is nothing to render, when the call succeeded but produced no
148
+ * viewable bytes — a document whose conversion is unavailable, typically.
149
+ * The card shows this verbatim instead of a generic placeholder.
150
+ */
151
+ unavailable?: string;
152
+ /**
153
+ * Whether the image ALSO entered model context. False is the ordinary
154
+ * outcome for every non-raster and for every text-only model route: the file
155
+ * is on the user's screen, not in the model's context.
156
+ */
157
+ inContext: boolean;
158
+ }
159
+ /** Narrow one unknown value to {@link ModelImage}. */
160
+ export declare function modelImageFrom(value: unknown): ModelImage | undefined;
161
+ /**
162
+ * Narrow opaque live or replayed result metadata to a {@link DisplayValue}.
163
+ *
164
+ * Defensive because the browser side reads data written by an older version of
165
+ * this plugin (or by nothing at all). Malformed metadata returns `undefined` so
166
+ * the card falls back to a plain row instead of throwing during replay.
167
+ * @param meta - the `tool/result` metadata, verbatim.
168
+ * @returns the validated projection, or `undefined` when absent or malformed.
169
+ */
170
+ export declare function displayValueFrom(meta: unknown): DisplayValue | undefined;
171
+ /**
172
+ * Render a byte count the way a file manager does.
173
+ * @param bytes - non-negative byte count.
174
+ * @returns a short human string, or an empty string when the size is unknown.
175
+ */
176
+ export declare function formatBytes(bytes: number): string;
177
+ /** Durable settings section owned by this plugin. */
178
+ export interface ViewerSettings {
179
+ /** Whether to register the `display_file` tool at all. */
180
+ tool: boolean;
181
+ /**
182
+ * Whether to refuse `read` on an opaque binary medium and point the model at
183
+ * `display_file`. Off leaves the shipped `read` free to decode a raster as
184
+ * UTF-8, which yields replacement characters rather than an error.
185
+ */
186
+ redirectRead: boolean;
187
+ /**
188
+ * Whether `display_file` may also put an admissible raster into model context
189
+ * on routes that accept image input. Off keeps every display UI-only, which is
190
+ * the cheaper choice when the files are for the human, not the model.
191
+ */
192
+ feedModel: boolean;
193
+ /**
194
+ * Whether to hide the shipped `read_image` from every agent, making
195
+ * `display_file` the single image entry point. On is the default because the
196
+ * two tools overlap on model-context ingestion, and a model offered both uses
197
+ * both — the same image then enters context twice in one turn.
198
+ */
199
+ supersedeReadImage: boolean;
200
+ }
201
+ /** Field carrying {@link ViewerSettings.tool}. */
202
+ export declare const TOOL_FIELD = "tool";
203
+ /** Field carrying {@link ViewerSettings.redirectRead}. */
204
+ export declare const REDIRECT_READ_FIELD = "redirectRead";
205
+ /** Field carrying {@link ViewerSettings.feedModel}. */
206
+ export declare const FEED_MODEL_FIELD = "feedModel";
207
+ /** Field carrying {@link ViewerSettings.supersedeReadImage}. */
208
+ export declare const SUPERSEDE_READ_IMAGE_FIELD = "supersedeReadImage";
209
+ export {};