pi-subagents-j0k3r 1.0.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/.releaserc.json +14 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/index.ts +531 -0
- package/package.json +73 -0
- package/scripts/verify-package-files.mjs +41 -0
- package/skills/subagents-configuration/SKILL.md +182 -0
- package/src/config.ts +262 -0
- package/src/debug.ts +38 -0
- package/src/history.ts +254 -0
- package/src/interaction-channel.ts +197 -0
- package/src/manager.ts +533 -0
- package/src/model-profiles-ui.ts +609 -0
- package/src/profile-resolver.ts +60 -0
- package/src/runner.ts +688 -0
- package/src/thread-view.ts +477 -0
- package/src/tools.ts +492 -0
- package/src/types.ts +234 -0
- package/src/ui.ts +399 -0
package/index.ts
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { SubagentManager } from './src/manager.js';
|
|
2
|
+
import { readSubagentsConfig } from './src/config.js';
|
|
3
|
+
import { registerSubagentTools, triggerClaudeBackgroundHandoff } from './src/tools.js';
|
|
4
|
+
import { runSubagentModelsCommand } from './src/model-profiles-ui.js';
|
|
5
|
+
import { SubagentsHistoryPanel } from './src/ui.js';
|
|
6
|
+
import type { SubagentTask } from './src/types.js';
|
|
7
|
+
|
|
8
|
+
type ClaudeBackgroundWidgetEntry = {
|
|
9
|
+
key: string;
|
|
10
|
+
line: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ClaudeBackgroundTerminalAction =
|
|
14
|
+
| { type: 'focus-editor' }
|
|
15
|
+
| { type: 'open-task'; taskId: string };
|
|
16
|
+
|
|
17
|
+
type ClaudeBackgroundTerminalInputResult = {
|
|
18
|
+
consume?: boolean;
|
|
19
|
+
data?: string;
|
|
20
|
+
action?: ClaudeBackgroundTerminalAction;
|
|
21
|
+
} | undefined;
|
|
22
|
+
|
|
23
|
+
function matchesKey(data: string, key: string): boolean {
|
|
24
|
+
const keys: Record<string, string[]> = {
|
|
25
|
+
escape: ['\u001b'],
|
|
26
|
+
'ctrl+c': ['\u0003'],
|
|
27
|
+
'ctrl+o': ['\u000f'],
|
|
28
|
+
'ctrl+w': ['\u0017'],
|
|
29
|
+
q: ['q', 'Q'],
|
|
30
|
+
up: ['\u001b[A'],
|
|
31
|
+
down: ['\u001b[B'],
|
|
32
|
+
right: ['\u001b[C'],
|
|
33
|
+
left: ['\u001b[D'],
|
|
34
|
+
pageUp: ['\u001b[5~'],
|
|
35
|
+
pageDown: ['\u001b[6~'],
|
|
36
|
+
home: ['\u001b[H', '\u001b[1~', '\u001bOH'],
|
|
37
|
+
end: ['\u001b[F', '\u001b[4~', '\u001bOF'],
|
|
38
|
+
};
|
|
39
|
+
return keys[key]?.includes(data) ?? data === key;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PANEL_KEYBINDINGS: Record<string, string[]> = {
|
|
43
|
+
escape: ['app.interrupt', 'tui.select.cancel'],
|
|
44
|
+
'ctrl+c': ['tui.select.cancel'],
|
|
45
|
+
'ctrl+o': ['app.tools.expand'],
|
|
46
|
+
detailCancel: ['subagents.detail.cancel'],
|
|
47
|
+
up: ['tui.select.up', 'tui.editor.cursorUp'],
|
|
48
|
+
down: ['tui.select.down', 'tui.editor.cursorDown'],
|
|
49
|
+
right: ['tui.editor.cursorRight'],
|
|
50
|
+
left: ['tui.editor.cursorLeft'],
|
|
51
|
+
pageUp: ['tui.select.pageUp', 'tui.editor.pageUp'],
|
|
52
|
+
pageDown: ['tui.select.pageDown', 'tui.editor.pageDown'],
|
|
53
|
+
home: ['tui.editor.cursorLineStart'],
|
|
54
|
+
end: ['tui.editor.cursorLineEnd'],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function createSubagentsPanelKeyMatcher(keybindings?: { matches?: (data: string, keybinding: string) => boolean }) {
|
|
58
|
+
return (data: string, key: string): boolean => {
|
|
59
|
+
const bindings = PANEL_KEYBINDINGS[key];
|
|
60
|
+
if (bindings?.some((binding) => keybindings?.matches?.(data, binding))) return true;
|
|
61
|
+
return matchesKey(data, key);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function visibleWidth(text: string): number {
|
|
66
|
+
return [...text.replace(/\u001b\][^\u001b\u0007]*(?:\u001b\\|\u0007)|\u001b\[[0-?]*[ -/]*[@-~]/g, '')].length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function truncateToWidth(text: string, width: number): string {
|
|
70
|
+
const chars = [...text];
|
|
71
|
+
return chars.length > width ? chars.slice(0, Math.max(0, width - 1)).join('') + '…' : text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function wrapLineToWidth(line: string, width: number): string[] {
|
|
75
|
+
const max = Math.max(1, width);
|
|
76
|
+
if (!line) return [''];
|
|
77
|
+
if ([...line].length <= max) return [line];
|
|
78
|
+
const out: string[] = [];
|
|
79
|
+
let current = '';
|
|
80
|
+
for (const word of line.split(/\s+/).filter(Boolean)) {
|
|
81
|
+
const wordLength = [...word].length;
|
|
82
|
+
const currentLength = [...current].length;
|
|
83
|
+
if (!current) {
|
|
84
|
+
if (wordLength <= max) {
|
|
85
|
+
current = word;
|
|
86
|
+
} else {
|
|
87
|
+
const chars = [...word];
|
|
88
|
+
for (let index = 0; index < chars.length; index += max) out.push(chars.slice(index, index + max).join(''));
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (currentLength + 1 + wordLength <= max) {
|
|
93
|
+
current += ` ${word}`;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
out.push(current);
|
|
97
|
+
current = '';
|
|
98
|
+
if (wordLength <= max) {
|
|
99
|
+
current = word;
|
|
100
|
+
} else {
|
|
101
|
+
const chars = [...word];
|
|
102
|
+
for (let index = 0; index < chars.length; index += max) out.push(chars.slice(index, index + max).join(''));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (current) out.push(current);
|
|
106
|
+
return out.length ? out : [''];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function currentSessionId(ctx: any): string | undefined {
|
|
110
|
+
const direct = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId;
|
|
111
|
+
if (typeof direct === 'string' && direct.length > 0) return direct;
|
|
112
|
+
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
113
|
+
return typeof file === 'string' && file.length > 0 ? file : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function setMouseTracking(tui: any, enabled: boolean): void {
|
|
117
|
+
const write = tui?.terminal?.write?.bind(tui.terminal);
|
|
118
|
+
if (typeof write !== 'function') return;
|
|
119
|
+
write(enabled ? '\u001b[?1000h\u001b[?1006h' : '\u001b[?1006l\u001b[?1000l');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function toolFromRegistry(registry: any, name: string): unknown {
|
|
123
|
+
if (!registry) return undefined;
|
|
124
|
+
if (typeof registry.get === 'function') return registry.get(name);
|
|
125
|
+
if (Array.isArray(registry)) return registry.find((tool) => tool?.name === name);
|
|
126
|
+
if (typeof registry === 'object') return registry[name];
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolveRegisteredToolDefinition(ctx: any, pi: any, name: string): unknown {
|
|
131
|
+
return ctx?.pi?.getToolDefinition?.(name)
|
|
132
|
+
?? pi?.getToolDefinition?.(name)
|
|
133
|
+
?? ctx?.getToolDefinition?.(name)
|
|
134
|
+
?? toolFromRegistry(ctx?.pi?.tools, name)
|
|
135
|
+
?? toolFromRegistry(pi?.tools, name)
|
|
136
|
+
?? toolFromRegistry(ctx?.tools, name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function clip(text: string | undefined, limit = 120): string {
|
|
140
|
+
if (!text) return '';
|
|
141
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
142
|
+
return normalized.length > limit ? `${normalized.slice(0, Math.max(0, limit - 1))}…` : normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isActiveBackgroundTask(task: SubagentTask): boolean {
|
|
146
|
+
return task.mode === 'background' && (task.status === 'queued' || task.status === 'running');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildClaudeBackgroundWidgetEntries(tasks: SubagentTask[]): ClaudeBackgroundWidgetEntry[] {
|
|
150
|
+
const active = tasks.filter(isActiveBackgroundTask);
|
|
151
|
+
if (!active.length) return [];
|
|
152
|
+
return [
|
|
153
|
+
{ key: 'main', line: 'main' },
|
|
154
|
+
...active.map((task) => ({ key: task.id, line: `${task.agent} ${clip(task.last_activity ?? task.task ?? task.id)}` })),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function coerceClaudeBackgroundSelection(entries: ClaudeBackgroundWidgetEntry[], selectedKey: string | undefined): string {
|
|
159
|
+
if (!entries.length) return 'main';
|
|
160
|
+
return entries.some((entry) => entry.key === selectedKey) ? selectedKey! : entries[0]!.key;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function moveClaudeBackgroundWidgetSelection(tasks: SubagentTask[], selectedKey: string | undefined, direction: 'up' | 'down'): string {
|
|
164
|
+
const entries = buildClaudeBackgroundWidgetEntries(tasks);
|
|
165
|
+
if (!entries.length) return 'main';
|
|
166
|
+
const current = coerceClaudeBackgroundSelection(entries, selectedKey);
|
|
167
|
+
const index = entries.findIndex((entry) => entry.key === current);
|
|
168
|
+
const nextIndex = direction === 'down'
|
|
169
|
+
? Math.min(index + 1, entries.length - 1)
|
|
170
|
+
: Math.max(index - 1, 0);
|
|
171
|
+
return entries[nextIndex]?.key ?? current;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function renderClaudeBackgroundWidgetLines(tasks: SubagentTask[], selectedKey?: string): string[] | undefined {
|
|
175
|
+
const entries = buildClaudeBackgroundWidgetEntries(tasks);
|
|
176
|
+
if (!entries.length) return undefined;
|
|
177
|
+
const current = selectedKey === undefined ? undefined : coerceClaudeBackgroundSelection(entries, selectedKey);
|
|
178
|
+
return entries.map((entry) => `${entry.key === current ? '●' : '○'} ${entry.line}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class ClaudeBackgroundWidgetState {
|
|
182
|
+
private selectedKey = 'main';
|
|
183
|
+
private navigationActive = false;
|
|
184
|
+
|
|
185
|
+
constructor(
|
|
186
|
+
private getTasks: () => SubagentTask[],
|
|
187
|
+
private onChange?: () => void,
|
|
188
|
+
) {}
|
|
189
|
+
|
|
190
|
+
getSelectedKey(): string {
|
|
191
|
+
this.selectedKey = coerceClaudeBackgroundSelection(buildClaudeBackgroundWidgetEntries(this.getTasks()), this.selectedKey);
|
|
192
|
+
return this.selectedKey;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
renderLines(): string[] {
|
|
196
|
+
return renderClaudeBackgroundWidgetLines(this.getTasks(), this.navigationActive ? this.getSelectedKey() : undefined) ?? [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
handleWidgetInput(data: string): void {
|
|
200
|
+
this.handleTerminalInput(data);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
handleTerminalInput(data: string): ClaudeBackgroundTerminalInputResult {
|
|
204
|
+
const tasks = this.getTasks();
|
|
205
|
+
if (!tasks.some(isActiveBackgroundTask)) {
|
|
206
|
+
if (this.navigationActive || this.selectedKey !== 'main') {
|
|
207
|
+
this.selectedKey = 'main';
|
|
208
|
+
this.navigationActive = false;
|
|
209
|
+
this.onChange?.();
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (matchesKey(data, 'down')) {
|
|
215
|
+
this.navigationActive = true;
|
|
216
|
+
const next = moveClaudeBackgroundWidgetSelection(tasks, this.getSelectedKey(), 'down');
|
|
217
|
+
if (next !== this.selectedKey) {
|
|
218
|
+
this.selectedKey = next;
|
|
219
|
+
this.onChange?.();
|
|
220
|
+
}
|
|
221
|
+
return { consume: true };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (matchesKey(data, 'up')) {
|
|
225
|
+
if (!this.navigationActive) return undefined;
|
|
226
|
+
if (this.getSelectedKey() === 'main') {
|
|
227
|
+
this.navigationActive = false;
|
|
228
|
+
this.onChange?.();
|
|
229
|
+
return { consume: true };
|
|
230
|
+
}
|
|
231
|
+
const next = moveClaudeBackgroundWidgetSelection(tasks, this.selectedKey, 'up');
|
|
232
|
+
if (next !== this.selectedKey) {
|
|
233
|
+
this.selectedKey = next;
|
|
234
|
+
this.onChange?.();
|
|
235
|
+
}
|
|
236
|
+
return { consume: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this.navigationActive && (data === '\r' || data === '\n')) {
|
|
240
|
+
const selectedKey = this.getSelectedKey();
|
|
241
|
+
this.navigationActive = false;
|
|
242
|
+
this.onChange?.();
|
|
243
|
+
if (selectedKey === 'main') return { consume: true, action: { type: 'focus-editor' } };
|
|
244
|
+
return { consume: true, action: { type: 'open-task', taskId: selectedKey } };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.navigationActive && (matchesKey(data, 'left') || matchesKey(data, 'right') || matchesKey(data, 'escape'))) {
|
|
248
|
+
this.navigationActive = false;
|
|
249
|
+
this.onChange?.();
|
|
250
|
+
return { consume: true, action: { type: 'focus-editor' } };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (this.navigationActive) return { consume: true };
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export class ClaudeBackgroundWidget {
|
|
259
|
+
constructor(
|
|
260
|
+
private state: ClaudeBackgroundWidgetState,
|
|
261
|
+
private theme: any,
|
|
262
|
+
) {}
|
|
263
|
+
|
|
264
|
+
invalidate(): void {}
|
|
265
|
+
|
|
266
|
+
render(width: number): string[] {
|
|
267
|
+
return this.state.renderLines().map((line) => truncateToWidth(this.decorate(line), width));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
handleInput(data: string): void {
|
|
271
|
+
this.state.handleWidgetInput(data);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private decorate(line: string): string {
|
|
275
|
+
if (!line.startsWith('● ')) return line;
|
|
276
|
+
return this.theme?.fg?.('warning', this.theme?.bold?.(line) ?? line) ?? line;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function completionMessage(task: any): string {
|
|
281
|
+
const result = task.result ?? task.error ?? task.output_preview ?? '(no result captured)';
|
|
282
|
+
return [
|
|
283
|
+
`Subagent ${task.agent} ${task.status}: ${task.id}`,
|
|
284
|
+
'',
|
|
285
|
+
'Read only this final response from the subagent. Do not reread the full execution transcript unless the user explicitly asks for debugging details.',
|
|
286
|
+
'',
|
|
287
|
+
'## response sent to the orchestrator',
|
|
288
|
+
'',
|
|
289
|
+
result,
|
|
290
|
+
].join('\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function renderSubagentCompletionMessage(message: any, options: any, theme: any) {
|
|
294
|
+
const details = message.details ?? {};
|
|
295
|
+
const task = details.task ?? details;
|
|
296
|
+
const status = task.status ?? 'completed';
|
|
297
|
+
const failed = status === 'failed' || status === 'cancelled';
|
|
298
|
+
const expanded = Boolean(options?.expanded);
|
|
299
|
+
const result = details.full_result ?? task.result ?? task.error ?? '';
|
|
300
|
+
const title = `[subagent] ${task.agent ?? 'subagent'} ${status}: ${task.id ?? task.task_id ?? ''}`.trim();
|
|
301
|
+
const sections: Array<{ text: string; style?: 'label' | 'status' | 'dim' | 'body' | 'heading' }> = [
|
|
302
|
+
{ text: title, style: 'label' },
|
|
303
|
+
{ text: `response: ${expanded ? 'expanded' : 'collapsed'}${expanded ? '' : ' · ctrl+o to expand'}`, style: expanded ? 'status' : 'dim' },
|
|
304
|
+
];
|
|
305
|
+
if (expanded && result) {
|
|
306
|
+
sections.push(
|
|
307
|
+
{ text: '─'.repeat(24), style: 'dim' },
|
|
308
|
+
{ text: 'response sent to the orchestrator', style: 'heading' },
|
|
309
|
+
...String(result).split('\n').map((line) => ({ text: line, style: 'body' as const })),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const color = (section: { text: string; style?: 'label' | 'status' | 'dim' | 'body' | 'heading' }, text: string) => {
|
|
313
|
+
if (section.style === 'label') return theme.fg?.(failed ? 'error' : 'customMessageLabel', text) ?? text;
|
|
314
|
+
if (section.style === 'status') return theme.fg?.(failed ? 'error' : 'success', text) ?? text;
|
|
315
|
+
if (section.style === 'dim') return theme.fg?.('dim', text) ?? text;
|
|
316
|
+
if (section.style === 'heading') return theme.fg?.('toolTitle', text) ?? text;
|
|
317
|
+
if (section.style === 'body') return theme.fg?.('customMessageText', text) ?? text;
|
|
318
|
+
return text;
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
invalidate() {},
|
|
322
|
+
render(width: number) {
|
|
323
|
+
const blockWidth = Math.max(1, width);
|
|
324
|
+
const contentWidth = Math.max(1, blockWidth - 2);
|
|
325
|
+
return sections.flatMap((section) => wrapLineToWidth(section.text, contentWidth).map((line) => {
|
|
326
|
+
const styled = color(section, line);
|
|
327
|
+
const paddedVisibleWidth = Math.min(blockWidth, [...` ${line}`].length);
|
|
328
|
+
const rightPadding = ' '.repeat(Math.max(0, blockWidth - paddedVisibleWidth));
|
|
329
|
+
const padded = ` ${styled}${rightPadding}`;
|
|
330
|
+
return theme.bg?.('customMessageBg', padded) ?? padded;
|
|
331
|
+
}));
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default function subagentsExtension(pi: any): void {
|
|
337
|
+
pi.registerMessageRenderer?.('subagent-completion', renderSubagentCompletionMessage);
|
|
338
|
+
const manager = new SubagentManager(undefined, undefined, (task) => {
|
|
339
|
+
pi.sendMessage?.({
|
|
340
|
+
customType: 'subagent-completion',
|
|
341
|
+
content: completionMessage(task),
|
|
342
|
+
display: true,
|
|
343
|
+
details: {
|
|
344
|
+
full_result: task.result ?? task.error ?? task.output_preview,
|
|
345
|
+
task: {
|
|
346
|
+
id: task.id,
|
|
347
|
+
agent: task.agent,
|
|
348
|
+
status: task.status,
|
|
349
|
+
mode: task.mode,
|
|
350
|
+
model: task.model,
|
|
351
|
+
effort: task.effort,
|
|
352
|
+
usage: task.usage,
|
|
353
|
+
result: task.result,
|
|
354
|
+
error: task.error,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
}, {
|
|
358
|
+
triggerTurn: true,
|
|
359
|
+
deliverAs: 'followUp',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
registerSubagentTools(pi, manager);
|
|
363
|
+
|
|
364
|
+
let widgetTimer: NodeJS.Timeout | undefined;
|
|
365
|
+
let widgetCtx: any;
|
|
366
|
+
let widgetRequestRender: (() => void) | undefined;
|
|
367
|
+
let removeTerminalInputListener: (() => void) | undefined;
|
|
368
|
+
let widgetState: ClaudeBackgroundWidgetState | undefined;
|
|
369
|
+
let widgetInputSuspended = false;
|
|
370
|
+
let activePanelCancelSelected: (() => void) | undefined;
|
|
371
|
+
let activePanelRequestRender: (() => void) | undefined;
|
|
372
|
+
|
|
373
|
+
const installClaudeBackgroundWidget = (ctx: any): boolean => {
|
|
374
|
+
if (typeof ctx?.ui?.setWidget !== 'function') return false;
|
|
375
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
376
|
+
const config = readSubagentsConfig(cwd);
|
|
377
|
+
if (config.mode !== 'claude') {
|
|
378
|
+
ctx.ui.setWidget('subagents-claude-background', undefined);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
const sessionId = currentSessionId(ctx);
|
|
382
|
+
widgetState = new ClaudeBackgroundWidgetState(
|
|
383
|
+
() => manager.listSessionTasks(cwd, sessionId).slice(0, 100),
|
|
384
|
+
() => widgetRequestRender?.(),
|
|
385
|
+
);
|
|
386
|
+
if (typeof ctx?.ui?.onTerminalInput === 'function') {
|
|
387
|
+
removeTerminalInputListener = ctx.ui.onTerminalInput((data: string) => {
|
|
388
|
+
if (widgetInputSuspended) return undefined;
|
|
389
|
+
const result = widgetState?.handleTerminalInput(data);
|
|
390
|
+
if (result?.action?.type === 'open-task' && widgetCtx) void showSubagentsPanel(widgetCtx, result.action.taskId);
|
|
391
|
+
return result;
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
ctx.ui.setWidget('subagents-claude-background', (tui: any, theme: any) => {
|
|
395
|
+
widgetRequestRender = () => tui?.requestRender?.();
|
|
396
|
+
return new ClaudeBackgroundWidget(widgetState!, theme);
|
|
397
|
+
}, { placement: 'belowEditor' });
|
|
398
|
+
return true;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const clearClaudeBackgroundWidget = () => {
|
|
402
|
+
if (widgetTimer) clearInterval(widgetTimer);
|
|
403
|
+
widgetTimer = undefined;
|
|
404
|
+
widgetRequestRender = undefined;
|
|
405
|
+
removeTerminalInputListener?.();
|
|
406
|
+
removeTerminalInputListener = undefined;
|
|
407
|
+
widgetState = undefined;
|
|
408
|
+
widgetInputSuspended = false;
|
|
409
|
+
widgetCtx?.ui?.setWidget?.('subagents-claude-background', undefined);
|
|
410
|
+
widgetCtx = undefined;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
pi.on?.('session_start', (_event: unknown, ctx: any) => {
|
|
414
|
+
clearClaudeBackgroundWidget();
|
|
415
|
+
if (typeof ctx?.ui?.setWidget !== 'function') return;
|
|
416
|
+
widgetCtx = ctx;
|
|
417
|
+
if (!installClaudeBackgroundWidget(ctx)) return;
|
|
418
|
+
widgetTimer = setInterval(() => widgetRequestRender?.(), 250);
|
|
419
|
+
widgetTimer.unref?.();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
pi.on?.('session_shutdown', () => {
|
|
423
|
+
clearClaudeBackgroundWidget();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
async function showSubagentsPanel(ctx: any, selectedTaskId?: string) {
|
|
427
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
428
|
+
const sessionId = currentSessionId(ctx);
|
|
429
|
+
let refresh: NodeJS.Timeout | undefined;
|
|
430
|
+
widgetInputSuspended = true;
|
|
431
|
+
try {
|
|
432
|
+
await ctx.ui.custom(
|
|
433
|
+
(tui: any, theme: any, _keybindings: any, done: () => void) => {
|
|
434
|
+
setMouseTracking(tui, true);
|
|
435
|
+
const close = () => {
|
|
436
|
+
if (refresh) clearInterval(refresh);
|
|
437
|
+
setMouseTracking(tui, false);
|
|
438
|
+
done();
|
|
439
|
+
};
|
|
440
|
+
const config = readSubagentsConfig(cwd);
|
|
441
|
+
const baseMatchesKey = createSubagentsPanelKeyMatcher(_keybindings);
|
|
442
|
+
const panel = new SubagentsHistoryPanel(
|
|
443
|
+
() => manager.listSessionTasks(cwd, sessionId).slice(0, 100),
|
|
444
|
+
theme,
|
|
445
|
+
close,
|
|
446
|
+
(data: string, key: string) => {
|
|
447
|
+
if (key !== 'detailCancel') return baseMatchesKey(data, key);
|
|
448
|
+
const detailShortcut = config.detail_cancel_shortcut ?? 'x';
|
|
449
|
+
return baseMatchesKey(data, detailShortcut)
|
|
450
|
+
|| baseMatchesKey(data, 'detailCancel')
|
|
451
|
+
|| (detailShortcut === 'ctrl+w' && _keybindings?.matches?.(data, 'tui.editor.deleteWordBackward'));
|
|
452
|
+
},
|
|
453
|
+
visibleWidth,
|
|
454
|
+
truncateToWidth,
|
|
455
|
+
{
|
|
456
|
+
theme,
|
|
457
|
+
tui,
|
|
458
|
+
cwd,
|
|
459
|
+
visibleWidth,
|
|
460
|
+
truncateToWidth,
|
|
461
|
+
getToolDefinition: (name: string) => resolveRegisteredToolDefinition(ctx, pi, name),
|
|
462
|
+
getMessageRenderer: (customType: string) => ctx?.pi?.getMessageRenderer?.(customType) ?? ctx?.pi?.customMessageRenderers?.get?.(customType) ?? ctx?.customMessageRenderers?.get?.(customType),
|
|
463
|
+
showImages: ctx?.showImages,
|
|
464
|
+
imageWidthCells: ctx?.imageWidthCells,
|
|
465
|
+
},
|
|
466
|
+
() => Math.max(12, process.stdout.rows || 42),
|
|
467
|
+
(id: string) => manager.getTask(id, cwd),
|
|
468
|
+
selectedTaskId,
|
|
469
|
+
(id: string) => manager.cancel(id, 'cancelled from subagents detail view'),
|
|
470
|
+
config.detail_cancel_shortcut ?? 'x',
|
|
471
|
+
);
|
|
472
|
+
activePanelCancelSelected = () => panel.cancelSelectedActiveTask();
|
|
473
|
+
activePanelRequestRender = () => tui.requestRender?.();
|
|
474
|
+
refresh = setInterval(() => tui.requestRender?.(), 1000);
|
|
475
|
+
return {
|
|
476
|
+
render: (width: number) => panel.render(width),
|
|
477
|
+
invalidate: () => panel.invalidate(),
|
|
478
|
+
handleInput: (data: string) => { panel.handleInput(data); tui.requestRender?.(); },
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
undefined,
|
|
482
|
+
);
|
|
483
|
+
} finally {
|
|
484
|
+
activePanelCancelSelected = undefined;
|
|
485
|
+
activePanelRequestRender = undefined;
|
|
486
|
+
widgetInputSuspended = false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const historyPanelShortcut = readSubagentsConfig(process.cwd()).history_panel_shortcut ?? 'ctrl+,';
|
|
491
|
+
pi.registerShortcut?.(historyPanelShortcut, {
|
|
492
|
+
description: 'Show subagent history panel',
|
|
493
|
+
handler: async (ctx: any) => {
|
|
494
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
495
|
+
if (readSubagentsConfig(cwd).mode === 'claude') return;
|
|
496
|
+
await showSubagentsPanel(ctx);
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const detailCancelShortcut = readSubagentsConfig(process.cwd()).detail_cancel_shortcut ?? 'x';
|
|
501
|
+
if (detailCancelShortcut.startsWith('ctrl+')) {
|
|
502
|
+
pi.registerShortcut?.(detailCancelShortcut, {
|
|
503
|
+
description: 'Cancel selected running subagent from the active subagents detail panel',
|
|
504
|
+
handler: async () => {
|
|
505
|
+
activePanelCancelSelected?.();
|
|
506
|
+
activePanelRequestRender?.();
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const backgroundHandoffShortcut = readSubagentsConfig(process.cwd()).background_handoff_shortcut ?? 'ctrl+h';
|
|
512
|
+
pi.registerShortcut?.(backgroundHandoffShortcut, {
|
|
513
|
+
description: 'Send running claude subagent task to background',
|
|
514
|
+
handler: async (ctx: any) => {
|
|
515
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
516
|
+
if (readSubagentsConfig(cwd).mode !== 'claude') return;
|
|
517
|
+
triggerClaudeBackgroundHandoff();
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
pi.registerCommand?.('subagents', {
|
|
522
|
+
description: 'Show subagent history panel',
|
|
523
|
+
handler: async (_args: string, ctx: any) => showSubagentsPanel({ ...ctx, pi }),
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
pi.registerCommand?.('subagent-models', {
|
|
527
|
+
description: 'Configure subagent and SDD phase model profiles',
|
|
528
|
+
handler: async (_args: string, ctx: any) => runSubagentModelsCommand({ ...ctx, pi }),
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-subagents-j0k3r",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Installable Pi package that adds markdown-defined subagents, delegated task tools, history, and model profiles.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "j0k3r",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/j0k3r-dev-rgl/pi-subagents-j0k3r.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"pi",
|
|
16
|
+
"subagents",
|
|
17
|
+
"coding-agent",
|
|
18
|
+
"agent",
|
|
19
|
+
"delegation"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.19.0"
|
|
23
|
+
},
|
|
24
|
+
"main": "./index.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"index.ts",
|
|
27
|
+
"src",
|
|
28
|
+
"skills",
|
|
29
|
+
"scripts/verify-package-files.mjs",
|
|
30
|
+
".releaserc.json",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"CHANGELOG.md"
|
|
34
|
+
],
|
|
35
|
+
"pi": {
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./index.ts"
|
|
38
|
+
],
|
|
39
|
+
"skills": [
|
|
40
|
+
"./skills"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"verify:package": "node scripts/verify-package-files.mjs",
|
|
47
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
48
|
+
"check": "npm run typecheck && npm test && npm run verify:package",
|
|
49
|
+
"release": "semantic-release",
|
|
50
|
+
"prepublishOnly": "npm run check"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
54
|
+
"typebox": "*"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@earendil-works/pi-coding-agent": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"typebox": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@semantic-release/github": "12.0.8",
|
|
66
|
+
"@semantic-release/npm": "13.1.5",
|
|
67
|
+
"@types/node": "24.10.1",
|
|
68
|
+
"semantic-release": "25.0.5",
|
|
69
|
+
"typebox": "1.1.38",
|
|
70
|
+
"typescript": "5.9.3",
|
|
71
|
+
"vitest": "4.1.7"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const root = join(fileURLToPath(new URL('..', import.meta.url)));
|
|
7
|
+
|
|
8
|
+
const requiredFiles = [
|
|
9
|
+
'index.ts',
|
|
10
|
+
'README.md',
|
|
11
|
+
'LICENSE',
|
|
12
|
+
'package.json',
|
|
13
|
+
'.releaserc.json',
|
|
14
|
+
'src/config.ts',
|
|
15
|
+
'src/debug.ts',
|
|
16
|
+
'src/history.ts',
|
|
17
|
+
'src/interaction-channel.ts',
|
|
18
|
+
'src/manager.ts',
|
|
19
|
+
'src/model-profiles-ui.ts',
|
|
20
|
+
'src/profile-resolver.ts',
|
|
21
|
+
'src/runner.ts',
|
|
22
|
+
'src/thread-view.ts',
|
|
23
|
+
'src/tools.ts',
|
|
24
|
+
'src/types.ts',
|
|
25
|
+
'src/ui.ts',
|
|
26
|
+
'skills/subagents-configuration/SKILL.md',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const missing = requiredFiles.filter((relativePath) => {
|
|
30
|
+
const absolutePath = join(root, relativePath);
|
|
31
|
+
return !existsSync(absolutePath) || !statSync(absolutePath).isFile();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
console.error('pi-subagents-j0k3r package is missing required Pi resources:');
|
|
36
|
+
for (const relativePath of missing) console.error(`- ${relativePath}`);
|
|
37
|
+
console.error('\nRefusing to pack/publish an incomplete npm package.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`pi-subagents-j0k3r package resource check passed (${requiredFiles.length} files).`);
|