@fyeeme/pi-thinking-ui 1.0.0 → 1.1.0
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 +22 -1
- package/index.ts +36 -124
- package/markdown-render.ts +104 -0
- package/package.json +8 -9
- package/parse.ts +42 -12
- package/state.ts +7 -297
- package/types.ts +0 -6
- package/internal-patch.ts +0 -441
- package/render.ts +0 -414
package/render.ts
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
-
import { deriveThinkingUI } from "./parse.ts";
|
|
4
|
-
import { getActiveThinkingState, getCurrentThinkingScopeKey, getThinkingUIMode } from "./state.ts";
|
|
5
|
-
import type { DerivedThinkingStep, ThinkingSemanticRole, ThinkingSourceBlock, ThinkingThemeLike } from "./types.ts";
|
|
6
|
-
|
|
7
|
-
interface RenderOptions {
|
|
8
|
-
mode: "collapsed" | "summary" | "expanded";
|
|
9
|
-
steps: DerivedThinkingStep[];
|
|
10
|
-
activeStepId?: string;
|
|
11
|
-
isActive: boolean;
|
|
12
|
-
nowMs?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function roleColor(role: ThinkingSemanticRole): string {
|
|
16
|
-
switch (role) {
|
|
17
|
-
case "verify":
|
|
18
|
-
return "success";
|
|
19
|
-
case "error":
|
|
20
|
-
return "error";
|
|
21
|
-
case "compare":
|
|
22
|
-
return "warning";
|
|
23
|
-
case "inspect":
|
|
24
|
-
case "search":
|
|
25
|
-
return "mdLink";
|
|
26
|
-
case "write":
|
|
27
|
-
case "plan":
|
|
28
|
-
return "accent";
|
|
29
|
-
default:
|
|
30
|
-
return "muted";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function pulseGlyph(theme: ThinkingThemeLike, nowMs: number): string {
|
|
35
|
-
const frames = [
|
|
36
|
-
theme.fg("dim", "·"),
|
|
37
|
-
theme.fg("muted", "•"),
|
|
38
|
-
theme.fg("accent", "•"),
|
|
39
|
-
theme.fg("muted", "•"),
|
|
40
|
-
];
|
|
41
|
-
const frame = Math.floor(nowMs / 180) % frames.length;
|
|
42
|
-
return frames[frame] ?? frames[0]!;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type InlineSegmentStyle = "plain" | "bold" | "code";
|
|
46
|
-
|
|
47
|
-
interface InlineSegment {
|
|
48
|
-
text: string;
|
|
49
|
-
style: InlineSegmentStyle;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sanitizeThinkingText(text: string): string {
|
|
53
|
-
return text
|
|
54
|
-
.replace(/\r\n?/g, "\n")
|
|
55
|
-
.replace(/\u001b[\]PX^_][\s\S]*?(?:\u0007|\u001b\\|\u009c)/g, "")
|
|
56
|
-
.replace(/[\u0090\u0098\u009d\u009e\u009f][\s\S]*?(?:\u0007|\u001b\\|\u009c)/g, "")
|
|
57
|
-
.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|[ -/]*[0-9@-~])/g, "")
|
|
58
|
-
.replace(/\u009b[0-?]*[ -/]*[@-~]/g, "")
|
|
59
|
-
.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function parseThinkingInlineSegments(text: string): InlineSegment[] {
|
|
63
|
-
const sanitized = sanitizeThinkingText(text);
|
|
64
|
-
const segments: InlineSegment[] = [];
|
|
65
|
-
const markerRe = /(\*\*|__)(?=\S)([\s\S]*?\S)\1|`([^`]+)`|(?<![\w/.-])\*(?!\*)(?=\S)([\s\S]*?\S)(?<!\*)\*(?![\w/.-])|(?<![\w/.-])_(?!_)(?=\S)([\s\S]*?\S)(?<!_)_(?![\w/.-])/g;
|
|
66
|
-
let lastIndex = 0;
|
|
67
|
-
for (const match of sanitized.matchAll(markerRe)) {
|
|
68
|
-
const markerIndex = match.index ?? 0;
|
|
69
|
-
if (markerIndex > lastIndex) {
|
|
70
|
-
segments.push({ text: sanitized.slice(lastIndex, markerIndex), style: "plain" });
|
|
71
|
-
}
|
|
72
|
-
if (match[2]) segments.push({ text: match[2], style: "bold" });
|
|
73
|
-
if (match[3]) segments.push({ text: match[3], style: "code" });
|
|
74
|
-
if (match[4]) segments.push({ text: match[4], style: "plain" });
|
|
75
|
-
if (match[5]) segments.push({ text: match[5], style: "plain" });
|
|
76
|
-
lastIndex = markerIndex + match[0].length;
|
|
77
|
-
}
|
|
78
|
-
if (lastIndex < sanitized.length) {
|
|
79
|
-
segments.push({ text: sanitized.slice(lastIndex), style: "plain" });
|
|
80
|
-
}
|
|
81
|
-
return segments;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function renderThinkingInlineSegment(theme: ThinkingThemeLike, segment: InlineSegment): string {
|
|
85
|
-
if (segment.style === "bold") return theme.bold(theme.fg("thinkingText", segment.text));
|
|
86
|
-
if (segment.style === "code") return theme.bold(theme.fg("accent", segment.text));
|
|
87
|
-
return theme.fg("thinkingText", segment.text);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function wrapStepHeader(theme: ThinkingThemeLike, width: number, step: DerivedThinkingStep, active: boolean, connector: string): string[] {
|
|
91
|
-
const connectorColor = active ? "accent" : "muted";
|
|
92
|
-
const icon = theme.fg(roleColor(step.role), step.icon);
|
|
93
|
-
const prefix = `${theme.fg(connectorColor, connector)} ${icon} `;
|
|
94
|
-
const continuationPrefix = " ".repeat(visibleWidth(`${connector} ${step.icon} `));
|
|
95
|
-
const renderedSummary = renderThinkingInlineMarkup(theme, step.summary);
|
|
96
|
-
const summaryText = active ? theme.bold(renderedSummary) : renderedSummary;
|
|
97
|
-
const wrappedSummary = wrapTextWithAnsi(summaryText, Math.max(8, width - visibleWidth(prefix)));
|
|
98
|
-
if (wrappedSummary.length === 0) {
|
|
99
|
-
return [truncateToWidth(prefix, width, "")];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return wrappedSummary.map((line, index) =>
|
|
103
|
-
truncateToWidth(`${index === 0 ? prefix : continuationPrefix}${line}`, width, ""),
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function pickCollapsedStep(steps: DerivedThinkingStep[], activeStepId?: string): DerivedThinkingStep | undefined {
|
|
108
|
-
if (steps.length === 0) return undefined;
|
|
109
|
-
if (activeStepId) {
|
|
110
|
-
const active = steps.find((step) => step.id === activeStepId);
|
|
111
|
-
if (active) return active;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let latestFailureIndex = -1;
|
|
115
|
-
let latestSuccessAfterFailureIndex = -1;
|
|
116
|
-
|
|
117
|
-
for (let index = 0; index < steps.length; index += 1) {
|
|
118
|
-
const step = steps[index]!;
|
|
119
|
-
if (step.hasExplicitFailure) {
|
|
120
|
-
latestFailureIndex = index;
|
|
121
|
-
latestSuccessAfterFailureIndex = -1;
|
|
122
|
-
}
|
|
123
|
-
if (latestFailureIndex !== -1 && step.hasExplicitSuccess && index > latestFailureIndex) {
|
|
124
|
-
latestSuccessAfterFailureIndex = index;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (latestSuccessAfterFailureIndex !== -1) return steps[latestSuccessAfterFailureIndex];
|
|
129
|
-
if (latestFailureIndex !== -1) return steps[latestFailureIndex];
|
|
130
|
-
|
|
131
|
-
return [...steps]
|
|
132
|
-
.sort((left, right) => (right.collapsedPriority ?? 0) - (left.collapsedPriority ?? 0) || right.blockIndex - left.blockIndex || right.stepIndex - left.stepIndex)[0];
|
|
133
|
-
}
|
|
134
|
-
function wrapCollapsedSummaryText(theme: ThinkingThemeLike, text: string, firstWidth: number, continuationWidth: number): string[] {
|
|
135
|
-
const words = parseThinkingInlineSegments(text).flatMap((segment) =>
|
|
136
|
-
segment.text
|
|
137
|
-
.split(/\s+/)
|
|
138
|
-
.filter(Boolean)
|
|
139
|
-
.map((word) => renderThinkingInlineSegment(theme, { ...segment, text: word })),
|
|
140
|
-
);
|
|
141
|
-
if (words.length === 0) return [];
|
|
142
|
-
|
|
143
|
-
const lines: string[] = [];
|
|
144
|
-
let current = "";
|
|
145
|
-
let currentWidth = Math.max(8, firstWidth);
|
|
146
|
-
const continuationLineWidth = () => Math.max(8, continuationWidth);
|
|
147
|
-
|
|
148
|
-
for (const word of words) {
|
|
149
|
-
let pending = word;
|
|
150
|
-
while (pending.length > 0) {
|
|
151
|
-
const candidate = current ? `${current} ${pending}` : pending;
|
|
152
|
-
if (visibleWidth(candidate) <= currentWidth) {
|
|
153
|
-
current = candidate;
|
|
154
|
-
pending = "";
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (current) {
|
|
159
|
-
lines.push(current);
|
|
160
|
-
current = "";
|
|
161
|
-
currentWidth = continuationLineWidth();
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const wrappedWord = wrapTextWithAnsi(pending, currentWidth);
|
|
166
|
-
if (wrappedWord.length === 0) {
|
|
167
|
-
pending = "";
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (wrappedWord.length === 1) {
|
|
172
|
-
current = wrappedWord[0] ?? "";
|
|
173
|
-
pending = "";
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
lines.push(...wrappedWord.slice(0, -1));
|
|
178
|
-
pending = wrappedWord[wrappedWord.length - 1] ?? "";
|
|
179
|
-
currentWidth = continuationLineWidth();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (current) lines.push(current);
|
|
184
|
-
return lines;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function renderCollapsed(theme: ThinkingThemeLike, width: number, steps: DerivedThinkingStep[], activeStepId?: string, isActive = false, nowMs = Date.now()): string[] {
|
|
188
|
-
const step = pickCollapsedStep(steps, activeStepId);
|
|
189
|
-
if (!step) return [];
|
|
190
|
-
|
|
191
|
-
const label = "Thinking";
|
|
192
|
-
const icon = theme.fg(roleColor(step.role), step.icon);
|
|
193
|
-
const activity = isActive ? pulseGlyph(theme, nowMs) : theme.fg("dim", "·");
|
|
194
|
-
const activitySuffix = ` ${activity}`;
|
|
195
|
-
const activityWidth = visibleWidth(activitySuffix);
|
|
196
|
-
const prefix = `${theme.fg("muted", "│")} ${theme.fg("dim", label)} ${icon} `;
|
|
197
|
-
const continuationPrefix = `${theme.fg("muted", "│")} ${" ".repeat(visibleWidth(`${label} ${step.icon} `))}`;
|
|
198
|
-
const summaryLines = wrapCollapsedSummaryText(
|
|
199
|
-
theme,
|
|
200
|
-
step.summary,
|
|
201
|
-
Math.max(1, width - visibleWidth(prefix) - activityWidth),
|
|
202
|
-
Math.max(1, width - visibleWidth(continuationPrefix) - activityWidth),
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
if (summaryLines.length <= 1) {
|
|
206
|
-
return [truncateToWidth(`${prefix}${summaryLines[0] ?? renderThinkingInlineMarkup(theme, step.summary)}${activitySuffix}`, width, "")];
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return summaryLines.map((line, index) => {
|
|
210
|
-
if (index === 0) return truncateToWidth(`${prefix}${line}`, width, "");
|
|
211
|
-
if (index === summaryLines.length - 1) return truncateToWidth(`${continuationPrefix}${line}${activitySuffix}`, width, "");
|
|
212
|
-
return truncateToWidth(`${continuationPrefix}${line}`, width, "");
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function stepHasEventType(step: DerivedThinkingStep, type: string): boolean {
|
|
217
|
-
return step.summaryEvents?.some((event) => event.type === type) ?? false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function selectSummarySteps(steps: DerivedThinkingStep[], activeStepId?: string): DerivedThinkingStep[] {
|
|
221
|
-
if (steps.length <= 5) return steps;
|
|
222
|
-
|
|
223
|
-
const indexed = steps.map((step, index) => ({ step, index }));
|
|
224
|
-
const selected = new Set<number>();
|
|
225
|
-
const activeIndex = activeStepId ? steps.findIndex((step) => step.id === activeStepId) : -1;
|
|
226
|
-
let latestFailureIndex = -1;
|
|
227
|
-
let latestSuccessAfterFailureIndex = -1;
|
|
228
|
-
|
|
229
|
-
for (let index = 0; index < steps.length; index += 1) {
|
|
230
|
-
const step = steps[index]!;
|
|
231
|
-
if (step.hasExplicitFailure) {
|
|
232
|
-
latestFailureIndex = index;
|
|
233
|
-
latestSuccessAfterFailureIndex = -1;
|
|
234
|
-
}
|
|
235
|
-
if (latestFailureIndex !== -1 && step.hasExplicitSuccess && index > latestFailureIndex) {
|
|
236
|
-
latestSuccessAfterFailureIndex = index;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (activeIndex !== -1) selected.add(activeIndex);
|
|
241
|
-
if (latestFailureIndex !== -1) selected.add(latestFailureIndex);
|
|
242
|
-
if (latestSuccessAfterFailureIndex !== -1) selected.add(latestSuccessAfterFailureIndex);
|
|
243
|
-
|
|
244
|
-
const scoreEntry = ({ step, index }: { step: DerivedThinkingStep; index: number }): number => {
|
|
245
|
-
let score = step.collapsedPriority ?? 0;
|
|
246
|
-
const isStaleSuccessBeforeLatestFailure = step.hasExplicitSuccess && latestFailureIndex !== -1 && index < latestFailureIndex;
|
|
247
|
-
if (index === latestFailureIndex && latestSuccessAfterFailureIndex === -1) score += 120;
|
|
248
|
-
if (index === latestSuccessAfterFailureIndex) score += 110;
|
|
249
|
-
if (stepHasEventType(step, "decision") || stepHasEventType(step, "plan_change")) score += 80;
|
|
250
|
-
if (step.hasExplicitFailure) score += 50;
|
|
251
|
-
if (step.hasExplicitSuccess && !isStaleSuccessBeforeLatestFailure) score += 45;
|
|
252
|
-
if (isStaleSuccessBeforeLatestFailure) score -= 200;
|
|
253
|
-
if (stepHasEventType(step, "focus") && !stepHasEventType(step, "decision") && !stepHasEventType(step, "plan_change") && !step.hasExplicitFailure && !step.hasExplicitSuccess) score -= 15;
|
|
254
|
-
return score + (index / 100);
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const targetCount = Math.min(5, steps.length);
|
|
258
|
-
for (const entry of [...indexed].sort((left, right) => scoreEntry(right) - scoreEntry(left))) {
|
|
259
|
-
if (selected.size >= targetCount) break;
|
|
260
|
-
selected.add(entry.index);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return [...selected]
|
|
264
|
-
.sort((left, right) => left - right)
|
|
265
|
-
.map((index) => steps[index]!)
|
|
266
|
-
.slice(0, targetCount);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function renderSummary(theme: ThinkingThemeLike, width: number, steps: DerivedThinkingStep[], activeStepId?: string): string[] {
|
|
270
|
-
const lines = [
|
|
271
|
-
truncateToWidth(`${theme.fg("muted", "┆")} ${theme.fg("dim", "Thinking UI · Summary")}`, width),
|
|
272
|
-
];
|
|
273
|
-
const visibleSteps = selectSummarySteps(steps, activeStepId);
|
|
274
|
-
for (let index = 0; index < visibleSteps.length; index++) {
|
|
275
|
-
const step = visibleSteps[index]!;
|
|
276
|
-
const connector = index === visibleSteps.length - 1 ? "└─" : "├─";
|
|
277
|
-
lines.push(...wrapStepHeader(theme, width, step, step.id === activeStepId, connector));
|
|
278
|
-
}
|
|
279
|
-
return lines;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function renderThinkingInlineMarkup(theme: ThinkingThemeLike, text: string): string {
|
|
283
|
-
const sanitized = sanitizeThinkingText(text);
|
|
284
|
-
const segments = parseThinkingInlineSegments(sanitized);
|
|
285
|
-
if (segments.length === 0) return theme.fg("thinkingText", sanitized);
|
|
286
|
-
return segments.map((segment) => renderThinkingInlineSegment(theme, segment)).join("");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function renderThinkingDisplayLine(theme: ThinkingThemeLike, text: string): string {
|
|
290
|
-
const headingMatch = text.match(/^(\s{0,3})#{1,6}\s+(.+)$/);
|
|
291
|
-
if (headingMatch) {
|
|
292
|
-
const indent = headingMatch[1] ?? "";
|
|
293
|
-
const content = headingMatch[2] ?? "";
|
|
294
|
-
return `${indent}${theme.bold(renderThinkingInlineMarkup(theme, content))}`;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const listMatch = text.match(/^(\s*)([-*+]|\d+[.)]|[a-z][.)])\s+(.+)$/i);
|
|
298
|
-
if (listMatch) {
|
|
299
|
-
const indent = listMatch[1] ?? "";
|
|
300
|
-
const marker = listMatch[2] ?? "";
|
|
301
|
-
const content = listMatch[3] ?? "";
|
|
302
|
-
const renderedMarker = /^[-*+]$/.test(marker) ? "•" : marker;
|
|
303
|
-
return `${indent}${theme.fg("muted", renderedMarker)} ${renderThinkingInlineMarkup(theme, content)}`;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return renderThinkingInlineMarkup(theme, text);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function renderWrappedRawText(theme: ThinkingThemeLike, text: string, width: number, prefix: string): string[] {
|
|
310
|
-
const innerWidth = Math.max(8, width - visibleWidth(prefix));
|
|
311
|
-
const sanitizedText = sanitizeThinkingText(text);
|
|
312
|
-
const rawLines = sanitizedText.replace(/\t/g, " " ).split("\n");
|
|
313
|
-
const rendered: string[] = [];
|
|
314
|
-
for (const rawLine of rawLines) {
|
|
315
|
-
if (rawLine.trim().length === 0) {
|
|
316
|
-
rendered.push(truncateToWidth(prefix, width, ""));
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
const styled = renderThinkingDisplayLine(theme, rawLine);
|
|
320
|
-
const wrapped = wrapTextWithAnsi(styled, innerWidth);
|
|
321
|
-
for (const line of wrapped) {
|
|
322
|
-
rendered.push(truncateToWidth(`${prefix}${line}`, width, ""));
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
return rendered;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function renderExpanded(theme: ThinkingThemeLike, width: number, steps: DerivedThinkingStep[], activeStepId?: string): string[] {
|
|
329
|
-
const lines = [
|
|
330
|
-
truncateToWidth(`${theme.fg("muted", "┆")} ${theme.fg("dim", "Thinking UI · Expanded")}`, width),
|
|
331
|
-
];
|
|
332
|
-
|
|
333
|
-
for (let index = 0; index < steps.length; index++) {
|
|
334
|
-
const step = steps[index]!;
|
|
335
|
-
const connector = index === steps.length - 1 ? "└─" : "├─";
|
|
336
|
-
const isActive = step.id === activeStepId;
|
|
337
|
-
lines.push(...wrapStepHeader(theme, width, step, isActive, connector));
|
|
338
|
-
|
|
339
|
-
const normalizedBody = step.body.trim();
|
|
340
|
-
if (!normalizedBody) continue;
|
|
341
|
-
|
|
342
|
-
const bodyPrefix = index === steps.length - 1 ? " " : `${theme.fg("muted", "│")} `;
|
|
343
|
-
lines.push(...renderWrappedRawText(theme, normalizedBody, width, bodyPrefix));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return lines;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export function renderThinkingUILines(theme: ThinkingThemeLike, width: number, options: RenderOptions): string[] {
|
|
350
|
-
if (options.steps.length === 0) return [];
|
|
351
|
-
if (options.mode === "collapsed") {
|
|
352
|
-
return renderCollapsed(theme, width, options.steps, options.activeStepId, options.isActive, options.nowMs);
|
|
353
|
-
}
|
|
354
|
-
if (options.mode === "expanded") {
|
|
355
|
-
return renderExpanded(theme, width, options.steps, options.activeStepId);
|
|
356
|
-
}
|
|
357
|
-
return renderSummary(theme, width, options.steps, options.activeStepId);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export class ThinkingUIComponent implements Component {
|
|
361
|
-
private steps: DerivedThinkingStep[];
|
|
362
|
-
private cacheKey?: string;
|
|
363
|
-
private cachedLines?: string[];
|
|
364
|
-
private readonly scopeKey: string;
|
|
365
|
-
private readonly theme: ThinkingThemeLike;
|
|
366
|
-
private readonly messageTimestamp: number;
|
|
367
|
-
|
|
368
|
-
constructor(
|
|
369
|
-
theme: ThinkingThemeLike,
|
|
370
|
-
messageTimestamp: number,
|
|
371
|
-
blocks: ThinkingSourceBlock[],
|
|
372
|
-
scopeKey?: string,
|
|
373
|
-
) {
|
|
374
|
-
this.theme = theme;
|
|
375
|
-
this.messageTimestamp = messageTimestamp;
|
|
376
|
-
this.steps = deriveThinkingUI(blocks);
|
|
377
|
-
this.scopeKey = scopeKey ?? getCurrentThinkingScopeKey();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
render(width: number): string[] {
|
|
381
|
-
const mode = getThinkingUIMode(this.scopeKey);
|
|
382
|
-
const active = getActiveThinkingState(this.messageTimestamp, this.scopeKey);
|
|
383
|
-
const activeStepId = active.active && active.contentIndex !== undefined
|
|
384
|
-
? [...this.steps].reverse().find((step) => step.contentIndex === active.contentIndex)?.id
|
|
385
|
-
: undefined;
|
|
386
|
-
const shouldBypassCache = mode === "collapsed" && active.active;
|
|
387
|
-
const nextCacheKey = `${width}:${mode}:${active.active ? 1 : 0}:${activeStepId ?? ""}`;
|
|
388
|
-
if (!shouldBypassCache && this.cachedLines && this.cacheKey === nextCacheKey) {
|
|
389
|
-
return this.cachedLines;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const lines = renderThinkingUILines(this.theme, width, {
|
|
393
|
-
mode,
|
|
394
|
-
steps: this.steps,
|
|
395
|
-
activeStepId,
|
|
396
|
-
isActive: active.active,
|
|
397
|
-
nowMs: Date.now(),
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
if (!shouldBypassCache) {
|
|
401
|
-
this.cacheKey = nextCacheKey;
|
|
402
|
-
this.cachedLines = lines;
|
|
403
|
-
} else {
|
|
404
|
-
this.cacheKey = undefined;
|
|
405
|
-
this.cachedLines = undefined;
|
|
406
|
-
}
|
|
407
|
-
return lines;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
invalidate(): void {
|
|
411
|
-
this.cacheKey = undefined;
|
|
412
|
-
this.cachedLines = undefined;
|
|
413
|
-
}
|
|
414
|
-
}
|