@rosetears/aili-pi 0.1.0 → 0.1.1
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/README.md +7 -1
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/extensions/header/index.ts +92 -0
- package/extensions/matrix/index.ts +375 -0
- package/extensions/zentui/config.ts +1014 -0
- package/extensions/zentui/extension-status.ts +96 -0
- package/extensions/zentui/fixed-editor/cluster.ts +98 -0
- package/extensions/zentui/fixed-editor/compositor.ts +719 -0
- package/extensions/zentui/fixed-editor/index.ts +223 -0
- package/extensions/zentui/fixed-editor/input.ts +85 -0
- package/extensions/zentui/fixed-editor/pi-compat.ts +296 -0
- package/extensions/zentui/fixed-editor/selection.ts +217 -0
- package/extensions/zentui/fixed-editor/terminal-modes.ts +75 -0
- package/extensions/zentui/fixed-editor/types.ts +37 -0
- package/extensions/zentui/footer-format.ts +279 -0
- package/extensions/zentui/footer.ts +595 -0
- package/extensions/zentui/format.ts +434 -0
- package/extensions/zentui/git.ts +384 -0
- package/extensions/zentui/gradient.ts +70 -0
- package/extensions/zentui/icons.ts +252 -0
- package/extensions/zentui/index.ts +577 -0
- package/extensions/zentui/live-context.ts +75 -0
- package/extensions/zentui/package-version.ts +650 -0
- package/extensions/zentui/project-refresh.ts +104 -0
- package/extensions/zentui/project-state.ts +59 -0
- package/extensions/zentui/prototype-patch-registry.ts +111 -0
- package/extensions/zentui/runtime.ts +841 -0
- package/extensions/zentui/selector-border.ts +77 -0
- package/extensions/zentui/session-lifecycle.ts +60 -0
- package/extensions/zentui/settings-command.ts +897 -0
- package/extensions/zentui/state.ts +55 -0
- package/extensions/zentui/style.ts +332 -0
- package/extensions/zentui/thinking-message.ts +159 -0
- package/extensions/zentui/tool-execution.ts +189 -0
- package/extensions/zentui/ui.ts +618 -0
- package/extensions/zentui/user-message.ts +252 -0
- package/licenses/pi-sakura-cyberdeck-MIT.txt +21 -0
- package/licenses/pi-zentui-MIT.txt +21 -0
- package/manifests/provenance.json +11 -0
- package/manifests/sbom.json +17 -1
- package/notices/pi-sakura-cyberdeck-NOTICE.txt +6 -0
- package/package.json +11 -2
- package/src/runtime/doctor.ts +1 -1
- package/src/runtime/registry.ts +1 -1
- package/src/runtime/rem-head.txt +38 -0
- package/themes/rem-cyberdeck.json +32 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
buildContextGauge,
|
|
4
|
+
buildCostLabel,
|
|
5
|
+
formatCount,
|
|
6
|
+
buildTokenLabel,
|
|
7
|
+
formatProviderLabel,
|
|
8
|
+
getUsageTotals,
|
|
9
|
+
} from "./format.js";
|
|
10
|
+
import type { GitStatusSummary } from "./git.js";
|
|
11
|
+
import type { PackageVersionResult } from "./package-version.js";
|
|
12
|
+
import type { RuntimeInfo } from "./runtime.js";
|
|
13
|
+
|
|
14
|
+
export type FooterState = GitStatusSummary & {
|
|
15
|
+
modelLabel: string;
|
|
16
|
+
providerLabel: string;
|
|
17
|
+
contextLabel: string;
|
|
18
|
+
contextUsedLabel: string;
|
|
19
|
+
tokenLabel: string;
|
|
20
|
+
costLabel: string;
|
|
21
|
+
runtime?: RuntimeInfo;
|
|
22
|
+
packageVersion?: PackageVersionResult;
|
|
23
|
+
sessionStartEpoch?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function createInitialState(gitDefaults: GitStatusSummary): FooterState {
|
|
27
|
+
return {
|
|
28
|
+
modelLabel: "no-model",
|
|
29
|
+
providerLabel: "Unknown",
|
|
30
|
+
contextLabel: "--",
|
|
31
|
+
contextUsedLabel: "--",
|
|
32
|
+
tokenLabel: "↑0 ↓0",
|
|
33
|
+
costLabel: "$0.000",
|
|
34
|
+
runtime: undefined,
|
|
35
|
+
packageVersion: undefined,
|
|
36
|
+
sessionStartEpoch: Date.now(),
|
|
37
|
+
...gitDefaults,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function syncState(state: FooterState, ctx: ExtensionContext, cacheHitIcon: string): void {
|
|
42
|
+
const totals = getUsageTotals(ctx);
|
|
43
|
+
state.modelLabel = ctx.model?.id ?? "no-model";
|
|
44
|
+
state.providerLabel = formatProviderLabel(ctx.model?.provider);
|
|
45
|
+
const usage = ctx.getContextUsage();
|
|
46
|
+
const contextWindow = ctx.model?.contextWindow ?? usage?.contextWindow;
|
|
47
|
+
state.contextLabel = contextWindow && contextWindow > 0
|
|
48
|
+
? `[${buildContextGauge(usage?.percent ?? 0, 18)}]`
|
|
49
|
+
: "--";
|
|
50
|
+
state.contextUsedLabel = contextWindow && contextWindow > 0 && typeof usage?.tokens === "number"
|
|
51
|
+
? `${formatCount(usage.tokens)}/${formatCount(contextWindow)}`
|
|
52
|
+
: "--";
|
|
53
|
+
state.tokenLabel = buildTokenLabel(totals, cacheHitIcon);
|
|
54
|
+
state.costLabel = buildCostLabel(totals);
|
|
55
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ColorSource, ColorSpec } from "./config.js";
|
|
3
|
+
|
|
4
|
+
type ThemeLike = {
|
|
5
|
+
fg(color: string, text: string): string;
|
|
6
|
+
bold?: (text: string) => string;
|
|
7
|
+
italic?: (text: string) => string;
|
|
8
|
+
underline?: (text: string) => string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type { ThemeLike };
|
|
12
|
+
|
|
13
|
+
export const EDITOR_ACCENT_STYLE = "blue";
|
|
14
|
+
export const EDITOR_BORDER_STYLE = "bright-black";
|
|
15
|
+
|
|
16
|
+
export type SourceStyleFallback = {
|
|
17
|
+
theme: ColorSpec;
|
|
18
|
+
terminal: ColorSpec;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const EDITOR_ACCENT_FALLBACK: SourceStyleFallback = {
|
|
22
|
+
theme: "accent",
|
|
23
|
+
terminal: EDITOR_ACCENT_STYLE,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const EDITOR_BORDER_FALLBACK: SourceStyleFallback = {
|
|
27
|
+
theme: "borderMuted",
|
|
28
|
+
terminal: EDITOR_BORDER_STYLE,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function isHexColor(value: string): boolean {
|
|
32
|
+
return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function expandHexColor(hex: string): string {
|
|
36
|
+
const body = hex.slice(1);
|
|
37
|
+
if (body.length === 3) {
|
|
38
|
+
return body
|
|
39
|
+
.split("")
|
|
40
|
+
.map((ch) => ch + ch)
|
|
41
|
+
.join("");
|
|
42
|
+
}
|
|
43
|
+
return body;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hexToAnsi(hex: string, isBackground = false): string {
|
|
47
|
+
const normalized = expandHexColor(hex);
|
|
48
|
+
const r = Number.parseInt(normalized.slice(0, 2), 16);
|
|
49
|
+
const g = Number.parseInt(normalized.slice(2, 4), 16);
|
|
50
|
+
const b = Number.parseInt(normalized.slice(4, 6), 16);
|
|
51
|
+
return `\x1b[${isBackground ? 48 : 38};2;${r};${g};${b}m`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const terminalColorCodes = new Map([
|
|
55
|
+
["black", 30],
|
|
56
|
+
["red", 31],
|
|
57
|
+
["green", 32],
|
|
58
|
+
["yellow", 33],
|
|
59
|
+
["blue", 34],
|
|
60
|
+
["purple", 35],
|
|
61
|
+
["cyan", 36],
|
|
62
|
+
["white", 37],
|
|
63
|
+
["bright-black", 90],
|
|
64
|
+
["bright-red", 91],
|
|
65
|
+
["bright-green", 92],
|
|
66
|
+
["bright-yellow", 93],
|
|
67
|
+
["bright-blue", 94],
|
|
68
|
+
["bright-purple", 95],
|
|
69
|
+
["bright-cyan", 96],
|
|
70
|
+
["bright-white", 97],
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const terminalStyleModifiers = new Map([
|
|
74
|
+
["bold", 1],
|
|
75
|
+
["dim", 2],
|
|
76
|
+
["dimmed", 2],
|
|
77
|
+
["italic", 3],
|
|
78
|
+
["underline", 4],
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const themeColorNameMap = new Map([
|
|
82
|
+
["red", "error"],
|
|
83
|
+
["bright-red", "error"],
|
|
84
|
+
["green", "success"],
|
|
85
|
+
["bright-green", "success"],
|
|
86
|
+
["yellow", "warning"],
|
|
87
|
+
["bright-yellow", "warning"],
|
|
88
|
+
["blue", "syntaxFunction"],
|
|
89
|
+
["bright-blue", "syntaxFunction"],
|
|
90
|
+
["cyan", "syntaxFunction"],
|
|
91
|
+
["bright-cyan", "syntaxFunction"],
|
|
92
|
+
["purple", "syntaxKeyword"],
|
|
93
|
+
["bright-purple", "syntaxKeyword"],
|
|
94
|
+
["black", "muted"],
|
|
95
|
+
["bright-black", "muted"],
|
|
96
|
+
["white", "text"],
|
|
97
|
+
["bright-white", "text"],
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const themeStyleModifiers = new Set(["bold", "italic", "underline"]);
|
|
101
|
+
|
|
102
|
+
const themeColorTokens = new Set<ThemeColor>([
|
|
103
|
+
"accent",
|
|
104
|
+
"border",
|
|
105
|
+
"borderAccent",
|
|
106
|
+
"borderMuted",
|
|
107
|
+
"success",
|
|
108
|
+
"error",
|
|
109
|
+
"warning",
|
|
110
|
+
"muted",
|
|
111
|
+
"dim",
|
|
112
|
+
"text",
|
|
113
|
+
"thinkingText",
|
|
114
|
+
"userMessageText",
|
|
115
|
+
"customMessageText",
|
|
116
|
+
"customMessageLabel",
|
|
117
|
+
"toolTitle",
|
|
118
|
+
"toolOutput",
|
|
119
|
+
"mdHeading",
|
|
120
|
+
"mdLink",
|
|
121
|
+
"mdLinkUrl",
|
|
122
|
+
"mdCode",
|
|
123
|
+
"mdCodeBlock",
|
|
124
|
+
"mdCodeBlockBorder",
|
|
125
|
+
"mdQuote",
|
|
126
|
+
"mdQuoteBorder",
|
|
127
|
+
"mdHr",
|
|
128
|
+
"mdListBullet",
|
|
129
|
+
"toolDiffAdded",
|
|
130
|
+
"toolDiffRemoved",
|
|
131
|
+
"toolDiffContext",
|
|
132
|
+
"syntaxComment",
|
|
133
|
+
"syntaxKeyword",
|
|
134
|
+
"syntaxFunction",
|
|
135
|
+
"syntaxVariable",
|
|
136
|
+
"syntaxString",
|
|
137
|
+
"syntaxNumber",
|
|
138
|
+
"syntaxType",
|
|
139
|
+
"syntaxOperator",
|
|
140
|
+
"syntaxPunctuation",
|
|
141
|
+
"thinkingOff",
|
|
142
|
+
"thinkingMinimal",
|
|
143
|
+
"thinkingLow",
|
|
144
|
+
"thinkingMedium",
|
|
145
|
+
"thinkingHigh",
|
|
146
|
+
"thinkingXhigh",
|
|
147
|
+
"bashMode",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
function terminalColorToAnsi(color: string, isBackground = false): string | undefined {
|
|
151
|
+
const normalized = color.toLowerCase();
|
|
152
|
+
const colorCode = terminalColorCodes.get(normalized);
|
|
153
|
+
if (colorCode !== undefined) return `${isBackground ? colorCode + 10 : colorCode}`;
|
|
154
|
+
|
|
155
|
+
if (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized)) {
|
|
156
|
+
return `${isBackground ? 48 : 38};5;${normalized}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isHexColor(normalized)) return hexToAnsi(normalized, isBackground).slice(2, -1);
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isExplicitTerminalColorToken(token: string): boolean {
|
|
164
|
+
const normalized = token.toLowerCase();
|
|
165
|
+
if (normalized.startsWith("fg:") || normalized.startsWith("bg:")) return true;
|
|
166
|
+
if (isHexColor(normalized)) return true;
|
|
167
|
+
return /^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(normalized);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isSupportedStyleToken(token: string): boolean {
|
|
171
|
+
const normalized = token.toLowerCase();
|
|
172
|
+
if (terminalStyleModifiers.has(normalized)) return true;
|
|
173
|
+
if (terminalColorToAnsi(normalized) !== undefined) return true;
|
|
174
|
+
|
|
175
|
+
const isForeground = normalized.startsWith("fg:");
|
|
176
|
+
const isBackground = normalized.startsWith("bg:");
|
|
177
|
+
if (isForeground || isBackground) {
|
|
178
|
+
return terminalColorToAnsi(normalized.slice(3), isBackground) !== undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return themeColorTokens.has(token as ThemeColor);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function isSupportedColorSpec(style: ColorSpec): boolean {
|
|
185
|
+
const trimmed = style.trim();
|
|
186
|
+
if (trimmed === "") return true;
|
|
187
|
+
return trimmed.split(/\s+/).every(isSupportedStyleToken);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function applyThemeModifiers(theme: ThemeLike, styleTokens: string[], text: string): string {
|
|
191
|
+
let rendered = text;
|
|
192
|
+
for (const token of styleTokens) {
|
|
193
|
+
const normalized = token.toLowerCase();
|
|
194
|
+
if (normalized === "bold") rendered = theme.bold?.(rendered) ?? rendered;
|
|
195
|
+
if (normalized === "italic") rendered = theme.italic?.(rendered) ?? rendered;
|
|
196
|
+
if (normalized === "underline") rendered = theme.underline?.(rendered) ?? rendered;
|
|
197
|
+
}
|
|
198
|
+
return rendered;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function safeThemeFg(theme: ThemeLike, color: string, text: string): string {
|
|
202
|
+
try {
|
|
203
|
+
return theme.fg(color, text);
|
|
204
|
+
} catch {
|
|
205
|
+
return text;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function mapThemeColor(styleTokens: string[]): string | undefined {
|
|
210
|
+
let fallback: string | undefined;
|
|
211
|
+
for (const token of styleTokens) {
|
|
212
|
+
const normalized = token.toLowerCase();
|
|
213
|
+
if (themeStyleModifiers.has(normalized)) continue;
|
|
214
|
+
if (normalized === "dim" || normalized === "dimmed") {
|
|
215
|
+
fallback = "muted";
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const mapped = themeColorNameMap.get(normalized);
|
|
220
|
+
if (mapped) return mapped;
|
|
221
|
+
return token;
|
|
222
|
+
}
|
|
223
|
+
return fallback;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Colorize text using a theme color token or hex color.
|
|
228
|
+
* Non-hex values are passed directly to `theme.fg()`; invalid tokens fall back
|
|
229
|
+
* to unstyled text so a config typo does not break rendering.
|
|
230
|
+
*/
|
|
231
|
+
export function colorize(theme: ThemeLike, color: ColorSpec, text: string): string {
|
|
232
|
+
if (isHexColor(color)) {
|
|
233
|
+
return `${hexToAnsi(color)}${text}\x1b[39m`;
|
|
234
|
+
}
|
|
235
|
+
return safeThemeFg(theme, color, text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Render text with Starship-style terminal styling strings (e.g. "bold red", "fg:202",
|
|
240
|
+
* "bg:blue", "underline bg:#bf5700").
|
|
241
|
+
*/
|
|
242
|
+
export function renderTerminalStyle(style: string, text: string): string {
|
|
243
|
+
const codes: string[] = [];
|
|
244
|
+
for (const token of style.trim().split(/\s+/)) {
|
|
245
|
+
if (!token) continue;
|
|
246
|
+
|
|
247
|
+
const normalized = token.toLowerCase();
|
|
248
|
+
const modifier = terminalStyleModifiers.get(normalized);
|
|
249
|
+
if (modifier !== undefined) {
|
|
250
|
+
codes.push(`${modifier}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const isForeground = normalized.startsWith("fg:");
|
|
255
|
+
const isBackground = normalized.startsWith("bg:");
|
|
256
|
+
const colorName = isForeground || isBackground ? normalized.slice(3) : normalized;
|
|
257
|
+
const color = terminalColorToAnsi(colorName, isBackground);
|
|
258
|
+
if (color) codes.push(color);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return codes.length ? `\x1b[${codes.join(";")}m${text}\x1b[0m` : text;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Apply Starship-style terminal styling first, falling back to Pi theme tokens for
|
|
266
|
+
* legacy config values such as "accent" or "syntaxKeyword".
|
|
267
|
+
*/
|
|
268
|
+
export function renderStyle(theme: ThemeLike, style: ColorSpec, text: string): string {
|
|
269
|
+
if (style.trim() === "") return text;
|
|
270
|
+
const styled = renderTerminalStyle(style, text);
|
|
271
|
+
return styled === text ? colorize(theme, style, text) : styled;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function renderThemeStyle(theme: ThemeLike, style: ColorSpec, text: string): string {
|
|
275
|
+
const trimmed = style.trim();
|
|
276
|
+
if (trimmed === "") return text;
|
|
277
|
+
|
|
278
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
279
|
+
if (tokens.some(isExplicitTerminalColorToken)) return renderTerminalStyle(style, text);
|
|
280
|
+
|
|
281
|
+
const color = mapThemeColor(tokens) ?? "text";
|
|
282
|
+
return safeThemeFg(theme, color, applyThemeModifiers(theme, tokens, text));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function renderStyleForSource(
|
|
286
|
+
theme: ThemeLike,
|
|
287
|
+
source: ColorSource,
|
|
288
|
+
style: ColorSpec,
|
|
289
|
+
text: string,
|
|
290
|
+
): string {
|
|
291
|
+
return source === "terminal"
|
|
292
|
+
? renderStyle(theme, style, text)
|
|
293
|
+
: renderThemeStyle(theme, style, text);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function renderStyleForSourceOrFallback(
|
|
297
|
+
theme: ThemeLike,
|
|
298
|
+
source: ColorSource,
|
|
299
|
+
style: ColorSpec | undefined,
|
|
300
|
+
fallback: ColorSpec | SourceStyleFallback,
|
|
301
|
+
text: string,
|
|
302
|
+
): string {
|
|
303
|
+
const fallbackStyle = typeof fallback === "string" ? fallback : fallback[source];
|
|
304
|
+
return renderStyleForSource(theme, source, style ?? fallbackStyle, text);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function renderEditorAccent(text: string): string {
|
|
308
|
+
return renderTerminalStyle(EDITOR_ACCENT_STYLE, text);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function renderEditorBorder(text: string): string {
|
|
312
|
+
return renderTerminalStyle(EDITOR_BORDER_STYLE, text);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function renderAccentLine(theme: ThemeLike, source: ColorSource, text: string): string {
|
|
316
|
+
return renderStyleForSourceOrFallback(theme, source, undefined, EDITOR_ACCENT_FALLBACK, text);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function renderChromeBorder(
|
|
320
|
+
theme: ThemeLike,
|
|
321
|
+
source: ColorSource,
|
|
322
|
+
terminalFallbackStyle: ColorSpec,
|
|
323
|
+
text: string,
|
|
324
|
+
): string {
|
|
325
|
+
return renderStyleForSourceOrFallback(
|
|
326
|
+
theme,
|
|
327
|
+
source,
|
|
328
|
+
undefined,
|
|
329
|
+
{ theme: "borderMuted", terminal: terminalFallbackStyle },
|
|
330
|
+
text,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { AssistantMessageComponent, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { renderSakuraGradient } from "./gradient.js";
|
|
4
|
+
import { installPrototypePatch } from "./prototype-patch-registry.js";
|
|
5
|
+
|
|
6
|
+
type Cleanup = () => void;
|
|
7
|
+
type AssistantContent = {
|
|
8
|
+
type: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
thinking?: string;
|
|
11
|
+
};
|
|
12
|
+
type AssistantMessageRuntime = {
|
|
13
|
+
contentContainer?: { children?: Component[] };
|
|
14
|
+
hideThinkingBlock?: boolean;
|
|
15
|
+
};
|
|
16
|
+
type AssistantMessageLike = {
|
|
17
|
+
content?: AssistantContent[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAX_REASONING_WIDTH = 96;
|
|
21
|
+
|
|
22
|
+
function isBlankRenderedLine(line: string): boolean {
|
|
23
|
+
return line
|
|
24
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
25
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
26
|
+
.trim().length === 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function removeTrailingPadding(line: string): string {
|
|
30
|
+
return line.replace(
|
|
31
|
+
/ +((?:(?:\x1b\[[0-?]*[ -/]*[@-~])|(?:\x1b\][^\x07]*(?:\x07|\x1b\\)))*)$/,
|
|
32
|
+
"$1",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeOutputPadding(line: string): string {
|
|
37
|
+
let index = 0;
|
|
38
|
+
while (index < line.length && line[index] === "\x1b") {
|
|
39
|
+
if (line[index + 1] === "[") {
|
|
40
|
+
const match = line.slice(index).match(/^\x1b\[[0-?]*[ -/]*[@-~]/);
|
|
41
|
+
if (!match) break;
|
|
42
|
+
index += match[0].length;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line[index + 1] === "]") {
|
|
46
|
+
const bell = line.indexOf("\x07", index + 2);
|
|
47
|
+
const st = line.indexOf("\x1b\\", index + 2);
|
|
48
|
+
const end = bell >= 0 && (st < 0 || bell < st) ? bell + 1 : st >= 0 ? st + 2 : -1;
|
|
49
|
+
if (end < 0) break;
|
|
50
|
+
index = end;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
return line[index] === " " ? `${line.slice(0, index)}${line.slice(index + 1)}` : line;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isVisibleContent(content: AssistantContent): boolean {
|
|
59
|
+
return (
|
|
60
|
+
(content.type === "text" && Boolean(content.text?.trim())) ||
|
|
61
|
+
(content.type === "thinking" && Boolean(content.thinking?.trim()))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Locate the children emitted for thinking runs by Pi's updateContent algorithm. */
|
|
66
|
+
function thinkingChildIndices(message: AssistantMessageLike): number[] {
|
|
67
|
+
const content = message.content ?? [];
|
|
68
|
+
let childIndex = content.some(isVisibleContent) ? 1 : 0; // Initial Spacer
|
|
69
|
+
const result: number[] = [];
|
|
70
|
+
|
|
71
|
+
for (let index = 0; index < content.length; index++) {
|
|
72
|
+
const item = content[index]!;
|
|
73
|
+
if (item.type === "text" && item.text?.trim()) {
|
|
74
|
+
childIndex += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (item.type !== "thinking") continue;
|
|
78
|
+
|
|
79
|
+
let hasThinking = false;
|
|
80
|
+
while (index < content.length && content[index]!.type === "thinking") {
|
|
81
|
+
hasThinking ||= Boolean(content[index]!.thinking?.trim());
|
|
82
|
+
index += 1;
|
|
83
|
+
}
|
|
84
|
+
index -= 1;
|
|
85
|
+
if (!hasThinking) continue;
|
|
86
|
+
|
|
87
|
+
result.push(childIndex);
|
|
88
|
+
childIndex += 1;
|
|
89
|
+
if (content.slice(index + 1).some(isVisibleContent)) childIndex += 1; // Spacer
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class ThinkingTrailComponent implements Component {
|
|
95
|
+
constructor(
|
|
96
|
+
private readonly inner: Component,
|
|
97
|
+
private readonly getTheme: () => Theme | undefined,
|
|
98
|
+
) {}
|
|
99
|
+
|
|
100
|
+
invalidate(): void {
|
|
101
|
+
this.inner.invalidate?.();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render(width: number): string[] {
|
|
105
|
+
if (width < 8) return this.inner.render(width);
|
|
106
|
+
const contentWidth = Math.max(1, Math.min(width - 2, MAX_REASONING_WIDTH));
|
|
107
|
+
const rawLines = this.inner.render(contentWidth);
|
|
108
|
+
const rows: Array<{ line: string; step: boolean }> = [];
|
|
109
|
+
let startsStep = true;
|
|
110
|
+
for (const line of rawLines) {
|
|
111
|
+
if (isBlankRenderedLine(line)) {
|
|
112
|
+
startsStep = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
rows.push({ line: removeOutputPadding(removeTrailingPadding(line)), step: startsStep });
|
|
116
|
+
startsStep = false;
|
|
117
|
+
}
|
|
118
|
+
if (rows.length === 0) return [];
|
|
119
|
+
|
|
120
|
+
const stepCount = rows.filter((row) => row.step).length;
|
|
121
|
+
const theme = this.getTheme();
|
|
122
|
+
if (!theme) return rawLines;
|
|
123
|
+
const count = theme.fg("muted", ` · ${stepCount} ${stepCount === 1 ? "STEP" : "STEPS"}`);
|
|
124
|
+
const title = `${renderSakuraGradient("✦ REASONING")}${count}`;
|
|
125
|
+
let currentStep = 0;
|
|
126
|
+
const body = rows.map(({ line, step }) => {
|
|
127
|
+
if (step) currentStep += 1;
|
|
128
|
+
const isLastStep = currentStep === stepCount;
|
|
129
|
+
const branchText = step ? (isLastStep ? "╰─ " : "├─ ") : isLastStep ? " " : "│ ";
|
|
130
|
+
const branch = theme.fg("thinkingXhigh", branchText);
|
|
131
|
+
const marker = step ? renderSakuraGradient("◇ ") : " ";
|
|
132
|
+
return truncateToWidth(`${branch}${marker}${line}`, width, "");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return [title, ...body];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Decorate only thinking runs; normal assistant Markdown remains untouched. */
|
|
140
|
+
export function installThinkingMessageStyle(getTheme: () => Theme | undefined): Cleanup {
|
|
141
|
+
return installPrototypePatch(
|
|
142
|
+
AssistantMessageComponent.prototype,
|
|
143
|
+
"updateContent",
|
|
144
|
+
"assistant-thinking-content",
|
|
145
|
+
({ predecessor, receiver, args }) => {
|
|
146
|
+
const result = Reflect.apply(predecessor, receiver, args);
|
|
147
|
+
const runtime = receiver as AssistantMessageRuntime;
|
|
148
|
+
const children = runtime.contentContainer?.children;
|
|
149
|
+
const message = args[0] as AssistantMessageLike | undefined;
|
|
150
|
+
if (!children || !message) return result;
|
|
151
|
+
|
|
152
|
+
for (const index of thinkingChildIndices(message)) {
|
|
153
|
+
const child = children[index];
|
|
154
|
+
if (child) children[index] = new ThinkingTrailComponent(child, getTheme);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
}
|