@khorsheed/dsh-file-preview 0.1.1 → 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 +26 -0
- package/README.en.md +20 -13
- package/README.i18n.yaml +2 -2
- package/README.md +34 -27
- package/lib/index.js +238 -86
- package/lib/typert.host.js +249 -49
- package/lib/typert.remote-client.d.ts +6 -1
- package/lib/typert.remote-client.js +83 -5
- package/lib/types/bash-writes.js +1 -1
- package/lib/types/fold.d.ts +12 -9
- package/lib/types/fold.js +26 -15
- package/lib/types/index.d.ts +51 -10
- package/lib/types/index.js +131 -68
- 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 -14
- 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/index.js
CHANGED
|
@@ -35,54 +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 skillFile = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills', '3d-artifact', 'SKILL.md');
|
|
71
|
-
const raw = readFileSync(skillFile, 'utf8');
|
|
72
|
-
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(raw);
|
|
73
|
-
const name = /^name: (.+)$/m.exec(match?.[1] ?? '')?.[1]?.trim();
|
|
74
|
-
const description = /^description: (.+)$/m.exec(match?.[1] ?? '')?.[1]?.trim();
|
|
75
|
-
const content = match?.[2];
|
|
76
|
-
if (match === null || name === undefined || description === undefined || content === undefined) {
|
|
77
|
-
ctx.logger.warn('file-preview: shipped SKILL.md is malformed — the 3d-artifact skill is not registered');
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
ctx.effect(() => skills.register({ name, description, content, source: 'runtime' }));
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
ctx.logger.warn(`file-preview: shipped SKILL.md unreadable (${String(error)}) — the 3d-artifact skill is not registered`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
44
|
/** Extensions treated as binary without reading (their text decode is meaningless). */
|
|
87
45
|
const BINARY_EXTENSIONS = new Set([
|
|
88
46
|
'.pdf', '.zip', '.gz', '.tar', '.7z', '.zstd', '.br',
|
|
@@ -145,8 +103,9 @@ function readErrorMessage(error) {
|
|
|
145
103
|
}
|
|
146
104
|
/**
|
|
147
105
|
* The `filePreview` Remote service: session-scoped read-only file inspection
|
|
148
|
-
* for web surfaces. `list` is a
|
|
149
|
-
*
|
|
106
|
+
* for web surfaces. `list` is a fold over the session log, then filtered to the
|
|
107
|
+
* paths that still exist on disk (resolved against the session cwd and stat-ed
|
|
108
|
+
* per call); `read` resolves the path against the session cwd and serves current
|
|
150
109
|
* text content through `ctx.fs` (or a browser URL for images), capped by
|
|
151
110
|
* config. Image bytes ride a dedicated host route so the browser loads them
|
|
152
111
|
* natively without bloating the RPC channel.
|
|
@@ -154,21 +113,27 @@ function readErrorMessage(error) {
|
|
|
154
113
|
let FilePreviewService = (() => {
|
|
155
114
|
let _classSuper = TypertRemoteService;
|
|
156
115
|
let _instanceExtraInitializers = [];
|
|
116
|
+
let _capabilities_decorators;
|
|
157
117
|
let _list_decorators;
|
|
158
118
|
let _turnFiles_decorators;
|
|
159
119
|
let _read_decorators;
|
|
160
120
|
let _reveal_decorators;
|
|
121
|
+
let _openExternal_decorators;
|
|
161
122
|
return class FilePreviewService extends _classSuper {
|
|
162
123
|
static {
|
|
163
124
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
125
|
+
_capabilities_decorators = [Remote('capabilities')];
|
|
164
126
|
_list_decorators = [Remote('list')];
|
|
165
127
|
_turnFiles_decorators = [Remote('turnFiles')];
|
|
166
128
|
_read_decorators = [Remote('read')];
|
|
167
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);
|
|
168
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);
|
|
169
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);
|
|
170
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);
|
|
171
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);
|
|
172
137
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
173
138
|
}
|
|
174
139
|
static inject = ['fs'];
|
|
@@ -188,6 +153,8 @@ let FilePreviewService = (() => {
|
|
|
188
153
|
agents;
|
|
189
154
|
/** Native reveal runner; a constructor seam for deterministic tests. */
|
|
190
155
|
revealNative;
|
|
156
|
+
/** Native open-in-app runner; a constructor seam for deterministic tests. */
|
|
157
|
+
openExternalNative;
|
|
191
158
|
/** Bash-write collector (S2 seam), or undefined when disabled in config. */
|
|
192
159
|
collector;
|
|
193
160
|
/** Per-session by-turn fold cache, invalidated by the log watermark. */
|
|
@@ -208,6 +175,7 @@ let FilePreviewService = (() => {
|
|
|
208
175
|
captureBashWrites: config.captureBashWrites ?? true,
|
|
209
176
|
};
|
|
210
177
|
this.revealNative = deps.revealNative ?? revealNativePath;
|
|
178
|
+
this.openExternalNative = deps.openExternalNative ?? openExternalNative;
|
|
211
179
|
if (config.captureBashWrites !== false) {
|
|
212
180
|
this.collector = new BashWriteCollector({ fs: this.fs, maxFiles: this.resolved.maxFiles });
|
|
213
181
|
ctx.effect(() => this.collector.attach(ctx), 'file-preview: bash-write capture');
|
|
@@ -228,7 +196,10 @@ let FilePreviewService = (() => {
|
|
|
228
196
|
handler: (req, res) => { void this.serveImage(req, res); },
|
|
229
197
|
}), 'file-preview: image route');
|
|
230
198
|
}
|
|
231
|
-
|
|
199
|
+
}
|
|
200
|
+
/** Zero-session availability handshake for browser companions. */
|
|
201
|
+
capabilities() {
|
|
202
|
+
return { protocolVersion: 1 };
|
|
232
203
|
}
|
|
233
204
|
get fs() {
|
|
234
205
|
return this.ctx.fs;
|
|
@@ -310,48 +281,85 @@ let FilePreviewService = (() => {
|
|
|
310
281
|
});
|
|
311
282
|
res.end(Buffer.from(bytes));
|
|
312
283
|
}
|
|
284
|
+
/** Resolve the session's recorded display paths against its cwd and keep only
|
|
285
|
+
* those that currently exist as a regular file. The products surfaces fold
|
|
286
|
+
* the session log (which is history, not disk state): a path the log wrote
|
|
287
|
+
* may have been cleaned up since (a temp script the turn deleted), and such
|
|
288
|
+
* a path is not a product to list. Existence is checked fresh on every call
|
|
289
|
+
* (never cached), so a deletion that lands after a fold is visible on the
|
|
290
|
+
* next fetch. Unresolvable/unstat-able paths are treated as absent.
|
|
291
|
+
* @param agent - owning live agent; its session cwd anchors relative paths.
|
|
292
|
+
* @param paths - the display paths to probe.
|
|
293
|
+
* @returns the subset that resolves to an existing regular file.
|
|
294
|
+
*/
|
|
295
|
+
async retainExisting(agent, paths) {
|
|
296
|
+
if (paths.length === 0)
|
|
297
|
+
return new Set();
|
|
298
|
+
const cwd = agent.session.header.cwd;
|
|
299
|
+
const existing = new Set();
|
|
300
|
+
for (const path of paths) {
|
|
301
|
+
if (existing.has(path))
|
|
302
|
+
continue;
|
|
303
|
+
try {
|
|
304
|
+
const target = await this.fs.resolve(path, cwd === undefined ? {} : { cwd });
|
|
305
|
+
const info = await this.fs.stat(target);
|
|
306
|
+
if (info?.type === 'file')
|
|
307
|
+
existing.add(path);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Unresolvable or stat failed: the path is not present to list.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return existing;
|
|
314
|
+
}
|
|
313
315
|
/**
|
|
314
316
|
* List the files one session wrote or edited: the log fold plus any
|
|
315
317
|
* bash-written files the collector verified (the S2 seam), merged so a
|
|
316
|
-
* captured path the fold already knows keeps its log-derived entry.
|
|
318
|
+
* captured path the fold already knows keeps its log-derived entry. Only
|
|
319
|
+
* paths that currently exist on disk are returned — a file the session
|
|
320
|
+
* wrote and later deleted is history, not a product.
|
|
317
321
|
* @param agent - owning live agent; its session log is the data source.
|
|
318
|
-
* @returns first-seen files capped by `maxFiles`, with the log watermark.
|
|
322
|
+
* @returns existing first-seen files capped by `maxFiles`, with the log watermark.
|
|
319
323
|
*/
|
|
320
|
-
list(agent) {
|
|
321
|
-
const folded = foldFilePreview(agent.session.
|
|
324
|
+
async list(agent) {
|
|
325
|
+
const folded = foldFilePreview(agent.session.snapshotEvents(), this.resolved.maxFiles);
|
|
322
326
|
const captured = this.collector?.captured(agent.session.id);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
let entries = folded.entries;
|
|
328
|
+
if (captured !== undefined && captured.size > 0) {
|
|
329
|
+
const seen = new Set(entries.map(entry => entry.path));
|
|
330
|
+
const extra = [];
|
|
331
|
+
for (const [path, write] of captured) {
|
|
332
|
+
if (seen.has(path) || entries.length + extra.length >= this.resolved.maxFiles)
|
|
333
|
+
continue;
|
|
334
|
+
extra.push({ path, op: 'write', seq: write.seq, turn: write.turn, step: write.step, diffs: [] });
|
|
335
|
+
}
|
|
336
|
+
entries = [...entries, ...extra];
|
|
331
337
|
}
|
|
332
|
-
|
|
333
|
-
return folded;
|
|
338
|
+
const existing = await this.retainExisting(agent, entries.map(entry => entry.path));
|
|
334
339
|
return {
|
|
335
|
-
entries:
|
|
340
|
+
entries: entries.filter(entry => existing.has(entry.path)),
|
|
336
341
|
asOfSeq: folded.asOfSeq,
|
|
337
|
-
truncated: folded.truncated ||
|
|
342
|
+
truncated: folded.truncated || entries.length >= this.resolved.maxFiles,
|
|
338
343
|
};
|
|
339
344
|
}
|
|
340
345
|
/**
|
|
341
346
|
* Every turn's file mutations for the turn-tail card — the single source of
|
|
342
347
|
* truth the card renders (the client's old write/edit-only fold retired).
|
|
343
|
-
* Built by folding the session log per turn (write/edit calls,
|
|
348
|
+
* Built by folding the session log per turn (write/edit calls, PTC
|
|
344
349
|
* dispatches borrowing the root call's turn, and render-intent paths from
|
|
345
350
|
* result diff meta) and merging the collector's bash-written captures by
|
|
346
351
|
* their own turn. The fold is cached per session and invalidated by the log
|
|
347
352
|
* watermark, so repeated card fetches do not refold. A path touched in two
|
|
348
|
-
* turns appears in BOTH turn groups — per-turn attribution is exact.
|
|
353
|
+
* turns appears in BOTH turn groups — per-turn attribution is exact. Only
|
|
354
|
+
* paths that currently exist on disk are returned (a deleted temp script is
|
|
355
|
+
* history, not a product); existence is probed fresh on every call, so the
|
|
356
|
+
* cached fold is filtered per request rather than cached.
|
|
349
357
|
* @param agent - owning live agent; its session log is the data source.
|
|
350
|
-
* @returns per-turn file groups plus the scanned watermark.
|
|
358
|
+
* @returns existing per-turn file groups plus the scanned watermark.
|
|
351
359
|
*/
|
|
352
|
-
turnFiles(agent) {
|
|
360
|
+
async turnFiles(agent) {
|
|
353
361
|
const session = agent.session;
|
|
354
|
-
const events = session.
|
|
362
|
+
const events = session.snapshotEvents();
|
|
355
363
|
const asOfSeq = events.length === 0 ? -1 : events[events.length - 1].seq;
|
|
356
364
|
const cached = this.turnCache.get(session.id);
|
|
357
365
|
let byTurn;
|
|
@@ -386,7 +394,15 @@ let FilePreviewService = (() => {
|
|
|
386
394
|
continue;
|
|
387
395
|
turns.push({ turn, files: files.sort((a, b) => a.seq - b.seq) });
|
|
388
396
|
}
|
|
389
|
-
|
|
397
|
+
// Filter the assembled groups to paths that still exist on disk (fresh per
|
|
398
|
+
// call — the fold cache is by log watermark, existence moves independently).
|
|
399
|
+
const existing = await this.retainExisting(agent, turns.flatMap(group => group.files.map(file => file.path)));
|
|
400
|
+
return {
|
|
401
|
+
turns: turns
|
|
402
|
+
.map(group => ({ turn: group.turn, files: group.files.filter(file => existing.has(file.path)) }))
|
|
403
|
+
.filter(group => group.files.length > 0),
|
|
404
|
+
asOfSeq,
|
|
405
|
+
};
|
|
390
406
|
}
|
|
391
407
|
/**
|
|
392
408
|
* Read the current content of one recorded file, resolved against the
|
|
@@ -511,6 +527,53 @@ let FilePreviewService = (() => {
|
|
|
511
527
|
return { revealed: false, reason: 'select-failed' };
|
|
512
528
|
}
|
|
513
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
|
+
}
|
|
514
577
|
};
|
|
515
578
|
})();
|
|
516
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,24 +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.
|
|
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"
|
|
72
72
|
},
|
|
73
73
|
"dsh": {
|
|
74
74
|
"bundle": {
|
|
75
75
|
"patch": "./cordis.patch.yml"
|
|
76
76
|
},
|
|
77
77
|
"compat": {
|
|
78
|
-
"minHost": "0.1.
|
|
79
|
-
"verifiedHost": "0.1.
|
|
78
|
+
"minHost": "0.1.5-rc.1",
|
|
79
|
+
"verifiedHost": "0.1.5-rc.1"
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|