@oh-my-pi/pi-coding-agent 16.3.13 → 16.3.14

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.
@@ -1,5 +1,10 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  export declare function canonicalizeMessage(text: string | null | undefined): string;
3
+ /**
4
+ * Thinking text prepared for display. Both modes drop empty `<!-- -->`
5
+ * sentinel lines outside code fences (see {@link isCommentNoise}); prose-only
6
+ * mode additionally elides fenced code down to a trailing ellipsis.
7
+ */
3
8
  export declare function formatThinkingForDisplay(text: string, proseOnly: boolean): string;
4
9
  /** Whether a formatted thinking block has non-placeholder content worth rendering. */
5
10
  export declare function hasDisplayableThinking(text: string | null | undefined, formattedText: string | null | undefined): boolean;
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": "16.3.13",
4
+ "version": "16.3.14",
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",
@@ -56,17 +56,17 @@
56
56
  "@agentclientprotocol/sdk": "0.25.0",
57
57
  "@babel/parser": "^7.29.7",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@oh-my-pi/hashline": "16.3.13",
60
- "@oh-my-pi/omp-stats": "16.3.13",
61
- "@oh-my-pi/pi-agent-core": "16.3.13",
62
- "@oh-my-pi/pi-ai": "16.3.13",
63
- "@oh-my-pi/pi-catalog": "16.3.13",
64
- "@oh-my-pi/pi-mnemopi": "16.3.13",
65
- "@oh-my-pi/pi-natives": "16.3.13",
66
- "@oh-my-pi/pi-tui": "16.3.13",
67
- "@oh-my-pi/pi-utils": "16.3.13",
68
- "@oh-my-pi/pi-wire": "16.3.13",
69
- "@oh-my-pi/snapcompact": "16.3.13",
59
+ "@oh-my-pi/hashline": "16.3.14",
60
+ "@oh-my-pi/omp-stats": "16.3.14",
61
+ "@oh-my-pi/pi-agent-core": "16.3.14",
62
+ "@oh-my-pi/pi-ai": "16.3.14",
63
+ "@oh-my-pi/pi-catalog": "16.3.14",
64
+ "@oh-my-pi/pi-mnemopi": "16.3.14",
65
+ "@oh-my-pi/pi-natives": "16.3.14",
66
+ "@oh-my-pi/pi-tui": "16.3.14",
67
+ "@oh-my-pi/pi-utils": "16.3.14",
68
+ "@oh-my-pi/pi-wire": "16.3.14",
69
+ "@oh-my-pi/snapcompact": "16.3.14",
70
70
  "@opentelemetry/api": "^1.9.1",
71
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -40,6 +40,18 @@ interface FinalizableBlock {
40
40
  * commits until the block finalizes.
41
41
  */
42
42
  getTranscriptBlockSettledRows?(): number;
43
+ /**
44
+ * Whether the block is a displaceable snapshot (todo/poll card) kept
45
+ * unfinalized only so a follow-up matching call can retract it. Paired
46
+ * with {@link seal}: once any of its rows enters native scrollback the
47
+ * container seals it — rows on the tape are immutable, so retraction is
48
+ * no longer possible, and an unfinalized block would otherwise pin the
49
+ * live-region seam open for the rest of the turn (every row committed
50
+ * below it audit-exempt, mass-recommitted when it finally finalizes).
51
+ */
52
+ isDisplaceableBlock?(): boolean;
53
+ /** Finalize a displaceable snapshot in place (settle animation, freeze bytes). */
54
+ seal?(): void;
43
55
  }
44
56
 
