@oh-my-pi/pi-coding-agent 15.5.2 → 15.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/types/hashline/constants.d.ts +9 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/src/hashline/constants.ts +10 -1
- package/src/hashline/executor.ts +15 -2
- package/src/hashline/grammar.lark +3 -4
- package/src/prompts/tools/hashline.md +39 -19
- package/src/tools/bash.ts +74 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.3] - 2026-05-27
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Disallowed inline payload on hashline `↑`, `↓`, and `:` operations (including BOF/EOF inserts), requiring payload text to be supplied on standalone `+` continuation rows
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Warned when legacy inline `LINE:TEXT` lines are accepted as payload continuations only when inside a pending multi-line `A-B:` replacement
|
|
13
|
+
|
|
5
14
|
## [15.5.2] - 2026-05-26
|
|
6
15
|
### Breaking Changes
|
|
7
16
|
|
|
@@ -37,4 +37,12 @@ export declare const IMPLICIT_CONTINUATION_WARNING = "Accepted continuation line
|
|
|
37
37
|
* `LINE:` prefix and append the body to the pending payload, but warn so the
|
|
38
38
|
* canonical `+`-continuation form remains preferred.
|
|
39
39
|
*/
|
|
40
|
-
export declare const PAYLOAD_LINE_PREFIX_DEMOTED_WARNING = "Detected one or more `LINE:TEXT` lines whose anchors fell inside a pending replace range; treated them as payload-continuation lines and stripped the `LINE:` prefix. Inside
|
|
40
|
+
export declare const PAYLOAD_LINE_PREFIX_DEMOTED_WARNING = "Detected one or more `LINE:TEXT` lines whose anchors fell inside a pending replace range; treated them as payload-continuation lines and stripped the `LINE:` prefix. Inside an `A-B:` block, every payload line must be on its own row prefixed with `+` \u2014 never reuse the read-output gutter format.";
|
|
41
|
+
/**
|
|
42
|
+
* Warning text appended when an op carries an inline payload (`LINE:TEXT`,
|
|
43
|
+
* `A-B:TEXT`, `LINE↑TEXT`, `LINE↓TEXT`). Canonical syntax is bare op +
|
|
44
|
+
* `+`-prefixed continuation rows; we accept the inline form leniently so the
|
|
45
|
+
* model's first-attempt edit still lands, but warn so the canonical form
|
|
46
|
+
* remains preferred.
|
|
47
|
+
*/
|
|
48
|
+
export declare const INLINE_PAYLOAD_ACCEPTED_WARNING = "Accepted inline payload on the op line (e.g. `LINE:CONTENT`, `LINE\u2191CONTENT`). Canonical syntax is the bare op followed by `+`-prefixed payload rows on the next line(s). Prefer the explicit form.";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.5.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.5.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.5.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.5.3",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.5.3",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.5.3",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.5.3",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.5.3",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.5.3",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
|
@@ -48,4 +48,13 @@ export const IMPLICIT_CONTINUATION_WARNING =
|
|
|
48
48
|
* canonical `+`-continuation form remains preferred.
|
|
49
49
|
*/
|
|
50
50
|
export const PAYLOAD_LINE_PREFIX_DEMOTED_WARNING =
|
|
51
|
-
"Detected one or more `LINE:TEXT` lines whose anchors fell inside a pending replace range; treated them as payload-continuation lines and stripped the `LINE:` prefix. Inside
|
|
51
|
+
"Detected one or more `LINE:TEXT` lines whose anchors fell inside a pending replace range; treated them as payload-continuation lines and stripped the `LINE:` prefix. Inside an `A-B:` block, every payload line must be on its own row prefixed with `+` — never reuse the read-output gutter format.";
|
|
52
|
+
/**
|
|
53
|
+
* Warning text appended when an op carries an inline payload (`LINE:TEXT`,
|
|
54
|
+
* `A-B:TEXT`, `LINE↑TEXT`, `LINE↓TEXT`). Canonical syntax is bare op +
|
|
55
|
+
* `+`-prefixed continuation rows; we accept the inline form leniently so the
|
|
56
|
+
* model's first-attempt edit still lands, but warn so the canonical form
|
|
57
|
+
* remains preferred.
|
|
58
|
+
*/
|
|
59
|
+
export const INLINE_PAYLOAD_ACCEPTED_WARNING =
|
|
60
|
+
"Accepted inline payload on the op line (e.g. `LINE:CONTENT`, `LINE↑CONTENT`). Canonical syntax is the bare op followed by `+`-prefixed payload rows on the next line(s). Prefer the explicit form.";
|
package/src/hashline/executor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ABORT_WARNING,
|
|
3
3
|
IMPLICIT_CONTINUATION_WARNING,
|
|
4
|
+
INLINE_PAYLOAD_ACCEPTED_WARNING,
|
|
4
5
|
PAYLOAD_LINE_PREFIX_DEMOTED_WARNING,
|
|
5
6
|
REPLACE_PAIR_COALESCED_WARNING,
|
|
6
7
|
} from "./constants";
|
|
@@ -126,8 +127,14 @@ export class HashlineExecutor {
|
|
|
126
127
|
this.#flushPending();
|
|
127
128
|
this.#pending = {
|
|
128
129
|
op: { kind: "insert", cursor: token.cursor, lineNum: token.lineNum },
|
|
129
|
-
payload:
|
|
130
|
+
payload: [],
|
|
130
131
|
};
|
|
132
|
+
if (token.inlineBody !== undefined) {
|
|
133
|
+
this.#pending.payload.push(token.inlineBody);
|
|
134
|
+
if (!this.#warnings.includes(INLINE_PAYLOAD_ACCEPTED_WARNING)) {
|
|
135
|
+
this.#warnings.push(INLINE_PAYLOAD_ACCEPTED_WARNING);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
131
138
|
return;
|
|
132
139
|
case "op-replace":
|
|
133
140
|
validateRangeOrder(token.range, token.lineNum);
|
|
@@ -161,8 +168,14 @@ export class HashlineExecutor {
|
|
|
161
168
|
this.#flushPending();
|
|
162
169
|
this.#pending = {
|
|
163
170
|
op: { kind: "replace", range: token.range, lineNum: token.lineNum },
|
|
164
|
-
payload:
|
|
171
|
+
payload: [],
|
|
165
172
|
};
|
|
173
|
+
if (token.inlineBody !== undefined) {
|
|
174
|
+
this.#pending.payload.push(token.inlineBody);
|
|
175
|
+
if (!this.#warnings.includes(INLINE_PAYLOAD_ACCEPTED_WARNING)) {
|
|
176
|
+
this.#warnings.push(INLINE_PAYLOAD_ACCEPTED_WARNING);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
166
179
|
return;
|
|
167
180
|
}
|
|
168
181
|
}
|
|
@@ -9,11 +9,10 @@ filename: /([^\s#]+)/
|
|
|
9
9
|
file_hash: /[0-9a-f]{4}/
|
|
10
10
|
|
|
11
11
|
line_op: insert_before | insert_after | replace | delete
|
|
12
|
-
insert_before: anchor "$HOP_INSERT_BEFORE$"
|
|
13
|
-
insert_after: anchor "$HOP_INSERT_AFTER$"
|
|
14
|
-
replace: range "$HOP_REPLACE$"
|
|
12
|
+
insert_before: anchor "$HOP_INSERT_BEFORE$" LF payload*
|
|
13
|
+
insert_after: anchor "$HOP_INSERT_AFTER$" LF payload*
|
|
14
|
+
replace: range "$HOP_REPLACE$" LF payload*
|
|
15
15
|
delete: range "$HOP_DELETE$" LF
|
|
16
|
-
inline_body: /[^\n]+/
|
|
17
16
|
payload: "+" /[^\n]*/ LF
|
|
18
17
|
|
|
19
18
|
anchor: LID | "EOF" | "BOF"
|
|
@@ -4,60 +4,80 @@ Your patch language is a compact, line-anchored edit format.
|
|
|
4
4
|
Patch payload is a series of hunks: `¶PATH#HASH` header followed by any number of operations. `HASH` should be copied as is from read/search. Missing? Re-`read`.
|
|
5
5
|
- No context rows, no gutters.
|
|
6
6
|
- NEVER restate unchanged lines "for context".
|
|
7
|
-
-
|
|
8
|
-
- Payload indentation
|
|
7
|
+
- Op lines carry NO payload. Every payload line lives on its own row and MUST start with `+`; that delimiter is stripped.
|
|
8
|
+
- Payload indentation is literal.
|
|
9
9
|
</payload>
|
|
10
10
|
|
|
11
11
|
<ops>
|
|
12
|
-
LINE↑
|
|
13
|
-
LINE↓
|
|
14
|
-
A-B:
|
|
15
|
-
A-B!
|
|
16
|
-
+PAYLOAD
|
|
12
|
+
LINE↑ insert before (or BOF↑)
|
|
13
|
+
LINE↓ insert after (or EOF↓)
|
|
14
|
+
A-B: replace A..B (or A: == A..A)
|
|
15
|
+
A-B! delete A..B (or A! == A..A)
|
|
16
|
+
+PAYLOAD payload line for the preceding op
|
|
17
17
|
</ops>
|
|
18
18
|
|
|
19
19
|
<rules>
|
|
20
20
|
- **Payload is only what's NEW.** `:` replaces inside; `↑`/`↓` add at anchor. NEVER repeat anchor lines or neighbors.
|
|
21
|
-
- **
|
|
21
|
+
- **Use `+` for a blank payload line; use `++text` to write a line starting with `+text`.**
|
|
22
|
+
- **Inserts add ONLY the rows you list.** The file's existing newlines around the anchor stay. NEVER tack a trailing `+` blank "for spacing" — it writes a literal blank line into the file, doubling whatever is already there.
|
|
23
|
+
- **A bare `LINE↑`/`LINE↓` with no payload still inserts ONE blank line.** Not a no-op. Omit the op if you want nothing there.
|
|
22
24
|
- **Go small.** Add → `↑`/`↓`; replace → `:`; delete → `!`.
|
|
23
|
-
- **Line numbers are frozen references to what you have seen.** Later ops still use original line numbers.
|
|
25
|
+
- **Line numbers are frozen references to what you have seen.** Later ops in the same hunk still use original line numbers; they do NOT shift as earlier ops apply.
|
|
24
26
|
</rules>
|
|
25
27
|
|
|
26
28
|
<common-failures>
|
|
27
29
|
- **NEVER replay past your range.** Stop before B+1; extend B if needed.
|
|
28
|
-
- **Read lines look like replace ops.** `84:content` = "make line 84 content" —
|
|
30
|
+
- **Read lines look like replace ops.** `84:content` = "make line 84 content" — and inline content is rejected. Don't echo read-style rows.
|
|
29
31
|
- **NEVER fabricate file hashes.** Missing? Re-`read`.
|
|
30
32
|
</common-failures>
|
|
31
33
|
|
|
32
34
|
<example>
|
|
33
35
|
```a.ts#1a2b
|
|
34
36
|
1:const X = "a";
|
|
35
|
-
2:
|
|
37
|
+
2:
|
|
38
|
+
3:export function f() { return X; }
|
|
39
|
+
4:f();
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
# replace
|
|
42
|
+
# replace one line, insert after, delete
|
|
39
43
|
```
|
|
40
44
|
¶a.ts#1a2b
|
|
41
|
-
1:
|
|
45
|
+
1:
|
|
46
|
+
+const X = "b";
|
|
42
47
|
+export const Y = X;
|
|
43
|
-
1↓
|
|
44
|
-
|
|
48
|
+
1↓
|
|
49
|
+
+const Z = Y;
|
|
50
|
+
4!
|
|
45
51
|
```
|
|
46
52
|
</example>
|
|
47
53
|
|
|
48
54
|
<anti-pattern>
|
|
55
|
+
# WRONG — inline payload after the sigil is rejected
|
|
56
|
+
1:const X = "b";
|
|
57
|
+
1↓const Z = Y;
|
|
58
|
+
1-2:const X = "b";
|
|
59
|
+
+export const Y = X;
|
|
49
60
|
# WRONG — INSERT used to change a line (old line survives)
|
|
50
|
-
1↓
|
|
61
|
+
1↓
|
|
62
|
+
+const X = "b";
|
|
51
63
|
# WRONG — echoing read-style lines as context before the real op
|
|
52
64
|
1:const X = "a";
|
|
53
|
-
1-2:
|
|
54
|
-
|
|
65
|
+
1-2:
|
|
66
|
+
+const X = "b";
|
|
67
|
+
+export const Y = X;
|
|
68
|
+
# WRONG — trailing `+` blank writes a literal empty line; the new blank lands right next to the orig blank at line 2, doubling it
|
|
69
|
+
1↓
|
|
70
|
+
+const Y = X;
|
|
71
|
+
+
|
|
72
|
+
# WRONG — `2↓` still anchors at PRE-EDIT line 2 (frozen), NOT at the line just inserted by `1↓`. Both inserts land at their own anchors, giving three consecutive blanks (new from `1↓`, orig blank line 2, new from `2↓`).
|
|
73
|
+
1↓
|
|
74
|
+
2↓
|
|
55
75
|
</anti-pattern>
|
|
56
76
|
|
|
57
77
|
<critical>
|
|
58
78
|
- One op per range, ever.
|
|
59
79
|
- Pick op precisely. Update: `:`, add: `↑`/`↓`, remove: `!`.
|
|
80
|
+
- Payload always lives on its own `+`-prefixed line — never inline with the op.
|
|
60
81
|
- Payload is only what's NEW; never repeat anchor lines or neighbors.
|
|
61
|
-
- Continuation payload lines after the op line must start with `+`.
|
|
62
82
|
- Anchor exactly; don't anchor neighbors.
|
|
63
83
|
</critical>
|
package/src/tools/bash.ts
CHANGED
|
@@ -128,6 +128,7 @@ export interface BashToolDetails {
|
|
|
128
128
|
meta?: OutputMeta;
|
|
129
129
|
timeoutSeconds?: number;
|
|
130
130
|
requestedTimeoutSeconds?: number;
|
|
131
|
+
wallTimeMs?: number;
|
|
131
132
|
terminalId?: string;
|
|
132
133
|
async?: {
|
|
133
134
|
state: "running" | "completed" | "failed";
|
|
@@ -272,6 +273,34 @@ function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutS
|
|
|
272
273
|
: undefined;
|
|
273
274
|
}
|
|
274
275
|
|
|
276
|
+
function formatWallTimeSeconds(wallTimeMs: number): string {
|
|
277
|
+
return (wallTimeMs / 1000).toFixed(2);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function formatWallTimeNotice(wallTimeMs: number): string {
|
|
281
|
+
return `Wall time: ${formatWallTimeSeconds(wallTimeMs)} seconds`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Strip the trailing `Wall time: <secs> seconds` notice from text so the TUI
|
|
286
|
+
* can render the wall time via its styled `[Wall: …]` label without echoing
|
|
287
|
+
* the same value verbatim in the output pane.
|
|
288
|
+
*/
|
|
289
|
+
function stripWallTimeNotice(text: string, wallTimeMs: number | undefined): string {
|
|
290
|
+
if (wallTimeMs === undefined) return text;
|
|
291
|
+
// Reconstruct the notice from the same value the result was tagged with so
|
|
292
|
+
// a literal sub-string match never strips a coincidental in-output token —
|
|
293
|
+
// only the exact line we appended in #buildCompletedResult.
|
|
294
|
+
const notice = formatWallTimeNotice(wallTimeMs);
|
|
295
|
+
const idx = text.lastIndexOf(notice);
|
|
296
|
+
if (idx === -1) return text;
|
|
297
|
+
let start = idx;
|
|
298
|
+
let end = idx + notice.length;
|
|
299
|
+
if (text[start - 1] === "\n") start -= 1;
|
|
300
|
+
if (text[end] === "\n") end += 1;
|
|
301
|
+
return (text.slice(0, start) + text.slice(end)).trimEnd();
|
|
302
|
+
}
|
|
303
|
+
|
|
275
304
|
/**
|
|
276
305
|
* Bash tool implementation.
|
|
277
306
|
*
|
|
@@ -347,10 +376,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
347
376
|
#buildCompletedResult(
|
|
348
377
|
result: BashResult | BashInteractiveResult,
|
|
349
378
|
timeoutSec: number,
|
|
350
|
-
options: {
|
|
379
|
+
options: {
|
|
380
|
+
requestedTimeoutSec?: number;
|
|
381
|
+
notices?: readonly string[];
|
|
382
|
+
terminalId?: string;
|
|
383
|
+
wallTimeMs?: number;
|
|
384
|
+
} = {},
|
|
351
385
|
): AgentToolResult<BashToolDetails> {
|
|
352
386
|
const outputLines = [this.#formatResultOutput(result)];
|
|
353
|
-
const notices =
|
|
387
|
+
const notices: string[] = [];
|
|
388
|
+
if (options.wallTimeMs !== undefined) {
|
|
389
|
+
notices.push(formatWallTimeNotice(options.wallTimeMs));
|
|
390
|
+
}
|
|
391
|
+
if (options.notices) {
|
|
392
|
+
for (const notice of options.notices) {
|
|
393
|
+
if (notice) notices.push(notice);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
354
396
|
if (notices.length > 0) outputLines.push("", ...notices);
|
|
355
397
|
const outputText = outputLines.join("\n");
|
|
356
398
|
const details: BashToolDetails = { timeoutSeconds: timeoutSec };
|
|
@@ -360,6 +402,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
360
402
|
if (options.terminalId !== undefined) {
|
|
361
403
|
details.terminalId = options.terminalId;
|
|
362
404
|
}
|
|
405
|
+
if (options.wallTimeMs !== undefined) {
|
|
406
|
+
details.wallTimeMs = options.wallTimeMs;
|
|
407
|
+
}
|
|
363
408
|
const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
|
|
364
409
|
this.#buildResultText(result, timeoutSec, outputText);
|
|
365
410
|
return resultBuilder.done();
|
|
@@ -430,6 +475,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
430
475
|
async ({ jobId, signal: runSignal, reportProgress }) => {
|
|
431
476
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
432
477
|
const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
|
|
478
|
+
const wallTimeStart = performance.now();
|
|
433
479
|
try {
|
|
434
480
|
const result = await executeBash(options.command, {
|
|
435
481
|
cwd: options.commandCwd,
|
|
@@ -446,9 +492,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
446
492
|
},
|
|
447
493
|
onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
448
494
|
});
|
|
495
|
+
const wallTimeMs = performance.now() - wallTimeStart;
|
|
449
496
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
450
497
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
451
498
|
notices: options.notices ?? [],
|
|
499
|
+
wallTimeMs,
|
|
452
500
|
});
|
|
453
501
|
const finalText = this.#extractTextResult(finalResult);
|
|
454
502
|
latestText = finalText;
|
|
@@ -697,6 +745,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
697
745
|
// Skip when pty=true (PTY needs the local terminal UI).
|
|
698
746
|
const clientBridge = this.session.getClientBridge?.();
|
|
699
747
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
748
|
+
const bridgeWallTimeStart = performance.now();
|
|
700
749
|
const handle = await clientBridge.createTerminal({
|
|
701
750
|
command,
|
|
702
751
|
cwd: commandCwd,
|
|
@@ -792,6 +841,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
792
841
|
requestedTimeoutSec,
|
|
793
842
|
notices: pendingNotices,
|
|
794
843
|
terminalId: handle.terminalId,
|
|
844
|
+
wallTimeMs: performance.now() - bridgeWallTimeStart,
|
|
795
845
|
});
|
|
796
846
|
}
|
|
797
847
|
|
|
@@ -852,6 +902,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
852
902
|
requestedTimeoutSec,
|
|
853
903
|
notices: bridgeNotices,
|
|
854
904
|
terminalId: handle.terminalId,
|
|
905
|
+
wallTimeMs: performance.now() - bridgeWallTimeStart,
|
|
855
906
|
});
|
|
856
907
|
} finally {
|
|
857
908
|
try {
|
|
@@ -869,6 +920,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
869
920
|
const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
|
|
870
921
|
|
|
871
922
|
const interactiveUi = canUseInteractiveBashPty(pty, ctx) ? ctx?.ui : undefined;
|
|
923
|
+
const wallTimeStart = performance.now();
|
|
872
924
|
const result: BashResult | BashInteractiveResult = interactiveUi
|
|
873
925
|
? await runInteractiveBashPty(interactiveUi, {
|
|
874
926
|
command,
|
|
@@ -890,6 +942,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
890
942
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
891
943
|
onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
|
|
892
944
|
});
|
|
945
|
+
const wallTimeMs = performance.now() - wallTimeStart;
|
|
893
946
|
if (result.cancelled) {
|
|
894
947
|
if (signal?.aborted) {
|
|
895
948
|
throw new ToolAbortError(normalizeResultOutput(result) || "Command aborted");
|
|
@@ -902,6 +955,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
902
955
|
return this.#buildCompletedResult(result, timeoutSec, {
|
|
903
956
|
requestedTimeoutSec,
|
|
904
957
|
notices: pendingNotices,
|
|
958
|
+
wallTimeMs,
|
|
905
959
|
});
|
|
906
960
|
}
|
|
907
961
|
}
|
|
@@ -1032,22 +1086,32 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1032
1086
|
// Strip the LLM-facing notice appended by wrappedExecute so we don't
|
|
1033
1087
|
// double-print it alongside the styled warning line below.
|
|
1034
1088
|
const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1035
|
-
const
|
|
1089
|
+
const strippedOutput = stripOutputNotice(rawOutput, details?.meta);
|
|
1090
|
+
const output = stripWallTimeNotice(strippedOutput, details?.wallTimeMs);
|
|
1036
1091
|
const displayOutput = output.trimEnd();
|
|
1037
1092
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
1038
1093
|
|
|
1039
1094
|
// Build truncation warning
|
|
1040
1095
|
const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
|
|
1041
1096
|
const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1097
|
+
const wallTimeMs = details?.wallTimeMs;
|
|
1098
|
+
const statsParts: string[] = [];
|
|
1099
|
+
if (wallTimeMs !== undefined) {
|
|
1100
|
+
statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
|
|
1101
|
+
}
|
|
1102
|
+
if (typeof timeoutSeconds === "number") {
|
|
1103
|
+
statsParts.push(
|
|
1104
|
+
requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
|
|
1045
1105
|
? `Timeout: ${timeoutSeconds}s (requested ${requestedTimeoutSeconds}s clamped)`
|
|
1046
|
-
: `Timeout: ${timeoutSeconds}s
|
|
1047
|
-
|
|
1106
|
+
: `Timeout: ${timeoutSeconds}s`,
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1048
1109
|
const timeoutLine =
|
|
1049
|
-
|
|
1050
|
-
? uiTheme.fg(
|
|
1110
|
+
statsParts.length > 0
|
|
1111
|
+
? uiTheme.fg(
|
|
1112
|
+
"dim",
|
|
1113
|
+
`${uiTheme.format.bracketLeft}${statsParts.join(" | ")}${uiTheme.format.bracketRight}`,
|
|
1114
|
+
)
|
|
1051
1115
|
: undefined;
|
|
1052
1116
|
let warningLine: string | undefined;
|
|
1053
1117
|
if (details?.meta?.truncation && !showingFullOutput) {
|