@khorsheed/dsh-file-preview 0.2.0 → 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.
- package/CHANGELOG.md +18 -0
- package/README.en.md +9 -6
- package/README.i18n.yaml +2 -2
- package/README.md +9 -6
- package/lib/index.js +174 -75
- package/lib/typert.host.js +139 -19
- package/lib/typert.remote-client.d.ts +6 -1
- package/lib/typert.remote-client.js +83 -5
- package/lib/types/fold.d.ts +7 -6
- package/lib/types/fold.js +24 -36
- package/lib/types/index.d.ts +25 -2
- package/lib/types/index.js +62 -55
- package/lib/types/open-external.d.ts +37 -0
- package/lib/types/open-external.js +61 -0
- package/lib/types/types.d.ts +11 -1
- package/package.json +14 -15
- package/lib/tsconfig.tsbuildinfo +0 -1
- package/lib/typert.remote-client.d.ts.map +0 -1
- package/lib/types/bash-writes.d.ts.map +0 -1
- package/lib/types/fold.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/invariant.d.ts.map +0 -1
- package/lib/types/reveal.d.ts.map +0 -1
- package/lib/types/types.d.ts.map +0 -1
- package/skills/3d-artifact/SKILL.md +0 -52
package/lib/types/fold.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/** Pure log fold over the files a session read, wrote, or edited. @module @khorsheed/dsh-file-preview/fold */
|
|
2
2
|
/** Tool names whose calls record a touched file path. */
|
|
3
3
|
const FILE_TOOL_NAMES = new Set(['read', 'write', 'edit']);
|
|
4
|
-
/**
|
|
5
|
-
* Settled nested-dispatch event names, matched by name (feature detection,
|
|
6
|
-
* never a version check): `tool/code-dispatch` is the Code Mode name every
|
|
7
|
-
* host through 0.1.2-rc.1 emits; `tool/ptc-dispatch` is the PTC rename later
|
|
8
|
-
* 0.1.2 seats move to. Both carry the same settled-dispatch payload, so one
|
|
9
|
-
* branch serves either.
|
|
10
|
-
*/
|
|
11
|
-
const DISPATCH_EVENT_NAMES = new Set(['tool/code-dispatch', 'tool/ptc-dispatch']);
|
|
12
4
|
/** The argument key carrying the touched path (shared by all three file tools). */
|
|
13
5
|
const FILE_PATH_ARGUMENT = 'file_path';
|
|
14
6
|
/**
|
|
@@ -65,10 +57,11 @@ export function diffsFromResultMeta(meta) {
|
|
|
65
57
|
* first-seen order and deduplicated by display path; a later occurrence
|
|
66
58
|
* refreshes the entry's op and location without moving it, and each write/edit
|
|
67
59
|
* tool result appends its change's diff to the entry's `diffs` in event order.
|
|
68
|
-
* Nested
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
60
|
+
* Nested PTC file dispatches (`tool/ptc-dispatch` events — the host's Code
|
|
61
|
+
* Mode name through 0.1.2, renamed with the 0.1.5 format; the v2→v3 log
|
|
62
|
+
* migration renames persisted rows, so only the ptc name is matched) count
|
|
63
|
+
* too — they carry no turn/step of their own, so the entry borrows the
|
|
64
|
+
* enclosing root call's location.
|
|
72
65
|
* @param events - the session's events in ascending seq order.
|
|
73
66
|
* @param maxFiles - cap on returned entries; excess files are dropped in
|
|
74
67
|
* first-seen order after the cap is reached.
|
|
@@ -76,7 +69,7 @@ export function diffsFromResultMeta(meta) {
|
|
|
76
69
|
*/
|
|
77
70
|
export function foldFilePreview(events, maxFiles) {
|
|
78
71
|
const entries = new Map();
|
|
79
|
-
/** Root tool/call locations, so nested
|
|
72
|
+
/** Root tool/call locations, so nested dispatches borrow their turn/step. */
|
|
80
73
|
const callSites = new Map();
|
|
81
74
|
let asOfSeq = -1;
|
|
82
75
|
/** Upsert one touched path, refreshing a repeat in place and keeping its diffs. */
|
|
@@ -97,19 +90,16 @@ export function foldFilePreview(events, maxFiles) {
|
|
|
97
90
|
record(target, event.seq, event.data.turn, event.data.step);
|
|
98
91
|
continue;
|
|
99
92
|
}
|
|
100
|
-
if (
|
|
93
|
+
if (event.type === 'tool/ptc-dispatch') {
|
|
101
94
|
// The dispatch event IS the settled outcome: a failed sub-call changed
|
|
102
|
-
// nothing, so only successful file touches record.
|
|
103
|
-
|
|
104
|
-
// row yet), so the data reads through the declared constituent.
|
|
105
|
-
const dispatch = event;
|
|
106
|
-
if (dispatch.data.isError)
|
|
95
|
+
// nothing, so only successful file touches record.
|
|
96
|
+
if (event.data.isError)
|
|
107
97
|
continue;
|
|
108
|
-
const target = targetFromArguments(
|
|
98
|
+
const target = targetFromArguments(event.data.name, event.data.arguments);
|
|
109
99
|
if (target === undefined)
|
|
110
100
|
continue;
|
|
111
|
-
const site = callSites.get(String(
|
|
112
|
-
record(target,
|
|
101
|
+
const site = callSites.get(String(event.data.rootCallId)) ?? { turn: 0, step: 0 };
|
|
102
|
+
record(target, event.seq, site.turn, site.step);
|
|
113
103
|
continue;
|
|
114
104
|
}
|
|
115
105
|
if (event.type === 'tool/result') {
|
|
@@ -186,7 +176,7 @@ function resultCallFacts(message) {
|
|
|
186
176
|
* turn maps, so each turn's card lists exactly what that turn mutated.
|
|
187
177
|
* Reads leave no card entry (the card is the mutation vocabulary — write/edit
|
|
188
178
|
* only), so a pure-read call never registers; write/edit calls register with
|
|
189
|
-
* their own turn; nested
|
|
179
|
+
* their own turn; nested PTC dispatches borrow the enclosing root call's
|
|
190
180
|
* turn (dispatch events carry none); a `tool/result` whose presentation meta
|
|
191
181
|
* carries diffs registers its paths (the render-intent vocabulary) with the
|
|
192
182
|
* result's turn, and per-turn line deltas are summed from the diffs. A `write` whose result carries no diffs is a
|
|
@@ -197,7 +187,7 @@ function resultCallFacts(message) {
|
|
|
197
187
|
* Count discipline: a write/edit tool/call only registers the path's location
|
|
198
188
|
* and never touches counts (its counted result follows); the first count
|
|
199
189
|
* contribution for a turn+path is adopted verbatim, later ones accumulate per
|
|
200
|
-
* field with unknown-wins — an uncounted mutation (a
|
|
190
|
+
* field with unknown-wins — an uncounted mutation (a PTC dispatch, or a
|
|
201
191
|
* diff whose side reports no prior content) poisons that field to undefined
|
|
202
192
|
* rather than reporting a misleading partial.
|
|
203
193
|
* @param events - the session's events in ascending seq order.
|
|
@@ -205,7 +195,7 @@ function resultCallFacts(message) {
|
|
|
205
195
|
*/
|
|
206
196
|
export function foldFilePreviewByTurn(events) {
|
|
207
197
|
const byTurn = new Map();
|
|
208
|
-
/** Root tool/call locations, so nested
|
|
198
|
+
/** Root tool/call locations, so nested dispatches borrow their turn/step. */
|
|
209
199
|
const callSites = new Map();
|
|
210
200
|
/** Write-tool calls keyed by callId (create fallback; see {@link WriteCallFacts}). */
|
|
211
201
|
const writeCalls = new Map();
|
|
@@ -235,7 +225,7 @@ export function foldFilePreviewByTurn(events) {
|
|
|
235
225
|
* a turn+path is adopted verbatim (a count-less placeholder from a
|
|
236
226
|
* preceding tool/call must not poison it); later contributions accumulate
|
|
237
227
|
* per field with unknown-wins. `unknown` marks an uncountable mutation (a
|
|
238
|
-
*
|
|
228
|
+
* PTC dispatch): it poisons both fields. */
|
|
239
229
|
const recordContribution = (path, turn, seq, step, added, removed, unknown) => {
|
|
240
230
|
const map = turnMap(turn);
|
|
241
231
|
const key = countKey(turn, path);
|
|
@@ -271,7 +261,7 @@ export function foldFilePreviewByTurn(events) {
|
|
|
271
261
|
if (target === undefined)
|
|
272
262
|
continue;
|
|
273
263
|
// The card's vocabulary is mutations only: a read call registers
|
|
274
|
-
// nothing (it may still anchor a nested
|
|
264
|
+
// nothing (it may still anchor a nested dispatch's location via
|
|
275
265
|
// callSites above).
|
|
276
266
|
if (target.op === 'read')
|
|
277
267
|
continue;
|
|
@@ -286,24 +276,22 @@ export function foldFilePreviewByTurn(events) {
|
|
|
286
276
|
recordLocation(target.path, event.data.turn, event.seq, event.data.step);
|
|
287
277
|
continue;
|
|
288
278
|
}
|
|
289
|
-
if (
|
|
279
|
+
if (event.type === 'tool/ptc-dispatch') {
|
|
290
280
|
// The dispatch event IS the settled outcome: a failed sub-call changed
|
|
291
|
-
// nothing, so only successful file touches record.
|
|
281
|
+
// nothing, so only successful file touches record. PTC writes are
|
|
292
282
|
// uncountable (no result diffs reach the session), so they register the
|
|
293
|
-
// path and poison the line-count totals (unknown wins).
|
|
294
|
-
|
|
295
|
-
const dispatch = event;
|
|
296
|
-
if (dispatch.data.isError)
|
|
283
|
+
// path and poison the line-count totals (unknown wins).
|
|
284
|
+
if (event.data.isError)
|
|
297
285
|
continue;
|
|
298
|
-
const target = targetFromArguments(
|
|
286
|
+
const target = targetFromArguments(event.data.name, event.data.arguments);
|
|
299
287
|
if (target === undefined)
|
|
300
288
|
continue;
|
|
301
289
|
// Reads leave no card entry here too — only mutations register.
|
|
302
290
|
if (target.op === 'read')
|
|
303
291
|
continue;
|
|
304
|
-
const site = callSites.get(String(
|
|
292
|
+
const site = callSites.get(String(event.data.rootCallId)) ?? { turn: 0, step: 0 };
|
|
305
293
|
sessionSeen.add(target.path);
|
|
306
|
-
recordContribution(target.path, site.turn,
|
|
294
|
+
recordContribution(target.path, site.turn, event.seq, site.step, undefined, undefined, true);
|
|
307
295
|
continue;
|
|
308
296
|
}
|
|
309
297
|
if (event.type === 'tool/result') {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
5
5
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
6
6
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
7
7
|
import z from '@deepseek-ai/schemastery';
|
|
8
|
-
import type { FilePreviewConfig, FilePreviewList, FilePreviewRead, FilePreviewReveal, FilePreviewTurnMap } from './types.ts';
|
|
8
|
+
import type { FilePreviewConfig, FilePreviewList, FilePreviewOpenExternal, FilePreviewRead, FilePreviewReveal, FilePreviewTurnMap } from './types.ts';
|
|
9
9
|
declare module '@deepseek-ai/cordis' {
|
|
10
10
|
interface Context {
|
|
11
11
|
filePreview: FilePreviewService;
|
|
@@ -33,6 +33,8 @@ export declare class FilePreviewService extends TypertRemoteService {
|
|
|
33
33
|
private readonly agents;
|
|
34
34
|
/** Native reveal runner; a constructor seam for deterministic tests. */
|
|
35
35
|
private readonly revealNative;
|
|
36
|
+
/** Native open-in-app runner; a constructor seam for deterministic tests. */
|
|
37
|
+
private readonly openExternalNative;
|
|
36
38
|
/** Bash-write collector (S2 seam), or undefined when disabled in config. */
|
|
37
39
|
private readonly collector;
|
|
38
40
|
/** Per-session by-turn fold cache, invalidated by the log watermark. */
|
|
@@ -46,7 +48,12 @@ export declare class FilePreviewService extends TypertRemoteService {
|
|
|
46
48
|
*/
|
|
47
49
|
constructor(ctx: Context, config?: FilePreviewConfig, deps?: {
|
|
48
50
|
revealNative?: (path: string, signal: AbortSignal) => Promise<void>;
|
|
51
|
+
openExternalNative?: (path: string, app: string, signal: AbortSignal) => Promise<void>;
|
|
49
52
|
});
|
|
53
|
+
/** Zero-session availability handshake for browser companions. */
|
|
54
|
+
capabilities(): {
|
|
55
|
+
protocolVersion: 1;
|
|
56
|
+
};
|
|
50
57
|
private get fs();
|
|
51
58
|
/**
|
|
52
59
|
* Serve one image file's bytes for a browser `<img>` request. The URL is
|
|
@@ -81,7 +88,7 @@ export declare class FilePreviewService extends TypertRemoteService {
|
|
|
81
88
|
/**
|
|
82
89
|
* Every turn's file mutations for the turn-tail card — the single source of
|
|
83
90
|
* truth the card renders (the client's old write/edit-only fold retired).
|
|
84
|
-
* Built by folding the session log per turn (write/edit calls,
|
|
91
|
+
* Built by folding the session log per turn (write/edit calls, PTC
|
|
85
92
|
* dispatches borrowing the root call's turn, and render-intent paths from
|
|
86
93
|
* result diff meta) and merging the collector's bash-written captures by
|
|
87
94
|
* their own turn. The fold is cached per session and invalidated by the log
|
|
@@ -118,6 +125,22 @@ export declare class FilePreviewService extends TypertRemoteService {
|
|
|
118
125
|
* @returns whether the file manager selected the file, or why not.
|
|
119
126
|
*/
|
|
120
127
|
reveal(agent: Agent, path: string, signal: AbortSignal): Promise<FilePreviewReveal>;
|
|
128
|
+
/**
|
|
129
|
+
* Open one recorded file in a specific host application (the "open in IDE"
|
|
130
|
+
* gesture). The official open-in-app route only accepts directories — a
|
|
131
|
+
* file path 404s — so file-exact opens go through this method: macOS
|
|
132
|
+
* `open -a <App> <path>` via the same native-command runner reveal uses.
|
|
133
|
+
* The browser names the application by official open-in-app catalog id
|
|
134
|
+
* (probed from `/open-in-app/apps`); the id → app-name map lives in
|
|
135
|
+
* open-external.ts. Non-macOS hosts and unknown ids answer without
|
|
136
|
+
* launching, and the client hides the gesture.
|
|
137
|
+
* @param agent - owning live agent; its session cwd anchors relative paths.
|
|
138
|
+
* @param path - the display path recorded by the read/write/edit tool call.
|
|
139
|
+
* @param app - official open-in-app catalog id (e.g. `cursor`).
|
|
140
|
+
* @param signal - cooperative cancellation from the calling UI request.
|
|
141
|
+
* @returns whether the application launched, or why not.
|
|
142
|
+
*/
|
|
143
|
+
openExternal(agent: Agent, path: string, app: string, signal: AbortSignal): Promise<FilePreviewOpenExternal>;
|
|
121
144
|
}
|
|
122
145
|
export default FilePreviewService;
|
|
123
146
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.js
CHANGED
|
@@ -35,64 +35,12 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
35
35
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
36
36
|
done = true;
|
|
37
37
|
};
|
|
38
|
-
import { readFileSync } from 'node:fs';
|
|
39
|
-
import { dirname, join } from 'node:path';
|
|
40
|
-
import { fileURLToPath } from 'node:url';
|
|
41
38
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
42
39
|
import z from '@deepseek-ai/schemastery';
|
|
43
40
|
import { BashWriteCollector } from './bash-writes.js';
|
|
44
41
|
import { foldFilePreview, foldFilePreviewByTurn } from './fold.js';
|
|
42
|
+
import { openExternalNative, macAppName } from './open-external.js';
|
|
45
43
|
import { revealNativePath } from './reveal.js';
|
|
46
|
-
/**
|
|
47
|
-
* Register the 3d-artifact skill — the generation-side contract for
|
|
48
|
-
* sandbox-runnable interactive 3D / digital-twin single-file HTML (self-
|
|
49
|
-
* contained, zero runtime network, GLB-inline zero-fetch models). Pull-based
|
|
50
|
-
* discovery: an agent whose task involves generating such an HTML page finds
|
|
51
|
-
* the contract through the skill catalog — no per-session push notice.
|
|
52
|
-
* Optional: compositions without the skill capability skip the registration.
|
|
53
|
-
* A missing/malformed shipped SKILL.md degrades to a warning — a discovery
|
|
54
|
-
* aid must never take a boot down; the pack-smoke test owns the file's
|
|
55
|
-
* presence in the tarball.
|
|
56
|
-
* @param ctx - plugin context.
|
|
57
|
-
*/
|
|
58
|
-
function registerArtifactSkill(ctx) {
|
|
59
|
-
const skills = ctx.get('skills');
|
|
60
|
-
if (skills === undefined) {
|
|
61
|
-
// Not a crash: file preview degrades without the skill (and a minimal
|
|
62
|
-
// composition may legitimately lack the capability). The line exists so a
|
|
63
|
-
// host-API migration that drops or renames the skills service shows up in
|
|
64
|
-
// boot logs instead of failing silently — same diagnostic as ankh-guard's
|
|
65
|
-
// restart-skill registration; grep "skill .* not registered".
|
|
66
|
-
ctx.logger.warn('file-preview: skills capability absent — the 3d-artifact skill is not registered');
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
const skillDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills', '3d-artifact');
|
|
71
|
-
const skillFile = join(skillDir, 'SKILL.md');
|
|
72
|
-
const raw = readFileSync(skillFile, 'utf8');
|
|
73
|
-
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(raw);
|
|
74
|
-
const name = /^name: (.+)$/m.exec(match?.[1] ?? '')?.[1]?.trim();
|
|
75
|
-
const description = /^description: (.+)$/m.exec(match?.[1] ?? '')?.[1]?.trim();
|
|
76
|
-
const content = match?.[2];
|
|
77
|
-
if (match === null || name === undefined || description === undefined || content === undefined) {
|
|
78
|
-
ctx.logger.warn('file-preview: shipped SKILL.md is malformed — the 3d-artifact skill is not registered');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
ctx.effect(() => skills.register({
|
|
82
|
-
name,
|
|
83
|
-
description,
|
|
84
|
-
content,
|
|
85
|
-
source: 'runtime',
|
|
86
|
-
provider: 'file-preview',
|
|
87
|
-
// Content-only (only SKILL.md in the bundle): no `resourceBase`, so the
|
|
88
|
-
// catalog renders a virtual single-SKILL.md node. Add `resourceBase`
|
|
89
|
-
// only when the bundle later gains scripts/assets beside SKILL.md.
|
|
90
|
-
}));
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
ctx.logger.warn(`file-preview: shipped SKILL.md unreadable (${String(error)}) — the 3d-artifact skill is not registered`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
44
|
/** Extensions treated as binary without reading (their text decode is meaningless). */
|
|
97
45
|
const BINARY_EXTENSIONS = new Set([
|
|
98
46
|
'.pdf', '.zip', '.gz', '.tar', '.7z', '.zstd', '.br',
|
|
@@ -165,21 +113,27 @@ function readErrorMessage(error) {
|
|
|
165
113
|
let FilePreviewService = (() => {
|
|
166
114
|
let _classSuper = TypertRemoteService;
|
|
167
115
|
let _instanceExtraInitializers = [];
|
|
116
|
+
let _capabilities_decorators;
|
|
168
117
|
let _list_decorators;
|
|
169
118
|
let _turnFiles_decorators;
|
|
170
119
|
let _read_decorators;
|
|
171
120
|
let _reveal_decorators;
|
|
121
|
+
let _openExternal_decorators;
|
|
172
122
|
return class FilePreviewService extends _classSuper {
|
|
173
123
|
static {
|
|
174
124
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
125
|
+
_capabilities_decorators = [Remote('capabilities')];
|
|
175
126
|
_list_decorators = [Remote('list')];
|
|
176
127
|
_turnFiles_decorators = [Remote('turnFiles')];
|
|
177
128
|
_read_decorators = [Remote('read')];
|
|
178
129
|
_reveal_decorators = [Remote('reveal')];
|
|
130
|
+
_openExternal_decorators = [Remote('openExternal')];
|
|
131
|
+
__esDecorate(this, null, _capabilities_decorators, { kind: "method", name: "capabilities", static: false, private: false, access: { has: obj => "capabilities" in obj, get: obj => obj.capabilities }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
179
132
|
__esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
180
133
|
__esDecorate(this, null, _turnFiles_decorators, { kind: "method", name: "turnFiles", static: false, private: false, access: { has: obj => "turnFiles" in obj, get: obj => obj.turnFiles }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
181
134
|
__esDecorate(this, null, _read_decorators, { kind: "method", name: "read", static: false, private: false, access: { has: obj => "read" in obj, get: obj => obj.read }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
182
135
|
__esDecorate(this, null, _reveal_decorators, { kind: "method", name: "reveal", static: false, private: false, access: { has: obj => "reveal" in obj, get: obj => obj.reveal }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
136
|
+
__esDecorate(this, null, _openExternal_decorators, { kind: "method", name: "openExternal", static: false, private: false, access: { has: obj => "openExternal" in obj, get: obj => obj.openExternal }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
183
137
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
184
138
|
}
|
|
185
139
|
static inject = ['fs'];
|
|
@@ -199,6 +153,8 @@ let FilePreviewService = (() => {
|
|
|
199
153
|
agents;
|
|
200
154
|
/** Native reveal runner; a constructor seam for deterministic tests. */
|
|
201
155
|
revealNative;
|
|
156
|
+
/** Native open-in-app runner; a constructor seam for deterministic tests. */
|
|
157
|
+
openExternalNative;
|
|
202
158
|
/** Bash-write collector (S2 seam), or undefined when disabled in config. */
|
|
203
159
|
collector;
|
|
204
160
|
/** Per-session by-turn fold cache, invalidated by the log watermark. */
|
|
@@ -219,6 +175,7 @@ let FilePreviewService = (() => {
|
|
|
219
175
|
captureBashWrites: config.captureBashWrites ?? true,
|
|
220
176
|
};
|
|
221
177
|
this.revealNative = deps.revealNative ?? revealNativePath;
|
|
178
|
+
this.openExternalNative = deps.openExternalNative ?? openExternalNative;
|
|
222
179
|
if (config.captureBashWrites !== false) {
|
|
223
180
|
this.collector = new BashWriteCollector({ fs: this.fs, maxFiles: this.resolved.maxFiles });
|
|
224
181
|
ctx.effect(() => this.collector.attach(ctx), 'file-preview: bash-write capture');
|
|
@@ -239,7 +196,10 @@ let FilePreviewService = (() => {
|
|
|
239
196
|
handler: (req, res) => { void this.serveImage(req, res); },
|
|
240
197
|
}), 'file-preview: image route');
|
|
241
198
|
}
|
|
242
|
-
|
|
199
|
+
}
|
|
200
|
+
/** Zero-session availability handshake for browser companions. */
|
|
201
|
+
capabilities() {
|
|
202
|
+
return { protocolVersion: 1 };
|
|
243
203
|
}
|
|
244
204
|
get fs() {
|
|
245
205
|
return this.ctx.fs;
|
|
@@ -385,7 +345,7 @@ let FilePreviewService = (() => {
|
|
|
385
345
|
/**
|
|
386
346
|
* Every turn's file mutations for the turn-tail card — the single source of
|
|
387
347
|
* truth the card renders (the client's old write/edit-only fold retired).
|
|
388
|
-
* Built by folding the session log per turn (write/edit calls,
|
|
348
|
+
* Built by folding the session log per turn (write/edit calls, PTC
|
|
389
349
|
* dispatches borrowing the root call's turn, and render-intent paths from
|
|
390
350
|
* result diff meta) and merging the collector's bash-written captures by
|
|
391
351
|
* their own turn. The fold is cached per session and invalidated by the log
|
|
@@ -567,6 +527,53 @@ let FilePreviewService = (() => {
|
|
|
567
527
|
return { revealed: false, reason: 'select-failed' };
|
|
568
528
|
}
|
|
569
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Open one recorded file in a specific host application (the "open in IDE"
|
|
532
|
+
* gesture). The official open-in-app route only accepts directories — a
|
|
533
|
+
* file path 404s — so file-exact opens go through this method: macOS
|
|
534
|
+
* `open -a <App> <path>` via the same native-command runner reveal uses.
|
|
535
|
+
* The browser names the application by official open-in-app catalog id
|
|
536
|
+
* (probed from `/open-in-app/apps`); the id → app-name map lives in
|
|
537
|
+
* open-external.ts. Non-macOS hosts and unknown ids answer without
|
|
538
|
+
* launching, and the client hides the gesture.
|
|
539
|
+
* @param agent - owning live agent; its session cwd anchors relative paths.
|
|
540
|
+
* @param path - the display path recorded by the read/write/edit tool call.
|
|
541
|
+
* @param app - official open-in-app catalog id (e.g. `cursor`).
|
|
542
|
+
* @param signal - cooperative cancellation from the calling UI request.
|
|
543
|
+
* @returns whether the application launched, or why not.
|
|
544
|
+
*/
|
|
545
|
+
async openExternal(agent, path, app, signal) {
|
|
546
|
+
if (typeof path !== 'string' || path.length === 0 || typeof app !== 'string' || app.length === 0) {
|
|
547
|
+
return { opened: false, reason: 'missing' };
|
|
548
|
+
}
|
|
549
|
+
if (macAppName(app) === undefined)
|
|
550
|
+
return { opened: false, reason: 'unknown-app' };
|
|
551
|
+
const cwd = agent.session.header.cwd;
|
|
552
|
+
let target;
|
|
553
|
+
try {
|
|
554
|
+
target = await this.fs.resolve(path, cwd === undefined ? { signal } : { cwd, signal });
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return { opened: false, reason: 'missing' };
|
|
558
|
+
}
|
|
559
|
+
let info;
|
|
560
|
+
try {
|
|
561
|
+
info = await this.fs.stat(target, signal);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
info = undefined;
|
|
565
|
+
}
|
|
566
|
+
if (info === undefined)
|
|
567
|
+
return { opened: false, reason: 'missing' };
|
|
568
|
+
try {
|
|
569
|
+
await this.openExternalNative(this.fs.processPath(target), app, signal);
|
|
570
|
+
return { opened: true };
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
574
|
+
return { opened: false, reason: message.startsWith('open-in-app is unsupported on ') ? 'unsupported-platform' : 'launch-failed' };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
570
577
|
};
|
|
571
578
|
})();
|
|
572
579
|
export { FilePreviewService };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native "open this file in a specific application" dispatch used by the
|
|
3
|
+
* file-preview Remote's `openExternal` method (the "open in IDE" gesture).
|
|
4
|
+
*
|
|
5
|
+
* The official open-in-app route (`POST /open-in-app/open`) only accepts
|
|
6
|
+
* directories — a file path 404s — so file-exact opens carry their own
|
|
7
|
+
* dispatch here. macOS only: `open -a <App> <path>` through the official
|
|
8
|
+
* `@deepseek-ai/dsh-native-command` runner (no shell, abort propagation),
|
|
9
|
+
* mirroring `reveal.ts`'s discipline. The browser picks the application by
|
|
10
|
+
* official open-in-app catalog id (probed from `/open-in-app/apps`); the
|
|
11
|
+
* id → `.app` name map below mirrors the official catalog's darwin entries
|
|
12
|
+
* (packages/host/open-in-app/src/catalog.ts at 0.1.5-rc.1 — re-check on every
|
|
13
|
+
* host upgrade). Other platforms and unknown ids answer unsupported, and the
|
|
14
|
+
* client hides the gesture.
|
|
15
|
+
* @module @khorsheed/dsh-file-preview
|
|
16
|
+
*/
|
|
17
|
+
import { type NativeCommandRunner } from '@deepseek-ai/dsh-native-command';
|
|
18
|
+
/** Injectable platform facts for deterministic adapter tests. */
|
|
19
|
+
export interface OpenExternalInternals {
|
|
20
|
+
platform?: NodeJS.Platform;
|
|
21
|
+
run?: NativeCommandRunner;
|
|
22
|
+
}
|
|
23
|
+
/** The application name one catalog id maps to, or undefined when unknown. */
|
|
24
|
+
export declare function macAppName(appId: string): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Open one file in the named application. Throws when the platform is not
|
|
27
|
+
* macOS or the catalog id is unknown — the caller maps both to an
|
|
28
|
+
* `unsupported`/`unknown-app` answer and the client hides the gesture. A
|
|
29
|
+
* launch failure propagates as the runner's rejection.
|
|
30
|
+
* @param path - absolute path in the host's execution world (caller owns
|
|
31
|
+
* resolution, typically `FileSystem.processPath`).
|
|
32
|
+
* @param appId - official open-in-app catalog id (e.g. `cursor`).
|
|
33
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
34
|
+
* @param internals - platform seam for deterministic tests.
|
|
35
|
+
*/
|
|
36
|
+
export declare function openExternalNative(path: string, appId: string, signal: AbortSignal, internals?: OpenExternalInternals): Promise<void>;
|
|
37
|
+
//# sourceMappingURL=open-external.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native "open this file in a specific application" dispatch used by the
|
|
3
|
+
* file-preview Remote's `openExternal` method (the "open in IDE" gesture).
|
|
4
|
+
*
|
|
5
|
+
* The official open-in-app route (`POST /open-in-app/open`) only accepts
|
|
6
|
+
* directories — a file path 404s — so file-exact opens carry their own
|
|
7
|
+
* dispatch here. macOS only: `open -a <App> <path>` through the official
|
|
8
|
+
* `@deepseek-ai/dsh-native-command` runner (no shell, abort propagation),
|
|
9
|
+
* mirroring `reveal.ts`'s discipline. The browser picks the application by
|
|
10
|
+
* official open-in-app catalog id (probed from `/open-in-app/apps`); the
|
|
11
|
+
* id → `.app` name map below mirrors the official catalog's darwin entries
|
|
12
|
+
* (packages/host/open-in-app/src/catalog.ts at 0.1.5-rc.1 — re-check on every
|
|
13
|
+
* host upgrade). Other platforms and unknown ids answer unsupported, and the
|
|
14
|
+
* client hides the gesture.
|
|
15
|
+
* @module @khorsheed/dsh-file-preview
|
|
16
|
+
*/
|
|
17
|
+
import { runNativeCommand } from '@deepseek-ai/dsh-native-command';
|
|
18
|
+
/** Official catalog id → macOS application name (darwin entries of the official catalog). */
|
|
19
|
+
const MAC_APP_NAMES = {
|
|
20
|
+
cursor: 'Cursor',
|
|
21
|
+
vscode: 'Visual Studio Code',
|
|
22
|
+
vscodeinsiders: 'Visual Studio Code - Insiders',
|
|
23
|
+
windsurf: 'Windsurf',
|
|
24
|
+
zed: 'Zed',
|
|
25
|
+
sublimetext: 'Sublime Text',
|
|
26
|
+
xcode: 'Xcode',
|
|
27
|
+
androidstudio: 'Android Studio',
|
|
28
|
+
intellij: 'IntelliJ IDEA',
|
|
29
|
+
pycharm: 'PyCharm',
|
|
30
|
+
webstorm: 'WebStorm',
|
|
31
|
+
phpstorm: 'PhpStorm',
|
|
32
|
+
goland: 'GoLand',
|
|
33
|
+
rider: 'Rider',
|
|
34
|
+
rustrover: 'RustRover',
|
|
35
|
+
};
|
|
36
|
+
/** The application name one catalog id maps to, or undefined when unknown. */
|
|
37
|
+
export function macAppName(appId) {
|
|
38
|
+
return MAC_APP_NAMES[appId];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Open one file in the named application. Throws when the platform is not
|
|
42
|
+
* macOS or the catalog id is unknown — the caller maps both to an
|
|
43
|
+
* `unsupported`/`unknown-app` answer and the client hides the gesture. A
|
|
44
|
+
* launch failure propagates as the runner's rejection.
|
|
45
|
+
* @param path - absolute path in the host's execution world (caller owns
|
|
46
|
+
* resolution, typically `FileSystem.processPath`).
|
|
47
|
+
* @param appId - official open-in-app catalog id (e.g. `cursor`).
|
|
48
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
49
|
+
* @param internals - platform seam for deterministic tests.
|
|
50
|
+
*/
|
|
51
|
+
export async function openExternalNative(path, appId, signal, internals = {}) {
|
|
52
|
+
const platform = internals.platform ?? process.platform;
|
|
53
|
+
if (platform !== 'darwin') {
|
|
54
|
+
throw new Error(`open-in-app is unsupported on ${platform}`);
|
|
55
|
+
}
|
|
56
|
+
const app = macAppName(appId);
|
|
57
|
+
if (app === undefined)
|
|
58
|
+
throw new Error(`unknown application id "${appId}"`);
|
|
59
|
+
const run = internals.run ?? runNativeCommand;
|
|
60
|
+
await run('open', ['-a', app, path], signal);
|
|
61
|
+
}
|
package/lib/types/types.d.ts
CHANGED
|
@@ -95,6 +95,16 @@ export type FilePreviewReveal = {
|
|
|
95
95
|
/** Why: `missing` = the recorded path does not resolve to an existing target; `select-failed` = the host could not select the file. */
|
|
96
96
|
readonly reason: 'missing' | 'select-failed';
|
|
97
97
|
};
|
|
98
|
+
/** Outcome of a `filePreview.openExternal` call (the "open in IDE" gesture). */
|
|
99
|
+
export type FilePreviewOpenExternal = {
|
|
100
|
+
/** The application launched on the file. */
|
|
101
|
+
readonly opened: true;
|
|
102
|
+
} | {
|
|
103
|
+
/** The file could not be opened in the named application. */
|
|
104
|
+
readonly opened: false;
|
|
105
|
+
/** Why: `missing` = the recorded path does not resolve to an existing target; `unknown-app` = the catalog id is not mapped; `unsupported-platform` = the host is not macOS; `launch-failed` = the native launch rejected. */
|
|
106
|
+
readonly reason: 'missing' | 'unknown-app' | 'unsupported-platform' | 'launch-failed';
|
|
107
|
+
};
|
|
98
108
|
/** One file's mutation facts within one turn — the turn card's vocabulary. */
|
|
99
109
|
export interface FilePreviewTurnFile {
|
|
100
110
|
/** The display path (absolute for bash-captured writes; as-recorded otherwise). */
|
|
@@ -105,7 +115,7 @@ export interface FilePreviewTurnFile {
|
|
|
105
115
|
/** Lines added across the turn's mutations of this path. */
|
|
106
116
|
readonly added?: number;
|
|
107
117
|
/** Lines removed; 0 for a create; absent when an uncounted mutation (a write
|
|
108
|
-
* whose prior content was not diffable, a
|
|
118
|
+
* whose prior content was not diffable, a PTC dispatch) reported no
|
|
109
119
|
* total. */
|
|
110
120
|
readonly removed?: number;
|
|
111
121
|
}
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"cordis",
|
|
8
8
|
"file-preview"
|
|
9
9
|
],
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.3.0",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "lib/index.js",
|
|
13
13
|
"types": "lib/types/index.d.ts",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"lib/typert.remote-client.d.ts",
|
|
43
43
|
"lib/typert.remote-client.js",
|
|
44
44
|
"cordis.patch.yml",
|
|
45
|
-
"skills/**/*.md",
|
|
46
45
|
"LICENSE",
|
|
47
46
|
"CHANGELOG.md"
|
|
48
47
|
],
|
|
@@ -59,25 +58,25 @@
|
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|
|
61
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
62
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
63
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
64
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-loader-smoke": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-native-command": "^0.1.
|
|
68
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
69
|
-
"@deepseek-ai/dsh-skill": "^0.1.
|
|
70
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
71
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.
|
|
72
|
-
"@deepseek-ai/dsh-util-values": "^0.1.
|
|
61
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-fs": "^0.1.5-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-loader-smoke": "^0.1.5-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-native-command": "^0.1.5-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-skill": "^0.1.5-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-util-values": "^0.1.5-rc.1"
|
|
73
72
|
},
|
|
74
73
|
"dsh": {
|
|
75
74
|
"bundle": {
|
|
76
75
|
"patch": "./cordis.patch.yml"
|
|
77
76
|
},
|
|
78
77
|
"compat": {
|
|
79
|
-
"minHost": "0.1.
|
|
80
|
-
"verifiedHost": "0.1.
|
|
78
|
+
"minHost": "0.1.5-rc.1",
|
|
79
|
+
"verifiedHost": "0.1.5-rc.1"
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
}
|