@nmzpy/pi-ember-stack 0.1.6 → 0.2.2
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 +83 -83
- package/package.json +62 -48
- package/plugins/devin-auth/extensions/index.ts +68 -7
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +35 -192
- package/plugins/pi-compact-tools/renderer.ts +589 -0
- package/plugins/pi-custom-agents/index.ts +727 -373
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +975 -0
- package/plugins/pi-ember-fff/query.ts +247 -0
- package/plugins/pi-ember-fff/test/query.test.ts +222 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
- package/plugins/pi-ember-tps/index.ts +144 -0
- package/plugins/pi-ember-ui/ember.json +99 -0
- package/plugins/pi-ember-ui/index.ts +805 -0
- package/plugins/pi-ember-ui/mode-colors.ts +196 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
AssistantMessageComponent,
|
|
6
|
+
Theme,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Editor, Markdown, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
import {
|
|
11
|
+
buildCodeBgHex,
|
|
12
|
+
buildThemeBgColors,
|
|
13
|
+
buildThemeFgColors,
|
|
14
|
+
getActiveModeColor,
|
|
15
|
+
isShellMode,
|
|
16
|
+
MUTED_COLOR,
|
|
17
|
+
setLatestSubagentRunning,
|
|
18
|
+
setShellMode,
|
|
19
|
+
} from "./mode-colors.ts";
|
|
20
|
+
|
|
21
|
+
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const THEME_JSON = path.join(SOURCE_ROOT, "ember.json");
|
|
23
|
+
const THEME_NAME = "ember";
|
|
24
|
+
|
|
25
|
+
const THINKING_FRAME_INTERVAL_MS = 33;
|
|
26
|
+
const LOGO = [
|
|
27
|
+
" ██████ ██",
|
|
28
|
+
" ██ ██ ██",
|
|
29
|
+
" ██████ ██",
|
|
30
|
+
" ██ ██",
|
|
31
|
+
" ██ ██",
|
|
32
|
+
" ██ ██",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const SHADOW_OFFSET_X = 1;
|
|
36
|
+
const SHADOW_OFFSET_Y = 0;
|
|
37
|
+
const SHADOW_GLYPH = "\u2591";
|
|
38
|
+
const SHADOW_OPACITY = 0.4;
|
|
39
|
+
|
|
40
|
+
let thinkingActive = false;
|
|
41
|
+
let workingActive = false;
|
|
42
|
+
let thinkingFrame = 0;
|
|
43
|
+
let thinkingInterval: ReturnType<typeof setInterval> | undefined;
|
|
44
|
+
let logoAnimating = false;
|
|
45
|
+
let logoFrame = 0;
|
|
46
|
+
let logoTimer: ReturnType<typeof setInterval> | undefined;
|
|
47
|
+
let editorRenderPatched = false;
|
|
48
|
+
let assistantMessagePatched = false;
|
|
49
|
+
let requestRender: (() => void) | undefined;
|
|
50
|
+
let sessionCtx: any;
|
|
51
|
+
|
|
52
|
+
type EditorWithBorder = Editor & {
|
|
53
|
+
borderColor: (text: string) => string;
|
|
54
|
+
getText: () => string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Cached result of the subagent-running scan. The scan itself is O(n)
|
|
59
|
+
* over the session branch (getBranch + two passes), so it MUST NOT run
|
|
60
|
+
* inside the per-frame Editor.prototype.render path. It is recomputed
|
|
61
|
+
* only on tool_execution_start / tool_execution_end (which fire once
|
|
62
|
+
* per tool call) and reset on session replacement. The render path
|
|
63
|
+
* reads this cached flag via subagentRunningCached.
|
|
64
|
+
*/
|
|
65
|
+
let subagentRunningCached = false;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Recompute whether the latest tool call in the session is a `subagent`
|
|
69
|
+
* that has not yet produced a toolResult. Scans session entries
|
|
70
|
+
* (available via module-level sessionCtx) and writes the result to both
|
|
71
|
+
* the shared mode-colors flag and the local cache. Called only from
|
|
72
|
+
* tool-execution event handlers — never from the render path.
|
|
73
|
+
*/
|
|
74
|
+
function recompute_latest_subagent_running(): boolean {
|
|
75
|
+
const entries = sessionCtx?.sessionManager?.getBranch?.() ?? [];
|
|
76
|
+
let latestSubagentCallId: string | undefined;
|
|
77
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
78
|
+
const entry = entries[i];
|
|
79
|
+
if (entry?.type !== "message") continue;
|
|
80
|
+
const msg = entry.message;
|
|
81
|
+
if (msg?.role !== "assistant") continue;
|
|
82
|
+
for (const part of msg.content ?? []) {
|
|
83
|
+
if (part?.type === "toolCall" && part?.name === "subagent") {
|
|
84
|
+
latestSubagentCallId = part.id;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (latestSubagentCallId) break;
|
|
89
|
+
}
|
|
90
|
+
let running = false;
|
|
91
|
+
if (latestSubagentCallId) {
|
|
92
|
+
running = true;
|
|
93
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
94
|
+
const entry = entries[i];
|
|
95
|
+
if (entry?.type !== "message") continue;
|
|
96
|
+
const msg = entry.message;
|
|
97
|
+
if (msg?.role === "toolResult" && msg?.toolCallId === latestSubagentCallId) {
|
|
98
|
+
running = false;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
subagentRunningCached = running;
|
|
104
|
+
setLatestSubagentRunning(running);
|
|
105
|
+
return running;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function currentLabelText(): string {
|
|
109
|
+
return thinkingActive ? "Thinking" : "Working";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function stopThinkingAnimation(): void {
|
|
113
|
+
thinkingActive = false;
|
|
114
|
+
if (!workingActive) {
|
|
115
|
+
if (thinkingInterval) clearInterval(thinkingInterval);
|
|
116
|
+
thinkingInterval = undefined;
|
|
117
|
+
}
|
|
118
|
+
requestRender?.();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function startThinkingAnimation(): void {
|
|
122
|
+
thinkingActive = true;
|
|
123
|
+
if (!thinkingInterval) {
|
|
124
|
+
thinkingFrame = 0;
|
|
125
|
+
thinkingInterval = setInterval(() => {
|
|
126
|
+
thinkingFrame += 0.09;
|
|
127
|
+
if (thinkingFrame > 1) thinkingFrame -= 1;
|
|
128
|
+
requestRender?.();
|
|
129
|
+
}, THINKING_FRAME_INTERVAL_MS);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function startWorkingAnimation(): void {
|
|
134
|
+
workingActive = true;
|
|
135
|
+
if (!thinkingInterval) {
|
|
136
|
+
thinkingFrame = 0;
|
|
137
|
+
thinkingInterval = setInterval(() => {
|
|
138
|
+
thinkingFrame += 0.09;
|
|
139
|
+
if (thinkingFrame > 1) thinkingFrame -= 1;
|
|
140
|
+
requestRender?.();
|
|
141
|
+
}, THINKING_FRAME_INTERVAL_MS);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function stopWorkingAnimation(): void {
|
|
146
|
+
workingActive = false;
|
|
147
|
+
if (!thinkingActive && thinkingInterval) {
|
|
148
|
+
clearInterval(thinkingInterval);
|
|
149
|
+
thinkingInterval = undefined;
|
|
150
|
+
}
|
|
151
|
+
requestRender?.();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function wrapThemeWithCodeBg(base: Theme): Theme {
|
|
155
|
+
return new Proxy(base, {
|
|
156
|
+
get(target: Theme, prop: string | symbol, receiver: any) {
|
|
157
|
+
if (prop === "fg") {
|
|
158
|
+
return (color: string, text: string) => {
|
|
159
|
+
if (color === "mdCode") {
|
|
160
|
+
return (
|
|
161
|
+
liveCodeBgAnsi +
|
|
162
|
+
target.getFgAnsi("mdCode" as any) +
|
|
163
|
+
" " + text + " " +
|
|
164
|
+
"\x1b[39m\x1b[49m"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return target.fg(color as any, text);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const val = Reflect.get(target, prop, receiver);
|
|
171
|
+
return typeof val === "function" ? val.bind(target) : val;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function fgAnsi(hex: string): string {
|
|
177
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
178
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function bgAnsi(hex: string): string {
|
|
182
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
183
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");
|
|
187
|
+
const THEME_KEY_OLD = Symbol.for("@mariozechner/pi-coding-agent:theme");
|
|
188
|
+
|
|
189
|
+
let liveTheme: Theme | undefined;
|
|
190
|
+
let liveCodeBgAnsi = "";
|
|
191
|
+
|
|
192
|
+
function installProxiedTheme(fgColors: Record<string, string>, bgColors: Record<string, string>, codeBg: string): void {
|
|
193
|
+
const base = new Theme(
|
|
194
|
+
fgColors as any,
|
|
195
|
+
bgColors as any,
|
|
196
|
+
"truecolor",
|
|
197
|
+
{ name: "ember" },
|
|
198
|
+
);
|
|
199
|
+
liveTheme = base;
|
|
200
|
+
liveCodeBgAnsi = bgAnsi(codeBg);
|
|
201
|
+
const wrapped = wrapThemeWithCodeBg(base);
|
|
202
|
+
(globalThis as any)[THEME_KEY] = wrapped;
|
|
203
|
+
(globalThis as any)[THEME_KEY_OLD] = wrapped;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function applyDynamicTheme(options: { invalidate?: boolean; render?: boolean } = {}): void {
|
|
207
|
+
const accent = getActiveModeColor();
|
|
208
|
+
const fgColors = buildThemeFgColors(accent);
|
|
209
|
+
const bgColors = buildThemeBgColors(accent);
|
|
210
|
+
const codeBg = buildCodeBgHex(accent);
|
|
211
|
+
|
|
212
|
+
if (liveTheme) {
|
|
213
|
+
liveCodeBgAnsi = bgAnsi(codeBg);
|
|
214
|
+
updateLiveThemeColors(fgColors, bgColors);
|
|
215
|
+
if (options.invalidate !== false) (tuiRef as any)?.invalidate();
|
|
216
|
+
if (options.render !== false) requestRender?.();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
installProxiedTheme(fgColors, bgColors, codeBg);
|
|
220
|
+
if (options.render !== false) requestRender?.();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateLiveThemeColors(fgColors: Record<string, string>, bgColors: Record<string, string>): void {
|
|
224
|
+
if (!liveTheme) return;
|
|
225
|
+
const fgMap = (liveTheme as any).fgColors as Map<string, string>;
|
|
226
|
+
const bgMap = (liveTheme as any).bgColors as Map<string, string>;
|
|
227
|
+
for (const [key, hex] of Object.entries(fgColors)) {
|
|
228
|
+
fgMap.set(key, fgAnsi(hex));
|
|
229
|
+
}
|
|
230
|
+
for (const [key, hex] of Object.entries(bgColors)) {
|
|
231
|
+
bgMap.set(key, bgAnsi(hex));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderGradientLabel(text: string, accent: string, phaseOffset = 0): string {
|
|
236
|
+
const chars = [...text];
|
|
237
|
+
const len = chars.length;
|
|
238
|
+
if (len === 0) return "";
|
|
239
|
+
const dimHex = blendToHex(accent, "#18181e", 0.4);
|
|
240
|
+
const result: string[] = [];
|
|
241
|
+
const phase = (thinkingFrame + phaseOffset) % 1;
|
|
242
|
+
for (let i = 0; i < len; i++) {
|
|
243
|
+
const charPos = i / Math.max(1, len - 1);
|
|
244
|
+
const dist = charPos - phase;
|
|
245
|
+
const wrapped = dist < -0.5 ? dist + 1 : dist > 0.5 ? dist - 1 : dist;
|
|
246
|
+
const intensity = Math.exp(-(wrapped * wrapped) * 8);
|
|
247
|
+
const hex = blendToHex(dimHex, accent, intensity);
|
|
248
|
+
result.push(colorize(chars[i], hex));
|
|
249
|
+
}
|
|
250
|
+
return result.join("");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Render the same animated gradient used by the live Thinking label. */
|
|
254
|
+
export function renderLiveThinkingGradient(text: string): string {
|
|
255
|
+
return renderGradientLabel(text, getActiveModeColor());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Render an animated gradient with a stable per-row phase offset. */
|
|
259
|
+
export { renderGradientLabel };
|
|
260
|
+
|
|
261
|
+
function installThinkingBorderOverride(): void {
|
|
262
|
+
if (editorRenderPatched) return;
|
|
263
|
+
editorRenderPatched = true;
|
|
264
|
+
const originalRender = Editor.prototype.render;
|
|
265
|
+
Editor.prototype.render = function renderThinkingBorder(this: EditorWithBorder, width: number): string[] {
|
|
266
|
+
const accent = getActiveModeColor();
|
|
267
|
+
const accentLight = lightenHex(accent, 0.5);
|
|
268
|
+
const borderColor = isShellMode() ? MUTED_COLOR : accentLight;
|
|
269
|
+
const accentBorder = (text: string): string => colorize(text, borderColor);
|
|
270
|
+
const dimInsetBorder = (): string =>
|
|
271
|
+
` ${colorWithOpacity("\u2500".repeat(Math.max(0, width - 2)), borderColor, 0.1875)} `;
|
|
272
|
+
const originalBorderColor = this.borderColor;
|
|
273
|
+
this.borderColor = accentBorder;
|
|
274
|
+
const innerWidth = Math.max(1, width - 2);
|
|
275
|
+
const lines = originalRender.call(this, innerWidth);
|
|
276
|
+
this.borderColor = originalBorderColor;
|
|
277
|
+
|
|
278
|
+
const stripped = (s: string): string => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
279
|
+
const isBorderLine = (s: string): boolean => {
|
|
280
|
+
const raw = stripped(s);
|
|
281
|
+
return raw.length > 0 && [...raw].every((ch) => ch === "\u2500" || ch === " ");
|
|
282
|
+
};
|
|
283
|
+
const borderIndices: number[] = [];
|
|
284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
285
|
+
if (isBorderLine(lines[i])) borderIndices.push(i);
|
|
286
|
+
}
|
|
287
|
+
const topIdx = borderIndices[0] ?? 0;
|
|
288
|
+
const bottomBorderIdx = borderIndices.length > 1 ? borderIndices[borderIndices.length - 1] : -1;
|
|
289
|
+
|
|
290
|
+
for (let i = 1; i < lines.length; i++) {
|
|
291
|
+
if (i === bottomBorderIdx) continue;
|
|
292
|
+
lines[i] = ` ${lines[i]} `;
|
|
293
|
+
}
|
|
294
|
+
const subagentActive = subagentRunningCached;
|
|
295
|
+
if (subagentActive) {
|
|
296
|
+
lines[topIdx] = dimInsetBorder();
|
|
297
|
+
} else {
|
|
298
|
+
lines[topIdx] = accentBorder("\u2500".repeat(width));
|
|
299
|
+
}
|
|
300
|
+
if (bottomBorderIdx >= 0) {
|
|
301
|
+
const inputText = this.getText?.() ?? "";
|
|
302
|
+
if (inputText.trimStart().startsWith("/")) {
|
|
303
|
+
lines[bottomBorderIdx] = dimInsetBorder();
|
|
304
|
+
} else {
|
|
305
|
+
lines[bottomBorderIdx] = accentBorder("\u2500".repeat(width));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const lastLineIdx = lines.length - 1;
|
|
309
|
+
if (lastLineIdx > bottomBorderIdx && lastLineIdx > 0) {
|
|
310
|
+
lines[lastLineIdx] = accentBorder("\u2500".repeat(width));
|
|
311
|
+
}
|
|
312
|
+
if (lines.length === 0) return lines;
|
|
313
|
+
if (subagentActive) return lines;
|
|
314
|
+
if (!thinkingActive && !workingActive) return lines;
|
|
315
|
+
|
|
316
|
+
const labelText = thinkingActive ? "Thinking" : "Working";
|
|
317
|
+
const label = ` ${renderGradientLabel(labelText, accent)} `;
|
|
318
|
+
const prefixLen = 2 + visibleWidth(label);
|
|
319
|
+
const remaining = Math.max(0, width - prefixLen);
|
|
320
|
+
lines[topIdx] = accentBorder("\u2500".repeat(2)) + label +
|
|
321
|
+
accentBorder("\u2500".repeat(remaining));
|
|
322
|
+
return lines;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function installAssistantMessagePatch(): void {
|
|
327
|
+
if (assistantMessagePatched) return;
|
|
328
|
+
assistantMessagePatched = true;
|
|
329
|
+
|
|
330
|
+
(AssistantMessageComponent as any).prototype.updateContent = function (this: any, message: any): void {
|
|
331
|
+
const hide = this.hideThinkingBlock;
|
|
332
|
+
const outputPad = this.outputPad;
|
|
333
|
+
// Skip the full rebuild when nothing that affects the rendered output
|
|
334
|
+
// has changed. invalidate() (from theme change, thinking toggle,
|
|
335
|
+
// output-pad change) calls updateContent with the SAME message —
|
|
336
|
+
// recreating every Markdown child and clearing contentContainer is
|
|
337
|
+
// O(blocks) per assistant message per rebuild, which freezes long
|
|
338
|
+
// transcripts on thinking-toggle. The child Markdown caches were
|
|
339
|
+
// already cleared by the invalidate() propagation, so the next
|
|
340
|
+
// render() will re-parse regardless — but we avoid the object
|
|
341
|
+
// churn and container rebuild.
|
|
342
|
+
const sameMessage = this._emberContentMessage === message;
|
|
343
|
+
const cacheKey = `${sameMessage ? "same" : "diff"}|${hide}|${outputPad}`;
|
|
344
|
+
if (sameMessage && this._emberContentKey === cacheKey) {
|
|
345
|
+
this.lastMessage = message;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this._emberContentKey = cacheKey;
|
|
349
|
+
this._emberContentMessage = message;
|
|
350
|
+
this.lastMessage = message;
|
|
351
|
+
|
|
352
|
+
this.contentContainer.clear();
|
|
353
|
+
|
|
354
|
+
const isVisibleBlock = (c: any): boolean => {
|
|
355
|
+
if (c.type === "text" && c.text?.trim()) return true;
|
|
356
|
+
if (c.type === "thinking" && c.thinking?.trim() && !hide) return true;
|
|
357
|
+
return false;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const hasVisibleContent = message.content.some(isVisibleBlock);
|
|
361
|
+
if (hasVisibleContent) {
|
|
362
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const theme = liveTheme ?? (globalThis as any)[THEME_KEY];
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
368
|
+
const content = message.content[i];
|
|
369
|
+
if (content.type === "text" && content.text?.trim()) {
|
|
370
|
+
this.contentContainer.addChild(
|
|
371
|
+
new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme),
|
|
372
|
+
);
|
|
373
|
+
} else if (content.type === "thinking" && content.thinking?.trim()) {
|
|
374
|
+
if (hide) continue;
|
|
375
|
+
const hasVisibleContentAfter = message.content.slice(i + 1).some(isVisibleBlock);
|
|
376
|
+
this.contentContainer.addChild(
|
|
377
|
+
new Markdown(
|
|
378
|
+
content.thinking.trim(),
|
|
379
|
+
this.outputPad,
|
|
380
|
+
0,
|
|
381
|
+
this.markdownTheme,
|
|
382
|
+
{
|
|
383
|
+
color: (text: string) => theme.fg("thinkingText", text),
|
|
384
|
+
italic: true,
|
|
385
|
+
},
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
if (hasVisibleContentAfter) {
|
|
389
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const hasToolCalls = message.content.some((c: any) => c.type === "toolCall");
|
|
395
|
+
this.hasToolCalls = hasToolCalls;
|
|
396
|
+
if (message.stopReason === "length") {
|
|
397
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
398
|
+
this.contentContainer.addChild(
|
|
399
|
+
new Text(
|
|
400
|
+
theme.fg(
|
|
401
|
+
"error",
|
|
402
|
+
"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
|
|
403
|
+
),
|
|
404
|
+
this.outputPad,
|
|
405
|
+
0,
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
} else if (!hasToolCalls) {
|
|
409
|
+
if (message.stopReason === "aborted") {
|
|
410
|
+
const abortMessage =
|
|
411
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
412
|
+
? message.errorMessage
|
|
413
|
+
: "Operation aborted";
|
|
414
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
415
|
+
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), this.outputPad, 0));
|
|
416
|
+
} else if (message.stopReason === "error") {
|
|
417
|
+
const errorMsg = message.errorMessage || "Unknown error";
|
|
418
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
419
|
+
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), this.outputPad, 0));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function colorWithOpacity(text: string, hex: string, opacity: number): string {
|
|
426
|
+
const source = hex.slice(1);
|
|
427
|
+
const base = [24, 24, 30];
|
|
428
|
+
const rgb = [
|
|
429
|
+
parseInt(source.slice(0, 2), 16),
|
|
430
|
+
parseInt(source.slice(2, 4), 16),
|
|
431
|
+
parseInt(source.slice(4, 6), 16),
|
|
432
|
+
].map((value, index) => Math.round(base[index] + (value - base[index]) * opacity));
|
|
433
|
+
return `\u001b[38;2;${rgb.join(";")}m${text}\u001b[39m`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function hexToRgbTriplet(hex: string): [number, number, number] {
|
|
437
|
+
return [
|
|
438
|
+
parseInt(hex.slice(1, 3), 16),
|
|
439
|
+
parseInt(hex.slice(3, 5), 16),
|
|
440
|
+
parseInt(hex.slice(5, 7), 16),
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function blendToHex(fgHex: string, bgHex: string, opacity: number): string {
|
|
445
|
+
const [fr, fg, fb] = hexToRgbTriplet(fgHex);
|
|
446
|
+
const [br, bg, bb] = hexToRgbTriplet(bgHex);
|
|
447
|
+
const r = Math.round(br + (fr - br) * opacity);
|
|
448
|
+
const g = Math.round(bg + (fg - bg) * opacity);
|
|
449
|
+
const b = Math.round(bb + (fb - bb) * opacity);
|
|
450
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function lightenHex(hex: string, amount: number): string {
|
|
454
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
455
|
+
const lr = Math.round(r + (255 - r) * amount);
|
|
456
|
+
const lg = Math.round(g + (255 - g) * amount);
|
|
457
|
+
const lb = Math.round(b + (255 - b) * amount);
|
|
458
|
+
return `#${lr.toString(16).padStart(2, "0")}${lg.toString(16).padStart(2, "0")}${lb.toString(16).padStart(2, "0")}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function colorize(text: string, hex: string): string {
|
|
462
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
463
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function folderNameFromCwd(cwd: string): string {
|
|
467
|
+
return cwd.split(/[/\\]/).filter(Boolean).pop() ?? cwd;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function welcomeConfigPath(): string {
|
|
471
|
+
return path.join(
|
|
472
|
+
process.env.PI_HOME || path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent"),
|
|
473
|
+
"welcome.json",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function setWelcomeUpdates(enabled: boolean): void {
|
|
478
|
+
const file = welcomeConfigPath();
|
|
479
|
+
let config: Record<string, unknown> = {};
|
|
480
|
+
try {
|
|
481
|
+
config = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
482
|
+
} catch { /* first-run, no file yet */ }
|
|
483
|
+
config.updates = enabled;
|
|
484
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
485
|
+
fs.writeFileSync(file, `${JSON.stringify(config, null, "\t")}\n`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
type RadialPoint = { x: number; y: number; r: number; g: number; b: number; falloff: number };
|
|
489
|
+
|
|
490
|
+
function lerp(a: number, b: number, t: number): number {
|
|
491
|
+
return a + (b - a) * t;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function radialColorForCell(x: number, y: number, points: RadialPoint[]): [number, number, number] {
|
|
495
|
+
let totalWeight = 0;
|
|
496
|
+
let r = 0, g = 0, b = 0;
|
|
497
|
+
for (const p of points) {
|
|
498
|
+
const dx = x - p.x;
|
|
499
|
+
const dy = y - p.y;
|
|
500
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
501
|
+
const weight = Math.exp(-(dist * dist) / (p.falloff * p.falloff));
|
|
502
|
+
r += p.r * weight;
|
|
503
|
+
g += p.g * weight;
|
|
504
|
+
b += p.b * weight;
|
|
505
|
+
totalWeight += weight;
|
|
506
|
+
}
|
|
507
|
+
return [
|
|
508
|
+
Math.round(r / totalWeight),
|
|
509
|
+
Math.round(g / totalWeight),
|
|
510
|
+
Math.round(b / totalWeight),
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function startLogoAnimation(): void {
|
|
515
|
+
if (logoTimer) return;
|
|
516
|
+
logoAnimating = true;
|
|
517
|
+
logoFrame = 0;
|
|
518
|
+
logoTimer = setInterval(() => {
|
|
519
|
+
logoFrame += 0.04;
|
|
520
|
+
if (logoFrame > 1) logoFrame -= 1;
|
|
521
|
+
requestRender?.();
|
|
522
|
+
}, 60);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function stopLogoAnimation(): void {
|
|
526
|
+
logoAnimating = false;
|
|
527
|
+
if (logoTimer) {
|
|
528
|
+
clearInterval(logoTimer);
|
|
529
|
+
logoTimer = undefined;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function renderLogoWithGradient(accent: string): string[] {
|
|
534
|
+
const [ar, ag, ab] = hexToRgbTriplet(accent);
|
|
535
|
+
const accent70 = blendToHex(accent, "#18181e", 0.7);
|
|
536
|
+
const [s7r, s7g, s7b] = hexToRgbTriplet(accent70);
|
|
537
|
+
const accent40 = blendToHex(accent, "#18181e", 0.4);
|
|
538
|
+
const [s4r, s4g, s4b] = hexToRgbTriplet(accent40);
|
|
539
|
+
const points: RadialPoint[] = [
|
|
540
|
+
{ x: 2, y: 0, r: 255, g: 255, b: 255, falloff: 3 },
|
|
541
|
+
{ x: 10, y: 1, r: ar, g: ag, b: ab, falloff: 4 },
|
|
542
|
+
{ x: 5, y: 3, r: s7r, g: s7g, b: s7b, falloff: 3.5 },
|
|
543
|
+
{ x: 11, y: 5, r: s4r, g: s4g, b: s4b, falloff: 2.5 },
|
|
544
|
+
{ x: 0, y: 4, r: 200, g: 180, b: 140, falloff: 2 },
|
|
545
|
+
];
|
|
546
|
+
|
|
547
|
+
const logoRows = LOGO.length;
|
|
548
|
+
const logoCols = LOGO[0].length;
|
|
549
|
+
const gridCols = logoCols + SHADOW_OFFSET_X;
|
|
550
|
+
const gridRows = logoRows + Math.ceil(SHADOW_OFFSET_Y);
|
|
551
|
+
|
|
552
|
+
const grid: Array<Array<{ ch: string; rgb?: [number, number, number] }>> = [];
|
|
553
|
+
for (let row = 0; row < gridRows; row++) {
|
|
554
|
+
grid.push(new Array(gridCols).fill(null).map(() => ({ ch: " " })));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
for (let row = 0; row < logoRows; row++) {
|
|
558
|
+
for (let col = 0; col < LOGO[row].length; col++) {
|
|
559
|
+
const ch = LOGO[row][col];
|
|
560
|
+
if (ch === "\u2588") {
|
|
561
|
+
let [r, g, b] = radialColorForCell(col, row, points);
|
|
562
|
+
if (logoAnimating) {
|
|
563
|
+
const charPos = col / Math.max(1, logoCols - 1);
|
|
564
|
+
const dist = charPos - logoFrame;
|
|
565
|
+
const wrapped = dist < -0.5 ? dist + 1 : dist > 0.5 ? dist - 1 : dist;
|
|
566
|
+
const intensity = Math.exp(-(wrapped * wrapped) * 6);
|
|
567
|
+
r = Math.round(r + (255 - r) * intensity);
|
|
568
|
+
g = Math.round(g + (255 - g) * intensity);
|
|
569
|
+
b = Math.round(b + (255 - b) * intensity);
|
|
570
|
+
}
|
|
571
|
+
grid[row][col] = { ch: "\u2588", rgb: [r, g, b] };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const halfOpacity = SHADOW_OPACITY / 2;
|
|
577
|
+
const placeShadow = (sRow: number, sCol: number, opacity: number): void => {
|
|
578
|
+
if (sRow < 0 || sRow >= gridRows || sCol < 0 || sCol >= gridCols) return;
|
|
579
|
+
const existing = grid[sRow][sCol];
|
|
580
|
+
if (existing.ch !== " ") return;
|
|
581
|
+
const [gr, gg, gb] = radialColorForCell(sCol, sRow, points);
|
|
582
|
+
const bgR = 24, bgG = 24, bgB = 30;
|
|
583
|
+
const sr = Math.round(bgR + (gr - bgR) * opacity);
|
|
584
|
+
const sg = Math.round(bgG + (gg - bgG) * opacity);
|
|
585
|
+
const sb = Math.round(bgB + (gb - bgB) * opacity);
|
|
586
|
+
grid[sRow][sCol] = { ch: SHADOW_GLYPH, rgb: [sr, sg, sb] };
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
for (let row = 0; row < logoRows; row++) {
|
|
590
|
+
for (let col = 0; col < LOGO[row].length; col++) {
|
|
591
|
+
const ch = LOGO[row][col];
|
|
592
|
+
if (ch === "\u2588") {
|
|
593
|
+
const sCol = col + SHADOW_OFFSET_X;
|
|
594
|
+
const sRowBase = row + Math.floor(SHADOW_OFFSET_Y);
|
|
595
|
+
placeShadow(sRowBase, sCol, halfOpacity);
|
|
596
|
+
placeShadow(sRowBase + 1, sCol, halfOpacity);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return grid.map((rowCells) =>
|
|
602
|
+
rowCells.map((cell) => {
|
|
603
|
+
if (cell.rgb) {
|
|
604
|
+
const [r, g, b] = cell.rgb;
|
|
605
|
+
return `\x1b[38;2;${r};${g};${b}m${cell.ch}\x1b[39m`;
|
|
606
|
+
}
|
|
607
|
+
return cell.ch;
|
|
608
|
+
}).join("")
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
let tuiRef: any;
|
|
613
|
+
|
|
614
|
+
function installStartupHeader(ctx: any): void {
|
|
615
|
+
if (ctx.mode !== "tui") return;
|
|
616
|
+
|
|
617
|
+
ctx.ui.setHeader((tui: any, theme: any) => {
|
|
618
|
+
tuiRef = tui;
|
|
619
|
+
return {
|
|
620
|
+
render(width: number): string[] {
|
|
621
|
+
// Re-read every render so model/dir/mode changes are reflected.
|
|
622
|
+
const dir = folderNameFromCwd(ctx.sessionManager?.getCwd?.() ?? ctx.cwd ?? process.cwd());
|
|
623
|
+
const model = ctx.model;
|
|
624
|
+
const modelName = model?.name ?? model?.id ?? "no model";
|
|
625
|
+
|
|
626
|
+
const accent = getActiveModeColor();
|
|
627
|
+
const logoLines = renderLogoWithGradient(accent);
|
|
628
|
+
const logoWidth = visibleWidth(logoLines[0] ?? "");
|
|
629
|
+
const leftPad = Math.max(0, Math.floor((width - logoWidth) / 2));
|
|
630
|
+
const padStr = " ".repeat(leftPad);
|
|
631
|
+
|
|
632
|
+
const infoLine = `${theme.fg("text", modelName)} ${theme.fg("accent", "\u2022")} ${theme.fg("dim", dir)}`;
|
|
633
|
+
const infoPad = Math.max(0, Math.floor((width - visibleWidth(infoLine)) / 2));
|
|
634
|
+
const infoPadStr = " ".repeat(infoPad);
|
|
635
|
+
|
|
636
|
+
const lines = [
|
|
637
|
+
...logoLines.map((line) => padStr + line),
|
|
638
|
+
"",
|
|
639
|
+
infoPadStr + infoLine,
|
|
640
|
+
];
|
|
641
|
+
return lines.map((line) => visibleWidth(line) > width ? truncateToWidth(line, width) : line);
|
|
642
|
+
},
|
|
643
|
+
invalidate() {},
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function getAgentDir(): string {
|
|
649
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
650
|
+
return path.join(home, ".pi", "agent");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function getThemesDir(): string {
|
|
654
|
+
return path.join(getAgentDir(), "themes");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function ensureThemeInstalled(): void {
|
|
658
|
+
const themesDir = getThemesDir();
|
|
659
|
+
fs.mkdirSync(themesDir, { recursive: true });
|
|
660
|
+
const dest = path.join(themesDir, `${THEME_NAME}.json`);
|
|
661
|
+
if (!fs.existsSync(dest)) {
|
|
662
|
+
fs.copyFileSync(THEME_JSON, dest);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const srcContent = fs.readFileSync(THEME_JSON, "utf-8");
|
|
666
|
+
const destContent = fs.readFileSync(dest, "utf-8");
|
|
667
|
+
if (srcContent !== destContent) {
|
|
668
|
+
fs.copyFileSync(THEME_JSON, dest);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
673
|
+
ensureThemeInstalled();
|
|
674
|
+
installThinkingBorderOverride();
|
|
675
|
+
installAssistantMessagePatch();
|
|
676
|
+
applyDynamicTheme();
|
|
677
|
+
|
|
678
|
+
pi.on("session_start", (event, ctx) => {
|
|
679
|
+
sessionCtx = ctx;
|
|
680
|
+
liveTheme = undefined;
|
|
681
|
+
subagentRunningCached = false;
|
|
682
|
+
if (ctx.mode === "tui") {
|
|
683
|
+
requestRender = () => ctx.ui.setStatus("pi-ember-ui-thinking-tick", undefined);
|
|
684
|
+
ctx.ui.setWorkingVisible(false);
|
|
685
|
+
ctx.ui.setHiddenThinkingLabel("");
|
|
686
|
+
}
|
|
687
|
+
applyDynamicTheme();
|
|
688
|
+
if (ctx.mode === "tui") {
|
|
689
|
+
installStartupHeader(ctx);
|
|
690
|
+
if (tuiRef?.children) {
|
|
691
|
+
for (const child of tuiRef.children) {
|
|
692
|
+
if (child?.children?.length > 1 && typeof child.addChild === "function" && child !== tuiRef) {
|
|
693
|
+
const nonSpacers = child.children.filter((c: any) => c?.constructor?.name !== "Spacer");
|
|
694
|
+
child.children.length = 0;
|
|
695
|
+
child.children.push(...nonSpacers);
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Only animate the logo on genuinely fresh sessions where the
|
|
701
|
+
// header is visible. On /resume, /fork, and /reload the header
|
|
702
|
+
// is scrolled off-screen (or it's a hot reload) — the 60ms
|
|
703
|
+
// setInterval from startLogoAnimation would call requestRender()
|
|
704
|
+
// every tick, fighting the scrollarea's lazy-load and causing
|
|
705
|
+
// an infinite render glitch on long resumed sessions.
|
|
706
|
+
if (event.reason === "startup" || event.reason === "new") {
|
|
707
|
+
startLogoAnimation();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Re-render header when model changes so the name updates live.
|
|
713
|
+
pi.on("model_select", (_event, ctx) => {
|
|
714
|
+
if (ctx.mode === "tui") requestRender?.();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Re-render header/footer when the active agent mode changes. Emitted by
|
|
718
|
+
// pi-custom-agents (and any other extension) via the shared event bus.
|
|
719
|
+
pi.events.on("pi-ember-ui:mode-change", (event: any) => {
|
|
720
|
+
if (event?.liveOnly === true) {
|
|
721
|
+
// Mode switches update the live editor/footer only. Invalidating the
|
|
722
|
+
// whole TUI makes a large resumed transcript re-render synchronously.
|
|
723
|
+
// The custom-agents extension supplies the single live render tick.
|
|
724
|
+
applyDynamicTheme({ invalidate: false, render: false });
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
applyDynamicTheme();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
pi.on("message_update", (event, ctx) => {
|
|
731
|
+
if (ctx.mode !== "tui") return;
|
|
732
|
+
const ev = event.assistantMessageEvent;
|
|
733
|
+
if (ev && (ev.type === "thinking_start" || ev.type === "thinking_delta")) {
|
|
734
|
+
if (!thinkingActive) startThinkingAnimation();
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
pi.on("message_end", () => {
|
|
739
|
+
stopThinkingAnimation();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
pi.on("message_start", () => {
|
|
743
|
+
stopLogoAnimation();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
747
|
+
if (ctx.mode !== "tui") return;
|
|
748
|
+
startWorkingAnimation();
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
pi.on("agent_end", () => {
|
|
752
|
+
stopThinkingAnimation();
|
|
753
|
+
stopWorkingAnimation();
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
pi.on("tool_execution_start", (_event, ctx) => {
|
|
757
|
+
if (ctx.mode !== "tui") return;
|
|
758
|
+
recompute_latest_subagent_running();
|
|
759
|
+
if (!thinkingActive) startWorkingAnimation();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
pi.on("tool_execution_end", (_event, ctx) => {
|
|
763
|
+
if (ctx.mode !== "tui") return;
|
|
764
|
+
recompute_latest_subagent_running();
|
|
765
|
+
if (workingActive && !thinkingActive) requestRender?.();
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
pi.registerCommand("welcome", {
|
|
769
|
+
description: "Configure the startup welcome header",
|
|
770
|
+
handler: async (args: string, ctx: any) => {
|
|
771
|
+
const normalized = args.trim().toLowerCase();
|
|
772
|
+
if (normalized === "updates on") {
|
|
773
|
+
setWelcomeUpdates(true);
|
|
774
|
+
ctx.ui.notify("Welcome update notices enabled for future sessions", "info");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (normalized === "updates off") {
|
|
778
|
+
setWelcomeUpdates(false);
|
|
779
|
+
ctx.ui.notify("Welcome update notices disabled for future sessions", "info");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
ctx.ui.notify("Usage: /welcome updates on | /welcome updates off", "info");
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Reset ALL session-bound module state on shutdown so a subsequent
|
|
787
|
+
// /resume (which re-runs the factory against a fresh runtime but keeps
|
|
788
|
+
// the cached module) does not call into the dead session's TUI/ctx via
|
|
789
|
+
// stale closures. The factory body calls applyDynamicTheme() on load,
|
|
790
|
+
// which would otherwise invoke the old requestRender/tuiRef.
|
|
791
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
792
|
+
sessionCtx = undefined;
|
|
793
|
+
requestRender = undefined;
|
|
794
|
+
tuiRef = undefined;
|
|
795
|
+
liveTheme = undefined;
|
|
796
|
+
liveCodeBgAnsi = "";
|
|
797
|
+
subagentRunningCached = false;
|
|
798
|
+
stopLogoAnimation();
|
|
799
|
+
stopThinkingAnimation();
|
|
800
|
+
stopWorkingAnimation();
|
|
801
|
+
setShellMode(false);
|
|
802
|
+
setLatestSubagentRunning(false);
|
|
803
|
+
if (ctx.hasUI) ctx.ui.setHeader(undefined);
|
|
804
|
+
});
|
|
805
|
+
}
|