@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 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 a multi-line `A-B:` block, payload lines after the first should be prefixed with `+` \u2014 never reuse the read-output gutter format.";
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.";
@@ -42,6 +42,7 @@ export interface BashToolDetails {
42
42
  meta?: OutputMeta;
43
43
  timeoutSeconds?: number;
44
44
  requestedTimeoutSeconds?: number;
45
+ wallTimeMs?: number;
45
46
  terminalId?: string;
46
47
  async?: {
47
48
  state: "running" | "completed" | "failed";
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.2",
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.2",
51
- "@oh-my-pi/pi-agent-core": "15.5.2",
52
- "@oh-my-pi/pi-ai": "15.5.2",
53
- "@oh-my-pi/pi-natives": "15.5.2",
54
- "@oh-my-pi/pi-tui": "15.5.2",
55
- "@oh-my-pi/pi-utils": "15.5.2",
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 a multi-line `A-B:` block, payload lines after the first should be prefixed with `+` — never reuse the read-output gutter format.";
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.";
@@ -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: token.inlineBody === undefined ? [] : [token.inlineBody],
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: token.inlineBody === undefined ? [] : [token.inlineBody],
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$" inline_body? LF payload*
13
- insert_after: anchor "$HOP_INSERT_AFTER$" inline_body? LF payload*
14
- replace: range "$HOP_REPLACE$" inline_body? LF payload*
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
- - Inline payload after an op is literal. Additional payload lines MUST start with `+`; that delimiter is stripped.
8
- - Payload indentation after the op sigil or after `+` is literal.
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↑PAYLOAD insert before (or BOF↑)
13
- LINE↓PAYLOAD insert after (or EOF↓)
14
- A-B:PAYLOAD replace A..B (or A: == A..A)
15
- A-B! delete A..B (or A! == A..A)
16
- +PAYLOAD continuation payload line; leading `+` is not written
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
- - **Continuation lines require `+`.** Use `+` for a blank payload line; use `++text` to write a line starting with `+text`.
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" — don't echo context before it.
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:export function f() { return X; }
37
+ 2:
38
+ 3:export function f() { return X; }
39
+ 4:f();
36
40
  ```
37
41
 
38
- # replace with a continuation line, insert after, delete
42
+ # replace one line, insert after, delete
39
43
  ```
40
44
  ¶a.ts#1a2b
41
- 1:const X = "b";
45
+ 1:
46
+ +const X = "b";
42
47
  +export const Y = X;
43
- 1↓const Z = Y;
44
- 2!
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↓const X = "b";
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:const X = "b";
54
- export const Y = X; # raw continuation line missing required `+`
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: { requestedTimeoutSec?: number; notices?: readonly string[]; terminalId?: string } = {},
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 = options.notices?.filter(Boolean) ?? [];
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 output = stripOutputNotice(rawOutput, details?.meta);
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 timeoutLabel =
1043
- typeof timeoutSeconds === "number"
1044
- ? requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
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
- : undefined;
1106
+ : `Timeout: ${timeoutSeconds}s`,
1107
+ );
1108
+ }
1048
1109
  const timeoutLine =
1049
- timeoutLabel !== undefined
1050
- ? uiTheme.fg("dim", `${uiTheme.format.bracketLeft}${timeoutLabel}${uiTheme.format.bracketRight}`)
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) {