@khorsheed/dsh-file-preview 0.1.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,77 @@
1
+ /** Pure log fold over the files a session read, wrote, or edited. @module @khorsheed/dsh-file-preview/fold */
2
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
3
+ import type { FilePreviewList, FilePreviewOp } from './types.ts';
4
+ /** The extracted touch operation and display path of one tool call. */
5
+ export interface FilePreviewCallTarget {
6
+ /** The operation the tool performs. */
7
+ readonly op: FilePreviewOp;
8
+ /** The display path recorded in the call arguments. */
9
+ readonly path: string;
10
+ }
11
+ /** One write/edit diff captured from a tool result's presentation meta. */
12
+ export type FilePreviewResultDiff = {
13
+ /** The changed file's path (the same display path the call recorded). */
14
+ readonly path: string;
15
+ /** Prior content, or null for a new file / an overwrite. */
16
+ readonly oldText: string | null;
17
+ /** Content after the change. */
18
+ readonly newText: string;
19
+ };
20
+ /** Extract the touched path and operation from one tool call's raw arguments JSON. */
21
+ export declare function pathFromToolCall(name: string, argumentsJson: string): FilePreviewCallTarget | undefined;
22
+ /** Read the write/edit diff list out of a tool result's tool-private meta. */
23
+ export declare function diffsFromResultMeta(meta: unknown): readonly FilePreviewResultDiff[] | undefined;
24
+ /**
25
+ * Fold a session's events into the files it read, wrote, or edited, in
26
+ * first-seen order and deduplicated by display path; a later occurrence
27
+ * refreshes the entry's op and location without moving it, and each write/edit
28
+ * tool result appends its change's diff to the entry's `diffs` in event order.
29
+ * Nested Code Mode file dispatches (`tool/code-dispatch` events) count too —
30
+ * they carry no turn/step of their own, so the entry borrows the enclosing
31
+ * root call's location.
32
+ * @param events - the session's events in ascending seq order.
33
+ * @param maxFiles - cap on returned entries; excess files are dropped in
34
+ * first-seen order after the cap is reached.
35
+ * @returns the capped list plus the last scanned seq and the truncation flag.
36
+ */
37
+ export declare function foldFilePreview(events: readonly SessionEvent[], maxFiles: number): FilePreviewList;
38
+ /** Lines in one diff text: a trailing newline terminates the last line rather than adding one. */
39
+ export declare function lineCount(text: string): number;
40
+ /** One file's mutation facts within one turn — the turn card's vocabulary. */
41
+ export interface TurnFileFact {
42
+ readonly path: string;
43
+ /** Seq of the first mutation recorded for this path in this turn (stable ordering). */
44
+ readonly seq: number;
45
+ readonly step: number;
46
+ /** Lines added across the turn's mutations of this path. */
47
+ readonly added?: number;
48
+ /** Lines removed; absent when any mutation reported no prior content (create/overwrite). */
49
+ readonly removed?: number;
50
+ }
51
+ /** Per-turn file maps: turn number → path → mutation facts (first-seen within the turn). */
52
+ export type TurnFilesByTurn = ReadonlyMap<number, ReadonlyMap<string, TurnFileFact>>;
53
+ /**
54
+ * Fold a session's events into per-turn file-mutation maps — the turn card's
55
+ * single source of truth. Unlike {@link foldFilePreview}, this does NOT dedupe
56
+ * a path to its last occurrence: a file touched in two turns appears in BOTH
57
+ * turn maps, so each turn's card lists exactly what that turn mutated.
58
+ * write/edit calls register with their own turn; nested Code Mode dispatches
59
+ * borrow the enclosing root call's turn (dispatch events carry none); a
60
+ * `tool/result` whose presentation meta carries diffs registers its paths (the
61
+ * render-intent vocabulary) with the result's turn, and per-turn line deltas
62
+ * are summed from the diffs. A `write` whose result carries no diffs is a
63
+ * create (the write tool records no diffs when the file had no prior content):
64
+ * its full written content — read from the call arguments, which the log keeps
65
+ * — is the added-line count, once per file per turn.
66
+ *
67
+ * Count discipline: a write/edit tool/call only registers the path's location
68
+ * and never touches counts (its counted result follows); the first count
69
+ * contribution for a turn+path is adopted verbatim, later ones accumulate per
70
+ * field with unknown-wins — an uncounted mutation (a Code Mode dispatch, or a
71
+ * diff whose side reports no prior content) poisons that field to undefined
72
+ * rather than reporting a misleading partial.
73
+ * @param events - the session's events in ascending seq order.
74
+ * @returns turn → path → facts, in event order per turn.
75
+ */
76
+ export declare function foldFilePreviewByTurn(events: readonly SessionEvent[]): TurnFilesByTurn;
77
+ //# sourceMappingURL=fold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fold.d.ts","sourceRoot":"","sources":["../../src/fold.ts"],"names":[],"mappings":"AAAA,8GAA8G;AAE9G,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,OAAO,KAAK,EAAqC,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAQnG,uEAAuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,aAAa,CAAA;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,2EAA2E;AAC3E,MAAM,MAAM,qBAAqB,GAAG;IAClC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,gCAAgC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB,CAAA;AAmBD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAOvG;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,qBAAqB,EAAE,GAAG,SAAS,CAc/F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CA8DlG;AAED,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,4FAA4F;AAC5F,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAA;AAuCpF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,eAAe,CAyHtF"}
@@ -0,0 +1,309 @@
1
+ /** Pure log fold over the files a session read, wrote, or edited. @module @khorsheed/dsh-file-preview/fold */
2
+ /** Tool names whose calls record a touched file path. */
3
+ const FILE_TOOL_NAMES = new Set(['read', 'write', 'edit']);
4
+ /** The argument key carrying the touched path (shared by all three file tools). */
5
+ const FILE_PATH_ARGUMENT = 'file_path';
6
+ /**
7
+ * Extract the touched path and operation from parsed file-tool arguments.
8
+ * @param name - the tool name.
9
+ * @param args - the parsed arguments payload (an object when well-formed).
10
+ * @returns the touch target, or undefined when the call is not a file touch.
11
+ */
12
+ function targetFromArguments(name, args) {
13
+ if (!FILE_TOOL_NAMES.has(name))
14
+ return undefined;
15
+ if (args !== null && typeof args === 'object') {
16
+ const candidate = args[FILE_PATH_ARGUMENT];
17
+ if (typeof candidate === 'string' && candidate.length > 0) {
18
+ return { op: name, path: candidate };
19
+ }
20
+ }
21
+ return undefined;
22
+ }
23
+ /** Extract the touched path and operation from one tool call's raw arguments JSON. */
24
+ export function pathFromToolCall(name, argumentsJson) {
25
+ try {
26
+ return targetFromArguments(name, JSON.parse(argumentsJson));
27
+ }
28
+ catch {
29
+ // Model arguments are expected JSON; a malformed payload contributes no path.
30
+ return undefined;
31
+ }
32
+ }
33
+ /** Read the write/edit diff list out of a tool result's tool-private meta. */
34
+ export function diffsFromResultMeta(meta) {
35
+ if (meta === null || typeof meta !== 'object')
36
+ return undefined;
37
+ const diffs = meta.diffs;
38
+ if (!Array.isArray(diffs) || diffs.length === 0)
39
+ return undefined;
40
+ const result = [];
41
+ for (const item of diffs) {
42
+ if (item === null || typeof item !== 'object')
43
+ return undefined;
44
+ const { path, oldText, newText } = item;
45
+ if (typeof path !== 'string' || path.length === 0)
46
+ return undefined;
47
+ if (typeof newText !== 'string')
48
+ return undefined;
49
+ if (oldText !== null && typeof oldText !== 'string')
50
+ return undefined;
51
+ result.push({ path, oldText, newText });
52
+ }
53
+ return result;
54
+ }
55
+ /**
56
+ * Fold a session's events into the files it read, wrote, or edited, in
57
+ * first-seen order and deduplicated by display path; a later occurrence
58
+ * refreshes the entry's op and location without moving it, and each write/edit
59
+ * tool result appends its change's diff to the entry's `diffs` in event order.
60
+ * Nested Code Mode file dispatches (`tool/code-dispatch` events) count too —
61
+ * they carry no turn/step of their own, so the entry borrows the enclosing
62
+ * root call's location.
63
+ * @param events - the session's events in ascending seq order.
64
+ * @param maxFiles - cap on returned entries; excess files are dropped in
65
+ * first-seen order after the cap is reached.
66
+ * @returns the capped list plus the last scanned seq and the truncation flag.
67
+ */
68
+ export function foldFilePreview(events, maxFiles) {
69
+ const entries = new Map();
70
+ /** Root tool/call locations, so nested code dispatches borrow their turn/step. */
71
+ const callSites = new Map();
72
+ let asOfSeq = -1;
73
+ /** Upsert one touched path, refreshing a repeat in place and keeping its diffs. */
74
+ const record = (target, seq, turn, step) => {
75
+ const next = { path: target.path, op: target.op, seq, turn, step, diffs: [] };
76
+ const existing = entries.get(target.path);
77
+ if (existing === undefined && entries.size >= maxFiles)
78
+ return;
79
+ entries.set(target.path, existing === undefined ? next : { ...existing, ...next, diffs: existing.diffs });
80
+ };
81
+ for (const event of events) {
82
+ asOfSeq = event.seq;
83
+ if (event.type === 'tool/call') {
84
+ callSites.set(String(event.data.callId), { turn: event.data.turn, step: event.data.step });
85
+ const target = pathFromToolCall(event.data.name, event.data.arguments);
86
+ if (target === undefined)
87
+ continue;
88
+ record(target, event.seq, event.data.turn, event.data.step);
89
+ continue;
90
+ }
91
+ if (event.type === 'tool/code-dispatch') {
92
+ // The dispatch event IS the settled outcome: a failed sub-call changed
93
+ // nothing, so only successful file touches record.
94
+ if (event.data.isError)
95
+ continue;
96
+ const target = targetFromArguments(event.data.name, event.data.arguments);
97
+ if (target === undefined)
98
+ continue;
99
+ const site = callSites.get(String(event.data.rootCallId)) ?? { turn: 0, step: 0 };
100
+ record(target, event.seq, site.turn, site.step);
101
+ continue;
102
+ }
103
+ if (event.type === 'tool/result') {
104
+ const diffs = diffsFromResultMeta(event.data.meta);
105
+ if (diffs === undefined)
106
+ continue;
107
+ for (const diff of diffs) {
108
+ // The render-intent vocabulary: a result diff registers its path even
109
+ // when no write/edit call recorded it (non-write/edit mutation tools
110
+ // like the official deliverables' follow-along locations).
111
+ if (!entries.has(diff.path)) {
112
+ record({ op: 'edit', path: diff.path }, event.seq, event.data.turn, event.data.step);
113
+ }
114
+ const entry = entries.get(diff.path);
115
+ if (entry === undefined)
116
+ continue;
117
+ const change = {
118
+ seq: event.seq,
119
+ turn: event.data.turn,
120
+ step: event.data.step,
121
+ oldText: diff.oldText,
122
+ newText: diff.newText,
123
+ };
124
+ entries.set(diff.path, { ...entry, diffs: [...entry.diffs, change] });
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ entries: [...entries.values()].map((entry) => {
130
+ const last = entry.diffs[entry.diffs.length - 1];
131
+ return last === undefined ? entry : { ...entry, lastDiff: { oldText: last.oldText, newText: last.newText } };
132
+ }),
133
+ asOfSeq,
134
+ truncated: entries.size >= maxFiles,
135
+ };
136
+ }
137
+ /** Lines in one diff text: a trailing newline terminates the last line rather than adding one. */
138
+ export function lineCount(text) {
139
+ if (text === '')
140
+ return 0;
141
+ const lines = text.split('\n').length;
142
+ return text.endsWith('\n') ? lines - 1 : lines;
143
+ }
144
+ /** Sum two optional counts; unknown wins (any uncounted mutation reports no total). */
145
+ function sumKnown(a, b) {
146
+ return a === undefined || b === undefined ? undefined : a + b;
147
+ }
148
+ /** Extract the full written content from a `write` tool call's arguments JSON. */
149
+ function contentFromWriteCall(argumentsJson) {
150
+ try {
151
+ const args = JSON.parse(argumentsJson);
152
+ if (args !== null && typeof args === 'object') {
153
+ const content = args.content;
154
+ if (typeof content === 'string')
155
+ return content;
156
+ }
157
+ }
158
+ catch {
159
+ // Model arguments are expected JSON; a malformed payload has no content.
160
+ }
161
+ return undefined;
162
+ }
163
+ /** Read the tool-result block's call identity and failure flag off a result message. */
164
+ function resultCallFacts(message) {
165
+ const block = message.content[0];
166
+ if (block?.type !== 'tool-result' || typeof block.toolCallId !== 'string')
167
+ return undefined;
168
+ return { callId: block.toolCallId, isError: block.isError === true };
169
+ }
170
+ /**
171
+ * Fold a session's events into per-turn file-mutation maps — the turn card's
172
+ * single source of truth. Unlike {@link foldFilePreview}, this does NOT dedupe
173
+ * a path to its last occurrence: a file touched in two turns appears in BOTH
174
+ * turn maps, so each turn's card lists exactly what that turn mutated.
175
+ * write/edit calls register with their own turn; nested Code Mode dispatches
176
+ * borrow the enclosing root call's turn (dispatch events carry none); a
177
+ * `tool/result` whose presentation meta carries diffs registers its paths (the
178
+ * render-intent vocabulary) with the result's turn, and per-turn line deltas
179
+ * are summed from the diffs. A `write` whose result carries no diffs is a
180
+ * create (the write tool records no diffs when the file had no prior content):
181
+ * its full written content — read from the call arguments, which the log keeps
182
+ * — is the added-line count, once per file per turn.
183
+ *
184
+ * Count discipline: a write/edit tool/call only registers the path's location
185
+ * and never touches counts (its counted result follows); the first count
186
+ * contribution for a turn+path is adopted verbatim, later ones accumulate per
187
+ * field with unknown-wins — an uncounted mutation (a Code Mode dispatch, or a
188
+ * diff whose side reports no prior content) poisons that field to undefined
189
+ * rather than reporting a misleading partial.
190
+ * @param events - the session's events in ascending seq order.
191
+ * @returns turn → path → facts, in event order per turn.
192
+ */
193
+ export function foldFilePreviewByTurn(events) {
194
+ const byTurn = new Map();
195
+ /** Root tool/call locations, so nested code dispatches borrow their turn/step. */
196
+ const callSites = new Map();
197
+ /** Write-tool calls keyed by callId (create fallback; see {@link WriteCallFacts}). */
198
+ const writeCalls = new Map();
199
+ /** Every path the session log has written/edited or diff-reported so far
200
+ * (create discrimination: a first-touch write is a create). */
201
+ const sessionSeen = new Set();
202
+ /** Turn+path keys that have received a line-count contribution. */
203
+ const counted = new Set();
204
+ const countKey = (turn, path) => `${turn}:${path}`;
205
+ const turnMap = (turn) => {
206
+ let map = byTurn.get(turn);
207
+ if (map === undefined) {
208
+ map = new Map();
209
+ byTurn.set(turn, map);
210
+ }
211
+ return map;
212
+ };
213
+ /** Upsert a path's location without touching its line counts: a write/edit
214
+ * tool/call precedes its counted result, so it must neither wipe counts a
215
+ * sibling mutation contributed nor adopt anything itself. */
216
+ const recordLocation = (path, turn, seq, step) => {
217
+ const map = turnMap(turn);
218
+ const existing = map.get(path);
219
+ map.set(path, existing === undefined ? { path, seq, step } : { ...existing, seq, step });
220
+ };
221
+ /** Apply one mutation's line-count contribution. The first contribution for
222
+ * a turn+path is adopted verbatim (a count-less placeholder from a
223
+ * preceding tool/call must not poison it); later contributions accumulate
224
+ * per field with unknown-wins. `unknown` marks an uncountable mutation (a
225
+ * Code Mode dispatch): it poisons both fields. */
226
+ const recordContribution = (path, turn, seq, step, added, removed, unknown) => {
227
+ const map = turnMap(turn);
228
+ const key = countKey(turn, path);
229
+ if (unknown) {
230
+ counted.add(key);
231
+ map.set(path, { path, seq, step });
232
+ return;
233
+ }
234
+ const existing = map.get(path);
235
+ if (!counted.has(key)) {
236
+ counted.add(key);
237
+ map.set(path, {
238
+ path, seq, step,
239
+ ...(added === undefined ? {} : { added }),
240
+ ...(removed === undefined ? {} : { removed }),
241
+ });
242
+ return;
243
+ }
244
+ // Narrow the optional totals before the conditional spread so the fact
245
+ // never carries an explicit `undefined` (exactOptionalPropertyTypes).
246
+ const mergedAdded = added === undefined ? undefined : sumKnown(existing?.added, added);
247
+ const mergedRemoved = removed === undefined ? undefined : sumKnown(existing?.removed, removed);
248
+ map.set(path, {
249
+ path, seq, step,
250
+ ...(mergedAdded === undefined ? {} : { added: mergedAdded }),
251
+ ...(mergedRemoved === undefined ? {} : { removed: mergedRemoved }),
252
+ });
253
+ };
254
+ for (const event of events) {
255
+ if (event.type === 'tool/call') {
256
+ callSites.set(String(event.data.callId), { turn: event.data.turn, step: event.data.step });
257
+ const target = pathFromToolCall(event.data.name, event.data.arguments);
258
+ if (target === undefined)
259
+ continue;
260
+ if (target.op === 'write') {
261
+ const content = contentFromWriteCall(event.data.arguments);
262
+ if (content !== undefined) {
263
+ const firstTouch = !sessionSeen.has(target.path);
264
+ sessionSeen.add(target.path);
265
+ writeCalls.set(String(event.data.callId), { path: target.path, content, firstTouch });
266
+ }
267
+ }
268
+ recordLocation(target.path, event.data.turn, event.seq, event.data.step);
269
+ continue;
270
+ }
271
+ if (event.type === 'tool/code-dispatch') {
272
+ // The dispatch event IS the settled outcome: a failed sub-call changed
273
+ // nothing, so only successful file touches record. Code Mode writes are
274
+ // uncountable (no result diffs reach the session), so they register the
275
+ // path and poison the line-count totals (unknown wins).
276
+ if (event.data.isError)
277
+ continue;
278
+ const target = targetFromArguments(event.data.name, event.data.arguments);
279
+ if (target === undefined)
280
+ continue;
281
+ const site = callSites.get(String(event.data.rootCallId)) ?? { turn: 0, step: 0 };
282
+ sessionSeen.add(target.path);
283
+ recordContribution(target.path, site.turn, event.seq, site.step, undefined, undefined, true);
284
+ continue;
285
+ }
286
+ if (event.type === 'tool/result') {
287
+ const diffs = diffsFromResultMeta(event.data.meta);
288
+ if (diffs !== undefined) {
289
+ for (const diff of diffs) {
290
+ sessionSeen.add(diff.path);
291
+ recordContribution(diff.path, event.data.turn, event.seq, event.data.step, lineCount(diff.newText), diff.oldText === null ? undefined : lineCount(diff.oldText), false);
292
+ }
293
+ continue;
294
+ }
295
+ // Create fallback: a first-touch write whose result carries no diff meta
296
+ // — the write tool records no diffs when the file had no prior content —
297
+ // contributed the whole written content, so the full content IS the
298
+ // added-line count and a create removes 0. A later empty-meta write to
299
+ // the same path (an overwrite whose prior content was not diffable)
300
+ // contributes nothing rather than double-counting.
301
+ const call = resultCallFacts(event.data.message);
302
+ const write = call === undefined ? undefined : writeCalls.get(call.callId);
303
+ if (write === undefined || !write.firstTouch || call?.isError === true)
304
+ continue;
305
+ recordContribution(write.path, event.data.turn, event.seq, event.data.step, lineCount(write.content), 0, false);
306
+ }
307
+ }
308
+ return byTurn;
309
+ }
@@ -0,0 +1,105 @@
1
+ /** Read-only file-preview Remote service over live sessions and the filesystem.
2
+ * @module @khorsheed/dsh-file-preview
3
+ */
4
+ import type { Context } from '@deepseek-ai/cordis';
5
+ import type { Agent } from '@deepseek-ai/dsh-agent';
6
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
7
+ import z from '@deepseek-ai/schemastery';
8
+ import type { FilePreviewConfig, FilePreviewList, FilePreviewRead, FilePreviewReveal, FilePreviewTurnMap } from './types.ts';
9
+ declare module '@deepseek-ai/cordis' {
10
+ interface Context {
11
+ filePreview: FilePreviewService;
12
+ }
13
+ }
14
+ /**
15
+ * The `filePreview` Remote service: session-scoped read-only file inspection
16
+ * for web surfaces. `list` is a pure fold over the session log (no filesystem
17
+ * access); `read` resolves the path against the session cwd and serves current
18
+ * text content through `ctx.fs` (or a browser URL for images), capped by
19
+ * config. Image bytes ride a dedicated host route so the browser loads them
20
+ * natively without bloating the RPC channel.
21
+ */
22
+ export declare class FilePreviewService extends TypertRemoteService {
23
+ static inject: string[];
24
+ static Config: z<FilePreviewConfig>;
25
+ /** URL prefix the image route answers under. */
26
+ static readonly IMAGE_ROUTE = "/file-preview-image";
27
+ private readonly resolved;
28
+ /** Whether a web server is present to host the image route; image reads
29
+ * answer `binary` without it (the headless face has no browser to serve). */
30
+ private readonly imageRoute;
31
+ /** The agents service when present (image route only; null headless). */
32
+ private readonly agents;
33
+ /** Native reveal runner; a constructor seam for deterministic tests. */
34
+ private readonly revealNative;
35
+ /** Bash-write collector (S2 seam), or undefined when disabled in config. */
36
+ private readonly collector;
37
+ /** Per-session by-turn fold cache, invalidated by the log watermark. */
38
+ private readonly turnCache;
39
+ /**
40
+ * @param ctx - owning Cordis Context carrying `fs`; `webServer` and `agents`
41
+ * are optional (the image route needs them, headless faces lack both).
42
+ * @param config - optional deployment tuning; defaults cap reads and lists,
43
+ * and enable the bash-write collector.
44
+ * @param deps - test seam for the native reveal dispatch.
45
+ */
46
+ constructor(ctx: Context, config?: FilePreviewConfig, deps?: {
47
+ revealNative?: (path: string, signal: AbortSignal) => Promise<void>;
48
+ });
49
+ private get fs();
50
+ /**
51
+ * Serve one image file's bytes for a browser `<img>` request. The URL is
52
+ * `IMAGE_ROUTE/<sessionId>/<encodeURIComponent(path)>`; the session must be
53
+ * live (the same scope `read` serves) and the path resolves against its cwd.
54
+ * @param req - the incoming image request.
55
+ * @param res - the response to fill with bytes or an error status.
56
+ */
57
+ private serveImage;
58
+ /**
59
+ * List the files one session wrote or edited: the log fold plus any
60
+ * bash-written files the collector verified (the S2 seam), merged so a
61
+ * captured path the fold already knows keeps its log-derived entry.
62
+ * @param agent - owning live agent; its session log is the data source.
63
+ * @returns first-seen files capped by `maxFiles`, with the log watermark.
64
+ */
65
+ list(agent: Agent): FilePreviewList;
66
+ /**
67
+ * Every turn's file mutations for the turn-tail card — the single source of
68
+ * truth the card renders (the client's old write/edit-only fold retired).
69
+ * Built by folding the session log per turn (write/edit calls, Code Mode
70
+ * dispatches borrowing the root call's turn, and render-intent paths from
71
+ * result diff meta) and merging the collector's bash-written captures by
72
+ * their own turn. The fold is cached per session and invalidated by the log
73
+ * watermark, so repeated card fetches do not refold. A path touched in two
74
+ * turns appears in BOTH turn groups — per-turn attribution is exact.
75
+ * @param agent - owning live agent; its session log is the data source.
76
+ * @returns per-turn file groups plus the scanned watermark.
77
+ */
78
+ turnFiles(agent: Agent): FilePreviewTurnMap;
79
+ /**
80
+ * Read the current content of one recorded file, resolved against the
81
+ * session cwd and capped by `maxReadBytes`.
82
+ * @param agent - owning live agent; its session cwd anchors relative paths.
83
+ * @param path - the display path recorded by the write/edit tool call.
84
+ * @param signal - cooperative cancellation from the calling UI request.
85
+ * @returns the classified read; `text` carries content, `image` carries a
86
+ * browser URL, other kinds carry the size or a human-readable reason.
87
+ */
88
+ read(agent: Agent, path: string, signal: AbortSignal): Promise<FilePreviewRead>;
89
+ /**
90
+ * Reveal one recorded file in the host file manager — open its folder and
91
+ * select the file (macOS Finder, Windows Explorer, or a select-capable
92
+ * desktop file manager). The display path resolves against the session cwd;
93
+ * the native reveal runs on the host. A path that does not resolve to an
94
+ * existing target, or a platform that cannot select, answers `revealed:
95
+ * false` and the caller opens the parent folder instead (the pre-reveal
96
+ * "show in folder" behavior), so the gesture always lands somewhere visible.
97
+ * @param agent - owning live agent; its session cwd anchors relative paths.
98
+ * @param path - the display path recorded by the read/write/edit tool call.
99
+ * @param signal - cooperative cancellation from the calling UI request.
100
+ * @returns whether the file manager selected the file, or why not.
101
+ */
102
+ reveal(agent: Agent, path: string, signal: AbortSignal): Promise<FilePreviewReveal>;
103
+ }
104
+ export default FilePreviewService;
105
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAiB,MAAM,wBAAwB,CAAA;AAGlE,OAAO,EAAU,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAC9E,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAIxC,OAAO,KAAK,EAAE,iBAAiB,EAAoB,eAAe,EAAE,eAAe,EAAE,iBAAiB,EAAuB,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAEnK,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,WAAW,EAAE,kBAAkB,CAAA;KAChC;CACF;AA4HD;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,mBAAmB;IACzD,MAAM,CAAC,MAAM,WAAS;IAEtB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAKjC;IAEF,gDAAgD;IAChD,MAAM,CAAC,QAAQ,CAAC,WAAW,yBAAwB;IAEnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD;iFAC6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsD;IACnF,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkE;IAE5F;;;;;;OAMG;IACH,YACE,GAAG,EAAE,OAAO,EACZ,MAAM,GAAE,iBAAsB,EAC9B,IAAI,GAAE;QAAE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAAO,EAoCnF;IAED,OAAO,KAAK,EAAE,GAEb;IAED;;;;;;OAMG;YACW,UAAU;IAmExB;;;;;;OAMG;IAEH,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAgBlC;IAED;;;;;;;;;;;OAWG;IAEH,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,kBAAkB,CAiC1C;IAED;;;;;;;;OAQG;IAEG,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CA6DpF;IAED;;;;;;;;;;;;OAYG;IAEG,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgCxF;CACF;eAEc,kBAAkB"}