takomi 2.1.2 → 2.1.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.
- package/.pi/README.md +124 -124
- package/.pi/agents/architect.md +15 -15
- package/.pi/agents/coder.md +14 -14
- package/.pi/agents/designer.md +17 -17
- package/.pi/agents/orchestrator.md +22 -22
- package/.pi/agents/reviewer.md +16 -16
- package/.pi/extensions/oauth-router/README.md +125 -125
- package/.pi/extensions/oauth-router/commands.ts +380 -380
- package/.pi/extensions/oauth-router/config.ts +200 -200
- package/.pi/extensions/oauth-router/index.ts +41 -41
- package/.pi/extensions/oauth-router/oauth-flow.ts +154 -154
- package/.pi/extensions/oauth-router/oauth-store.ts +121 -121
- package/.pi/extensions/oauth-router/package.json +14 -14
- package/.pi/extensions/oauth-router/policies.ts +27 -27
- package/.pi/extensions/oauth-router/provider.ts +492 -492
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -98
- package/.pi/extensions/oauth-router/state.ts +174 -174
- package/.pi/extensions/oauth-router/types.ts +153 -153
- package/.pi/extensions/takomi-runtime/command-text.ts +130 -130
- package/.pi/extensions/takomi-runtime/commands.ts +179 -179
- package/.pi/extensions/takomi-runtime/context-panel.ts +282 -282
- package/.pi/extensions/takomi-runtime/index.ts +1288 -1288
- package/.pi/extensions/takomi-runtime/profile.ts +114 -114
- package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -105
- package/.pi/extensions/takomi-runtime/shared.ts +492 -492
- package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -364
- package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -501
- package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -83
- package/.pi/extensions/takomi-runtime/ui.ts +133 -133
- package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -18
- package/.pi/extensions/takomi-subagents/agents.ts +113 -113
- package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -95
- package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -26
- package/.pi/extensions/takomi-subagents/dispatch.ts +215 -215
- package/.pi/extensions/takomi-subagents/index.ts +75 -75
- package/.pi/extensions/takomi-subagents/live-updates.ts +83 -83
- package/.pi/extensions/takomi-subagents/native-render.ts +174 -174
- package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
- package/.pi/themes/takomi-noir.json +81 -81
- package/package.json +59 -59
- package/src/doctor.js +87 -84
- package/src/pi-harness.js +355 -351
- package/src/pi-installer.js +193 -171
- package/src/pi-takomi-core/index.ts +4 -4
- package/src/pi-takomi-core/orchestration.ts +402 -402
- package/src/pi-takomi-core/routing.ts +93 -93
- package/src/pi-takomi-core/types.ts +173 -173
- package/src/pi-takomi-core/workflows.ts +299 -299
- package/src/skills-installer.js +101 -101
|
@@ -1,364 +1,364 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
FullscreenSubagentComponent,
|
|
4
|
-
renderSubagentStatus,
|
|
5
|
-
renderSubagentWidget,
|
|
6
|
-
} from "./subagent-render";
|
|
7
|
-
import { appendLiveLogChunk } from "./shared";
|
|
8
|
-
import type {
|
|
9
|
-
SubagentFocusDirection,
|
|
10
|
-
SubagentViewMode,
|
|
11
|
-
TakomiSubagentController,
|
|
12
|
-
TakomiSubagentRenderEntry,
|
|
13
|
-
TakomiSubagentRenderState,
|
|
14
|
-
TakomiSubagentRun,
|
|
15
|
-
TakomiSubagentRunInit,
|
|
16
|
-
TakomiSubagentRunPatch,
|
|
17
|
-
} from "./subagent-types";
|
|
18
|
-
|
|
19
|
-
const SUBAGENT_UI_KEY = "takomi-subagent";
|
|
20
|
-
const SUBAGENT_WIDGET_OPTIONS = { placement: "belowEditor" as const };
|
|
21
|
-
const LEGACY_SUBAGENT_CHROME_ENABLED = false;
|
|
22
|
-
|
|
23
|
-
class TakomiSharedSubagentController implements TakomiSubagentController {
|
|
24
|
-
private readonly runs = new Map<string, TakomiSubagentRun>();
|
|
25
|
-
private focusedRunKey?: string;
|
|
26
|
-
private lastCtx?: ExtensionContext;
|
|
27
|
-
private viewMode: SubagentViewMode = "compact";
|
|
28
|
-
private lastNonFullscreenMode: Exclude<SubagentViewMode, "fullscreen"> = "compact";
|
|
29
|
-
private fullscreenActive = false;
|
|
30
|
-
private fullscreenDismiss?: () => void;
|
|
31
|
-
private fullscreenRequestRender?: () => void;
|
|
32
|
-
|
|
33
|
-
hasRuns(): boolean {
|
|
34
|
-
return this.runs.size > 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
getStatusSummary(): string {
|
|
38
|
-
const runs = this.getOrderedRuns();
|
|
39
|
-
if (runs.length === 0) return "No Takomi subagents are active.";
|
|
40
|
-
const counts = runs.reduce<Record<string, number>>((acc, run) => {
|
|
41
|
-
const status = run.boardTaskStatus ?? run.status;
|
|
42
|
-
acc[status] = (acc[status] ?? 0) + 1;
|
|
43
|
-
return acc;
|
|
44
|
-
}, {});
|
|
45
|
-
const focused = this.getFocusedRun();
|
|
46
|
-
const parts = ["running", "in-progress", "completed", "blocked", "pending"]
|
|
47
|
-
.map((status) => counts[status] ? `${status}:${counts[status]}` : "")
|
|
48
|
-
.filter(Boolean);
|
|
49
|
-
return [
|
|
50
|
-
`Takomi subagents: ${runs.length}${parts.length ? ` (${parts.join(", ")})` : ""}.`,
|
|
51
|
-
focused ? `Focused: ${focused.agent} | ${focused.taskLabel} | ${focused.model ?? "default model"} | thinking=${focused.thinking ?? "default"}.` : "",
|
|
52
|
-
].filter(Boolean).join("\n");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
getViewMode(): SubagentViewMode {
|
|
56
|
-
return this.viewMode;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async start(ctx: ExtensionContext, state: TakomiSubagentRunInit, runKey?: string): Promise<void> {
|
|
60
|
-
const now = Date.now();
|
|
61
|
-
const resolvedRunKey = runKey ?? state.conversationId ?? `${state.agent}-${now}`;
|
|
62
|
-
const parentRunKey = state.parentRunKey ?? (state.parentTaskId ? this.getKnownParentRunKey(state.parentTaskId) : undefined);
|
|
63
|
-
this.runs.set(resolvedRunKey, {
|
|
64
|
-
...state,
|
|
65
|
-
runKey: resolvedRunKey,
|
|
66
|
-
parentRunKey,
|
|
67
|
-
logs: [...(state.logs ?? [])].slice(-60),
|
|
68
|
-
status: state.status ?? "running",
|
|
69
|
-
startedAt: now,
|
|
70
|
-
updatedAt: now,
|
|
71
|
-
});
|
|
72
|
-
this.focusedRunKey = resolvedRunKey;
|
|
73
|
-
this.lastCtx = ctx;
|
|
74
|
-
this.syncChrome(ctx);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async update(ctx: ExtensionContext, patch: TakomiSubagentRunPatch, runKey?: string): Promise<void> {
|
|
78
|
-
const resolvedRunKey = this.resolveRunKey(runKey, patch);
|
|
79
|
-
if (!resolvedRunKey) return;
|
|
80
|
-
const current = this.runs.get(resolvedRunKey);
|
|
81
|
-
if (!current) return;
|
|
82
|
-
this.runs.set(resolvedRunKey, this.mergeRun(current, patch));
|
|
83
|
-
this.lastCtx = ctx;
|
|
84
|
-
this.syncChrome(ctx);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async appendLog(ctx: ExtensionContext, chunk: string, runKey?: string): Promise<void> {
|
|
88
|
-
const resolvedRunKey = this.resolveRunKey(runKey);
|
|
89
|
-
if (!resolvedRunKey) return;
|
|
90
|
-
const current = this.runs.get(resolvedRunKey);
|
|
91
|
-
if (!current) return;
|
|
92
|
-
const logs = appendLiveLogChunk(current.logs, chunk);
|
|
93
|
-
if (logs.length === current.logs.length && logs.at(-1) === current.logs.at(-1)) return;
|
|
94
|
-
this.runs.set(resolvedRunKey, {
|
|
95
|
-
...current,
|
|
96
|
-
logs,
|
|
97
|
-
updatedAt: Date.now(),
|
|
98
|
-
});
|
|
99
|
-
this.lastCtx = ctx;
|
|
100
|
-
this.syncChrome(ctx);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async complete(ctx: ExtensionContext, patch?: TakomiSubagentRunPatch, runKey?: string): Promise<void> {
|
|
104
|
-
const resolvedRunKey = this.resolveRunKey(runKey, patch);
|
|
105
|
-
if (!resolvedRunKey) return;
|
|
106
|
-
const current = this.runs.get(resolvedRunKey);
|
|
107
|
-
if (!current) return;
|
|
108
|
-
this.runs.set(resolvedRunKey, this.mergeRun(current, { ...patch, status: "completed" }));
|
|
109
|
-
this.lastCtx = ctx;
|
|
110
|
-
this.syncChrome(ctx);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async block(ctx: ExtensionContext, patch?: TakomiSubagentRunPatch, runKey?: string): Promise<void> {
|
|
114
|
-
const resolvedRunKey = this.resolveRunKey(runKey, patch);
|
|
115
|
-
if (!resolvedRunKey) return;
|
|
116
|
-
const current = this.runs.get(resolvedRunKey);
|
|
117
|
-
if (!current) return;
|
|
118
|
-
this.runs.set(resolvedRunKey, this.mergeRun(current, { ...patch, status: "blocked" }));
|
|
119
|
-
this.lastCtx = ctx;
|
|
120
|
-
this.syncChrome(ctx);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
reset(ctx?: ExtensionContext): void {
|
|
124
|
-
this.runs.clear();
|
|
125
|
-
this.focusedRunKey = undefined;
|
|
126
|
-
if (ctx) this.lastCtx = ctx;
|
|
127
|
-
this.dismissFullscreen();
|
|
128
|
-
const targetCtx = ctx ?? this.lastCtx;
|
|
129
|
-
if (!targetCtx?.hasUI) return;
|
|
130
|
-
targetCtx.ui.setStatus(SUBAGENT_UI_KEY, undefined);
|
|
131
|
-
targetCtx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
refresh(): void {
|
|
135
|
-
if (this.lastCtx) this.refreshWithContext(this.lastCtx);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
refreshWithContext(ctx: ExtensionContext): void {
|
|
139
|
-
this.lastCtx = ctx;
|
|
140
|
-
this.syncChrome(ctx);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
cycleFocus(direction: SubagentFocusDirection, ctx?: ExtensionContext): boolean {
|
|
144
|
-
const ordered = this.getOrderedRuns();
|
|
145
|
-
if (ordered.length <= 1) return false;
|
|
146
|
-
const currentIndex = Math.max(0, ordered.findIndex((run) => run.runKey === this.focusedRunKey));
|
|
147
|
-
const delta = direction === "next" ? 1 : -1;
|
|
148
|
-
this.focusedRunKey = ordered[(currentIndex + delta + ordered.length) % ordered.length]?.runKey;
|
|
149
|
-
const targetCtx = ctx ?? this.lastCtx;
|
|
150
|
-
if (targetCtx) this.syncChrome(targetCtx);
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
setViewMode(mode: SubagentViewMode, ctx?: ExtensionContext): SubagentViewMode | undefined {
|
|
155
|
-
if (!this.hasRuns()) return undefined;
|
|
156
|
-
this.viewMode = mode;
|
|
157
|
-
if (mode !== "fullscreen") this.lastNonFullscreenMode = mode;
|
|
158
|
-
const targetCtx = ctx ?? this.lastCtx;
|
|
159
|
-
if (targetCtx) this.syncChrome(targetCtx);
|
|
160
|
-
return this.viewMode;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
cycleViewMode(ctx?: ExtensionContext): SubagentViewMode | undefined {
|
|
164
|
-
if (!this.hasRuns()) return undefined;
|
|
165
|
-
const nextMode = this.viewMode === "compact"
|
|
166
|
-
? "expanded"
|
|
167
|
-
: this.viewMode === "expanded"
|
|
168
|
-
? "fullscreen"
|
|
169
|
-
: "compact";
|
|
170
|
-
return this.setViewMode(nextMode, ctx);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
closeFullscreen(ctx?: ExtensionContext): SubagentViewMode {
|
|
174
|
-
this.viewMode = this.lastNonFullscreenMode;
|
|
175
|
-
const targetCtx = ctx ?? this.lastCtx;
|
|
176
|
-
if (targetCtx) this.syncChrome(targetCtx);
|
|
177
|
-
return this.viewMode;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
getKnownParentRunKey(parentTaskId: string): string | undefined {
|
|
181
|
-
if (this.runs.has(parentTaskId)) return parentTaskId;
|
|
182
|
-
for (const run of this.runs.values()) {
|
|
183
|
-
if (run.conversationId === parentTaskId) return run.runKey;
|
|
184
|
-
}
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private mergeRun(current: TakomiSubagentRun, patch?: TakomiSubagentRunPatch): TakomiSubagentRun {
|
|
189
|
-
const nextLogs = patch?.logs ? [...current.logs, ...patch.logs].slice(-60) : current.logs;
|
|
190
|
-
const parentRunKey = patch?.parentRunKey
|
|
191
|
-
?? current.parentRunKey
|
|
192
|
-
?? (patch?.parentTaskId ? this.getKnownParentRunKey(patch.parentTaskId) : undefined)
|
|
193
|
-
?? (current.parentTaskId ? this.getKnownParentRunKey(current.parentTaskId) : undefined);
|
|
194
|
-
return {
|
|
195
|
-
...current,
|
|
196
|
-
...patch,
|
|
197
|
-
parentRunKey,
|
|
198
|
-
logs: nextLogs,
|
|
199
|
-
updatedAt: Date.now(),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private syncChrome(ctx: ExtensionContext): void {
|
|
204
|
-
if (!ctx.hasUI) return;
|
|
205
|
-
|
|
206
|
-
if (!LEGACY_SUBAGENT_CHROME_ENABLED) {
|
|
207
|
-
ctx.ui.setStatus(SUBAGENT_UI_KEY, undefined);
|
|
208
|
-
ctx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
209
|
-
this.dismissFullscreen();
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const renderState = this.buildRenderState();
|
|
214
|
-
if (!renderState.focusedRun) {
|
|
215
|
-
ctx.ui.setStatus(SUBAGENT_UI_KEY, undefined);
|
|
216
|
-
ctx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
217
|
-
this.dismissFullscreen();
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
ctx.ui.setStatus(SUBAGENT_UI_KEY, renderSubagentStatus(ctx.ui.theme, renderState));
|
|
222
|
-
|
|
223
|
-
if (this.viewMode === "fullscreen") {
|
|
224
|
-
ctx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
225
|
-
if (!this.fullscreenActive) this.showFullscreen(ctx);
|
|
226
|
-
else this.fullscreenRequestRender?.();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (this.fullscreenActive) this.dismissFullscreen();
|
|
231
|
-
ctx.ui.setWidget(SUBAGENT_UI_KEY, renderSubagentWidget(ctx.ui.theme, renderState), SUBAGENT_WIDGET_OPTIONS);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private showFullscreen(ctx: ExtensionContext): void {
|
|
235
|
-
if (!ctx.hasUI || this.fullscreenActive) return;
|
|
236
|
-
this.fullscreenActive = true;
|
|
237
|
-
void ctx.ui.custom<void>(
|
|
238
|
-
(tui, theme, _keybindings, done) => {
|
|
239
|
-
this.fullscreenDismiss = () => done();
|
|
240
|
-
this.fullscreenRequestRender = () => tui.requestRender();
|
|
241
|
-
return new FullscreenSubagentComponent(
|
|
242
|
-
tui,
|
|
243
|
-
theme,
|
|
244
|
-
() => this.buildRenderState(),
|
|
245
|
-
() => {
|
|
246
|
-
this.viewMode = this.lastNonFullscreenMode;
|
|
247
|
-
this.dismissFullscreen();
|
|
248
|
-
},
|
|
249
|
-
() => {
|
|
250
|
-
this.viewMode = "compact";
|
|
251
|
-
this.lastNonFullscreenMode = "compact";
|
|
252
|
-
this.dismissFullscreen();
|
|
253
|
-
},
|
|
254
|
-
() => {
|
|
255
|
-
this.cycleFocus("next", ctx);
|
|
256
|
-
},
|
|
257
|
-
() => {
|
|
258
|
-
this.cycleFocus("prev", ctx);
|
|
259
|
-
},
|
|
260
|
-
);
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
overlay: true,
|
|
264
|
-
overlayOptions: {
|
|
265
|
-
width: "88%",
|
|
266
|
-
maxHeight: 20,
|
|
267
|
-
anchor: "center",
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
).then(() => {
|
|
271
|
-
this.fullscreenActive = false;
|
|
272
|
-
this.fullscreenDismiss = undefined;
|
|
273
|
-
this.fullscreenRequestRender = undefined;
|
|
274
|
-
const targetCtx = this.lastCtx;
|
|
275
|
-
if (targetCtx?.hasUI) this.syncChrome(targetCtx);
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private dismissFullscreen(): void {
|
|
280
|
-
if (this.fullscreenDismiss) this.fullscreenDismiss();
|
|
281
|
-
this.fullscreenDismiss = undefined;
|
|
282
|
-
this.fullscreenRequestRender = undefined;
|
|
283
|
-
this.fullscreenActive = false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private resolveRunKey(runKey?: string, patch?: TakomiSubagentRunPatch): string | undefined {
|
|
287
|
-
const explicit = runKey ?? patch?.conversationId;
|
|
288
|
-
if (explicit) return this.runs.has(explicit) ? explicit : undefined;
|
|
289
|
-
if (this.focusedRunKey && this.runs.has(this.focusedRunKey)) return this.focusedRunKey;
|
|
290
|
-
return this.getOrderedRuns()[0]?.runKey;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private getOrderedRuns(): TakomiSubagentRun[] {
|
|
294
|
-
return [...this.runs.values()].sort((a, b) => {
|
|
295
|
-
if (b.updatedAt !== a.updatedAt) return b.updatedAt - a.updatedAt;
|
|
296
|
-
return b.startedAt - a.startedAt;
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private getFocusedRun(): TakomiSubagentRun | undefined {
|
|
301
|
-
if (this.focusedRunKey && this.runs.has(this.focusedRunKey)) {
|
|
302
|
-
return this.runs.get(this.focusedRunKey);
|
|
303
|
-
}
|
|
304
|
-
const fallback = this.getOrderedRuns()[0];
|
|
305
|
-
if (fallback) this.focusedRunKey = fallback.runKey;
|
|
306
|
-
return fallback;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private buildRenderState(): TakomiSubagentRenderState {
|
|
310
|
-
const orderedRuns = this.getOrderedRuns();
|
|
311
|
-
const focusedRun = this.getFocusedRun();
|
|
312
|
-
const focusPosition = focusedRun ? Math.max(1, orderedRuns.findIndex((run) => run.runKey === focusedRun.runKey) + 1) : 0;
|
|
313
|
-
const activePathRuns = focusedRun ? this.getActivePathRuns(focusedRun.runKey) : [];
|
|
314
|
-
const activePathKeys = new Set(activePathRuns.map((run) => run.runKey));
|
|
315
|
-
const activePath = activePathRuns.map((run, index) => ({
|
|
316
|
-
run,
|
|
317
|
-
depth: index,
|
|
318
|
-
relation: index === activePathRuns.length - 1 ? "focused" : "ancestor",
|
|
319
|
-
} satisfies TakomiSubagentRenderEntry));
|
|
320
|
-
const peerRuns = orderedRuns
|
|
321
|
-
.filter((run) => !activePathKeys.has(run.runKey))
|
|
322
|
-
.map((run) => ({ run, depth: 0, relation: "peer" } satisfies TakomiSubagentRenderEntry));
|
|
323
|
-
|
|
324
|
-
const compactRuns: TakomiSubagentRenderEntry[] = [];
|
|
325
|
-
const ancestorEntries = activePath.slice(0, -1).slice(-2);
|
|
326
|
-
compactRuns.push(...ancestorEntries);
|
|
327
|
-
const focusedEntry = activePath.at(-1);
|
|
328
|
-
if (focusedEntry) compactRuns.push(focusedEntry);
|
|
329
|
-
for (const peer of peerRuns) {
|
|
330
|
-
if (compactRuns.length >= 3) break;
|
|
331
|
-
compactRuns.push(peer);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
mode: this.viewMode,
|
|
336
|
-
activeCount: orderedRuns.length,
|
|
337
|
-
focusPosition,
|
|
338
|
-
focusedRun,
|
|
339
|
-
activePath,
|
|
340
|
-
peerRuns,
|
|
341
|
-
compactRuns,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private getActivePathRuns(runKey: string): TakomiSubagentRun[] {
|
|
346
|
-
const path: TakomiSubagentRun[] = [];
|
|
347
|
-
const seen = new Set<string>();
|
|
348
|
-
let current = this.runs.get(runKey);
|
|
349
|
-
while (current && !seen.has(current.runKey)) {
|
|
350
|
-
seen.add(current.runKey);
|
|
351
|
-
path.unshift(current);
|
|
352
|
-
const parentRunKey = current.parentRunKey
|
|
353
|
-
?? (current.parentTaskId ? this.getKnownParentRunKey(current.parentTaskId) : undefined);
|
|
354
|
-
current = parentRunKey ? this.runs.get(parentRunKey) : undefined;
|
|
355
|
-
}
|
|
356
|
-
return path;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const controller = new TakomiSharedSubagentController();
|
|
361
|
-
|
|
362
|
-
export function getTakomiSubagentController(): TakomiSubagentController {
|
|
363
|
-
return controller;
|
|
364
|
-
}
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
FullscreenSubagentComponent,
|
|
4
|
+
renderSubagentStatus,
|
|
5
|
+
renderSubagentWidget,
|
|
6
|
+
} from "./subagent-render";
|
|
7
|
+
import { appendLiveLogChunk } from "./shared";
|
|
8
|
+
import type {
|
|
9
|
+
SubagentFocusDirection,
|
|
10
|
+
SubagentViewMode,
|
|
11
|
+
TakomiSubagentController,
|
|
12
|
+
TakomiSubagentRenderEntry,
|
|
13
|
+
TakomiSubagentRenderState,
|
|
14
|
+
TakomiSubagentRun,
|
|
15
|
+
TakomiSubagentRunInit,
|
|
16
|
+
TakomiSubagentRunPatch,
|
|
17
|
+
} from "./subagent-types";
|
|
18
|
+
|
|
19
|
+
const SUBAGENT_UI_KEY = "takomi-subagent";
|
|
20
|
+
const SUBAGENT_WIDGET_OPTIONS = { placement: "belowEditor" as const };
|
|
21
|
+
const LEGACY_SUBAGENT_CHROME_ENABLED = false;
|
|
22
|
+
|
|
23
|
+
class TakomiSharedSubagentController implements TakomiSubagentController {
|
|
24
|
+
private readonly runs = new Map<string, TakomiSubagentRun>();
|
|
25
|
+
private focusedRunKey?: string;
|
|
26
|
+
private lastCtx?: ExtensionContext;
|
|
27
|
+
private viewMode: SubagentViewMode = "compact";
|
|
28
|
+
private lastNonFullscreenMode: Exclude<SubagentViewMode, "fullscreen"> = "compact";
|
|
29
|
+
private fullscreenActive = false;
|
|
30
|
+
private fullscreenDismiss?: () => void;
|
|
31
|
+
private fullscreenRequestRender?: () => void;
|
|
32
|
+
|
|
33
|
+
hasRuns(): boolean {
|
|
34
|
+
return this.runs.size > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getStatusSummary(): string {
|
|
38
|
+
const runs = this.getOrderedRuns();
|
|
39
|
+
if (runs.length === 0) return "No Takomi subagents are active.";
|
|
40
|
+
const counts = runs.reduce<Record<string, number>>((acc, run) => {
|
|
41
|
+
const status = run.boardTaskStatus ?? run.status;
|
|
42
|
+
acc[status] = (acc[status] ?? 0) + 1;
|
|
43
|
+
return acc;
|
|
44
|
+
}, {});
|
|
45
|
+
const focused = this.getFocusedRun();
|
|
46
|
+
const parts = ["running", "in-progress", "completed", "blocked", "pending"]
|
|
47
|
+
.map((status) => counts[status] ? `${status}:${counts[status]}` : "")
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
return [
|
|
50
|
+
`Takomi subagents: ${runs.length}${parts.length ? ` (${parts.join(", ")})` : ""}.`,
|
|
51
|
+
focused ? `Focused: ${focused.agent} | ${focused.taskLabel} | ${focused.model ?? "default model"} | thinking=${focused.thinking ?? "default"}.` : "",
|
|
52
|
+
].filter(Boolean).join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getViewMode(): SubagentViewMode {
|
|
56
|
+
return this.viewMode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async start(ctx: ExtensionContext, state: TakomiSubagentRunInit, runKey?: string): Promise<void> {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const resolvedRunKey = runKey ?? state.conversationId ?? `${state.agent}-${now}`;
|
|
62
|
+
const parentRunKey = state.parentRunKey ?? (state.parentTaskId ? this.getKnownParentRunKey(state.parentTaskId) : undefined);
|
|
63
|
+
this.runs.set(resolvedRunKey, {
|
|
64
|
+
...state,
|
|
65
|
+
runKey: resolvedRunKey,
|
|
66
|
+
parentRunKey,
|
|
67
|
+
logs: [...(state.logs ?? [])].slice(-60),
|
|
68
|
+
status: state.status ?? "running",
|
|
69
|
+
startedAt: now,
|
|
70
|
+
updatedAt: now,
|
|
71
|
+
});
|
|
72
|
+
this.focusedRunKey = resolvedRunKey;
|
|
73
|
+
this.lastCtx = ctx;
|
|
74
|
+
this.syncChrome(ctx);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async update(ctx: ExtensionContext, patch: TakomiSubagentRunPatch, runKey?: string): Promise<void> {
|
|
78
|
+
const resolvedRunKey = this.resolveRunKey(runKey, patch);
|
|
79
|
+
if (!resolvedRunKey) return;
|
|
80
|
+
const current = this.runs.get(resolvedRunKey);
|
|
81
|
+
if (!current) return;
|
|
82
|
+
this.runs.set(resolvedRunKey, this.mergeRun(current, patch));
|
|
83
|
+
this.lastCtx = ctx;
|
|
84
|
+
this.syncChrome(ctx);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async appendLog(ctx: ExtensionContext, chunk: string, runKey?: string): Promise<void> {
|
|
88
|
+
const resolvedRunKey = this.resolveRunKey(runKey);
|
|
89
|
+
if (!resolvedRunKey) return;
|
|
90
|
+
const current = this.runs.get(resolvedRunKey);
|
|
91
|
+
if (!current) return;
|
|
92
|
+
const logs = appendLiveLogChunk(current.logs, chunk);
|
|
93
|
+
if (logs.length === current.logs.length && logs.at(-1) === current.logs.at(-1)) return;
|
|
94
|
+
this.runs.set(resolvedRunKey, {
|
|
95
|
+
...current,
|
|
96
|
+
logs,
|
|
97
|
+
updatedAt: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
this.lastCtx = ctx;
|
|
100
|
+
this.syncChrome(ctx);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async complete(ctx: ExtensionContext, patch?: TakomiSubagentRunPatch, runKey?: string): Promise<void> {
|
|
104
|
+
const resolvedRunKey = this.resolveRunKey(runKey, patch);
|
|
105
|
+
if (!resolvedRunKey) return;
|
|
106
|
+
const current = this.runs.get(resolvedRunKey);
|
|
107
|
+
if (!current) return;
|
|
108
|
+
this.runs.set(resolvedRunKey, this.mergeRun(current, { ...patch, status: "completed" }));
|
|
109
|
+
this.lastCtx = ctx;
|
|
110
|
+
this.syncChrome(ctx);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async block(ctx: ExtensionContext, patch?: TakomiSubagentRunPatch, runKey?: string): Promise<void> {
|
|
114
|
+
const resolvedRunKey = this.resolveRunKey(runKey, patch);
|
|
115
|
+
if (!resolvedRunKey) return;
|
|
116
|
+
const current = this.runs.get(resolvedRunKey);
|
|
117
|
+
if (!current) return;
|
|
118
|
+
this.runs.set(resolvedRunKey, this.mergeRun(current, { ...patch, status: "blocked" }));
|
|
119
|
+
this.lastCtx = ctx;
|
|
120
|
+
this.syncChrome(ctx);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
reset(ctx?: ExtensionContext): void {
|
|
124
|
+
this.runs.clear();
|
|
125
|
+
this.focusedRunKey = undefined;
|
|
126
|
+
if (ctx) this.lastCtx = ctx;
|
|
127
|
+
this.dismissFullscreen();
|
|
128
|
+
const targetCtx = ctx ?? this.lastCtx;
|
|
129
|
+
if (!targetCtx?.hasUI) return;
|
|
130
|
+
targetCtx.ui.setStatus(SUBAGENT_UI_KEY, undefined);
|
|
131
|
+
targetCtx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
refresh(): void {
|
|
135
|
+
if (this.lastCtx) this.refreshWithContext(this.lastCtx);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
refreshWithContext(ctx: ExtensionContext): void {
|
|
139
|
+
this.lastCtx = ctx;
|
|
140
|
+
this.syncChrome(ctx);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
cycleFocus(direction: SubagentFocusDirection, ctx?: ExtensionContext): boolean {
|
|
144
|
+
const ordered = this.getOrderedRuns();
|
|
145
|
+
if (ordered.length <= 1) return false;
|
|
146
|
+
const currentIndex = Math.max(0, ordered.findIndex((run) => run.runKey === this.focusedRunKey));
|
|
147
|
+
const delta = direction === "next" ? 1 : -1;
|
|
148
|
+
this.focusedRunKey = ordered[(currentIndex + delta + ordered.length) % ordered.length]?.runKey;
|
|
149
|
+
const targetCtx = ctx ?? this.lastCtx;
|
|
150
|
+
if (targetCtx) this.syncChrome(targetCtx);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setViewMode(mode: SubagentViewMode, ctx?: ExtensionContext): SubagentViewMode | undefined {
|
|
155
|
+
if (!this.hasRuns()) return undefined;
|
|
156
|
+
this.viewMode = mode;
|
|
157
|
+
if (mode !== "fullscreen") this.lastNonFullscreenMode = mode;
|
|
158
|
+
const targetCtx = ctx ?? this.lastCtx;
|
|
159
|
+
if (targetCtx) this.syncChrome(targetCtx);
|
|
160
|
+
return this.viewMode;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
cycleViewMode(ctx?: ExtensionContext): SubagentViewMode | undefined {
|
|
164
|
+
if (!this.hasRuns()) return undefined;
|
|
165
|
+
const nextMode = this.viewMode === "compact"
|
|
166
|
+
? "expanded"
|
|
167
|
+
: this.viewMode === "expanded"
|
|
168
|
+
? "fullscreen"
|
|
169
|
+
: "compact";
|
|
170
|
+
return this.setViewMode(nextMode, ctx);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
closeFullscreen(ctx?: ExtensionContext): SubagentViewMode {
|
|
174
|
+
this.viewMode = this.lastNonFullscreenMode;
|
|
175
|
+
const targetCtx = ctx ?? this.lastCtx;
|
|
176
|
+
if (targetCtx) this.syncChrome(targetCtx);
|
|
177
|
+
return this.viewMode;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getKnownParentRunKey(parentTaskId: string): string | undefined {
|
|
181
|
+
if (this.runs.has(parentTaskId)) return parentTaskId;
|
|
182
|
+
for (const run of this.runs.values()) {
|
|
183
|
+
if (run.conversationId === parentTaskId) return run.runKey;
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private mergeRun(current: TakomiSubagentRun, patch?: TakomiSubagentRunPatch): TakomiSubagentRun {
|
|
189
|
+
const nextLogs = patch?.logs ? [...current.logs, ...patch.logs].slice(-60) : current.logs;
|
|
190
|
+
const parentRunKey = patch?.parentRunKey
|
|
191
|
+
?? current.parentRunKey
|
|
192
|
+
?? (patch?.parentTaskId ? this.getKnownParentRunKey(patch.parentTaskId) : undefined)
|
|
193
|
+
?? (current.parentTaskId ? this.getKnownParentRunKey(current.parentTaskId) : undefined);
|
|
194
|
+
return {
|
|
195
|
+
...current,
|
|
196
|
+
...patch,
|
|
197
|
+
parentRunKey,
|
|
198
|
+
logs: nextLogs,
|
|
199
|
+
updatedAt: Date.now(),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private syncChrome(ctx: ExtensionContext): void {
|
|
204
|
+
if (!ctx.hasUI) return;
|
|
205
|
+
|
|
206
|
+
if (!LEGACY_SUBAGENT_CHROME_ENABLED) {
|
|
207
|
+
ctx.ui.setStatus(SUBAGENT_UI_KEY, undefined);
|
|
208
|
+
ctx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
209
|
+
this.dismissFullscreen();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const renderState = this.buildRenderState();
|
|
214
|
+
if (!renderState.focusedRun) {
|
|
215
|
+
ctx.ui.setStatus(SUBAGENT_UI_KEY, undefined);
|
|
216
|
+
ctx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
217
|
+
this.dismissFullscreen();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
ctx.ui.setStatus(SUBAGENT_UI_KEY, renderSubagentStatus(ctx.ui.theme, renderState));
|
|
222
|
+
|
|
223
|
+
if (this.viewMode === "fullscreen") {
|
|
224
|
+
ctx.ui.setWidget(SUBAGENT_UI_KEY, undefined);
|
|
225
|
+
if (!this.fullscreenActive) this.showFullscreen(ctx);
|
|
226
|
+
else this.fullscreenRequestRender?.();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (this.fullscreenActive) this.dismissFullscreen();
|
|
231
|
+
ctx.ui.setWidget(SUBAGENT_UI_KEY, renderSubagentWidget(ctx.ui.theme, renderState), SUBAGENT_WIDGET_OPTIONS);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private showFullscreen(ctx: ExtensionContext): void {
|
|
235
|
+
if (!ctx.hasUI || this.fullscreenActive) return;
|
|
236
|
+
this.fullscreenActive = true;
|
|
237
|
+
void ctx.ui.custom<void>(
|
|
238
|
+
(tui, theme, _keybindings, done) => {
|
|
239
|
+
this.fullscreenDismiss = () => done();
|
|
240
|
+
this.fullscreenRequestRender = () => tui.requestRender();
|
|
241
|
+
return new FullscreenSubagentComponent(
|
|
242
|
+
tui,
|
|
243
|
+
theme,
|
|
244
|
+
() => this.buildRenderState(),
|
|
245
|
+
() => {
|
|
246
|
+
this.viewMode = this.lastNonFullscreenMode;
|
|
247
|
+
this.dismissFullscreen();
|
|
248
|
+
},
|
|
249
|
+
() => {
|
|
250
|
+
this.viewMode = "compact";
|
|
251
|
+
this.lastNonFullscreenMode = "compact";
|
|
252
|
+
this.dismissFullscreen();
|
|
253
|
+
},
|
|
254
|
+
() => {
|
|
255
|
+
this.cycleFocus("next", ctx);
|
|
256
|
+
},
|
|
257
|
+
() => {
|
|
258
|
+
this.cycleFocus("prev", ctx);
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
overlay: true,
|
|
264
|
+
overlayOptions: {
|
|
265
|
+
width: "88%",
|
|
266
|
+
maxHeight: 20,
|
|
267
|
+
anchor: "center",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
).then(() => {
|
|
271
|
+
this.fullscreenActive = false;
|
|
272
|
+
this.fullscreenDismiss = undefined;
|
|
273
|
+
this.fullscreenRequestRender = undefined;
|
|
274
|
+
const targetCtx = this.lastCtx;
|
|
275
|
+
if (targetCtx?.hasUI) this.syncChrome(targetCtx);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private dismissFullscreen(): void {
|
|
280
|
+
if (this.fullscreenDismiss) this.fullscreenDismiss();
|
|
281
|
+
this.fullscreenDismiss = undefined;
|
|
282
|
+
this.fullscreenRequestRender = undefined;
|
|
283
|
+
this.fullscreenActive = false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private resolveRunKey(runKey?: string, patch?: TakomiSubagentRunPatch): string | undefined {
|
|
287
|
+
const explicit = runKey ?? patch?.conversationId;
|
|
288
|
+
if (explicit) return this.runs.has(explicit) ? explicit : undefined;
|
|
289
|
+
if (this.focusedRunKey && this.runs.has(this.focusedRunKey)) return this.focusedRunKey;
|
|
290
|
+
return this.getOrderedRuns()[0]?.runKey;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private getOrderedRuns(): TakomiSubagentRun[] {
|
|
294
|
+
return [...this.runs.values()].sort((a, b) => {
|
|
295
|
+
if (b.updatedAt !== a.updatedAt) return b.updatedAt - a.updatedAt;
|
|
296
|
+
return b.startedAt - a.startedAt;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private getFocusedRun(): TakomiSubagentRun | undefined {
|
|
301
|
+
if (this.focusedRunKey && this.runs.has(this.focusedRunKey)) {
|
|
302
|
+
return this.runs.get(this.focusedRunKey);
|
|
303
|
+
}
|
|
304
|
+
const fallback = this.getOrderedRuns()[0];
|
|
305
|
+
if (fallback) this.focusedRunKey = fallback.runKey;
|
|
306
|
+
return fallback;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private buildRenderState(): TakomiSubagentRenderState {
|
|
310
|
+
const orderedRuns = this.getOrderedRuns();
|
|
311
|
+
const focusedRun = this.getFocusedRun();
|
|
312
|
+
const focusPosition = focusedRun ? Math.max(1, orderedRuns.findIndex((run) => run.runKey === focusedRun.runKey) + 1) : 0;
|
|
313
|
+
const activePathRuns = focusedRun ? this.getActivePathRuns(focusedRun.runKey) : [];
|
|
314
|
+
const activePathKeys = new Set(activePathRuns.map((run) => run.runKey));
|
|
315
|
+
const activePath = activePathRuns.map((run, index) => ({
|
|
316
|
+
run,
|
|
317
|
+
depth: index,
|
|
318
|
+
relation: index === activePathRuns.length - 1 ? "focused" : "ancestor",
|
|
319
|
+
} satisfies TakomiSubagentRenderEntry));
|
|
320
|
+
const peerRuns = orderedRuns
|
|
321
|
+
.filter((run) => !activePathKeys.has(run.runKey))
|
|
322
|
+
.map((run) => ({ run, depth: 0, relation: "peer" } satisfies TakomiSubagentRenderEntry));
|
|
323
|
+
|
|
324
|
+
const compactRuns: TakomiSubagentRenderEntry[] = [];
|
|
325
|
+
const ancestorEntries = activePath.slice(0, -1).slice(-2);
|
|
326
|
+
compactRuns.push(...ancestorEntries);
|
|
327
|
+
const focusedEntry = activePath.at(-1);
|
|
328
|
+
if (focusedEntry) compactRuns.push(focusedEntry);
|
|
329
|
+
for (const peer of peerRuns) {
|
|
330
|
+
if (compactRuns.length >= 3) break;
|
|
331
|
+
compactRuns.push(peer);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
mode: this.viewMode,
|
|
336
|
+
activeCount: orderedRuns.length,
|
|
337
|
+
focusPosition,
|
|
338
|
+
focusedRun,
|
|
339
|
+
activePath,
|
|
340
|
+
peerRuns,
|
|
341
|
+
compactRuns,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private getActivePathRuns(runKey: string): TakomiSubagentRun[] {
|
|
346
|
+
const path: TakomiSubagentRun[] = [];
|
|
347
|
+
const seen = new Set<string>();
|
|
348
|
+
let current = this.runs.get(runKey);
|
|
349
|
+
while (current && !seen.has(current.runKey)) {
|
|
350
|
+
seen.add(current.runKey);
|
|
351
|
+
path.unshift(current);
|
|
352
|
+
const parentRunKey = current.parentRunKey
|
|
353
|
+
?? (current.parentTaskId ? this.getKnownParentRunKey(current.parentTaskId) : undefined);
|
|
354
|
+
current = parentRunKey ? this.runs.get(parentRunKey) : undefined;
|
|
355
|
+
}
|
|
356
|
+
return path;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const controller = new TakomiSharedSubagentController();
|
|
361
|
+
|
|
362
|
+
export function getTakomiSubagentController(): TakomiSubagentController {
|
|
363
|
+
return controller;
|
|
364
|
+
}
|