@nmzpy/pi-ember-stack 0.2.1 → 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 +63 -59
- package/plugins/devin-auth/extensions/index.ts +23 -0
- 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/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/pi-compact-tools/index.ts +19 -1
- package/plugins/pi-compact-tools/renderer.ts +231 -61
- package/plugins/pi-custom-agents/index.ts +310 -102
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +116 -224
- 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 +241 -177
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +275 -17
- package/plugins/pi-ember-fff/query.ts +170 -10
- package/plugins/pi-ember-fff/test/query.test.ts +157 -1
- package/plugins/pi-ember-fff/test/renderer.test.ts +367 -16
- package/plugins/pi-ember-tps/index.ts +27 -5
- package/plugins/pi-ember-ui/ember.json +3 -0
- package/plugins/pi-ember-ui/index.ts +223 -36
- package/plugins/pi-ember-ui/mode-colors.ts +36 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ember TPS Meter — minimal tokens-per-second tracker
|
|
3
3
|
*
|
|
4
|
-
* Tracks output token rate during streaming and exposes the live
|
|
5
|
-
* via getLiveTps() for the custom footer to render.
|
|
4
|
+
* Tracks output plus thinking token rate during streaming and exposes the live
|
|
5
|
+
* value via getLiveTps() for the custom footer to render.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -12,6 +12,7 @@ const STREAM_INTERVAL_MS = 500;
|
|
|
12
12
|
let streamStartMs = 0;
|
|
13
13
|
let firstTokenMs = 0;
|
|
14
14
|
let streamChars = 0;
|
|
15
|
+
let streamThinkingChars = 0;
|
|
15
16
|
let streamTokens = 0;
|
|
16
17
|
let tickTimer: ReturnType<typeof setInterval> | null = null;
|
|
17
18
|
let streaming = false;
|
|
@@ -61,6 +62,7 @@ export default function piEmberTps(pi: ExtensionAPI): void {
|
|
|
61
62
|
streamStartMs = now();
|
|
62
63
|
firstTokenMs = 0;
|
|
63
64
|
streamChars = 0;
|
|
65
|
+
streamThinkingChars = 0;
|
|
64
66
|
streamTokens = 0;
|
|
65
67
|
liveTps = 0;
|
|
66
68
|
streaming = true;
|
|
@@ -79,6 +81,7 @@ export default function piEmberTps(pi: ExtensionAPI): void {
|
|
|
79
81
|
if (!d) return;
|
|
80
82
|
if (firstTokenMs === 0) firstTokenMs = now();
|
|
81
83
|
streamChars += d.length;
|
|
84
|
+
if (evt.type === "thinking_delta") streamThinkingChars += d.length;
|
|
82
85
|
streamTokens = tokEst(streamChars);
|
|
83
86
|
}
|
|
84
87
|
});
|
|
@@ -88,9 +91,16 @@ export default function piEmberTps(pi: ExtensionAPI): void {
|
|
|
88
91
|
streaming = false;
|
|
89
92
|
stopTick();
|
|
90
93
|
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
+
const usage = event.message?.usage;
|
|
95
|
+
const realOut = usage?.output;
|
|
96
|
+
// Some providers report output tokens without their thinking tokens.
|
|
97
|
+
// Keep the streamed combined estimate in that case; providers that
|
|
98
|
+
// expose `reasoning` already include it in `usage.output`.
|
|
99
|
+
const tokens = typeof realOut === "number" && realOut > 0
|
|
100
|
+
? streamThinkingChars > 0 && usage?.reasoning === undefined
|
|
101
|
+
? Math.max(realOut, streamTokens)
|
|
102
|
+
: realOut
|
|
103
|
+
: streamTokens;
|
|
94
104
|
|
|
95
105
|
const ref = firstTokenMs > 0 ? firstTokenMs : streamStartMs;
|
|
96
106
|
const elapsed = (now() - ref) / 1000;
|
|
@@ -114,9 +124,21 @@ export default function piEmberTps(pi: ExtensionAPI): void {
|
|
|
114
124
|
streamStartMs = 0;
|
|
115
125
|
firstTokenMs = 0;
|
|
116
126
|
streamChars = 0;
|
|
127
|
+
streamThinkingChars = 0;
|
|
117
128
|
streamTokens = 0;
|
|
118
129
|
liveTps = 0;
|
|
119
130
|
renderTrigger = undefined;
|
|
120
131
|
ctx.ui.setStatus("tps", undefined);
|
|
121
132
|
});
|
|
133
|
+
|
|
134
|
+
// Clear the tick interval and stale render trigger on shutdown so a
|
|
135
|
+
// subsequent /resume does not keep a setInterval alive against the dead
|
|
136
|
+
// session's ctx.ui. Without this, the 500ms tick keeps firing and
|
|
137
|
+
// calls the old renderTrigger long after the session is gone.
|
|
138
|
+
pi.on("session_shutdown", async () => {
|
|
139
|
+
streaming = false;
|
|
140
|
+
stopTick();
|
|
141
|
+
renderTrigger = undefined;
|
|
142
|
+
liveTps = 0;
|
|
143
|
+
});
|
|
122
144
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"emberAccent45": "#A8501F",
|
|
10
10
|
"emberAccent30": "#8F4627",
|
|
11
11
|
"emberAccent15": "#783C2F",
|
|
12
|
+
"emberAccent25": "#4D2E17",
|
|
12
13
|
"emberGray": "#808080",
|
|
13
14
|
"emberDarkGray": "#505050",
|
|
14
15
|
"cyan": "#00d7ff",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"accent": "#EB6E00",
|
|
24
25
|
"selectedBg": "#3a3a4a",
|
|
25
26
|
"userMsgBg": "#343541",
|
|
27
|
+
"subagentBg": "#32333f",
|
|
26
28
|
"toolPendingBg": "#282832",
|
|
27
29
|
"toolSuccessBg": "#283228",
|
|
28
30
|
"toolErrorBg": "#3c2828",
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
|
|
44
46
|
"selectedBg": "selectedBg",
|
|
45
47
|
"userMessageBg": "userMsgBg",
|
|
48
|
+
"subagentBg": "subagentBg",
|
|
46
49
|
"userMessageText": "text",
|
|
47
50
|
"customMessageBg": "customMsgBg",
|
|
48
51
|
"customMessageText": "text",
|
|
@@ -2,22 +2,27 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import {
|
|
5
|
+
AssistantMessageComponent,
|
|
5
6
|
Theme,
|
|
6
7
|
type ExtensionAPI,
|
|
7
8
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { Editor, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import { Editor, Markdown, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
10
|
import {
|
|
10
11
|
buildCodeBgHex,
|
|
11
12
|
buildThemeBgColors,
|
|
12
13
|
buildThemeFgColors,
|
|
13
14
|
getActiveModeColor,
|
|
15
|
+
isShellMode,
|
|
16
|
+
MUTED_COLOR,
|
|
17
|
+
setLatestSubagentRunning,
|
|
18
|
+
setShellMode,
|
|
14
19
|
} from "./mode-colors.ts";
|
|
15
20
|
|
|
16
21
|
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
17
22
|
const THEME_JSON = path.join(SOURCE_ROOT, "ember.json");
|
|
18
23
|
const THEME_NAME = "ember";
|
|
19
24
|
|
|
20
|
-
const THINKING_FRAME_INTERVAL_MS =
|
|
25
|
+
const THINKING_FRAME_INTERVAL_MS = 33;
|
|
21
26
|
const LOGO = [
|
|
22
27
|
" ██████ ██",
|
|
23
28
|
" ██ ██ ██",
|
|
@@ -40,6 +45,7 @@ let logoAnimating = false;
|
|
|
40
45
|
let logoFrame = 0;
|
|
41
46
|
let logoTimer: ReturnType<typeof setInterval> | undefined;
|
|
42
47
|
let editorRenderPatched = false;
|
|
48
|
+
let assistantMessagePatched = false;
|
|
43
49
|
let requestRender: (() => void) | undefined;
|
|
44
50
|
let sessionCtx: any;
|
|
45
51
|
|
|
@@ -48,6 +54,57 @@ type EditorWithBorder = Editor & {
|
|
|
48
54
|
getText: () => string;
|
|
49
55
|
};
|
|
50
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
|
+
|
|
51
108
|
function currentLabelText(): string {
|
|
52
109
|
return thinkingActive ? "Thinking" : "Working";
|
|
53
110
|
}
|
|
@@ -66,7 +123,7 @@ function startThinkingAnimation(): void {
|
|
|
66
123
|
if (!thinkingInterval) {
|
|
67
124
|
thinkingFrame = 0;
|
|
68
125
|
thinkingInterval = setInterval(() => {
|
|
69
|
-
thinkingFrame += 0.
|
|
126
|
+
thinkingFrame += 0.09;
|
|
70
127
|
if (thinkingFrame > 1) thinkingFrame -= 1;
|
|
71
128
|
requestRender?.();
|
|
72
129
|
}, THINKING_FRAME_INTERVAL_MS);
|
|
@@ -78,7 +135,7 @@ function startWorkingAnimation(): void {
|
|
|
78
135
|
if (!thinkingInterval) {
|
|
79
136
|
thinkingFrame = 0;
|
|
80
137
|
thinkingInterval = setInterval(() => {
|
|
81
|
-
thinkingFrame += 0.
|
|
138
|
+
thinkingFrame += 0.09;
|
|
82
139
|
if (thinkingFrame > 1) thinkingFrame -= 1;
|
|
83
140
|
requestRender?.();
|
|
84
141
|
}, THINKING_FRAME_INTERVAL_MS);
|
|
@@ -146,7 +203,7 @@ function installProxiedTheme(fgColors: Record<string, string>, bgColors: Record<
|
|
|
146
203
|
(globalThis as any)[THEME_KEY_OLD] = wrapped;
|
|
147
204
|
}
|
|
148
205
|
|
|
149
|
-
function applyDynamicTheme(): void {
|
|
206
|
+
function applyDynamicTheme(options: { invalidate?: boolean; render?: boolean } = {}): void {
|
|
150
207
|
const accent = getActiveModeColor();
|
|
151
208
|
const fgColors = buildThemeFgColors(accent);
|
|
152
209
|
const bgColors = buildThemeBgColors(accent);
|
|
@@ -155,12 +212,12 @@ function applyDynamicTheme(): void {
|
|
|
155
212
|
if (liveTheme) {
|
|
156
213
|
liveCodeBgAnsi = bgAnsi(codeBg);
|
|
157
214
|
updateLiveThemeColors(fgColors, bgColors);
|
|
158
|
-
(tuiRef as any)?.invalidate();
|
|
159
|
-
requestRender?.();
|
|
215
|
+
if (options.invalidate !== false) (tuiRef as any)?.invalidate();
|
|
216
|
+
if (options.render !== false) requestRender?.();
|
|
160
217
|
return;
|
|
161
218
|
}
|
|
162
219
|
installProxiedTheme(fgColors, bgColors, codeBg);
|
|
163
|
-
requestRender?.();
|
|
220
|
+
if (options.render !== false) requestRender?.();
|
|
164
221
|
}
|
|
165
222
|
|
|
166
223
|
function updateLiveThemeColors(fgColors: Record<string, string>, bgColors: Record<string, string>): void {
|
|
@@ -175,23 +232,32 @@ function updateLiveThemeColors(fgColors: Record<string, string>, bgColors: Recor
|
|
|
175
232
|
}
|
|
176
233
|
}
|
|
177
234
|
|
|
178
|
-
function renderGradientLabel(text: string, accent: string): string {
|
|
235
|
+
function renderGradientLabel(text: string, accent: string, phaseOffset = 0): string {
|
|
179
236
|
const chars = [...text];
|
|
180
237
|
const len = chars.length;
|
|
181
238
|
if (len === 0) return "";
|
|
182
|
-
const
|
|
239
|
+
const dimHex = blendToHex(accent, "#18181e", 0.4);
|
|
183
240
|
const result: string[] = [];
|
|
241
|
+
const phase = (thinkingFrame + phaseOffset) % 1;
|
|
184
242
|
for (let i = 0; i < len; i++) {
|
|
185
243
|
const charPos = i / Math.max(1, len - 1);
|
|
186
|
-
const dist = charPos -
|
|
244
|
+
const dist = charPos - phase;
|
|
187
245
|
const wrapped = dist < -0.5 ? dist + 1 : dist > 0.5 ? dist - 1 : dist;
|
|
188
246
|
const intensity = Math.exp(-(wrapped * wrapped) * 8);
|
|
189
|
-
const hex = blendToHex(
|
|
247
|
+
const hex = blendToHex(dimHex, accent, intensity);
|
|
190
248
|
result.push(colorize(chars[i], hex));
|
|
191
249
|
}
|
|
192
250
|
return result.join("");
|
|
193
251
|
}
|
|
194
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
|
+
|
|
195
261
|
function installThinkingBorderOverride(): void {
|
|
196
262
|
if (editorRenderPatched) return;
|
|
197
263
|
editorRenderPatched = true;
|
|
@@ -199,7 +265,10 @@ function installThinkingBorderOverride(): void {
|
|
|
199
265
|
Editor.prototype.render = function renderThinkingBorder(this: EditorWithBorder, width: number): string[] {
|
|
200
266
|
const accent = getActiveModeColor();
|
|
201
267
|
const accentLight = lightenHex(accent, 0.5);
|
|
202
|
-
const
|
|
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)} `;
|
|
203
272
|
const originalBorderColor = this.borderColor;
|
|
204
273
|
this.borderColor = accentBorder;
|
|
205
274
|
const innerWidth = Math.max(1, width - 2);
|
|
@@ -222,11 +291,16 @@ function installThinkingBorderOverride(): void {
|
|
|
222
291
|
if (i === bottomBorderIdx) continue;
|
|
223
292
|
lines[i] = ` ${lines[i]} `;
|
|
224
293
|
}
|
|
225
|
-
|
|
294
|
+
const subagentActive = subagentRunningCached;
|
|
295
|
+
if (subagentActive) {
|
|
296
|
+
lines[topIdx] = dimInsetBorder();
|
|
297
|
+
} else {
|
|
298
|
+
lines[topIdx] = accentBorder("\u2500".repeat(width));
|
|
299
|
+
}
|
|
226
300
|
if (bottomBorderIdx >= 0) {
|
|
227
301
|
const inputText = this.getText?.() ?? "";
|
|
228
302
|
if (inputText.trimStart().startsWith("/")) {
|
|
229
|
-
lines[bottomBorderIdx] =
|
|
303
|
+
lines[bottomBorderIdx] = dimInsetBorder();
|
|
230
304
|
} else {
|
|
231
305
|
lines[bottomBorderIdx] = accentBorder("\u2500".repeat(width));
|
|
232
306
|
}
|
|
@@ -236,6 +310,7 @@ function installThinkingBorderOverride(): void {
|
|
|
236
310
|
lines[lastLineIdx] = accentBorder("\u2500".repeat(width));
|
|
237
311
|
}
|
|
238
312
|
if (lines.length === 0) return lines;
|
|
313
|
+
if (subagentActive) return lines;
|
|
239
314
|
if (!thinkingActive && !workingActive) return lines;
|
|
240
315
|
|
|
241
316
|
const labelText = thinkingActive ? "Thinking" : "Working";
|
|
@@ -248,6 +323,105 @@ function installThinkingBorderOverride(): void {
|
|
|
248
323
|
};
|
|
249
324
|
}
|
|
250
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
|
+
|
|
251
425
|
function colorWithOpacity(text: string, hex: string, opacity: number): string {
|
|
252
426
|
const source = hex.slice(1);
|
|
253
427
|
const base = [24, 24, 30];
|
|
@@ -498,11 +672,13 @@ function ensureThemeInstalled(): void {
|
|
|
498
672
|
export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
499
673
|
ensureThemeInstalled();
|
|
500
674
|
installThinkingBorderOverride();
|
|
675
|
+
installAssistantMessagePatch();
|
|
501
676
|
applyDynamicTheme();
|
|
502
677
|
|
|
503
|
-
pi.on("session_start", (
|
|
678
|
+
pi.on("session_start", (event, ctx) => {
|
|
504
679
|
sessionCtx = ctx;
|
|
505
680
|
liveTheme = undefined;
|
|
681
|
+
subagentRunningCached = false;
|
|
506
682
|
if (ctx.mode === "tui") {
|
|
507
683
|
requestRender = () => ctx.ui.setStatus("pi-ember-ui-thinking-tick", undefined);
|
|
508
684
|
ctx.ui.setWorkingVisible(false);
|
|
@@ -521,11 +697,15 @@ export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
|
521
697
|
}
|
|
522
698
|
}
|
|
523
699
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
+
}
|
|
529
709
|
}
|
|
530
710
|
});
|
|
531
711
|
|
|
@@ -536,7 +716,14 @@ export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
|
536
716
|
|
|
537
717
|
// Re-render header/footer when the active agent mode changes. Emitted by
|
|
538
718
|
// pi-custom-agents (and any other extension) via the shared event bus.
|
|
539
|
-
pi.events.on("pi-ember-ui:mode-change", () => {
|
|
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
|
+
}
|
|
540
727
|
applyDynamicTheme();
|
|
541
728
|
});
|
|
542
729
|
|
|
@@ -568,11 +755,13 @@ export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
|
568
755
|
|
|
569
756
|
pi.on("tool_execution_start", (_event, ctx) => {
|
|
570
757
|
if (ctx.mode !== "tui") return;
|
|
758
|
+
recompute_latest_subagent_running();
|
|
571
759
|
if (!thinkingActive) startWorkingAnimation();
|
|
572
760
|
});
|
|
573
761
|
|
|
574
762
|
pi.on("tool_execution_end", (_event, ctx) => {
|
|
575
763
|
if (ctx.mode !== "tui") return;
|
|
764
|
+
recompute_latest_subagent_running();
|
|
576
765
|
if (workingActive && !thinkingActive) requestRender?.();
|
|
577
766
|
});
|
|
578
767
|
|
|
@@ -594,25 +783,23 @@ export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
|
594
783
|
},
|
|
595
784
|
});
|
|
596
785
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
invalidate() {},
|
|
603
|
-
} as any;
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
607
|
-
if (ctx.mode !== "tui") return;
|
|
608
|
-
pi.appendEntry("ember-turn-separator");
|
|
609
|
-
});
|
|
610
|
-
|
|
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.
|
|
611
791
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
612
792
|
sessionCtx = undefined;
|
|
793
|
+
requestRender = undefined;
|
|
794
|
+
tuiRef = undefined;
|
|
795
|
+
liveTheme = undefined;
|
|
796
|
+
liveCodeBgAnsi = "";
|
|
797
|
+
subagentRunningCached = false;
|
|
613
798
|
stopLogoAnimation();
|
|
614
799
|
stopThinkingAnimation();
|
|
615
800
|
stopWorkingAnimation();
|
|
801
|
+
setShellMode(false);
|
|
802
|
+
setLatestSubagentRunning(false);
|
|
616
803
|
if (ctx.hasUI) ctx.ui.setHeader(undefined);
|
|
617
804
|
});
|
|
618
805
|
}
|
|
@@ -6,6 +6,7 @@ export const MODE_COLORS: Record<string, string> = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export const MUTED_BULLET_COLOR = "#666666";
|
|
9
|
+
export const MUTED_COLOR = "#808080";
|
|
9
10
|
|
|
10
11
|
export const PAGE_BG = "#18181e";
|
|
11
12
|
|
|
@@ -23,6 +24,32 @@ export function setActiveMode(modeId: string): void {
|
|
|
23
24
|
activeModeId = modeId in MODE_COLORS ? modeId : "code";
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
let shellModeActive = false;
|
|
28
|
+
|
|
29
|
+
export function isShellMode(): boolean {
|
|
30
|
+
return shellModeActive;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setShellMode(active: boolean): void {
|
|
34
|
+
shellModeActive = active;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let latestSubagentRunningFlag = false;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether the latest tool call in the session is a running subagent.
|
|
41
|
+
* Set by pi-ember-ui's editor border patch (which has session access)
|
|
42
|
+
* and read by both the border patch and the subagent renderer (via
|
|
43
|
+
* this shared module) to draw the integrated border + cap line.
|
|
44
|
+
*/
|
|
45
|
+
export function isLatestSubagentRunning(): boolean {
|
|
46
|
+
return latestSubagentRunningFlag;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function setLatestSubagentRunning(active: boolean): void {
|
|
50
|
+
latestSubagentRunningFlag = active;
|
|
51
|
+
}
|
|
52
|
+
|
|
26
53
|
export function hexToRgb(hex: string): string {
|
|
27
54
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
28
55
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
@@ -126,22 +153,22 @@ export function buildThemeFgColors(accentHex: string): Record<string, string> {
|
|
|
126
153
|
success: "#b5bd68",
|
|
127
154
|
error: "#cc6666",
|
|
128
155
|
warning: "#ffff00",
|
|
129
|
-
muted:
|
|
156
|
+
muted: MUTED_COLOR,
|
|
130
157
|
dim: "#666666",
|
|
131
158
|
text: "#d4d4d4",
|
|
132
|
-
thinkingText:
|
|
159
|
+
thinkingText: MUTED_COLOR,
|
|
133
160
|
userMessageText: "#d4d4d4",
|
|
134
161
|
customMessageText: "#d4d4d4",
|
|
135
|
-
toolOutput:
|
|
162
|
+
toolOutput: MUTED_COLOR,
|
|
136
163
|
mdLinkUrl: "#666666",
|
|
137
164
|
mdCodeBlock: "#b5bd68",
|
|
138
|
-
mdCodeBlockBorder:
|
|
139
|
-
mdQuote:
|
|
140
|
-
mdQuoteBorder:
|
|
141
|
-
mdHr:
|
|
165
|
+
mdCodeBlockBorder: MUTED_COLOR,
|
|
166
|
+
mdQuote: MUTED_COLOR,
|
|
167
|
+
mdQuoteBorder: MUTED_COLOR,
|
|
168
|
+
mdHr: MUTED_COLOR,
|
|
142
169
|
toolDiffAdded: "#b5bd68",
|
|
143
170
|
toolDiffRemoved: "#cc6666",
|
|
144
|
-
toolDiffContext:
|
|
171
|
+
toolDiffContext: MUTED_COLOR,
|
|
145
172
|
syntaxComment: "#6A9955",
|
|
146
173
|
syntaxKeyword: "#569CD6",
|
|
147
174
|
syntaxFunction: "#DCDCAA",
|
|
@@ -160,6 +187,7 @@ export function buildThemeBgColors(accentHex: string): Record<string, string> {
|
|
|
160
187
|
return {
|
|
161
188
|
selectedBg: "#3a3a4a",
|
|
162
189
|
userMessageBg: userMsgBg,
|
|
190
|
+
subagentBg: blendToHex(accentHex, PAGE_BG, 0.09),
|
|
163
191
|
customMessageBg: "#2d2838",
|
|
164
192
|
toolPendingBg: "#282832",
|
|
165
193
|
toolSuccessBg: "#283228",
|