@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.en.md +88 -0
- package/README.i18n.yaml +6 -0
- package/README.md +88 -0
- package/cordis.patch.yml +7 -0
- package/lib/index.js +1338 -0
- package/lib/invariant.js +25 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +549 -0
- package/lib/typert.remote-client.d.ts +35 -0
- package/lib/typert.remote-client.d.ts.map +1 -0
- package/lib/typert.remote-client.js +210 -0
- package/lib/types/bash-writes.d.ts +83 -0
- package/lib/types/bash-writes.d.ts.map +1 -0
- package/lib/types/bash-writes.js +367 -0
- package/lib/types/fold.d.ts +77 -0
- package/lib/types/fold.d.ts.map +1 -0
- package/lib/types/fold.js +309 -0
- package/lib/types/index.d.ts +105 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +517 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/reveal.d.ts +36 -0
- package/lib/types/reveal.d.ts.map +1 -0
- package/lib/types/reveal.js +77 -0
- package/lib/types/types.d.ts +124 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +2 -0
- package/package.json +81 -0
- package/skills/3d-artifact/SKILL.md +52 -0
|
@@ -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"}
|