pum-agent 0.1.0-beta.3

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.
@@ -0,0 +1,86 @@
1
+ import type { ImageContent } from "@earendil-works/pi-ai";
2
+ import type { Line, PendingLine } from "../transcript";
3
+ import type { WorktreeRecord } from "../worktree";
4
+ import type { AgentUsage } from "../agent-usage";
5
+
6
+ export const SUBAGENT_CUSTOM_TYPE = "pum.subagent";
7
+ export const AGENT_MESSAGE_CUSTOM_TYPE = "pum.agent_message";
8
+ export const AGENT_MESSAGE_DISPLAY_TYPE = "pum.agent_message_display";
9
+ export const TOOL_EVENT_CUSTOM_TYPE = "pum.tool_event";
10
+ /** Hidden user-message prefix used only to guarantee that the main loop wakes. */
11
+ export const SUBAGENT_WAKE_PREFIX = "[PUM internal subagent wake]";
12
+
13
+ export type SubagentStatus =
14
+ | "starting"
15
+ | "running"
16
+ | "idle"
17
+ | "completed"
18
+ | "failed"
19
+ | "stopped"
20
+ | "interrupted";
21
+
22
+ export type AgentStream = { kind: "assistant" | "thinking"; text: string } | null;
23
+
24
+ export type AgentTranscript = {
25
+ lines: Line[];
26
+ stream: AgentStream;
27
+ pending: PendingLine[];
28
+ };
29
+
30
+ export type SubagentSnapshot = {
31
+ id: string;
32
+ name: string;
33
+ task: string;
34
+ status: SubagentStatus;
35
+ worktree: WorktreeRecord;
36
+ sessionFile?: string;
37
+ /** The retained agent that spawned this agent. Missing legacy values mean main. */
38
+ parentAgentId: string | null;
39
+ modelId: string;
40
+ thinkingLevel: string;
41
+ transcript: AgentTranscript;
42
+ summary?: string;
43
+ startedAt: number;
44
+ updatedAt: number;
45
+ runStartedAt?: number;
46
+ usage: AgentUsage;
47
+ };
48
+
49
+ export type SubagentRegistryEvent = {
50
+ event: "spawned" | "status" | "usage" | "removed";
51
+ id: string;
52
+ at: number;
53
+ snapshot?: Omit<SubagentSnapshot, "transcript">;
54
+ status?: SubagentStatus;
55
+ summary?: string;
56
+ usage?: AgentUsage;
57
+ };
58
+
59
+ export type AgentMessageData = {
60
+ id: string;
61
+ sender: string;
62
+ recipient: string;
63
+ text: string;
64
+ at: number;
65
+ };
66
+
67
+ export type SubagentManagerEvent =
68
+ | { type: "changed" }
69
+ | { type: "main-line"; line: Line }
70
+ | { type: "main-pending-add"; pending: PendingLine }
71
+ | { type: "main-pending-resolve"; id: string }
72
+ | { type: "main-pending-drop"; id: string };
73
+
74
+ export type SpawnSubagentOptions = {
75
+ task: string;
76
+ name?: string;
77
+ modelId: string;
78
+ thinkingLevel: string;
79
+ createWorktree?: boolean;
80
+ parentAgentId?: string | null;
81
+ };
82
+
83
+ export type RoutedPrompt = {
84
+ text: string;
85
+ images?: ImageContent[];
86
+ };
package/src/syntax.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { SyntaxStyle } from "@opentui/core";
2
+ import type { Theme } from "./theme";
3
+
4
+ /**
5
+ * OpenTUI's <markdown> and <code> need a SyntaxStyle and ship no default. The
6
+ * keys are tree-sitter capture names from the bundled highlight queries;
7
+ * lookup falls back along the dots, so "markup.heading" covers
8
+ * "markup.heading.1" and friends.
9
+ */
10
+ export function buildSyntaxStyle(t: Theme): SyntaxStyle {
11
+ return SyntaxStyle.fromStyles({
12
+ default: { fg: t.assistant },
13
+ conceal: { fg: t.dim },
14
+
15
+ // markdown. Headings are only ever captured numbered (markup.heading.1…6)
16
+ // and the tree-sitter path does not fall back along the dots, so each
17
+ // level has to be registered.
18
+ "markup.heading": { fg: t.accent, bold: true },
19
+ "markup.heading.1": { fg: t.accent, bold: true },
20
+ "markup.heading.2": { fg: t.accent, bold: true },
21
+ "markup.heading.3": { fg: t.accent, bold: true },
22
+ "markup.heading.4": { fg: t.accent },
23
+ "markup.heading.5": { fg: t.accent },
24
+ "markup.heading.6": { fg: t.accent },
25
+ "markup.list": { fg: t.tool },
26
+ "markup.list.checked": { fg: t.success },
27
+ "markup.list.unchecked": { fg: t.dim },
28
+ "markup.quote": { fg: t.dim, italic: true },
29
+ "markup.raw": { fg: t.toolArg },
30
+ "markup.raw.block": { fg: t.toolArg },
31
+ "markup.strong": { fg: t.fg, bold: true },
32
+ "markup.italic": { fg: t.fg, italic: true },
33
+ "markup.strikethrough": { fg: t.dim, dim: true },
34
+ "markup.link": { fg: t.accent, underline: true },
35
+ "markup.link.label": { fg: t.accent },
36
+ "markup.link.url": { fg: t.dim, underline: true },
37
+ label: { fg: t.accent },
38
+ "character.special": { fg: t.tool },
39
+
40
+ // code
41
+ keyword: { fg: t.codeKeyword },
42
+ operator: { fg: t.codeKeyword },
43
+ string: { fg: t.codeString },
44
+ "string.escape": { fg: t.codeNumber },
45
+ number: { fg: t.codeNumber },
46
+ boolean: { fg: t.codeNumber },
47
+ constant: { fg: t.codeNumber },
48
+ comment: { fg: t.codeComment, italic: true },
49
+ function: { fg: t.codeFunction },
50
+ constructor: { fg: t.codeFunction },
51
+ type: { fg: t.codeType },
52
+ attribute: { fg: t.codeType },
53
+ module: { fg: t.codeType },
54
+ variable: { fg: t.fg },
55
+ property: { fg: t.fg },
56
+ "punctuation.delimiter": { fg: t.dim },
57
+ "punctuation.bracket": { fg: t.dim },
58
+ "punctuation.special": { fg: t.dim },
59
+ });
60
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,346 @@
1
+ import { RGBA, parseColor } from "@opentui/core";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { AGENT_DIR } from "./config";
5
+
6
+ export type Theme = {
7
+ name: string;
8
+ bg: string;
9
+ fg: string;
10
+ dim: string;
11
+ accent: string;
12
+ border: string;
13
+ /** Foreground and background of a user turn's full-width bar. */
14
+ user: string;
15
+ userBg: string;
16
+ /** Foreground and background for inter-agent communication rows. */
17
+ agentMessage: string;
18
+ agentMessageBg: string;
19
+ assistant: string;
20
+ thinking: string;
21
+ tool: string;
22
+ toolArg: string;
23
+ success: string;
24
+ error: string;
25
+ warn: string;
26
+ selectionBg: string;
27
+ popupBg: string;
28
+ /** The bright colour a shimmer sweeps toward. */
29
+ highlight: string;
30
+ /** Syntax colours for markdown code blocks. */
31
+ codeKeyword: string;
32
+ codeString: string;
33
+ codeNumber: string;
34
+ codeComment: string;
35
+ codeFunction: string;
36
+ codeType: string;
37
+ };
38
+
39
+ const tokyonight: Theme = {
40
+ name: "tokyonight",
41
+ bg: "#1a1b26",
42
+ fg: "#c0caf5",
43
+ dim: "#565f89",
44
+ accent: "#7aa2f7",
45
+ border: "#292e42",
46
+ user: "#c0caf5",
47
+ userBg: "#283457",
48
+ agentMessage: "#bb9af7",
49
+ agentMessageBg: "#2b234a",
50
+ assistant: "#c0caf5",
51
+ thinking: "#565f89",
52
+ tool: "#e0af68",
53
+ toolArg: "#9ece6a",
54
+ success: "#9ece6a",
55
+ error: "#f7768e",
56
+ warn: "#ff9e64",
57
+ selectionBg: "#33467c",
58
+ popupBg: "#1f2335",
59
+ highlight: "#ffffff",
60
+ codeKeyword: "#bb9af7",
61
+ codeString: "#9ece6a",
62
+ codeNumber: "#ff9e64",
63
+ codeComment: "#565f89",
64
+ codeFunction: "#7aa2f7",
65
+ codeType: "#2ac3de",
66
+ };
67
+
68
+ const gruvbox: Theme = {
69
+ name: "gruvbox",
70
+ bg: "#282828",
71
+ fg: "#ebdbb2",
72
+ dim: "#928374",
73
+ accent: "#83a598",
74
+ border: "#3c3836",
75
+ user: "#ebdbb2",
76
+ userBg: "#3c3836",
77
+ agentMessage: "#d3869b",
78
+ agentMessageBg: "#4a3346",
79
+ assistant: "#ebdbb2",
80
+ thinking: "#928374",
81
+ tool: "#fabd2f",
82
+ toolArg: "#b8bb26",
83
+ success: "#b8bb26",
84
+ error: "#fb4934",
85
+ warn: "#fe8019",
86
+ selectionBg: "#504945",
87
+ popupBg: "#32302f",
88
+ highlight: "#fbf1c7",
89
+ codeKeyword: "#fb4934",
90
+ codeString: "#b8bb26",
91
+ codeNumber: "#d3869b",
92
+ codeComment: "#928374",
93
+ codeFunction: "#fabd2f",
94
+ codeType: "#8ec07c",
95
+ };
96
+
97
+ const catppuccin: Theme = {
98
+ name: "catppuccin",
99
+ bg: "#1e1e2e",
100
+ fg: "#cdd6f4",
101
+ dim: "#6c7086",
102
+ accent: "#89b4fa",
103
+ border: "#313244",
104
+ user: "#cdd6f4",
105
+ userBg: "#313244",
106
+ agentMessage: "#cba6f7",
107
+ agentMessageBg: "#3b3156",
108
+ assistant: "#cdd6f4",
109
+ thinking: "#6c7086",
110
+ tool: "#f9e2af",
111
+ toolArg: "#a6e3a1",
112
+ success: "#a6e3a1",
113
+ error: "#f38ba8",
114
+ warn: "#fab387",
115
+ selectionBg: "#45475a",
116
+ popupBg: "#181825",
117
+ highlight: "#f5e0dc",
118
+ codeKeyword: "#cba6f7",
119
+ codeString: "#a6e3a1",
120
+ codeNumber: "#fab387",
121
+ codeComment: "#6c7086",
122
+ codeFunction: "#89b4fa",
123
+ codeType: "#f9e2af",
124
+ };
125
+
126
+ const nord: Theme = {
127
+ name: "nord",
128
+ bg: "#2e3440",
129
+ fg: "#d8dee9",
130
+ dim: "#7b88a1",
131
+ accent: "#88c0d0",
132
+ border: "#3b4252",
133
+ user: "#eceff4",
134
+ userBg: "#434c5e",
135
+ agentMessage: "#b48ead",
136
+ agentMessageBg: "#453b53",
137
+ assistant: "#d8dee9",
138
+ thinking: "#7b88a1",
139
+ tool: "#ebcb8b",
140
+ toolArg: "#a3be8c",
141
+ success: "#a3be8c",
142
+ error: "#bf616a",
143
+ warn: "#d08770",
144
+ selectionBg: "#4c566a",
145
+ popupBg: "#3b4252",
146
+ highlight: "#eceff4",
147
+ codeKeyword: "#b48ead",
148
+ codeString: "#a3be8c",
149
+ codeNumber: "#d08770",
150
+ codeComment: "#616e88",
151
+ codeFunction: "#88c0d0",
152
+ codeType: "#8fbcbb",
153
+ };
154
+
155
+ const dracula: Theme = {
156
+ name: "dracula",
157
+ bg: "#282a36",
158
+ fg: "#f8f8f2",
159
+ dim: "#6272a4",
160
+ accent: "#bd93f9",
161
+ border: "#44475a",
162
+ user: "#f8f8f2",
163
+ userBg: "#44475a",
164
+ agentMessage: "#ff79c6",
165
+ agentMessageBg: "#4a304d",
166
+ assistant: "#f8f8f2",
167
+ thinking: "#6272a4",
168
+ tool: "#f1fa8c",
169
+ toolArg: "#50fa7b",
170
+ success: "#50fa7b",
171
+ error: "#ff5555",
172
+ warn: "#ffb86c",
173
+ selectionBg: "#44475a",
174
+ popupBg: "#21222c",
175
+ highlight: "#ffffff",
176
+ codeKeyword: "#ff79c6",
177
+ codeString: "#f1fa8c",
178
+ codeNumber: "#bd93f9",
179
+ codeComment: "#6272a4",
180
+ codeFunction: "#50fa7b",
181
+ codeType: "#8be9fd",
182
+ };
183
+
184
+ const rosepine: Theme = {
185
+ name: "rosepine",
186
+ bg: "#191724",
187
+ fg: "#e0def4",
188
+ dim: "#6e6a86",
189
+ accent: "#c4a7e7",
190
+ border: "#26233a",
191
+ user: "#e0def4",
192
+ userBg: "#26233a",
193
+ agentMessage: "#c4a7e7",
194
+ agentMessageBg: "#302940",
195
+ assistant: "#e0def4",
196
+ thinking: "#6e6a86",
197
+ tool: "#f6c177",
198
+ toolArg: "#9ccfd8",
199
+ success: "#31748f",
200
+ error: "#eb6f92",
201
+ warn: "#f6c177",
202
+ selectionBg: "#403d52",
203
+ popupBg: "#1f1d2e",
204
+ highlight: "#fffaf3",
205
+ codeKeyword: "#c4a7e7",
206
+ codeString: "#9ccfd8",
207
+ codeNumber: "#ebbcba",
208
+ codeComment: "#6e6a86",
209
+ codeFunction: "#31748f",
210
+ codeType: "#f6c177",
211
+ };
212
+
213
+ const solarized: Theme = {
214
+ name: "solarized",
215
+ bg: "#002b36",
216
+ fg: "#839496",
217
+ dim: "#586e75",
218
+ accent: "#268bd2",
219
+ border: "#073642",
220
+ user: "#93a1a1",
221
+ userBg: "#073642",
222
+ agentMessage: "#6c71c4",
223
+ agentMessageBg: "#173b4a",
224
+ assistant: "#93a1a1",
225
+ thinking: "#586e75",
226
+ tool: "#b58900",
227
+ toolArg: "#859900",
228
+ success: "#859900",
229
+ error: "#dc322f",
230
+ warn: "#cb4b16",
231
+ selectionBg: "#0b4b59",
232
+ popupBg: "#073642",
233
+ highlight: "#fdf6e3",
234
+ codeKeyword: "#6c71c4",
235
+ codeString: "#859900",
236
+ codeNumber: "#d33682",
237
+ codeComment: "#586e75",
238
+ codeFunction: "#268bd2",
239
+ codeType: "#2aa198",
240
+ };
241
+
242
+ const kanagawa: Theme = {
243
+ name: "kanagawa",
244
+ bg: "#1f1f28",
245
+ fg: "#dcd7ba",
246
+ dim: "#727169",
247
+ accent: "#7e9cd8",
248
+ border: "#2a2a37",
249
+ user: "#dcd7ba",
250
+ userBg: "#2d4f67",
251
+ agentMessage: "#957fb8",
252
+ agentMessageBg: "#3a3048",
253
+ assistant: "#dcd7ba",
254
+ thinking: "#727169",
255
+ tool: "#e6c384",
256
+ toolArg: "#98bb6c",
257
+ success: "#98bb6c",
258
+ error: "#e46876",
259
+ warn: "#ffa066",
260
+ selectionBg: "#2d4f67",
261
+ popupBg: "#16161d",
262
+ highlight: "#c8c093",
263
+ codeKeyword: "#957fb8",
264
+ codeString: "#98bb6c",
265
+ codeNumber: "#d27e99",
266
+ codeComment: "#727169",
267
+ codeFunction: "#7e9cd8",
268
+ codeType: "#7fb4ca",
269
+ };
270
+
271
+ const githubLight: Theme = {
272
+ name: "github-light",
273
+ bg: "#ffffff",
274
+ fg: "#24292f",
275
+ dim: "#6e7781",
276
+ accent: "#0969da",
277
+ border: "#d0d7de",
278
+ user: "#24292f",
279
+ userBg: "#ddf4ff",
280
+ agentMessage: "#8250df",
281
+ agentMessageBg: "#f3e8ff",
282
+ assistant: "#24292f",
283
+ thinking: "#6e7781",
284
+ tool: "#9a6700",
285
+ toolArg: "#1a7f37",
286
+ success: "#1a7f37",
287
+ error: "#cf222e",
288
+ warn: "#bc4c00",
289
+ selectionBg: "#b6d7ff",
290
+ popupBg: "#f6f8fa",
291
+ highlight: "#000000",
292
+ codeKeyword: "#cf222e",
293
+ codeString: "#0a3069",
294
+ codeNumber: "#0550ae",
295
+ codeComment: "#6e7781",
296
+ codeFunction: "#8250df",
297
+ codeType: "#953800",
298
+ };
299
+
300
+ export const PRESETS: Record<string, Theme> = {
301
+ tokyonight,
302
+ gruvbox,
303
+ catppuccin,
304
+ nord,
305
+ dracula,
306
+ rosepine,
307
+ solarized,
308
+ kanagawa,
309
+ "github-light": githubLight,
310
+ };
311
+ export const PRESET_NAMES = Object.keys(PRESETS);
312
+
313
+ const THEME_PATH = join(AGENT_DIR, "theme.json");
314
+
315
+ /**
316
+ * A preset, with <AGENT_DIR>/theme.json merged over it. The file may set any
317
+ * subset of tokens; unknown keys are ignored and a bad file is ignored whole.
318
+ */
319
+ export function loadTheme(name: string): Theme {
320
+ const base = PRESETS[name] ?? tokyonight;
321
+ let override: Partial<Theme> = {};
322
+ try {
323
+ override = JSON.parse(readFileSync(THEME_PATH, "utf8"));
324
+ } catch {
325
+ // no file, or unreadable — the preset stands on its own
326
+ }
327
+ const merged = { ...base };
328
+ for (const key of Object.keys(base) as (keyof Theme)[]) {
329
+ const value = override[key];
330
+ if (typeof value === "string") merged[key] = value;
331
+ }
332
+ merged.name = base.name;
333
+ return merged;
334
+ }
335
+
336
+ /** @opentui/core exports RGBA and parseColor but no interpolation helper. */
337
+ export function mix(a: RGBA, b: RGBA, t: number): RGBA {
338
+ return RGBA.fromValues(
339
+ a.r + (b.r - a.r) * t,
340
+ a.g + (b.g - a.g) * t,
341
+ a.b + (b.b - a.b) * t,
342
+ 1,
343
+ );
344
+ }
345
+
346
+ export const rgba = (color: string): RGBA => parseColor(color);
@@ -0,0 +1,72 @@
1
+ import { relative } from "node:path";
2
+
3
+ export type ToolCall = {
4
+ id: string;
5
+ name: string;
6
+ /** The one argument worth showing for this tool. */
7
+ arg: string;
8
+ state: "running" | "ok" | "error" | "rejected";
9
+ /** "+3 −1" for edits, or an error note. */
10
+ detail?: string;
11
+ };
12
+
13
+ /** Tool args are typed `any`, so every access here is defensive. */
14
+ export function toolArg(name: string, args: any, cwd: string): string {
15
+ if (!args || typeof args !== "object") return "";
16
+
17
+ if (name === "bash" && typeof args.command === "string") {
18
+ return args.command.split("\n")[0]!.trim();
19
+ }
20
+ if (name === "apply_patch" && typeof args.patch === "string") {
21
+ const paths: string[] = [];
22
+ let updateIndex = -1;
23
+ for (const line of args.patch.replace(/\r\n/g, "\n").split("\n")) {
24
+ const file = line.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
25
+ if (file) {
26
+ paths.push(file[2]!.trim().replaceAll("\\", "/"));
27
+ updateIndex = file[1] === "Update" ? paths.length - 1 : -1;
28
+ continue;
29
+ }
30
+ const move = line.match(/^\*\*\* Move to: (.+)$/);
31
+ if (move && updateIndex >= 0) {
32
+ paths[updateIndex] = `${paths[updateIndex]} → ${move[1]!.trim().replaceAll("\\", "/")}`;
33
+ }
34
+ }
35
+ if (paths.length === 1) return paths[0]!;
36
+ if (paths.length > 1) return `${paths.length} files · ${paths[0]}`;
37
+ }
38
+ if (name === "spawn_subagent" && typeof args.task === "string") {
39
+ return typeof args.name === "string" ? `${args.name} · ${args.task}` : args.task;
40
+ }
41
+ if (name === "message_agent" && typeof args.target === "string") {
42
+ return typeof args.message === "string" ? `${args.target} · ${args.message}` : args.target;
43
+ }
44
+ if (name === "stop_subagent" && typeof args.target === "string") return args.target;
45
+ if (name === "finish_subagent" && typeof args.summary === "string") return args.summary;
46
+ if (name === "worktree" && typeof args.action === "string") {
47
+ const target = args.target ?? args.name;
48
+ return target ? `${args.action} ${target}` : args.action;
49
+ }
50
+ if (typeof args.path === "string") {
51
+ const rel = relative(cwd, args.path);
52
+ return rel && !rel.startsWith("..") ? rel : args.path;
53
+ }
54
+ const first = Object.values(args).find((v) => typeof v === "string");
55
+ return typeof first === "string" ? first.split("\n")[0]! : "";
56
+ }
57
+
58
+ /** Count changed lines from a tool result's unified patch details. */
59
+ export function editCounts(result: any): string | undefined {
60
+ const patch = result?.details?.patch;
61
+ if (typeof patch !== "string") return undefined;
62
+
63
+ let added = 0;
64
+ let removed = 0;
65
+ for (const line of patch.split("\n")) {
66
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
67
+ if (line.startsWith("+")) added++;
68
+ else if (line.startsWith("-")) removed++;
69
+ }
70
+ if (!added && !removed) return undefined;
71
+ return `+${added} −${removed}`;
72
+ }