pi-extended-teams 2.1.16 → 2.1.18
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/extensions/index.ts +3 -2
- package/extensions/ui/agent-follow-view.test.ts +64 -6
- package/extensions/ui/agent-follow-view.ts +97 -84
- package/extensions/ui/frame.ts +33 -19
- package/extensions/ui/status-widget.test.ts +26 -0
- package/extensions/ui/status-widget.ts +53 -20
- package/extensions/ui/theme.ts +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,13 @@ Read agents are the default. Edit agents are opt-in and should own isolated file
|
|
|
10
10
|
|
|
11
11
|
## Install and run
|
|
12
12
|
|
|
13
|
-
Install from
|
|
13
|
+
Install from npm:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:pi-extended-teams
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly from GitHub:
|
|
14
20
|
|
|
15
21
|
```bash
|
|
16
22
|
pi install git:github.com/dantetekanem/pi-extended-teams
|
package/extensions/index.ts
CHANGED
|
@@ -496,12 +496,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
496
496
|
teamActivityWidgetMounted = true;
|
|
497
497
|
sessionCtx.ui.setWidget(
|
|
498
498
|
"01-pi-extended-teams-readers",
|
|
499
|
-
(tui: any) => {
|
|
499
|
+
(tui: any, theme: any) => {
|
|
500
500
|
teamActivityWidgetTui = tui;
|
|
501
501
|
return teamActivityStatusWidget(
|
|
502
502
|
() => teamActivityStatusSnapshot,
|
|
503
503
|
isTeamActivityExpanded,
|
|
504
|
-
() => tui.requestRender?.()
|
|
504
|
+
() => tui.requestRender?.(),
|
|
505
|
+
theme
|
|
505
506
|
);
|
|
506
507
|
},
|
|
507
508
|
{ placement: "belowEditor" }
|
|
@@ -10,6 +10,29 @@ function stripAnsi(text: string): string {
|
|
|
10
10
|
return text.replace(ANSI_SGR_SEQUENCE, "");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function makeTheme() {
|
|
14
|
+
const foregroundCodes: Record<string, number> = {
|
|
15
|
+
accent: 213,
|
|
16
|
+
border: 141,
|
|
17
|
+
borderAccent: 141,
|
|
18
|
+
customMessageLabel: 213,
|
|
19
|
+
dim: 247,
|
|
20
|
+
error: 210,
|
|
21
|
+
muted: 247,
|
|
22
|
+
success: 114,
|
|
23
|
+
syntaxFunction: 117,
|
|
24
|
+
syntaxString: 213,
|
|
25
|
+
text: 253,
|
|
26
|
+
thinkingText: 141,
|
|
27
|
+
warning: 222,
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
fg: vi.fn((token: string, text: string) => `\x1b[38;5;${foregroundCodes[token] ?? 253}m${text}\x1b[39m`),
|
|
31
|
+
bg: vi.fn((_token: string, text: string) => `\x1b[48;5;235m${text}\x1b[49m`),
|
|
32
|
+
bold: vi.fn((text: string) => `\x1b[1m${text}\x1b[22m`),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
function makeAgent(overrides: Partial<RunningReadAgent> = {}): RunningReadAgent {
|
|
14
37
|
return {
|
|
15
38
|
runId: "run-1",
|
|
@@ -223,7 +246,8 @@ describe("agent follow transcript", () => {
|
|
|
223
246
|
expect(lines.join("\n")).not.toContain("complete final report body");
|
|
224
247
|
});
|
|
225
248
|
|
|
226
|
-
it("uses semantic chrome
|
|
249
|
+
it("uses Pi theme tokens for semantic chrome while leaving prose and raw output neutral", () => {
|
|
250
|
+
const theme = makeTheme();
|
|
227
251
|
const lines = formatAgentFollowTranscript([
|
|
228
252
|
{ role: "assistant", content: [
|
|
229
253
|
{ type: "thinking", thinking: "Neutral thinking prose" },
|
|
@@ -243,7 +267,7 @@ describe("agent follow transcript", () => {
|
|
|
243
267
|
},
|
|
244
268
|
{ role: "toolResult", toolCallId: "edit-failure", toolName: "edit", content: "Edit failed", isError: true },
|
|
245
269
|
{ role: "toolResult", toolCallId: "bash-raw", toolName: "bash", content: "neutral shell output", isError: false },
|
|
246
|
-
], { width: 80 });
|
|
270
|
+
], { width: 80, theme });
|
|
247
271
|
|
|
248
272
|
const pendingHeader = lines.find((line) => stripAnsi(line).startsWith("read")) || "";
|
|
249
273
|
const pendingState = lines.find((line) => stripAnsi(line).includes("waiting for result")) || "";
|
|
@@ -261,6 +285,12 @@ describe("agent follow transcript", () => {
|
|
|
261
285
|
expect(rawOutput).toBe("\x1b[38;5;141m│\x1b[39m neutral shell output");
|
|
262
286
|
expect(lines).toContain("Neutral thinking prose");
|
|
263
287
|
expect(lines).toContain("Neutral assistant prose");
|
|
288
|
+
expect(theme.fg).toHaveBeenCalledWith("syntaxFunction", "read");
|
|
289
|
+
expect(theme.fg).toHaveBeenCalledWith("syntaxString", "src/pending.ts");
|
|
290
|
+
expect(theme.fg).toHaveBeenCalledWith("warning", "waiting for result…");
|
|
291
|
+
expect(theme.fg).toHaveBeenCalledWith("success", "+2");
|
|
292
|
+
expect(theme.fg).toHaveBeenCalledWith("error", "−1");
|
|
293
|
+
expect(theme.fg).toHaveBeenCalledWith("borderAccent", "│");
|
|
264
294
|
});
|
|
265
295
|
|
|
266
296
|
it("keeps claim and release tools compact and semantically colored", () => {
|
|
@@ -285,7 +315,7 @@ describe("agent follow transcript", () => {
|
|
|
285
315
|
details: { released: ["src/a.ts"] },
|
|
286
316
|
isError: false,
|
|
287
317
|
},
|
|
288
|
-
], { width: 80 });
|
|
318
|
+
], { width: 80, theme: makeTheme() });
|
|
289
319
|
|
|
290
320
|
expect(lines.map(stripAnsi)).toEqual([
|
|
291
321
|
"claim · src/a.ts, src/b.ts · ✓",
|
|
@@ -367,6 +397,7 @@ describe("agent follow component", () => {
|
|
|
367
397
|
let contextPercent = 23;
|
|
368
398
|
const done = vi.fn();
|
|
369
399
|
const tui = { terminal: { rows: 30 }, requestRender: vi.fn() };
|
|
400
|
+
const theme = makeTheme();
|
|
370
401
|
const agent = makeAgent({
|
|
371
402
|
session: {
|
|
372
403
|
messages: [{ role: "assistant", content: [{ type: "text", text: "Working now" }] }],
|
|
@@ -376,15 +407,17 @@ describe("agent follow component", () => {
|
|
|
376
407
|
}),
|
|
377
408
|
} as any,
|
|
378
409
|
});
|
|
379
|
-
const component = createAgentFollowComponent(tui, done, { getAgents: () => [agent] });
|
|
410
|
+
const component = createAgentFollowComponent(tui, done, { getAgents: () => [agent] }, theme);
|
|
380
411
|
|
|
381
412
|
const first = component.render(140).join("\n");
|
|
382
413
|
expect(first).toMatch(/\(reader\) gpt-model\/high · reading-default · 1m00s · 46k tok \(23%\) · Verifying assumptions\.{1,3}/);
|
|
383
414
|
expect(first).not.toContain("502k tok");
|
|
384
415
|
expect(first).toContain("Working now");
|
|
385
416
|
expect(stripAnsi(first)).not.toContain("progress:");
|
|
386
|
-
expect(
|
|
387
|
-
expect(
|
|
417
|
+
expect(theme.bg).toHaveBeenCalledWith("customMessageBg", expect.any(String));
|
|
418
|
+
expect(theme.fg).toHaveBeenCalledWith("borderAccent", expect.any(String));
|
|
419
|
+
expect(first).toContain("\x1b[48;5;235m");
|
|
420
|
+
expect(first).not.toContain("\x1b[48;2;22;23;32m");
|
|
388
421
|
|
|
389
422
|
billedTokens = 2_300_000;
|
|
390
423
|
contextTokens = 80_000;
|
|
@@ -400,6 +433,31 @@ describe("agent follow component", () => {
|
|
|
400
433
|
component.dispose();
|
|
401
434
|
});
|
|
402
435
|
|
|
436
|
+
it("rebuilds cached themed content when Pi invalidates the component", () => {
|
|
437
|
+
const tui = { terminal: { rows: 20 }, requestRender: vi.fn() };
|
|
438
|
+
let color = 31;
|
|
439
|
+
const theme = {
|
|
440
|
+
fg: vi.fn((_token: string, text: string) => `\x1b[${color}m${text}\x1b[39m`),
|
|
441
|
+
bg: vi.fn((_token: string, text: string) => text),
|
|
442
|
+
bold: vi.fn((text: string) => text),
|
|
443
|
+
};
|
|
444
|
+
const agent = makeAgent({
|
|
445
|
+
session: {
|
|
446
|
+
messages: [{ role: "assistant", content: [{ type: "toolCall", id: "read-1", name: "read", arguments: { path: "README.md" } }] }],
|
|
447
|
+
} as any,
|
|
448
|
+
});
|
|
449
|
+
const component = createAgentFollowComponent(tui, vi.fn(), { getAgents: () => [agent] }, theme);
|
|
450
|
+
|
|
451
|
+
expect(component.render(100).join("\n")).toContain("\x1b[31mread");
|
|
452
|
+
color = 32;
|
|
453
|
+
component.invalidate();
|
|
454
|
+
const rerendered = component.render(100).join("\n");
|
|
455
|
+
|
|
456
|
+
expect(rerendered).toContain("\x1b[32mread");
|
|
457
|
+
expect(rerendered).not.toContain("\x1b[31mread");
|
|
458
|
+
component.dispose();
|
|
459
|
+
});
|
|
460
|
+
|
|
403
461
|
it("uses the ordinary agent status when no stored progress is present", () => {
|
|
404
462
|
const tui = { terminal: { rows: 20 }, requestRender: vi.fn() };
|
|
405
463
|
const agent = makeAgent({ latestProgress: undefined, status: "working" });
|
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import { Input, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
2
2
|
import type { RunningReadAgent } from "../runtime/types";
|
|
3
3
|
import { initialContextUsage } from "../../src/utils/runtime";
|
|
4
|
-
import {
|
|
5
|
-
import { createFramePanelRowRenderer, framePanel } from "./frame";
|
|
4
|
+
import { createFramePanelRowRenderer, framePanel, type FramePanelStyle } from "./frame";
|
|
6
5
|
import { extractTextParts, formatAnimatedProgress, formatContextUsage, formatElapsed, formatModelLabel, sanitizePlainTuiLine, sanitizeTuiLine, sanitizeTuiText } from "./renderers";
|
|
6
|
+
import { resolveExtendedTeamsTheme, type ExtendedTeamsForegroundToken, type ExtendedTeamsTheme } from "./theme";
|
|
7
7
|
|
|
8
8
|
const REFRESH_INTERVAL_MS = 250;
|
|
9
9
|
const MAX_NAVIGATION_AGENTS = 6;
|
|
10
|
-
const AGENT_FOLLOW_BACKGROUND = "\x1b[48;2;22;23;32m";
|
|
11
|
-
const ACTION_FOREGROUND = "\x1b[38;5;117m";
|
|
12
|
-
const PATH_FOREGROUND = "\x1b[38;5;213m";
|
|
13
|
-
const SUCCESS_FOREGROUND = "\x1b[38;5;114m";
|
|
14
|
-
const FAILURE_FOREGROUND = "\x1b[38;5;210m";
|
|
15
|
-
const PENDING_FOREGROUND = "\x1b[38;5;222m";
|
|
16
|
-
const BODY_FOREGROUND = "\x1b[38;5;253m";
|
|
17
|
-
const MUTED_FOREGROUND = "\x1b[38;5;247m";
|
|
18
|
-
const STRUCTURAL_FOREGROUND = "\x1b[38;5;141m";
|
|
19
|
-
const ANSI_FOREGROUND_RESET = "\x1b[39m";
|
|
20
|
-
const ANSI_RESET = "\x1b[0m";
|
|
21
10
|
const COLLAPSED_TOOL_RESULT_LINE_LIMIT = 14;
|
|
22
11
|
const COLLAPSED_TOOL_RESULT_HEAD_LINES = 8;
|
|
23
12
|
const COLLAPSED_TOOL_RESULT_TAIL_LINES = 3;
|
|
@@ -32,6 +21,7 @@ export interface AgentFollowViewOptions {
|
|
|
32
21
|
export interface AgentFollowTranscriptOptions {
|
|
33
22
|
expandLargeToolResults?: boolean;
|
|
34
23
|
width?: number;
|
|
24
|
+
theme?: ExtendedTeamsTheme;
|
|
35
25
|
}
|
|
36
26
|
|
|
37
27
|
type TranscriptBlock =
|
|
@@ -78,40 +68,40 @@ function compactTranscriptLine(text: string): string {
|
|
|
78
68
|
return sanitizePlainTuiLine(text).replace(/\s+/g, " ").trim();
|
|
79
69
|
}
|
|
80
70
|
|
|
81
|
-
function
|
|
82
|
-
return
|
|
71
|
+
function themed(theme: ExtendedTeamsTheme, token: ExtendedTeamsForegroundToken, text: string): string {
|
|
72
|
+
return theme.fg(token, text);
|
|
83
73
|
}
|
|
84
74
|
|
|
85
|
-
function
|
|
86
|
-
return
|
|
75
|
+
function actionText(theme: ExtendedTeamsTheme, text: string): string {
|
|
76
|
+
return themed(theme, "syntaxFunction", text);
|
|
87
77
|
}
|
|
88
78
|
|
|
89
|
-
function
|
|
90
|
-
return
|
|
79
|
+
function pathText(theme: ExtendedTeamsTheme, text: string): string {
|
|
80
|
+
return themed(theme, "syntaxString", text);
|
|
91
81
|
}
|
|
92
82
|
|
|
93
|
-
function
|
|
94
|
-
return
|
|
83
|
+
function successText(theme: ExtendedTeamsTheme, text: string): string {
|
|
84
|
+
return themed(theme, "success", text);
|
|
95
85
|
}
|
|
96
86
|
|
|
97
|
-
function
|
|
98
|
-
return
|
|
87
|
+
function failureText(theme: ExtendedTeamsTheme, text: string): string {
|
|
88
|
+
return themed(theme, "error", text);
|
|
99
89
|
}
|
|
100
90
|
|
|
101
|
-
function
|
|
102
|
-
return
|
|
91
|
+
function pendingText(theme: ExtendedTeamsTheme, text: string): string {
|
|
92
|
+
return themed(theme, "warning", text);
|
|
103
93
|
}
|
|
104
94
|
|
|
105
|
-
function
|
|
106
|
-
return
|
|
95
|
+
function bodyText(theme: ExtendedTeamsTheme, text: string): string {
|
|
96
|
+
return themed(theme, "text", text);
|
|
107
97
|
}
|
|
108
98
|
|
|
109
|
-
function
|
|
110
|
-
return
|
|
99
|
+
function mutedText(theme: ExtendedTeamsTheme, text: string): string {
|
|
100
|
+
return themed(theme, "muted", text);
|
|
111
101
|
}
|
|
112
102
|
|
|
113
|
-
function
|
|
114
|
-
return
|
|
103
|
+
function structuralText(theme: ExtendedTeamsTheme, text: string): string {
|
|
104
|
+
return themed(theme, "borderAccent", text);
|
|
115
105
|
}
|
|
116
106
|
|
|
117
107
|
function boundTranscriptLine(line: string, width?: number): string {
|
|
@@ -146,26 +136,26 @@ function editDiffCounts(details: unknown): { added: number; removed: number } |
|
|
|
146
136
|
return { added, removed };
|
|
147
137
|
}
|
|
148
138
|
|
|
149
|
-
function renderState(state: string): string {
|
|
150
|
-
if (state === "failed") return
|
|
151
|
-
if (state === "working" || state === "submitting") return
|
|
152
|
-
if (state === "duplicate") return
|
|
153
|
-
return
|
|
139
|
+
function renderState(theme: ExtendedTeamsTheme, state: string): string {
|
|
140
|
+
if (state === "failed") return failureText(theme, state);
|
|
141
|
+
if (state === "working" || state === "submitting") return pendingText(theme, state);
|
|
142
|
+
if (state === "duplicate") return mutedText(theme, state);
|
|
143
|
+
return successText(theme, state);
|
|
154
144
|
}
|
|
155
145
|
|
|
156
|
-
function renderActionPathState(action: string, path: string, state: string, width?: number): string {
|
|
146
|
+
function renderActionPathState(theme: ExtendedTeamsTheme, action: string, path: string, state: string, width?: number): string {
|
|
157
147
|
const renderedState = state === "failed"
|
|
158
|
-
?
|
|
148
|
+
? failureText(theme, "✗")
|
|
159
149
|
: state === "succeeded"
|
|
160
|
-
?
|
|
161
|
-
: renderState(state);
|
|
150
|
+
? successText(theme, "✓")
|
|
151
|
+
: renderState(theme, state);
|
|
162
152
|
return boundTranscriptLine(
|
|
163
|
-
`${
|
|
153
|
+
`${actionText(theme, action)}${mutedText(theme, " · ")}${pathText(theme, path)}${mutedText(theme, " · ")}${renderedState}`,
|
|
164
154
|
width,
|
|
165
155
|
);
|
|
166
156
|
}
|
|
167
157
|
|
|
168
|
-
function renderCompactToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }>, width?: number): string[] | undefined {
|
|
158
|
+
function renderCompactToolBlock(theme: ExtendedTeamsTheme, block: Extract<TranscriptBlock, { kind: "tool" }>, width?: number): string[] | undefined {
|
|
169
159
|
if (block.name === "report_progress") {
|
|
170
160
|
const details = asRecord(block.details);
|
|
171
161
|
const args = asRecord(block.args);
|
|
@@ -178,20 +168,20 @@ function renderCompactToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }
|
|
|
178
168
|
|
|
179
169
|
if (block.name === "edit") {
|
|
180
170
|
const path = toolPath(block.args);
|
|
181
|
-
if (block.result === undefined) return [renderActionPathState("edit", path, "working", width)];
|
|
182
|
-
if (block.isError) return [renderActionPathState("edit", path, "failed", width)];
|
|
171
|
+
if (block.result === undefined) return [renderActionPathState(theme, "edit", path, "working", width)];
|
|
172
|
+
if (block.isError) return [renderActionPathState(theme, "edit", path, "failed", width)];
|
|
183
173
|
const counts = editDiffCounts(block.details);
|
|
184
174
|
const added = counts ? `+${counts.added}` : "+?";
|
|
185
175
|
const removed = counts ? `−${counts.removed}` : "−?";
|
|
186
176
|
return [boundTranscriptLine(
|
|
187
|
-
`${
|
|
177
|
+
`${actionText(theme, "edit")}${mutedText(theme, " · ")}${pathText(theme, path)}${mutedText(theme, " · ")}${successText(theme, added)} ${failureText(theme, removed)}${mutedText(theme, " · ")}${successText(theme, "✓")}`,
|
|
188
178
|
width,
|
|
189
179
|
)];
|
|
190
180
|
}
|
|
191
181
|
|
|
192
182
|
if (block.name === "write") {
|
|
193
183
|
const state = block.result === undefined ? "working" : block.isError ? "failed" : "succeeded";
|
|
194
|
-
return [renderActionPathState("write", toolPath(block.args), state, width)];
|
|
184
|
+
return [renderActionPathState(theme, "write", toolPath(block.args), state, width)];
|
|
195
185
|
}
|
|
196
186
|
|
|
197
187
|
if (block.name === "claim_file" || block.name === "release_file") {
|
|
@@ -203,7 +193,7 @@ function renderCompactToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }
|
|
|
203
193
|
? "failed"
|
|
204
194
|
: "succeeded";
|
|
205
195
|
const action = block.name === "claim_file" ? "claim" : "release";
|
|
206
|
-
return [renderActionPathState(action, toolPaths(block.args), state, width)];
|
|
196
|
+
return [renderActionPathState(theme, action, toolPaths(block.args), state, width)];
|
|
207
197
|
}
|
|
208
198
|
|
|
209
199
|
if (block.name === "report_and_exit") {
|
|
@@ -215,27 +205,27 @@ function renderCompactToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }
|
|
|
215
205
|
: accepted === false
|
|
216
206
|
? "duplicate"
|
|
217
207
|
: "accepted";
|
|
218
|
-
return [boundTranscriptLine(`${
|
|
208
|
+
return [boundTranscriptLine(`${actionText(theme, "final report")}${mutedText(theme, " · ")}${renderState(theme, state)}`, width)];
|
|
219
209
|
}
|
|
220
210
|
|
|
221
211
|
return undefined;
|
|
222
212
|
}
|
|
223
213
|
|
|
224
|
-
function renderToolHeader(block: Extract<TranscriptBlock, { kind: "tool" }>): string {
|
|
214
|
+
function renderToolHeader(theme: ExtendedTeamsTheme, block: Extract<TranscriptBlock, { kind: "tool" }>): string {
|
|
225
215
|
const detail = compactToolArgs(block.name, block.args);
|
|
226
|
-
if (!detail) return
|
|
216
|
+
if (!detail) return actionText(theme, block.name);
|
|
227
217
|
const isPath = typeof asRecord(block.args)?.path === "string";
|
|
228
|
-
const renderedDetail = isPath ?
|
|
229
|
-
return `${
|
|
218
|
+
const renderedDetail = isPath ? pathText(theme, detail) : bodyText(theme, `${block.name === "bash" ? "$ " : ""}${detail}`);
|
|
219
|
+
return `${actionText(theme, block.name)}${mutedText(theme, " · ")}${renderedDetail}`;
|
|
230
220
|
}
|
|
231
221
|
|
|
232
|
-
function renderToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }>, expandLargeToolResults: boolean, width?: number): string[] {
|
|
233
|
-
const compactBlock = renderCompactToolBlock(block, width);
|
|
222
|
+
function renderToolBlock(theme: ExtendedTeamsTheme, block: Extract<TranscriptBlock, { kind: "tool" }>, expandLargeToolResults: boolean, width?: number): string[] {
|
|
223
|
+
const compactBlock = renderCompactToolBlock(theme, block, width);
|
|
234
224
|
if (compactBlock) return compactBlock;
|
|
235
225
|
|
|
236
|
-
const header = renderToolHeader(block);
|
|
226
|
+
const header = renderToolHeader(theme, block);
|
|
237
227
|
if (block.result === undefined) {
|
|
238
|
-
return [header, `${
|
|
228
|
+
return [header, `${structuralText(theme, "│")} ${pendingText(theme, "waiting for result…")}`, `${structuralText(theme, "╰─")} ${pendingText(theme, "running")}`, ""];
|
|
239
229
|
}
|
|
240
230
|
|
|
241
231
|
const result = block.result || "(no output)";
|
|
@@ -253,14 +243,15 @@ function renderToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }>, expa
|
|
|
253
243
|
? line
|
|
254
244
|
: truncateToWidth(line, resultLineWidth, "…"));
|
|
255
245
|
const body = boundedLines.map((line, index) => isCollapsed && index === COLLAPSED_TOOL_RESULT_HEAD_LINES
|
|
256
|
-
? `${
|
|
257
|
-
: `${
|
|
246
|
+
? `${structuralText(theme, "│")} ${mutedText(theme, line)}`
|
|
247
|
+
: `${structuralText(theme, "│")} ${line}`);
|
|
258
248
|
const summary = `${resultLines.length} line${resultLines.length === 1 ? "" : "s"} · ${formatResultSize(result)}${isCollapsed ? " · collapsed" : ""}`;
|
|
259
|
-
const renderedSummary = block.isError ?
|
|
260
|
-
return [header, ...body, `${
|
|
249
|
+
const renderedSummary = block.isError ? failureText(theme, summary) : successText(theme, summary);
|
|
250
|
+
return [header, ...body, `${structuralText(theme, "╰─")} ${renderedSummary}`, ""];
|
|
261
251
|
}
|
|
262
252
|
|
|
263
253
|
export function formatAgentFollowTranscript(messages: any[], options: AgentFollowTranscriptOptions = {}): string[] {
|
|
254
|
+
const theme = resolveExtendedTeamsTheme(options.theme);
|
|
264
255
|
const blocks: TranscriptBlock[] = [];
|
|
265
256
|
const toolsById = new Map<string, Extract<TranscriptBlock, { kind: "tool" }>>();
|
|
266
257
|
|
|
@@ -311,14 +302,15 @@ export function formatAgentFollowTranscript(messages: any[], options: AgentFollo
|
|
|
311
302
|
|
|
312
303
|
const lines = blocks.flatMap(block => {
|
|
313
304
|
if (block.kind === "tool") {
|
|
314
|
-
return renderToolBlock(block, options.expandLargeToolResults === true, options.width);
|
|
305
|
+
return renderToolBlock(theme, block, options.expandLargeToolResults === true, options.width);
|
|
315
306
|
}
|
|
316
307
|
if (block.label === "thinking") {
|
|
317
|
-
return [
|
|
308
|
+
return [theme.fg("thinkingText", block.label), block.text.replace(/\*\*/g, ""), ""];
|
|
318
309
|
}
|
|
319
|
-
|
|
310
|
+
const labelToken = block.label === "user" ? "customMessageLabel" : "accent";
|
|
311
|
+
return [theme.fg(labelToken, block.label), block.text, ""];
|
|
320
312
|
});
|
|
321
|
-
return lines.length > 0 ? lines : [
|
|
313
|
+
return lines.length > 0 ? lines : [theme.fg("dim", "Waiting for the agent's first transcript event…")];
|
|
322
314
|
}
|
|
323
315
|
|
|
324
316
|
function currentAgent(agents: RunningReadAgent[], selectedName: string | undefined): RunningReadAgent | undefined {
|
|
@@ -328,8 +320,14 @@ function currentAgent(agents: RunningReadAgent[], selectedName: string | undefin
|
|
|
328
320
|
export function createAgentFollowComponent(
|
|
329
321
|
tui: any,
|
|
330
322
|
done: () => void,
|
|
331
|
-
options: AgentFollowViewOptions
|
|
323
|
+
options: AgentFollowViewOptions,
|
|
324
|
+
providedTheme?: ExtendedTeamsTheme
|
|
332
325
|
) {
|
|
326
|
+
const theme = resolveExtendedTeamsTheme(providedTheme);
|
|
327
|
+
const frameStyle: FramePanelStyle = {
|
|
328
|
+
border: (text) => theme.fg("borderAccent", text),
|
|
329
|
+
background: (text) => theme.bg("customMessageBg", text),
|
|
330
|
+
};
|
|
333
331
|
let selectedName = options.initialAgentName;
|
|
334
332
|
let offsetFromBottom = 0;
|
|
335
333
|
let lastBodyHeight = 10;
|
|
@@ -501,32 +499,32 @@ export function createAgentFollowComponent(
|
|
|
501
499
|
if (!agent) {
|
|
502
500
|
const emptyBodyHeight = Math.max(4, terminalRows - 6);
|
|
503
501
|
return framePanel([
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
502
|
+
theme.fg("accent", "agent navigation"),
|
|
503
|
+
theme.fg("borderAccent", "↑ main agent"),
|
|
504
|
+
theme.fg("dim", "No active agents. Press ↑ or esc to return to main."),
|
|
507
505
|
...Array.from({ length: emptyBodyHeight }, () => ""),
|
|
508
|
-
], innerWidth,
|
|
506
|
+
], innerWidth, frameStyle);
|
|
509
507
|
}
|
|
510
508
|
|
|
511
509
|
const selectedIndex = Math.max(0, agents.findIndex(item => item.name === agent.name));
|
|
512
510
|
const navigationStart = Math.max(0, Math.min(selectedIndex - 2, agents.length - MAX_NAVIGATION_AGENTS));
|
|
513
511
|
const visibleAgents = agents.slice(navigationStart, navigationStart + MAX_NAVIGATION_AGENTS);
|
|
514
|
-
const navigationLines = [
|
|
515
|
-
if (navigationStart > 0) navigationLines.push(
|
|
512
|
+
const navigationLines = [theme.fg("accent", "agent navigation"), theme.fg("borderAccent", "↑ main agent")];
|
|
513
|
+
if (navigationStart > 0) navigationLines.push(theme.fg("dim", ` … ${navigationStart} agent${navigationStart === 1 ? "" : "s"} above`));
|
|
516
514
|
for (const item of visibleAgents) {
|
|
517
515
|
const selected = item.name === agent.name;
|
|
518
|
-
navigationLines.push(`${selected ?
|
|
516
|
+
navigationLines.push(`${selected ? theme.fg("accent", "->") : " "} ${item.name}`);
|
|
519
517
|
}
|
|
520
518
|
const remainingAgents = agents.length - navigationStart - visibleAgents.length;
|
|
521
|
-
if (remainingAgents > 0) navigationLines.push(
|
|
519
|
+
if (remainingAgents > 0) navigationLines.push(theme.fg("dim", `↓ … ${remainingAgents} more agent${remainingAgents === 1 ? "" : "s"}`));
|
|
522
520
|
|
|
523
521
|
const messageLines = options.sendMessage ? [
|
|
524
|
-
|
|
525
|
-
composingMessage ?
|
|
526
|
-
...(composingMessage ? messageInput.render(innerWidth) : [
|
|
522
|
+
theme.fg("border", "─".repeat(innerWidth)),
|
|
523
|
+
composingMessage ? theme.fg("accent", `message ${agent.name}`) : theme.fg("dim", `message ${agent.name}`),
|
|
524
|
+
...(composingMessage ? messageInput.render(innerWidth) : [theme.fg("dim", "> Press m to start typing")]),
|
|
527
525
|
...(messageStatus
|
|
528
|
-
? [
|
|
529
|
-
: composingMessage ? [
|
|
526
|
+
? [theme.fg("dim", messageStatus)]
|
|
527
|
+
: composingMessage ? [theme.fg("dim", "enter send · esc cancel")] : []),
|
|
530
528
|
] : [];
|
|
531
529
|
const bodyHeight = Math.max(4, terminalRows - navigationLines.length - 6 - messageLines.length);
|
|
532
530
|
lastBodyHeight = bodyHeight;
|
|
@@ -611,6 +609,7 @@ export function createAgentFollowComponent(
|
|
|
611
609
|
cachedTranscriptLines = formatAgentFollowTranscript(currentMessages, {
|
|
612
610
|
expandLargeToolResults,
|
|
613
611
|
width: currentTranscriptWidth,
|
|
612
|
+
theme,
|
|
614
613
|
}).flatMap(line => wrapTextWithAnsi(line, currentTranscriptWidth));
|
|
615
614
|
}
|
|
616
615
|
const transcriptLines = cachedTranscriptLines;
|
|
@@ -623,10 +622,10 @@ export function createAgentFollowComponent(
|
|
|
623
622
|
|
|
624
623
|
const frameContent = [
|
|
625
624
|
...navigationLines,
|
|
626
|
-
|
|
625
|
+
theme.fg("border", "─".repeat(innerWidth)),
|
|
627
626
|
headline,
|
|
628
|
-
|
|
629
|
-
|
|
627
|
+
theme.fg("dim", help),
|
|
628
|
+
theme.fg("border", "─".repeat(innerWidth)),
|
|
630
629
|
...visible,
|
|
631
630
|
...messageLines,
|
|
632
631
|
];
|
|
@@ -670,8 +669,8 @@ export function createAgentFollowComponent(
|
|
|
670
669
|
}
|
|
671
670
|
}
|
|
672
671
|
if (!renderedFrame) {
|
|
673
|
-
renderedFrame = framePanel(frameContent, innerWidth,
|
|
674
|
-
cachedFrameRowRenderer = createFramePanelRowRenderer(innerWidth,
|
|
672
|
+
renderedFrame = framePanel(frameContent, innerWidth, frameStyle);
|
|
673
|
+
cachedFrameRowRenderer = createFramePanelRowRenderer(innerWidth, frameStyle);
|
|
675
674
|
}
|
|
676
675
|
cachedFrameWidth = innerWidth;
|
|
677
676
|
cachedFrameContent = frameContent;
|
|
@@ -684,6 +683,20 @@ export function createAgentFollowComponent(
|
|
|
684
683
|
},
|
|
685
684
|
invalidate() {
|
|
686
685
|
messageInput.invalidate();
|
|
686
|
+
transcriptAgent = undefined;
|
|
687
|
+
transcriptMessages = undefined;
|
|
688
|
+
transcriptMessageCount = -1;
|
|
689
|
+
transcriptLastMessage = undefined;
|
|
690
|
+
transcriptWidth = -1;
|
|
691
|
+
cachedTranscriptLines = [];
|
|
692
|
+
cachedFrameWidth = -1;
|
|
693
|
+
cachedFrameRowRenderer = undefined;
|
|
694
|
+
cachedFrameContent = null;
|
|
695
|
+
cachedFrameLines = null;
|
|
696
|
+
fastFrameKey = "";
|
|
697
|
+
fastFrameMessages = undefined;
|
|
698
|
+
fastFrameMessageCount = -1;
|
|
699
|
+
fastFrameLastMessage = undefined;
|
|
687
700
|
},
|
|
688
701
|
dispose() {
|
|
689
702
|
clearInterval(refreshTimer);
|
|
@@ -765,7 +778,7 @@ export function createAgentFollowComponent(
|
|
|
765
778
|
export async function openAgentFollowView(ctx: any, options: AgentFollowViewOptions): Promise<void> {
|
|
766
779
|
if (ctx.mode && ctx.mode !== "tui") return;
|
|
767
780
|
await ctx.ui.custom(
|
|
768
|
-
(tui: any,
|
|
781
|
+
(tui: any, theme: any, _keybindings: any, done: () => void) => createAgentFollowComponent(tui, done, options, theme),
|
|
769
782
|
{
|
|
770
783
|
overlay: true,
|
|
771
784
|
overlayOptions: {
|
package/extensions/ui/frame.ts
CHANGED
|
@@ -5,44 +5,58 @@ const ANSI_PURPLE = "\x1b[38;5;141m";
|
|
|
5
5
|
const ANSI_PANEL_BG = "\x1b[48;5;235m";
|
|
6
6
|
const SINGLE_COLUMN_FRAME_TEXT = /^[\x20-\x7E\u00B7]*$/;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
export interface FramePanelStyle {
|
|
9
|
+
border(text: string): string;
|
|
10
|
+
background(text: string): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type FramePanelBackground = string | FramePanelStyle;
|
|
14
|
+
|
|
15
|
+
// Fill a line to `width` visible columns. The legacy string path preserves the
|
|
16
|
+
// public ANSI helpers; custom layouts can instead compose Pi theme callbacks.
|
|
17
|
+
function panelBgFillMeasured(line: string, lineWidth: number, width: number, background: FramePanelBackground): string {
|
|
18
|
+
const padded = `${line}${" ".repeat(Math.max(0, width - lineWidth))}`;
|
|
19
|
+
if (typeof background === "string") {
|
|
20
|
+
const reasserted = padded.includes(ANSI_RESET)
|
|
21
|
+
? padded.split(ANSI_RESET).join(ANSI_RESET + background)
|
|
22
|
+
: padded;
|
|
23
|
+
return `${background}${reasserted}${ANSI_RESET}`;
|
|
24
|
+
}
|
|
25
|
+
return padded.includes(ANSI_RESET)
|
|
26
|
+
? padded.split(ANSI_RESET).map((segment) => background.background(segment)).join(ANSI_RESET)
|
|
27
|
+
: background.background(padded);
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export function panelBgFill(line: string, width: number, background = ANSI_PANEL_BG): string {
|
|
20
31
|
return panelBgFillMeasured(line, visibleWidth(line), width, background);
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
function
|
|
34
|
+
function styledBorder(text: string, background: FramePanelBackground): string {
|
|
35
|
+
return typeof background === "string"
|
|
36
|
+
? `${background}${ANSI_PURPLE}${text}${ANSI_RESET}`
|
|
37
|
+
: background.background(background.border(text));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderFramePanelRow(line: string, innerWidth: number, span: number, background: FramePanelBackground, sideBorder: string): string {
|
|
24
41
|
const singleColumn = SINGLE_COLUMN_FRAME_TEXT.test(line);
|
|
25
|
-
if (singleColumn && line.length <= innerWidth) {
|
|
26
|
-
return `${sideBorder}${background} ${line} ${" ".repeat(innerWidth - line.length)}${ANSI_RESET}${sideBorder}`;
|
|
27
|
-
}
|
|
28
42
|
const lineWidth = singleColumn ? line.length : visibleWidth(line);
|
|
29
43
|
const boundedLine = lineWidth > innerWidth ? truncateToWidth(line, innerWidth, "…", true) : line;
|
|
30
44
|
const boundedWidth = lineWidth > innerWidth ? (singleColumn ? boundedLine.length : visibleWidth(boundedLine)) : lineWidth;
|
|
31
45
|
return sideBorder + panelBgFillMeasured(` ${boundedLine} `, boundedWidth + 2, span, background) + sideBorder;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
export function createFramePanelRowRenderer(innerWidth: number, background = ANSI_PANEL_BG): (line: string) => string {
|
|
48
|
+
export function createFramePanelRowRenderer(innerWidth: number, background: FramePanelBackground = ANSI_PANEL_BG): (line: string) => string {
|
|
35
49
|
const span = innerWidth + 2;
|
|
36
|
-
const sideBorder =
|
|
50
|
+
const sideBorder = styledBorder("│", background);
|
|
37
51
|
return (line: string) => renderFramePanelRow(line, innerWidth, span, background, sideBorder);
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
// Wrap content lines in a rounded border
|
|
41
|
-
//
|
|
42
|
-
export function framePanel(contentLines: string[], innerWidth: number, background = ANSI_PANEL_BG): string[] {
|
|
54
|
+
// Wrap content lines in a rounded border. `innerWidth` is the column count
|
|
55
|
+
// between the one-space padding inside each side border.
|
|
56
|
+
export function framePanel(contentLines: string[], innerWidth: number, background: FramePanelBackground = ANSI_PANEL_BG): string[] {
|
|
43
57
|
const span = innerWidth + 2;
|
|
44
58
|
const rule = "─".repeat(span);
|
|
45
|
-
const border = (text: string) =>
|
|
59
|
+
const border = (text: string) => styledBorder(text, background);
|
|
46
60
|
const sideBorder = border("│");
|
|
47
61
|
const out: string[] = [border(`╭${rule}╮`)];
|
|
48
62
|
for (const line of contentLines) {
|
|
@@ -14,6 +14,32 @@ function makeSnapshot(overrides: Partial<TeamActivityStatusSnapshot> = {}): Team
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
describe("agent activity status widget", () => {
|
|
17
|
+
it("uses Pi theme tokens and refreshes cached styles after invalidation", () => {
|
|
18
|
+
const snapshot = makeSnapshot({
|
|
19
|
+
activeCount: 1,
|
|
20
|
+
readCount: 1,
|
|
21
|
+
entries: [{ name: "reader", role: "read", status: "thinking" }],
|
|
22
|
+
});
|
|
23
|
+
let color = 31;
|
|
24
|
+
const theme = {
|
|
25
|
+
fg: vi.fn((_token: string, text: string) => `\x1b[${color}m${text}\x1b[39m`),
|
|
26
|
+
bg: vi.fn((_token: string, text: string) => text),
|
|
27
|
+
bold: vi.fn((text: string) => text),
|
|
28
|
+
};
|
|
29
|
+
const widget = teamActivityStatusWidget(() => snapshot, () => false, undefined, theme);
|
|
30
|
+
|
|
31
|
+
expect(widget.render(120).join("\n")).toContain("\x1b[31magent activity");
|
|
32
|
+
expect(theme.fg).toHaveBeenCalledWith("accent", "agent activity");
|
|
33
|
+
expect(theme.fg).toHaveBeenCalledWith("border", expect.any(String));
|
|
34
|
+
|
|
35
|
+
color = 32;
|
|
36
|
+
widget.invalidate();
|
|
37
|
+
const rerendered = widget.render(120).join("\n");
|
|
38
|
+
expect(rerendered).toContain("\x1b[32magent activity");
|
|
39
|
+
expect(rerendered).not.toContain("\x1b[31magent activity");
|
|
40
|
+
widget.dispose();
|
|
41
|
+
});
|
|
42
|
+
|
|
17
43
|
it("renders open snapshots without a collapse hint", () => {
|
|
18
44
|
const entries: TeamActivityStatusEntry[] = [{
|
|
19
45
|
name: "reader",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
2
|
-
import { dimAnsi, pink, purple } from "./ansi";
|
|
3
2
|
import { formatAnimatedProgress } from "./renderers";
|
|
3
|
+
import { resolveExtendedTeamsTheme, type ExtendedTeamsTheme } from "./theme";
|
|
4
4
|
|
|
5
5
|
export interface TeamActivityStatusEntry {
|
|
6
6
|
name: string;
|
|
@@ -62,21 +62,21 @@ function formatStatusSummary(statusCounts: TeamActivityStatusCounts | undefined)
|
|
|
62
62
|
return shown.join(" · ");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function formatAggregatePreview(snapshot: TeamActivityStatusSnapshot): string {
|
|
65
|
+
function formatAggregatePreview(snapshot: TeamActivityStatusSnapshot, theme: ExtendedTeamsTheme): string {
|
|
66
66
|
const summary = formatStatusSummary(snapshot.statusCounts) || formatRoleSummary(snapshot);
|
|
67
|
-
return `${
|
|
67
|
+
return `${theme.fg("accent", "summary")} ${theme.fg("borderAccent", summary)} ${theme.fg("dim", "↓ navigate")}`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function formatExpandedEntry(entry: TeamActivityStatusEntry): string {
|
|
70
|
+
function formatExpandedEntry(entry: TeamActivityStatusEntry, theme: ExtendedTeamsTheme): string {
|
|
71
71
|
if (entry.displayText) return entry.displayText;
|
|
72
|
-
const status = entry.status ? ` ${
|
|
73
|
-
const detail = entry.detail ? ` ${
|
|
74
|
-
return `${
|
|
72
|
+
const status = entry.status ? ` ${theme.fg("borderAccent", entry.status)}` : "";
|
|
73
|
+
const detail = entry.detail ? ` ${theme.fg("dim", entry.detail)}` : "";
|
|
74
|
+
return `${theme.fg("accent", entry.name)} ${theme.fg("borderAccent", entry.role)}${status}${detail}`;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function formatHeader(snapshot: TeamActivityStatusSnapshot): string {
|
|
77
|
+
function formatHeader(snapshot: TeamActivityStatusSnapshot, theme: ExtendedTeamsTheme): string {
|
|
78
78
|
const summary = `${formatCountSummary(snapshot)} · ↓ navigate`;
|
|
79
|
-
return `${
|
|
79
|
+
return `${theme.fg("accent", "agent activity")} ${theme.fg("dim", summary)}`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
interface ProgressTransition {
|
|
@@ -131,8 +131,10 @@ function splitProgressDisplay(displayText: string | undefined): { prefix: string
|
|
|
131
131
|
export function teamActivityStatusWidget(
|
|
132
132
|
getSnapshot: () => TeamActivityStatusSnapshot | null | undefined,
|
|
133
133
|
_getExpanded: () => boolean,
|
|
134
|
-
requestRender?: () => void
|
|
134
|
+
requestRender?: () => void,
|
|
135
|
+
providedTheme?: ExtendedTeamsTheme
|
|
135
136
|
) {
|
|
137
|
+
const theme = resolveExtendedTeamsTheme(providedTheme);
|
|
136
138
|
const transitions = new Map<string, ProgressTransition>();
|
|
137
139
|
let animationTimer: NodeJS.Timeout | null = null;
|
|
138
140
|
let summarySnapshot: TeamActivityStatusSnapshot | null | undefined;
|
|
@@ -186,15 +188,15 @@ export function teamActivityStatusWidget(
|
|
|
186
188
|
if (!hasCachedTransitionEntry || transition) transitionHints[hintIndex] = transition;
|
|
187
189
|
}
|
|
188
190
|
if (!entry.displayText) {
|
|
189
|
-
return animationResult(`${
|
|
191
|
+
return animationResult(`${theme.fg("border", branch)} ${formatExpandedEntry(entry, theme)}`, false, width);
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
if (!transition || transition.source !== entry.displayText) {
|
|
193
195
|
const parsed = splitProgressDisplay(entry.displayText);
|
|
194
196
|
if (!parsed) {
|
|
195
|
-
return animationResult(`${
|
|
197
|
+
return animationResult(`${theme.fg("border", branch)} ${formatExpandedEntry(entry, theme)}`, false, width);
|
|
196
198
|
}
|
|
197
|
-
const branchPrefix = `${
|
|
199
|
+
const branchPrefix = `${theme.fg("border", branch)} `;
|
|
198
200
|
const prefixVisibleWidth = SINGLE_COLUMN_STATUS_TEXT.test(parsed.prefix)
|
|
199
201
|
? parsed.prefix.length
|
|
200
202
|
: visibleWidth(parsed.prefix);
|
|
@@ -273,14 +275,14 @@ export function teamActivityStatusWidget(
|
|
|
273
275
|
transitionHints[hintIndex] = transition;
|
|
274
276
|
} else if (transition.branch !== branch) {
|
|
275
277
|
transition.branch = branch;
|
|
276
|
-
transition.linePrefix = `${
|
|
278
|
+
transition.linePrefix = `${theme.fg("border", branch)} ${transition.prefix}`;
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
const elapsed = Math.max(0, now - transition.startedAt);
|
|
280
282
|
if (elapsed < 200) {
|
|
281
283
|
const remaining = Math.max(0, Math.ceil(transition.previous.length * (1 - elapsed / 200)));
|
|
282
284
|
const progressText = transition.previous.slice(0, remaining);
|
|
283
|
-
return animationResult(`${transition.linePrefix}${
|
|
285
|
+
return animationResult(`${transition.linePrefix}${theme.fg("dim", progressText)}`, true, width, currentTransitionRenderedWidth(transition, progressText, transition.previousSingleColumn, width));
|
|
284
286
|
}
|
|
285
287
|
if (elapsed < 1000) {
|
|
286
288
|
const revealed = Math.floor(transition.target.length * ((elapsed - 200) / 800));
|
|
@@ -324,7 +326,7 @@ export function teamActivityStatusWidget(
|
|
|
324
326
|
headerWriteCount = snapshot.writeCount;
|
|
325
327
|
headerUnreadCount = snapshot.unreadCount;
|
|
326
328
|
headerWidth = width;
|
|
327
|
-
headerLine = truncateToWidth(formatHeader(snapshot), width, "…", true);
|
|
329
|
+
headerLine = truncateToWidth(formatHeader(snapshot, theme), width, "…", true);
|
|
328
330
|
}
|
|
329
331
|
summaryLines = [headerLine];
|
|
330
332
|
if (shouldUseAggregatePreview(snapshot)) {
|
|
@@ -350,7 +352,7 @@ export function teamActivityStatusWidget(
|
|
|
350
352
|
aggregateReadCount = snapshot.readCount;
|
|
351
353
|
aggregateWriteCount = snapshot.writeCount;
|
|
352
354
|
aggregateWidth = width;
|
|
353
|
-
aggregateLine = truncateToWidth(formatAggregatePreview(snapshot), width, "…", true);
|
|
355
|
+
aggregateLine = truncateToWidth(formatAggregatePreview(snapshot, theme), width, "…", true);
|
|
354
356
|
}
|
|
355
357
|
summaryLines.push(aggregateLine);
|
|
356
358
|
}
|
|
@@ -409,7 +411,7 @@ export function teamActivityStatusWidget(
|
|
|
409
411
|
if (remainingCount !== remaining || remainingWidth !== width) {
|
|
410
412
|
remainingCount = remaining;
|
|
411
413
|
remainingWidth = width;
|
|
412
|
-
remainingLine = truncateToWidth(
|
|
414
|
+
remainingLine = truncateToWidth(theme.fg("dim", `└─ … ${remaining} more active agent${remaining === 1 ? "" : "s"}`), width, "…", true);
|
|
413
415
|
}
|
|
414
416
|
lines.push(remainingLine);
|
|
415
417
|
}
|
|
@@ -420,7 +422,7 @@ export function teamActivityStatusWidget(
|
|
|
420
422
|
stopAnimationTimer();
|
|
421
423
|
}
|
|
422
424
|
|
|
423
|
-
const border =
|
|
425
|
+
const border = theme.fg("border", "─".repeat(Math.max(0, width)));
|
|
424
426
|
const rendered = [...lines, border];
|
|
425
427
|
if (animationActive) {
|
|
426
428
|
stableLines = null;
|
|
@@ -432,7 +434,38 @@ export function teamActivityStatusWidget(
|
|
|
432
434
|
}
|
|
433
435
|
return rendered;
|
|
434
436
|
},
|
|
435
|
-
invalidate() {
|
|
437
|
+
invalidate() {
|
|
438
|
+
transitions.clear();
|
|
439
|
+
summarySnapshot = undefined;
|
|
440
|
+
summaryUpdatedAt = -1;
|
|
441
|
+
summaryWidth = -1;
|
|
442
|
+
summaryLines = [];
|
|
443
|
+
headerActiveCount = -1;
|
|
444
|
+
headerReadCount = -1;
|
|
445
|
+
headerWriteCount = -1;
|
|
446
|
+
headerUnreadCount = -1;
|
|
447
|
+
headerWidth = -1;
|
|
448
|
+
aggregateStatusCounts = {};
|
|
449
|
+
aggregateStatusCountSize = -1;
|
|
450
|
+
aggregateActiveCount = -1;
|
|
451
|
+
aggregateReadCount = -1;
|
|
452
|
+
aggregateWriteCount = -1;
|
|
453
|
+
aggregateWidth = -1;
|
|
454
|
+
remainingCount = -1;
|
|
455
|
+
remainingWidth = -1;
|
|
456
|
+
transitionHintSnapshot = undefined;
|
|
457
|
+
transitionHintUpdatedAt = -1;
|
|
458
|
+
transitionHintEntries.length = 0;
|
|
459
|
+
transitionHints.length = 0;
|
|
460
|
+
transitionRosterRoles.length = 0;
|
|
461
|
+
transitionRosterNames.length = 0;
|
|
462
|
+
transitionRosterKeys.length = 0;
|
|
463
|
+
transitionRosterCount = -1;
|
|
464
|
+
stableSnapshot = undefined;
|
|
465
|
+
stableUpdatedAt = -1;
|
|
466
|
+
stableWidth = -1;
|
|
467
|
+
stableLines = null;
|
|
468
|
+
},
|
|
436
469
|
dispose() {
|
|
437
470
|
stopAnimationTimer();
|
|
438
471
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ExtendedTeamsForegroundToken =
|
|
2
|
+
| "accent"
|
|
3
|
+
| "border"
|
|
4
|
+
| "borderAccent"
|
|
5
|
+
| "customMessageLabel"
|
|
6
|
+
| "dim"
|
|
7
|
+
| "error"
|
|
8
|
+
| "muted"
|
|
9
|
+
| "success"
|
|
10
|
+
| "syntaxFunction"
|
|
11
|
+
| "syntaxString"
|
|
12
|
+
| "text"
|
|
13
|
+
| "thinkingText"
|
|
14
|
+
| "warning";
|
|
15
|
+
|
|
16
|
+
export type ExtendedTeamsBackgroundToken = "customMessageBg";
|
|
17
|
+
|
|
18
|
+
export interface ExtendedTeamsTheme {
|
|
19
|
+
fg(token: ExtendedTeamsForegroundToken, text: string): string;
|
|
20
|
+
bg(token: ExtendedTeamsBackgroundToken, text: string): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const plainTheme: ExtendedTeamsTheme = {
|
|
24
|
+
fg: (_token, text) => text,
|
|
25
|
+
bg: (_token, text) => text,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function resolveExtendedTeamsTheme(theme?: Partial<ExtendedTeamsTheme> | null): ExtendedTeamsTheme {
|
|
29
|
+
if (!theme) return plainTheme;
|
|
30
|
+
return {
|
|
31
|
+
fg: (token, text) => typeof theme.fg === "function" ? theme.fg(token, text) : text,
|
|
32
|
+
bg: (token, text) => typeof theme.bg === "function" ? theme.bg(token, text) : text,
|
|
33
|
+
};
|
|
34
|
+
}
|