pi-extended-teams 2.1.16
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 +105 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-report.ts +181 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
- package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
- package/extensions/agents/read-agent.test.ts +3077 -0
- package/extensions/agents/read-agent.ts +1119 -0
- package/extensions/agents/write-agent.test.ts +513 -0
- package/extensions/agents/write-agent.ts +392 -0
- package/extensions/events/register-events.test.ts +465 -0
- package/extensions/events/register-events.ts +409 -0
- package/extensions/index.test.ts +1659 -0
- package/extensions/index.ts +1228 -0
- package/extensions/internal/agent-session-files.test.ts +164 -0
- package/extensions/internal/agent-session-files.ts +320 -0
- package/extensions/internal/debug.ts +44 -0
- package/extensions/internal/model-selection.ts +82 -0
- package/extensions/internal/pi-command.test.ts +191 -0
- package/extensions/internal/pi-command.ts +224 -0
- package/extensions/internal/pi-runtime-api.test.ts +43 -0
- package/extensions/internal/pi-runtime-api.ts +78 -0
- package/extensions/internal/schema.ts +19 -0
- package/extensions/internal/session-context-reference.test.ts +290 -0
- package/extensions/internal/session-context-reference.ts +430 -0
- package/extensions/internal/session-files.test.ts +225 -0
- package/extensions/internal/session-files.ts +273 -0
- package/extensions/internal/session-usage.ts +59 -0
- package/extensions/resources/spawn-resource-plan.test.ts +233 -0
- package/extensions/resources/spawn-resource-plan.ts +247 -0
- package/extensions/runtime/active-agent-sleep.test.ts +157 -0
- package/extensions/runtime/active-agent-sleep.ts +117 -0
- package/extensions/runtime/nested-read-agents.ts +22 -0
- package/extensions/runtime/pending-child-controller.test.ts +169 -0
- package/extensions/runtime/pending-child-controller.ts +280 -0
- package/extensions/runtime/types.ts +68 -0
- package/extensions/team/contracts.test.ts +111 -0
- package/extensions/team/lifecycle.test.ts +1095 -0
- package/extensions/team/lifecycle.ts +502 -0
- package/extensions/team/recipient-closure.test.ts +97 -0
- package/extensions/team/recipient-closure.ts +120 -0
- package/extensions/team/roster.test.ts +129 -0
- package/extensions/team/roster.ts +165 -0
- package/extensions/team/team-contracts.json +43 -0
- package/extensions/team/writer-screens.test.ts +50 -0
- package/extensions/team/writer-screens.ts +156 -0
- package/extensions/tools/agent-communication-tools.test.ts +306 -0
- package/extensions/tools/agent-communication-tools.ts +183 -0
- package/extensions/tools/coordination-tools.test.ts +670 -0
- package/extensions/tools/coordination-tools.ts +304 -0
- package/extensions/tools/delegation-guard.test.ts +95 -0
- package/extensions/tools/delegation-guard.ts +65 -0
- package/extensions/tools/file-claim-tools.test.ts +104 -0
- package/extensions/tools/file-claim-tools.ts +68 -0
- package/extensions/tools/model-tools.ts +53 -0
- package/extensions/tools/predefined-tools.test.ts +647 -0
- package/extensions/tools/predefined-tools.ts +331 -0
- package/extensions/tools/read-helper.test.ts +35 -0
- package/extensions/tools/task-runtime-tools.test.ts +334 -0
- package/extensions/tools/task-runtime-tools.ts +227 -0
- package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
- package/extensions/tools/team-tools.ts +1270 -0
- package/extensions/ui/agent-follow-view.test.ts +520 -0
- package/extensions/ui/agent-follow-view.ts +779 -0
- package/extensions/ui/agent-navigation.test.ts +52 -0
- package/extensions/ui/agent-navigation.ts +55 -0
- package/extensions/ui/ansi.ts +16 -0
- package/extensions/ui/extensions-command.test.ts +336 -0
- package/extensions/ui/extensions-command.ts +282 -0
- package/extensions/ui/favorite-models-command.test.ts +280 -0
- package/extensions/ui/favorite-models-command.ts +423 -0
- package/extensions/ui/frame.ts +77 -0
- package/extensions/ui/input.ts +17 -0
- package/extensions/ui/read-agent-status.test.ts +117 -0
- package/extensions/ui/read-agent-status.ts +125 -0
- package/extensions/ui/renderers.ts +243 -0
- package/extensions/ui/status-widget.test.ts +143 -0
- package/extensions/ui/status-widget.ts +440 -0
- package/extensions/ui-frame.test.ts +69 -0
- package/package.json +75 -0
- package/skills/teams.md +256 -0
- package/src/adapters/terminal-registry.ts +78 -0
- package/src/adapters/tmux-adapter.test.ts +276 -0
- package/src/adapters/tmux-adapter.ts +200 -0
- package/src/orchestration/index.ts +437 -0
- package/src/orchestration/orchestrator.test.ts +396 -0
- package/src/orchestration/types.ts +125 -0
- package/src/utils/atomic-json.ts +21 -0
- package/src/utils/claims.test.ts +137 -0
- package/src/utils/claims.ts +169 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lifecycle-tombstone.test.ts +105 -0
- package/src/utils/lifecycle-tombstone.ts +268 -0
- package/src/utils/lock.race.child.ts +44 -0
- package/src/utils/lock.race.test.ts +198 -0
- package/src/utils/lock.test.ts +90 -0
- package/src/utils/lock.ts +186 -0
- package/src/utils/messaging.test.ts +337 -0
- package/src/utils/messaging.ts +441 -0
- package/src/utils/model-resolution.test.ts +231 -0
- package/src/utils/model-resolution.ts +322 -0
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +115 -0
- package/src/utils/paths.ts +81 -0
- package/src/utils/predefined-teams/types.ts +48 -0
- package/src/utils/predefined-teams.save-template.test.ts +74 -0
- package/src/utils/predefined-teams.test.ts +441 -0
- package/src/utils/predefined-teams.ts +471 -0
- package/src/utils/read-helper-queue.ts +99 -0
- package/src/utils/report-events.test.ts +154 -0
- package/src/utils/report-events.ts +222 -0
- package/src/utils/runtime.test.ts +314 -0
- package/src/utils/runtime.ts +261 -0
- package/src/utils/security.test.ts +43 -0
- package/src/utils/settings.test.ts +480 -0
- package/src/utils/settings.ts +645 -0
- package/src/utils/shared-memory.test.ts +80 -0
- package/src/utils/shared-memory.ts +81 -0
- package/src/utils/tasks.race.test.ts +44 -0
- package/src/utils/tasks.test.ts +229 -0
- package/src/utils/tasks.ts +396 -0
- package/src/utils/teams.ts +231 -0
- package/src/utils/terminal-adapter.ts +103 -0
- package/src/utils/thinking-levels.test.ts +56 -0
- package/src/utils/thinking-levels.ts +47 -0
- package/src/utils/workflow-metadata.test.ts +11 -0
- package/src/utils/workflow-metadata.ts +88 -0
- package/src/utils/write-queue.test.ts +251 -0
- package/src/utils/write-queue.ts +202 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createAgentFollowComponent, formatAgentFollowTranscript } from "./agent-follow-view";
|
|
4
|
+
import { formatAnimatedProgress } from "./renderers";
|
|
5
|
+
import type { RunningReadAgent } from "../runtime/types";
|
|
6
|
+
|
|
7
|
+
const ANSI_SGR_SEQUENCE = /\x1b\[[0-9;]*m/g;
|
|
8
|
+
|
|
9
|
+
function stripAnsi(text: string): string {
|
|
10
|
+
return text.replace(ANSI_SGR_SEQUENCE, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeAgent(overrides: Partial<RunningReadAgent> = {}): RunningReadAgent {
|
|
14
|
+
return {
|
|
15
|
+
runId: "run-1",
|
|
16
|
+
name: "reader",
|
|
17
|
+
teamName: "team",
|
|
18
|
+
startedAt: Date.now() - 60_000,
|
|
19
|
+
tokensUsed: 42,
|
|
20
|
+
contextUsage: { tokens: 42, contextWindow: 200_000, percent: 0.021 },
|
|
21
|
+
status: "thinking",
|
|
22
|
+
recentEvents: [],
|
|
23
|
+
lastActivityAt: Date.now(),
|
|
24
|
+
role: "read",
|
|
25
|
+
model: "provider/gpt-model",
|
|
26
|
+
thinking: "high",
|
|
27
|
+
modelSlot: "reading-default",
|
|
28
|
+
latestProgress: "Verifying assumptions",
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("animated agent progress", () => {
|
|
34
|
+
it("cycles one, two, and three dots without accumulating punctuation", () => {
|
|
35
|
+
expect(formatAnimatedProgress("Checking files...", 0)).toBe("Checking files.");
|
|
36
|
+
expect(formatAnimatedProgress("Checking files...", 1_000)).toBe("Checking files..");
|
|
37
|
+
expect(formatAnimatedProgress("Checking files...", 2_000)).toBe("Checking files...");
|
|
38
|
+
expect(formatAnimatedProgress("Checking files...", 3_000)).toBe("Checking files.");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("agent follow transcript", () => {
|
|
43
|
+
it("sanitizes hostile control sequences and newlines from tool names", () => {
|
|
44
|
+
const lines = formatAgentFollowTranscript([
|
|
45
|
+
{ role: "assistant", content: [
|
|
46
|
+
{ type: "toolCall", name: "read\u001b[2J-safe\nnext", arguments: {} },
|
|
47
|
+
] },
|
|
48
|
+
{ role: "toolResult", toolName: "bash\u001b]52;c;payload\u0007-safe\r\nnext", content: "ok" },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
expect(stripAnsi(lines[0] || "")).toContain("read-safe next");
|
|
52
|
+
expect(stripAnsi(lines[4] || "")).toContain("bash-safe next");
|
|
53
|
+
expect(lines.join("\n")).not.toContain("\u001b[2J");
|
|
54
|
+
expect(lines.join("\n")).not.toContain("\u001b]52");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders user, exposed thinking, assistant text, and paired tool activity blocks", () => {
|
|
58
|
+
const lines = formatAgentFollowTranscript([
|
|
59
|
+
{ role: "user", content: [{ type: "text", text: "Inspect the project" }] },
|
|
60
|
+
{ role: "assistant", content: [
|
|
61
|
+
{ type: "thinking", thinking: "I should inspect the files." },
|
|
62
|
+
{ type: "text", text: "I will inspect it." },
|
|
63
|
+
{ type: "toolCall", id: "call-1", name: "read", arguments: { path: "README.md" } },
|
|
64
|
+
] },
|
|
65
|
+
{ role: "toolResult", toolCallId: "call-1", toolName: "read", content: [{ type: "text", text: "Line one\nLine two" }] },
|
|
66
|
+
]).join("\n");
|
|
67
|
+
const plain = stripAnsi(lines);
|
|
68
|
+
|
|
69
|
+
expect(plain).toContain("Inspect the project");
|
|
70
|
+
expect(plain).toContain("I should inspect the files.");
|
|
71
|
+
expect(plain).toContain("I will inspect it.");
|
|
72
|
+
expect(plain.split("\n")).toContain("read · README.md");
|
|
73
|
+
expect(plain).not.toContain("╭─ read");
|
|
74
|
+
expect(plain).toContain("│ Line one\n│ Line two");
|
|
75
|
+
expect(plain).toContain("╰─ 2 lines");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps thinking multiline while removing Markdown bold markers", () => {
|
|
79
|
+
const lines = formatAgentFollowTranscript([
|
|
80
|
+
{ role: "assistant", content: [
|
|
81
|
+
{ type: "thinking", thinking: "**Planning RNG test isolation**\n\n**Designing deterministic tests**" },
|
|
82
|
+
] },
|
|
83
|
+
]).map(stripAnsi);
|
|
84
|
+
|
|
85
|
+
expect(lines).toEqual([
|
|
86
|
+
"thinking",
|
|
87
|
+
"Planning RNG test isolation\n\nDesigning deterministic tests",
|
|
88
|
+
"",
|
|
89
|
+
]);
|
|
90
|
+
expect(lines.join("\n")).not.toContain("**");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("collapses large tool results with head and tail context and can expand them", () => {
|
|
94
|
+
const output = Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n");
|
|
95
|
+
const messages = [
|
|
96
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "call-1", name: "bash", arguments: { command: "rg TODO src" } }] },
|
|
97
|
+
{ role: "toolResult", toolCallId: "call-1", toolName: "bash", content: output },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const collapsed = stripAnsi(formatAgentFollowTranscript(messages).join("\n"));
|
|
101
|
+
expect(collapsed.split("\n")).toContain("bash · $ rg TODO src");
|
|
102
|
+
expect(collapsed).not.toContain("╭─ bash");
|
|
103
|
+
expect(collapsed).toContain("9 lines hidden · press l to expand logs");
|
|
104
|
+
expect(collapsed).not.toContain("│ line 10");
|
|
105
|
+
expect(collapsed).toContain("│ line 20");
|
|
106
|
+
expect(collapsed).toContain("collapsed");
|
|
107
|
+
|
|
108
|
+
const expanded = stripAnsi(formatAgentFollowTranscript(messages, { expandLargeToolResults: true }).join("\n"));
|
|
109
|
+
expect(expanded).toContain("│ line 10");
|
|
110
|
+
expect(expanded).not.toContain("lines hidden");
|
|
111
|
+
expect(expanded).not.toContain("collapsed");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("renders report_progress calls as one ordinary neutral field", () => {
|
|
115
|
+
const lines = formatAgentFollowTranscript([
|
|
116
|
+
{ role: "assistant", content: [
|
|
117
|
+
{ type: "toolCall", id: "progress-1", name: "report_progress", arguments: { status: "Checking the follow renderer" } },
|
|
118
|
+
] },
|
|
119
|
+
{
|
|
120
|
+
role: "toolResult",
|
|
121
|
+
toolCallId: "progress-1",
|
|
122
|
+
toolName: "report_progress",
|
|
123
|
+
content: [{ type: "text", text: "Progress updated: Checking the follow renderer" }],
|
|
124
|
+
details: { session: "team", status: "Checking the follow renderer", updatedAt: 1 },
|
|
125
|
+
isError: false,
|
|
126
|
+
},
|
|
127
|
+
], { width: 80 });
|
|
128
|
+
const plain = lines.map(stripAnsi);
|
|
129
|
+
|
|
130
|
+
expect(plain[0]).toBe("Checking the follow renderer");
|
|
131
|
+
expect(plain.join("\n")).not.toContain("report_progress");
|
|
132
|
+
expect(lines[0]).not.toContain("\x1b[");
|
|
133
|
+
expect(lines.join("\n")).not.toContain("\x1b[48;2;31;33;47m");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("places the spacer after progress and immediately before a block tool", () => {
|
|
137
|
+
const lines = formatAgentFollowTranscript([
|
|
138
|
+
{ role: "assistant", content: [
|
|
139
|
+
{ type: "thinking", thinking: "**Planning the read**" },
|
|
140
|
+
{ type: "toolCall", id: "progress-1", name: "report_progress", arguments: { status: "Reading the requested line" } },
|
|
141
|
+
] },
|
|
142
|
+
{ role: "toolResult", toolCallId: "progress-1", toolName: "report_progress", content: "Progress updated", isError: false },
|
|
143
|
+
{ role: "assistant", content: [
|
|
144
|
+
{ type: "toolCall", id: "read-1", name: "read", arguments: { path: "README.md" } },
|
|
145
|
+
] },
|
|
146
|
+
{ role: "toolResult", toolCallId: "read-1", toolName: "read", content: "# pi-extended-teams", isError: false },
|
|
147
|
+
]).map(stripAnsi);
|
|
148
|
+
const progressIndex = lines.indexOf("Reading the requested line");
|
|
149
|
+
const readIndex = lines.indexOf("read · README.md");
|
|
150
|
+
|
|
151
|
+
expect(lines.slice(0, progressIndex)).toEqual(["thinking", "Planning the read", ""]);
|
|
152
|
+
expect(lines.slice(progressIndex, readIndex + 1)).toEqual([
|
|
153
|
+
"Reading the requested line",
|
|
154
|
+
"",
|
|
155
|
+
"read · README.md",
|
|
156
|
+
]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("renders successful and failed edits without source or result noise", () => {
|
|
160
|
+
const successful = formatAgentFollowTranscript([
|
|
161
|
+
{ role: "assistant", content: [
|
|
162
|
+
{
|
|
163
|
+
type: "toolCall",
|
|
164
|
+
id: "edit-1",
|
|
165
|
+
name: "edit",
|
|
166
|
+
arguments: { path: "src/app.ts", edits: [{ oldText: "private old source", newText: "private new source" }] },
|
|
167
|
+
},
|
|
168
|
+
] },
|
|
169
|
+
{
|
|
170
|
+
role: "toolResult",
|
|
171
|
+
toolCallId: "edit-1",
|
|
172
|
+
toolName: "edit",
|
|
173
|
+
content: [{ type: "text", text: "Successfully replaced 1 block(s) in src/app.ts." }],
|
|
174
|
+
details: { diff: " 1 context\n- 2 old line\n+ 2 new line\n+12 another line", firstChangedLine: 2 },
|
|
175
|
+
isError: false,
|
|
176
|
+
},
|
|
177
|
+
], { width: 100 });
|
|
178
|
+
|
|
179
|
+
expect(successful.map(stripAnsi)).toEqual(["edit · src/app.ts · +2 −1 · ✓"]);
|
|
180
|
+
expect(successful.join("\n")).not.toContain("private old source");
|
|
181
|
+
expect(successful.join("\n")).not.toContain("Successfully replaced");
|
|
182
|
+
|
|
183
|
+
const failed = formatAgentFollowTranscript([
|
|
184
|
+
{ role: "assistant", content: [
|
|
185
|
+
{ type: "toolCall", id: "edit-2", name: "edit", arguments: { path: "src/missing.ts", edits: [{ oldText: "secret", newText: "replacement" }] } },
|
|
186
|
+
] },
|
|
187
|
+
{
|
|
188
|
+
role: "toolResult",
|
|
189
|
+
toolCallId: "edit-2",
|
|
190
|
+
toolName: "edit",
|
|
191
|
+
content: [{ type: "text", text: "Could not find the exact text" }],
|
|
192
|
+
isError: true,
|
|
193
|
+
},
|
|
194
|
+
], { width: 100 });
|
|
195
|
+
|
|
196
|
+
expect(failed.map(stripAnsi)).toEqual(["edit · src/missing.ts · ✗"]);
|
|
197
|
+
expect(failed.join("\n")).not.toContain("Could not find");
|
|
198
|
+
expect(failed.join("\n")).not.toContain("secret");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("compacts write and final-report tools that otherwise repeat large arguments", () => {
|
|
202
|
+
const lines = formatAgentFollowTranscript([
|
|
203
|
+
{ role: "assistant", content: [
|
|
204
|
+
{ type: "toolCall", id: "write-1", name: "write", arguments: { path: "src/generated.ts", content: "large generated file contents" } },
|
|
205
|
+
{ type: "toolCall", id: "report-1", name: "report_and_exit", arguments: { content: "complete final report body", summary: "Done" } },
|
|
206
|
+
] },
|
|
207
|
+
{ role: "toolResult", toolCallId: "write-1", toolName: "write", content: "Wrote src/generated.ts", isError: false },
|
|
208
|
+
{
|
|
209
|
+
role: "toolResult",
|
|
210
|
+
toolCallId: "report-1",
|
|
211
|
+
toolName: "report_and_exit",
|
|
212
|
+
content: "A final report was already accepted for this run.",
|
|
213
|
+
details: { session: "team", accepted: false },
|
|
214
|
+
isError: false,
|
|
215
|
+
},
|
|
216
|
+
], { width: 100 });
|
|
217
|
+
|
|
218
|
+
expect(lines.map(stripAnsi)).toEqual([
|
|
219
|
+
"write · src/generated.ts · ✓",
|
|
220
|
+
"final report · duplicate",
|
|
221
|
+
]);
|
|
222
|
+
expect(lines.join("\n")).not.toContain("large generated file contents");
|
|
223
|
+
expect(lines.join("\n")).not.toContain("complete final report body");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("uses semantic chrome colors while leaving prose and raw output neutral", () => {
|
|
227
|
+
const lines = formatAgentFollowTranscript([
|
|
228
|
+
{ role: "assistant", content: [
|
|
229
|
+
{ type: "thinking", thinking: "Neutral thinking prose" },
|
|
230
|
+
{ type: "text", text: "Neutral assistant prose" },
|
|
231
|
+
{ type: "toolCall", id: "read-pending", name: "read", arguments: { path: "src/pending.ts" } },
|
|
232
|
+
{ type: "toolCall", id: "edit-success", name: "edit", arguments: { path: "src/succeeded.ts", edits: [] } },
|
|
233
|
+
{ type: "toolCall", id: "edit-failure", name: "edit", arguments: { path: "src/failed.ts", edits: [] } },
|
|
234
|
+
{ type: "toolCall", id: "bash-raw", name: "bash", arguments: { command: "printf output" } },
|
|
235
|
+
] },
|
|
236
|
+
{
|
|
237
|
+
role: "toolResult",
|
|
238
|
+
toolCallId: "edit-success",
|
|
239
|
+
toolName: "edit",
|
|
240
|
+
content: "Successfully replaced source",
|
|
241
|
+
details: { diff: "- 2 old\n+ 2 new\n+12 added" },
|
|
242
|
+
isError: false,
|
|
243
|
+
},
|
|
244
|
+
{ role: "toolResult", toolCallId: "edit-failure", toolName: "edit", content: "Edit failed", isError: true },
|
|
245
|
+
{ role: "toolResult", toolCallId: "bash-raw", toolName: "bash", content: "neutral shell output", isError: false },
|
|
246
|
+
], { width: 80 });
|
|
247
|
+
|
|
248
|
+
const pendingHeader = lines.find((line) => stripAnsi(line).startsWith("read")) || "";
|
|
249
|
+
const pendingState = lines.find((line) => stripAnsi(line).includes("waiting for result")) || "";
|
|
250
|
+
const successfulEdit = lines.find((line) => stripAnsi(line).includes("src/succeeded.ts")) || "";
|
|
251
|
+
const failedEdit = lines.find((line) => stripAnsi(line).includes("src/failed.ts")) || "";
|
|
252
|
+
const rawOutput = lines.find((line) => stripAnsi(line) === "│ neutral shell output") || "";
|
|
253
|
+
|
|
254
|
+
expect(pendingHeader).toContain("\x1b[38;5;117mread\x1b[39m");
|
|
255
|
+
expect(pendingHeader).toContain("\x1b[38;5;213msrc/pending.ts\x1b[39m");
|
|
256
|
+
expect(pendingState).toContain("\x1b[38;5;222mwaiting for result…\x1b[39m");
|
|
257
|
+
expect(successfulEdit).toContain("\x1b[38;5;114m+2\x1b[39m");
|
|
258
|
+
expect(successfulEdit).toContain("\x1b[38;5;210m−1\x1b[39m");
|
|
259
|
+
expect(successfulEdit).toContain("\x1b[38;5;114m✓\x1b[39m");
|
|
260
|
+
expect(failedEdit).toContain("\x1b[38;5;210m✗\x1b[39m");
|
|
261
|
+
expect(rawOutput).toBe("\x1b[38;5;141m│\x1b[39m neutral shell output");
|
|
262
|
+
expect(lines).toContain("Neutral thinking prose");
|
|
263
|
+
expect(lines).toContain("Neutral assistant prose");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("keeps claim and release tools compact and semantically colored", () => {
|
|
267
|
+
const lines = formatAgentFollowTranscript([
|
|
268
|
+
{ role: "assistant", content: [
|
|
269
|
+
{ type: "toolCall", id: "claim-1", name: "claim_file", arguments: { paths: ["src/a.ts", "src/b.ts"] } },
|
|
270
|
+
{ type: "toolCall", id: "release-1", name: "release_file", arguments: { paths: ["src/a.ts"] } },
|
|
271
|
+
] },
|
|
272
|
+
{
|
|
273
|
+
role: "toolResult",
|
|
274
|
+
toolCallId: "claim-1",
|
|
275
|
+
toolName: "claim_file",
|
|
276
|
+
content: "Claimed 2 file(s)",
|
|
277
|
+
details: { granted: ["src/a.ts", "src/b.ts"], conflicts: [] },
|
|
278
|
+
isError: false,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
role: "toolResult",
|
|
282
|
+
toolCallId: "release-1",
|
|
283
|
+
toolName: "release_file",
|
|
284
|
+
content: "Released 1 file claim(s)",
|
|
285
|
+
details: { released: ["src/a.ts"] },
|
|
286
|
+
isError: false,
|
|
287
|
+
},
|
|
288
|
+
], { width: 80 });
|
|
289
|
+
|
|
290
|
+
expect(lines.map(stripAnsi)).toEqual([
|
|
291
|
+
"claim · src/a.ts, src/b.ts · ✓",
|
|
292
|
+
"release · src/a.ts · ✓",
|
|
293
|
+
]);
|
|
294
|
+
expect(lines[0]).toContain("\x1b[38;5;117mclaim\x1b[39m");
|
|
295
|
+
expect(lines[0]).toContain("\x1b[38;5;213msrc/a.ts, src/b.ts\x1b[39m");
|
|
296
|
+
expect(lines[1]).toContain("\x1b[38;5;114m✓\x1b[39m");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it.each([
|
|
300
|
+
["1,000-character", "x".repeat(1_000)],
|
|
301
|
+
["roughly 50 KiB", "x".repeat(50 * 1_024)],
|
|
302
|
+
])("bounds a %s single result line in collapsed and expanded modes", (_label, output) => {
|
|
303
|
+
const width = 42;
|
|
304
|
+
const messages = [
|
|
305
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "long-result", name: "bash", arguments: { command: "printf output" } }] },
|
|
306
|
+
{ role: "toolResult", toolCallId: "long-result", toolName: "bash", content: output, isError: false },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
for (const expandLargeToolResults of [false, true]) {
|
|
310
|
+
const lines = formatAgentFollowTranscript(messages, { expandLargeToolResults, width });
|
|
311
|
+
const body = lines.find((line) => stripAnsi(line).startsWith("│ ")) || "";
|
|
312
|
+
const wrapped = lines.flatMap((line) => wrapTextWithAnsi(line, width));
|
|
313
|
+
|
|
314
|
+
expect(lines).toHaveLength(4);
|
|
315
|
+
expect(wrapped).toHaveLength(4);
|
|
316
|
+
expect(lines.every((line) => visibleWidth(line) <= width)).toBe(true);
|
|
317
|
+
expect(visibleWidth(body)).toBe(width);
|
|
318
|
+
expect(stripAnsi(body)).toMatch(/^│ x+…$/);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("truncates ANSI-colored long result lines without width or control-sequence leakage", () => {
|
|
323
|
+
const width = 31;
|
|
324
|
+
const output = `\x1b[31m${"彩".repeat(1_000)}\x1b[0m`;
|
|
325
|
+
const lines = formatAgentFollowTranscript([
|
|
326
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "ansi-result", name: "bash", arguments: { command: "printf color" } }] },
|
|
327
|
+
{ role: "toolResult", toolCallId: "ansi-result", toolName: "bash", content: output, isError: false },
|
|
328
|
+
], { width });
|
|
329
|
+
const body = lines.find((line) => stripAnsi(line).startsWith("│ ")) || "";
|
|
330
|
+
|
|
331
|
+
expect(visibleWidth(body)).toBeLessThanOrEqual(width);
|
|
332
|
+
expect(wrapTextWithAnsi(body, width)).toHaveLength(1);
|
|
333
|
+
expect(body).toContain("\x1b[31m");
|
|
334
|
+
expect(body).toMatch(/\x1b\[0m…\x1b\[0m$/);
|
|
335
|
+
expect(stripAnsi(body)).toContain("…");
|
|
336
|
+
expect(stripAnsi(body)).not.toContain("\x1b");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe("agent follow component", () => {
|
|
341
|
+
beforeEach(() => vi.useFakeTimers());
|
|
342
|
+
afterEach(() => vi.useRealTimers());
|
|
343
|
+
|
|
344
|
+
it("starts at zero instead of showing the pre-response message estimate", () => {
|
|
345
|
+
const tui = { terminal: { rows: 24 }, requestRender: vi.fn() };
|
|
346
|
+
const agent = makeAgent({
|
|
347
|
+
contextUsage: undefined,
|
|
348
|
+
session: {
|
|
349
|
+
messages: [{ role: "user", content: [{ type: "text", text: "A substantial agent mission" }] }],
|
|
350
|
+
getSessionStats: () => ({
|
|
351
|
+
tokens: { total: 0 },
|
|
352
|
+
contextUsage: { tokens: 597, contextWindow: 200_000, percent: 0.2985 },
|
|
353
|
+
}),
|
|
354
|
+
} as any,
|
|
355
|
+
});
|
|
356
|
+
const component = createAgentFollowComponent(tui, vi.fn(), { getAgents: () => [agent] });
|
|
357
|
+
|
|
358
|
+
const rendered = stripAnsi(component.render(120).join("\n"));
|
|
359
|
+
expect(rendered).toContain("0 tok (0%)");
|
|
360
|
+
expect(rendered).not.toContain("597 tok");
|
|
361
|
+
component.dispose();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("renders current context usage instead of cumulative billed tokens", () => {
|
|
365
|
+
let billedTokens = 502_000;
|
|
366
|
+
let contextTokens = 46_000;
|
|
367
|
+
let contextPercent = 23;
|
|
368
|
+
const done = vi.fn();
|
|
369
|
+
const tui = { terminal: { rows: 30 }, requestRender: vi.fn() };
|
|
370
|
+
const agent = makeAgent({
|
|
371
|
+
session: {
|
|
372
|
+
messages: [{ role: "assistant", content: [{ type: "text", text: "Working now" }] }],
|
|
373
|
+
getSessionStats: () => ({
|
|
374
|
+
tokens: { total: billedTokens },
|
|
375
|
+
contextUsage: { tokens: contextTokens, contextWindow: 200_000, percent: contextPercent },
|
|
376
|
+
}),
|
|
377
|
+
} as any,
|
|
378
|
+
});
|
|
379
|
+
const component = createAgentFollowComponent(tui, done, { getAgents: () => [agent] });
|
|
380
|
+
|
|
381
|
+
const first = component.render(140).join("\n");
|
|
382
|
+
expect(first).toMatch(/\(reader\) gpt-model\/high · reading-default · 1m00s · 46k tok \(23%\) · Verifying assumptions\.{1,3}/);
|
|
383
|
+
expect(first).not.toContain("502k tok");
|
|
384
|
+
expect(first).toContain("Working now");
|
|
385
|
+
expect(stripAnsi(first)).not.toContain("progress:");
|
|
386
|
+
expect(first).toContain("\x1b[48;2;22;23;32m");
|
|
387
|
+
expect(first).not.toContain("\x1b[48;5;235m");
|
|
388
|
+
|
|
389
|
+
billedTokens = 2_300_000;
|
|
390
|
+
contextTokens = 80_000;
|
|
391
|
+
contextPercent = 40;
|
|
392
|
+
agent.latestProgress = "Writing final report";
|
|
393
|
+
const updated = component.render(140).join("\n");
|
|
394
|
+
expect(updated).toMatch(/80k tok \(40%\) · Writing final report\.{1,3}/);
|
|
395
|
+
expect(updated).not.toContain("2.3M tok");
|
|
396
|
+
expect(stripAnsi(updated)).not.toContain("progress:");
|
|
397
|
+
|
|
398
|
+
component.handleInput("\x1b[A");
|
|
399
|
+
expect(done).toHaveBeenCalledOnce();
|
|
400
|
+
component.dispose();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("uses the ordinary agent status when no stored progress is present", () => {
|
|
404
|
+
const tui = { terminal: { rows: 20 }, requestRender: vi.fn() };
|
|
405
|
+
const agent = makeAgent({ latestProgress: undefined, status: "working" });
|
|
406
|
+
const component = createAgentFollowComponent(tui, vi.fn(), { getAgents: () => [agent] });
|
|
407
|
+
|
|
408
|
+
const rendered = stripAnsi(component.render(80).join("\n"));
|
|
409
|
+
expect(rendered).toContain("42 tok (0%) · working");
|
|
410
|
+
expect(rendered).not.toContain("progress:");
|
|
411
|
+
component.dispose();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("shows context usage as unknown immediately after compaction", () => {
|
|
415
|
+
const tui = { terminal: { rows: 20 }, requestRender: vi.fn() };
|
|
416
|
+
const agent = makeAgent({
|
|
417
|
+
contextUsage: { tokens: null, contextWindow: 200_000, percent: null },
|
|
418
|
+
latestProgress: undefined,
|
|
419
|
+
status: "thinking",
|
|
420
|
+
});
|
|
421
|
+
const component = createAgentFollowComponent(tui, vi.fn(), { getAgents: () => [agent] });
|
|
422
|
+
|
|
423
|
+
expect(stripAnsi(component.render(100).join("\n"))).toContain("? tok (?%) · thinking");
|
|
424
|
+
component.dispose();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("stops the selected agent with x and keeps the live view open", () => {
|
|
428
|
+
const tui = { terminal: { rows: 24 }, requestRender: vi.fn() };
|
|
429
|
+
const agents = [makeAgent({ name: "alpha" }), makeAgent({ name: "beta" })];
|
|
430
|
+
const stopAgent = vi.fn((name: string) => {
|
|
431
|
+
const index = agents.findIndex(agent => agent.name === name);
|
|
432
|
+
if (index >= 0) agents.splice(index, 1);
|
|
433
|
+
});
|
|
434
|
+
const component = createAgentFollowComponent(tui, vi.fn(), {
|
|
435
|
+
getAgents: () => agents,
|
|
436
|
+
initialAgentName: "beta",
|
|
437
|
+
stopAgent,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(component.render(120).join("\n")).toContain("(beta)");
|
|
441
|
+
component.handleInput("x");
|
|
442
|
+
|
|
443
|
+
expect(stopAgent).toHaveBeenCalledWith("beta");
|
|
444
|
+
expect(component.render(120).join("\n")).toContain("(alpha)");
|
|
445
|
+
component.dispose();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("shows a direct-message input and sends to the selected agent", async () => {
|
|
449
|
+
const tui = { terminal: { rows: 30 }, requestRender: vi.fn() };
|
|
450
|
+
const sendMessage = vi.fn(async () => {});
|
|
451
|
+
const component = createAgentFollowComponent(tui, vi.fn(), {
|
|
452
|
+
getAgents: () => [makeAgent()],
|
|
453
|
+
sendMessage,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(component.render(120).join("\n")).toContain("message reader");
|
|
457
|
+
expect(component.render(120).join("\n")).toContain("Press m to start typing");
|
|
458
|
+
|
|
459
|
+
component.focused = true;
|
|
460
|
+
component.handleInput("m");
|
|
461
|
+
expect(component.render(120).join("\n")).toContain("enter send · esc cancel");
|
|
462
|
+
component.handleInput("Please inspect the failing test");
|
|
463
|
+
component.handleInput("\r");
|
|
464
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
465
|
+
|
|
466
|
+
expect(sendMessage).toHaveBeenCalledWith("reader", "Please inspect the failing test");
|
|
467
|
+
expect(component.render(120).join("\n")).toContain("Message sent to reader.");
|
|
468
|
+
expect(component.render(120).join("\n")).toContain("Press m to start typing");
|
|
469
|
+
component.dispose();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("keeps the message draft open when delivery fails", async () => {
|
|
473
|
+
const tui = { terminal: { rows: 30 }, requestRender: vi.fn() };
|
|
474
|
+
const component = createAgentFollowComponent(tui, vi.fn(), {
|
|
475
|
+
getAgents: () => [makeAgent()],
|
|
476
|
+
sendMessage: async () => { throw new Error("Cannot send message to reader: agent is not running."); },
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
component.handleInput("m");
|
|
480
|
+
component.handleInput("Are you still there?");
|
|
481
|
+
component.handleInput("\r");
|
|
482
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
483
|
+
|
|
484
|
+
const rendered = component.render(120).join("\n");
|
|
485
|
+
expect(rendered).toContain("Cannot send message to reader: agent is not running.");
|
|
486
|
+
expect(rendered).toContain("Are you still there?");
|
|
487
|
+
component.dispose();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("navigates up to the previous agent before returning to main", () => {
|
|
491
|
+
const done = vi.fn();
|
|
492
|
+
const tui = { terminal: { rows: 24 }, requestRender: vi.fn() };
|
|
493
|
+
const first = makeAgent({ name: "alpha" });
|
|
494
|
+
const second = makeAgent({ name: "beta", modelSlot: "reading-hard" });
|
|
495
|
+
const component = createAgentFollowComponent(tui, done, { getAgents: () => [first, second], initialAgentName: "alpha" });
|
|
496
|
+
|
|
497
|
+
const initial = component.render(120).join("\n");
|
|
498
|
+
expect(initial).toContain("↑ main agent");
|
|
499
|
+
expect(initial).toContain("->");
|
|
500
|
+
expect(initial).toContain("alpha");
|
|
501
|
+
expect(initial).not.toContain("you are here");
|
|
502
|
+
|
|
503
|
+
component.handleInput("\x1b[B");
|
|
504
|
+
const next = component.render(120).join("\n");
|
|
505
|
+
expect(next).toContain("->");
|
|
506
|
+
expect(next).toContain("beta");
|
|
507
|
+
expect(next).toContain("(beta)");
|
|
508
|
+
|
|
509
|
+
component.handleInput("\x1b[A");
|
|
510
|
+
expect(component.render(120).join("\n")).toContain("(alpha)");
|
|
511
|
+
expect(done).not.toHaveBeenCalled();
|
|
512
|
+
|
|
513
|
+
component.handleInput("\x1b[A");
|
|
514
|
+
expect(done).toHaveBeenCalledOnce();
|
|
515
|
+
|
|
516
|
+
vi.advanceTimersByTime(250);
|
|
517
|
+
expect(tui.requestRender).toHaveBeenCalled();
|
|
518
|
+
component.dispose();
|
|
519
|
+
});
|
|
520
|
+
});
|