@gotgenes/pi-subagents 6.12.0 → 6.13.0
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/CHANGELOG.md +23 -0
- package/docs/architecture/architecture.md +17 -18
- package/docs/plans/0135-extract-display-helpers.md +182 -0
- package/docs/plans/0136-decompose-agent-menu.md +300 -0
- package/docs/retro/0134-reduce-as-any-casts.md +56 -0
- package/docs/retro/0135-extract-display-helpers.md +38 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/renderer.ts +1 -1
- package/src/tools/agent-tool.ts +2 -2
- package/src/tools/background-spawner.ts +1 -1
- package/src/tools/foreground-runner.ts +1 -1
- package/src/tools/get-result-tool.ts +1 -1
- package/src/tools/helpers.ts +1 -1
- package/src/ui/agent-config-editor.ts +202 -0
- package/src/ui/agent-creation-wizard.ts +246 -0
- package/src/ui/agent-file-ops.ts +59 -0
- package/src/ui/agent-menu.ts +22 -394
- package/src/ui/agent-widget.ts +13 -165
- package/src/ui/conversation-viewer.ts +1 -1
- package/src/ui/display.ts +178 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display.ts — Pure formatting helpers and display utilities for agent UI.
|
|
3
|
+
*
|
|
4
|
+
* All functions are stateless and dependency-free (no SDK, no widget lifecycle).
|
|
5
|
+
* Consumed by the widget, the menu, tool modules, and the notification renderer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentConfigLookup } from "../agent-types.js";
|
|
9
|
+
import type { AgentInvocation, SubagentType } from "../types.js";
|
|
10
|
+
|
|
11
|
+
// ---- Types ----
|
|
12
|
+
|
|
13
|
+
export type Theme = {
|
|
14
|
+
fg(color: string, text: string): string;
|
|
15
|
+
bold(text: string): string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Metadata attached to Agent tool results for custom rendering. */
|
|
19
|
+
export interface AgentDetails {
|
|
20
|
+
displayName: string;
|
|
21
|
+
description: string;
|
|
22
|
+
subagentType: string;
|
|
23
|
+
toolUses: number;
|
|
24
|
+
tokens: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "background";
|
|
27
|
+
/** Human-readable description of what the agent is currently doing. */
|
|
28
|
+
activity?: string;
|
|
29
|
+
/** Current spinner frame index (for animated running indicator). */
|
|
30
|
+
spinnerFrame?: number;
|
|
31
|
+
/** Short model name if different from parent (e.g. "haiku", "sonnet"). */
|
|
32
|
+
modelName?: string;
|
|
33
|
+
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
|
|
34
|
+
tags?: string[];
|
|
35
|
+
/** Current turn count. */
|
|
36
|
+
turnCount?: number;
|
|
37
|
+
/** Effective max turns (undefined = unlimited). */
|
|
38
|
+
maxTurns?: number;
|
|
39
|
+
agentId?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- Constants ----
|
|
44
|
+
|
|
45
|
+
/** Braille spinner frames for animated running indicator. */
|
|
46
|
+
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
47
|
+
|
|
48
|
+
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
49
|
+
export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
|
|
50
|
+
|
|
51
|
+
/** Tool name → human-readable action for activity descriptions. */
|
|
52
|
+
const TOOL_DISPLAY: Record<string, string> = {
|
|
53
|
+
read: "reading",
|
|
54
|
+
bash: "running command",
|
|
55
|
+
edit: "editing",
|
|
56
|
+
write: "writing",
|
|
57
|
+
grep: "searching",
|
|
58
|
+
find: "finding files",
|
|
59
|
+
ls: "listing",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ---- Pure formatters ----
|
|
63
|
+
|
|
64
|
+
/** Format a token count compactly: "33.8k token", "1.2M token". */
|
|
65
|
+
export function formatTokens(count: number): string {
|
|
66
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
67
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
68
|
+
return `${count} token`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Token count with optional context-fill % and compaction-count annotations.
|
|
73
|
+
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
74
|
+
* Compaction count rendered as `↻N` in dim.
|
|
75
|
+
*
|
|
76
|
+
* "12.3k token" — no annotations
|
|
77
|
+
* "12.3k token (45%)" — percent only
|
|
78
|
+
* "12.3k token (↻2)" — compactions only (e.g. right after compact)
|
|
79
|
+
* "12.3k token (45% · ↻2)" — both
|
|
80
|
+
*/
|
|
81
|
+
export function formatSessionTokens(
|
|
82
|
+
tokens: number,
|
|
83
|
+
percent: number | null,
|
|
84
|
+
theme: Theme,
|
|
85
|
+
compactions = 0,
|
|
86
|
+
): string {
|
|
87
|
+
const tokenStr = formatTokens(tokens);
|
|
88
|
+
const annot: string[] = [];
|
|
89
|
+
if (percent !== null) {
|
|
90
|
+
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
91
|
+
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
92
|
+
}
|
|
93
|
+
if (compactions > 0) {
|
|
94
|
+
annot.push(theme.fg("dim", `↻${compactions}`));
|
|
95
|
+
}
|
|
96
|
+
if (annot.length === 0) return tokenStr;
|
|
97
|
+
return `${tokenStr} (${annot.join(" · ")})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */
|
|
101
|
+
export function formatTurns(turnCount: number, maxTurns?: number | null): string {
|
|
102
|
+
return maxTurns != null ? `⟳${turnCount}≤${maxTurns}` : `⟳${turnCount}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Format milliseconds as human-readable duration. */
|
|
106
|
+
export function formatMs(ms: number): string {
|
|
107
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Format duration from start/completed timestamps. */
|
|
111
|
+
export function formatDuration(startedAt: number, completedAt?: number): string {
|
|
112
|
+
if (completedAt) return formatMs(completedAt - startedAt);
|
|
113
|
+
return `${formatMs(Date.now() - startedAt)} (running)`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- Display helpers ----
|
|
117
|
+
|
|
118
|
+
/** Get display name for any agent type (built-in or custom). */
|
|
119
|
+
export function getDisplayName(type: SubagentType, registry: AgentConfigLookup): string {
|
|
120
|
+
const config = registry.resolveAgentConfig(type);
|
|
121
|
+
return config.displayName ?? config.name;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
125
|
+
export function getPromptModeLabel(type: SubagentType, registry: AgentConfigLookup): string | undefined {
|
|
126
|
+
const config = registry.resolveAgentConfig(type);
|
|
127
|
+
return config.promptMode === "append" ? "twin" : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Mode label is not included — callers add it where they want it. */
|
|
131
|
+
export function buildInvocationTags(
|
|
132
|
+
invocation: AgentInvocation | undefined,
|
|
133
|
+
): { modelName?: string; tags: string[] } {
|
|
134
|
+
const tags: string[] = [];
|
|
135
|
+
if (!invocation) return { tags };
|
|
136
|
+
if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
|
|
137
|
+
if (invocation.isolated) tags.push("isolated");
|
|
138
|
+
if (invocation.isolation === "worktree") tags.push("worktree");
|
|
139
|
+
if (invocation.inheritContext) tags.push("inherit context");
|
|
140
|
+
if (invocation.runInBackground) tags.push("background");
|
|
141
|
+
if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
|
|
142
|
+
return { modelName: invocation.modelName, tags };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Truncate text to a single line, max `len` chars. */
|
|
146
|
+
function truncateLine(text: string, len = 60): string {
|
|
147
|
+
const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
148
|
+
if (line.length <= len) return line;
|
|
149
|
+
return line.slice(0, len) + "…";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
153
|
+
export function describeActivity(activeTools: ReadonlyMap<string, string>, responseText?: string): string {
|
|
154
|
+
if (activeTools.size > 0) {
|
|
155
|
+
const groups = new Map<string, number>();
|
|
156
|
+
for (const toolName of activeTools.values()) {
|
|
157
|
+
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
158
|
+
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
for (const [action, count] of groups) {
|
|
163
|
+
if (count > 1) {
|
|
164
|
+
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
165
|
+
} else {
|
|
166
|
+
parts.push(action);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return parts.join(", ") + "…";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// No tools active — show truncated response text if available
|
|
173
|
+
if (responseText && responseText.trim().length > 0) {
|
|
174
|
+
return truncateLine(responseText);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return "thinking…";
|
|
178
|
+
}
|