@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/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
1
|
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
5
2
|
import z from "@deepseek-ai/schemastery";
|
|
6
3
|
import { homedir, release } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
7
5
|
import { runNativeCommand } from "@deepseek-ai/dsh-native-command";
|
|
8
6
|
//#region lib/types/bash-writes.js
|
|
9
7
|
/**
|
|
@@ -271,7 +269,7 @@ var BashWriteCollector = class {
|
|
|
271
269
|
#replay(session) {
|
|
272
270
|
this.#pending.set(session.id, /* @__PURE__ */ new Map());
|
|
273
271
|
this.#captured.set(session.id, /* @__PURE__ */ new Map());
|
|
274
|
-
for (const event of session.
|
|
272
|
+
for (const event of session.snapshotEvents()) this.#onEvent(session, event);
|
|
275
273
|
}
|
|
276
274
|
#onEvent(session, event) {
|
|
277
275
|
if (event.type === "tool/call") {
|
|
@@ -381,9 +379,11 @@ function diffsFromResultMeta(meta) {
|
|
|
381
379
|
* first-seen order and deduplicated by display path; a later occurrence
|
|
382
380
|
* refreshes the entry's op and location without moving it, and each write/edit
|
|
383
381
|
* tool result appends its change's diff to the entry's `diffs` in event order.
|
|
384
|
-
* Nested
|
|
385
|
-
*
|
|
386
|
-
*
|
|
382
|
+
* Nested PTC file dispatches (`tool/ptc-dispatch` events — the host's Code
|
|
383
|
+
* Mode name through 0.1.2, renamed with the 0.1.5 format; the v2→v3 log
|
|
384
|
+
* migration renames persisted rows, so only the ptc name is matched) count
|
|
385
|
+
* too — they carry no turn/step of their own, so the entry borrows the
|
|
386
|
+
* enclosing root call's location.
|
|
387
387
|
* @param events - the session's events in ascending seq order.
|
|
388
388
|
* @param maxFiles - cap on returned entries; excess files are dropped in
|
|
389
389
|
* first-seen order after the cap is reached.
|
|
@@ -391,7 +391,7 @@ function diffsFromResultMeta(meta) {
|
|
|
391
391
|
*/
|
|
392
392
|
function foldFilePreview(events, maxFiles) {
|
|
393
393
|
const entries = /* @__PURE__ */ new Map();
|
|
394
|
-
/** Root tool/call locations, so nested
|
|
394
|
+
/** Root tool/call locations, so nested dispatches borrow their turn/step. */
|
|
395
395
|
const callSites = /* @__PURE__ */ new Map();
|
|
396
396
|
let asOfSeq = -1;
|
|
397
397
|
/** Upsert one touched path, refreshing a repeat in place and keeping its diffs. */
|
|
@@ -424,7 +424,7 @@ function foldFilePreview(events, maxFiles) {
|
|
|
424
424
|
record(target, event.seq, event.data.turn, event.data.step);
|
|
425
425
|
continue;
|
|
426
426
|
}
|
|
427
|
-
if (event.type === "tool/
|
|
427
|
+
if (event.type === "tool/ptc-dispatch") {
|
|
428
428
|
if (event.data.isError) continue;
|
|
429
429
|
const target = targetFromArguments(event.data.name, event.data.arguments);
|
|
430
430
|
if (target === void 0) continue;
|
|
@@ -508,11 +508,12 @@ function resultCallFacts(message) {
|
|
|
508
508
|
* single source of truth. Unlike {@link foldFilePreview}, this does NOT dedupe
|
|
509
509
|
* a path to its last occurrence: a file touched in two turns appears in BOTH
|
|
510
510
|
* turn maps, so each turn's card lists exactly what that turn mutated.
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
511
|
+
* Reads leave no card entry (the card is the mutation vocabulary — write/edit
|
|
512
|
+
* only), so a pure-read call never registers; write/edit calls register with
|
|
513
|
+
* their own turn; nested PTC dispatches borrow the enclosing root call's
|
|
514
|
+
* turn (dispatch events carry none); a `tool/result` whose presentation meta
|
|
515
|
+
* carries diffs registers its paths (the render-intent vocabulary) with the
|
|
516
|
+
* result's turn, and per-turn line deltas are summed from the diffs. A `write` whose result carries no diffs is a
|
|
516
517
|
* create (the write tool records no diffs when the file had no prior content):
|
|
517
518
|
* its full written content — read from the call arguments, which the log keeps
|
|
518
519
|
* — is the added-line count, once per file per turn.
|
|
@@ -520,7 +521,7 @@ function resultCallFacts(message) {
|
|
|
520
521
|
* Count discipline: a write/edit tool/call only registers the path's location
|
|
521
522
|
* and never touches counts (its counted result follows); the first count
|
|
522
523
|
* contribution for a turn+path is adopted verbatim, later ones accumulate per
|
|
523
|
-
* field with unknown-wins — an uncounted mutation (a
|
|
524
|
+
* field with unknown-wins — an uncounted mutation (a PTC dispatch, or a
|
|
524
525
|
* diff whose side reports no prior content) poisons that field to undefined
|
|
525
526
|
* rather than reporting a misleading partial.
|
|
526
527
|
* @param events - the session's events in ascending seq order.
|
|
@@ -528,7 +529,7 @@ function resultCallFacts(message) {
|
|
|
528
529
|
*/
|
|
529
530
|
function foldFilePreviewByTurn(events) {
|
|
530
531
|
const byTurn = /* @__PURE__ */ new Map();
|
|
531
|
-
/** Root tool/call locations, so nested
|
|
532
|
+
/** Root tool/call locations, so nested dispatches borrow their turn/step. */
|
|
532
533
|
const callSites = /* @__PURE__ */ new Map();
|
|
533
534
|
/** Write-tool calls keyed by callId (create fallback; see {@link WriteCallFacts}). */
|
|
534
535
|
const writeCalls = /* @__PURE__ */ new Map();
|
|
@@ -566,7 +567,7 @@ function foldFilePreviewByTurn(events) {
|
|
|
566
567
|
* a turn+path is adopted verbatim (a count-less placeholder from a
|
|
567
568
|
* preceding tool/call must not poison it); later contributions accumulate
|
|
568
569
|
* per field with unknown-wins. `unknown` marks an uncountable mutation (a
|
|
569
|
-
*
|
|
570
|
+
* PTC dispatch): it poisons both fields. */
|
|
570
571
|
const recordContribution = (path, turn, seq, step, added, removed, unknown) => {
|
|
571
572
|
const map = turnMap(turn);
|
|
572
573
|
const key = countKey(turn, path);
|
|
@@ -609,6 +610,7 @@ function foldFilePreviewByTurn(events) {
|
|
|
609
610
|
});
|
|
610
611
|
const target = pathFromToolCall(event.data.name, event.data.arguments);
|
|
611
612
|
if (target === void 0) continue;
|
|
613
|
+
if (target.op === "read") continue;
|
|
612
614
|
if (target.op === "write") {
|
|
613
615
|
const content = contentFromWriteCall(event.data.arguments);
|
|
614
616
|
if (content !== void 0) {
|
|
@@ -624,10 +626,11 @@ function foldFilePreviewByTurn(events) {
|
|
|
624
626
|
recordLocation(target.path, event.data.turn, event.seq, event.data.step);
|
|
625
627
|
continue;
|
|
626
628
|
}
|
|
627
|
-
if (event.type === "tool/
|
|
629
|
+
if (event.type === "tool/ptc-dispatch") {
|
|
628
630
|
if (event.data.isError) continue;
|
|
629
631
|
const target = targetFromArguments(event.data.name, event.data.arguments);
|
|
630
632
|
if (target === void 0) continue;
|
|
633
|
+
if (target.op === "read") continue;
|
|
631
634
|
const site = callSites.get(String(event.data.rootCallId)) ?? {
|
|
632
635
|
turn: 0,
|
|
633
636
|
step: 0
|
|
@@ -654,6 +657,68 @@ function foldFilePreviewByTurn(events) {
|
|
|
654
657
|
return byTurn;
|
|
655
658
|
}
|
|
656
659
|
//#endregion
|
|
660
|
+
//#region lib/types/open-external.js
|
|
661
|
+
/**
|
|
662
|
+
* Native "open this file in a specific application" dispatch used by the
|
|
663
|
+
* file-preview Remote's `openExternal` method (the "open in IDE" gesture).
|
|
664
|
+
*
|
|
665
|
+
* The official open-in-app route (`POST /open-in-app/open`) only accepts
|
|
666
|
+
* directories — a file path 404s — so file-exact opens carry their own
|
|
667
|
+
* dispatch here. macOS only: `open -a <App> <path>` through the official
|
|
668
|
+
* `@deepseek-ai/dsh-native-command` runner (no shell, abort propagation),
|
|
669
|
+
* mirroring `reveal.ts`'s discipline. The browser picks the application by
|
|
670
|
+
* official open-in-app catalog id (probed from `/open-in-app/apps`); the
|
|
671
|
+
* id → `.app` name map below mirrors the official catalog's darwin entries
|
|
672
|
+
* (packages/host/open-in-app/src/catalog.ts at 0.1.5-rc.1 — re-check on every
|
|
673
|
+
* host upgrade). Other platforms and unknown ids answer unsupported, and the
|
|
674
|
+
* client hides the gesture.
|
|
675
|
+
* @module @khorsheed/dsh-file-preview
|
|
676
|
+
*/
|
|
677
|
+
/** Official catalog id → macOS application name (darwin entries of the official catalog). */
|
|
678
|
+
const MAC_APP_NAMES = {
|
|
679
|
+
cursor: "Cursor",
|
|
680
|
+
vscode: "Visual Studio Code",
|
|
681
|
+
vscodeinsiders: "Visual Studio Code - Insiders",
|
|
682
|
+
windsurf: "Windsurf",
|
|
683
|
+
zed: "Zed",
|
|
684
|
+
sublimetext: "Sublime Text",
|
|
685
|
+
xcode: "Xcode",
|
|
686
|
+
androidstudio: "Android Studio",
|
|
687
|
+
intellij: "IntelliJ IDEA",
|
|
688
|
+
pycharm: "PyCharm",
|
|
689
|
+
webstorm: "WebStorm",
|
|
690
|
+
phpstorm: "PhpStorm",
|
|
691
|
+
goland: "GoLand",
|
|
692
|
+
rider: "Rider",
|
|
693
|
+
rustrover: "RustRover"
|
|
694
|
+
};
|
|
695
|
+
/** The application name one catalog id maps to, or undefined when unknown. */
|
|
696
|
+
function macAppName(appId) {
|
|
697
|
+
return MAC_APP_NAMES[appId];
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Open one file in the named application. Throws when the platform is not
|
|
701
|
+
* macOS or the catalog id is unknown — the caller maps both to an
|
|
702
|
+
* `unsupported`/`unknown-app` answer and the client hides the gesture. A
|
|
703
|
+
* launch failure propagates as the runner's rejection.
|
|
704
|
+
* @param path - absolute path in the host's execution world (caller owns
|
|
705
|
+
* resolution, typically `FileSystem.processPath`).
|
|
706
|
+
* @param appId - official open-in-app catalog id (e.g. `cursor`).
|
|
707
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
708
|
+
* @param internals - platform seam for deterministic tests.
|
|
709
|
+
*/
|
|
710
|
+
async function openExternalNative(path, appId, signal, internals = {}) {
|
|
711
|
+
const platform = internals.platform ?? process.platform;
|
|
712
|
+
if (platform !== "darwin") throw new Error(`open-in-app is unsupported on ${platform}`);
|
|
713
|
+
const app = macAppName(appId);
|
|
714
|
+
if (app === void 0) throw new Error(`unknown application id "${appId}"`);
|
|
715
|
+
await (internals.run ?? runNativeCommand)("open", [
|
|
716
|
+
"-a",
|
|
717
|
+
app,
|
|
718
|
+
path
|
|
719
|
+
], signal);
|
|
720
|
+
}
|
|
721
|
+
//#endregion
|
|
657
722
|
//#region lib/types/reveal.js
|
|
658
723
|
/**
|
|
659
724
|
* Cross-platform native "show in folder, selecting the file" dispatch used by
|
|
@@ -771,45 +836,6 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
|
|
|
771
836
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
772
837
|
done = true;
|
|
773
838
|
};
|
|
774
|
-
/**
|
|
775
|
-
* Register the 3d-artifact skill — the generation-side contract for
|
|
776
|
-
* sandbox-runnable interactive 3D / digital-twin single-file HTML (self-
|
|
777
|
-
* contained, zero runtime network, GLB-inline zero-fetch models). Pull-based
|
|
778
|
-
* discovery: an agent whose task involves generating such an HTML page finds
|
|
779
|
-
* the contract through the skill catalog — no per-session push notice.
|
|
780
|
-
* Optional: compositions without the skill capability skip the registration.
|
|
781
|
-
* A missing/malformed shipped SKILL.md degrades to a warning — a discovery
|
|
782
|
-
* aid must never take a boot down; the pack-smoke test owns the file's
|
|
783
|
-
* presence in the tarball.
|
|
784
|
-
* @param ctx - plugin context.
|
|
785
|
-
*/
|
|
786
|
-
function registerArtifactSkill(ctx) {
|
|
787
|
-
const skills = ctx.get("skills");
|
|
788
|
-
if (skills === void 0) {
|
|
789
|
-
ctx.logger.warn("file-preview: skills capability absent — the 3d-artifact skill is not registered");
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
try {
|
|
793
|
-
const skillFile = join(dirname(fileURLToPath(import.meta.url)), "..", "skills", "3d-artifact", "SKILL.md");
|
|
794
|
-
const raw = readFileSync(skillFile, "utf8");
|
|
795
|
-
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(raw);
|
|
796
|
-
const name = /^name: (.+)$/m.exec(match?.[1] ?? "")?.[1]?.trim();
|
|
797
|
-
const description = /^description: (.+)$/m.exec(match?.[1] ?? "")?.[1]?.trim();
|
|
798
|
-
const content = match?.[2];
|
|
799
|
-
if (match === null || name === void 0 || description === void 0 || content === void 0) {
|
|
800
|
-
ctx.logger.warn("file-preview: shipped SKILL.md is malformed — the 3d-artifact skill is not registered");
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
ctx.effect(() => skills.register({
|
|
804
|
-
name,
|
|
805
|
-
description,
|
|
806
|
-
content,
|
|
807
|
-
source: "runtime"
|
|
808
|
-
}));
|
|
809
|
-
} catch (error) {
|
|
810
|
-
ctx.logger.warn(`file-preview: shipped SKILL.md unreadable (${String(error)}) — the 3d-artifact skill is not registered`);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
839
|
/** Extensions treated as binary without reading (their text decode is meaningless). */
|
|
814
840
|
const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
815
841
|
".pdf",
|
|
@@ -900,8 +926,9 @@ function readErrorMessage(error) {
|
|
|
900
926
|
}
|
|
901
927
|
/**
|
|
902
928
|
* The `filePreview` Remote service: session-scoped read-only file inspection
|
|
903
|
-
* for web surfaces. `list` is a
|
|
904
|
-
*
|
|
929
|
+
* for web surfaces. `list` is a fold over the session log, then filtered to the
|
|
930
|
+
* paths that still exist on disk (resolved against the session cwd and stat-ed
|
|
931
|
+
* per call); `read` resolves the path against the session cwd and serves current
|
|
905
932
|
* text content through `ctx.fs` (or a browser URL for images), capped by
|
|
906
933
|
* config. Image bytes ride a dedicated host route so the browser loads them
|
|
907
934
|
* natively without bloating the RPC channel.
|
|
@@ -909,17 +936,32 @@ function readErrorMessage(error) {
|
|
|
909
936
|
let FilePreviewService = (() => {
|
|
910
937
|
let _classSuper = TypertRemoteService;
|
|
911
938
|
let _instanceExtraInitializers = [];
|
|
939
|
+
let _capabilities_decorators;
|
|
912
940
|
let _list_decorators;
|
|
913
941
|
let _turnFiles_decorators;
|
|
914
942
|
let _read_decorators;
|
|
915
943
|
let _reveal_decorators;
|
|
944
|
+
let _openExternal_decorators;
|
|
916
945
|
return class FilePreviewService extends _classSuper {
|
|
917
946
|
static {
|
|
918
947
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
948
|
+
_capabilities_decorators = [Remote("capabilities")];
|
|
919
949
|
_list_decorators = [Remote("list")];
|
|
920
950
|
_turnFiles_decorators = [Remote("turnFiles")];
|
|
921
951
|
_read_decorators = [Remote("read")];
|
|
922
952
|
_reveal_decorators = [Remote("reveal")];
|
|
953
|
+
_openExternal_decorators = [Remote("openExternal")];
|
|
954
|
+
__esDecorate(this, null, _capabilities_decorators, {
|
|
955
|
+
kind: "method",
|
|
956
|
+
name: "capabilities",
|
|
957
|
+
static: false,
|
|
958
|
+
private: false,
|
|
959
|
+
access: {
|
|
960
|
+
has: (obj) => "capabilities" in obj,
|
|
961
|
+
get: (obj) => obj.capabilities
|
|
962
|
+
},
|
|
963
|
+
metadata: _metadata
|
|
964
|
+
}, null, _instanceExtraInitializers);
|
|
923
965
|
__esDecorate(this, null, _list_decorators, {
|
|
924
966
|
kind: "method",
|
|
925
967
|
name: "list",
|
|
@@ -964,6 +1006,17 @@ let FilePreviewService = (() => {
|
|
|
964
1006
|
},
|
|
965
1007
|
metadata: _metadata
|
|
966
1008
|
}, null, _instanceExtraInitializers);
|
|
1009
|
+
__esDecorate(this, null, _openExternal_decorators, {
|
|
1010
|
+
kind: "method",
|
|
1011
|
+
name: "openExternal",
|
|
1012
|
+
static: false,
|
|
1013
|
+
private: false,
|
|
1014
|
+
access: {
|
|
1015
|
+
has: (obj) => "openExternal" in obj,
|
|
1016
|
+
get: (obj) => obj.openExternal
|
|
1017
|
+
},
|
|
1018
|
+
metadata: _metadata
|
|
1019
|
+
}, null, _instanceExtraInitializers);
|
|
967
1020
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
968
1021
|
enumerable: true,
|
|
969
1022
|
configurable: true,
|
|
@@ -988,6 +1041,8 @@ let FilePreviewService = (() => {
|
|
|
988
1041
|
agents;
|
|
989
1042
|
/** Native reveal runner; a constructor seam for deterministic tests. */
|
|
990
1043
|
revealNative;
|
|
1044
|
+
/** Native open-in-app runner; a constructor seam for deterministic tests. */
|
|
1045
|
+
openExternalNative;
|
|
991
1046
|
/** Bash-write collector (S2 seam), or undefined when disabled in config. */
|
|
992
1047
|
collector;
|
|
993
1048
|
/** Per-session by-turn fold cache, invalidated by the log watermark. */
|
|
@@ -1008,6 +1063,7 @@ let FilePreviewService = (() => {
|
|
|
1008
1063
|
captureBashWrites: config.captureBashWrites ?? true
|
|
1009
1064
|
};
|
|
1010
1065
|
this.revealNative = deps.revealNative ?? revealNativePath;
|
|
1066
|
+
this.openExternalNative = deps.openExternalNative ?? openExternalNative;
|
|
1011
1067
|
if (config.captureBashWrites !== false) {
|
|
1012
1068
|
this.collector = new BashWriteCollector({
|
|
1013
1069
|
fs: this.fs,
|
|
@@ -1028,7 +1084,10 @@ let FilePreviewService = (() => {
|
|
|
1028
1084
|
this.serveImage(req, res);
|
|
1029
1085
|
}
|
|
1030
1086
|
}), "file-preview: image route");
|
|
1031
|
-
|
|
1087
|
+
}
|
|
1088
|
+
/** Zero-session availability handshake for browser companions. */
|
|
1089
|
+
capabilities() {
|
|
1090
|
+
return { protocolVersion: 1 };
|
|
1032
1091
|
}
|
|
1033
1092
|
get fs() {
|
|
1034
1093
|
return this.ctx.fs;
|
|
@@ -1104,52 +1163,84 @@ let FilePreviewService = (() => {
|
|
|
1104
1163
|
});
|
|
1105
1164
|
res.end(Buffer.from(bytes));
|
|
1106
1165
|
}
|
|
1166
|
+
/** Resolve the session's recorded display paths against its cwd and keep only
|
|
1167
|
+
* those that currently exist as a regular file. The products surfaces fold
|
|
1168
|
+
* the session log (which is history, not disk state): a path the log wrote
|
|
1169
|
+
* may have been cleaned up since (a temp script the turn deleted), and such
|
|
1170
|
+
* a path is not a product to list. Existence is checked fresh on every call
|
|
1171
|
+
* (never cached), so a deletion that lands after a fold is visible on the
|
|
1172
|
+
* next fetch. Unresolvable/unstat-able paths are treated as absent.
|
|
1173
|
+
* @param agent - owning live agent; its session cwd anchors relative paths.
|
|
1174
|
+
* @param paths - the display paths to probe.
|
|
1175
|
+
* @returns the subset that resolves to an existing regular file.
|
|
1176
|
+
*/
|
|
1177
|
+
async retainExisting(agent, paths) {
|
|
1178
|
+
if (paths.length === 0) return /* @__PURE__ */ new Set();
|
|
1179
|
+
const cwd = agent.session.header.cwd;
|
|
1180
|
+
const existing = /* @__PURE__ */ new Set();
|
|
1181
|
+
for (const path of paths) {
|
|
1182
|
+
if (existing.has(path)) continue;
|
|
1183
|
+
try {
|
|
1184
|
+
const target = await this.fs.resolve(path, cwd === void 0 ? {} : { cwd });
|
|
1185
|
+
if ((await this.fs.stat(target))?.type === "file") existing.add(path);
|
|
1186
|
+
} catch {}
|
|
1187
|
+
}
|
|
1188
|
+
return existing;
|
|
1189
|
+
}
|
|
1107
1190
|
/**
|
|
1108
1191
|
* List the files one session wrote or edited: the log fold plus any
|
|
1109
1192
|
* bash-written files the collector verified (the S2 seam), merged so a
|
|
1110
|
-
* captured path the fold already knows keeps its log-derived entry.
|
|
1193
|
+
* captured path the fold already knows keeps its log-derived entry. Only
|
|
1194
|
+
* paths that currently exist on disk are returned — a file the session
|
|
1195
|
+
* wrote and later deleted is history, not a product.
|
|
1111
1196
|
* @param agent - owning live agent; its session log is the data source.
|
|
1112
|
-
* @returns first-seen files capped by `maxFiles`, with the log watermark.
|
|
1197
|
+
* @returns existing first-seen files capped by `maxFiles`, with the log watermark.
|
|
1113
1198
|
*/
|
|
1114
|
-
list(agent) {
|
|
1115
|
-
const folded = foldFilePreview(agent.session.
|
|
1199
|
+
async list(agent) {
|
|
1200
|
+
const folded = foldFilePreview(agent.session.snapshotEvents(), this.resolved.maxFiles);
|
|
1116
1201
|
const captured = this.collector?.captured(agent.session.id);
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1202
|
+
let entries = folded.entries;
|
|
1203
|
+
if (captured !== void 0 && captured.size > 0) {
|
|
1204
|
+
const seen = new Set(entries.map((entry) => entry.path));
|
|
1205
|
+
const extra = [];
|
|
1206
|
+
for (const [path, write] of captured) {
|
|
1207
|
+
if (seen.has(path) || entries.length + extra.length >= this.resolved.maxFiles) continue;
|
|
1208
|
+
extra.push({
|
|
1209
|
+
path,
|
|
1210
|
+
op: "write",
|
|
1211
|
+
seq: write.seq,
|
|
1212
|
+
turn: write.turn,
|
|
1213
|
+
step: write.step,
|
|
1214
|
+
diffs: []
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
entries = [...entries, ...extra];
|
|
1130
1218
|
}
|
|
1131
|
-
|
|
1219
|
+
const existing = await this.retainExisting(agent, entries.map((entry) => entry.path));
|
|
1132
1220
|
return {
|
|
1133
|
-
entries:
|
|
1221
|
+
entries: entries.filter((entry) => existing.has(entry.path)),
|
|
1134
1222
|
asOfSeq: folded.asOfSeq,
|
|
1135
|
-
truncated: folded.truncated ||
|
|
1223
|
+
truncated: folded.truncated || entries.length >= this.resolved.maxFiles
|
|
1136
1224
|
};
|
|
1137
1225
|
}
|
|
1138
1226
|
/**
|
|
1139
1227
|
* Every turn's file mutations for the turn-tail card — the single source of
|
|
1140
1228
|
* truth the card renders (the client's old write/edit-only fold retired).
|
|
1141
|
-
* Built by folding the session log per turn (write/edit calls,
|
|
1229
|
+
* Built by folding the session log per turn (write/edit calls, PTC
|
|
1142
1230
|
* dispatches borrowing the root call's turn, and render-intent paths from
|
|
1143
1231
|
* result diff meta) and merging the collector's bash-written captures by
|
|
1144
1232
|
* their own turn. The fold is cached per session and invalidated by the log
|
|
1145
1233
|
* watermark, so repeated card fetches do not refold. A path touched in two
|
|
1146
|
-
* turns appears in BOTH turn groups — per-turn attribution is exact.
|
|
1234
|
+
* turns appears in BOTH turn groups — per-turn attribution is exact. Only
|
|
1235
|
+
* paths that currently exist on disk are returned (a deleted temp script is
|
|
1236
|
+
* history, not a product); existence is probed fresh on every call, so the
|
|
1237
|
+
* cached fold is filtered per request rather than cached.
|
|
1147
1238
|
* @param agent - owning live agent; its session log is the data source.
|
|
1148
|
-
* @returns per-turn file groups plus the scanned watermark.
|
|
1239
|
+
* @returns existing per-turn file groups plus the scanned watermark.
|
|
1149
1240
|
*/
|
|
1150
|
-
turnFiles(agent) {
|
|
1241
|
+
async turnFiles(agent) {
|
|
1151
1242
|
const session = agent.session;
|
|
1152
|
-
const events = session.
|
|
1243
|
+
const events = session.snapshotEvents();
|
|
1153
1244
|
const asOfSeq = events.length === 0 ? -1 : events[events.length - 1].seq;
|
|
1154
1245
|
const cached = this.turnCache.get(session.id);
|
|
1155
1246
|
let byTurn;
|
|
@@ -1182,8 +1273,12 @@ let FilePreviewService = (() => {
|
|
|
1182
1273
|
files: files.sort((a, b) => a.seq - b.seq)
|
|
1183
1274
|
});
|
|
1184
1275
|
}
|
|
1276
|
+
const existing = await this.retainExisting(agent, turns.flatMap((group) => group.files.map((file) => file.path)));
|
|
1185
1277
|
return {
|
|
1186
|
-
turns
|
|
1278
|
+
turns: turns.map((group) => ({
|
|
1279
|
+
turn: group.turn,
|
|
1280
|
+
files: group.files.filter((file) => existing.has(file.path))
|
|
1281
|
+
})).filter((group) => group.files.length > 0),
|
|
1187
1282
|
asOfSeq
|
|
1188
1283
|
};
|
|
1189
1284
|
}
|
|
@@ -1333,6 +1428,63 @@ let FilePreviewService = (() => {
|
|
|
1333
1428
|
};
|
|
1334
1429
|
}
|
|
1335
1430
|
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Open one recorded file in a specific host application (the "open in IDE"
|
|
1433
|
+
* gesture). The official open-in-app route only accepts directories — a
|
|
1434
|
+
* file path 404s — so file-exact opens go through this method: macOS
|
|
1435
|
+
* `open -a <App> <path>` via the same native-command runner reveal uses.
|
|
1436
|
+
* The browser names the application by official open-in-app catalog id
|
|
1437
|
+
* (probed from `/open-in-app/apps`); the id → app-name map lives in
|
|
1438
|
+
* open-external.ts. Non-macOS hosts and unknown ids answer without
|
|
1439
|
+
* launching, and the client hides the gesture.
|
|
1440
|
+
* @param agent - owning live agent; its session cwd anchors relative paths.
|
|
1441
|
+
* @param path - the display path recorded by the read/write/edit tool call.
|
|
1442
|
+
* @param app - official open-in-app catalog id (e.g. `cursor`).
|
|
1443
|
+
* @param signal - cooperative cancellation from the calling UI request.
|
|
1444
|
+
* @returns whether the application launched, or why not.
|
|
1445
|
+
*/
|
|
1446
|
+
async openExternal(agent, path, app, signal) {
|
|
1447
|
+
if (typeof path !== "string" || path.length === 0 || typeof app !== "string" || app.length === 0) return {
|
|
1448
|
+
opened: false,
|
|
1449
|
+
reason: "missing"
|
|
1450
|
+
};
|
|
1451
|
+
if (macAppName(app) === void 0) return {
|
|
1452
|
+
opened: false,
|
|
1453
|
+
reason: "unknown-app"
|
|
1454
|
+
};
|
|
1455
|
+
const cwd = agent.session.header.cwd;
|
|
1456
|
+
let target;
|
|
1457
|
+
try {
|
|
1458
|
+
target = await this.fs.resolve(path, cwd === void 0 ? { signal } : {
|
|
1459
|
+
cwd,
|
|
1460
|
+
signal
|
|
1461
|
+
});
|
|
1462
|
+
} catch {
|
|
1463
|
+
return {
|
|
1464
|
+
opened: false,
|
|
1465
|
+
reason: "missing"
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
let info;
|
|
1469
|
+
try {
|
|
1470
|
+
info = await this.fs.stat(target, signal);
|
|
1471
|
+
} catch {
|
|
1472
|
+
info = void 0;
|
|
1473
|
+
}
|
|
1474
|
+
if (info === void 0) return {
|
|
1475
|
+
opened: false,
|
|
1476
|
+
reason: "missing"
|
|
1477
|
+
};
|
|
1478
|
+
try {
|
|
1479
|
+
await this.openExternalNative(this.fs.processPath(target), app, signal);
|
|
1480
|
+
return { opened: true };
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
return {
|
|
1483
|
+
opened: false,
|
|
1484
|
+
reason: (error instanceof Error ? error.message : String(error)).startsWith("open-in-app is unsupported on ") ? "unsupported-platform" : "launch-failed"
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1336
1488
|
};
|
|
1337
1489
|
})();
|
|
1338
1490
|
//#endregion
|