pi-subagents-j0k3r 1.2.0 → 1.3.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 (44) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/index.ts +5 -621
  3. package/package.json +1 -1
  4. package/src/extension/subagents-extension.ts +149 -0
  5. package/src/model-profiles/command.ts +378 -0
  6. package/src/model-profiles/data.ts +110 -0
  7. package/src/model-profiles/editor.ts +74 -0
  8. package/src/model-profiles/formatting.ts +86 -0
  9. package/src/model-profiles-ui.ts +4 -631
  10. package/src/render/completion-message.ts +89 -0
  11. package/src/render/config.ts +1 -0
  12. package/src/render/result-details.ts +1 -0
  13. package/src/render/text-width.ts +43 -0
  14. package/src/render/tools/components.ts +39 -0
  15. package/src/render/tools/formatting.ts +127 -0
  16. package/src/render/tools/progress.ts +15 -0
  17. package/src/render/tools/subagent-list-tasks.ts +9 -0
  18. package/src/render/tools/subagent-result.ts +21 -0
  19. package/src/render/tools/subagent-run.ts +47 -0
  20. package/src/render/types.ts +1 -0
  21. package/src/runner/event-processing.ts +303 -0
  22. package/src/runner/index.ts +3 -0
  23. package/src/runner/interaction-session-registry.ts +9 -0
  24. package/src/runner/pi-sdk-module.ts +7 -0
  25. package/src/runner/prompt.ts +8 -0
  26. package/src/runner/sdk-runner.ts +178 -0
  27. package/src/runner/snapshot-builder.ts +309 -0
  28. package/src/runner.ts +3 -775
  29. package/src/tools/background-handoff-state.ts +55 -0
  30. package/src/tools/registry.ts +16 -0
  31. package/src/tools/result-details.ts +39 -0
  32. package/src/tools/subagent-cancel.ts +17 -0
  33. package/src/tools/subagent-list-agents.ts +16 -0
  34. package/src/tools/subagent-list-tasks.ts +24 -0
  35. package/src/tools/subagent-result.ts +24 -0
  36. package/src/tools/subagent-run.ts +105 -0
  37. package/src/tools/subagent-status.ts +21 -0
  38. package/src/tools/tool-response.ts +2 -0
  39. package/src/tools.ts +2 -501
  40. package/src/ui/background-widget.ts +170 -0
  41. package/src/ui/panel-input.ts +67 -0
  42. package/src/ui/panel-overlay.ts +159 -0
  43. package/src/ui/subagents-history-panel.ts +508 -0
  44. package/src/ui.ts +1 -508
package/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.2.1 - 2026-07-12
4
+
5
+ ### Fixed
6
+ - Stabilized `/subagents` rendering with a full-screen overlay that prevents parent-chat flicker and reserves space for the panel border.
7
+
8
+ ## 1.2.0 - 2026-07-12
4
9
 
5
10
  ### Added
6
11
  - Added opt-in render diagnostics with bounded JSONL logging for subagent UI and completion rendering.
