bermudis-pi-goodies 0.27.7 → 0.27.9
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/README.md +9 -4
- package/clean-tui.ts +13 -0
- package/index.ts +1 -1
- package/package.json +1 -1
- package/prefer-tools.ts +212 -78
- package/provider-balance.ts +46 -6
- package/review.ts +125 -27
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ extensions. One entry point, thirteen independent features.
|
|
|
8
8
|
| `copy-with-model` | `/copy-with-model` | Copy last assistant message to the clipboard in a code fence tagged with the model name. |
|
|
9
9
|
| `copy-trajectory` | `/copy-trajectory [thinking]` | Copy the whole conversation (user + assistant text, tool calls stripped) to the clipboard; `thinking` also includes assistant thinking blocks. |
|
|
10
10
|
| `name-with-ai` | `/name-with-ai [name]` | Generate a short session name from the first user message (or set one manually). |
|
|
11
|
-
| `prefer-tools` | hook (no command) | Blocks legacy tools: `rm` → `trash <path>` (and `trash-rm`/`trash-empty`, which delete permanently),
|
|
11
|
+
| `prefer-tools` | hook (no command) | Blocks legacy tools: `rm` → `trash <path>` (and `trash-rm`/`trash-empty`, which delete permanently), `python`/`pip`/`pytest`/`mypy` are not blocked (removed 2026-10-24 — steering to `uv` is AGENTS.md's job, not the hook's). Bash `rg` is allowed, except the replace flag: `-r` (including clusters like `-rn`) is blocked because it is `--replace` and silently replaces matched text — use `--replace` explicitly if you mean it. The built-in grep tool still works. |
|
|
12
12
|
| `keep-model-on-new` | hook (no command) | Keep the active model when `/new` starts a fresh session instead of reverting to pi's saved default model. |
|
|
13
13
|
| `model-thinking` | `/model-thinking` | Per-model default thinking levels: save the current level as this model's default and get it back on every switch to that model, instead of pi's global default. |
|
|
14
14
|
| `clean-tui` | tool overrides (no command) | Collapse built-in tool output for a cleaner TUI: back-to-back same-tool calls share one block (e.g. `read ×2`) until a boundary — visible text (assistant prose or a typed user message) or a thinking block (even an empty one, as OpenAI emits between tool calls) — so reasoning-per-call models render one block per call. Images stay visible without expanding, expand a row with ctrl+o to see the full command and results/diffs. Long bash commands get an AI-generated summary once you pick a model with `/goodies summary-model <provider/model>` (see "Smart summaries" below) — expanding a row swaps the summary back out for the raw command. While enabled, also flips `@bermudi/pi-codex`'s `apply_patch`/`web_search` into the same burst style. Long thinking runs can show a live plain-English line above the editor (`/goodies thinking-summaries on`) instead of a static `Thinking...` — see "Smart summaries". |
|
|
@@ -25,7 +25,7 @@ extensions. One entry point, thirteen independent features.
|
|
|
25
25
|
First install, pinned to a published version:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
pi install npm:bermudis-pi-goodies@0.27.
|
|
28
|
+
pi install npm:bermudis-pi-goodies@0.27.9
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Picking up a new release on an existing install (`pi install` is not the
|
|
@@ -42,8 +42,13 @@ the balance footer). State persists to `~/.pi/agent/goodies.json`.
|
|
|
42
42
|
While enabled, the tool overrides carry the built-ins' full definitions —
|
|
43
43
|
schema, execution, and system-prompt contributions (`promptSnippet`,
|
|
44
44
|
`promptGuidelines`) — so the model sees exactly what vanilla pi would show it;
|
|
45
|
-
the extension is purely presentational.
|
|
46
|
-
|
|
45
|
+
the extension is purely presentational. They re-register with
|
|
46
|
+
`defaultActive: false`: registering a tool name in pi activates it by
|
|
47
|
+
default, which used to force `find`/`grep`/`ls` (pi's bash-less fallback
|
|
48
|
+
trio) into every session — registration is now strictly a renderer swap and
|
|
49
|
+
the active tool set stays exactly what vanilla pi would pick. Every feature
|
|
50
|
+
toggle, clean-tui included, takes effect on the next `/reload` or new
|
|
51
|
+
session.
|
|
47
52
|
Kilo's provider and its balance footer are bundled here.
|
|
48
53
|
|
|
49
54
|
## Smart summaries for long bash commands
|
package/clean-tui.ts
CHANGED
|
@@ -1383,6 +1383,19 @@ export default function cleanTui(pi: ExtensionAPI): void {
|
|
|
1383
1383
|
const definition = {
|
|
1384
1384
|
...native, // retain promptSnippet/Guidelines, schema, execution mode, preparation and all future metadata
|
|
1385
1385
|
...burst,
|
|
1386
|
+
// Renderer override ONLY — registration must never touch activation.
|
|
1387
|
+
// pi activates any direct-exposure tool on registration unless
|
|
1388
|
+
// defaultActive is false (agent-session _isActivatedOnRegistration),
|
|
1389
|
+
// so re-registering the built-in names here used to force-activate
|
|
1390
|
+
// find/grep/ls — pi keeps them for bash-less read-only sessions — in
|
|
1391
|
+
// every session, and symmetrically would force bash into one. With
|
|
1392
|
+
// defaultActive: false the definitions sit in the registry with their
|
|
1393
|
+
// burst renderers for whenever the tool IS active: already-active
|
|
1394
|
+
// tools stay active through re-registration (the refresh seeds the
|
|
1395
|
+
// next active set from the previous one), inactive ones stay inactive
|
|
1396
|
+
// until pi's own gating (base defaults, --tools/defaultTools, /tools,
|
|
1397
|
+
// setActiveTools) names them.
|
|
1398
|
+
defaultActive: false,
|
|
1386
1399
|
async execute(
|
|
1387
1400
|
toolCallId: string,
|
|
1388
1401
|
params: never,
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - copy-with-model /copy-with-model copy last reply tagged with the model
|
|
7
7
|
* - copy-trajectory /copy-trajectory copy the whole conversation (text only) to the clipboard
|
|
8
8
|
* - name-with-ai /name-with-ai generate a session name via the model
|
|
9
|
-
* - prefer-tools hook block legacy tools (trash
|
|
9
|
+
* - prefer-tools hook block legacy tools (trash; blocks rg's -r replace flag)
|
|
10
10
|
* - keep-model hook preserve the active model across /new
|
|
11
11
|
* - model-thinking /model-thinking per-model default thinking levels, applied on switch
|
|
12
12
|
* - clean-tui tool overrides collapse built-in tool output; keep a one-line call header, hide results/diffs until expanded
|
package/package.json
CHANGED
package/prefer-tools.ts
CHANGED
|
@@ -13,12 +13,17 @@
|
|
|
13
13
|
* rm -> trash <path> (moves to the trash can; recoverable)
|
|
14
14
|
* trash-rm -> blocked (permanently deletes from the trash can)
|
|
15
15
|
* trash-empty -> blocked (permanently empties the trash can)
|
|
16
|
-
* python/pip/pytest/ -> uv
|
|
17
|
-
* mypy
|
|
18
16
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* python/pip/pytest/mypy are deliberately NOT blocked (removed 2026-10-24):
|
|
18
|
+
* steering those is AGENTS.md's job ("uv always"), not the hook's — telling
|
|
19
|
+
* "inline" python apart from script-file python would need a bash-semantics
|
|
20
|
+
* matrix, and the uv block fired mostly as friction after the fact.
|
|
21
|
+
*
|
|
22
|
+
* `rg` is allowed, but its replace flag is not: `-r` is ripgrep's
|
|
23
|
+
* `--replace`, so `rg -rn` silently replaces matches with "n" instead of
|
|
24
|
+
* showing line numbers. Any short-option cluster containing `r` is blocked;
|
|
25
|
+
* spell out `--replace` if you actually mean it. The built-in grep tool
|
|
26
|
+
* (structured args, no raw flags) and bash `grep -rn` still work.
|
|
22
27
|
*/
|
|
23
28
|
import {
|
|
24
29
|
isToolCallEventType,
|
|
@@ -41,32 +46,16 @@ const RULES: Rule[] = [
|
|
|
41
46
|
reason:
|
|
42
47
|
"trash-rm and trash-empty are blocked — they delete permanently from the trash can. Use `trash <path>` to move files to the trash (recoverable).",
|
|
43
48
|
},
|
|
44
|
-
{
|
|
45
|
-
names: ["python", "python3", "pip", "pip3", "pytest", "mypy"],
|
|
46
|
-
reason:
|
|
47
|
-
"bare python/pip/pytest/mypy are blocked — use `uv` (e.g. `uv run python`, `uv add`, `uv pip install <pkg>`, `uv run pytest`/`mypy`)",
|
|
48
|
-
},
|
|
49
49
|
];
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* provider-prefixed "kilo/glm-5" all match. glm-4.x is unaffected.
|
|
52
|
+
* `rg -r` is ripgrep's `--replace`, not "recursive": it replaces matched text
|
|
53
|
+
* in the output, so `rg -rn` replaces with "n" and shows no line numbers
|
|
54
|
+
* (machine lore: it once spawned a phantom `[ln]` log tag). Block the short
|
|
55
|
+
* flag in any cluster, but allow the explicit long form as the escape hatch.
|
|
57
56
|
*/
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
typeof modelId === "string" && /(^|[^a-z0-9])glm-5(?![0-9])/i.test(modelId)
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Extra rule appended when the active model is glm-5.*. */
|
|
65
|
-
export const RG_RULE: Rule = {
|
|
66
|
-
names: ["rg"],
|
|
67
|
-
reason:
|
|
68
|
-
'rg is blocked for this model — it invents ripgrep flags. Use `grep -rn "pattern" path` instead (or pi\'s built-in grep tool).',
|
|
69
|
-
};
|
|
57
|
+
export const RG_REPLACE_REASON =
|
|
58
|
+
'`rg -r` is `--replace` — it silently replaces matched text in the output (`rg -rn` replaces with "n" instead of showing line numbers). If you really mean it, use `--replace` explicitly.';
|
|
70
59
|
|
|
71
60
|
const COMMAND_PREFIX_KEYWORDS = new Set([
|
|
72
61
|
"if",
|
|
@@ -86,8 +75,8 @@ const WORD_STOP = " \t\n\r|&;<>()\"'`$";
|
|
|
86
75
|
* `NAME=value` (or `NAME+=value`) in command position is an env-var
|
|
87
76
|
* assignment, never the command itself — bash runs the word *after* it.
|
|
88
77
|
* Without this check the lexer treated `FOO=bar` as the command, found no
|
|
89
|
-
* rule match, demoted command position, and `rm
|
|
90
|
-
* a mere argument (`FOO=bar rm x
|
|
78
|
+
* rule match, demoted command position, and `rm` sailed through as
|
|
79
|
+
* a mere argument (`FOO=bar rm x`).
|
|
91
80
|
* The name must be a portable shell identifier, matching bash's own rule —
|
|
92
81
|
* `1=2 x` is not an assignment in bash either (it is a command name, so the
|
|
93
82
|
* real command never runs). readWord stops at `$`/quotes, so `FOO="a b"` and
|
|
@@ -97,19 +86,34 @@ function isAssignmentWord(word: string): boolean {
|
|
|
97
86
|
return /^[A-Za-z_][A-Za-z0-9_]*\+?=/.test(word);
|
|
98
87
|
}
|
|
99
88
|
|
|
100
|
-
function
|
|
101
|
-
name:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (const rule of rules) {
|
|
89
|
+
function commandBase(name: string): string {
|
|
90
|
+
return name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function matchCommand(name: string): string | undefined {
|
|
94
|
+
const base = commandBase(name);
|
|
95
|
+
for (const rule of RULES) {
|
|
108
96
|
if (rule.names.includes(base)) return rule.reason;
|
|
109
97
|
}
|
|
110
98
|
return undefined;
|
|
111
99
|
}
|
|
112
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Match a command word, plus the one argument-sensitive rule: an `rg`
|
|
103
|
+
* invocation that uses the replace flag. `argStart` is the offset just past
|
|
104
|
+
* the command word, where its arguments begin.
|
|
105
|
+
*/
|
|
106
|
+
function matchCommandAt(
|
|
107
|
+
command: string,
|
|
108
|
+
word: string,
|
|
109
|
+
argStart: number,
|
|
110
|
+
): string | undefined {
|
|
111
|
+
if (commandBase(word) === "rg" && rgUsesReplaceFlag(command, argStart)) {
|
|
112
|
+
return RG_REPLACE_REASON;
|
|
113
|
+
}
|
|
114
|
+
return matchCommand(word);
|
|
115
|
+
}
|
|
116
|
+
|
|
113
117
|
function readWord(s: string, i: number): { word: string; next: number } {
|
|
114
118
|
// Build the word char by char so backslash escapes strip the backslash:
|
|
115
119
|
// `\rm` → `rm`, `foo\ bar` → `foo bar`. The old slice-based version kept
|
|
@@ -253,7 +257,6 @@ function scanExecutedSubs(
|
|
|
253
257
|
s: string,
|
|
254
258
|
start: number,
|
|
255
259
|
end: number,
|
|
256
|
-
rules: readonly Rule[],
|
|
257
260
|
mode: "plain" | "double",
|
|
258
261
|
): string | undefined {
|
|
259
262
|
let i = start;
|
|
@@ -273,7 +276,7 @@ function scanExecutedSubs(
|
|
|
273
276
|
if (c === '"') {
|
|
274
277
|
const stop = readQuote(s, i, '"', true);
|
|
275
278
|
const innerEnd = s[stop - 1] === '"' ? stop - 1 : stop;
|
|
276
|
-
const reason = scanExecutedSubs(s, i + 1, innerEnd,
|
|
279
|
+
const reason = scanExecutedSubs(s, i + 1, innerEnd, "double");
|
|
277
280
|
if (reason) return reason;
|
|
278
281
|
i = stop;
|
|
279
282
|
continue;
|
|
@@ -293,7 +296,7 @@ function scanExecutedSubs(
|
|
|
293
296
|
if (c === "`") {
|
|
294
297
|
const stop = readQuote(s, i, "`", true);
|
|
295
298
|
const innerEnd = s[stop - 1] === "`" ? stop - 1 : stop;
|
|
296
|
-
const reason = detectLegacyTool(s.slice(i + 1, innerEnd)
|
|
299
|
+
const reason = detectLegacyTool(s.slice(i + 1, innerEnd));
|
|
297
300
|
if (reason) return reason;
|
|
298
301
|
i = stop;
|
|
299
302
|
continue;
|
|
@@ -306,7 +309,7 @@ function scanExecutedSubs(
|
|
|
306
309
|
const stop = skipBalancedParens(s, i, 3);
|
|
307
310
|
const innerEnd =
|
|
308
311
|
s[stop - 1] === ")" && s[stop - 2] === ")" ? stop - 2 : stop;
|
|
309
|
-
const reason = scanExecutedSubs(s, i + 3, innerEnd,
|
|
312
|
+
const reason = scanExecutedSubs(s, i + 3, innerEnd, mode);
|
|
310
313
|
if (reason) return reason;
|
|
311
314
|
i = stop;
|
|
312
315
|
continue;
|
|
@@ -314,7 +317,7 @@ function scanExecutedSubs(
|
|
|
314
317
|
if (c === "$" && s.startsWith("$(", i)) {
|
|
315
318
|
const stop = skipBalancedParens(s, i, 2);
|
|
316
319
|
const innerEnd = s[stop - 1] === ")" ? stop - 1 : stop;
|
|
317
|
-
const reason = detectLegacyTool(s.slice(i + 2, innerEnd)
|
|
320
|
+
const reason = detectLegacyTool(s.slice(i + 2, innerEnd));
|
|
318
321
|
if (reason) return reason;
|
|
319
322
|
i = stop;
|
|
320
323
|
continue;
|
|
@@ -327,7 +330,7 @@ function scanExecutedSubs(
|
|
|
327
330
|
// substitution runs (`"${X:-'$(rm)'}"` executes).
|
|
328
331
|
const stop = skipBalancedBraces(s, i);
|
|
329
332
|
const innerEnd = s[stop - 1] === "}" ? stop - 1 : stop;
|
|
330
|
-
const reason = scanExecutedSubs(s, i + 2, innerEnd,
|
|
333
|
+
const reason = scanExecutedSubs(s, i + 2, innerEnd, mode);
|
|
331
334
|
if (reason) return reason;
|
|
332
335
|
i = stop;
|
|
333
336
|
continue;
|
|
@@ -340,7 +343,6 @@ function scanExecutedSubs(
|
|
|
340
343
|
function readDollar(
|
|
341
344
|
s: string,
|
|
342
345
|
i: number,
|
|
343
|
-
rules: readonly Rule[],
|
|
344
346
|
): { next: number; reason?: string } | null {
|
|
345
347
|
if (i >= s.length || s[i] !== "$") return null;
|
|
346
348
|
|
|
@@ -354,14 +356,14 @@ function readDollar(
|
|
|
354
356
|
const innerEnd = s[end - 1] === ")" && s[end - 2] === ")" ? end - 2 : end;
|
|
355
357
|
return {
|
|
356
358
|
next: end,
|
|
357
|
-
reason: scanExecutedSubs(s, i + 3, innerEnd,
|
|
359
|
+
reason: scanExecutedSubs(s, i + 3, innerEnd, "plain"),
|
|
358
360
|
};
|
|
359
361
|
}
|
|
360
362
|
if (s.startsWith("(", i + 1)) {
|
|
361
363
|
const end = skipBalancedParens(s, i, 2);
|
|
362
364
|
const closeParen = end > 0 && s[end - 1] === ")" ? 1 : 0;
|
|
363
365
|
const inner = s.slice(i + 2, end - closeParen);
|
|
364
|
-
const reason = detectLegacyTool(inner
|
|
366
|
+
const reason = detectLegacyTool(inner);
|
|
365
367
|
return { next: end, reason };
|
|
366
368
|
}
|
|
367
369
|
if (s.startsWith("{", i + 1)) {
|
|
@@ -370,7 +372,7 @@ function readDollar(
|
|
|
370
372
|
const innerEnd = s[end - 1] === "}" ? end - 1 : end;
|
|
371
373
|
return {
|
|
372
374
|
next: end,
|
|
373
|
-
reason: scanExecutedSubs(s, i + 2, innerEnd,
|
|
375
|
+
reason: scanExecutedSubs(s, i + 2, innerEnd, "plain"),
|
|
374
376
|
};
|
|
375
377
|
}
|
|
376
378
|
if (i + 1 < s.length && /[0-9?@*#\-!$]/.test(s[i + 1])) {
|
|
@@ -447,10 +449,7 @@ function readHeredocDelimiter(
|
|
|
447
449
|
* backslash suppresses expansion (`\$(rm)` is literal). Returns the matched
|
|
448
450
|
* rule's reason if a blocked tool is found, undefined otherwise.
|
|
449
451
|
*/
|
|
450
|
-
function scanHeredocLineForCommandSubs(
|
|
451
|
-
line: string,
|
|
452
|
-
rules: readonly Rule[],
|
|
453
|
-
): string | undefined {
|
|
452
|
+
function scanHeredocLineForCommandSubs(line: string): string | undefined {
|
|
454
453
|
let i = 0;
|
|
455
454
|
while (i < line.length) {
|
|
456
455
|
const c = line[i];
|
|
@@ -461,7 +460,7 @@ function scanHeredocLineForCommandSubs(
|
|
|
461
460
|
if (line.startsWith("$(", i)) {
|
|
462
461
|
const end = skipBalancedParens(line, i, 2);
|
|
463
462
|
const inner = line.slice(i + 2, end - 1);
|
|
464
|
-
const reason = detectLegacyTool(inner
|
|
463
|
+
const reason = detectLegacyTool(inner);
|
|
465
464
|
if (reason) return reason;
|
|
466
465
|
i = end;
|
|
467
466
|
continue;
|
|
@@ -469,7 +468,7 @@ function scanHeredocLineForCommandSubs(
|
|
|
469
468
|
if (c === "`") {
|
|
470
469
|
const end = readQuote(line, i, "`", true);
|
|
471
470
|
const inner = line.slice(i + 1, end - 1);
|
|
472
|
-
const reason = detectLegacyTool(inner
|
|
471
|
+
const reason = detectLegacyTool(inner);
|
|
473
472
|
if (reason) return reason;
|
|
474
473
|
i = end;
|
|
475
474
|
continue;
|
|
@@ -485,7 +484,6 @@ function skipHeredocBody(
|
|
|
485
484
|
delimiter: string,
|
|
486
485
|
indented: boolean,
|
|
487
486
|
quoted: boolean,
|
|
488
|
-
rules: readonly Rule[],
|
|
489
487
|
): { next: number; reason?: string } {
|
|
490
488
|
let pos = i;
|
|
491
489
|
while (pos <= s.length) {
|
|
@@ -499,7 +497,7 @@ function skipHeredocBody(
|
|
|
499
497
|
// In unquoted heredocs, command substitutions execute — scan for
|
|
500
498
|
// blocked tools inside $(...) and backticks.
|
|
501
499
|
if (!quoted) {
|
|
502
|
-
const reason = scanHeredocLineForCommandSubs(line
|
|
500
|
+
const reason = scanHeredocLineForCommandSubs(line);
|
|
503
501
|
if (reason) return { next: end, reason };
|
|
504
502
|
}
|
|
505
503
|
if (nl === -1) break;
|
|
@@ -530,11 +528,157 @@ const SUDO_OPTS_WITH_ARG = new Set([
|
|
|
530
528
|
"--user",
|
|
531
529
|
]);
|
|
532
530
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
|
|
531
|
+
/**
|
|
532
|
+
* Short rg options that consume a value: the rest of the cluster is their
|
|
533
|
+
* value, or — at cluster end — the next word is. `r` is handled separately
|
|
534
|
+
* (and blocked) before this set is consulted.
|
|
535
|
+
*/
|
|
536
|
+
const RG_SHORT_VALUE_OPTS = new Set([
|
|
537
|
+
"A",
|
|
538
|
+
"B",
|
|
539
|
+
"C",
|
|
540
|
+
"d",
|
|
541
|
+
"E",
|
|
542
|
+
"e",
|
|
543
|
+
"f",
|
|
544
|
+
"g",
|
|
545
|
+
"j",
|
|
546
|
+
"M",
|
|
547
|
+
"m",
|
|
548
|
+
"t",
|
|
549
|
+
"T",
|
|
550
|
+
]);
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Long rg options that consume the next word as a value. Values are skipped
|
|
554
|
+
* before flag inspection, so `--field-match-separator -r` (ripgrep accepts
|
|
555
|
+
* `-r` there as literal output text) is not mistaken for the replace flag.
|
|
556
|
+
*/
|
|
557
|
+
const RG_LONG_VALUE_OPTS = new Set([
|
|
558
|
+
"--after-context",
|
|
559
|
+
"--before-context",
|
|
560
|
+
"--context",
|
|
561
|
+
"--encoding",
|
|
562
|
+
"--field-context-separator",
|
|
563
|
+
"--field-match-separator",
|
|
564
|
+
"--file",
|
|
565
|
+
"--glob",
|
|
566
|
+
"--iglob",
|
|
567
|
+
"--max-columns",
|
|
568
|
+
"--max-count",
|
|
569
|
+
"--max-depth",
|
|
570
|
+
"--max-filesize",
|
|
571
|
+
"--path-separator",
|
|
572
|
+
"--pre",
|
|
573
|
+
"--regexp",
|
|
574
|
+
"--replace",
|
|
575
|
+
"--sort",
|
|
576
|
+
"--sortr",
|
|
577
|
+
"--threads",
|
|
578
|
+
"--type",
|
|
579
|
+
"--type-add",
|
|
580
|
+
"--type-clear",
|
|
581
|
+
"--type-not",
|
|
582
|
+
]);
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Read one shell word after an `rg` command, keeping quoted content as part
|
|
586
|
+
* of the token so `rg "-r"` and `rg '-rn'` are caught like their unquoted
|
|
587
|
+
* twins. Stops at whitespace or an unquoted operator.
|
|
588
|
+
*/
|
|
589
|
+
function readArgWord(s: string, i: number): { word: string; next: number } {
|
|
590
|
+
let word = "";
|
|
591
|
+
while (i < s.length) {
|
|
592
|
+
const c = s[i];
|
|
593
|
+
if (c === " " || c === "\t" || c === "\r" || c === "\n") break;
|
|
594
|
+
if (readOperator(s, i)) break;
|
|
595
|
+
if (c === "'" || c === '"') {
|
|
596
|
+
const end = readQuote(s, i, c, c === '"');
|
|
597
|
+
const innerEnd = s[end - 1] === c ? end - 1 : end;
|
|
598
|
+
word += s.slice(i + 1, innerEnd);
|
|
599
|
+
i = end;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
603
|
+
word += s[i + 1];
|
|
604
|
+
i += 2;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (c === "$") {
|
|
608
|
+
const d = readDollar(s, i);
|
|
609
|
+
if (d) {
|
|
610
|
+
word += s.slice(i, d.next);
|
|
611
|
+
i = d.next;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
word += c;
|
|
616
|
+
i++;
|
|
617
|
+
}
|
|
618
|
+
return { word, next: i };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* True when the `rg` invocation starting at `start` uses the replace flag in
|
|
623
|
+
* short form, including clusters (`-rn`, `-rln`, `-nr`). The explicit
|
|
624
|
+
* `--replace` long form stays allowed. Value-taking options are skipped so a
|
|
625
|
+
* pattern/replacement that merely looks like `-r` is not a false hit.
|
|
626
|
+
*/
|
|
627
|
+
function rgUsesReplaceFlag(s: string, start: number): boolean {
|
|
628
|
+
let i = start;
|
|
629
|
+
let skipValue = false;
|
|
630
|
+
while (i < s.length) {
|
|
631
|
+
const c = s[i];
|
|
632
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
633
|
+
i++;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (c === "\n") break;
|
|
637
|
+
// Unquoted `#` at the start of a word begins a shell comment: bash never
|
|
638
|
+
// passes it (or anything after it) to rg, so `rg -n foo . # avoid -rn`
|
|
639
|
+
// must not be blocked for what its comment merely mentions. Mid-word `#`
|
|
640
|
+
// (`rg foo#bar`) stays inside the word read below, matching bash.
|
|
641
|
+
if (c === "#") break;
|
|
642
|
+
const op = readOperator(s, i);
|
|
643
|
+
if (op) {
|
|
644
|
+
if (op.type !== "redirect") break;
|
|
645
|
+
// Redirections may appear among a command's arguments; consume the
|
|
646
|
+
// target (`>out`) and keep looking at the real rg options after it.
|
|
647
|
+
i = op.next;
|
|
648
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
649
|
+
if (i < s.length) i = readArgWord(s, i).next;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const { word, next } = readArgWord(s, i);
|
|
653
|
+
if (next === i) {
|
|
654
|
+
i++;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
i = next;
|
|
658
|
+
if (skipValue) {
|
|
659
|
+
skipValue = false;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (word === "--") break;
|
|
663
|
+
if (word.startsWith("--")) {
|
|
664
|
+
if (!word.includes("=") && RG_LONG_VALUE_OPTS.has(word)) skipValue = true;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (!word.startsWith("-") || word === "-") continue;
|
|
668
|
+
for (let k = 1; k < word.length; k++) {
|
|
669
|
+
const flag = word[k];
|
|
670
|
+
if (flag === "r") return true;
|
|
671
|
+
if (RG_SHORT_VALUE_OPTS.has(flag)) {
|
|
672
|
+
// The rest of the cluster (or the next word) is this option's value.
|
|
673
|
+
if (k === word.length - 1) skipValue = true;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function detectLegacyTool(command: string): string | undefined {
|
|
538
682
|
let i = 0;
|
|
539
683
|
let commandPos = true;
|
|
540
684
|
// Set once an env-var assignment word is seen in the current simple
|
|
@@ -560,7 +704,6 @@ export function detectLegacyTool(
|
|
|
560
704
|
heredoc.delimiter,
|
|
561
705
|
heredoc.indented,
|
|
562
706
|
heredoc.quoted,
|
|
563
|
-
rules,
|
|
564
707
|
);
|
|
565
708
|
if (result.reason) return result.reason;
|
|
566
709
|
i = result.next;
|
|
@@ -671,17 +814,11 @@ export function detectLegacyTool(
|
|
|
671
814
|
// their unquoted twin.
|
|
672
815
|
if (quote === '"') {
|
|
673
816
|
const innerEnd = command[end - 1] === '"' ? end - 1 : end;
|
|
674
|
-
const reason = scanExecutedSubs(
|
|
675
|
-
command,
|
|
676
|
-
i + 1,
|
|
677
|
-
innerEnd,
|
|
678
|
-
rules,
|
|
679
|
-
"double",
|
|
680
|
-
);
|
|
817
|
+
const reason = scanExecutedSubs(command, i + 1, innerEnd, "double");
|
|
681
818
|
if (reason) return reason;
|
|
682
819
|
} else if (quote === "`") {
|
|
683
820
|
const innerEnd = command[end - 1] === "`" ? end - 1 : end;
|
|
684
|
-
const reason = detectLegacyTool(command.slice(i + 1, innerEnd)
|
|
821
|
+
const reason = detectLegacyTool(command.slice(i + 1, innerEnd));
|
|
685
822
|
if (reason) return reason;
|
|
686
823
|
}
|
|
687
824
|
i = end;
|
|
@@ -694,7 +831,7 @@ export function detectLegacyTool(
|
|
|
694
831
|
}
|
|
695
832
|
|
|
696
833
|
if (c === "$") {
|
|
697
|
-
const d = readDollar(command, i
|
|
834
|
+
const d = readDollar(command, i);
|
|
698
835
|
if (d) {
|
|
699
836
|
if (d.reason) return d.reason;
|
|
700
837
|
i = d.next;
|
|
@@ -730,7 +867,7 @@ export function detectLegacyTool(
|
|
|
730
867
|
continue;
|
|
731
868
|
}
|
|
732
869
|
// Env-var assignment prefix: skip it and stay in command position so
|
|
733
|
-
// the word after it (`rm`,
|
|
870
|
+
// the word after it (`rm`, …) is the one checked. Under
|
|
734
871
|
// sudo it likewise keeps sudoNext armed (`sudo FOO=bar rm x`).
|
|
735
872
|
if (isAssignmentWord(word) && !skipNextWord) {
|
|
736
873
|
assignmentPrefix = true;
|
|
@@ -746,7 +883,7 @@ export function detectLegacyTool(
|
|
|
746
883
|
skipNextWord = false;
|
|
747
884
|
continue;
|
|
748
885
|
}
|
|
749
|
-
const reason =
|
|
886
|
+
const reason = matchCommandAt(command, word, i);
|
|
750
887
|
if (reason) return reason;
|
|
751
888
|
sudoNext = false;
|
|
752
889
|
} else {
|
|
@@ -754,7 +891,7 @@ export function detectLegacyTool(
|
|
|
754
891
|
skipNextWord = false;
|
|
755
892
|
continue;
|
|
756
893
|
}
|
|
757
|
-
const reason =
|
|
894
|
+
const reason = matchCommandAt(command, word, i);
|
|
758
895
|
if (reason) return reason;
|
|
759
896
|
}
|
|
760
897
|
if (!COMMAND_PREFIX_KEYWORDS.has(word)) {
|
|
@@ -769,13 +906,10 @@ export function detectLegacyTool(
|
|
|
769
906
|
}
|
|
770
907
|
|
|
771
908
|
export default function preferTools(pi: ExtensionAPI) {
|
|
772
|
-
pi.on("tool_call", async (event
|
|
909
|
+
pi.on("tool_call", async (event) => {
|
|
773
910
|
if (!isToolCallEventType("bash", event)) return;
|
|
774
911
|
|
|
775
|
-
|
|
776
|
-
// read per call — model switches mid-session take effect immediately).
|
|
777
|
-
const extraRules = isRipgrepBlockedModel(ctx.model?.id) ? [RG_RULE] : [];
|
|
778
|
-
const reason = detectLegacyTool(event.input.command ?? "", extraRules);
|
|
912
|
+
const reason = detectLegacyTool(event.input.command ?? "");
|
|
779
913
|
if (reason) {
|
|
780
914
|
return { block: true, reason };
|
|
781
915
|
}
|
package/provider-balance.ts
CHANGED
|
@@ -1733,10 +1733,19 @@ export default function providerBalance(
|
|
|
1733
1733
|
return;
|
|
1734
1734
|
}
|
|
1735
1735
|
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
|
|
1739
|
-
|
|
1736
|
+
// Hide the balance while credentials are being resolved — but only when
|
|
1737
|
+
// nothing is displayed yet (cold start, provider switch, post-logout).
|
|
1738
|
+
// A same-account refresh keeps its last value on screen: blanking on
|
|
1739
|
+
// every refresh would flicker the footer for the full duration of
|
|
1740
|
+
// keychain reads and OAuth token refreshes (seconds under lock
|
|
1741
|
+
// contention). If resolution reveals a different account, the
|
|
1742
|
+
// displayedCacheKey mismatch below still clears it, so a foreign
|
|
1743
|
+
// reading is never painted. Same principle as the post-fetch re-check
|
|
1744
|
+
// below: never blank a displayed same-account value.
|
|
1745
|
+
if (!balance) {
|
|
1746
|
+
identityPending = true;
|
|
1747
|
+
requestRender?.();
|
|
1748
|
+
}
|
|
1740
1749
|
|
|
1741
1750
|
// Preserve a known same-account value during routine credential refresh,
|
|
1742
1751
|
// but clear it if identity resolution fails or reveals another account.
|
|
@@ -1831,6 +1840,14 @@ export default function providerBalance(
|
|
|
1831
1840
|
);
|
|
1832
1841
|
const observedAtMs = now();
|
|
1833
1842
|
if (generation !== refreshGeneration) return;
|
|
1843
|
+
if (nextBalance.length === 0) {
|
|
1844
|
+
// An empty reading would render as an empty string — visually
|
|
1845
|
+
// identical to the balance vanishing — and parseCachedBalance already
|
|
1846
|
+
// refuses to serve empty entries from the cache. Treat it as a failed
|
|
1847
|
+
// observation: the catch below keeps the previous display and records
|
|
1848
|
+
// the failure.
|
|
1849
|
+
throw new Error("provider returned no balance segments");
|
|
1850
|
+
}
|
|
1834
1851
|
|
|
1835
1852
|
// Login/logout does not emit model_select. Re-resolve identity before
|
|
1836
1853
|
// committing so an account switch during the request cannot paint the
|
|
@@ -1910,7 +1927,14 @@ export default function providerBalance(
|
|
|
1910
1927
|
companionTokenAfter,
|
|
1911
1928
|
);
|
|
1912
1929
|
}
|
|
1913
|
-
// Recovery detected:
|
|
1930
|
+
// Recovery detected: tell the log once, then clear the marker so
|
|
1931
|
+
// sibling idle polls resume.
|
|
1932
|
+
if (readBalanceFailureAt(cacheKey, now(), cacheDir) !== null) {
|
|
1933
|
+
logGoodiesEvent({
|
|
1934
|
+
type: "provider_balance_refresh_recovered",
|
|
1935
|
+
provider: providerId,
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1914
1938
|
clearBalanceFailureMarker(cacheKey, cacheDir);
|
|
1915
1939
|
const persisted = writeCachedBalance(
|
|
1916
1940
|
cacheKey,
|
|
@@ -1929,12 +1953,28 @@ export default function providerBalance(
|
|
|
1929
1953
|
return;
|
|
1930
1954
|
}
|
|
1931
1955
|
identityPending = false;
|
|
1932
|
-
|
|
1956
|
+
// A failed fetch does not make the displayed value wrong — it stays
|
|
1957
|
+
// TTL-gated at render time. Keep a same-account reading on screen
|
|
1958
|
+
// instead of blanking the footer for the whole backoff window; clear
|
|
1959
|
+
// only when the failure landed before any account was identified.
|
|
1960
|
+
if (cacheKey === undefined || cacheKey !== displayedCacheKey) {
|
|
1961
|
+
clearBalance();
|
|
1962
|
+
}
|
|
1933
1963
|
// Record the failure machine-wide so sibling sessions' idle polls back
|
|
1934
1964
|
// off instead of stacking their own timeouts onto the same outage.
|
|
1965
|
+
const firstFailureOfStreak =
|
|
1966
|
+
cacheKey === undefined ||
|
|
1967
|
+
readBalanceFailureAt(cacheKey, now(), cacheDir) === null;
|
|
1935
1968
|
if (cacheKey !== undefined) {
|
|
1936
1969
|
writeBalanceFailureMarker(cacheKey, now(), cacheDir);
|
|
1937
1970
|
}
|
|
1971
|
+
if (firstFailureOfStreak) {
|
|
1972
|
+
logGoodiesEvent({
|
|
1973
|
+
type: "provider_balance_refresh_failed",
|
|
1974
|
+
provider: providerId,
|
|
1975
|
+
message: describeError(error),
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1938
1978
|
// This is a best-effort background refresh. Writing to stdout/stderr while
|
|
1939
1979
|
// Pi owns the terminal corrupts the TUI (the text appears in the editor),
|
|
1940
1980
|
// so expose failures to other extensions without producing terminal output.
|
package/review.ts
CHANGED
|
@@ -54,6 +54,15 @@
|
|
|
54
54
|
* ctx.ui.select dialogs when ctx.mode !== "tui", the end-review loader
|
|
55
55
|
* only renders in TUI mode, and the review widget renders as string
|
|
56
56
|
* lines outside the TUI
|
|
57
|
+
* - /end-review "Return and fix findings" fast path: the review branch's
|
|
58
|
+
* final assistant response already IS the handoff, so capture it verbatim
|
|
59
|
+
* (getFinalReviewMessage: walk the leaf path back to the common ancestor
|
|
60
|
+
* with the origin, the same boundary pi's summarizer would use), return
|
|
61
|
+
* with summarize:false (no LLM call), and queue the fix follow-up with the
|
|
62
|
+
* review embedded in the prompt (buildFastFixPrompt). Upstream f1de050
|
|
63
|
+
* (still its tip as of 2026-10-24) pays a full branch-summary round trip
|
|
64
|
+
* first via navigateWithSummary; "Return and summarize" keeps that flow.
|
|
65
|
+
* If upstream grows a comparable fast path, prefer the port.
|
|
57
66
|
*/
|
|
58
67
|
|
|
59
68
|
import type {
|
|
@@ -75,6 +84,7 @@ import {
|
|
|
75
84
|
} from "@earendil-works/pi-tui";
|
|
76
85
|
import path from "node:path";
|
|
77
86
|
import { promises as fs } from "node:fs";
|
|
87
|
+
import type { TextContent } from "@earendil-works/pi-ai";
|
|
78
88
|
import { describeError } from "./json-file.ts";
|
|
79
89
|
|
|
80
90
|
// State to track fresh session review (where we branched from).
|
|
@@ -173,6 +183,76 @@ function applyReviewSettings(ctx: ExtensionContext) {
|
|
|
173
183
|
reviewCustomInstructions = state.customInstructions?.trim() || undefined;
|
|
174
184
|
}
|
|
175
185
|
|
|
186
|
+
// Fix-findings follow-up instructions shared by the fast-path prompt.
|
|
187
|
+
const REVIEW_FIX_INSTRUCTIONS = `Instructions:
|
|
188
|
+
1. Treat the review's findings as a checklist.
|
|
189
|
+
2. Fix in priority order: P0, P1, then P2 (include P3 if quick and safe).
|
|
190
|
+
3. If a finding is invalid/already fixed/not possible right now, briefly explain why and continue.
|
|
191
|
+
4. Treat "Human Reviewer Callouts (Non-Blocking)" as informational only; do not convert them into fix tasks unless there is a separate explicit finding.
|
|
192
|
+
5. Follow fail-fast error handling: do not add local catch/fallback recovery unless this scope is an explicit boundary that can safely translate the failure.
|
|
193
|
+
6. If you add or keep a \`try/catch\`, explain the expected failure mode and either rethrow with context or return a boundary-safe error response.
|
|
194
|
+
7. JSON parsing/decoding should fail loudly by default; avoid silent fallback parsing.
|
|
195
|
+
8. Run relevant tests/checks for touched code where practical.
|
|
196
|
+
9. End with: fixed items, deferred/skipped items (with reasons), and verification results.`;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build the fix-findings follow-up for the fast path: the verbatim review
|
|
200
|
+
* rides in the prompt, so no branch-summary LLM round trip is needed.
|
|
201
|
+
*/
|
|
202
|
+
export function buildFastFixPrompt(review: string): string {
|
|
203
|
+
return `Implement the findings in the following review. The review ran on a separate branch of this session that is no longer in context — the review text below is the complete handoff.
|
|
204
|
+
|
|
205
|
+
${REVIEW_FIX_INSTRUCTIONS}
|
|
206
|
+
|
|
207
|
+
<review>
|
|
208
|
+
${review}
|
|
209
|
+
</review>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Capture the review branch's final assistant response (the review itself).
|
|
214
|
+
*
|
|
215
|
+
* The review branch diverged from the main branch, so the origin entry is
|
|
216
|
+
* NOT on the leaf's path — the boundary is the common ancestor of the two
|
|
217
|
+
* paths (the same boundary pi's branch summarizer collects entries for).
|
|
218
|
+
* Returns the newest non-empty assistant text between the leaf and that
|
|
219
|
+
* boundary, or undefined when the review branch holds no completed review
|
|
220
|
+
* response (or provenance cannot be established — stale origin, disjoint
|
|
221
|
+
* paths), in which case the caller must not pretend there is a review.
|
|
222
|
+
*/
|
|
223
|
+
export function getFinalReviewMessage(
|
|
224
|
+
ctx: Pick<ExtensionCommandContext, "sessionManager">,
|
|
225
|
+
originId: string,
|
|
226
|
+
): string | undefined {
|
|
227
|
+
const leafPath = ctx.sessionManager.getBranch(); // leaf → root
|
|
228
|
+
const originPathIds = new Set(
|
|
229
|
+
ctx.sessionManager.getBranch(originId).map((entry) => entry.id),
|
|
230
|
+
);
|
|
231
|
+
if (originPathIds.size === 0) {
|
|
232
|
+
return undefined; // unknown origin: refuse instead of grabbing main-branch text
|
|
233
|
+
}
|
|
234
|
+
// Both paths end at the root, so the first shared entry on the leaf path
|
|
235
|
+
// is the common ancestor. Review-branch entries strictly precede it.
|
|
236
|
+
const boundaryIndex = leafPath.findIndex((entry) =>
|
|
237
|
+
originPathIds.has(entry.id),
|
|
238
|
+
);
|
|
239
|
+
if (boundaryIndex === -1) {
|
|
240
|
+
return undefined; // disjoint paths (orphaned entries): provenance unprovable
|
|
241
|
+
}
|
|
242
|
+
for (let i = 0; i < boundaryIndex; i++) {
|
|
243
|
+
const entry = leafPath[i];
|
|
244
|
+
if (entry.type !== "message") continue;
|
|
245
|
+
const message = entry.message;
|
|
246
|
+
if (message.role !== "assistant") continue;
|
|
247
|
+
const text = message.content
|
|
248
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
249
|
+
.map((part) => part.text)
|
|
250
|
+
.join("\n\n");
|
|
251
|
+
if (text.trim()) return text;
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
176
256
|
// Review target types (matching Codex's approach)
|
|
177
257
|
type ReviewTarget =
|
|
178
258
|
| { type: "uncommitted" }
|
|
@@ -1984,19 +2064,6 @@ These are informational callouts for humans and are not fix items by themselves.
|
|
|
1984
2064
|
|
|
1985
2065
|
Preserve exact file paths, function names, and error messages where available.`;
|
|
1986
2066
|
|
|
1987
|
-
const REVIEW_FIX_FINDINGS_PROMPT = `Use the latest review summary in this session and implement the review findings now.
|
|
1988
|
-
|
|
1989
|
-
Instructions:
|
|
1990
|
-
1. Treat the summary's Findings/Fix Queue as a checklist.
|
|
1991
|
-
2. Fix in priority order: P0, P1, then P2 (include P3 if quick and safe).
|
|
1992
|
-
3. If a finding is invalid/already fixed/not possible right now, briefly explain why and continue.
|
|
1993
|
-
4. Treat "Human Reviewer Callouts (Non-Blocking)" as informational only; do not convert them into fix tasks unless there is a separate explicit finding.
|
|
1994
|
-
5. Follow fail-fast error handling: do not add local catch/fallback recovery unless this scope is an explicit boundary that can safely translate the failure.
|
|
1995
|
-
6. If you add or keep a \`try/catch\`, explain the expected failure mode and either rethrow with context or return a boundary-safe error response.
|
|
1996
|
-
7. JSON parsing/decoding should fail loudly by default; avoid silent fallback parsing.
|
|
1997
|
-
8. Run relevant tests/checks for touched code where practical.
|
|
1998
|
-
9. End with: fixed items, deferred/skipped items (with reasons), and verification results.`;
|
|
1999
|
-
|
|
2000
2067
|
type EndReviewAction = "returnOnly" | "returnAndFix" | "returnAndSummarize";
|
|
2001
2068
|
type EndReviewActionResult = "ok" | "cancelled" | "error";
|
|
2002
2069
|
type EndReviewActionOptions = {
|
|
@@ -2171,6 +2238,48 @@ Instructions:
|
|
|
2171
2238
|
return "ok";
|
|
2172
2239
|
}
|
|
2173
2240
|
|
|
2241
|
+
if (action === "returnAndFix") {
|
|
2242
|
+
// Fast path: the review's final assistant response already is the
|
|
2243
|
+
// handoff — carry it across the branch boundary verbatim instead of
|
|
2244
|
+
// paying a branch-summary LLM round trip before the fixing turn can
|
|
2245
|
+
// even start. Returning is a pure tree move (summarize: false).
|
|
2246
|
+
const reviewText = getFinalReviewMessage(ctx, originId);
|
|
2247
|
+
if (!reviewText) {
|
|
2248
|
+
ctx.ui.notify(
|
|
2249
|
+
"No completed review response found on the review branch — nothing to fix. Use /end-review → Return and summarize if you still want a branch summary.",
|
|
2250
|
+
"error",
|
|
2251
|
+
);
|
|
2252
|
+
return "error";
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
try {
|
|
2256
|
+
const result = await ctx.navigateTree(originId, { summarize: false });
|
|
2257
|
+
if (result.cancelled) {
|
|
2258
|
+
ctx.ui.notify(
|
|
2259
|
+
"Navigation cancelled. Use /end-review to try again.",
|
|
2260
|
+
"info",
|
|
2261
|
+
);
|
|
2262
|
+
return "cancelled";
|
|
2263
|
+
}
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
ctx.ui.notify(`Failed to return: ${describeError(error)}`, "error");
|
|
2266
|
+
return "error";
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
clearReviewState(ctx);
|
|
2270
|
+
pi.sendUserMessage(buildFastFixPrompt(reviewText), {
|
|
2271
|
+
deliverAs: "followUp",
|
|
2272
|
+
});
|
|
2273
|
+
if (notifySuccess) {
|
|
2274
|
+
ctx.ui.notify(
|
|
2275
|
+
"Review complete! Returned and queued a follow-up to fix findings.",
|
|
2276
|
+
"info",
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
return "ok";
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// action === "returnAndSummarize"
|
|
2174
2283
|
const summaryResult = await navigateWithSummary(
|
|
2175
2284
|
ctx,
|
|
2176
2285
|
originId,
|
|
@@ -2199,22 +2308,11 @@ Instructions:
|
|
|
2199
2308
|
|
|
2200
2309
|
clearReviewState(ctx);
|
|
2201
2310
|
|
|
2202
|
-
if (
|
|
2203
|
-
|
|
2204
|
-
ctx.ui.setEditorText("Act on the review findings");
|
|
2205
|
-
}
|
|
2206
|
-
if (notifySuccess) {
|
|
2207
|
-
ctx.ui.notify("Review complete! Returned and summarized.", "info");
|
|
2208
|
-
}
|
|
2209
|
-
return "ok";
|
|
2311
|
+
if (!ctx.ui.getEditorText().trim()) {
|
|
2312
|
+
ctx.ui.setEditorText("Act on the review findings");
|
|
2210
2313
|
}
|
|
2211
|
-
|
|
2212
|
-
pi.sendUserMessage(REVIEW_FIX_FINDINGS_PROMPT, { deliverAs: "followUp" });
|
|
2213
2314
|
if (notifySuccess) {
|
|
2214
|
-
ctx.ui.notify(
|
|
2215
|
-
"Review complete! Returned and queued a follow-up to fix findings.",
|
|
2216
|
-
"info",
|
|
2217
|
-
);
|
|
2315
|
+
ctx.ui.notify("Review complete! Returned and summarized.", "info");
|
|
2218
2316
|
}
|
|
2219
2317
|
return "ok";
|
|
2220
2318
|
}
|