@pi-unipi/subagents 0.1.12 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/config.test.d.ts +11 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +196 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/esc-propagation.test.d.ts +10 -0
- package/dist/__tests__/esc-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/esc-propagation.test.js +140 -0
- package/dist/__tests__/esc-propagation.test.js.map +1 -0
- package/dist/__tests__/file-lock.test.d.ts +12 -0
- package/dist/__tests__/file-lock.test.d.ts.map +1 -0
- package/dist/__tests__/file-lock.test.js +187 -0
- package/dist/__tests__/file-lock.test.js.map +1 -0
- package/dist/__tests__/workflow-integration.test.d.ts +12 -0
- package/dist/__tests__/workflow-integration.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-integration.test.js +261 -0
- package/dist/__tests__/workflow-integration.test.js.map +1 -0
- package/dist/agent-manager.d.ts +4 -1
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +10 -0
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-runner.d.ts +2 -1
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +23 -7
- package/dist/agent-runner.js.map +1 -1
- package/dist/conversation-viewer.d.ts +40 -0
- package/dist/conversation-viewer.d.ts.map +1 -0
- package/dist/conversation-viewer.js +276 -0
- package/dist/conversation-viewer.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +410 -58
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -1
- package/dist/widget.d.ts +32 -3
- package/dist/widget.d.ts.map +1 -1
- package/dist/widget.js +298 -56
- package/dist/widget.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +12 -1
- package/src/agent-runner.ts +23 -8
- package/src/conversation-viewer.ts +299 -0
- package/src/index.ts +411 -49
- package/src/types.ts +49 -0
- package/src/widget.ts +332 -72
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Conversation Viewer
|
|
3
|
+
*
|
|
4
|
+
* Live-scrolling overlay for viewing agent conversations.
|
|
5
|
+
* Subscribes to session events for real-time streaming updates.
|
|
6
|
+
* Supports keyboard navigation: ↑↓, PgUp/PgDn, Home/End, Esc/q to close.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
type Component,
|
|
12
|
+
matchesKey,
|
|
13
|
+
type TUI,
|
|
14
|
+
truncateToWidth,
|
|
15
|
+
visibleWidth,
|
|
16
|
+
wrapTextWithAnsi,
|
|
17
|
+
} from "@mariozechner/pi-tui";
|
|
18
|
+
import type { AgentActivity } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
21
|
+
const CHROME_LINES = 6;
|
|
22
|
+
const MIN_VIEWPORT = 3;
|
|
23
|
+
|
|
24
|
+
/** Extract text from content array. */
|
|
25
|
+
function extractText(content: string | Array<{ type: string; text?: string }>): string {
|
|
26
|
+
if (typeof content === "string") return content;
|
|
27
|
+
return content
|
|
28
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text" && typeof p.text === "string")
|
|
29
|
+
.map((p) => p.text)
|
|
30
|
+
.join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Format duration. */
|
|
34
|
+
function formatMs(ms: number): string {
|
|
35
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
36
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
37
|
+
return `${ms}ms`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Format tokens compactly. */
|
|
41
|
+
function formatTokens(count: number): string {
|
|
42
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
43
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
44
|
+
return `${count} token`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Describe current activity from active tools. */
|
|
48
|
+
function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
|
|
49
|
+
if (activeTools.size > 0) {
|
|
50
|
+
const names = [...new Set(activeTools.values())];
|
|
51
|
+
return names.join(", ") + "…";
|
|
52
|
+
}
|
|
53
|
+
if (responseText && responseText.trim().length > 0) {
|
|
54
|
+
const lastLine = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
|
|
55
|
+
if (lastLine.length > 60) return lastLine.slice(0, 60) + "…";
|
|
56
|
+
if (lastLine.length > 0) return lastLine;
|
|
57
|
+
}
|
|
58
|
+
return "thinking…";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ViewerRecord {
|
|
62
|
+
type: string;
|
|
63
|
+
description: string;
|
|
64
|
+
status: string;
|
|
65
|
+
toolUses: number;
|
|
66
|
+
startedAt: number;
|
|
67
|
+
completedAt?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ConversationViewer implements Component {
|
|
71
|
+
private scrollOffset = 0;
|
|
72
|
+
private autoScroll = true;
|
|
73
|
+
private unsubscribe: (() => void) | undefined;
|
|
74
|
+
private lastInnerW = 0;
|
|
75
|
+
private closed = false;
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
private tui: TUI,
|
|
79
|
+
private session: AgentSession,
|
|
80
|
+
private record: ViewerRecord,
|
|
81
|
+
private activity: AgentActivity | undefined,
|
|
82
|
+
private theme: any,
|
|
83
|
+
private done: (result: undefined) => void,
|
|
84
|
+
) {
|
|
85
|
+
this.unsubscribe = session.subscribe(() => {
|
|
86
|
+
if (this.closed) return;
|
|
87
|
+
this.tui.requestRender();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
handleInput(data: string): void {
|
|
92
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
93
|
+
this.closed = true;
|
|
94
|
+
this.done(undefined);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const totalLines = this.buildContentLines(this.lastInnerW).length;
|
|
99
|
+
const viewportHeight = this.viewportHeight();
|
|
100
|
+
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
101
|
+
|
|
102
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
103
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
104
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
105
|
+
} else if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
106
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
107
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
108
|
+
} else if (matchesKey(data, "pageUp")) {
|
|
109
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
110
|
+
this.autoScroll = false;
|
|
111
|
+
} else if (matchesKey(data, "pageDown")) {
|
|
112
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
113
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
114
|
+
} else if (matchesKey(data, "home")) {
|
|
115
|
+
this.scrollOffset = 0;
|
|
116
|
+
this.autoScroll = false;
|
|
117
|
+
} else if (matchesKey(data, "end")) {
|
|
118
|
+
this.scrollOffset = maxScroll;
|
|
119
|
+
this.autoScroll = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
render(width: number): string[] {
|
|
124
|
+
if (width < 6) return [];
|
|
125
|
+
const th = this.theme;
|
|
126
|
+
const innerW = width - 4; // border + padding
|
|
127
|
+
this.lastInnerW = innerW;
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
|
|
130
|
+
const pad = (s: string, len: number) => {
|
|
131
|
+
const vis = visibleWidth(s);
|
|
132
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
133
|
+
};
|
|
134
|
+
const row = (content: string) =>
|
|
135
|
+
th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW) + " " + th.fg("border", "│");
|
|
136
|
+
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
137
|
+
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
138
|
+
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
139
|
+
|
|
140
|
+
// Header
|
|
141
|
+
lines.push(hrTop);
|
|
142
|
+
const name = this.record.type;
|
|
143
|
+
const statusIcon =
|
|
144
|
+
this.record.status === "running"
|
|
145
|
+
? th.fg("accent", "●")
|
|
146
|
+
: this.record.status === "completed"
|
|
147
|
+
? th.fg("success", "✓")
|
|
148
|
+
: this.record.status === "error"
|
|
149
|
+
? th.fg("error", "✗")
|
|
150
|
+
: th.fg("dim", "○");
|
|
151
|
+
|
|
152
|
+
const duration = this.record.completedAt
|
|
153
|
+
? formatMs(this.record.completedAt - this.record.startedAt)
|
|
154
|
+
: `${formatMs(Date.now() - this.record.startedAt)} (running)`;
|
|
155
|
+
|
|
156
|
+
const headerParts: string[] = [duration];
|
|
157
|
+
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
|
|
158
|
+
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
159
|
+
if (this.activity?.session) {
|
|
160
|
+
try {
|
|
161
|
+
const tokens = (this.activity.session as any).getSessionStats().tokens.total;
|
|
162
|
+
if (tokens > 0) headerParts.push(formatTokens(tokens));
|
|
163
|
+
} catch {
|
|
164
|
+
/* */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lines.push(
|
|
169
|
+
row(
|
|
170
|
+
`${statusIcon} ${th.bold(name)} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
lines.push(hrMid);
|
|
174
|
+
|
|
175
|
+
// Content area
|
|
176
|
+
const contentLines = this.buildContentLines(innerW);
|
|
177
|
+
const viewportHeight = this.viewportHeight();
|
|
178
|
+
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
179
|
+
|
|
180
|
+
if (this.autoScroll) {
|
|
181
|
+
this.scrollOffset = maxScroll;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
185
|
+
const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < viewportHeight; i++) {
|
|
188
|
+
lines.push(row(visible[i] ?? ""));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Footer
|
|
192
|
+
lines.push(hrMid);
|
|
193
|
+
const scrollPct =
|
|
194
|
+
contentLines.length <= viewportHeight
|
|
195
|
+
? "100%"
|
|
196
|
+
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
197
|
+
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
198
|
+
const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn · Esc close");
|
|
199
|
+
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
200
|
+
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
201
|
+
lines.push(hrBot);
|
|
202
|
+
|
|
203
|
+
return lines;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
invalidate(): void {
|
|
207
|
+
/* no cached state to clear */
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
dispose(): void {
|
|
211
|
+
this.closed = true;
|
|
212
|
+
if (this.unsubscribe) {
|
|
213
|
+
this.unsubscribe();
|
|
214
|
+
this.unsubscribe = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- Private ----
|
|
219
|
+
|
|
220
|
+
private viewportHeight(): number {
|
|
221
|
+
return Math.max(MIN_VIEWPORT, this.tui.terminal.rows - CHROME_LINES);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private buildContentLines(width: number): string[] {
|
|
225
|
+
if (width <= 0) return [];
|
|
226
|
+
|
|
227
|
+
const th = this.theme;
|
|
228
|
+
const messages = (this.session as any).messages;
|
|
229
|
+
const lines: string[] = [];
|
|
230
|
+
|
|
231
|
+
if (!messages || messages.length === 0) {
|
|
232
|
+
lines.push(th.fg("dim", "(waiting for first message...)"));
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let needsSeparator = false;
|
|
237
|
+
for (const msg of messages) {
|
|
238
|
+
if (msg.role === "user") {
|
|
239
|
+
const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
|
|
240
|
+
if (!text.trim()) continue;
|
|
241
|
+
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
242
|
+
lines.push(th.fg("accent", "[User]"));
|
|
243
|
+
for (const line of wrapTextWithAnsi(text.trim(), width)) {
|
|
244
|
+
lines.push(line);
|
|
245
|
+
}
|
|
246
|
+
} else if (msg.role === "assistant") {
|
|
247
|
+
const textParts: string[] = [];
|
|
248
|
+
const toolCalls: string[] = [];
|
|
249
|
+
for (const c of msg.content) {
|
|
250
|
+
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
251
|
+
else if (c.type === "tool_use" || c.type === "toolCall") {
|
|
252
|
+
toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
256
|
+
lines.push(th.bold("[Assistant]"));
|
|
257
|
+
if (textParts.length > 0) {
|
|
258
|
+
for (const line of wrapTextWithAnsi(textParts.join("\n").trim(), width)) {
|
|
259
|
+
lines.push(line);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const name of toolCalls) {
|
|
263
|
+
lines.push(truncateToWidth(th.fg("muted", ` [Tool: ${name}]`), width));
|
|
264
|
+
}
|
|
265
|
+
} else if (msg.role === "toolResult") {
|
|
266
|
+
const text = extractText(msg.content);
|
|
267
|
+
const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
|
|
268
|
+
if (!truncated.trim()) continue;
|
|
269
|
+
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
270
|
+
lines.push(th.fg("dim", "[Result]"));
|
|
271
|
+
for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
|
|
272
|
+
lines.push(th.fg("dim", line));
|
|
273
|
+
}
|
|
274
|
+
} else if ((msg as any).role === "bashExecution") {
|
|
275
|
+
const bash = msg as any;
|
|
276
|
+
if (needsSeparator) lines.push(th.fg("dim", "───"));
|
|
277
|
+
lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
|
|
278
|
+
if (bash.output?.trim()) {
|
|
279
|
+
const out = bash.output.length > 500 ? bash.output.slice(0, 500) + "... (truncated)" : bash.output;
|
|
280
|
+
for (const line of wrapTextWithAnsi(out.trim(), width)) {
|
|
281
|
+
lines.push(th.fg("dim", line));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
needsSeparator = true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Streaming indicator for running agents
|
|
291
|
+
if (this.record.status === "running" && this.activity) {
|
|
292
|
+
const act = describeActivity(this.activity.activeTools, this.activity.responseText);
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(truncateToWidth(th.fg("accent", "▍ ") + th.fg("dim", act), width));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return lines.map((l) => truncateToWidth(l, width));
|
|
298
|
+
}
|
|
299
|
+
}
|