@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/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 Code Mode file dispatches (`tool/code-dispatch` events, or
69
- * `tool/ptc-dispatch` on seats running the PTC rename) count too —
70
- * they carry no turn/step of their own, so the entry borrows the enclosing
71
- * root call's location.
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 code dispatches borrow their turn/step. */
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 (DISPATCH_EVENT_NAMES.has(event.type)) {
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. The PTC rename keeps
103
- // the code-dispatch payload shape (the ptc name has no SessionEventMap
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(dispatch.data.name, dispatch.data.arguments);
98
+ const target = targetFromArguments(event.data.name, event.data.arguments);
109
99
  if (target === undefined)
110
100
  continue;
111
- const site = callSites.get(String(dispatch.data.rootCallId)) ?? { turn: 0, step: 0 };
112
- record(target, dispatch.seq, site.turn, site.step);
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 Code Mode dispatches borrow the enclosing root call's
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 Code Mode dispatch, or 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 code dispatches borrow their turn/step. */
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
- * Code Mode dispatch): it poisons both fields. */
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 code dispatch's location via
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 (DISPATCH_EVENT_NAMES.has(event.type)) {
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. Code Mode writes are
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). Same payload
294
- // shape caveat as foldFilePreview above.
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(dispatch.data.name, dispatch.data.arguments);
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(dispatch.data.rootCallId)) ?? { turn: 0, step: 0 };
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, dispatch.seq, site.step, undefined, undefined, true);
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') {
@@ -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, Code Mode
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
@@ -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
- registerArtifactSkill(ctx);
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, Code Mode
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
+ }
@@ -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 Code Mode dispatch) reported no
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.2.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.2-rc.1",
63
- "@deepseek-ai/dsh-fs": "^0.1.2-rc.1",
64
- "@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
65
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
66
- "@deepseek-ai/dsh-loader-smoke": "^0.1.2-rc.1",
67
- "@deepseek-ai/dsh-native-command": "^0.1.2-rc.1",
68
- "@deepseek-ai/dsh-session": "^0.1.2-rc.1",
69
- "@deepseek-ai/dsh-skill": "^0.1.2-rc.1",
70
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
71
- "@deepseek-ai/dsh-typert-protocol": "^0.1.2-rc.1",
72
- "@deepseek-ai/dsh-util-values": "^0.1.2-rc.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.2-rc.1",
80
- "verifiedHost": "0.1.2-rc.1"
78
+ "minHost": "0.1.5-rc.1",
79
+ "verifiedHost": "0.1.5-rc.1"
81
80
  }
82
81
  }
83
82
  }