package/index.ts CHANGED
@@ -1,621 +1,5 @@
1
- import { SubagentManager } from './src/manager.js';
2
- import { readSubagentsConfig, subagentSourceWarnings } 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 { createSubagentsRenderLogger } from './src/render-debug.js';
7
- import { safeErrorMetadataDetails } from './src/error-metadata.js';
8
- import type { SubagentTask } from './src/types.js';
9
-
10
- type ClaudeBackgroundWidgetEntry = {
11
- key: string;
12
- line: string;
13
- };
14
-
15
- type ClaudeBackgroundTerminalAction =
16
- | { type: 'focus-editor' }
17
- | { type: 'open-task'; taskId: string };
18
-
19
- type ClaudeBackgroundTerminalInputResult = {
20
- consume?: boolean;
21
- data?: string;
22
- action?: ClaudeBackgroundTerminalAction;
23
- } | undefined;
24
-
25
- function matchesKey(data: string, key: string): boolean {
26
- const keys: Record<string, string[]> = {
27
- escape: ['\u001b'],
28
- 'ctrl+c': ['\u0003'],
29
- 'ctrl+o': ['\u000f'],
30
- 'ctrl+w': ['\u0017'],
31
- q: ['q', 'Q'],
32
- up: ['\u001b[A'],
33
- down: ['\u001b[B'],
34
- right: ['\u001b[C'],
35
- left: ['\u001b[D'],
36
- pageUp: ['\u001b[5~'],
37
- pageDown: ['\u001b[6~'],
38
- home: ['\u001b[H', '\u001b[1~', '\u001bOH'],
39
- end: ['\u001b[F', '\u001b[4~', '\u001bOF'],
40
- };
41
- return keys[key]?.includes(data) ?? data === key;
42
- }
43
-
44
- const PANEL_KEYBINDINGS: Record<string, string[]> = {
45
- escape: ['app.interrupt', 'tui.select.cancel'],
46
- 'ctrl+c': ['tui.select.cancel'],
47
- 'ctrl+o': ['app.tools.expand'],
48
- detailCancel: ['subagents.detail.cancel'],
49
- up: ['tui.select.up', 'tui.editor.cursorUp'],
50
- down: ['tui.select.down', 'tui.editor.cursorDown'],
51
- right: ['tui.editor.cursorRight'],
52
- left: ['tui.editor.cursorLeft'],
53
- pageUp: ['tui.select.pageUp', 'tui.editor.pageUp'],
54
- pageDown: ['tui.select.pageDown', 'tui.editor.pageDown'],
55
- home: ['tui.editor.cursorLineStart'],
56
- end: ['tui.editor.cursorLineEnd'],
57
- };
58
-
59
- export function createSubagentsPanelKeyMatcher(keybindings?: { matches?: (data: string, keybinding: string) => boolean }) {
60
- return (data: string, key: string): boolean => {
61
- const bindings = PANEL_KEYBINDINGS[key];
62
- if (bindings?.some((binding) => keybindings?.matches?.(data, binding))) return true;
63
- return matchesKey(data, key);
64
- };
65
- }
66
-
67
- function visibleWidth(text: string): number {
68
- return [...text.replace(/\u001b\][^\u001b\u0007]*(?:\u001b\\|\u0007)|\u001b\[[0-?]*[ -/]*[@-~]/g, '')].length;
69
- }
70
-
71
- function truncateToWidth(text: string, width: number): string {
72
- const chars = [...text];
73
- return chars.length > width ? chars.slice(0, Math.max(0, width - 1)).join('') + '…' : text;
74
- }
75
-
76
- function wrapLineToWidth(line: string, width: number): string[] {
77
- const max = Math.max(1, width);
78
- if (!line) return [''];
79
- if ([...line].length <= max) return [line];
80
- const out: string[] = [];
81
- let current = '';
82
- for (const word of line.split(/\s+/).filter(Boolean)) {
83
- const wordLength = [...word].length;
84
- const currentLength = [...current].length;
85
- if (!current) {
86
- if (wordLength <= max) {
87
- current = word;
88
- } else {
89
- const chars = [...word];
90
- for (let index = 0; index < chars.length; index += max) out.push(chars.slice(index, index + max).join(''));
91
- }
92
- continue;
93
- }
94
- if (currentLength + 1 + wordLength <= max) {
95
- current += ` ${word}`;
96
- continue;
97
- }
98
- out.push(current);
99
- current = '';
100
- if (wordLength <= max) {
101
- current = word;
102
- } else {
103
- const chars = [...word];
104
- for (let index = 0; index < chars.length; index += max) out.push(chars.slice(index, index + max).join(''));
105
- }
106
- }
107
- if (current) out.push(current);
108
- return out.length ? out : [''];
109
- }
110
-
111
- function currentSessionId(ctx: any): string | undefined {
112
- const direct = ctx?.sessionManager?.getSessionId?.() ?? ctx?.sessionId;
113
- if (typeof direct === 'string' && direct.length > 0) return direct;
114
- const file = ctx?.sessionManager?.getSessionFile?.();
115
- return typeof file === 'string' && file.length > 0 ? file : undefined;
116
- }
117
-
118
- function setMouseTracking(tui: any, enabled: boolean): void {
119
- const write = tui?.terminal?.write?.bind(tui.terminal);
120
- if (typeof write !== 'function') return;
121
- write(enabled ? '\u001b[?1000h\u001b[?1006h' : '\u001b[?1006l\u001b[?1000l');
122
- }
123
-
124
- function subagentsPanelMouseWheelDelta(data: string): -1 | 1 | undefined {
125
- const sgr = data.match(/^\u001b\[<(\d+);\d+;\d+M$/);
126
- const urxvt = data.match(/^\u001b\[(\d+);\d+;\d+M$/);
127
- const button = sgr || urxvt ? Number((sgr ?? urxvt)![1]) : data.startsWith('\u001b[M') && data.length >= 6 ? data.charCodeAt(3) - 32 : undefined;
128
- if (button === undefined || !Number.isFinite(button) || (button & 64) === 0) return undefined;
129
- return (button & 1) === 0 ? -1 : 1;
130
- }
131
-
132
- function classifySubagentsPanelInput(data: string, matchesPanelKey: (data: string, key: string) => boolean): { category: string; action: string } {
133
- if (matchesPanelKey(data, 'escape') || matchesPanelKey(data, 'ctrl+c') || matchesPanelKey(data, 'q')) return { category: 'lifecycle', action: 'close' };
134
- if (matchesPanelKey(data, 'ctrl+o') || data === '\u000f') return { category: 'display', action: 'toggle_expand' };
135
- if (matchesPanelKey(data, 'detailCancel')) return { category: 'task', action: 'cancel_selected' };
136
- const wheel = subagentsPanelMouseWheelDelta(data);
137
- if (wheel === -1) return { category: 'scroll', action: 'up' };
138
- if (wheel === 1) return { category: 'scroll', action: 'down' };
139
- if (matchesPanelKey(data, 'right')) return { category: 'navigation', action: 'right' };
140
- if (matchesPanelKey(data, 'left')) return { category: 'navigation', action: 'left' };
141
- if (matchesPanelKey(data, 'down')) return { category: 'navigation', action: 'down' };
142
- if (matchesPanelKey(data, 'up')) return { category: 'navigation', action: 'up' };
143
- if (matchesPanelKey(data, 'pageDown')) return { category: 'scroll', action: 'page_down' };
144
- if (matchesPanelKey(data, 'pageUp')) return { category: 'scroll', action: 'page_up' };
145
- if (matchesPanelKey(data, 'home')) return { category: 'scroll', action: 'home' };
146
- if (matchesPanelKey(data, 'end')) return { category: 'scroll', action: 'end' };
147
- return { category: 'other', action: 'unmatched' };
148
- }
149
-
150
- function toolFromRegistry(registry: any, name: string): unknown {
151
- if (!registry) return undefined;
152
- if (typeof registry.get === 'function') return registry.get(name);
153
- if (Array.isArray(registry)) return registry.find((tool) => tool?.name === name);
154
- if (typeof registry === 'object') return registry[name];
155
- return undefined;
156
- }
157
-
158
- export function resolveRegisteredToolDefinition(ctx: any, pi: any, name: string): unknown {
159
- return ctx?.pi?.getToolDefinition?.(name)
160
- ?? pi?.getToolDefinition?.(name)
161
- ?? ctx?.getToolDefinition?.(name)
162
- ?? toolFromRegistry(ctx?.pi?.tools, name)
163
- ?? toolFromRegistry(pi?.tools, name)
164
- ?? toolFromRegistry(ctx?.tools, name);
165
- }
166
-
167
- function clip(text: string | undefined, limit = 120): string {
168
- if (!text) return '';
169
- const normalized = text.replace(/\s+/g, ' ').trim();
170
- return normalized.length > limit ? `${normalized.slice(0, Math.max(0, limit - 1))}…` : normalized;
171
- }
172
-
173
- function isActiveBackgroundTask(task: SubagentTask): boolean {
174
- return task.mode === 'background' && (task.status === 'queued' || task.status === 'running');
175
- }
176
-
177
- function buildClaudeBackgroundWidgetEntries(tasks: SubagentTask[]): ClaudeBackgroundWidgetEntry[] {
178
- const active = tasks.filter(isActiveBackgroundTask);
179
- if (!active.length) return [];
180
- return [
181
- { key: 'main', line: 'main' },
182
- ...active.map((task) => ({ key: task.id, line: `${task.agent} ${clip(task.last_activity ?? task.task ?? task.id)}` })),
183
- ];
184
- }
185
-
186
- function coerceClaudeBackgroundSelection(entries: ClaudeBackgroundWidgetEntry[], selectedKey: string | undefined): string {
187
- if (!entries.length) return 'main';
188
- return entries.some((entry) => entry.key === selectedKey) ? selectedKey! : entries[0]!.key;
189
- }
190
-
191
- export function moveClaudeBackgroundWidgetSelection(tasks: SubagentTask[], selectedKey: string | undefined, direction: 'up' | 'down'): string {
192
- const entries = buildClaudeBackgroundWidgetEntries(tasks);
193
- if (!entries.length) return 'main';
194
- const current = coerceClaudeBackgroundSelection(entries, selectedKey);
195
- const index = entries.findIndex((entry) => entry.key === current);
196
- const nextIndex = direction === 'down'
197
- ? Math.min(index + 1, entries.length - 1)
198
- : Math.max(index - 1, 0);
199
- return entries[nextIndex]?.key ?? current;
200
- }
201
-
202
- export function renderClaudeBackgroundWidgetLines(tasks: SubagentTask[], selectedKey?: string): string[] | undefined {
203
- const entries = buildClaudeBackgroundWidgetEntries(tasks);
204
- if (!entries.length) return undefined;
205
- const current = selectedKey === undefined ? undefined : coerceClaudeBackgroundSelection(entries, selectedKey);
206
- return entries.map((entry) => `${entry.key === current ? '●' : '○'} ${entry.line}`);
207
- }
208
-
209
- export class ClaudeBackgroundWidgetState {
210
- private selectedKey = 'main';
211
- private navigationActive = false;
212
-
213
- constructor(
214
- private getTasks: () => SubagentTask[],
215
- private onChange?: () => void,
216
- ) {}
217
-
218
- getSelectedKey(): string {
219
- this.selectedKey = coerceClaudeBackgroundSelection(buildClaudeBackgroundWidgetEntries(this.getTasks()), this.selectedKey);
220
- return this.selectedKey;
221
- }
222
-
223
- renderLines(): string[] {
224
- return renderClaudeBackgroundWidgetLines(this.getTasks(), this.navigationActive ? this.getSelectedKey() : undefined) ?? [];
225
- }
226
-
227
- handleWidgetInput(data: string): void {
228
- this.handleTerminalInput(data);
229
- }
230
-
231
- handleTerminalInput(data: string): ClaudeBackgroundTerminalInputResult {
232
- const tasks = this.getTasks();
233
- if (!tasks.some(isActiveBackgroundTask)) {
234
- if (this.navigationActive || this.selectedKey !== 'main') {
235
- this.selectedKey = 'main';
236
- this.navigationActive = false;
237
- this.onChange?.();
238
- }
239
- return undefined;
240
- }
241
-
242
- if (matchesKey(data, 'down')) {
243
- this.navigationActive = true;
244
- const next = moveClaudeBackgroundWidgetSelection(tasks, this.getSelectedKey(), 'down');
245
- if (next !== this.selectedKey) {
246
- this.selectedKey = next;
247
- this.onChange?.();
248
- }
249
- return { consume: true };
250
- }
251
-
252
- if (matchesKey(data, 'up')) {
253
- if (!this.navigationActive) return undefined;
254
- if (this.getSelectedKey() === 'main') {
255
- this.navigationActive = false;
256
- this.onChange?.();
257
- return { consume: true };
258
- }
259
- const next = moveClaudeBackgroundWidgetSelection(tasks, this.selectedKey, 'up');
260
- if (next !== this.selectedKey) {
261
- this.selectedKey = next;
262
- this.onChange?.();
263
- }
264
- return { consume: true };
265
- }
266
-
267
- if (this.navigationActive && (data === '\r' || data === '\n')) {
268
- const selectedKey = this.getSelectedKey();
269
- this.navigationActive = false;
270
- this.onChange?.();
271
- if (selectedKey === 'main') return { consume: true, action: { type: 'focus-editor' } };
272
- return { consume: true, action: { type: 'open-task', taskId: selectedKey } };
273
- }
274
-
275
- if (this.navigationActive && (matchesKey(data, 'left') || matchesKey(data, 'right') || matchesKey(data, 'escape'))) {
276
- this.navigationActive = false;
277
- this.onChange?.();
278
- return { consume: true, action: { type: 'focus-editor' } };
279
- }
280
-
281
- if (this.navigationActive) return { consume: true };
282
- return undefined;
283
- }
284
- }
285
-
286
- export class ClaudeBackgroundWidget {
287
- constructor(
288
- private state: ClaudeBackgroundWidgetState,
289
- private theme: any,
290
- ) {}
291
-
292
- invalidate(): void {}
293
-
294
- render(width: number): string[] {
295
- return this.state.renderLines().map((line) => truncateToWidth(this.decorate(line), width));
296
- }
297
-
298
- handleInput(data: string): void {
299
- this.state.handleWidgetInput(data);
300
- }
301
-
302
- private decorate(line: string): string {
303
- if (!line.startsWith('● ')) return line;
304
- return this.theme?.fg?.('warning', this.theme?.bold?.(line) ?? line) ?? line;
305
- }
306
- }
307
-
308
- export function completionMessage(task: any): string {
309
- const result = task.result ?? task.error ?? task.output_preview ?? '(no result captured)';
310
- return [
311
- `Subagent ${task.agent} ${task.status}: ${task.id}`,
312
- '',
313
- 'Read only this final response from the subagent. Do not reread the full execution transcript unless the user explicitly asks for debugging details.',
314
- '',
315
- '## response sent to the orchestrator',
316
- '',
317
- result,
318
- ].join('\n');
319
- }
320
-
321
- function safeCompletionErrorMetadata(task: any): Record<string, unknown> | undefined {
322
- if (!task?.error_metadata) return undefined;
323
- return safeErrorMetadataDetails(task.error_metadata as any);
324
- }
325
-
326
- export function sendSubagentCompletionMessage(pi: any, task: any): void {
327
- pi.sendMessage?.({
328
- customType: 'subagent-completion',
329
- content: completionMessage(task),
330
- display: true,
331
- details: {
332
- full_result: task.result ?? task.error ?? task.output_preview,
333
- task: {
334
- id: task.id,
335
- agent: task.agent,
336
- status: task.status,
337
- mode: task.mode,
338
- model: task.model,
339
- effort: task.effort,
340
- usage: task.usage,
341
- result: task.result,
342
- error: task.error,
343
- error_metadata: safeCompletionErrorMetadata(task),
344
- },
345
- },
346
- }, {
347
- triggerTurn: false,
348
- deliverAs: 'steer',
349
- });
350
- }
351
-
352
- export function renderSubagentCompletionMessage(message: any, options: any, theme: any) {
353
- const details = message.details ?? {};
354
- const task = details.task ?? details;
355
- const status = task.status ?? 'completed';
356
- const failed = status === 'failed' || status === 'cancelled';
357
- const expanded = Boolean(options?.expanded);
358
- const result = details.full_result ?? task.result ?? task.error ?? '';
359
- const title = `[subagent] ${task.agent ?? 'subagent'} ${status}: ${task.id ?? task.task_id ?? ''}`.trim();
360
- const sections: Array<{ text: string; style?: 'label' | 'status' | 'dim' | 'body' | 'heading' }> = [
361
- { text: title, style: 'label' },
362
- { text: `response: ${expanded ? 'expanded' : 'collapsed'}${expanded ? '' : ' · ctrl+o to expand'}`, style: expanded ? 'status' : 'dim' },
363
- ];
364
- if (expanded && result) {
365
- sections.push(
366
- { text: '─'.repeat(24), style: 'dim' },
367
- { text: 'response sent to the orchestrator', style: 'heading' },
368
- ...String(result).split('\n').map((line) => ({ text: line, style: 'body' as const })),
369
- );
370
- }
371
- const color = (section: { text: string; style?: 'label' | 'status' | 'dim' | 'body' | 'heading' }, text: string) => {
372
- if (section.style === 'label') return theme.fg?.(failed ? 'error' : 'customMessageLabel', text) ?? text;
373
- if (section.style === 'status') return theme.fg?.(failed ? 'error' : 'success', text) ?? text;
374
- if (section.style === 'dim') return theme.fg?.('dim', text) ?? text;
375
- if (section.style === 'heading') return theme.fg?.('toolTitle', text) ?? text;
376
- if (section.style === 'body') return theme.fg?.('customMessageText', text) ?? text;
377
- return text;
378
- };
379
- return {
380
- invalidate() {},
381
- render(width: number) {
382
- const blockWidth = Math.max(1, width);
383
- const contentWidth = Math.max(1, blockWidth - 2);
384
- return sections.flatMap((section) => wrapLineToWidth(section.text, contentWidth).map((line) => {
385
- const styled = color(section, line);
386
- const paddedVisibleWidth = Math.min(blockWidth, [...` ${line}`].length);
387
- const rightPadding = ' '.repeat(Math.max(0, blockWidth - paddedVisibleWidth));
388
- const padded = ` ${styled}${rightPadding}`;
389
- return theme.bg?.('customMessageBg', padded) ?? padded;
390
- }));
391
- },
392
- };
393
- }
394
-
395
- export default function subagentsExtension(pi: any): void {
396
- pi.registerMessageRenderer?.('subagent-completion', renderSubagentCompletionMessage);
397
- const manager = new SubagentManager(undefined, undefined, (task) => {
398
- sendSubagentCompletionMessage(pi, task);
399
- });
400
- registerSubagentTools(pi, manager);
401
-
402
- let widgetTimer: NodeJS.Timeout | undefined;
403
- let widgetCtx: any;
404
- let widgetRequestRender: (() => void) | undefined;
405
- let removeTerminalInputListener: (() => void) | undefined;
406
- let widgetState: ClaudeBackgroundWidgetState | undefined;
407
- let widgetInputSuspended = false;
408
- let activePanelCancelSelected: (() => void) | undefined;
409
- let activePanelRequestRender: (() => void) | undefined;
410
-
411
- const installClaudeBackgroundWidget = (ctx: any): boolean => {
412
- if (typeof ctx?.ui?.setWidget !== 'function') return false;
413
- const cwd = ctx?.cwd ?? process.cwd();
414
- const config = readSubagentsConfig(cwd);
415
- if (config.mode !== 'claude') {
416
- ctx.ui.setWidget('subagents-claude-background', undefined);
417
- return false;
418
- }
419
- const sessionId = currentSessionId(ctx);
420
- widgetState = new ClaudeBackgroundWidgetState(
421
- () => manager.listSessionTasks(cwd, sessionId).slice(0, 100),
422
- () => widgetRequestRender?.(),
423
- );
424
- if (typeof ctx?.ui?.onTerminalInput === 'function') {
425
- removeTerminalInputListener = ctx.ui.onTerminalInput((data: string) => {
426
- if (widgetInputSuspended) return undefined;
427
- const result = widgetState?.handleTerminalInput(data);
428
- if (result?.action?.type === 'open-task' && widgetCtx) void showSubagentsPanel(widgetCtx, result.action.taskId);
429
- return result;
430
- });
431
- }
432
- ctx.ui.setWidget('subagents-claude-background', (tui: any, theme: any) => {
433
- widgetRequestRender = () => tui?.requestRender?.();
434
- return new ClaudeBackgroundWidget(widgetState!, theme);
435
- }, { placement: 'belowEditor' });
436
- return true;
437
- };
438
-
439
- const clearClaudeBackgroundWidget = () => {
440
- if (widgetTimer) clearInterval(widgetTimer);
441
- widgetTimer = undefined;
442
- widgetRequestRender = undefined;
443
- removeTerminalInputListener?.();
444
- removeTerminalInputListener = undefined;
445
- widgetState = undefined;
446
- widgetInputSuspended = false;
447
- widgetCtx?.ui?.setWidget?.('subagents-claude-background', undefined);
448
- widgetCtx = undefined;
449
- };
450
-
451
- pi.on?.('session_start', (_event: unknown, ctx: any) => {
452
- clearClaudeBackgroundWidget();
453
- const cwd = ctx?.cwd ?? process.cwd();
454
- for (const warning of subagentSourceWarnings(cwd)) ctx?.ui?.notify?.(warning, 'warning');
455
- if (typeof ctx?.ui?.setWidget !== 'function') return;
456
- widgetCtx = ctx;
457
- if (!installClaudeBackgroundWidget(ctx)) return;
458
- widgetTimer = setInterval(() => widgetRequestRender?.(), 250);
459
- widgetTimer.unref?.();
460
- });
461
-
462
- pi.on?.('session_shutdown', () => {
463
- clearClaudeBackgroundWidget();
464
- });
465
-
466
- async function showSubagentsPanel(ctx: any, selectedTaskId?: string) {
467
- const cwd = ctx?.cwd ?? process.cwd();
468
- const sessionId = currentSessionId(ctx);
469
- let refresh: NodeJS.Timeout | undefined;
470
- widgetInputSuspended = true;
471
- try {
472
- await ctx.ui.custom(
473
- (tui: any, theme: any, _keybindings: any, done: () => void) => {
474
- setMouseTracking(tui, true);
475
- const config = readSubagentsConfig(cwd);
476
- const renderLogger = createSubagentsRenderLogger({ cwd, sessionId, config: config.render_debug });
477
- let nextRenderReason = 'initial';
478
- let renderCycle = 0;
479
- const close = () => {
480
- if (refresh) clearInterval(refresh);
481
- renderLogger.log({ event: 'panel_disposed' });
482
- setMouseTracking(tui, false);
483
- done();
484
- };
485
- const baseMatchesKey = createSubagentsPanelKeyMatcher(_keybindings);
486
- const panel = new SubagentsHistoryPanel(
487
- () => manager.listSessionTasks(cwd, sessionId).slice(0, 100),
488
- theme,
489
- close,
490
- (data: string, key: string) => {
491
- if (key !== 'detailCancel') return baseMatchesKey(data, key);
492
- const detailShortcut = config.detail_cancel_shortcut ?? 'x';
493
- return baseMatchesKey(data, detailShortcut)
494
- || baseMatchesKey(data, 'detailCancel')
495
- || (detailShortcut === 'ctrl+w' && _keybindings?.matches?.(data, 'tui.editor.deleteWordBackward'));
496
- },
497
- visibleWidth,
498
- truncateToWidth,
499
- {
500
- theme,
501
- tui,
502
- cwd,
503
- visibleWidth,
504
- truncateToWidth,
505
- getToolDefinition: (name: string) => resolveRegisteredToolDefinition(ctx, pi, name),
506
- getMessageRenderer: (customType: string) => ctx?.pi?.getMessageRenderer?.(customType) ?? ctx?.pi?.customMessageRenderers?.get?.(customType) ?? ctx?.customMessageRenderers?.get?.(customType),
507
- showImages: ctx?.showImages,
508
- imageWidthCells: ctx?.imageWidthCells,
509
- },
510
- () => Math.max(12, process.stdout.rows || 42),
511
- (id: string) => manager.getTask(id, cwd),
512
- selectedTaskId,
513
- (id: string) => manager.cancel(id, 'cancelled from subagents detail view'),
514
- config.detail_cancel_shortcut ?? 'x',
515
- );
516
- renderLogger.log({ event: 'panel_created' });
517
- renderLogger.log({ event: 'render_requested', reason: nextRenderReason });
518
- activePanelCancelSelected = () => panel.cancelSelectedActiveTask();
519
- activePanelRequestRender = () => {
520
- nextRenderReason = 'external';
521
- renderLogger.log({ event: 'render_requested', reason: nextRenderReason });
522
- tui.requestRender?.();
523
- };
524
- refresh = setInterval(() => {
525
- nextRenderReason = 'interval';
526
- renderLogger.log({ event: 'render_requested', reason: nextRenderReason });
527
- tui.requestRender?.();
528
- }, 1000);
529
- return {
530
- render: (width: number) => {
531
- const reason = nextRenderReason;
532
- const currentRenderCycle = ++renderCycle;
533
- renderLogger.log({
534
- event: 'render_started',
535
- reason,
536
- renderCycle: currentRenderCycle,
537
- dimensions: {
538
- stdoutColumns: process.stdout.columns,
539
- stdoutRows: process.stdout.rows,
540
- renderWidth: width,
541
- },
542
- });
543
- const startedAt = process.hrtime.bigint();
544
- const lines = panel.render(width);
545
- const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
546
- renderLogger.log({
547
- event: 'render_completed',
548
- reason,
549
- renderCycle: currentRenderCycle,
550
- durationMs,
551
- dimensions: {
552
- stdoutColumns: process.stdout.columns,
553
- stdoutRows: process.stdout.rows,
554
- renderWidth: width,
555
- },
556
- state: panel.getRenderDebugState(),
557
- });
558
- nextRenderReason = 'external';
559
- return lines;
560
- },
561
- invalidate: () => panel.invalidate(),
562
- handleInput: (data: string) => {
563
- renderLogger.log({ event: 'input_received', input: classifySubagentsPanelInput(data, baseMatchesKey) });
564
- nextRenderReason = 'input';
565
- renderLogger.log({ event: 'render_requested', reason: nextRenderReason });
566
- panel.handleInput(data);
567
- tui.requestRender?.();
568
- },
569
- };
570
- },
571
- undefined,
572
- );
573
- } finally {
574
- activePanelCancelSelected = undefined;
575
- activePanelRequestRender = undefined;
576
- widgetInputSuspended = false;
577
- }
578
- }
579
-
580
- const historyPanelShortcut = readSubagentsConfig(process.cwd()).history_panel_shortcut ?? 'ctrl+,';
581
- pi.registerShortcut?.(historyPanelShortcut, {
582
- description: 'Show subagent history panel',
583
- handler: async (ctx: any) => {
584
- const cwd = ctx?.cwd ?? process.cwd();
585
- if (readSubagentsConfig(cwd).mode === 'claude') return;
586
- await showSubagentsPanel(ctx);
587
- },
588
- });
589
-
590
- const detailCancelShortcut = readSubagentsConfig(process.cwd()).detail_cancel_shortcut ?? 'x';
591
- if (detailCancelShortcut.startsWith('ctrl+')) {
592
- pi.registerShortcut?.(detailCancelShortcut, {
593
- description: 'Cancel selected running subagent from the active subagents detail panel',
594
- handler: async () => {
595
- activePanelCancelSelected?.();
596
- activePanelRequestRender?.();
597
- },
598
- });
599
- }
600
-
601
- const backgroundHandoffShortcut = readSubagentsConfig(process.cwd()).background_handoff_shortcut ?? 'ctrl+h';
602
- pi.registerShortcut?.(backgroundHandoffShortcut, {
603
- description: 'Send running claude subagent task to background',
604
- handler: async (ctx: any) => {
605
- const cwd = ctx?.cwd ?? process.cwd();
606
- if (readSubagentsConfig(cwd).mode !== 'claude') return;
607
- triggerClaudeBackgroundHandoff();
608
- },
609
- });
610
-
611
- pi.registerCommand?.('subagents', {
612
- description: 'Show subagent history panel',
613
- handler: async (_args: string, ctx: any) => showSubagentsPanel({ ...ctx, pi }),
614
- });
615
-
616
- pi.registerCommand?.('subagent-models', {
617
- description: 'Configure subagent and SDD phase model profiles',
618
- handler: async (_args: string, ctx: any) => runSubagentModelsCommand({ ...ctx, pi }),
619
- });
620
-
621
- }
1
+ export { createSubagentsPanelKeyMatcher } from './src/ui/panel-input.js';
2
+ export { resolveRegisteredToolDefinition } from './src/ui/panel-overlay.js';
3
+ export { ClaudeBackgroundWidget, ClaudeBackgroundWidgetState, moveClaudeBackgroundWidgetSelection, renderClaudeBackgroundWidgetLines } from './src/ui/background-widget.js';
4
+ export { completionMessage, renderSubagentCompletionMessage, sendSubagentCompletionMessage } from './src/render/completion-message.js';
5
+ export { default } from './src/extension/subagents-extension.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents-j0k3r",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Installable Pi package that adds markdown-defined subagents, delegated task tools, history, and model profiles.",
5
5
  "type": "module",
6
6
  "license": "MIT",