takomi 2.0.7 → 2.1.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.
Files changed (66) hide show
  1. package/.pi/README.md +124 -0
  2. package/.pi/agents/architect.md +16 -0
  3. package/.pi/agents/coder.md +15 -0
  4. package/.pi/agents/designer.md +18 -0
  5. package/.pi/agents/orchestrator.md +23 -0
  6. package/.pi/agents/reviewer.md +17 -0
  7. package/.pi/extensions/oauth-router/README.md +125 -0
  8. package/.pi/extensions/oauth-router/commands.ts +380 -0
  9. package/.pi/extensions/oauth-router/config.ts +200 -0
  10. package/.pi/extensions/oauth-router/index.ts +41 -0
  11. package/.pi/extensions/oauth-router/oauth-flow.ts +154 -0
  12. package/.pi/extensions/oauth-router/oauth-store.ts +121 -0
  13. package/.pi/extensions/oauth-router/package.json +14 -0
  14. package/.pi/extensions/oauth-router/policies.ts +27 -0
  15. package/.pi/extensions/oauth-router/provider.ts +492 -0
  16. package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -0
  17. package/.pi/extensions/oauth-router/state.ts +174 -0
  18. package/.pi/extensions/oauth-router/types.ts +153 -0
  19. package/.pi/extensions/takomi-runtime/command-text.ts +130 -0
  20. package/.pi/extensions/takomi-runtime/commands.ts +179 -0
  21. package/.pi/extensions/takomi-runtime/context-panel.ts +282 -0
  22. package/.pi/extensions/takomi-runtime/index.ts +1288 -0
  23. package/.pi/extensions/takomi-runtime/profile.ts +114 -0
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -0
  25. package/.pi/extensions/takomi-runtime/shared.ts +492 -0
  26. package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -0
  27. package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -0
  28. package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -0
  29. package/.pi/extensions/takomi-runtime/ui.ts +133 -0
  30. package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -0
  31. package/.pi/extensions/takomi-subagents/agents.ts +113 -0
  32. package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -0
  33. package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -0
  34. package/.pi/extensions/takomi-subagents/dispatch.ts +215 -0
  35. package/.pi/extensions/takomi-subagents/index.ts +75 -0
  36. package/.pi/extensions/takomi-subagents/live-updates.ts +83 -0
  37. package/.pi/extensions/takomi-subagents/native-render.ts +174 -0
  38. package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -0
  39. package/.pi/prompts/build-prompt.md +199 -0
  40. package/.pi/prompts/design-prompt.md +134 -0
  41. package/.pi/prompts/genesis-prompt.md +133 -0
  42. package/.pi/prompts/orch-prompt.md +144 -0
  43. package/.pi/prompts/prime-prompt.md +80 -0
  44. package/.pi/prompts/takomi-prompt.md +96 -0
  45. package/.pi/prompts/vibe-primeAgent.md +97 -0
  46. package/.pi/prompts/vibe-spawnTask.md +133 -0
  47. package/.pi/prompts/vibe-syncDocs.md +100 -0
  48. package/.pi/themes/takomi-noir.json +81 -0
  49. package/README.md +28 -2
  50. package/assets/.agent/skills/pr-comment-fix/SKILL.md +182 -0
  51. package/assets/.agent/skills/takomi/SKILL.md +59 -59
  52. package/package.json +58 -45
  53. package/src/cli.js +158 -8
  54. package/src/doctor.js +84 -0
  55. package/src/pi-harness.js +351 -0
  56. package/src/pi-installer.js +171 -0
  57. package/src/pi-takomi-core/index.ts +4 -0
  58. package/src/pi-takomi-core/orchestration.ts +402 -0
  59. package/src/pi-takomi-core/routing.ts +93 -0
  60. package/src/pi-takomi-core/types.ts +173 -0
  61. package/src/pi-takomi-core/workflows.ts +299 -0
  62. package/src/skills-installer.js +101 -0
  63. package/src/utils.js +479 -447
  64. package/assets/.agent/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-311.pyc +0 -0
  65. package/assets/.agent/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  66. package/assets/.agent/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-311.pyc +0 -0
