@pi-kaush/pi-tool-call-markers 0.3.14 → 0.3.16
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 +28 -2
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/index.ts +24 -18
- package/src/thinking-block-merger.ts +180 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Draw subagent rows with the same faint rail as every other collapsed
|
|
6
|
+
tool call. They used a bold `│` of their own, and failed subagent plans
|
|
7
|
+
painted the rail in the error color.
|
|
8
|
+
- Stop the thinking spinner from forcing full transcript redraws. Changing
|
|
9
|
+
a line above the viewport makes the terminal clear and replay the whole
|
|
10
|
+
transcript, and the live label kept animating after answer text pushed it
|
|
11
|
+
out of view. The label now settles when the answer or a tool call starts,
|
|
12
|
+
so its duration is the real thinking time. With several thinking runs in
|
|
13
|
+
one message, earlier runs show a static `│ * Thought`, and the current
|
|
14
|
+
run animates only while it is the message's last line.
|
|
15
|
+
- Never animate over a click-revealed thinking trace or answer Markdown
|
|
16
|
+
that happens to equal the label.
|
|
17
|
+
- Keep `/toggle-info` replay from bringing back thinking that a message's
|
|
18
|
+
final update dropped.
|
|
19
|
+
|
|
20
|
+
- Fix assistant replies vanishing behind the thinking label. The label
|
|
21
|
+
restyle matched any text node containing the label, so a reply that quoted
|
|
22
|
+
`Thinking...` or `│ * Thought` was replaced by the label until Ctrl+T. Only
|
|
23
|
+
Pi's thinking-run label nodes are restyled now, on an exact match, and only
|
|
24
|
+
on rows that carry thinking.
|
|
25
|
+
- Keep the live thinking spinner moving between thinking deltas. The frame is
|
|
26
|
+
read from the clock on each redraw that Pi's working indicator already
|
|
27
|
+
drives, instead of only when new thinking text arrives.
|
|
28
|
+
|
|
5
29
|
- Anchor the settled thinking label like a collapsed tool call:
|
|
6
30
|
`│ * Thought · 2.5s` instead of `+ Thought · 2.5s`. The faint rail and
|
|
7
31
|
bold glyph match the tool rows around it, so a turn that alternates
|
|
8
|
-
thinking and calls reads as one column. The live
|
|
9
|
-
|
|
32
|
+
thinking and calls reads as one column. The live label takes the same
|
|
33
|
+
anchor, `│ ⠋ Thinking…`, with the spinner in the glyph slot, so settling
|
|
34
|
+
swaps only the glyph, the words, and the tint instead of shifting the text
|
|
35
|
+
two columns and drawing a rail in.
|
|
10
36
|
|
|
11
37
|
- Paint collapsed rows with the theme's `dim` token instead of
|
|
12
38
|
`syntaxComment`. Pi's generated `system` theme solves `syntaxComment` to
|
package/README.md
CHANGED
|
@@ -97,11 +97,11 @@ The second package entrypoint, `src/thinking-block-merger.ts`, combines only dir
|
|
|
97
97
|
When Pi exposes its per-row hidden-thinking and streaming fields, hidden reasoning uses these native-themed labels:
|
|
98
98
|
|
|
99
99
|
```text
|
|
100
|
-
⠋ Thinking… → ⠙ Thinking… → …
|
|
100
|
+
│ ⠋ Thinking… → │ ⠙ Thinking… → …
|
|
101
101
|
│ * Thought · 2.5s
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
The live label samples Pi's native braille spinner sequence from the
|
|
104
|
+
The live label samples Pi's native braille spinner sequence from the clock each time the TUI draws it, so it steps with the redraws Pi's working indicator already drives instead of freezing between bursty thinking deltas. It does not add a timer; with the working indicator hidden, the spinner advances on each content update. The adapter stores the first local streaming timestamp per assistant row in a `WeakMap`. A restored message or an older runtime with no streaming argument uses `│ * Thought`. Both labels borrow the collapsed tool-row anchor — a faint `│` rail and a bold glyph — so reasoning reads like the tool calls around it. The spinner holds the glyph slot and settles into `*` in place: the rail and the label text keep their columns, and the rail keeps the settled tone throughout. There is no interval, timeout, render request, model call, or network work.
|
|
105
105
|
|
|
106
106
|
Pi renders thinking labels and traces italic. This package drops those italics: both labels read as plain collapsed rows (the live spinner keeps its thinking-level tint, the settled row the muted tone, and a theme that resolves no color still loses the italics), and a visible trace stays italic only while it streams, then settles into ordinary transcript text. `PI_TOOL_CALL_MARKERS_THOUGHT_COLOR=inherit` is the opt-out that keeps Pi's native italic `thinkingText` styling.
|
|
107
107
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,8 +24,7 @@ import { isQuestionToolCall, renderQuestionBlock } from "./question-block.ts";
|
|
|
24
24
|
import { sanitizeInline } from "./sanitize.ts";
|
|
25
25
|
|
|
26
26
|
const OUTER_INSET = 2;
|
|
27
|
-
const
|
|
28
|
-
const GROUP_CALL_MARKER = "│";
|
|
27
|
+
const TOOL_RAIL_MARKER = "│";
|
|
29
28
|
// Collapsed rows anchor with a glyph instead of repeating the tool name;
|
|
30
29
|
// tools that read as one family share a glyph, and unmapped tools fall back
|
|
31
30
|
// to `*`. A single space separates the glyph from the call content: the
|
|
@@ -898,11 +897,23 @@ function codemodeCallLabel(row: ToolExecutionRow): string | undefined {
|
|
|
898
897
|
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
899
898
|
}
|
|
900
899
|
|
|
900
|
+
// One anchor builder for every collapsed row: the faint rail, then the
|
|
901
|
+
// row's bold glyph when it has one — `│ ● src/a.ts`, `│ $ npm test`. A
|
|
902
|
+
// subagent row anchors on the rail alone and keeps its `│ subagent …`
|
|
903
|
+
// reading instead of a glyph.
|
|
904
|
+
function collapsedAnchor(
|
|
905
|
+
theme: ThemeLike,
|
|
906
|
+
color: string,
|
|
907
|
+
glyph: string | undefined,
|
|
908
|
+
): string {
|
|
909
|
+
const rail = `${fgCollapsedRail(theme, TOOL_RAIL_MARKER)} `;
|
|
910
|
+
return glyph === undefined
|
|
911
|
+
? rail
|
|
912
|
+
: `${rail}${fgCollapsed(theme, color, glyph, true)} `;
|
|
913
|
+
}
|
|
914
|
+
|
|
901
915
|
// The collapsed-row seam lives here and nowhere else: which anchor a row
|
|
902
|
-
// takes,
|
|
903
|
-
// Anchors always render bold and take no joining colon — `│ ● src/a.ts`,
|
|
904
|
-
// `│ $ npm test` — and subagent fallbacks keep their `│ subagent …`
|
|
905
|
-
// reading instead of a glyph. Callers append the outcome tail.
|
|
916
|
+
// takes, and the call text that follows it. Callers append the outcome tail.
|
|
906
917
|
function collapsedSeam(
|
|
907
918
|
row: ToolExecutionRow,
|
|
908
919
|
width: number,
|
|
@@ -911,17 +922,12 @@ function collapsedSeam(
|
|
|
911
922
|
): { anchor: string; content: string } {
|
|
912
923
|
const isBash = row.toolName === "bash";
|
|
913
924
|
const isSubagent = row.toolName === "subagent";
|
|
914
|
-
const glyph =
|
|
915
|
-
?
|
|
916
|
-
:
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
theme,
|
|
921
|
-
color,
|
|
922
|
-
glyph,
|
|
923
|
-
true,
|
|
924
|
-
)} `;
|
|
925
|
+
const glyph = isSubagent
|
|
926
|
+
? undefined
|
|
927
|
+
: isBash
|
|
928
|
+
? "$"
|
|
929
|
+
: (TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*");
|
|
930
|
+
const anchor = collapsedAnchor(theme, color, glyph);
|
|
925
931
|
const budget = Math.max(1, width - visibleWidth(anchor));
|
|
926
932
|
// Self-rendered rows build their own `tool {args}` label — Pi's call title
|
|
927
933
|
// is gone once settled — while ordinary rows scrape the rendered call
|
|
@@ -1209,7 +1215,7 @@ function renderSubagentPlan(
|
|
|
1209
1215
|
const previewOf = (step: SubagentStep) =>
|
|
1210
1216
|
fgCollapsed(theme, detailColor, ` ${subagentStepPreview(step.task)}`);
|
|
1211
1217
|
|
|
1212
|
-
const marker =
|
|
1218
|
+
const marker = collapsedAnchor(theme, color, undefined);
|
|
1213
1219
|
const budget = Math.max(1, width - visibleWidth(marker));
|
|
1214
1220
|
const progress = subagentProgressText(row);
|
|
1215
1221
|
const outcome = failed
|
|
@@ -105,21 +105,34 @@ function labelStyle(
|
|
|
105
105
|
return styledWith(token, theme);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
108
|
+
// Both labels wear the collapsed tool-row anchor — faint rail, bold glyph —
|
|
109
|
+
// so reasoning reads like the tool calls around it. The live spinner holds
|
|
110
|
+
// the glyph slot and settles into `*` in place: the rail and the label text
|
|
111
|
+
// keep their columns, and only the glyph, the words, and the tint change.
|
|
111
112
|
const THOUGHT_RAIL = "│";
|
|
112
113
|
const THOUGHT_GLYPH = "*";
|
|
113
|
-
const
|
|
114
|
-
const
|
|
114
|
+
const SETTLED_THOUGHT = `${THOUGHT_RAIL} ${THOUGHT_GLYPH} Thought`;
|
|
115
|
+
const ANCHORED_LABEL_RE = /^│ (\S) (.*)$/su;
|
|
115
116
|
|
|
116
117
|
function isSettledThoughtLabel(label: string): boolean {
|
|
117
118
|
return label.startsWith(SETTLED_THOUGHT);
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// The rail keeps the settled collapsed tone in both states, like a pending
|
|
122
|
+
// tool row's rail, so settling never repaints it.
|
|
123
|
+
function styledAnchoredLabel(
|
|
124
|
+
raw: string,
|
|
125
|
+
style: LabelStyle,
|
|
126
|
+
theme: ThemeDetail,
|
|
127
|
+
): string {
|
|
128
|
+
const match = ANCHORED_LABEL_RE.exec(raw);
|
|
129
|
+
if (!match) return `${style.prefix}${raw}${style.suffix}`;
|
|
130
|
+
const rail = settledMutedStyle(theme) ?? style;
|
|
131
|
+
const railText = `\x1b[2m${THOUGHT_RAIL}\x1b[22m`;
|
|
132
|
+
const body = ` \x1b[1m${match[1]}\x1b[22m ${match[2]}${style.suffix}`;
|
|
133
|
+
return rail.prefix === style.prefix
|
|
134
|
+
? `${style.prefix}${railText}${body}`
|
|
135
|
+
: `${rail.prefix}${railText}${rail.suffix}${style.prefix}${body}`;
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
export function visibleThoughtLabel(label: string): string {
|
|
@@ -129,10 +142,8 @@ export function visibleThoughtLabel(label: string): string {
|
|
|
129
142
|
const settled = isSettledThoughtLabel(raw);
|
|
130
143
|
const style = theme ? labelStyle(settled, theme) : undefined;
|
|
131
144
|
// A theme that cannot resolve a color still drops Pi's italics.
|
|
132
|
-
if (!style) return `${ITALIC_OFF}${raw}`;
|
|
133
|
-
return
|
|
134
|
-
? styledSettledThought(raw, style)
|
|
135
|
-
: `${style.prefix}${raw}${style.suffix}`;
|
|
145
|
+
if (!style || !theme) return `${ITALIC_OFF}${raw}`;
|
|
146
|
+
return styledAnchoredLabel(raw, style, theme);
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
type AssistantMessageLike = {
|
|
@@ -149,6 +160,7 @@ type AssistantMessageRow = {
|
|
|
149
160
|
type TextLikeChild = {
|
|
150
161
|
text?: unknown;
|
|
151
162
|
setText?(text: string): void;
|
|
163
|
+
render?(width: number): string[];
|
|
152
164
|
};
|
|
153
165
|
|
|
154
166
|
type MarkdownLikeChild = {
|
|
@@ -192,7 +204,85 @@ function unitalicizeSettledThinking(row: AssistantMessageRow): void {
|
|
|
192
204
|
// previous (italic) frame, so an in-line italic reset may never reach the
|
|
193
205
|
// terminal. Swap the node's text for a self-contained styled version after
|
|
194
206
|
// each native render pass instead.
|
|
195
|
-
|
|
207
|
+
//
|
|
208
|
+
// Only label nodes qualify. Answer prose and a click-revealed trace are
|
|
209
|
+
// Markdown nodes that also carry `text` and `setText`, so Markdown never
|
|
210
|
+
// matches, and a candidate's plain text must equal the label exactly. Pi 1.0
|
|
211
|
+
// wraps each thinking run in a MouseRegion; each child is judged on its own,
|
|
212
|
+
// unwrapped when it has a wrapped inner.
|
|
213
|
+
function hiddenLabelNodes(
|
|
214
|
+
row: AssistantMessageRow,
|
|
215
|
+
plainLabel: string,
|
|
216
|
+
): Array<{ node: TextLikeChild; trailing: boolean }> {
|
|
217
|
+
const children = row.contentContainer?.children;
|
|
218
|
+
if (!Array.isArray(children)) return [];
|
|
219
|
+
const labels: Array<{ node: TextLikeChild; trailing: boolean }> = [];
|
|
220
|
+
children.forEach((child, index) => {
|
|
221
|
+
const node = ((child as { child?: unknown } | undefined)?.child ??
|
|
222
|
+
child) as TextLikeChild | undefined;
|
|
223
|
+
if (
|
|
224
|
+
isMarkdownLike(node) ||
|
|
225
|
+
typeof node?.text !== "string" ||
|
|
226
|
+
typeof node.setText !== "function" ||
|
|
227
|
+
plainText(node.text) !== plainLabel
|
|
228
|
+
) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
labels.push({ node, trailing: index === children.length - 1 });
|
|
232
|
+
});
|
|
233
|
+
return labels;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Duck-typed, not `instanceof`: a locally loaded extension can resolve its
|
|
237
|
+
// own pi-tui copy, so class identity is not shared with Pi's. pi-tui's
|
|
238
|
+
// Markdown declares `theme` and `defaultTextStyle` fields; Text has neither.
|
|
239
|
+
function isMarkdownLike(node: unknown): boolean {
|
|
240
|
+
return (
|
|
241
|
+
!!node &&
|
|
242
|
+
typeof node === "object" &&
|
|
243
|
+
("defaultTextStyle" in node || "theme" in node)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function plainText(text: string): string {
|
|
248
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// A live label re-reads the clock whenever the TUI draws it, so the spinner
|
|
252
|
+
// steps with the redraws Pi's working indicator already drives instead of
|
|
253
|
+
// freezing between bursty thinking deltas. Pi rebuilds the label node on
|
|
254
|
+
// every content update, so the override dies with the node; no timer is
|
|
255
|
+
// added, and with no redraws the spinner still advances on each delta.
|
|
256
|
+
function animateLiveLabel(node: TextLikeChild, startedAt: number): void {
|
|
257
|
+
const render = node.render;
|
|
258
|
+
if (typeof render !== "function") return;
|
|
259
|
+
node.render = function renderLiveThinkingLabel(
|
|
260
|
+
this: TextLikeChild,
|
|
261
|
+
width: number,
|
|
262
|
+
): string[] {
|
|
263
|
+
try {
|
|
264
|
+
const styled = visibleThoughtLabel(
|
|
265
|
+
liveThinkingLabel(startedAt, Date.now()),
|
|
266
|
+
);
|
|
267
|
+
if (this.text !== styled) this.setText?.(styled);
|
|
268
|
+
} catch {
|
|
269
|
+
// Keep the last frame if styling fails mid-render.
|
|
270
|
+
}
|
|
271
|
+
return Reflect.apply(render, this, [width]) as string[];
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Pi gives every thinking run in a message the same label. Only the last
|
|
276
|
+
// run is the current one; earlier runs read as settled so a line above later
|
|
277
|
+
// content never changes. The current run animates only when it is the
|
|
278
|
+
// message's last line and no tool rows render below the message: changing
|
|
279
|
+
// a line above the viewport makes pi-tui clear and replay the transcript.
|
|
280
|
+
// Otherwise it holds one spinner frame until it settles.
|
|
281
|
+
function restyleHiddenThinkingLabel(
|
|
282
|
+
row: AssistantMessageRow,
|
|
283
|
+
message: AssistantMessageLike,
|
|
284
|
+
liveSince?: number,
|
|
285
|
+
): void {
|
|
196
286
|
if (
|
|
197
287
|
row.hideThinkingBlock !== true ||
|
|
198
288
|
typeof row.hiddenThinkingLabel !== "string"
|
|
@@ -201,18 +291,28 @@ function restyleHiddenThinkingLabel(row: AssistantMessageRow): void {
|
|
|
201
291
|
}
|
|
202
292
|
const label = row.hiddenThinkingLabel;
|
|
203
293
|
if (!stylesThinkingLabels()) return;
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
) {
|
|
212
|
-
|
|
294
|
+
const labels = hiddenLabelNodes(row, plainText(label));
|
|
295
|
+
const hasToolCalls =
|
|
296
|
+
Array.isArray(message.content) &&
|
|
297
|
+
message.content.some(
|
|
298
|
+
(block) => (block as { type?: unknown } | undefined)?.type === "toolCall",
|
|
299
|
+
);
|
|
300
|
+
labels.forEach(({ node, trailing }, index) => {
|
|
301
|
+
if (index < labels.length - 1) {
|
|
302
|
+
node.setText?.(visibleThoughtLabel(SETTLED_THOUGHT));
|
|
303
|
+
return;
|
|
213
304
|
}
|
|
214
|
-
|
|
215
|
-
|
|
305
|
+
if (liveSince === undefined) {
|
|
306
|
+
node.setText?.(visibleThoughtLabel(label));
|
|
307
|
+
} else if (trailing && !hasToolCalls) {
|
|
308
|
+
node.setText?.(visibleThoughtLabel(label));
|
|
309
|
+
animateLiveLabel(node, liveSince);
|
|
310
|
+
} else {
|
|
311
|
+
node.setText?.(
|
|
312
|
+
visibleThoughtLabel(liveThinkingLabel(liveSince, liveSince)),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
216
316
|
}
|
|
217
317
|
|
|
218
318
|
type ThinkingTiming = {
|
|
@@ -256,7 +356,9 @@ function trackReplayRow(
|
|
|
256
356
|
args: unknown[],
|
|
257
357
|
): void {
|
|
258
358
|
try {
|
|
259
|
-
|
|
359
|
+
// A tracked row keeps tracking even when its final content drops
|
|
360
|
+
// thinking, so a replay never resurrects an earlier streaming snapshot.
|
|
361
|
+
if (!hasThinkingContent(message) && !state.rows.has(row)) return;
|
|
260
362
|
state.rows.set(row, { message, args });
|
|
261
363
|
while (state.rows.size > MAX_REPLAY_ROWS) {
|
|
262
364
|
const oldest = state.rows.keys().next().value;
|
|
@@ -321,6 +423,10 @@ function formatThoughtDuration(startedAt: number, finishedAt: number): string {
|
|
|
321
423
|
return `${(Math.max(0, finishedAt - startedAt) / 1000).toFixed(1)}s`;
|
|
322
424
|
}
|
|
323
425
|
|
|
426
|
+
function liveThinkingLabel(startedAt: number, now: number): string {
|
|
427
|
+
return `${THOUGHT_RAIL} ${thinkingSpinner(startedAt, now)} Thinking…`;
|
|
428
|
+
}
|
|
429
|
+
|
|
324
430
|
function thinkingSpinner(startedAt: number, now: number): string {
|
|
325
431
|
const frame = Math.floor(Math.max(0, now - startedAt) / SPINNER_INTERVAL_MS);
|
|
326
432
|
return PI_SPINNER_FRAMES[frame % PI_SPINNER_FRAMES.length]!;
|
|
@@ -342,23 +448,59 @@ function thinkingStillLive(
|
|
|
342
448
|
);
|
|
343
449
|
}
|
|
344
450
|
|
|
451
|
+
// Thinking ends when the answer starts: visible text or a tool call after
|
|
452
|
+
// the last thinking block. Settling there keeps the duration honest and
|
|
453
|
+
// keeps a live label as the transcript's last line, so its spinner never
|
|
454
|
+
// changes a line scrolled above the viewport (which forces a full replay).
|
|
455
|
+
function answerStarted(message: AssistantMessageLike): boolean {
|
|
456
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
457
|
+
let lastThinking = -1;
|
|
458
|
+
content.forEach((block, index) => {
|
|
459
|
+
if (isThinkingContent(block)) lastThinking = index;
|
|
460
|
+
});
|
|
461
|
+
return content.slice(lastThinking + 1).some((block) => {
|
|
462
|
+
const b = block as { type?: unknown; text?: unknown } | undefined;
|
|
463
|
+
return (
|
|
464
|
+
b?.type === "toolCall" ||
|
|
465
|
+
(b?.type === "text" && typeof b.text === "string" && b.text.trim() !== "")
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function timingLive(
|
|
471
|
+
streaming: boolean | undefined,
|
|
472
|
+
timing: ThinkingTiming | undefined,
|
|
473
|
+
): boolean {
|
|
474
|
+
return (
|
|
475
|
+
streaming !== false &&
|
|
476
|
+
timing !== undefined &&
|
|
477
|
+
timing.finishedAt === undefined
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
345
481
|
function lifecycleLabel(
|
|
346
482
|
row: AssistantMessageRow,
|
|
483
|
+
message: AssistantMessageLike,
|
|
347
484
|
streaming: boolean | undefined,
|
|
348
485
|
timings: WeakMap<AssistantMessageRow, ThinkingTiming>,
|
|
349
486
|
): string {
|
|
350
487
|
const now = Date.now();
|
|
351
488
|
const previous = timings.get(row);
|
|
352
|
-
|
|
489
|
+
const answering = answerStarted(message);
|
|
490
|
+
if (!answering && thinkingStillLive(streaming, previous)) {
|
|
353
491
|
const current =
|
|
354
492
|
previous && previous.finishedAt === undefined
|
|
355
493
|
? previous
|
|
356
494
|
: { startedAt: now };
|
|
357
495
|
timings.set(row, current);
|
|
358
|
-
return
|
|
496
|
+
return liveThinkingLabel(current.startedAt, now);
|
|
359
497
|
}
|
|
360
498
|
|
|
361
|
-
if (
|
|
499
|
+
if (
|
|
500
|
+
(streaming === false || answering) &&
|
|
501
|
+
previous &&
|
|
502
|
+
previous.finishedAt === undefined
|
|
503
|
+
) {
|
|
362
504
|
previous.finishedAt = now;
|
|
363
505
|
}
|
|
364
506
|
|
|
@@ -411,7 +553,7 @@ function applyHiddenThinkingLabel(
|
|
|
411
553
|
): void {
|
|
412
554
|
// Record the component's first streaming update even when the provider has
|
|
413
555
|
// not emitted a non-empty thinking block yet.
|
|
414
|
-
const label = lifecycleLabel(row, streaming, timings);
|
|
556
|
+
const label = lifecycleLabel(row, message, streaming, timings);
|
|
415
557
|
if (!hasThinkingContent(message)) return;
|
|
416
558
|
if (
|
|
417
559
|
row.hideThinkingBlock !== true ||
|
|
@@ -483,14 +625,21 @@ function installThinkingGroupingPatch():
|
|
|
483
625
|
}
|
|
484
626
|
Reflect.apply(state.originalUpdateContent, this, [combined, ...args]);
|
|
485
627
|
try {
|
|
486
|
-
|
|
628
|
+
if (hasThinkingContent(combined)) {
|
|
629
|
+
const timing = state.timings.get(this);
|
|
630
|
+
const liveSince =
|
|
631
|
+
timing && timingLive(streaming, timing)
|
|
632
|
+
? timing.startedAt
|
|
633
|
+
: undefined;
|
|
634
|
+
restyleHiddenThinkingLabel(this, combined, liveSince);
|
|
635
|
+
}
|
|
487
636
|
} catch {
|
|
488
637
|
// Keep Pi's native label styling if the row shape changes.
|
|
489
638
|
}
|
|
490
639
|
try {
|
|
491
640
|
if (
|
|
492
641
|
hasThinkingContent(combined) &&
|
|
493
|
-
!
|
|
642
|
+
!timingLive(streaming, state.timings.get(this))
|
|
494
643
|
) {
|
|
495
644
|
unitalicizeSettledThinking(this);
|
|
496
645
|
}
|