45
57
  function isBlockFinalized(child: Component): boolean {
@@ -60,6 +72,12 @@ function getBlockSettledRows(child: Component): number {
60
72
  return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
61
73
  }
62
74
 
75
+ /** Seal a displaceable snapshot whose rows entered native scrollback (see {@link FinalizableBlock.isDisplaceableBlock}). */
76
+ function sealCommittedSnapshot(child: Component): void {
77
+ const block = child as Component & FinalizableBlock;
78
+ if (block.isDisplaceableBlock?.()) block.seal?.();
79
+ }
80
+
63
81
  // A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
64
82
  // separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
65
83
  // to a background-colored padding row, whose escape sequences contain `\S` and
@@ -275,6 +293,21 @@ export class TranscriptContainer
275
293
 
276
294
  const count = this.children.length;
277
295
 
296
+ // Seal displaceable snapshots whose rows are already on the tape (per the
297
+ // previous frame's segments — the geometry the committed count was
298
+ // computed against): immutable history can no longer be retracted, and
299
+ // left unfinalized such a block would pin the live-region seam open below
300
+ // it. Runs before the live-block scan so the seam unpins in this same
301
+ // frame, and every frame so a block that BECAME displaceable after its
302
+ // pending-preview rows committed (late result on a scrolled-off call) is
303
+ // caught too.
304
+ for (let i = 0; i < count && i < this.#segments.length; i++) {
305
+ const previous = this.#segments[i]!;
306
+ if (previous.startRow >= this.#committedRows) break;
307
+ if (previous.rowCount === 0 || previous.component !== this.children[i]) continue;
308
+ sealCommittedSnapshot(previous.component);
309
+ }
310
+
278
311
  // The commit boundary stops at the earliest still-mutating block. A
279
312
  // block that has not finalized must gate it: out-of-band inserts
280
313
  // (TTSR/todo cards) can append a finalized block *below* a tool that is
@@ -51,20 +51,20 @@ Decompose first, then {{#if taskBatch}}batch the independent leaves{{else}}issue
51
51
 
52
52
  {{#if taskBatch}}
53
53
  task(
54
- context: "# Goal\nReview the auth diff...\n# Constraints\nRead-only...\n# Contract\nReturn findings as severity/file/line/fix...",
54
+ context: "# Goal\nReview the auth diff…\n# Constraints\nRead-only…\n# Contract\nReturn findings as severity/file/line/fix",
55
55
  tasks: [
56
- { id: "AuthOwner", role: "Auth Storage Reviewer", assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nTrace credential selection...\n# Acceptance\nReturn confirmed findings only..." },
57
- { id: "PromptOwner", role: "Prompt Contract Reviewer", assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance...\n# Acceptance\nReturn mismatches and exact prompt lines..." },
56
+ { id: "AuthOwner", role: "Auth Storage Reviewer", assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nTrace credential selection…\n# Acceptance\nReturn confirmed findings only" },
57
+ { id: "PromptOwner", role: "Prompt Contract Reviewer", assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance…\n# Acceptance\nReturn mismatches and exact prompt lines" },
58
58
  ]
59
59
  )
60
60
  {{else}}
61
61
  task(
62
62
  role: "Auth Storage Reviewer",
63
- assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nReview the auth diff. Shared contract: read-only; return findings as severity/file/line/fix.\n# Acceptance\nReturn confirmed findings only..."
63
+ assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nReview the auth diff. Shared contract: read-only; return findings as severity/file/line/fix.\n# Acceptance\nReturn confirmed findings only"
64
64
  )
65
65
  task(
66
66
  role: "Prompt Contract Reviewer",
67
- assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance. Shared contract: read-only; return mismatches and exact prompt lines.\n# Acceptance\nReturn confirmed findings only..."
67
+ assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance. Shared contract: read-only; return mismatches and exact prompt lines.\n# Acceptance\nReturn confirmed findings only"
68
68
  )
69
69
  {{/if}}
70
70
 
@@ -1,14 +1,16 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
 
3
- // Single-entry memo for the proseOnly formatting path. During a streaming tick
4
- // the same growing thinking text is formatted up to three times (reveal count,
5
- // reveal slice, component render); this collapses them to one computation. The
6
- // `proseOnly === false` branch is a passthrough and never consults the cache, so
7
- // the key can be the text alone. A single entry is enough for the common case of
8
- // one active thinking block and never regresses (a miss recomputes exactly as
9
- // before).
10
- let formatCacheKey = "";
11
- let formatCacheValue = "";
3
+ // Single-slot-per-mode memo for formatThinkingForDisplay. During a streaming
4
+ // tick the same growing thinking text is formatted up to three times (reveal
5
+ // count, reveal slice, component render); this collapses them to one
6
+ // computation. Prose and raw modes produce different output for the same text,
7
+ // so each mode keeps its own slot. One entry per mode is enough for the common
8
+ // case of one active thinking block and never regresses (a miss recomputes
9
+ // exactly as before).
10
+ let proseCacheKey = "";
11
+ let proseCacheValue = "";
12
+ let rawCacheKey = "";
13
+ let rawCacheValue = "";
12
14
 
13
15
  export function canonicalizeMessage(text: string | null | undefined): string {
14
16
  if (!text) return "";
@@ -22,9 +24,35 @@ export function canonicalizeMessage(text: string | null | undefined): string {
22
24
  return "";
23
25
  }
24
26
 
27
+ // gpt-5.x reasoning summaries pad every summary part with an empty HTML
28
+ // comment (`**Headline**\n\n<!-- -->`), streamed as a `<!--` delta followed by
29
+ // ` -->`. Comments with actual content are left untouched.
30
+ const EMPTY_COMMENT_RE = /^<!--\s*-->$/;
31
+ const OPEN_COMMENT_RE = /^<!--\s*$/;
32
+
33
+ /**
34
+ * Whether `line` is reasoning-summary comment noise: an empty HTML comment,
35
+ * or its still-unterminated `<!--` prefix on the last line while streaming.
36
+ */
37
+ function isCommentNoise(line: string, isLastLine: boolean): boolean {
38
+ const trimmed = line.trim();
39
+ return EMPTY_COMMENT_RE.test(trimmed) || (isLastLine && OPEN_COMMENT_RE.test(trimmed));
40
+ }
41
+
42
+ /**
43
+ * Thinking text prepared for display. Both modes drop empty `<!-- -->`
44
+ * sentinel lines outside code fences (see {@link isCommentNoise}); prose-only
45
+ * mode additionally elides fenced code down to a trailing ellipsis.
46
+ */
25
47
  export function formatThinkingForDisplay(text: string, proseOnly: boolean): string {
26
- if (!proseOnly || !text) return text;
27
- if (text === formatCacheKey) return formatCacheValue;
48
+ if (!text) return text;
49
+ const hasComment = text.includes("<!--");
50
+ if (proseOnly) {
51
+ if (text === proseCacheKey) return proseCacheValue;
52
+ } else {
53
+ if (!hasComment) return text;
54
+ if (text === rawCacheKey) return rawCacheValue;
55
+ }
28
56
 
29
57
  const lines = text.split("\n");
30
58
  const resultLines: string[] = [];
@@ -56,22 +84,31 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
56
84
 
57
85
  for (let i = 0; i < lines.length; i++) {
58
86
  const line = lines[i]!;
59
- const open = FENCE.exec(line);
60
87
 
61
88
  if (inFence) {
89
+ const close = FENCE.exec(line);
62
90
  // A closing fence is the same char, at least as long, with nothing else on the line.
63
91
  if (
64
- open &&
65
- open[2]![0] === fenceChar &&
66
- open[2]!.length >= fenceLen &&
67
- line.slice(open[1]!.length + open[2]!.length).trim() === ""
92
+ close &&
93
+ close[2]![0] === fenceChar &&
94
+ close[2]!.length >= fenceLen &&
95
+ line.slice(close[1]!.length + close[2]!.length).trim() === ""
68
96
  ) {
69
97
  inFence = false;
70
98
  fenceChar = "";
71
99
  fenceLen = 0;
72
100
  }
73
- // We skip all internal lines of a code fence.
74
- } else if (open) {
101
+ // Prose mode skips all fence lines; raw mode keeps them verbatim
102
+ // (comment markers inside fences are code, not noise).
103
+ if (!proseOnly) resultLines.push(line);
104
+ continue;
105
+ }
106
+
107
+ // Drop the whole line so `**Headline**\n\n<!-- -->` leaves no blank tail.
108
+ if (hasComment && isCommentNoise(line, i === lines.length - 1)) continue;
109
+
110
+ const open = FENCE.exec(line);
111
+ if (open) {
75
112
  const marker = open[2]!;
76
113
  const ch = marker[0]!;
77
114
  // A backtick fence's info string may not contain a backtick.
@@ -79,18 +116,25 @@ export function formatThinkingForDisplay(text: string, proseOnly: boolean): stri
79
116
  inFence = true;
80
117
  fenceChar = ch;
81
118
  fenceLen = marker.length;
82
- appendEllipsis();
83
- } else {
84
- resultLines.push(line);
119
+ if (proseOnly) {
120
+ appendEllipsis();
121
+ } else {
122
+ resultLines.push(line);
123
+ }
124
+ continue;
85
125
  }
86
- } else {
87
- resultLines.push(line);
88
126
  }
127
+ resultLines.push(line);
89
128
  }
90
129
 
91
130
  const formatted = resultLines.join("\n");
92
- formatCacheKey = text;
93
- formatCacheValue = formatted;
131
+ if (proseOnly) {
132
+ proseCacheKey = text;
133
+ proseCacheValue = formatted;
134
+ } else {
135
+ rawCacheKey = text;
136
+ rawCacheValue = formatted;
137
+ }
94
138
  return formatted;
95
139
  }
96
140
 
@@ -99,9 +143,11 @@ export function hasDisplayableThinking(
99
143
  text: string | null | undefined,
100
144
  formattedText: string | null | undefined,
101
145
  ): boolean {
102
- if (!text) return false;
103
- if (!formattedText) return false;
104
- return formattedText.length > 0 && canonicalizeMessage(text).length > 0;
146
+ if (!text || !formattedText) return false;
147
+ // Visibility keys off the formatted text: a block whose raw text is only
148
+ // comment noise (`<!-- -->\n`) formats to whitespace and stays hidden. The
149
+ // raw canonicalize check still hides dot/ellipsis-only placeholder blocks.
150
+ return formattedText.trim().length > 0 && canonicalizeMessage(text).length > 0;
105
151
  }
106
152
 
107
153
  /** Whether an assistant message contains thinking content the TUI can reveal. */