@stigmer/sdk 3.0.9-dev.20260615153829 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/execution/__tests__/approval-provenance.test.d.ts +2 -0
- package/execution/__tests__/approval-provenance.test.d.ts.map +1 -0
- package/execution/__tests__/approval-provenance.test.js +42 -0
- package/execution/__tests__/approval-provenance.test.js.map +1 -0
- package/execution/__tests__/file-review-fold.corpus.test.d.ts +2 -0
- package/execution/__tests__/file-review-fold.corpus.test.d.ts.map +1 -0
- package/execution/__tests__/file-review-fold.corpus.test.js +82 -0
- package/execution/__tests__/file-review-fold.corpus.test.js.map +1 -0
- package/execution/__tests__/file-review-fold.test.d.ts +2 -0
- package/execution/__tests__/file-review-fold.test.d.ts.map +1 -0
- package/execution/__tests__/file-review-fold.test.js +184 -0
- package/execution/__tests__/file-review-fold.test.js.map +1 -0
- package/execution/__tests__/to-display-file-change.test.d.ts +2 -0
- package/execution/__tests__/to-display-file-change.test.d.ts.map +1 -0
- package/execution/__tests__/to-display-file-change.test.js +69 -0
- package/execution/__tests__/to-display-file-change.test.js.map +1 -0
- package/execution/__tests__/tool-view.file-changes.test.d.ts +2 -0
- package/execution/__tests__/tool-view.file-changes.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.file-changes.test.js +86 -0
- package/execution/__tests__/tool-view.file-changes.test.js.map +1 -0
- package/execution/__tests__/tool-view.fixtures.test.js +3 -0
- package/execution/__tests__/tool-view.fixtures.test.js.map +1 -1
- package/execution/__tests__/tool-view.output-ref.test.d.ts +2 -0
- package/execution/__tests__/tool-view.output-ref.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.output-ref.test.js +64 -0
- package/execution/__tests__/tool-view.output-ref.test.js.map +1 -0
- package/execution/__tests__/tool-view.search.test.d.ts +2 -0
- package/execution/__tests__/tool-view.search.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.search.test.js +200 -0
- package/execution/__tests__/tool-view.search.test.js.map +1 -0
- package/execution/__tests__/tool-view.shell.test.d.ts +2 -0
- package/execution/__tests__/tool-view.shell.test.d.ts.map +1 -0
- package/execution/__tests__/tool-view.shell.test.js +104 -0
- package/execution/__tests__/tool-view.shell.test.js.map +1 -0
- package/execution/approval-provenance.d.ts +30 -0
- package/execution/approval-provenance.d.ts.map +1 -0
- package/execution/approval-provenance.js +73 -0
- package/execution/approval-provenance.js.map +1 -0
- package/execution/execution-phases.d.ts +7 -0
- package/execution/execution-phases.d.ts.map +1 -0
- package/execution/execution-phases.js +21 -0
- package/execution/execution-phases.js.map +1 -0
- package/execution/file-review-fold.d.ts +44 -0
- package/execution/file-review-fold.d.ts.map +1 -0
- package/execution/file-review-fold.js +182 -0
- package/execution/file-review-fold.js.map +1 -0
- package/execution/to-display-file-change.d.ts +14 -0
- package/execution/to-display-file-change.d.ts.map +1 -0
- package/execution/to-display-file-change.js +57 -0
- package/execution/to-display-file-change.js.map +1 -0
- package/execution/tool-view.d.ts +8 -2
- package/execution/tool-view.d.ts.map +1 -1
- package/execution/tool-view.js +205 -21
- package/execution/tool-view.js.map +1 -1
- package/gen/agentexecution.d.ts +2 -1
- package/gen/agentexecution.d.ts.map +1 -1
- package/gen/agentexecution.js +8 -0
- package/gen/agentexecution.js.map +1 -1
- package/gen/mcpserver.d.ts +1 -0
- package/gen/mcpserver.d.ts.map +1 -1
- package/gen/mcpserver.js +1 -0
- package/gen/mcpserver.js.map +1 -1
- package/gen/workflowexecution.d.ts +2 -1
- package/gen/workflowexecution.d.ts.map +1 -1
- package/gen/workflowexecution.js +8 -0
- package/gen/workflowexecution.js.map +1 -1
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/package.json +2 -2
- package/src/execution/__tests__/approval-provenance.test.ts +60 -0
- package/src/execution/__tests__/file-review-fold.corpus.test.ts +127 -0
- package/src/execution/__tests__/file-review-fold.test.ts +281 -0
- package/src/execution/__tests__/to-display-file-change.test.ts +91 -0
- package/src/execution/__tests__/tool-view.file-changes.test.ts +100 -0
- package/src/execution/__tests__/tool-view.fixtures.test.ts +4 -0
- package/src/execution/__tests__/tool-view.output-ref.test.ts +76 -0
- package/src/execution/__tests__/tool-view.search.test.ts +264 -0
- package/src/execution/__tests__/tool-view.shell.test.ts +127 -0
- package/src/execution/approval-provenance.ts +80 -0
- package/src/execution/execution-phases.ts +23 -0
- package/src/execution/file-review-fold.ts +210 -0
- package/src/execution/to-display-file-change.ts +66 -0
- package/src/execution/tool-view.ts +261 -30
- package/src/gen/agentexecution.ts +7 -1
- package/src/gen/mcpserver.ts +2 -0
- package/src/gen/workflowexecution.ts +7 -1
- package/src/index.ts +12 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Framework-agnostic fold of the file-review event ledger into FileChangeSets.
|
|
2
|
+
//
|
|
3
|
+
// The server persists an append-only `file_review_event_stream` ledger on the
|
|
4
|
+
// execution status and computes `file_change_sets` as its projection. That
|
|
5
|
+
// server projection is phase-aware: it is nil for a TERMINAL execution (a dead
|
|
6
|
+
// execution has no *actionable* review). Settled/historical display therefore
|
|
7
|
+
// cannot read the projection — it must fold the always-preserved ledger itself.
|
|
8
|
+
//
|
|
9
|
+
// `foldFileReviewEventStream` is the TypeScript port of the Go projector
|
|
10
|
+
// `ProjectFileChangeSets` (backend/services/stigmer-server/pkg/domain/
|
|
11
|
+
// agentexecution/filereview/project.go), and its Java mirror
|
|
12
|
+
// `FileChangeSetProjector`. It is byte-for-byte identical to them EXCEPT it does
|
|
13
|
+
// not apply the terminal-phase gate — that is the whole point of the client-side
|
|
14
|
+
// fold (it must render settled sets for a terminal execution). Every other rule
|
|
15
|
+
// (first-seen change-set ordering, last-writer-wins scalars, decision
|
|
16
|
+
// accumulation, wholesale change replacement, sticky terminal status) matches
|
|
17
|
+
// the server exactly, so live and settled views agree.
|
|
18
|
+
//
|
|
19
|
+
// Parity is not maintained by discipline: the shared cross-edition corpus at
|
|
20
|
+
// apis/testdata/hitl/file-review/*.json — already replayed by the Go and Java
|
|
21
|
+
// projector suites — is replayed against this fold too (see the corpus parity
|
|
22
|
+
// test), so a drift between TS and the backends fails a test rather than
|
|
23
|
+
// shipping.
|
|
24
|
+
//
|
|
25
|
+
// This module has no React/framework dependency (mirrors tool-view.ts) so it is
|
|
26
|
+
// shared by @stigmer/react and @stigmer/ink and mirror-able by the Go CLI.
|
|
27
|
+
import { create } from "@bufbuild/protobuf";
|
|
28
|
+
import { FileChangeSetSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
29
|
+
import { FileChangeSetStatus, FileDecisionScope, FileReviewEventType, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
30
|
+
/**
|
|
31
|
+
* Folds a file-review event ledger into its list of {@link FileChangeSet}s,
|
|
32
|
+
* grouped by `change_set_id` in first-seen order.
|
|
33
|
+
*
|
|
34
|
+
* This is the display fold: unlike the server projection it does NOT collapse to
|
|
35
|
+
* an empty list for a terminal execution, so a completed/failed/cancelled
|
|
36
|
+
* execution still yields its settled sets for read-only display. For the LIVE
|
|
37
|
+
* (actionable) view, prefer the server-computed `status.file_change_sets`
|
|
38
|
+
* projection (it is structurally shared across stream frames); this fold is the
|
|
39
|
+
* settled/terminal complement — see {@link displayFileChangeSets}.
|
|
40
|
+
*
|
|
41
|
+
* The fold is a faithful port of the Go/Java projector: scalar and snapshot
|
|
42
|
+
* fields are last-writer-wins, `decisions` accumulate, `changes` are replaced
|
|
43
|
+
* wholesale by the latest `CANDIDATE_CAPTURED`, and terminal `RECONCILED` /
|
|
44
|
+
* `FAILED` statuses are sticky (a later non-terminal event never downgrades
|
|
45
|
+
* them).
|
|
46
|
+
*
|
|
47
|
+
* @param stream - The execution's `file_review_event_stream`, or `undefined`.
|
|
48
|
+
* @returns One {@link FileChangeSet} per `change_set_id`, in first-seen order.
|
|
49
|
+
*/
|
|
50
|
+
export function foldFileReviewEventStream(stream) {
|
|
51
|
+
if (!stream || stream.events.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
const order = [];
|
|
54
|
+
const byId = new Map();
|
|
55
|
+
for (const ev of stream.events) {
|
|
56
|
+
const changeSetId = ev.changeSetId;
|
|
57
|
+
// An event with no correlation id cannot be attributed to a set; the server
|
|
58
|
+
// skips it, so we do too.
|
|
59
|
+
if (!changeSetId)
|
|
60
|
+
continue;
|
|
61
|
+
let set = byId.get(changeSetId);
|
|
62
|
+
if (!set) {
|
|
63
|
+
set = create(FileChangeSetSchema, { id: changeSetId });
|
|
64
|
+
byId.set(changeSetId, set);
|
|
65
|
+
order.push(changeSetId);
|
|
66
|
+
}
|
|
67
|
+
applyEvent(set, ev);
|
|
68
|
+
}
|
|
69
|
+
return order.map((id) => byId.get(id));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The single read seam for displaying an execution's file change sets.
|
|
73
|
+
*
|
|
74
|
+
* Returns the server-computed `status.file_change_sets` projection when it is
|
|
75
|
+
* present (a LIVE execution — the projection is structurally shared across
|
|
76
|
+
* stream frames, so consumers keep stable references and streaming re-renders
|
|
77
|
+
* stay scoped to the actively-changing card), and otherwise folds the ledger
|
|
78
|
+
* (a TERMINAL execution — the server projects nil there, but the ledger is
|
|
79
|
+
* preserved). The fold therefore never runs on the streaming hot path; it only
|
|
80
|
+
* reconstructs the settled sets a dead execution would otherwise not display.
|
|
81
|
+
*
|
|
82
|
+
* The two branches are equivalent by construction for a non-terminal execution
|
|
83
|
+
* (the fold minus its terminal gate IS the server projection — locked by the
|
|
84
|
+
* corpus parity test), so this accessor is a single consistent source whichever
|
|
85
|
+
* branch it takes.
|
|
86
|
+
*
|
|
87
|
+
* @param status - The execution status, or `undefined`.
|
|
88
|
+
* @returns The change sets to display, or an empty array when there are none.
|
|
89
|
+
*/
|
|
90
|
+
export function displayFileChangeSets(status) {
|
|
91
|
+
const projected = status?.fileChangeSets;
|
|
92
|
+
if (projected && projected.length > 0)
|
|
93
|
+
return projected;
|
|
94
|
+
return foldFileReviewEventStream(status?.fileReviewEventStream);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Applies one ledger event to its change-set accumulator, mirroring the Go
|
|
98
|
+
* projector's `applyEvent`. The switch is keyed on `event_type` (the coarse
|
|
99
|
+
* bucket the server keys on); the typed payload is read defensively so a
|
|
100
|
+
* malformed event degrades to zero values exactly as the Go nil-getters do,
|
|
101
|
+
* rather than throwing.
|
|
102
|
+
*/
|
|
103
|
+
function applyEvent(set, ev) {
|
|
104
|
+
switch (ev.eventType) {
|
|
105
|
+
case FileReviewEventType.BASELINE_CAPTURED: {
|
|
106
|
+
const b = ev.payload.case === "baselineCaptured" ? ev.payload.value : undefined;
|
|
107
|
+
set.turnId = b?.turnId ?? "";
|
|
108
|
+
set.harnessId = b?.harnessId ?? "";
|
|
109
|
+
set.baselineSnapshot = b?.baselineSnapshot;
|
|
110
|
+
set.status = FileChangeSetStatus.CAPTURING;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case FileReviewEventType.CANDIDATE_CAPTURED: {
|
|
114
|
+
const c = ev.payload.case === "candidateCaptured" ? ev.payload.value : undefined;
|
|
115
|
+
set.candidateSnapshot = c?.candidateSnapshot;
|
|
116
|
+
set.changes = c?.changes ?? [];
|
|
117
|
+
set.aggregateDigest = c?.aggregateDigest ?? "";
|
|
118
|
+
set.diffCompleteness = c?.diffCompleteness ?? set.diffCompleteness;
|
|
119
|
+
set.status = deriveStatusAfterCandidate(set);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case FileReviewEventType.FILE_DECIDED: {
|
|
123
|
+
if (ev.payload.case === "fileDecided") {
|
|
124
|
+
set.decisions.push(ev.payload.value);
|
|
125
|
+
}
|
|
126
|
+
set.status = deriveStatusAfterCandidate(set);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case FileReviewEventType.RECONCILED: {
|
|
130
|
+
const r = ev.payload.case === "reconciled" ? ev.payload.value : undefined;
|
|
131
|
+
set.approvedSnapshot = r?.approvedSnapshot;
|
|
132
|
+
set.status = FileChangeSetStatus.RECONCILED;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case FileReviewEventType.FAILED: {
|
|
136
|
+
set.status = FileChangeSetStatus.FAILED;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
// UNSPECIFIED / any future type: ignored, matching the Go projector.
|
|
140
|
+
default:
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Derives a change set's status after a candidate or decision event, mirroring
|
|
146
|
+
* the Go projector's `deriveStatusAfterCandidate`. Terminal statuses are sticky:
|
|
147
|
+
* a `FILE_DECIDED` arriving after `RECONCILED`/`FAILED` never reverts the set to
|
|
148
|
+
* an actionable state.
|
|
149
|
+
*/
|
|
150
|
+
function deriveStatusAfterCandidate(set) {
|
|
151
|
+
if (set.status === FileChangeSetStatus.RECONCILED ||
|
|
152
|
+
set.status === FileChangeSetStatus.FAILED) {
|
|
153
|
+
return set.status;
|
|
154
|
+
}
|
|
155
|
+
return isFullyDecided(set)
|
|
156
|
+
? FileChangeSetStatus.DECIDED
|
|
157
|
+
: FileChangeSetStatus.AWAITING_REVIEW;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Whether every captured change in the set has a verdict, mirroring the Go
|
|
161
|
+
* projector's `isFullyDecided`: a set with no captured changes is never fully
|
|
162
|
+
* decided; any CHANGE_SET-scoped decision covers all files; otherwise every
|
|
163
|
+
* change id must appear among the FILE-scoped decisions.
|
|
164
|
+
*/
|
|
165
|
+
function isFullyDecided(set) {
|
|
166
|
+
if (set.changes.length === 0)
|
|
167
|
+
return false;
|
|
168
|
+
const decidedFileIds = new Set();
|
|
169
|
+
for (const decision of set.decisions) {
|
|
170
|
+
if (decision.scope === FileDecisionScope.CHANGE_SET)
|
|
171
|
+
return true;
|
|
172
|
+
if (decision.scope === FileDecisionScope.FILE) {
|
|
173
|
+
decidedFileIds.add(decision.fileChangeId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const change of set.changes) {
|
|
177
|
+
if (!decidedFileIds.has(change.id))
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=file-review-fold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-review-fold.js","sourceRoot":"","sources":["../../src/execution/file-review-fold.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
2
|
+
import type { FileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
3
|
+
/**
|
|
4
|
+
* Adapts a {@link CapturedFileChange} to the {@link FileChange} the diff
|
|
5
|
+
* renderers consume. The two reuse the same `FileContent` for before/after, so
|
|
6
|
+
* the bodies map directly; a capture is always WHOLE_FILE (the candidate carries
|
|
7
|
+
* the byte-exact sides), so the resulting `FileChange` renders as a true
|
|
8
|
+
* before/after diff.
|
|
9
|
+
*
|
|
10
|
+
* @param captured - A captured file change from a `FileChangeSet`.
|
|
11
|
+
* @returns The equivalent display-side {@link FileChange}.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toDisplayFileChange(captured: CapturedFileChange): FileChange;
|
|
14
|
+
//# sourceMappingURL=to-display-file-change.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-display-file-change.d.ts","sourceRoot":"","sources":["../../src/execution/to-display-file-change.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oEAAoE,CAAC;AAC7G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iEAAiE,CAAC;AAQlG;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,UAAU,CAU5E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Projects a CapturedFileChange (the file-review ledger/domain type) onto the
|
|
2
|
+
// FileChange display view-model that every diff renderer speaks.
|
|
3
|
+
//
|
|
4
|
+
// The two representations coexist by design (see the CapturedFileChange proto
|
|
5
|
+
// comment): CapturedFileChange is the apply-then-review domain type (always
|
|
6
|
+
// whole-file, byte-exact before/after, carries digests / capture_class /
|
|
7
|
+
// blocked_reason for enforcement), while FileChange is the *display superset* —
|
|
8
|
+
// it additionally models captureLevel and inline HUNK_ONLY diffs, which the
|
|
9
|
+
// still-live deny-gate approval produces directly. Because FileChange is the
|
|
10
|
+
// superset, this projection is lossless; the reverse would not be (a whole-file
|
|
11
|
+
// CapturedFileChange has no inline hunk to lose, but a HUNK_ONLY FileChange has
|
|
12
|
+
// no whole-file bytes). That asymmetry is exactly why display renders through
|
|
13
|
+
// FileChange and this adapter only ever runs one way.
|
|
14
|
+
//
|
|
15
|
+
// This is a pure derive-on-read projection from the single source (the ledger),
|
|
16
|
+
// not a stored second copy: it is called at render time by both FileReviewCard
|
|
17
|
+
// and useSessionFileChanges, never persisted.
|
|
18
|
+
//
|
|
19
|
+
// Framework-agnostic (no React) so it is shared across @stigmer/react,
|
|
20
|
+
// @stigmer/ink, and mirror-able by the Go CLI, alongside tool-view.ts.
|
|
21
|
+
import { create } from "@bufbuild/protobuf";
|
|
22
|
+
import { FileChangeSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
23
|
+
import { FileChangeCaptureLevel, FileChangeKind, FileChangeType, } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
24
|
+
/**
|
|
25
|
+
* Adapts a {@link CapturedFileChange} to the {@link FileChange} the diff
|
|
26
|
+
* renderers consume. The two reuse the same `FileContent` for before/after, so
|
|
27
|
+
* the bodies map directly; a capture is always WHOLE_FILE (the candidate carries
|
|
28
|
+
* the byte-exact sides), so the resulting `FileChange` renders as a true
|
|
29
|
+
* before/after diff.
|
|
30
|
+
*
|
|
31
|
+
* @param captured - A captured file change from a `FileChangeSet`.
|
|
32
|
+
* @returns The equivalent display-side {@link FileChange}.
|
|
33
|
+
*/
|
|
34
|
+
export function toDisplayFileChange(captured) {
|
|
35
|
+
return create(FileChangeSchema, {
|
|
36
|
+
path: captured.pathAfter || captured.pathBefore,
|
|
37
|
+
changeType: toFileChangeType(captured.kind),
|
|
38
|
+
captureLevel: FileChangeCaptureLevel.WHOLE_FILE,
|
|
39
|
+
before: captured.before,
|
|
40
|
+
after: captured.after,
|
|
41
|
+
renameFrom: captured.kind === FileChangeKind.RENAME ? captured.pathBefore : "",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Maps the ledger's {@link FileChangeKind} to the display {@link FileChangeType}. */
|
|
45
|
+
function toFileChangeType(kind) {
|
|
46
|
+
switch (kind) {
|
|
47
|
+
case FileChangeKind.ADD:
|
|
48
|
+
return FileChangeType.CREATE;
|
|
49
|
+
case FileChangeKind.DELETE:
|
|
50
|
+
return FileChangeType.DELETE;
|
|
51
|
+
case FileChangeKind.RENAME:
|
|
52
|
+
return FileChangeType.RENAME;
|
|
53
|
+
default:
|
|
54
|
+
return FileChangeType.MODIFY;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=to-display-file-change.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-display-file-change.js","sourceRoot":"","sources":["../../src/execution/to-display-file-change.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,iEAAiE;AACjE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,yEAAyE;AACzE,gFAAgF;AAChF,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,gFAAgF;AAChF,8EAA8E;AAC9E,sDAAsD;AACtD,EAAE;AACF,gFAAgF;AAChF,+EAA+E;AAC/E,8CAA8C;AAC9C,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AAEvE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iEAAiE,CAAC;AACnG,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,cAAc,GACf,MAAM,8DAA8D,CAAC;AAEtE;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAA4B;IAC9D,OAAO,MAAM,CAAC,gBAAgB,EAAE;QAC9B,IAAI,EAAE,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,UAAU;QAC/C,UAAU,EAAE,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3C,YAAY,EAAE,sBAAsB,CAAC,UAAU;QAC/C,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,UAAU,EACR,QAAQ,CAAC,IAAI,KAAK,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;KACrE,CAAC,CAAC;AACL,CAAC;AAED,sFAAsF;AACtF,SAAS,gBAAgB,CAAC,IAAoB;IAC5C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC,GAAG;YACrB,OAAO,cAAc,CAAC,MAAM,CAAC;QAC/B,KAAK,cAAc,CAAC,MAAM;YACxB,OAAO,cAAc,CAAC,MAAM,CAAC;QAC/B,KAAK,cAAc,CAAC,MAAM;YACxB,OAAO,cAAc,CAAC,MAAM,CAAC;QAC/B;YACE,OAAO,cAAc,CAAC,MAAM,CAAC;IACjC,CAAC;AACH,CAAC"}
|
package/execution/tool-view.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
2
|
+
import { FileChangeCaptureLevel } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
2
3
|
import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
3
|
-
export { ToolKind };
|
|
4
|
+
export { ToolKind, FileChangeCaptureLevel };
|
|
4
5
|
/** A single match in a search/grep result. */
|
|
5
6
|
export interface ToolSearchMatch {
|
|
6
7
|
/** File path the match was found in, when the result format provides it. */
|
|
@@ -33,6 +34,7 @@ export type ToolResultView = {
|
|
|
33
34
|
readonly linesAdded?: number;
|
|
34
35
|
readonly linesRemoved?: number;
|
|
35
36
|
readonly unifiedDiff?: string;
|
|
37
|
+
readonly captureLevel?: FileChangeCaptureLevel;
|
|
36
38
|
} | {
|
|
37
39
|
readonly type: "file";
|
|
38
40
|
readonly path: string;
|
|
@@ -41,6 +43,7 @@ export type ToolResultView = {
|
|
|
41
43
|
readonly truncated: boolean;
|
|
42
44
|
} | {
|
|
43
45
|
readonly type: "terminal";
|
|
46
|
+
readonly command?: string;
|
|
44
47
|
readonly stdout: string;
|
|
45
48
|
readonly stderr: string;
|
|
46
49
|
readonly exitCode?: number;
|
|
@@ -48,6 +51,8 @@ export type ToolResultView = {
|
|
|
48
51
|
readonly type: "search";
|
|
49
52
|
readonly matches: readonly ToolSearchMatch[];
|
|
50
53
|
readonly count: number;
|
|
54
|
+
readonly kind?: "files" | "content";
|
|
55
|
+
readonly truncated?: boolean;
|
|
51
56
|
} | {
|
|
52
57
|
readonly type: "list";
|
|
53
58
|
readonly entries: readonly string[];
|
|
@@ -67,7 +72,8 @@ export type ToolResultView = {
|
|
|
67
72
|
readonly message: string;
|
|
68
73
|
} | {
|
|
69
74
|
readonly type: "outputRef";
|
|
70
|
-
readonly
|
|
75
|
+
readonly storageKey: string;
|
|
76
|
+
readonly contentHash: string;
|
|
71
77
|
readonly isImage: boolean;
|
|
72
78
|
readonly mimeType: string;
|
|
73
79
|
readonly sizeBytes: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-view.d.ts","sourceRoot":"","sources":["../../src/execution/tool-view.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-view.d.ts","sourceRoot":"","sources":["../../src/execution/tool-view.ts"],"names":[],"mappings":"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"}
|
package/execution/tool-view.js
CHANGED
|
@@ -8,13 +8,24 @@
|
|
|
8
8
|
// that presentation layers (React, Ink, the Go CLI's equivalent) render
|
|
9
9
|
// without re-parsing third-party engine formats.
|
|
10
10
|
//
|
|
11
|
+
// For file edits, normalizeToolResult prefers the runner's authoritative
|
|
12
|
+
// before/after capture (ToolCall.file_changes, #186) over reconstructing a diff
|
|
13
|
+
// from args, surfacing the capture's fidelity via the diff view's captureLevel so
|
|
14
|
+
// the renderer is honest about whole-file vs hunk-only. The capture is a clean,
|
|
15
|
+
// structured proto, so it is consumed proto-first rather than re-modeled here;
|
|
16
|
+
// the repeated/multi-file projection and offloaded-body fetching live in the
|
|
17
|
+
// React layer. Counts and unified diffs for a whole-file capture stay derivable
|
|
18
|
+
// by the presentation layer (the runner emits 0/"" sentinels for them), keeping
|
|
19
|
+
// one diff implementation as the source of truth.
|
|
20
|
+
//
|
|
11
21
|
// This module has no React or framework dependency so it can be shared by
|
|
12
22
|
// @stigmer/react and @stigmer/ink. The Go CLI mirrors it; the shared contract
|
|
13
23
|
// is test/fixtures/tool-view/. Engine result formats are version-fragile, so
|
|
14
24
|
// every assumption here is fixture-backed and degrades gracefully to json/text.
|
|
15
25
|
import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
26
|
import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
17
|
-
|
|
27
|
+
import { FileChangeCaptureLevel } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
28
|
+
export { ToolKind, FileChangeCaptureLevel };
|
|
18
29
|
// ---------------------------------------------------------------------------
|
|
19
30
|
// Identity: resolveToolKind
|
|
20
31
|
// ---------------------------------------------------------------------------
|
|
@@ -107,12 +118,15 @@ export function normalizeToolResult(toolCall) {
|
|
|
107
118
|
}
|
|
108
119
|
// Output was offloaded to artifact storage (too large to inline): the real
|
|
109
120
|
// bytes are no longer in `result` (which holds only a short head/label), so
|
|
110
|
-
// render from the reference instead of re-parsing the placeholder.
|
|
111
|
-
|
|
121
|
+
// render from the reference instead of re-parsing the placeholder. The view
|
|
122
|
+
// carries the stable `storageKey` so consumers resolve the bytes/URL on
|
|
123
|
+
// demand — the persisted URL was ephemeral and is no longer trusted.
|
|
124
|
+
if (toolCall.outputRef && toolCall.outputRef.storageKey) {
|
|
112
125
|
const ref = toolCall.outputRef;
|
|
113
126
|
return {
|
|
114
127
|
type: "outputRef",
|
|
115
|
-
|
|
128
|
+
storageKey: ref.storageKey,
|
|
129
|
+
contentHash: ref.contentHash,
|
|
116
130
|
isImage: ref.isImage,
|
|
117
131
|
mimeType: ref.mimeType,
|
|
118
132
|
sizeBytes: Number(ref.sizeBytes),
|
|
@@ -130,7 +144,7 @@ export function normalizeToolResult(toolCall) {
|
|
|
130
144
|
case ToolKind.FILE_DELETE:
|
|
131
145
|
return result ? { type: "text", text: result } : { type: "empty" };
|
|
132
146
|
case ToolKind.SHELL:
|
|
133
|
-
return normalizeShell(result);
|
|
147
|
+
return normalizeShell(result, args);
|
|
134
148
|
case ToolKind.SEARCH:
|
|
135
149
|
return normalizeSearch(result);
|
|
136
150
|
case ToolKind.LIST:
|
|
@@ -144,14 +158,16 @@ export function normalizeToolResult(toolCall) {
|
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
function normalizeEdit(args, result) {
|
|
161
|
+
// The inline transcript diff for a file edit is reconstructed from the tool
|
|
162
|
+
// args (there is no captured `file_changes` mirror — apply-then-review renders
|
|
163
|
+
// captured diffs via FileReviewCard/FileChangeDiff, not here). The native
|
|
164
|
+
// engine returns prose with no diff, so the presentation layer computes the
|
|
165
|
+
// visual diff from the args fragments; the Cursor SDK returns a stringified
|
|
166
|
+
// envelope with precomputed diff stats.
|
|
147
167
|
const path = firstString(args, PATH_FIELDS) ?? "";
|
|
148
168
|
const oldText = firstString(args, OLD_TEXT_FIELDS);
|
|
149
169
|
const newText = firstString(args, NEW_TEXT_FIELDS);
|
|
150
|
-
|
|
151
|
-
// prefer those when present. The native engine returns prose with no diff, so
|
|
152
|
-
// the presentation layer computes the visual diff from oldText/newText.
|
|
153
|
-
const envelope = tryParseJson(result);
|
|
154
|
-
const value = isRecord(envelope) && isRecord(envelope.value) ? envelope.value : undefined;
|
|
170
|
+
const value = cursorEnvelopeValue(tryParseJson(result));
|
|
155
171
|
const linesAdded = value ? asNumber(value.linesAdded) : undefined;
|
|
156
172
|
const linesRemoved = value ? asNumber(value.linesRemoved) : undefined;
|
|
157
173
|
const unifiedDiff = value ? asString(value.diffString) : undefined;
|
|
@@ -166,6 +182,8 @@ function normalizeEdit(args, result) {
|
|
|
166
182
|
};
|
|
167
183
|
}
|
|
168
184
|
function normalizeWrite(args) {
|
|
185
|
+
// The inline transcript view for a file write is the proposed content from the
|
|
186
|
+
// tool args (captured diffs render via FileReviewCard, not here).
|
|
169
187
|
const path = firstString(args, PATH_FIELDS) ?? "";
|
|
170
188
|
const content = firstString(args, WRITE_CONTENT_FIELDS) ?? "";
|
|
171
189
|
return { type: "file", path, content, language: languageFromPath(path), truncated: false };
|
|
@@ -184,15 +202,26 @@ function normalizeRead(args, result) {
|
|
|
184
202
|
// or "[Command succeeded]". Format owned by the engine — see DD-003; covered by
|
|
185
203
|
// test/fixtures/tool-view/result-views.json so a format change fails one test.
|
|
186
204
|
const SHELL_EXIT_MARKER = /\n?\[Command (?:succeeded|failed with exit code (\d+))\]\s*$/;
|
|
187
|
-
function normalizeShell(result) {
|
|
188
|
-
//
|
|
205
|
+
function normalizeShell(result, args) {
|
|
206
|
+
// The command is the session's prompt line. "command" matches the shell
|
|
207
|
+
// primaryField shared with the runner/Go classifier and both native and
|
|
208
|
+
// Cursor args, so no fallback fields are needed.
|
|
209
|
+
const command = firstString(args, ["command"]);
|
|
210
|
+
// Two shapes carry structured shell output: native sub-agent steps emit the
|
|
211
|
+
// {stdout,stderr,exitCode} record directly; the Cursor SDK nests it under a
|
|
212
|
+
// {status, value} envelope (the same envelope the edit tool uses). Read
|
|
213
|
+
// whichever is present so the real output surfaces instead of the raw JSON
|
|
214
|
+
// string. A failed shell never reaches here — normalizeToolResult routes
|
|
215
|
+
// TOOL_CALL_FAILED to the error view first — so only success is handled.
|
|
189
216
|
const parsed = tryParseJson(result);
|
|
190
|
-
|
|
217
|
+
const shell = hasShellFields(parsed) ? parsed : cursorEnvelopeValue(parsed);
|
|
218
|
+
if (hasShellFields(shell)) {
|
|
191
219
|
return {
|
|
192
220
|
type: "terminal",
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
221
|
+
command,
|
|
222
|
+
stdout: asString(shell.stdout) ?? "",
|
|
223
|
+
stderr: asString(shell.stderr) ?? "",
|
|
224
|
+
exitCode: asNumber(shell.exitCode),
|
|
196
225
|
};
|
|
197
226
|
}
|
|
198
227
|
const marker = result.match(SHELL_EXIT_MARKER);
|
|
@@ -200,23 +229,166 @@ function normalizeShell(result) {
|
|
|
200
229
|
const stdout = result.replace(SHELL_EXIT_MARKER, "");
|
|
201
230
|
// Group 1 is set only on failure; a matched marker without it is success (0).
|
|
202
231
|
const exitCode = marker[1] !== undefined ? Number(marker[1]) : 0;
|
|
203
|
-
return { type: "terminal", stdout, stderr: "", exitCode };
|
|
232
|
+
return { type: "terminal", command, stdout, stderr: "", exitCode };
|
|
204
233
|
}
|
|
205
|
-
return { type: "terminal", stdout: result, stderr: "" };
|
|
234
|
+
return { type: "terminal", command, stdout: result, stderr: "" };
|
|
206
235
|
}
|
|
207
236
|
// A grep-style match line, e.g. " 12: // TODO: fix".
|
|
208
237
|
const GREP_MATCH_LINE = /^\s*\d+[:\t]/;
|
|
238
|
+
// The Cursor SDK returns search results as a stringified JSON envelope, not the
|
|
239
|
+
// plain text the native harness emits. Two shapes are observed, both fixture-
|
|
240
|
+
// backed in test/fixtures/tool-view/result-views.json:
|
|
241
|
+
// - file-name search (Glob/file_search): the {status,value} envelope with
|
|
242
|
+
// value.files (paths), value.totalFiles, and the truncation flags.
|
|
243
|
+
// - grep / codebase search: {workspaceResults: {<path>: {type, output}}},
|
|
244
|
+
// where each workspace's output carries files and/or line-bearing matches.
|
|
245
|
+
// Anything we don't recognise degrades to the json view (readable) rather than
|
|
246
|
+
// being wrapped as a single fake match by the plain-text path below.
|
|
209
247
|
function normalizeSearch(result) {
|
|
248
|
+
const parsed = tryParseJson(result);
|
|
249
|
+
if (parsed !== undefined) {
|
|
250
|
+
const fromEnvelope = searchFromEnvelope(parsed);
|
|
251
|
+
if (fromEnvelope) {
|
|
252
|
+
return fromEnvelope;
|
|
253
|
+
}
|
|
254
|
+
// Recognised JSON shape but nothing searchable in it (or an unknown shape):
|
|
255
|
+
// a clean json/object view beats wrapping the raw string as one "match".
|
|
256
|
+
if (isRecord(parsed) || Array.isArray(parsed)) {
|
|
257
|
+
return { type: "json", value: parsed };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return searchFromPlainText(result);
|
|
261
|
+
}
|
|
262
|
+
// Projects a parsed Cursor search envelope into a search view, or null when the
|
|
263
|
+
// parsed JSON is not one of the recognised search shapes.
|
|
264
|
+
//
|
|
265
|
+
// Both shapes are wrapped in the Cursor {status,value} envelope (shared with
|
|
266
|
+
// edit/shell), so the payload lives under `value`: file-name search puts `files`
|
|
267
|
+
// there, grep/codebase search puts `workspaceResults` there. We read the inner
|
|
268
|
+
// value when present and fall back to the parsed object itself, so a future
|
|
269
|
+
// un-enveloped shape still resolves rather than dropping to the json fallback.
|
|
270
|
+
function searchFromEnvelope(parsed) {
|
|
271
|
+
const inner = cursorEnvelopeValue(parsed) ?? (isRecord(parsed) ? parsed : undefined);
|
|
272
|
+
if (!inner)
|
|
273
|
+
return null;
|
|
274
|
+
const truncated = Boolean(inner.clientTruncated) || Boolean(inner.ripgrepTruncated);
|
|
275
|
+
// file-name search (Glob/file_search): inner.files is an array of path strings.
|
|
276
|
+
if (Array.isArray(inner.files)) {
|
|
277
|
+
const matches = pathsToFileMatches(inner.files);
|
|
278
|
+
const count = asNumber(inner.totalFiles) ?? matches.length;
|
|
279
|
+
// Safety net: the engine reports results but we extracted none — the shape
|
|
280
|
+
// drifted (e.g. files are objects, not strings). Surface the raw JSON rather
|
|
281
|
+
// than a misleading "No files found" that hides real data. (Truncation always
|
|
282
|
+
// returns a non-empty page, so this never fires on a legitimately capped result.)
|
|
283
|
+
if (count > 0 && matches.length === 0)
|
|
284
|
+
return null;
|
|
285
|
+
return { type: "search", matches, count, kind: "files", truncated };
|
|
286
|
+
}
|
|
287
|
+
// grep / codebase search: inner.workspaceResults maps a workspace path to its
|
|
288
|
+
// { type, output }. Flatten every workspace's output into one match list so a
|
|
289
|
+
// multi-root search reads as a single result. Truncation can be reported at the
|
|
290
|
+
// envelope level (passed in) or per-workspace (read inside).
|
|
291
|
+
if (isRecord(inner.workspaceResults)) {
|
|
292
|
+
return searchFromWorkspaceResults(inner.workspaceResults, truncated);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
// Flattens the per-workspace results of a grep/codebase search. Each workspace's
|
|
297
|
+
// `output` carries `files` (path matches) and/or `matches` (line-bearing content
|
|
298
|
+
// matches); both are collected, and `truncated` is OR-ed across workspaces.
|
|
299
|
+
//
|
|
300
|
+
// Returns null when the engine reports results (count > 0) but none could be
|
|
301
|
+
// extracted — a shape drift — so the caller degrades to the raw JSON view rather
|
|
302
|
+
// than silently rendering "No files found" over data that is actually there.
|
|
303
|
+
function searchFromWorkspaceResults(workspaceResults, envelopeTruncated) {
|
|
304
|
+
const fileMatches = [];
|
|
305
|
+
const contentMatches = [];
|
|
306
|
+
let reportedCount = 0;
|
|
307
|
+
let hasReportedCount = false;
|
|
308
|
+
let truncated = envelopeTruncated;
|
|
309
|
+
for (const ws of Object.values(workspaceResults)) {
|
|
310
|
+
const output = isRecord(ws) ? ws.output : undefined;
|
|
311
|
+
if (!isRecord(output))
|
|
312
|
+
continue;
|
|
313
|
+
if (Array.isArray(output.files)) {
|
|
314
|
+
fileMatches.push(...pathsToFileMatches(output.files));
|
|
315
|
+
}
|
|
316
|
+
if (Array.isArray(output.matches)) {
|
|
317
|
+
contentMatches.push(...toContentMatches(output.matches));
|
|
318
|
+
}
|
|
319
|
+
const count = asNumber(output.count);
|
|
320
|
+
if (count !== undefined) {
|
|
321
|
+
reportedCount += count;
|
|
322
|
+
hasReportedCount = true;
|
|
323
|
+
}
|
|
324
|
+
if (Boolean(output.clientTruncated) || Boolean(output.ripgrepTruncated)) {
|
|
325
|
+
truncated = true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Content matches are the richer signal, so when present they drive the view;
|
|
329
|
+
// otherwise the result is a file-name list.
|
|
330
|
+
const isContent = contentMatches.length > 0;
|
|
331
|
+
const matches = isContent ? contentMatches : fileMatches;
|
|
332
|
+
// Safety net: results were reported but none extracted — the per-workspace
|
|
333
|
+
// output shape drifted. Bail to the raw JSON view (see the caller) instead of
|
|
334
|
+
// hiding the data behind a "No matches".
|
|
335
|
+
if (hasReportedCount && reportedCount > 0 && matches.length === 0) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
type: "search",
|
|
340
|
+
matches,
|
|
341
|
+
count: hasReportedCount ? reportedCount : matches.length,
|
|
342
|
+
kind: isContent ? "content" : "files",
|
|
343
|
+
truncated,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// Maps an engine-provided array of file paths into file-name matches. Both
|
|
347
|
+
// `file` and `text` are set to the path so a kind-unaware consumer (e.g. the Ink
|
|
348
|
+
// summary) still shows something useful. Non-string entries are skipped.
|
|
349
|
+
function pathsToFileMatches(files) {
|
|
350
|
+
const matches = [];
|
|
351
|
+
for (const f of files) {
|
|
352
|
+
if (typeof f === "string" && f.length > 0) {
|
|
353
|
+
matches.push({ file: f, text: f });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return matches;
|
|
357
|
+
}
|
|
358
|
+
// Maps an engine-provided array of grep matches into content matches. Each entry
|
|
359
|
+
// is read defensively (the exact shape is engine-fragile): a `file`/`path` and a
|
|
360
|
+
// 1-based `line` are carried when present, and the line text is taken from the
|
|
361
|
+
// first available text field, falling back to the stringified entry.
|
|
362
|
+
function toContentMatches(rawMatches) {
|
|
363
|
+
const matches = [];
|
|
364
|
+
for (const m of rawMatches) {
|
|
365
|
+
if (typeof m === "string") {
|
|
366
|
+
if (m.length > 0)
|
|
367
|
+
matches.push({ text: m });
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!isRecord(m))
|
|
371
|
+
continue;
|
|
372
|
+
const file = asString(m.file) ?? asString(m.path);
|
|
373
|
+
const line = asNumber(m.line) ?? asNumber(m.lineNumber);
|
|
374
|
+
const text = asString(m.text) ?? asString(m.line_text) ?? asString(m.content) ?? "";
|
|
375
|
+
matches.push({ file, line, text });
|
|
376
|
+
}
|
|
377
|
+
return matches;
|
|
378
|
+
}
|
|
379
|
+
// The plain-text path for the native harness: grep emits " 12: match" lines;
|
|
380
|
+
// glob/semantic search emit one path per line.
|
|
381
|
+
function searchFromPlainText(result) {
|
|
210
382
|
const lines = nonEmptyLines(result);
|
|
211
383
|
const matchLines = lines.filter((l) => GREP_MATCH_LINE.test(l));
|
|
212
384
|
if (matchLines.length > 0) {
|
|
213
385
|
const matches = matchLines.map((l) => ({ text: l.trim() }));
|
|
214
|
-
return { type: "search", matches, count: matches.length };
|
|
386
|
+
return { type: "search", matches, count: matches.length, kind: "content" };
|
|
215
387
|
}
|
|
216
388
|
// Path/name results (glob, semantic search): each meaningful line is a match.
|
|
217
389
|
const entries = lines.filter((l) => !/^no (files|matches|results)/i.test(l));
|
|
218
|
-
const matches = entries.map((text) => ({ text }));
|
|
219
|
-
return { type: "search", matches, count: matches.length };
|
|
390
|
+
const matches = entries.map((text) => ({ file: text, text }));
|
|
391
|
+
return { type: "search", matches, count: matches.length, kind: "files" };
|
|
220
392
|
}
|
|
221
393
|
function normalizeList(result) {
|
|
222
394
|
const entries = nonEmptyLines(result);
|
|
@@ -289,6 +461,18 @@ function extractContentBlocks(parsed) {
|
|
|
289
461
|
function isRecord(v) {
|
|
290
462
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
291
463
|
}
|
|
464
|
+
// The Cursor SDK wraps tool payloads in a {status, value} envelope. Returns the
|
|
465
|
+
// inner `value` record when present, so edit and shell read the envelope through
|
|
466
|
+
// one definition instead of each re-deriving its shape.
|
|
467
|
+
function cursorEnvelopeValue(parsed) {
|
|
468
|
+
return isRecord(parsed) && isRecord(parsed.value) ? parsed.value : undefined;
|
|
469
|
+
}
|
|
470
|
+
// True when a record carries structured shell output (stdout/exitCode). Used to
|
|
471
|
+
// pick between the un-enveloped native shape and the Cursor envelope's inner
|
|
472
|
+
// value in normalizeShell.
|
|
473
|
+
function hasShellFields(v) {
|
|
474
|
+
return isRecord(v) && ("stdout" in v || "exitCode" in v);
|
|
475
|
+
}
|
|
292
476
|
function asString(v) {
|
|
293
477
|
return typeof v === "string" ? v : undefined;
|
|
294
478
|
}
|