@stigmer/ink 3.1.3 → 3.1.4
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/components/ApprovalPrompt.d.ts +15 -1
- package/components/ApprovalPrompt.d.ts.map +1 -1
- package/components/ApprovalPrompt.js +9 -3
- package/components/ApprovalPrompt.js.map +1 -1
- package/components/FileChangeProgressBar.d.ts.map +1 -1
- package/components/FileChangeProgressBar.js +3 -18
- package/components/FileChangeProgressBar.js.map +1 -1
- package/components/FileDiffBody.d.ts +30 -0
- package/components/FileDiffBody.d.ts.map +1 -0
- package/components/FileDiffBody.js +106 -0
- package/components/FileDiffBody.js.map +1 -0
- package/components/FileReviewAtoms.d.ts +23 -0
- package/components/FileReviewAtoms.d.ts.map +1 -0
- package/components/FileReviewAtoms.js +26 -0
- package/components/FileReviewAtoms.js.map +1 -0
- package/components/FileReviewPrompt.d.ts.map +1 -1
- package/components/FileReviewPrompt.js +16 -5
- package/components/FileReviewPrompt.js.map +1 -1
- package/components/FileReviewRecord.d.ts.map +1 -1
- package/components/FileReviewRecord.js +5 -3
- package/components/FileReviewRecord.js.map +1 -1
- package/components/MessageThread.d.ts.map +1 -1
- package/components/MessageThread.js +11 -3
- package/components/MessageThread.js.map +1 -1
- package/file-review.d.ts +17 -0
- package/file-review.d.ts.map +1 -1
- package/file-review.js +18 -0
- package/file-review.js.map +1 -1
- package/index.d.ts +3 -1
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/components.test.tsx +65 -0
- package/src/__tests__/data-pickers.test.tsx +1 -6
- package/src/__tests__/file-review.test.tsx +318 -3
- package/src/__tests__/test-support.tsx +25 -0
- package/src/components/ApprovalPrompt.tsx +33 -11
- package/src/components/FileChangeProgressBar.tsx +3 -29
- package/src/components/FileDiffBody.tsx +164 -0
- package/src/components/FileReviewAtoms.tsx +37 -0
- package/src/components/FileReviewPrompt.tsx +34 -2
- package/src/components/FileReviewRecord.tsx +11 -0
- package/src/components/MessageThread.tsx +15 -2
- package/src/file-review.ts +27 -0
- package/src/index.ts +4 -0
package/file-review.d.ts
CHANGED
|
@@ -25,6 +25,23 @@ export declare function progressEntryDisplayPath(entry: FileChangeProgressEntry)
|
|
|
25
25
|
* terminal reviewer sees the same explanation.
|
|
26
26
|
*/
|
|
27
27
|
export declare function blockReasonNote(change: CapturedFileChange): string | null;
|
|
28
|
+
/** The aggregate `+N −M` line counts for a change set. */
|
|
29
|
+
export interface LineStats {
|
|
30
|
+
/** Total lines added across the set's changes. */
|
|
31
|
+
readonly linesAdded: number;
|
|
32
|
+
/** Total lines removed across the set's changes. */
|
|
33
|
+
readonly linesRemoved: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sums a change set's per-file `linesAdded`/`linesRemoved` into one aggregate,
|
|
37
|
+
* for the set-level `+N −M` shown beside the prompt header and the settled
|
|
38
|
+
* record summary (parity with the web card's collapsed-bar aggregate). Counts
|
|
39
|
+
* every change regardless of verdict — it reports what changed, not what was
|
|
40
|
+
* kept. A file with no counts (binary, secret-withheld, or a record predating
|
|
41
|
+
* the fields) contributes zero, so the aggregate honestly understates rather
|
|
42
|
+
* than guessing; when NO file has counts it is zero and the stat is hidden.
|
|
43
|
+
*/
|
|
44
|
+
export declare function changeSetLineStats(set: FileChangeSet): LineStats;
|
|
28
45
|
/** The effective per-file verdict counts for a settled change set. */
|
|
29
46
|
export interface SettledCounts {
|
|
30
47
|
/** Files kept (effective verdict APPROVE). */
|
package/file-review.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-review.d.ts","sourceRoot":"","sources":["../src/file-review.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACd,MAAM,oEAAoE,CAAC;AAC5E,OAAO,EACL,cAAc,EAEf,MAAM,8DAA8D,CAAC;AAGtE;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAWvD;AAoBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAE/E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAiBzE;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CAc/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CASzD"}
|
|
1
|
+
{"version":3,"file":"file-review.d.ts","sourceRoot":"","sources":["../src/file-review.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACd,MAAM,oEAAoE,CAAC;AAC5E,OAAO,EACL,cAAc,EAEf,MAAM,8DAA8D,CAAC;AAGtE;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAWvD;AAoBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAE/E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAiBzE;AAED,0DAA0D;AAC1D,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,CAQhE;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CAc/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CASzD"}
|
package/file-review.js
CHANGED
|
@@ -70,6 +70,24 @@ export function blockReasonNote(change) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Sums a change set's per-file `linesAdded`/`linesRemoved` into one aggregate,
|
|
75
|
+
* for the set-level `+N −M` shown beside the prompt header and the settled
|
|
76
|
+
* record summary (parity with the web card's collapsed-bar aggregate). Counts
|
|
77
|
+
* every change regardless of verdict — it reports what changed, not what was
|
|
78
|
+
* kept. A file with no counts (binary, secret-withheld, or a record predating
|
|
79
|
+
* the fields) contributes zero, so the aggregate honestly understates rather
|
|
80
|
+
* than guessing; when NO file has counts it is zero and the stat is hidden.
|
|
81
|
+
*/
|
|
82
|
+
export function changeSetLineStats(set) {
|
|
83
|
+
let linesAdded = 0;
|
|
84
|
+
let linesRemoved = 0;
|
|
85
|
+
for (const change of set.changes) {
|
|
86
|
+
linesAdded += change.linesAdded;
|
|
87
|
+
linesRemoved += change.linesRemoved;
|
|
88
|
+
}
|
|
89
|
+
return { linesAdded, linesRemoved };
|
|
90
|
+
}
|
|
73
91
|
/**
|
|
74
92
|
* Folds a settled change set's decisions into kept / discarded / not-reviewed
|
|
75
93
|
* counts, using the same `deriveEffectiveVerdicts` fold the web uses so a
|
package/file-review.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-review.js","sourceRoot":"","sources":["../src/file-review.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-review.js","sourceRoot":"","sources":["../src/file-review.ts"],"names":[],"mappings":"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"}
|
package/index.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export { TodoList, type TodoListProps } from "./components/TodoList.js";
|
|
|
10
10
|
export { ApprovalPrompt, type ApprovalPromptProps } from "./components/ApprovalPrompt.js";
|
|
11
11
|
export { FileReviewPrompt, type FileReviewPromptProps } from "./components/FileReviewPrompt.js";
|
|
12
12
|
export { FileReviewRecord, type FileReviewRecordProps } from "./components/FileReviewRecord.js";
|
|
13
|
+
export { FileDiffBody, type FileDiffBodyProps } from "./components/FileDiffBody.js";
|
|
13
14
|
export { FileChangeProgressBar, type FileChangeProgressBarProps, } from "./components/FileChangeProgressBar.js";
|
|
14
|
-
export {
|
|
15
|
+
export { FileLineStats } from "./components/FileReviewAtoms.js";
|
|
16
|
+
export { kindLetter, changeDisplayPath, progressEntryDisplayPath, blockReasonNote, changeSetLineStats, settledCounts, settledSummary, type LineStats, type SettledCounts, } from "./file-review.js";
|
|
15
17
|
export { ExecutionProgress, type ExecutionProgressProps } from "./components/ExecutionProgress.js";
|
|
16
18
|
export { FollowUpInput, type FollowUpInputProps } from "./components/FollowUpInput.js";
|
|
17
19
|
export { UsageWidget, type UsageWidgetProps } from "./components/UsageWidget.js";
|
package/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAGjF,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAChG,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,uCAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAGjF,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,wBAAwB,EACxB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,KAAK,SAAS,EACd,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AACnG,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGpF,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC3G,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAGvF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAChG,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
|
package/index.js
CHANGED
|
@@ -14,9 +14,11 @@ export { TodoList } from "./components/TodoList.js";
|
|
|
14
14
|
export { ApprovalPrompt } from "./components/ApprovalPrompt.js";
|
|
15
15
|
export { FileReviewPrompt } from "./components/FileReviewPrompt.js";
|
|
16
16
|
export { FileReviewRecord } from "./components/FileReviewRecord.js";
|
|
17
|
+
export { FileDiffBody } from "./components/FileDiffBody.js";
|
|
17
18
|
export { FileChangeProgressBar, } from "./components/FileChangeProgressBar.js";
|
|
19
|
+
export { FileLineStats } from "./components/FileReviewAtoms.js";
|
|
18
20
|
// File-review pure helpers (kind letters, paths, block reasons, settled counts)
|
|
19
|
-
export { kindLetter, changeDisplayPath, progressEntryDisplayPath, blockReasonNote, settledCounts, settledSummary, } from "./file-review.js";
|
|
21
|
+
export { kindLetter, changeDisplayPath, progressEntryDisplayPath, blockReasonNote, changeSetLineStats, settledCounts, settledSummary, } from "./file-review.js";
|
|
20
22
|
export { ExecutionProgress } from "./components/ExecutionProgress.js";
|
|
21
23
|
export { FollowUpInput } from "./components/FollowUpInput.js";
|
|
22
24
|
export { UsageWidget } from "./components/UsageWidget.js";
|
package/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,WAAW;AACX,OAAO,EAAE,kBAAkB,EAAgC,MAAM,eAAe,CAAC;AAEjF,iDAAiD;AACjD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAE3B,WAAW;AACX,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,aAAa;AACb,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAsB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAA8B,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAA8B,MAAM,kCAAkC,CAAC;AAChG,OAAO,EACL,qBAAqB,GAEtB,MAAM,uCAAuC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,WAAW;AACX,OAAO,EAAE,kBAAkB,EAAgC,MAAM,eAAe,CAAC;AAEjF,iDAAiD;AACjD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GAEpB,MAAM,mBAAmB,CAAC;AAE3B,WAAW;AACX,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,aAAa;AACb,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAsB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,gBAAgB,EAA8B,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAA8B,MAAM,kCAAkC,CAAC;AAChG,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AACpF,OAAO,EACL,qBAAqB,GAEtB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAEhE,gFAAgF;AAChF,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,wBAAwB,EACxB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,cAAc,GAGf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAA+B,MAAM,mCAAmC,CAAC;AACnG,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,WAAW,EAAyB,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,YAAY,EAA0B,MAAM,8BAA8B,CAAC;AAEpF,0CAA0C;AAC1C,OAAO,EAAE,cAAc,EAA6C,MAAM,gCAAgC,CAAC;AAC3G,OAAO,EAAE,WAAW,EAAyB,MAAM,6BAA6B,CAAC;AACjF,OAAO,EAAE,aAAa,EAA2B,MAAM,+BAA+B,CAAC;AAEvF,iBAAiB;AACjB,OAAO,EAAE,WAAW,EAA+C,MAAM,sBAAsB,CAAC;AAChG,OAAO,EAAE,UAAU,EAAwB,MAAM,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/ink",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
4
4
|
"description": "Ink (React for terminals) components for rendering Stigmer agent sessions in the terminal",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@connectrpc/connect-node": "^2.1.1",
|
|
38
|
-
"@stigmer/react": "3.1.
|
|
38
|
+
"@stigmer/react": "3.1.4",
|
|
39
39
|
"ink-spinner": "^5.0.0",
|
|
40
40
|
"ink-text-input": "^6.0.0",
|
|
41
41
|
"marked": "^15.0.7",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@bufbuild/protobuf": "^2.0.0",
|
|
46
|
-
"@stigmer/protos": "3.1.
|
|
47
|
-
"@stigmer/sdk": "3.1.
|
|
46
|
+
"@stigmer/protos": "3.1.4",
|
|
47
|
+
"@stigmer/sdk": "3.1.4",
|
|
48
48
|
"ink": "^7.0.0",
|
|
49
49
|
"react": ">=19.0.0"
|
|
50
50
|
}
|
|
@@ -7,10 +7,12 @@ import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexec
|
|
|
7
7
|
import { MessageType, ExecutionPhase, ToolCallStatus, ApprovalAction, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
8
|
import { ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
9
9
|
import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
10
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
10
11
|
import { MessageEntry } from "../components/MessageEntry.js";
|
|
11
12
|
import { ExecutionProgress } from "../components/ExecutionProgress.js";
|
|
12
13
|
import { ToolCallItem } from "../components/ToolCallItem.js";
|
|
13
14
|
import { ApprovalPrompt } from "../components/ApprovalPrompt.js";
|
|
15
|
+
import { MessageThread } from "../components/MessageThread.js";
|
|
14
16
|
|
|
15
17
|
describe("MessageEntry", () => {
|
|
16
18
|
it("renders a human message with 'You' prefix", () => {
|
|
@@ -221,4 +223,67 @@ describe("ApprovalPrompt", () => {
|
|
|
221
223
|
|
|
222
224
|
expect(onSubmit).toHaveBeenCalledWith(ApprovalAction.APPROVE);
|
|
223
225
|
});
|
|
226
|
+
|
|
227
|
+
it("ignores keyboard input when inactive", () => {
|
|
228
|
+
const pending = create(PendingApprovalSchema);
|
|
229
|
+
pending.toolCallId = "tc-1";
|
|
230
|
+
pending.toolName = "write_file";
|
|
231
|
+
const onSubmit = vi.fn();
|
|
232
|
+
|
|
233
|
+
const { stdin } = render(
|
|
234
|
+
<ApprovalPrompt
|
|
235
|
+
pendingApproval={pending}
|
|
236
|
+
onSubmit={onSubmit}
|
|
237
|
+
isActive={false}
|
|
238
|
+
/>,
|
|
239
|
+
);
|
|
240
|
+
stdin.write("y");
|
|
241
|
+
stdin.write("a");
|
|
242
|
+
|
|
243
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("MessageThread — multi-approval keyboard arbitration", () => {
|
|
248
|
+
function execWithApprovals(...toolCallIds: string[]) {
|
|
249
|
+
const pendingApprovals = toolCallIds.map((id) => {
|
|
250
|
+
const pa = create(PendingApprovalSchema);
|
|
251
|
+
pa.toolCallId = id;
|
|
252
|
+
pa.toolName = "write_file";
|
|
253
|
+
return pa;
|
|
254
|
+
});
|
|
255
|
+
return create(AgentExecutionSchema, {
|
|
256
|
+
metadata: { id: "aex-1" },
|
|
257
|
+
status: { pendingApprovals },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
it("routes a single keystroke to only the FIRST of multiple pending approvals", () => {
|
|
262
|
+
// The regression lock for the keyboard-sharing bug: two live ApprovalPrompts
|
|
263
|
+
// each had an unconditional useInput, so one keystroke settled both. Now only
|
|
264
|
+
// the first is active.
|
|
265
|
+
const onApprovalSubmit = vi.fn();
|
|
266
|
+
const exec = execWithApprovals("tc-1", "tc-2");
|
|
267
|
+
|
|
268
|
+
const { stdin } = render(
|
|
269
|
+
<MessageThread executions={[exec]} onApprovalSubmit={onApprovalSubmit} />,
|
|
270
|
+
);
|
|
271
|
+
stdin.write("y");
|
|
272
|
+
|
|
273
|
+
expect(onApprovalSubmit).toHaveBeenCalledTimes(1);
|
|
274
|
+
expect(onApprovalSubmit).toHaveBeenCalledWith("tc-1", ApprovalAction.APPROVE);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("keeps the sole pending approval interactive", () => {
|
|
278
|
+
const onApprovalSubmit = vi.fn();
|
|
279
|
+
const exec = execWithApprovals("tc-1");
|
|
280
|
+
|
|
281
|
+
const { stdin } = render(
|
|
282
|
+
<MessageThread executions={[exec]} onApprovalSubmit={onApprovalSubmit} />,
|
|
283
|
+
);
|
|
284
|
+
stdin.write("y");
|
|
285
|
+
|
|
286
|
+
expect(onApprovalSubmit).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(onApprovalSubmit).toHaveBeenCalledWith("tc-1", ApprovalAction.APPROVE);
|
|
288
|
+
});
|
|
224
289
|
});
|
|
@@ -2,12 +2,12 @@ import React from "react";
|
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
3
|
import { render } from "ink-testing-library";
|
|
4
4
|
import { create } from "@bufbuild/protobuf";
|
|
5
|
-
import type { Stigmer } from "@stigmer/sdk";
|
|
6
5
|
import { SearchResultSchema } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
|
|
7
6
|
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
8
7
|
import { InkStigmerProvider } from "../provider.js";
|
|
9
8
|
import { AgentPicker } from "../components/AgentPicker.js";
|
|
10
9
|
import { SessionPicker } from "../components/SessionPicker.js";
|
|
10
|
+
import { fakeClient } from "./test-support.js";
|
|
11
11
|
|
|
12
12
|
const KEY = { enter: "\r" } as const;
|
|
13
13
|
|
|
@@ -27,11 +27,6 @@ function session(id: string, subject: string, agentInstanceId: string) {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Minimal Stigmer stub exposing only the methods the picker hooks call.
|
|
31
|
-
function fakeClient(overrides: Record<string, unknown>): Stigmer {
|
|
32
|
-
return overrides as unknown as Stigmer;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
30
|
describe("AgentPicker", () => {
|
|
36
31
|
it("renders search results and returns the chosen agent on Enter", async () => {
|
|
37
32
|
const results = [
|
|
@@ -6,8 +6,10 @@ import { FileReviewContext } from "@stigmer/react";
|
|
|
6
6
|
import {
|
|
7
7
|
ToolCallSchema,
|
|
8
8
|
FileContentSchema,
|
|
9
|
+
ToolCallOutputRefSchema,
|
|
9
10
|
AgentMessageSchema,
|
|
10
11
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
12
|
+
import type { FileContent } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
11
13
|
import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
12
14
|
import {
|
|
13
15
|
FileChangeSetSchema,
|
|
@@ -30,7 +32,11 @@ import { ToolCallItem } from "../components/ToolCallItem.js";
|
|
|
30
32
|
import { ToolCallGroup } from "../components/ToolCallGroup.js";
|
|
31
33
|
import { FileReviewPrompt } from "../components/FileReviewPrompt.js";
|
|
32
34
|
import { FileReviewRecord } from "../components/FileReviewRecord.js";
|
|
35
|
+
import { FileDiffBody } from "../components/FileDiffBody.js";
|
|
33
36
|
import { MessageThread } from "../components/MessageThread.js";
|
|
37
|
+
import { FileLineStats } from "../components/FileReviewAtoms.js";
|
|
38
|
+
import { changeSetLineStats } from "../file-review.js";
|
|
39
|
+
import { fakeClient, renderWithClient } from "./test-support.js";
|
|
34
40
|
|
|
35
41
|
// --- fixtures -------------------------------------------------------------
|
|
36
42
|
|
|
@@ -41,6 +47,8 @@ interface ChangeOpts {
|
|
|
41
47
|
readonly diffComplete?: boolean;
|
|
42
48
|
readonly binary?: boolean;
|
|
43
49
|
readonly blockedReason?: FileReviewBlockReason;
|
|
50
|
+
readonly linesAdded?: number;
|
|
51
|
+
readonly linesRemoved?: number;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
function makeChange(opts: ChangeOpts) {
|
|
@@ -52,6 +60,8 @@ function makeChange(opts: ChangeOpts) {
|
|
|
52
60
|
diffComplete: opts.diffComplete ?? true,
|
|
53
61
|
fileDigest: `fd-${opts.id}`,
|
|
54
62
|
blockedReason: opts.blockedReason ?? FileReviewBlockReason.UNSPECIFIED,
|
|
63
|
+
linesAdded: opts.linesAdded ?? 0,
|
|
64
|
+
linesRemoved: opts.linesRemoved ?? 0,
|
|
55
65
|
before: opts.binary ? create(FileContentSchema, { isBinary: true }) : undefined,
|
|
56
66
|
after: opts.binary ? create(FileContentSchema, { isBinary: true }) : undefined,
|
|
57
67
|
});
|
|
@@ -215,7 +225,9 @@ describe("FileReviewPrompt — per-file", () => {
|
|
|
215
225
|
|
|
216
226
|
it("enters per-file mode and keeps a reviewable file with its file digest", async () => {
|
|
217
227
|
const onSubmit = vi.fn();
|
|
218
|
-
|
|
228
|
+
// Per-file mode mounts FileDiffBody, whose useFileChangeContent reads the
|
|
229
|
+
// Stigmer client — so these tests render under a provider.
|
|
230
|
+
const { stdin, lastFrame } = renderWithClient(
|
|
219
231
|
<FileReviewPrompt changeSet={blockedSet} onSubmit={onSubmit} />,
|
|
220
232
|
);
|
|
221
233
|
stdin.write("f");
|
|
@@ -235,7 +247,7 @@ describe("FileReviewPrompt — per-file", () => {
|
|
|
235
247
|
|
|
236
248
|
it("refuses to keep an unavailable file but allows discard", async () => {
|
|
237
249
|
const onSubmit = vi.fn();
|
|
238
|
-
const { stdin } =
|
|
250
|
+
const { stdin } = renderWithClient(
|
|
239
251
|
<FileReviewPrompt changeSet={blockedSet} onSubmit={onSubmit} />,
|
|
240
252
|
);
|
|
241
253
|
stdin.write("f");
|
|
@@ -262,7 +274,7 @@ describe("FileReviewPrompt — per-file", () => {
|
|
|
262
274
|
],
|
|
263
275
|
});
|
|
264
276
|
const onSubmit = vi.fn();
|
|
265
|
-
const { stdin } =
|
|
277
|
+
const { stdin } = renderWithClient(
|
|
266
278
|
<FileReviewPrompt changeSet={binarySet} onSubmit={onSubmit} />,
|
|
267
279
|
);
|
|
268
280
|
stdin.write("f");
|
|
@@ -494,3 +506,306 @@ describe("MessageThread — file-review integration", () => {
|
|
|
494
506
|
expect(out).not.toContain("File review —"); // but no settled record
|
|
495
507
|
});
|
|
496
508
|
});
|
|
509
|
+
|
|
510
|
+
// --- FileLineStats (shared atom) -----------------------------------------
|
|
511
|
+
|
|
512
|
+
describe("FileLineStats", () => {
|
|
513
|
+
it("renders nothing when both counts are zero (never +0 -0)", () => {
|
|
514
|
+
const { lastFrame } = render(<FileLineStats linesAdded={0} linesRemoved={0} />);
|
|
515
|
+
expect((lastFrame() ?? "").trim()).toBe("");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("renders +N -M when counts exist", () => {
|
|
519
|
+
const { lastFrame } = render(<FileLineStats linesAdded={7} linesRemoved={3} />);
|
|
520
|
+
const out = lastFrame() ?? "";
|
|
521
|
+
expect(out).toContain("+7");
|
|
522
|
+
expect(out).toContain("-3");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("renders a one-sided stat (added only) without hiding it", () => {
|
|
526
|
+
const { lastFrame } = render(<FileLineStats linesAdded={5} linesRemoved={0} />);
|
|
527
|
+
const out = lastFrame() ?? "";
|
|
528
|
+
expect(out).toContain("+5");
|
|
529
|
+
expect(out).toContain("-0"); // both sides always shown once any count exists
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// --- changeSetLineStats (pure helper) ------------------------------------
|
|
534
|
+
|
|
535
|
+
describe("changeSetLineStats", () => {
|
|
536
|
+
it("sums per-file counts across the set", () => {
|
|
537
|
+
const set = makeSet({
|
|
538
|
+
status: FileChangeSetStatus.RECONCILED,
|
|
539
|
+
changes: [
|
|
540
|
+
makeChange({ id: "a", path: "one.ts", linesAdded: 10, linesRemoved: 2 }),
|
|
541
|
+
makeChange({ id: "b", path: "two.ts", linesAdded: 5, linesRemoved: 3 }),
|
|
542
|
+
],
|
|
543
|
+
});
|
|
544
|
+
expect(changeSetLineStats(set)).toEqual({ linesAdded: 15, linesRemoved: 5 });
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("treats an uncountable (binary/withheld) file as a zero contributor — honest understatement", () => {
|
|
548
|
+
const set = makeSet({
|
|
549
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
550
|
+
changes: [
|
|
551
|
+
makeChange({ id: "txt", path: "app.ts", linesAdded: 8, linesRemoved: 1 }),
|
|
552
|
+
makeChange({ id: "img", path: "logo.png", kind: FileChangeKind.BINARY_CHANGE, binary: true }),
|
|
553
|
+
],
|
|
554
|
+
});
|
|
555
|
+
expect(changeSetLineStats(set)).toEqual({ linesAdded: 8, linesRemoved: 1 });
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("is zero for a set whose files carry no counts (stat then hidden by the atom)", () => {
|
|
559
|
+
const set = makeSet({
|
|
560
|
+
status: FileChangeSetStatus.RECONCILED,
|
|
561
|
+
changes: [makeChange({ id: "a", path: "one.ts" })],
|
|
562
|
+
});
|
|
563
|
+
expect(changeSetLineStats(set)).toEqual({ linesAdded: 0, linesRemoved: 0 });
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// --- Line stats on the review surfaces -----------------------------------
|
|
568
|
+
|
|
569
|
+
describe("FileReviewPrompt — line stats", () => {
|
|
570
|
+
it("shows the set aggregate +N -M on the header", () => {
|
|
571
|
+
const set = makeSet({
|
|
572
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
573
|
+
changes: [
|
|
574
|
+
makeChange({ id: "a", path: "one.ts", linesAdded: 12, linesRemoved: 3 }),
|
|
575
|
+
makeChange({ id: "b", path: "two.ts", linesAdded: 4, linesRemoved: 1 }),
|
|
576
|
+
],
|
|
577
|
+
});
|
|
578
|
+
const out = render(<FileReviewPrompt changeSet={set} onSubmit={() => {}} />).lastFrame() ?? "";
|
|
579
|
+
expect(out).toContain("2 files awaiting review");
|
|
580
|
+
expect(out).toContain("+16");
|
|
581
|
+
expect(out).toContain("-4");
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("hides the header aggregate when no file has counts (never +0 -0)", () => {
|
|
585
|
+
const set = makeSet({
|
|
586
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
587
|
+
changes: [makeChange({ id: "a", path: "one.ts" })],
|
|
588
|
+
});
|
|
589
|
+
const out = render(<FileReviewPrompt changeSet={set} onSubmit={() => {}} />).lastFrame() ?? "";
|
|
590
|
+
expect(out).not.toContain("+");
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("shows per-file +N -M in per-file mode", async () => {
|
|
594
|
+
const set = makeSet({
|
|
595
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
596
|
+
changes: [
|
|
597
|
+
makeChange({ id: "a", path: "one.ts", linesAdded: 10, linesRemoved: 2 }),
|
|
598
|
+
makeChange({ id: "b", path: "two.ts", linesAdded: 4, linesRemoved: 0 }),
|
|
599
|
+
],
|
|
600
|
+
});
|
|
601
|
+
const { stdin, lastFrame } = renderWithClient(
|
|
602
|
+
<FileReviewPrompt changeSet={set} onSubmit={() => {}} />,
|
|
603
|
+
);
|
|
604
|
+
stdin.write("f");
|
|
605
|
+
await tick();
|
|
606
|
+
const out = lastFrame() ?? "";
|
|
607
|
+
expect(out).toContain("+10");
|
|
608
|
+
expect(out).toContain("+4");
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe("FileReviewRecord — line stats", () => {
|
|
613
|
+
it("shows the set aggregate and per-file +N -M", () => {
|
|
614
|
+
const set = makeSet({
|
|
615
|
+
status: FileChangeSetStatus.RECONCILED,
|
|
616
|
+
changes: [
|
|
617
|
+
makeChange({ id: "a", path: "one.ts", linesAdded: 9, linesRemoved: 2 }),
|
|
618
|
+
makeChange({ id: "b", path: "two.ts", linesAdded: 3, linesRemoved: 1 }),
|
|
619
|
+
],
|
|
620
|
+
decisions: [
|
|
621
|
+
{ scope: FileDecisionScope.CHANGE_SET, action: FileDecisionAction.APPROVE },
|
|
622
|
+
],
|
|
623
|
+
});
|
|
624
|
+
const out = render(<FileReviewRecord fileChangeSet={set} />).lastFrame() ?? "";
|
|
625
|
+
expect(out).toContain("+12"); // aggregate 9+3
|
|
626
|
+
expect(out).toContain("+9"); // per-file
|
|
627
|
+
expect(out).toContain("+3"); // per-file
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("renders no stat when the set carries no counts", () => {
|
|
631
|
+
const set = makeSet({
|
|
632
|
+
status: FileChangeSetStatus.RECONCILED,
|
|
633
|
+
changes: [makeChange({ id: "a", path: "one.ts" })],
|
|
634
|
+
});
|
|
635
|
+
expect(render(<FileReviewRecord fileChangeSet={set} />).lastFrame() ?? "").not.toContain("+");
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// --- FileDiffBody ---------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Polls the rendered frame until it contains `needle`, then returns. Replaces a
|
|
643
|
+
* fixed sleep: an artifact fetch resolves on its own microtask schedule (and a
|
|
644
|
+
* truncated side chains a second download-url fetch), so any single delay raced
|
|
645
|
+
* green locally but timed out under CI load. Polling waits exactly as long as
|
|
646
|
+
* the render needs and no longer, and fails with the actual frame on timeout.
|
|
647
|
+
*/
|
|
648
|
+
async function waitForFrame(
|
|
649
|
+
lastFrame: () => string | undefined,
|
|
650
|
+
needle: string,
|
|
651
|
+
timeoutMs = 2000,
|
|
652
|
+
): Promise<void> {
|
|
653
|
+
const deadline = Date.now() + timeoutMs;
|
|
654
|
+
for (;;) {
|
|
655
|
+
if ((lastFrame() ?? "").includes(needle)) return;
|
|
656
|
+
if (Date.now() >= deadline) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`waitForFrame timed out after ${timeoutMs}ms waiting for ${JSON.stringify(
|
|
659
|
+
needle,
|
|
660
|
+
)}; last frame was ${JSON.stringify(lastFrame() ?? "")}`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function inlineSide(text: string): FileContent {
|
|
668
|
+
return create(FileContentSchema, { body: { case: "inline", value: text } });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function offloadedSide(storageKey: string, contentHash = "h"): FileContent {
|
|
672
|
+
return create(FileContentSchema, {
|
|
673
|
+
body: {
|
|
674
|
+
case: "ref",
|
|
675
|
+
value: create(ToolCallOutputRefSchema, { storageKey, contentHash }),
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function changeWith(opts: {
|
|
681
|
+
id: string;
|
|
682
|
+
kind?: FileChangeKind;
|
|
683
|
+
before?: FileContent;
|
|
684
|
+
after?: FileContent;
|
|
685
|
+
}) {
|
|
686
|
+
return create(CapturedFileChangeSchema, {
|
|
687
|
+
id: opts.id,
|
|
688
|
+
pathAfter: "file.ts",
|
|
689
|
+
pathBefore: "file.ts",
|
|
690
|
+
kind: opts.kind ?? FileChangeKind.MODIFY,
|
|
691
|
+
fileDigest: `fd-${opts.id}`,
|
|
692
|
+
before: opts.before,
|
|
693
|
+
after: opts.after,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
describe("FileDiffBody", () => {
|
|
698
|
+
it("renders +/- lines for an inline whole-file diff (computeDiff parity)", () => {
|
|
699
|
+
const change = changeWith({
|
|
700
|
+
id: "m",
|
|
701
|
+
before: inlineSide("keep\nold\ntail\n"),
|
|
702
|
+
after: inlineSide("keep\nnew\ntail\n"),
|
|
703
|
+
});
|
|
704
|
+
const out = renderWithClient(<FileDiffBody change={change} />).lastFrame() ?? "";
|
|
705
|
+
expect(out).toContain("-old"); // removed, red
|
|
706
|
+
expect(out).toContain("+new"); // added, green
|
|
707
|
+
expect(out).toContain("keep"); // context retained
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it("shows 'Binary file changed.' for a binary side", () => {
|
|
711
|
+
const change = create(CapturedFileChangeSchema, {
|
|
712
|
+
id: "bin",
|
|
713
|
+
pathAfter: "logo.png",
|
|
714
|
+
pathBefore: "logo.png",
|
|
715
|
+
kind: FileChangeKind.BINARY_CHANGE,
|
|
716
|
+
before: create(FileContentSchema, { isBinary: true }),
|
|
717
|
+
after: create(FileContentSchema, { isBinary: true }),
|
|
718
|
+
});
|
|
719
|
+
const out = renderWithClient(<FileDiffBody change={change} />).lastFrame() ?? "";
|
|
720
|
+
expect(out).toContain("Binary file changed.");
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("labels a genuinely empty new file (CREATE with no content)", () => {
|
|
724
|
+
const change = changeWith({ id: "new", kind: FileChangeKind.ADD, after: inlineSide("") });
|
|
725
|
+
const out = renderWithClient(<FileDiffBody change={change} />).lastFrame() ?? "";
|
|
726
|
+
expect(out).toContain("Empty new file.");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("falls back to 'No preview available.' for a MODIFY with no renderable diff", () => {
|
|
730
|
+
const change = changeWith({ id: "np" }); // no before/after content
|
|
731
|
+
const out = renderWithClient(<FileDiffBody change={change} />).lastFrame() ?? "";
|
|
732
|
+
expect(out).toContain("No preview available.");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("shows a loading notice while an offloaded side is in flight", () => {
|
|
736
|
+
const change = changeWith({ id: "load", after: offloadedSide("artifacts/aex-1/after") });
|
|
737
|
+
const client = fakeClient({
|
|
738
|
+
agentExecution: { getArtifactContent: () => new Promise(() => {}) },
|
|
739
|
+
});
|
|
740
|
+
const out = renderWithClient(<FileDiffBody change={change} />, client).lastFrame() ?? "";
|
|
741
|
+
expect(out).toContain("Loading diff");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("reports a server-truncated offloaded side as un-diffable inline", async () => {
|
|
745
|
+
const change = changeWith({ id: "trunc", after: offloadedSide("artifacts/aex-1/after") });
|
|
746
|
+
const client = fakeClient({
|
|
747
|
+
agentExecution: {
|
|
748
|
+
getArtifactContent: async () => ({
|
|
749
|
+
content: new TextEncoder().encode("way too big"),
|
|
750
|
+
contentType: "text/plain",
|
|
751
|
+
truncated: true,
|
|
752
|
+
}),
|
|
753
|
+
// A truncated side activates useArtifactDownloadUrl (the notice offers a
|
|
754
|
+
// full-file download). Stub it so the fetch resolves instead of throwing
|
|
755
|
+
// on an undefined method — an unstubbed throw unmounts the ink tree and
|
|
756
|
+
// blanks the frame, which raced green locally but failed under CI load.
|
|
757
|
+
getArtifactDownloadUrl: async () => ({ downloadUrl: "https://example.test/full" }),
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
const { lastFrame } = renderWithClient(<FileDiffBody change={change} />, client);
|
|
761
|
+
await waitForFrame(lastFrame, "too large to diff inline");
|
|
762
|
+
expect(lastFrame() ?? "").toContain("too large to diff inline");
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("surfaces a fetch failure as an honest error notice", async () => {
|
|
766
|
+
const change = changeWith({ id: "err", after: offloadedSide("artifacts/aex-1/after") });
|
|
767
|
+
const client = fakeClient({
|
|
768
|
+
agentExecution: {
|
|
769
|
+
getArtifactContent: async () => {
|
|
770
|
+
throw new Error("boom");
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
const { lastFrame } = renderWithClient(<FileDiffBody change={change} />, client);
|
|
775
|
+
await waitForFrame(lastFrame, "Could not load this file's contents.");
|
|
776
|
+
expect(lastFrame() ?? "").toContain("Could not load this file's contents.");
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("caps an oversized diff and reports the overflow", () => {
|
|
780
|
+
const many = Array.from({ length: 250 }, (_, i) => `line ${i}`).join("\n");
|
|
781
|
+
const change = changeWith({ id: "big", before: inlineSide(""), after: inlineSide(`${many}\n`) });
|
|
782
|
+
const out = renderWithClient(<FileDiffBody change={change} />).lastFrame() ?? "";
|
|
783
|
+
expect(out).toMatch(/… \d+ more lines/);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
describe("FileReviewPrompt — inline diff (master-detail)", () => {
|
|
788
|
+
it("shows only the selected file's diff and follows the selection", async () => {
|
|
789
|
+
const set = makeSet({
|
|
790
|
+
status: FileChangeSetStatus.AWAITING_REVIEW,
|
|
791
|
+
changes: [
|
|
792
|
+
changeWith({ id: "a", before: inlineSide("alpha\n"), after: inlineSide("ALPHA\n") }),
|
|
793
|
+
changeWith({ id: "b", before: inlineSide("bravo\n"), after: inlineSide("BRAVO\n") }),
|
|
794
|
+
],
|
|
795
|
+
});
|
|
796
|
+
const { stdin, lastFrame } = renderWithClient(
|
|
797
|
+
<FileReviewPrompt changeSet={set} onSubmit={() => {}} />,
|
|
798
|
+
);
|
|
799
|
+
stdin.write("f");
|
|
800
|
+
await tick();
|
|
801
|
+
const first = lastFrame() ?? "";
|
|
802
|
+
expect(first).toContain("+ALPHA"); // selected file's diff
|
|
803
|
+
expect(first).not.toContain("+BRAVO"); // the other file's diff is not mounted
|
|
804
|
+
|
|
805
|
+
stdin.write("\u001B[B"); // down → select the second file
|
|
806
|
+
await tick();
|
|
807
|
+
const second = lastFrame() ?? "";
|
|
808
|
+
expect(second).toContain("+BRAVO");
|
|
809
|
+
expect(second).not.toContain("+ALPHA");
|
|
810
|
+
});
|
|
811
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { type ReactElement } from "react";
|
|
2
|
+
import { render } from "ink-testing-library";
|
|
3
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
4
|
+
import { InkStigmerProvider } from "../provider.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds a minimal Stigmer stub exposing only the methods a test's hooks call.
|
|
8
|
+
* Anything not overridden is absent, so a test that touches an unstubbed method
|
|
9
|
+
* fails loudly rather than silently no-op'ing.
|
|
10
|
+
*/
|
|
11
|
+
export function fakeClient(overrides: Record<string, unknown> = {}): Stigmer {
|
|
12
|
+
return overrides as unknown as Stigmer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders a tree under an {@link InkStigmerProvider} so hooks that read the
|
|
17
|
+
* Stigmer client (e.g. `useFileChangeContent` → `useStigmer`, which throws
|
|
18
|
+
* without a provider) work. Returns the `ink-testing-library` handle.
|
|
19
|
+
*/
|
|
20
|
+
export function renderWithClient(
|
|
21
|
+
node: ReactElement,
|
|
22
|
+
client: Stigmer = fakeClient(),
|
|
23
|
+
) {
|
|
24
|
+
return render(<InkStigmerProvider client={client}>{node}</InkStigmerProvider>);
|
|
25
|
+
}
|