@@ -0,0 +1,501 @@
1
+ import type { Theme } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ ellipsizeMiddle,
4
+ firstMeaningfulLine,
5
+ formatDuration,
6
+ truncateToWidth,
7
+ wrapLabel,
8
+ } from "./shared";
9
+ import type {
10
+ SubagentViewMode,
11
+ TakomiSubagentRenderEntry,
12
+ TakomiSubagentRenderState,
13
+ TakomiBoardTaskStatus,
14
+ TakomiSubagentRun,
15
+ TakomiSubagentStatus,
16
+ } from "./subagent-types";
17
+
18
+ interface Component {
19
+ render(width: number): string[];
20
+ handleInput?(data: string): void;
21
+ invalidate(): void;
22
+ dispose?(): void;
23
+ }
24
+
25
+ interface RenderTui {
26
+ requestRender(force?: boolean): void;
27
+ }
28
+
29
+ type Tone = "accent" | "warning" | "success" | "error" | "muted" | "dim" | "thinkingMinimal";
30
+
31
+ const EXPANDED_WIDGET_OUTPUT_LINES = 2;
32
+ const EXPANDED_WIDGET_ACTIVITY_LINES = 1;
33
+ const FULLSCREEN_MAX_VISIBLE_LINES = 15;
34
+ const FULLSCREEN_PAGE_STEP = 5;
35
+
36
+ function statusTone(status: TakomiSubagentStatus): Tone {
37
+ return status === "blocked" ? "error" : status === "completed" ? "success" : "warning";
38
+ }
39
+
40
+ function statusLabel(status: TakomiSubagentStatus): string {
41
+ return status === "blocked" ? "BLOCKED" : status === "completed" ? "COMPLETED" : "RUNNING";
42
+ }
43
+
44
+ function statusIcon(status: TakomiSubagentStatus): string {
45
+ return status === "blocked" ? "✖" : status === "completed" ? "✔" : "⠧";
46
+ }
47
+
48
+ function summaryText(run: TakomiSubagentRun): string {
49
+ const fallback = isBoardBackedRun(run) && run.status === "completed" && run.boardTaskStatus !== "completed"
50
+ ? "Subagent run finished. Board task is still awaiting checklist/status completion."
51
+ : "Waiting for live events.";
52
+ const summary = firstMeaningfulLine(run.outputText) ?? firstMeaningfulLine(run.summary) ?? firstMeaningfulLine(run.logs.at(-1)) ?? fallback;
53
+ return summary.length > 140 ? `${summary.slice(0, 137)}...` : summary;
54
+ }
55
+
56
+ function checklistText(run: TakomiSubagentRun): string | undefined {
57
+ if (!run.checklist?.length) return undefined;
58
+ const normalized = run.checklist.map((item) => (typeof item === "string" ? { text: item, done: false } : item));
59
+ const done = normalized.filter((item) => item.done).length;
60
+ return `${done}/${normalized.length}`;
61
+ }
62
+
63
+ function metadata(run: TakomiSubagentRun, includeThread: boolean): string[] {
64
+ const lines = [`agent:${run.agent}`];
65
+ if (run.stage) lines.push(`stage:${run.stage}`);
66
+ if (run.workflow) lines.push(`flow:${run.workflow}`);
67
+ if (run.model) lines.push(`model:${ellipsizeMiddle(run.model, 28)}`);
68
+ if (run.thinking) lines.push(`think:${run.thinking}`);
69
+ if (run.fallbackModels?.length) lines.push(`fallbacks:${run.fallbackModels.length}`);
70
+ if (includeThread && run.conversationId) lines.push(`thread:${ellipsizeMiddle(run.conversationId, 20)}`);
71
+ if (isBoardBackedRun(run)) {
72
+ const taskState = boardStatusLabel(run.boardTaskStatus);
73
+ if (taskState) lines.push(`task:${taskState}`);
74
+ lines.push(`run:${runLifecycleLabel(run)}`);
75
+ }
76
+ return lines;
77
+ }
78
+
79
+ function isBoardBackedRun(run: TakomiSubagentRun): boolean {
80
+ return run.source === "runtime-board" || Boolean(run.boardTaskStatus);
81
+ }
82
+
83
+ function boardStatusLabel(status?: TakomiBoardTaskStatus): string | undefined {
84
+ return status;
85
+ }
86
+
87
+ function runLifecycleLabel(run: TakomiSubagentRun): string {
88
+ return run.status === "blocked" ? "blocked" : run.status === "completed" ? "finished" : "running";
89
+ }
90
+
91
+ function displayTone(run: TakomiSubagentRun): Tone {
92
+ if (run.boardTaskStatus === "blocked" || run.status === "blocked") return "error";
93
+ if (run.boardTaskStatus === "completed") return "success";
94
+ if (isBoardBackedRun(run) && run.status === "completed") return "accent";
95
+ return "warning";
96
+ }
97
+
98
+ function displayLabel(run: TakomiSubagentRun): string {
99
+ if (run.boardTaskStatus === "blocked" || run.status === "blocked") return "BLOCKED";
100
+ if (run.boardTaskStatus === "completed") return "COMPLETED";
101
+ if (isBoardBackedRun(run) && run.status === "completed") return "FINISHED";
102
+ if (run.boardTaskStatus === "pending") return "PENDING";
103
+ return "RUNNING";
104
+ }
105
+
106
+ function displayIcon(run: TakomiSubagentRun): string {
107
+ if (run.boardTaskStatus === "blocked" || run.status === "blocked") return "x";
108
+ if (run.boardTaskStatus === "completed") return "+";
109
+ if (isBoardBackedRun(run) && run.status === "completed") return ">";
110
+ return "~";
111
+ }
112
+
113
+ function outputParagraphs(run: TakomiSubagentRun): string[] {
114
+ if (!run.outputText?.trim()) return [];
115
+ return run.outputText
116
+ .split(/\n\s*\n/)
117
+ .map((paragraph) => paragraph.replace(/\s*\n\s*/g, " ").replace(/\s+/g, " ").trim())
118
+ .filter(Boolean);
119
+ }
120
+
121
+ function renderWrappedBlock(
122
+ theme: Theme,
123
+ tone: Tone,
124
+ prefix: string,
125
+ text: string,
126
+ wrapWidth: number,
127
+ width: number,
128
+ ): string[] {
129
+ return wrapLabel(text, wrapWidth)
130
+ .map((line) => theme.fg(tone, truncateToWidth(`${prefix}${line}`, width)));
131
+ }
132
+
133
+ function renderWrappedTail(
134
+ theme: Theme,
135
+ tone: Tone,
136
+ prefix: string,
137
+ lines: string[],
138
+ wrapWidth: number,
139
+ width: number,
140
+ limit: number,
141
+ ): string[] {
142
+ if (limit <= 0) return [];
143
+ const rendered = lines.flatMap((line) => renderWrappedBlock(theme, tone, prefix, line, wrapWidth, width));
144
+ return rendered.slice(-limit);
145
+ }
146
+
147
+ function viewHint(theme: Theme, mode: SubagentViewMode): string {
148
+ const hints = {
149
+ compact: "Alt+T: expand | Alt+Shift+T: fullscreen | Alt+N/P: focus",
150
+ expanded: "Alt+T: fullscreen | Alt+Shift+T: fullscreen | Alt+N/P: focus",
151
+ fullscreen: "Esc: back | Up/Down/PgUp/PgDn: scroll | End: live",
152
+ };
153
+ return theme.fg("muted", hints[mode]);
154
+ }
155
+
156
+ function relationPrefix(entry: TakomiSubagentRenderEntry): string {
157
+ if (entry.relation === "focused") return "▼";
158
+ if (entry.relation === "ancestor") return "├─";
159
+ return "│ ";
160
+ }
161
+
162
+ function renderCompactCard(theme: Theme, entry: TakomiSubagentRenderEntry, width: number): string[] {
163
+ const run = entry.run;
164
+ const isFocused = entry.relation === "focused";
165
+ const tone = isFocused ? displayTone(run) : "muted";
166
+ const badge = theme.fg(tone, `[${displayLabel(run)}]`);
167
+
168
+ const indent = " ".repeat(entry.depth * 2);
169
+ const prefix = relationPrefix(entry);
170
+
171
+ const line1 = truncateToWidth(
172
+ `${indent}${theme.fg(tone, prefix)} ${theme.fg(isFocused ? "accent" : tone, run.agent)} ${badge} ${theme.fg("dim", run.taskLabel)}`,
173
+ width,
174
+ );
175
+
176
+ const metaParts = [
177
+ ...metadata(run, false),
178
+ checklistText(run) ? `tasks:${checklistText(run)}` : "",
179
+ run.logs.length ? `activity:${run.logs.length}` : "",
180
+ formatDuration(Date.now() - run.startedAt),
181
+ ].filter(Boolean);
182
+
183
+ const branch = isFocused ? "└─" : "│ ";
184
+ const line2 = truncateToWidth(`${indent}${theme.fg(tone, branch)} ${theme.fg("dim", metaParts.join(" | "))}`, width);
185
+
186
+ return [line1, line2];
187
+ }
188
+
189
+ function renderHeader(theme: Theme, state: TakomiSubagentRenderState, width: number): string {
190
+ const label = [
191
+ theme.fg("dim", "[ takomi ]"),
192
+ theme.fg("accent", "ACTIVE RUNTIME"),
193
+ theme.fg("dim", `${state.focusPosition}/${state.activeCount}`),
194
+ ].join(" - ");
195
+ return truncateToWidth(label, width);
196
+ }
197
+
198
+ function widgetStackEntries(state: TakomiSubagentRenderState): TakomiSubagentRenderEntry[] {
199
+ const focused = state.activePath.at(-1);
200
+ const parent = state.activePath.at(-2);
201
+ if (!focused) return [];
202
+ if (parent) return [parent, focused];
203
+ const firstPeer = state.peerRuns[0];
204
+ return firstPeer ? [focused, firstPeer] : [focused];
205
+ }
206
+
207
+ function renderExpandedWidget(theme: Theme, state: TakomiSubagentRenderState, width: number): string[] {
208
+ const focusedRun = state.focusedRun;
209
+ if (!focusedRun) return [theme.fg("dim", "No active Takomi subagent.")];
210
+
211
+ const lines = [renderHeader(theme, state, width), ""];
212
+ for (const entry of widgetStackEntries(state)) {
213
+ lines.push(...renderCompactCard(theme, entry, width));
214
+ }
215
+
216
+ lines.push("");
217
+
218
+ const checklist = checklistText(focusedRun);
219
+ if (checklist) {
220
+ lines.push(theme.fg("warning", truncateToWidth(`Checklist ${checklist}`, width)));
221
+ }
222
+
223
+ lines.push(theme.fg("thinkingMinimal", truncateToWidth(`Summary: ${summaryText(focusedRun)}`, width)));
224
+ lines.push(theme.fg("dim", "Live output:"));
225
+
226
+ const outputTail = renderWrappedTail(
227
+ theme,
228
+ "dim",
229
+ " ",
230
+ outputParagraphs(focusedRun).slice(-1),
231
+ Math.max(24, width - 4),
232
+ width,
233
+ EXPANDED_WIDGET_OUTPUT_LINES,
234
+ );
235
+ if (outputTail.length > 0) {
236
+ lines.push(...outputTail);
237
+ } else {
238
+ lines.push(theme.fg("dim", truncateToWidth(" Waiting for first live output...", width)));
239
+ }
240
+
241
+ if (focusedRun.logs.length > 0) {
242
+ lines.push(theme.fg("muted", "Recent activity:"));
243
+ lines.push(
244
+ ...renderWrappedTail(
245
+ theme,
246
+ "muted",
247
+ " - ",
248
+ focusedRun.logs.slice(-1),
249
+ Math.max(24, width - 6),
250
+ width,
251
+ EXPANDED_WIDGET_ACTIVITY_LINES,
252
+ ),
253
+ );
254
+ }
255
+
256
+ if (state.activeCount > 1) {
257
+ const extra = state.activeCount - 1;
258
+ lines.push(theme.fg("muted", truncateToWidth(`Also active: ${extra} other run${extra === 1 ? "" : "s"}`, width)));
259
+ }
260
+
261
+ lines.push("");
262
+ lines.push(viewHint(theme, state.mode));
263
+ return lines;
264
+ }
265
+
266
+ function renderExpandedPath(theme: Theme, state: TakomiSubagentRenderState, width: number, fullscreen: boolean): string[] {
267
+ const lines: string[] = [];
268
+ const detailWidth = Math.max(32, width - 4);
269
+
270
+ for (let i = 0; i < state.activePath.length; i++) {
271
+ const entry = state.activePath[i];
272
+ const run = entry.run;
273
+ const isFocused = entry.relation === "focused";
274
+ const tone = isFocused ? displayTone(run) : "muted";
275
+
276
+ const prefix = relationPrefix(entry);
277
+ const indentStr = " ".repeat(entry.depth * 2);
278
+ const innerIndent = indentStr + (isFocused ? "│ " : "│ ");
279
+
280
+ const agentHeader = theme.fg(tone, `${indentStr}${prefix} ${run.agent}`);
281
+ const taskHeader = theme.fg("dim", run.taskLabel);
282
+ const badge = theme.fg(tone, `[${displayLabel(run)}]`);
283
+
284
+ lines.push(truncateToWidth(`${agentHeader} ${badge} ${taskHeader}`, width));
285
+
286
+ const metaParts = metadata(run, true);
287
+ if (metaParts.length > 0) {
288
+ lines.push(theme.fg("muted", truncateToWidth(`${innerIndent}${metaParts.join(" | ")}`, width)));
289
+ }
290
+
291
+ if (!isFocused) continue;
292
+
293
+ lines.push(theme.fg(tone, innerIndent));
294
+
295
+ if (run.checklist?.length) {
296
+ const normalized = run.checklist.map((item) => (typeof item === "string" ? { text: item, done: false } : item));
297
+ lines.push(theme.fg(tone, `${innerIndent}Checklist ${checklistText(run)}`));
298
+ for (const item of normalized.slice(0, fullscreen ? 10 : 5)) {
299
+ const icon = item.done ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
300
+ lines.push(truncateToWidth(`${innerIndent} ${icon} ${item.text}`, width));
301
+ }
302
+ lines.push(theme.fg(tone, innerIndent));
303
+ }
304
+
305
+ lines.push(theme.fg("thinkingMinimal", truncateToWidth(`${innerIndent}Summary: ${summaryText(run)}`, width)));
306
+
307
+ lines.push(theme.fg("dim", `${innerIndent}Output:`));
308
+ const paragraphs = outputParagraphs(run);
309
+ if (paragraphs.length === 0) {
310
+ lines.push(theme.fg("dim", truncateToWidth(`${innerIndent} Waiting for first live output...`, width)));
311
+ } else {
312
+ const outputWrapWidth = detailWidth - entry.depth * 2 - 2;
313
+ paragraphs.slice(0, fullscreen ? 4 : 2).forEach((paragraph, paragraphIndex) => {
314
+ lines.push(...renderWrappedBlock(theme, "dim", `${innerIndent} `, paragraph, outputWrapWidth, width));
315
+ if (paragraphIndex < Math.min(paragraphs.length, fullscreen ? 4 : 2) - 1) {
316
+ lines.push(theme.fg("dim", innerIndent));
317
+ }
318
+ });
319
+ }
320
+
321
+ if (run.logs.length > 0) {
322
+ const logTail = run.logs.slice(-(fullscreen ? 6 : 3));
323
+ lines.push(theme.fg("muted", `${innerIndent}Activity:`));
324
+ for (const logLine of logTail) {
325
+ lines.push(...renderWrappedBlock(theme, "muted", `${innerIndent} - `, logLine, detailWidth - entry.depth * 2 - 4, width).slice(0, 2));
326
+ }
327
+ }
328
+
329
+ if (i === state.activePath.length - 1) {
330
+ lines.push(theme.fg(tone, `${indentStr}└${"─".repeat(Math.max(10, width - indentStr.length - 1))}`));
331
+ }
332
+ }
333
+ return lines;
334
+ }
335
+
336
+ function renderPeerSection(theme: Theme, state: TakomiSubagentRenderState, width: number): string[] {
337
+ if (state.peerRuns.length === 0) return [];
338
+ const lines = ["", theme.fg("muted", "--- Background / Queued ---")];
339
+ for (const entry of state.peerRuns.slice(0, 6)) {
340
+ const run = entry.run;
341
+ const tone = displayTone(run);
342
+ const badge = theme.fg(tone, `[${displayLabel(run)}]`);
343
+ const line = truncateToWidth(` ${badge} ${run.agent} ${summaryText(run)}`, width);
344
+ lines.push(theme.fg("dim", line));
345
+ }
346
+ return lines;
347
+ }
348
+
349
+ export function renderSubagentWidget(theme: Theme, state: TakomiSubagentRenderState, width = 110): string[] {
350
+ if (!state.focusedRun) return [theme.fg("dim", "No active Takomi subagent.")];
351
+
352
+ if (state.mode === "compact") {
353
+ const lines = [renderHeader(theme, state, width), ""];
354
+ for (const entry of state.compactRuns) {
355
+ lines.push(...renderCompactCard(theme, entry, width));
356
+ }
357
+ const indent = " ".repeat(Math.max(0, (state.compactRuns.length - 1) * 2));
358
+ lines.push(theme.fg("thinkingMinimal", truncateToWidth(`${indent} Summary: ${summaryText(state.focusedRun)}`, width)));
359
+ lines.push("");
360
+ lines.push(viewHint(theme, state.mode));
361
+ return lines;
362
+ }
363
+
364
+ return renderExpandedWidget(theme, state, width);
365
+ }
366
+
367
+ export function renderSubagentStatus(theme: Theme, state: TakomiSubagentRenderState): string | undefined {
368
+ const run = state.focusedRun;
369
+ if (!run) return undefined;
370
+ const tone = displayTone(run);
371
+ const parts = [
372
+ theme.fg(tone, displayIcon(run)),
373
+ theme.fg("dim", `${run.agent} ${displayLabel(run).toLowerCase()} ${ellipsizeMiddle(run.taskLabel, 36)}`),
374
+ run.model ? theme.fg("dim", ellipsizeMiddle(run.model, 24)) : "",
375
+ run.thinking ? theme.fg("dim", `think:${run.thinking}`) : "",
376
+ state.activeCount > 1 ? theme.fg("dim", `+${state.activeCount - 1} more`) : "",
377
+ ].filter(Boolean);
378
+ return parts.join(" ");
379
+ }
380
+
381
+ export class FullscreenSubagentComponent implements Component {
382
+ private readonly timer: ReturnType<typeof setInterval>;
383
+ private scrollOffset = 0;
384
+ private autoFollow = true;
385
+ private lastFocusedRunKey?: string;
386
+
387
+ constructor(
388
+ private readonly tui: RenderTui,
389
+ private readonly theme: Theme,
390
+ private readonly getState: () => TakomiSubagentRenderState,
391
+ private readonly onEscape: () => void,
392
+ private readonly onToggle: () => void,
393
+ private readonly onNextFocus: () => void,
394
+ private readonly onPrevFocus: () => void,
395
+ ) {
396
+ this.timer = setInterval(() => {
397
+ this.tui.requestRender();
398
+ }, 1000);
399
+ }
400
+
401
+ dispose(): void {
402
+ clearInterval(this.timer);
403
+ }
404
+
405
+ invalidate(): void {}
406
+
407
+ handleInput(data: string): void {
408
+ if (data === "\x1b") {
409
+ this.onEscape();
410
+ return;
411
+ }
412
+ if (data === "\x1b[A" || data === "k") {
413
+ this.scrollBy(-1);
414
+ return;
415
+ }
416
+ if (data === "\x1b[B" || data === "j") {
417
+ this.scrollBy(1);
418
+ return;
419
+ }
420
+ if (data === "\x1b[5~") {
421
+ this.scrollBy(-FULLSCREEN_PAGE_STEP);
422
+ return;
423
+ }
424
+ if (data === "\x1b[6~" || data === " ") {
425
+ this.scrollBy(FULLSCREEN_PAGE_STEP);
426
+ return;
427
+ }
428
+ if (data === "\x1b[F" || data === "\x1bOF" || data === "G") {
429
+ this.autoFollow = true;
430
+ this.tui.requestRender();
431
+ return;
432
+ }
433
+ if (data === "\x1b[H" || data === "\x1bOH" || data === "g") {
434
+ this.autoFollow = false;
435
+ this.scrollOffset = 0;
436
+ this.tui.requestRender();
437
+ return;
438
+ }
439
+ if (data === "\x1bt" || data === "\x1b\x74") {
440
+ this.onToggle();
441
+ return;
442
+ }
443
+ if (data === "\x1bn" || data === "\x1b\x6e") {
444
+ this.onNextFocus();
445
+ return;
446
+ }
447
+ if (data === "\x1bp" || data === "\x1b\x70") {
448
+ this.onPrevFocus();
449
+ }
450
+ }
451
+
452
+ private scrollBy(delta: number): void {
453
+ this.autoFollow = false;
454
+ this.scrollOffset = Math.max(0, this.scrollOffset + delta);
455
+ this.tui.requestRender();
456
+ }
457
+
458
+ render(width: number): string[] {
459
+ const state = this.getState();
460
+ if (!state.focusedRun) {
461
+ return [
462
+ this.theme.fg("dim", "No active Takomi subagent."),
463
+ "",
464
+ viewHint(this.theme, "fullscreen"),
465
+ ];
466
+ }
467
+
468
+ const contentLines = [
469
+ ...renderExpandedPath(this.theme, state, width, true),
470
+ ...renderPeerSection(this.theme, state, width),
471
+ ];
472
+ const focusedRunKey = state.focusedRun.runKey;
473
+ if (focusedRunKey !== this.lastFocusedRunKey) {
474
+ this.lastFocusedRunKey = focusedRunKey;
475
+ this.autoFollow = true;
476
+ }
477
+
478
+ const maxOffset = Math.max(0, contentLines.length - FULLSCREEN_MAX_VISIBLE_LINES);
479
+ if (this.autoFollow || this.scrollOffset > maxOffset) {
480
+ this.scrollOffset = maxOffset;
481
+ }
482
+ this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset));
483
+
484
+ const visibleLines = contentLines.slice(this.scrollOffset, this.scrollOffset + FULLSCREEN_MAX_VISIBLE_LINES);
485
+ while (visibleLines.length < FULLSCREEN_MAX_VISIBLE_LINES) visibleLines.push("");
486
+
487
+ const remainingBelow = Math.max(0, maxOffset - this.scrollOffset);
488
+ const scrollLabel = this.autoFollow && remainingBelow === 0
489
+ ? "Scroll: live"
490
+ : `Scroll: up:${this.scrollOffset} down:${remainingBelow}`;
491
+
492
+ return [
493
+ this.theme.fg("dim", "=".repeat(Math.max(20, width))),
494
+ renderHeader(this.theme, state, width),
495
+ this.theme.fg("muted", truncateToWidth(scrollLabel, width)),
496
+ ...visibleLines,
497
+ viewHint(this.theme, "fullscreen"),
498
+ this.theme.fg("dim", "=".repeat(Math.max(20, width))),
499
+ ];
500
+ }
501
+ }
@@ -0,0 +1,83 @@
1
+ import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
2
+ import type { ChecklistInput } from "./shared";
3
+
4
+ export type SubagentViewMode = "compact" | "expanded" | "fullscreen";
5
+ export type SubagentFocusDirection = "next" | "prev";
6
+ export type TakomiSubagentStatus = "running" | "completed" | "blocked";
7
+ export type TakomiBoardTaskStatus = "pending" | "in-progress" | "completed" | "blocked";
8
+ export type TakomiSubagentSource = "runtime-board" | "takomi-tool";
9
+ export const TAKOMI_SUBAGENT_EVENT_CHANNEL = "takomi:subagent-runtime";
10
+
11
+ export type TakomiSubagentRun = {
12
+ runKey: string;
13
+ conversationId?: string;
14
+ parentTaskId?: string;
15
+ parentRunKey?: string;
16
+ agent: string;
17
+ taskLabel: string;
18
+ status: TakomiSubagentStatus;
19
+ stage?: string;
20
+ workflow?: string;
21
+ model?: string;
22
+ fallbackModels?: string[];
23
+ thinking?: TakomiThinkingLevel;
24
+ checklist?: ChecklistInput;
25
+ boardTaskStatus?: TakomiBoardTaskStatus;
26
+ summary?: string;
27
+ outputText?: string;
28
+ logs: string[];
29
+ startedAt: number;
30
+ updatedAt: number;
31
+ source: TakomiSubagentSource;
32
+ };
33
+
34
+ export type TakomiSubagentRunInit = Omit<TakomiSubagentRun, "runKey" | "logs" | "startedAt" | "updatedAt" | "status"> & {
35
+ logs?: string[];
36
+ status?: TakomiSubagentStatus;
37
+ };
38
+
39
+ export type TakomiSubagentRunPatch = Partial<Omit<TakomiSubagentRun, "runKey" | "startedAt" | "source">> & {
40
+ source?: TakomiSubagentSource;
41
+ };
42
+
43
+ export type TakomiSubagentRenderEntry = {
44
+ run: TakomiSubagentRun;
45
+ depth: number;
46
+ relation: "focused" | "ancestor" | "peer";
47
+ };
48
+
49
+ export type TakomiSubagentRenderState = {
50
+ mode: SubagentViewMode;
51
+ activeCount: number;
52
+ focusPosition: number;
53
+ focusedRun?: TakomiSubagentRun;
54
+ activePath: TakomiSubagentRenderEntry[];
55
+ peerRuns: TakomiSubagentRenderEntry[];
56
+ compactRuns: TakomiSubagentRenderEntry[];
57
+ };
58
+
59
+ export interface TakomiSubagentController {
60
+ hasRuns(): boolean;
61
+ getStatusSummary(): string;
62
+ getViewMode(): SubagentViewMode;
63
+ start(ctx: import("@mariozechner/pi-coding-agent").ExtensionContext, state: TakomiSubagentRunInit, runKey?: string): Promise<void>;
64
+ update(ctx: import("@mariozechner/pi-coding-agent").ExtensionContext, patch: TakomiSubagentRunPatch, runKey?: string): Promise<void>;
65
+ appendLog(ctx: import("@mariozechner/pi-coding-agent").ExtensionContext, chunk: string, runKey?: string): Promise<void>;
66
+ complete(ctx: import("@mariozechner/pi-coding-agent").ExtensionContext, patch?: TakomiSubagentRunPatch, runKey?: string): Promise<void>;
67
+ block(ctx: import("@mariozechner/pi-coding-agent").ExtensionContext, patch?: TakomiSubagentRunPatch, runKey?: string): Promise<void>;
68
+ reset(ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext): void;
69
+ refresh(): void;
70
+ refreshWithContext(ctx: import("@mariozechner/pi-coding-agent").ExtensionContext): void;
71
+ cycleFocus(direction: SubagentFocusDirection, ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext): boolean;
72
+ setViewMode(mode: SubagentViewMode, ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext): SubagentViewMode | undefined;
73
+ cycleViewMode(ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext): SubagentViewMode | undefined;
74
+ closeFullscreen(ctx?: import("@mariozechner/pi-coding-agent").ExtensionContext): SubagentViewMode;
75
+ getKnownParentRunKey(parentTaskId: string): string | undefined;
76
+ }
77
+
78
+ export type TakomiSubagentRuntimeEvent =
79
+ | { type: "start"; runKey?: string; state: TakomiSubagentRunInit }
80
+ | { type: "update"; runKey?: string; patch: TakomiSubagentRunPatch }
81
+ | { type: "appendLog"; runKey?: string; chunk: string }
82
+ | { type: "complete"; runKey?: string; patch?: TakomiSubagentRunPatch }
83
+ | { type: "block"; runKey?: string; patch?: TakomiSubagentRunPatch };