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,779 @@
|
|
|
1
|
+
import { Input, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { RunningReadAgent } from "../runtime/types";
|
|
3
|
+
import { initialContextUsage } from "../../src/utils/runtime";
|
|
4
|
+
import { dimAnsi, pink, purple } from "./ansi";
|
|
5
|
+
import { createFramePanelRowRenderer, framePanel } from "./frame";
|
|
6
|
+
import { extractTextParts, formatAnimatedProgress, formatContextUsage, formatElapsed, formatModelLabel, sanitizePlainTuiLine, sanitizeTuiLine, sanitizeTuiText } from "./renderers";
|
|
7
|
+
|
|
8
|
+
const REFRESH_INTERVAL_MS = 250;
|
|
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
|
+
const COLLAPSED_TOOL_RESULT_LINE_LIMIT = 14;
|
|
22
|
+
const COLLAPSED_TOOL_RESULT_HEAD_LINES = 8;
|
|
23
|
+
const COLLAPSED_TOOL_RESULT_TAIL_LINES = 3;
|
|
24
|
+
|
|
25
|
+
export interface AgentFollowViewOptions {
|
|
26
|
+
getAgents(): RunningReadAgent[];
|
|
27
|
+
initialAgentName?: string;
|
|
28
|
+
stopAgent?(name: string): void | Promise<void>;
|
|
29
|
+
sendMessage?(name: string, content: string): void | Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AgentFollowTranscriptOptions {
|
|
33
|
+
expandLargeToolResults?: boolean;
|
|
34
|
+
width?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TranscriptBlock =
|
|
38
|
+
| { kind: "section"; label: "user" | "thinking" | "assistant"; text: string }
|
|
39
|
+
| { kind: "tool"; id?: string; name: string; args: unknown; result?: string; details?: unknown; isError?: boolean };
|
|
40
|
+
|
|
41
|
+
function stringifyToolArgs(args: unknown): string {
|
|
42
|
+
if (args === undefined) return "";
|
|
43
|
+
try {
|
|
44
|
+
return JSON.stringify(args, null, 2);
|
|
45
|
+
} catch {
|
|
46
|
+
return String(args);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compactToolArgs(name: string, args: unknown): string {
|
|
51
|
+
if (!args || typeof args !== "object") return sanitizeTuiLine(stringifyToolArgs(args));
|
|
52
|
+
const values = args as Record<string, unknown>;
|
|
53
|
+
const primary = name === "bash"
|
|
54
|
+
? values.command
|
|
55
|
+
: name === "read"
|
|
56
|
+
? values.path
|
|
57
|
+
: name === "agentic_search"
|
|
58
|
+
? values.query
|
|
59
|
+
: undefined;
|
|
60
|
+
const raw = primary === undefined ? JSON.stringify(values) : String(primary);
|
|
61
|
+
const compact = sanitizeTuiLine(raw).replace(/\s+/g, " ").trim();
|
|
62
|
+
return compact.length > 240 ? `${compact.slice(0, 239)}…` : compact;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatResultSize(text: string): string {
|
|
66
|
+
const size = Buffer.byteLength(text, "utf8");
|
|
67
|
+
if (size < 1_024) return `${size} B`;
|
|
68
|
+
return `${(size / 1_024).toFixed(size < 10_240 ? 1 : 0)} KB`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
72
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
73
|
+
? value as Record<string, unknown>
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compactTranscriptLine(text: string): string {
|
|
78
|
+
return sanitizePlainTuiLine(text).replace(/\s+/g, " ").trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function semanticAnsi(text: string, foreground: string): string {
|
|
82
|
+
return `${foreground}${text}${ANSI_FOREGROUND_RESET}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function actionAnsi(text: string): string {
|
|
86
|
+
return semanticAnsi(text, ACTION_FOREGROUND);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pathAnsi(text: string): string {
|
|
90
|
+
return semanticAnsi(text, PATH_FOREGROUND);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function successAnsi(text: string): string {
|
|
94
|
+
return semanticAnsi(text, SUCCESS_FOREGROUND);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function failureAnsi(text: string): string {
|
|
98
|
+
return semanticAnsi(text, FAILURE_FOREGROUND);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pendingAnsi(text: string): string {
|
|
102
|
+
return semanticAnsi(text, PENDING_FOREGROUND);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function bodyAnsi(text: string): string {
|
|
106
|
+
return semanticAnsi(text, BODY_FOREGROUND);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mutedAnsi(text: string): string {
|
|
110
|
+
return semanticAnsi(text, MUTED_FOREGROUND);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function structuralAnsi(text: string): string {
|
|
114
|
+
return semanticAnsi(text, STRUCTURAL_FOREGROUND);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function boundTranscriptLine(line: string, width?: number): string {
|
|
118
|
+
return width === undefined ? line : truncateToWidth(line, Math.max(1, width), "…");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toolPath(args: unknown): string {
|
|
122
|
+
const path = asRecord(args)?.path;
|
|
123
|
+
return compactTranscriptLine(typeof path === "string" ? path : "(unknown file)");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toolPaths(args: unknown): string {
|
|
127
|
+
const paths = asRecord(args)?.paths;
|
|
128
|
+
if (!Array.isArray(paths)) return toolPath(args);
|
|
129
|
+
const compactPaths = paths
|
|
130
|
+
.filter((path): path is string => typeof path === "string")
|
|
131
|
+
.map(compactTranscriptLine)
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
return compactPaths.length > 0 ? compactPaths.join(", ") : "(unknown file)";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function editDiffCounts(details: unknown): { added: number; removed: number } | undefined {
|
|
137
|
+
const diff = asRecord(details)?.diff;
|
|
138
|
+
if (typeof diff !== "string") return undefined;
|
|
139
|
+
|
|
140
|
+
let added = 0;
|
|
141
|
+
let removed = 0;
|
|
142
|
+
for (const line of diff.split("\n")) {
|
|
143
|
+
if (/^\+\s*\d+(?:\s|$)/.test(line)) added += 1;
|
|
144
|
+
else if (/^-\s*\d+(?:\s|$)/.test(line)) removed += 1;
|
|
145
|
+
}
|
|
146
|
+
return { added, removed };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderState(state: string): string {
|
|
150
|
+
if (state === "failed") return failureAnsi(state);
|
|
151
|
+
if (state === "working" || state === "submitting") return pendingAnsi(state);
|
|
152
|
+
if (state === "duplicate") return mutedAnsi(state);
|
|
153
|
+
return successAnsi(state);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderActionPathState(action: string, path: string, state: string, width?: number): string {
|
|
157
|
+
const renderedState = state === "failed"
|
|
158
|
+
? failureAnsi("✗")
|
|
159
|
+
: state === "succeeded"
|
|
160
|
+
? successAnsi("✓")
|
|
161
|
+
: renderState(state);
|
|
162
|
+
return boundTranscriptLine(
|
|
163
|
+
`${actionAnsi(action)}${mutedAnsi(" · ")}${pathAnsi(path)}${mutedAnsi(" · ")}${renderedState}`,
|
|
164
|
+
width,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderCompactToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }>, width?: number): string[] | undefined {
|
|
169
|
+
if (block.name === "report_progress") {
|
|
170
|
+
const details = asRecord(block.details);
|
|
171
|
+
const args = asRecord(block.args);
|
|
172
|
+
const resultStatus = block.result?.replace(/^Progress (?:updated|update skipped):\s*/i, "");
|
|
173
|
+
const rawStatus = details?.status ?? args?.status ?? resultStatus ?? "updating";
|
|
174
|
+
const status = compactTranscriptLine(String(rawStatus)) || "updating";
|
|
175
|
+
const failureSuffix = block.isError ? " (failed)" : "";
|
|
176
|
+
return [boundTranscriptLine(`${status}${failureSuffix}`, width), ""];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (block.name === "edit") {
|
|
180
|
+
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)];
|
|
183
|
+
const counts = editDiffCounts(block.details);
|
|
184
|
+
const added = counts ? `+${counts.added}` : "+?";
|
|
185
|
+
const removed = counts ? `−${counts.removed}` : "−?";
|
|
186
|
+
return [boundTranscriptLine(
|
|
187
|
+
`${actionAnsi("edit")}${mutedAnsi(" · ")}${pathAnsi(path)}${mutedAnsi(" · ")}${successAnsi(added)} ${failureAnsi(removed)}${mutedAnsi(" · ")}${successAnsi("✓")}`,
|
|
188
|
+
width,
|
|
189
|
+
)];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (block.name === "write") {
|
|
193
|
+
const state = block.result === undefined ? "working" : block.isError ? "failed" : "succeeded";
|
|
194
|
+
return [renderActionPathState("write", toolPath(block.args), state, width)];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (block.name === "claim_file" || block.name === "release_file") {
|
|
198
|
+
const details = asRecord(block.details);
|
|
199
|
+
const conflicts = Array.isArray(details?.conflicts) ? details.conflicts : [];
|
|
200
|
+
const state = block.result === undefined
|
|
201
|
+
? "working"
|
|
202
|
+
: block.isError || conflicts.length > 0
|
|
203
|
+
? "failed"
|
|
204
|
+
: "succeeded";
|
|
205
|
+
const action = block.name === "claim_file" ? "claim" : "release";
|
|
206
|
+
return [renderActionPathState(action, toolPaths(block.args), state, width)];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (block.name === "report_and_exit") {
|
|
210
|
+
const accepted = asRecord(block.details)?.accepted;
|
|
211
|
+
const state = block.result === undefined
|
|
212
|
+
? "submitting"
|
|
213
|
+
: block.isError
|
|
214
|
+
? "failed"
|
|
215
|
+
: accepted === false
|
|
216
|
+
? "duplicate"
|
|
217
|
+
: "accepted";
|
|
218
|
+
return [boundTranscriptLine(`${actionAnsi("final report")}${mutedAnsi(" · ")}${renderState(state)}`, width)];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function renderToolHeader(block: Extract<TranscriptBlock, { kind: "tool" }>): string {
|
|
225
|
+
const detail = compactToolArgs(block.name, block.args);
|
|
226
|
+
if (!detail) return actionAnsi(block.name);
|
|
227
|
+
const isPath = typeof asRecord(block.args)?.path === "string";
|
|
228
|
+
const renderedDetail = isPath ? pathAnsi(detail) : bodyAnsi(`${block.name === "bash" ? "$ " : ""}${detail}`);
|
|
229
|
+
return `${actionAnsi(block.name)}${mutedAnsi(" · ")}${renderedDetail}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderToolBlock(block: Extract<TranscriptBlock, { kind: "tool" }>, expandLargeToolResults: boolean, width?: number): string[] {
|
|
233
|
+
const compactBlock = renderCompactToolBlock(block, width);
|
|
234
|
+
if (compactBlock) return compactBlock;
|
|
235
|
+
|
|
236
|
+
const header = renderToolHeader(block);
|
|
237
|
+
if (block.result === undefined) {
|
|
238
|
+
return [header, `${structuralAnsi("│")} ${pendingAnsi("waiting for result…")}`, `${structuralAnsi("╰─")} ${pendingAnsi("running")}`, ""];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const result = block.result || "(no output)";
|
|
242
|
+
const resultLines = result.split("\n");
|
|
243
|
+
const isCollapsed = !expandLargeToolResults && resultLines.length > COLLAPSED_TOOL_RESULT_LINE_LIMIT;
|
|
244
|
+
const visibleLines = isCollapsed
|
|
245
|
+
? [
|
|
246
|
+
...resultLines.slice(0, COLLAPSED_TOOL_RESULT_HEAD_LINES),
|
|
247
|
+
`… ${resultLines.length - COLLAPSED_TOOL_RESULT_HEAD_LINES - COLLAPSED_TOOL_RESULT_TAIL_LINES} lines hidden · press l to expand logs`,
|
|
248
|
+
...resultLines.slice(-COLLAPSED_TOOL_RESULT_TAIL_LINES),
|
|
249
|
+
]
|
|
250
|
+
: resultLines;
|
|
251
|
+
const resultLineWidth = width === undefined ? undefined : Math.max(1, width - visibleWidth("│ "));
|
|
252
|
+
const boundedLines = visibleLines.map((line) => resultLineWidth === undefined
|
|
253
|
+
? line
|
|
254
|
+
: truncateToWidth(line, resultLineWidth, "…"));
|
|
255
|
+
const body = boundedLines.map((line, index) => isCollapsed && index === COLLAPSED_TOOL_RESULT_HEAD_LINES
|
|
256
|
+
? `${structuralAnsi("│")} ${mutedAnsi(line)}`
|
|
257
|
+
: `${structuralAnsi("│")} ${line}`);
|
|
258
|
+
const summary = `${resultLines.length} line${resultLines.length === 1 ? "" : "s"} · ${formatResultSize(result)}${isCollapsed ? " · collapsed" : ""}`;
|
|
259
|
+
const renderedSummary = block.isError ? failureAnsi(summary) : successAnsi(summary);
|
|
260
|
+
return [header, ...body, `${structuralAnsi("╰─")} ${renderedSummary}`, ""];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function formatAgentFollowTranscript(messages: any[], options: AgentFollowTranscriptOptions = {}): string[] {
|
|
264
|
+
const blocks: TranscriptBlock[] = [];
|
|
265
|
+
const toolsById = new Map<string, Extract<TranscriptBlock, { kind: "tool" }>>();
|
|
266
|
+
|
|
267
|
+
for (const message of messages || []) {
|
|
268
|
+
if (message?.role === "user") {
|
|
269
|
+
const text = sanitizeTuiText(extractTextParts(message.content));
|
|
270
|
+
if (text) blocks.push({ kind: "section", label: "user", text });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (message?.role === "assistant") {
|
|
275
|
+
for (const part of Array.isArray(message.content) ? message.content : []) {
|
|
276
|
+
if (part?.type === "thinking" && typeof part.thinking === "string" && part.thinking.trim()) {
|
|
277
|
+
blocks.push({ kind: "section", label: "thinking", text: sanitizeTuiText(part.thinking.trim()) });
|
|
278
|
+
} else if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
279
|
+
blocks.push({ kind: "section", label: "assistant", text: sanitizeTuiText(part.text.trim()) });
|
|
280
|
+
} else if (part?.type === "toolCall") {
|
|
281
|
+
const id = typeof part.id === "string" ? part.id : typeof part.toolCallId === "string" ? part.toolCallId : undefined;
|
|
282
|
+
const tool: Extract<TranscriptBlock, { kind: "tool" }> = {
|
|
283
|
+
kind: "tool",
|
|
284
|
+
id,
|
|
285
|
+
name: sanitizeTuiLine(String(part.name || "unknown")),
|
|
286
|
+
args: part.arguments ?? part.args,
|
|
287
|
+
};
|
|
288
|
+
blocks.push(tool);
|
|
289
|
+
if (id) toolsById.set(id, tool);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (message?.role === "toolResult") {
|
|
296
|
+
const id = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
297
|
+
const name = sanitizeTuiLine(String(message.toolName || "tool"));
|
|
298
|
+
const matchingTool = (id ? toolsById.get(id) : undefined)
|
|
299
|
+
?? blocks.slice().reverse().find((block): block is Extract<TranscriptBlock, { kind: "tool" }> => block.kind === "tool" && block.result === undefined && block.name === name);
|
|
300
|
+
const result = sanitizeTuiText(extractTextParts(message.content));
|
|
301
|
+
const isError = typeof message.isError === "boolean" ? message.isError : undefined;
|
|
302
|
+
if (matchingTool) {
|
|
303
|
+
matchingTool.result = result;
|
|
304
|
+
matchingTool.details = message.details;
|
|
305
|
+
matchingTool.isError = isError;
|
|
306
|
+
} else {
|
|
307
|
+
blocks.push({ kind: "tool", id, name, args: undefined, result, details: message.details, isError });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const lines = blocks.flatMap(block => {
|
|
313
|
+
if (block.kind === "tool") {
|
|
314
|
+
return renderToolBlock(block, options.expandLargeToolResults === true, options.width);
|
|
315
|
+
}
|
|
316
|
+
if (block.label === "thinking") {
|
|
317
|
+
return [purple(block.label), block.text.replace(/\*\*/g, ""), ""];
|
|
318
|
+
}
|
|
319
|
+
return [block.label === "user" ? pink(block.label) : purple(block.label), block.text, ""];
|
|
320
|
+
});
|
|
321
|
+
return lines.length > 0 ? lines : [dimAnsi("Waiting for the agent's first transcript event…")];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function currentAgent(agents: RunningReadAgent[], selectedName: string | undefined): RunningReadAgent | undefined {
|
|
325
|
+
return agents.find(agent => agent.name === selectedName) ?? agents[0];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function createAgentFollowComponent(
|
|
329
|
+
tui: any,
|
|
330
|
+
done: () => void,
|
|
331
|
+
options: AgentFollowViewOptions
|
|
332
|
+
) {
|
|
333
|
+
let selectedName = options.initialAgentName;
|
|
334
|
+
let offsetFromBottom = 0;
|
|
335
|
+
let lastBodyHeight = 10;
|
|
336
|
+
let lastTranscriptRows = 0;
|
|
337
|
+
let expandLargeToolResults = false;
|
|
338
|
+
let composingMessage = false;
|
|
339
|
+
let sendingMessage = false;
|
|
340
|
+
let messageStatus = "";
|
|
341
|
+
let focused = false;
|
|
342
|
+
const messageInput = new Input();
|
|
343
|
+
const stoppingAgents = new Set<string>();
|
|
344
|
+
let transcriptAgent: RunningReadAgent | undefined;
|
|
345
|
+
let transcriptMessages: unknown[] | undefined;
|
|
346
|
+
let transcriptMessageCount = -1;
|
|
347
|
+
let transcriptLastMessage: unknown;
|
|
348
|
+
let transcriptWidth = -1;
|
|
349
|
+
let transcriptExpanded = false;
|
|
350
|
+
let cachedTranscriptLines: string[] = [];
|
|
351
|
+
let cachedFrameWidth = -1;
|
|
352
|
+
let cachedFrameRowRenderer: ((line: string) => string) | undefined;
|
|
353
|
+
let cachedFrameContent: string[] | null = null;
|
|
354
|
+
let cachedFrameLines: string[] | null = null;
|
|
355
|
+
let fastFrameKey = "";
|
|
356
|
+
let fastFrameMessages: unknown[] | undefined;
|
|
357
|
+
let fastFrameMessageCount = -1;
|
|
358
|
+
let fastFrameLastMessage: unknown;
|
|
359
|
+
let refreshStateKey = "";
|
|
360
|
+
let refreshMessages: unknown[] | undefined;
|
|
361
|
+
let refreshMessageCount = -1;
|
|
362
|
+
let refreshLastMessage: unknown;
|
|
363
|
+
const refreshTimer = setInterval(() => {
|
|
364
|
+
const agents = options.getAgents().slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
365
|
+
const agent = currentAgent(agents, selectedName);
|
|
366
|
+
if (!agent) {
|
|
367
|
+
const emptyKey = agents.map((item) => item.name).join("\0");
|
|
368
|
+
if (emptyKey !== refreshStateKey) {
|
|
369
|
+
refreshStateKey = emptyKey;
|
|
370
|
+
tui.requestRender();
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const session = agent.session;
|
|
377
|
+
const stats = session?.getSessionStats();
|
|
378
|
+
if (stats) {
|
|
379
|
+
agent.tokensUsed = stats.tokens.total;
|
|
380
|
+
const contextUsage = stats.contextUsage ?? session?.getContextUsage?.();
|
|
381
|
+
agent.contextUsage = stats.tokens.total > 0
|
|
382
|
+
? contextUsage
|
|
383
|
+
: initialContextUsage(contextUsage?.contextWindow);
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
// The nested session may be shutting down while this view refreshes.
|
|
387
|
+
}
|
|
388
|
+
const messages = agent.session?.messages || [];
|
|
389
|
+
const lastMessage = messages[messages.length - 1];
|
|
390
|
+
const stateKey = [
|
|
391
|
+
Math.floor(Date.now() / 1000),
|
|
392
|
+
agents.map((item) => item.name).join(","),
|
|
393
|
+
agent.name,
|
|
394
|
+
agent.model,
|
|
395
|
+
agent.thinking,
|
|
396
|
+
agent.modelSlot,
|
|
397
|
+
agent.startedAt,
|
|
398
|
+
agent.tokensUsed,
|
|
399
|
+
formatContextUsage(agent.contextUsage),
|
|
400
|
+
agent.status,
|
|
401
|
+
agent.latestProgress,
|
|
402
|
+
stoppingAgents.has(agent.name),
|
|
403
|
+
expandLargeToolResults,
|
|
404
|
+
offsetFromBottom,
|
|
405
|
+
composingMessage,
|
|
406
|
+
messageStatus,
|
|
407
|
+
tui.terminal?.rows ?? 24,
|
|
408
|
+
].join("\0");
|
|
409
|
+
if (stateKey === refreshStateKey
|
|
410
|
+
&& refreshMessages === messages
|
|
411
|
+
&& refreshMessageCount === messages.length
|
|
412
|
+
&& refreshLastMessage === lastMessage) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
refreshStateKey = stateKey;
|
|
416
|
+
refreshMessages = messages;
|
|
417
|
+
refreshMessageCount = messages.length;
|
|
418
|
+
refreshLastMessage = lastMessage;
|
|
419
|
+
tui.requestRender();
|
|
420
|
+
}, REFRESH_INTERVAL_MS);
|
|
421
|
+
|
|
422
|
+
const syncInputFocus = () => {
|
|
423
|
+
messageInput.focused = focused && composingMessage;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const stopComposingMessage = () => {
|
|
427
|
+
composingMessage = false;
|
|
428
|
+
messageInput.setValue("");
|
|
429
|
+
syncInputFocus();
|
|
430
|
+
tui.requestRender();
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
messageInput.onEscape = stopComposingMessage;
|
|
434
|
+
messageInput.onSubmit = (value: string) => {
|
|
435
|
+
const content = value.trim();
|
|
436
|
+
const agent = currentAgent(sortedAgents(), selectedName);
|
|
437
|
+
if (!content || !agent || !options.sendMessage || sendingMessage) {
|
|
438
|
+
if (!content) messageStatus = "Write a message before sending.";
|
|
439
|
+
tui.requestRender();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const recipient = agent.name;
|
|
444
|
+
sendingMessage = true;
|
|
445
|
+
messageStatus = `Sending to ${recipient}…`;
|
|
446
|
+
tui.requestRender();
|
|
447
|
+
void Promise.resolve()
|
|
448
|
+
.then(() => options.sendMessage?.(recipient, content))
|
|
449
|
+
.then(() => {
|
|
450
|
+
messageStatus = `Message sent to ${recipient}.`;
|
|
451
|
+
stopComposingMessage();
|
|
452
|
+
})
|
|
453
|
+
.catch((error: unknown) => {
|
|
454
|
+
messageStatus = error instanceof Error ? error.message : `Could not message ${recipient}.`;
|
|
455
|
+
tui.requestRender();
|
|
456
|
+
})
|
|
457
|
+
.finally(() => {
|
|
458
|
+
sendingMessage = false;
|
|
459
|
+
tui.requestRender();
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const sortedAgents = () => options.getAgents().slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
464
|
+
|
|
465
|
+
const selectRelative = (delta: number) => {
|
|
466
|
+
const agents = sortedAgents();
|
|
467
|
+
if (agents.length === 0) return;
|
|
468
|
+
const selected = currentAgent(agents, selectedName);
|
|
469
|
+
const currentIndex = Math.max(0, agents.findIndex(agent => agent.name === selected?.name));
|
|
470
|
+
selectedName = agents[(currentIndex + delta + agents.length) % agents.length]?.name;
|
|
471
|
+
offsetFromBottom = 0;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const selectPreviousOrMain = () => {
|
|
475
|
+
const agents = sortedAgents();
|
|
476
|
+
const selected = currentAgent(agents, selectedName);
|
|
477
|
+
const currentIndex = agents.findIndex(agent => agent.name === selected?.name);
|
|
478
|
+
if (currentIndex <= 0) {
|
|
479
|
+
done();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
selectedName = agents[currentIndex - 1]?.name;
|
|
483
|
+
offsetFromBottom = 0;
|
|
484
|
+
tui.requestRender();
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
get focused() {
|
|
489
|
+
return focused;
|
|
490
|
+
},
|
|
491
|
+
set focused(value: boolean) {
|
|
492
|
+
focused = value;
|
|
493
|
+
syncInputFocus();
|
|
494
|
+
},
|
|
495
|
+
render(width: number): string[] {
|
|
496
|
+
const agents = sortedAgents();
|
|
497
|
+
const agent = currentAgent(agents, selectedName);
|
|
498
|
+
const innerWidth = Math.max(40, width - 4);
|
|
499
|
+
const terminalRows = Math.max(12, tui.terminal?.rows ?? 24);
|
|
500
|
+
|
|
501
|
+
if (!agent) {
|
|
502
|
+
const emptyBodyHeight = Math.max(4, terminalRows - 6);
|
|
503
|
+
return framePanel([
|
|
504
|
+
pink("agent navigation"),
|
|
505
|
+
purple("↑ main agent"),
|
|
506
|
+
dimAnsi("No active agents. Press ↑ or esc to return to main."),
|
|
507
|
+
...Array.from({ length: emptyBodyHeight }, () => ""),
|
|
508
|
+
], innerWidth, AGENT_FOLLOW_BACKGROUND);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const selectedIndex = Math.max(0, agents.findIndex(item => item.name === agent.name));
|
|
512
|
+
const navigationStart = Math.max(0, Math.min(selectedIndex - 2, agents.length - MAX_NAVIGATION_AGENTS));
|
|
513
|
+
const visibleAgents = agents.slice(navigationStart, navigationStart + MAX_NAVIGATION_AGENTS);
|
|
514
|
+
const navigationLines = [pink("agent navigation"), purple("↑ main agent")];
|
|
515
|
+
if (navigationStart > 0) navigationLines.push(dimAnsi(` … ${navigationStart} agent${navigationStart === 1 ? "" : "s"} above`));
|
|
516
|
+
for (const item of visibleAgents) {
|
|
517
|
+
const selected = item.name === agent.name;
|
|
518
|
+
navigationLines.push(`${selected ? pink("->") : " "} ${item.name}`);
|
|
519
|
+
}
|
|
520
|
+
const remainingAgents = agents.length - navigationStart - visibleAgents.length;
|
|
521
|
+
if (remainingAgents > 0) navigationLines.push(dimAnsi(`↓ … ${remainingAgents} more agent${remainingAgents === 1 ? "" : "s"}`));
|
|
522
|
+
|
|
523
|
+
const messageLines = options.sendMessage ? [
|
|
524
|
+
purple("─".repeat(innerWidth)),
|
|
525
|
+
composingMessage ? pink(`message ${agent.name}`) : dimAnsi(`message ${agent.name}`),
|
|
526
|
+
...(composingMessage ? messageInput.render(innerWidth) : [dimAnsi("> Press m to start typing")]),
|
|
527
|
+
...(messageStatus
|
|
528
|
+
? [dimAnsi(messageStatus)]
|
|
529
|
+
: composingMessage ? [dimAnsi("enter send · esc cancel")] : []),
|
|
530
|
+
] : [];
|
|
531
|
+
const bodyHeight = Math.max(4, terminalRows - navigationLines.length - 6 - messageLines.length);
|
|
532
|
+
lastBodyHeight = bodyHeight;
|
|
533
|
+
|
|
534
|
+
selectedName = agent.name;
|
|
535
|
+
try {
|
|
536
|
+
const session = agent.session;
|
|
537
|
+
const stats = session?.getSessionStats();
|
|
538
|
+
if (stats) {
|
|
539
|
+
agent.tokensUsed = stats.tokens.total;
|
|
540
|
+
const contextUsage = stats.contextUsage ?? session?.getContextUsage?.();
|
|
541
|
+
agent.contextUsage = stats.tokens.total > 0
|
|
542
|
+
? contextUsage
|
|
543
|
+
: initialContextUsage(contextUsage?.contextWindow);
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// The nested session may be shutting down while this view renders.
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const currentMessages = agent.session?.messages || [];
|
|
550
|
+
const lastMessage = currentMessages[currentMessages.length - 1];
|
|
551
|
+
const renderNow = Date.now();
|
|
552
|
+
const isStopping = stoppingAgents.has(agent.name);
|
|
553
|
+
const currentFastFrameKey = composingMessage ? "" : [
|
|
554
|
+
innerWidth,
|
|
555
|
+
terminalRows,
|
|
556
|
+
Math.floor(renderNow / 1000),
|
|
557
|
+
agents.map((item) => item.name).join(","),
|
|
558
|
+
selectedName,
|
|
559
|
+
agent.model,
|
|
560
|
+
agent.thinking,
|
|
561
|
+
agent.modelSlot,
|
|
562
|
+
agent.startedAt,
|
|
563
|
+
agent.tokensUsed,
|
|
564
|
+
formatContextUsage(agent.contextUsage),
|
|
565
|
+
agent.status,
|
|
566
|
+
agent.latestProgress,
|
|
567
|
+
isStopping,
|
|
568
|
+
expandLargeToolResults,
|
|
569
|
+
offsetFromBottom,
|
|
570
|
+
messageStatus,
|
|
571
|
+
options.sendMessage ? 1 : 0,
|
|
572
|
+
].join("\0");
|
|
573
|
+
if (currentFastFrameKey
|
|
574
|
+
&& currentFastFrameKey === fastFrameKey
|
|
575
|
+
&& fastFrameMessages === currentMessages
|
|
576
|
+
&& fastFrameMessageCount === currentMessages.length
|
|
577
|
+
&& fastFrameLastMessage === lastMessage
|
|
578
|
+
&& cachedFrameLines) {
|
|
579
|
+
return cachedFrameLines.slice();
|
|
580
|
+
}
|
|
581
|
+
const model = formatModelLabel(agent.model, agent.thinking).replace(" · ", "/");
|
|
582
|
+
const slot = agent.modelSlot || "level inherited";
|
|
583
|
+
const elapsed = formatElapsed(renderNow - agent.startedAt);
|
|
584
|
+
const activity = isStopping
|
|
585
|
+
? "stopping"
|
|
586
|
+
: agent.latestProgress
|
|
587
|
+
? formatAnimatedProgress(agent.latestProgress, renderNow)
|
|
588
|
+
: agent.status;
|
|
589
|
+
const headline = `(${agent.name}) ${model} · ${slot} · ${elapsed} · ${formatContextUsage(agent.contextUsage)} · ${activity}`;
|
|
590
|
+
const logAction = expandLargeToolResults ? "l collapse logs" : "l expand logs";
|
|
591
|
+
const messageAction = options.sendMessage ? " · m message" : "";
|
|
592
|
+
const help = composingMessage
|
|
593
|
+
? `message ${agent.name} · enter send · esc cancel`
|
|
594
|
+
: agents.length > 1
|
|
595
|
+
? `↑ previous/main · ↓ next agent · ←/→ agent · ${logAction}${messageAction} · x stop · pgup/pgdn scroll · esc main`
|
|
596
|
+
: `↑/esc main · ${logAction}${messageAction} · x stop · pgup/pgdn scroll · end follow`;
|
|
597
|
+
|
|
598
|
+
const currentTranscriptWidth = Math.max(20, innerWidth);
|
|
599
|
+
if (transcriptAgent !== agent
|
|
600
|
+
|| transcriptMessages !== currentMessages
|
|
601
|
+
|| transcriptMessageCount !== currentMessages.length
|
|
602
|
+
|| transcriptLastMessage !== lastMessage
|
|
603
|
+
|| transcriptWidth !== currentTranscriptWidth
|
|
604
|
+
|| transcriptExpanded !== expandLargeToolResults) {
|
|
605
|
+
transcriptAgent = agent;
|
|
606
|
+
transcriptMessages = currentMessages;
|
|
607
|
+
transcriptMessageCount = currentMessages.length;
|
|
608
|
+
transcriptLastMessage = lastMessage;
|
|
609
|
+
transcriptWidth = currentTranscriptWidth;
|
|
610
|
+
transcriptExpanded = expandLargeToolResults;
|
|
611
|
+
cachedTranscriptLines = formatAgentFollowTranscript(currentMessages, {
|
|
612
|
+
expandLargeToolResults,
|
|
613
|
+
width: currentTranscriptWidth,
|
|
614
|
+
}).flatMap(line => wrapTextWithAnsi(line, currentTranscriptWidth));
|
|
615
|
+
}
|
|
616
|
+
const transcriptLines = cachedTranscriptLines;
|
|
617
|
+
lastTranscriptRows = transcriptLines.length;
|
|
618
|
+
const maxOffset = Math.max(0, transcriptLines.length - bodyHeight);
|
|
619
|
+
offsetFromBottom = Math.min(offsetFromBottom, maxOffset);
|
|
620
|
+
const start = Math.max(0, maxOffset - offsetFromBottom);
|
|
621
|
+
const visible = transcriptLines.slice(start, start + bodyHeight);
|
|
622
|
+
while (visible.length < bodyHeight) visible.push("");
|
|
623
|
+
|
|
624
|
+
const frameContent = [
|
|
625
|
+
...navigationLines,
|
|
626
|
+
purple("─".repeat(innerWidth)),
|
|
627
|
+
headline,
|
|
628
|
+
dimAnsi(help),
|
|
629
|
+
purple("─".repeat(innerWidth)),
|
|
630
|
+
...visible,
|
|
631
|
+
...messageLines,
|
|
632
|
+
];
|
|
633
|
+
let renderedFrame: string[] | undefined;
|
|
634
|
+
if (cachedFrameWidth === innerWidth
|
|
635
|
+
&& cachedFrameContent
|
|
636
|
+
&& cachedFrameContent.length === frameContent.length
|
|
637
|
+
&& cachedFrameLines) {
|
|
638
|
+
let changedCount = 0;
|
|
639
|
+
let changed0 = -1;
|
|
640
|
+
let changed1 = -1;
|
|
641
|
+
let changed2 = -1;
|
|
642
|
+
let changed3 = -1;
|
|
643
|
+
for (let index = 0; index < frameContent.length; index++) {
|
|
644
|
+
if (frameContent[index] === cachedFrameContent[index]) continue;
|
|
645
|
+
if (changedCount === 0) changed0 = index;
|
|
646
|
+
else if (changedCount === 1) changed1 = index;
|
|
647
|
+
else if (changedCount === 2) changed2 = index;
|
|
648
|
+
else if (changedCount === 3) changed3 = index;
|
|
649
|
+
changedCount++;
|
|
650
|
+
if (changedCount > 4) break;
|
|
651
|
+
}
|
|
652
|
+
if (changedCount === 0) {
|
|
653
|
+
fastFrameKey = currentFastFrameKey;
|
|
654
|
+
fastFrameMessages = currentMessages;
|
|
655
|
+
fastFrameMessageCount = currentMessages.length;
|
|
656
|
+
fastFrameLastMessage = lastMessage;
|
|
657
|
+
return cachedFrameLines.slice();
|
|
658
|
+
}
|
|
659
|
+
if (changedCount <= 4) {
|
|
660
|
+
const renderFrameRow = cachedFrameRowRenderer!;
|
|
661
|
+
renderedFrame = cachedFrameLines.slice();
|
|
662
|
+
renderedFrame[changed0 + 1] = renderFrameRow(frameContent[changed0]);
|
|
663
|
+
if (changedCount > 1) {
|
|
664
|
+
renderedFrame[changed1 + 1] = renderFrameRow(frameContent[changed1]);
|
|
665
|
+
if (changedCount > 2) {
|
|
666
|
+
renderedFrame[changed2 + 1] = renderFrameRow(frameContent[changed2]);
|
|
667
|
+
if (changedCount > 3) renderedFrame[changed3 + 1] = renderFrameRow(frameContent[changed3]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!renderedFrame) {
|
|
673
|
+
renderedFrame = framePanel(frameContent, innerWidth, AGENT_FOLLOW_BACKGROUND);
|
|
674
|
+
cachedFrameRowRenderer = createFramePanelRowRenderer(innerWidth, AGENT_FOLLOW_BACKGROUND);
|
|
675
|
+
}
|
|
676
|
+
cachedFrameWidth = innerWidth;
|
|
677
|
+
cachedFrameContent = frameContent;
|
|
678
|
+
cachedFrameLines = renderedFrame;
|
|
679
|
+
fastFrameKey = currentFastFrameKey;
|
|
680
|
+
fastFrameMessages = currentMessages;
|
|
681
|
+
fastFrameMessageCount = currentMessages.length;
|
|
682
|
+
fastFrameLastMessage = lastMessage;
|
|
683
|
+
return renderedFrame;
|
|
684
|
+
},
|
|
685
|
+
invalidate() {
|
|
686
|
+
messageInput.invalidate();
|
|
687
|
+
},
|
|
688
|
+
dispose() {
|
|
689
|
+
clearInterval(refreshTimer);
|
|
690
|
+
},
|
|
691
|
+
handleInput(data: string) {
|
|
692
|
+
if (composingMessage) {
|
|
693
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
694
|
+
done();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
messageInput.handleInput(data);
|
|
698
|
+
tui.requestRender();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
702
|
+
done();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (matchesKey(data, Key.up)) {
|
|
706
|
+
selectPreviousOrMain();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (data.toLowerCase() === "l") {
|
|
710
|
+
expandLargeToolResults = !expandLargeToolResults;
|
|
711
|
+
offsetFromBottom = 0;
|
|
712
|
+
tui.requestRender();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (data.toLowerCase() === "m" && options.sendMessage) {
|
|
716
|
+
composingMessage = true;
|
|
717
|
+
messageStatus = "";
|
|
718
|
+
syncInputFocus();
|
|
719
|
+
tui.requestRender();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (data.toLowerCase() === "x" && options.stopAgent) {
|
|
723
|
+
const agent = currentAgent(sortedAgents(), selectedName);
|
|
724
|
+
if (!agent || stoppingAgents.has(agent.name)) return;
|
|
725
|
+
stoppingAgents.add(agent.name);
|
|
726
|
+
tui.requestRender();
|
|
727
|
+
try {
|
|
728
|
+
const result = options.stopAgent(agent.name);
|
|
729
|
+
if (result && typeof result.then === "function") {
|
|
730
|
+
void result.finally(() => {
|
|
731
|
+
stoppingAgents.delete(agent.name);
|
|
732
|
+
tui.requestRender();
|
|
733
|
+
});
|
|
734
|
+
} else {
|
|
735
|
+
stoppingAgents.delete(agent.name);
|
|
736
|
+
tui.requestRender();
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
stoppingAgents.delete(agent.name);
|
|
740
|
+
tui.requestRender();
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (matchesKey(data, Key.left)) {
|
|
745
|
+
selectRelative(-1);
|
|
746
|
+
tui.requestRender();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (matchesKey(data, Key.right) || matchesKey(data, Key.down)) {
|
|
750
|
+
selectRelative(1);
|
|
751
|
+
tui.requestRender();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const maxOffset = Math.max(0, lastTranscriptRows - lastBodyHeight);
|
|
755
|
+
if (matchesKey(data, Key.pageUp)) offsetFromBottom = Math.min(maxOffset, offsetFromBottom + lastBodyHeight);
|
|
756
|
+
else if (matchesKey(data, Key.pageDown)) offsetFromBottom = Math.max(0, offsetFromBottom - lastBodyHeight);
|
|
757
|
+
else if (matchesKey(data, Key.home)) offsetFromBottom = maxOffset;
|
|
758
|
+
else if (matchesKey(data, Key.end)) offsetFromBottom = 0;
|
|
759
|
+
else return;
|
|
760
|
+
tui.requestRender();
|
|
761
|
+
},
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export async function openAgentFollowView(ctx: any, options: AgentFollowViewOptions): Promise<void> {
|
|
766
|
+
if (ctx.mode && ctx.mode !== "tui") return;
|
|
767
|
+
await ctx.ui.custom(
|
|
768
|
+
(tui: any, _theme: any, _keybindings: any, done: () => void) => createAgentFollowComponent(tui, done, options),
|
|
769
|
+
{
|
|
770
|
+
overlay: true,
|
|
771
|
+
overlayOptions: {
|
|
772
|
+
width: "100%",
|
|
773
|
+
maxHeight: "100%",
|
|
774
|
+
anchor: "center",
|
|
775
|
+
margin: 0,
|
|
776
|
+
},
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
}
|