@howarewoo/pi-subagents 0.1.1 → 0.2.1

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.
@@ -1,3 +1,4 @@
1
+ import { requireSupportedHost } from "./supported-host.ts";
1
2
  import { createProfileViewer, resolvedProfiles, profileToolPolicy } from "./profile-viewer.ts";
2
3
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
3
4
  import { EFFORTS, sameSelection } from "./model-policy.ts";
@@ -5,7 +6,7 @@ import { clippedText } from "./outcomes.ts";
5
6
  import { readTaskInput } from "./task-input.ts";
6
7
  import { resolveWorkspace } from "./workspace.ts";
7
8
  import { join } from "node:path";
8
- import { Type } from "typebox";
9
+ import { parameters, coordinationParameters, compatibleParameters } from "./tool-schemas.ts";
9
10
  import {
10
11
  truncateHead,
11
12
  type ExtensionToolContext,
@@ -21,14 +22,11 @@ import {
21
22
  type ChildSnapshot,
22
23
  type StartTask,
23
24
  } from "./coordinator.ts";
24
- import {
25
- READ_TOOLS,
26
- resolveProfile,
27
- bundledProfiles,
28
- COORDINATION_ACTIONS,
29
- intersectProfileGrants,
30
- } from "./profiles.ts";
31
- import { createChildWidget, createHub, safe, taskCard } from "./hub.ts";
25
+ import { READ_TOOLS, resolveProfile, bundledProfiles, intersectProfileGrants } from "./profiles.ts";
26
+ import { createHub } from "./hub.ts";
27
+ import { createChildWidget } from "./subagent-widget.ts";
28
+ import { taskCard } from "./task-presentation.ts";
29
+ import { safe } from "./thread.ts";
32
30
 
33
31
  export default function delegation(pi: ExtensionAPI) {
34
32
  let coordinator: DelegationCoordinator;
@@ -157,64 +155,13 @@ export default function delegation(pi: ExtensionAPI) {
157
155
  throw new Error(`Delegation requires an unmodified built-in parent tool: ${name}`);
158
156
  }
159
157
  };
160
- const role = Type.Union([
161
- Type.Literal("general"),
162
- Type.Literal("scout"),
163
- Type.Literal("review"),
164
- Type.Literal("implementation"),
165
- ]);
166
- const limits = Type.Object(
167
- {
168
- depth: Type.Optional(Type.Integer({ minimum: 0 })),
169
- spawns: Type.Optional(Type.Integer({ minimum: 0 })),
170
- deadlineMs: Type.Optional(Type.Integer({ minimum: 1 })),
171
- requests: Type.Optional(Type.Integer({ minimum: 1 })),
172
- tools: Type.Optional(Type.Integer({ minimum: 1 })),
173
- },
174
- { additionalProperties: false },
175
- );
176
- const modelSelection = Type.Object(
177
- {
178
- provider: Type.String({ minLength: 1 }),
179
- id: Type.String({ minLength: 1 }),
180
- effort: Type.Enum(EFFORTS),
181
- },
182
- { additionalProperties: false },
183
- );
184
- const assignment = {
185
- cwd: Type.Optional(
186
- Type.String({
187
- minLength: 1,
188
- description:
189
- "Existing workspace; absolute or relative to root parent cwd. No new permissions or sandbox.",
190
- }),
191
- ),
192
- models: Type.Optional(Type.Array(modelSelection, { maxItems: 64 })),
193
- model: Type.Optional(modelSelection),
194
- assignment: Type.Optional(Type.String({ minLength: 1 })),
195
- taskFile: Type.Optional(Type.String({ minLength: 1 })),
196
- resultSchema: Type.Optional(Type.Unknown()),
197
- profile: role,
198
- spawn: Type.Optional(Type.Array(role)),
199
- coordination: Type.Optional(
200
- Type.Array(Type.Union(COORDINATION_ACTIONS.map((action) => Type.Literal(action)))),
201
- ),
202
- limits: Type.Optional(limits),
203
- };
204
- const item = Type.Object(assignment, { additionalProperties: false });
205
- const parameters = Type.Union([
206
- Type.Object(
207
- { ...assignment, background: Type.Optional(Type.Boolean()) },
208
- { additionalProperties: false },
209
- ),
210
- Type.Object(
211
- {
212
- tasks: Type.Array(item, { minItems: 1, maxItems: 64 }),
213
- background: Type.Optional(Type.Boolean()),
214
- },
215
- { additionalProperties: false },
216
- ),
217
- ]);
158
+ const codemodeEnabled = () =>
159
+ pi.getActiveTools().includes("codemode") &&
160
+ pi
161
+ .getAllTools()
162
+ .some(
163
+ (tool) => tool.name === "codemode" && tool.exposure === "model-only" && !!tool.reachesTools,
164
+ );
218
165
  const taskTool = (
219
166
  parent?: ChildRef,
220
167
  rootContext?: ExtensionToolContext,
@@ -248,7 +195,7 @@ export default function delegation(pi: ExtensionAPI) {
248
195
  invalidate() {},
249
196
  };
250
197
  },
251
- renderResult(result, options, theme) {
198
+ renderResult(result, options, theme, context) {
252
199
  const stored = result.details as ChildSnapshot | { children: ChildSnapshot[] } | undefined;
253
200
  const children =
254
201
  stored && "children" in stored ? stored.children : stored && "id" in stored ? [stored] : [];
@@ -272,6 +219,8 @@ export default function delegation(pi: ExtensionAPI) {
272
219
  .join("\n"),
273
220
  ),
274
221
  ];
222
+ if (children.length && !options.expanded)
223
+ lines.push(context.keyHint("app.tools.expand", "to expand"));
275
224
  return new Text(lines.map((line) => theme.fg("text", line)).join("\n"), 0, 0).render(
276
225
  width,
277
226
  );
@@ -280,6 +229,7 @@ export default function delegation(pi: ExtensionAPI) {
280
229
  };
281
230
  },
282
231
  async execute(callerId, args, signal, onUpdate, localContext) {
232
+ requireSupportedHost();
283
233
  const ctx = rootContext ?? localContext;
284
234
  const owner = coordinator;
285
235
  if (parent && args.background)
@@ -321,6 +271,7 @@ export default function delegation(pi: ExtensionAPI) {
321
271
  throw new Error("Request cannot mint model grants");
322
272
  profile = { ...profile, models: task.models };
323
273
  }
274
+ signal?.throwIfAborted();
324
275
  let text = task.assignment;
325
276
  if (task.taskFile !== undefined) {
326
277
  assertTools(["read"]);
@@ -328,7 +279,16 @@ export default function delegation(pi: ExtensionAPI) {
328
279
  task.taskFile,
329
280
  ctx,
330
281
  signal,
331
- parent ? () => owner.taskInputRead(parent) : undefined,
282
+ parent
283
+ ? async () => {
284
+ await owner.authorizeSpawn(parent, profile, task.model, {
285
+ context: ctx,
286
+ toolMetadata: () => pi.getAllTools(),
287
+ cwd,
288
+ });
289
+ await owner.taskInputRead(parent, signal);
290
+ }
291
+ : undefined,
332
292
  cwd,
333
293
  );
334
294
  }
@@ -342,6 +302,7 @@ export default function delegation(pi: ExtensionAPI) {
342
302
  context: ctx,
343
303
  assertTools,
344
304
  toolMetadata: () => pi.getAllTools(),
305
+ codemodeEnabled,
345
306
  availableSkills,
346
307
  callerId: rootCallerId ?? callerId,
347
308
  parent,
@@ -351,8 +312,11 @@ export default function delegation(pi: ExtensionAPI) {
351
312
  });
352
313
  }
353
314
  signal?.throwIfAborted();
354
- const children = owner.admit(inputs);
355
- const stop = () => owner.stopMany(children);
315
+ const children = await owner.admit(inputs, undefined, signal);
316
+ const stop = () =>
317
+ parent && ["pausing", "paused"].includes(owner.snapshot(parent)!.status)
318
+ ? Promise.resolve()
319
+ : owner.stopMany(children);
356
320
  const abort = () => {
357
321
  void stop();
358
322
  };
@@ -400,66 +364,25 @@ export default function delegation(pi: ExtensionAPI) {
400
364
  }
401
365
  },
402
366
  });
403
- const identity = Type.Object(
404
- { id: Type.String({ minLength: 1 }), generation: Type.Integer({ minimum: 1 }) },
405
- { additionalProperties: false },
406
- );
407
- const coordinationParameters = Type.Union([
408
- Type.Object(
409
- {
410
- action: Type.Literal("message"),
411
- target: Type.Optional(identity),
412
- text: Type.String({ minLength: 1, maxLength: 4096 }),
413
- mode: Type.Optional(
414
- Type.Union([Type.Literal("inbox"), Type.Literal("steer"), Type.Literal("followUp")]),
415
- ),
416
- },
417
- { additionalProperties: false },
418
- ),
419
- Type.Object({ action: Type.Literal("inbox") }, { additionalProperties: false }),
420
- Type.Object(
421
- {
422
- action: Type.Literal("wait"),
423
- target: Type.Optional(identity),
424
- messageId: Type.Optional(Type.String({ minLength: 1 })),
425
- timeoutMs: Type.Integer({ minimum: 1, maximum: 60000 }),
426
- },
427
- { additionalProperties: false },
428
- ),
429
- Type.Object(
430
- {
431
- action: Type.Union([Type.Literal("park"), Type.Literal("stop"), Type.Literal("status")]),
432
- target: identity,
433
- },
434
- { additionalProperties: false },
435
- ),
436
- Type.Object(
437
- {
438
- action: Type.Union([Type.Literal("followup"), Type.Literal("revive")]),
439
- target: identity,
440
- assignment: Type.String({ minLength: 1 }),
441
- resultSchema: Type.Optional(Type.Unknown()),
442
- },
443
- { additionalProperties: false },
444
- ),
445
- ]);
446
367
  const coordinationTool = (
447
368
  owner?: DelegationCoordinator,
448
369
  actor?: ChildRef,
370
+ wake = false,
449
371
  ): ToolDefinition<typeof coordinationParameters> => ({
450
372
  name: "delegate_coordinate",
451
373
  label: "Coordinate delegation",
452
374
  exposure: "model-only",
453
375
  description:
454
- "Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park and idle followup/parked revive. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
376
+ "Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park, paused resume (assignment is the appended continuation), and idle followup/parked revive. Resume retains identity and full context, resets only the deadline window, and requires explicit child resumes after parent resume. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
455
377
  parameters: coordinationParameters,
456
378
  async execute(callerId, args, signal, _update, ctx) {
379
+ if (["resume", "followup", "revive"].includes(args.action)) requireSupportedHost();
457
380
  const registry = owner ?? coordinator;
458
381
  signal?.throwIfAborted();
459
382
  let value: unknown;
460
383
  if (args.action === "message")
461
- value = await registry.message(actor, args.target, args.text, args.mode);
462
- else if (args.action === "inbox") value = registry.inbox(actor);
384
+ value = await registry.message(actor, args.target, args.text, args.mode, signal);
385
+ else if (args.action === "inbox") value = await registry.inbox(actor, signal);
463
386
  else if (args.action === "wait")
464
387
  value = await registry.coordinateWait(
465
388
  actor,
@@ -482,7 +405,15 @@ export default function delegation(pi: ExtensionAPI) {
482
405
  generations: registry.generations(args.target.id),
483
406
  agents: registry.agents(),
484
407
  };
485
- else if (args.action === "followup" || args.action === "revive") {
408
+ else if (args.action === "resume") {
409
+ const next = await registry.resume(args.target, args.assignment);
410
+ value = registry.snapshot(next);
411
+ // Each continuation owns exactly one notification for its new window.
412
+ void registry.wait(next).then((state) => {
413
+ if (!closing && coordinator === registry)
414
+ show(wake ? compatible(state) : state, state.transcript, wake);
415
+ });
416
+ } else if (args.action === "followup" || args.action === "revive") {
486
417
  const presence = registry
487
418
  .agents()
488
419
  .find(
@@ -523,21 +454,26 @@ export default function delegation(pi: ExtensionAPI) {
523
454
  : { id: original.parentId, generation: original.parentGeneration };
524
455
  }
525
456
  signal?.throwIfAborted();
526
- const next = registry.renew(args.target, {
527
- cwd: state.cwd,
528
- assignment: args.assignment,
529
- resultSchema: args.resultSchema,
530
- profile,
531
- skills,
532
- context: ctx,
533
- assertTools,
534
- toolMetadata: () => pi.getAllTools(),
535
- availableSkills,
536
- callerId,
537
- parent,
538
- coordinationTools: (ref) => [coordinationTool(registry, ref)],
539
- nestedTool: (ref) => taskTool(ref, ctx, callerId),
540
- });
457
+ const next = await registry.renew(
458
+ args.target,
459
+ {
460
+ cwd: state.cwd,
461
+ assignment: args.assignment,
462
+ resultSchema: args.resultSchema,
463
+ profile,
464
+ skills,
465
+ context: ctx,
466
+ assertTools,
467
+ toolMetadata: () => pi.getAllTools(),
468
+ codemodeEnabled,
469
+ availableSkills,
470
+ callerId,
471
+ parent,
472
+ coordinationTools: (ref) => [coordinationTool(registry, ref)],
473
+ nestedTool: (ref) => taskTool(ref, ctx, callerId),
474
+ },
475
+ signal,
476
+ );
541
477
  value = registry.snapshot(next);
542
478
  }
543
479
  }
@@ -548,56 +484,6 @@ export default function delegation(pi: ExtensionAPI) {
548
484
  const taskDefinition = taskTool();
549
485
  pi.registerTool(taskDefinition);
550
486
  const compatibleTaskDefinition = taskTool(undefined, undefined, undefined, true);
551
- const worker = Type.Union([role, Type.Literal("worker")]);
552
- const compatibleItem = Type.Object(
553
- {
554
- agent: worker,
555
- task: Type.String({ minLength: 1 }),
556
- cwd: Type.Optional(
557
- Type.String({
558
- minLength: 1,
559
- description:
560
- "Existing workspace; absolute or relative to root parent cwd. Omit to inherit the call default.",
561
- }),
562
- ),
563
- model: Type.Optional(Type.Union([modelSelection, Type.String({ minLength: 1 })])),
564
- limits: Type.Optional(limits),
565
- },
566
- { additionalProperties: false },
567
- );
568
- const compatibleParameters = Type.Union([
569
- Type.Object(
570
- { ...compatibleItem.properties, async: Type.Optional(Type.Boolean()) },
571
- { additionalProperties: false },
572
- ),
573
- Type.Object(
574
- {
575
- tasks: Type.Array(compatibleItem, { minItems: 1, maxItems: 64 }),
576
- async: Type.Optional(Type.Boolean()),
577
- cwd: Type.Optional(Type.String({ minLength: 1 })),
578
- },
579
- { additionalProperties: false },
580
- ),
581
- Type.Object(
582
- { action: Type.Union([Type.Literal("list"), Type.Literal("agents")]) },
583
- { additionalProperties: false },
584
- ),
585
- Type.Object(
586
- {
587
- action: Type.Union([Type.Literal("status"), Type.Literal("stop")]),
588
- id: Type.String({ minLength: 1 }),
589
- },
590
- { additionalProperties: false },
591
- ),
592
- Type.Object(
593
- {
594
- action: Type.Union([Type.Literal("steer"), Type.Literal("resume")]),
595
- id: Type.String({ minLength: 1 }),
596
- message: Type.String({ minLength: 1 }),
597
- },
598
- { additionalProperties: false },
599
- ),
600
- ]);
601
487
  pi.registerTool({
602
488
  name: "subagent",
603
489
  label: "Subagent",
@@ -607,12 +493,14 @@ export default function delegation(pi: ExtensionAPI) {
607
493
  promptGuidelines: [
608
494
  "Use subagent for native worker delegation requested by the user or an active workflow; no extension-specific user instruction or enable call is needed. Use agent worker for implementation, scout for research, review for review, or general for restricted inherited capabilities.",
609
495
  "Pass subagent the approved task handoff and existing workspace cwd. Preserve selected model routing; no model override is needed to inherit the parent. Pass accessible skill/reference paths in the task; general inherits available skills and callable builtins, plus operator-reviewed leaf tools only; parent instructions/history and project trust are not inherited.",
610
- "subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume starts a fresh conversation in the same workspace; include needed context.",
496
+ "subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume continues a paused conversation with its full context and cumulative budgets; completed idle/parked work starts a fresh generation.",
611
497
  ],
612
498
  description:
613
499
  "Native delegation for orchestration and coding task owners. Launch {agent,task,cwd?,model?,async?} or an ordered {tasks:[...],async?,cwd?} batch. worker maps explicitly to the implementation profile; unknown/custom agent names reject. Model strings require exact provider/id[:effort] and operator grants. Actions: agents, list, status/stop with exact returned id, steer/resume with id and message. Uses the same registry, parent permissions, limits and global writer lease as delegate_task. Existing workspaces only: file paths and shell cwd are explicitly adapted through root builtin hooks. No automatic worktree creation/commits/apply/deletion, detached survival, script workflows or blanket competing API parity. Do not load another provider of the subagent tool alongside this extension.",
614
500
  parameters: compatibleParameters,
615
501
  async execute(callerId, args, signal, onUpdate, ctx) {
502
+ if (!("action" in args) || args.action === "resume" || args.action === "steer")
503
+ requireSupportedHost();
616
504
  const owner = coordinator;
617
505
  if (!("action" in args)) {
618
506
  const items = "tasks" in args ? args.tasks : [args];
@@ -681,10 +569,15 @@ export default function delegation(pi: ExtensionAPI) {
681
569
  .find(
682
570
  (state) => state.id === target.id && state.generation === target.generation,
683
571
  )?.presence;
684
- const response = await coordinationTool().execute(
572
+ const response = await coordinationTool(undefined, undefined, true).execute(
685
573
  callerId,
686
574
  {
687
- action: presence === "parked" ? "revive" : "followup",
575
+ action:
576
+ owner.snapshot(target)?.status === "paused"
577
+ ? "resume"
578
+ : presence === "parked"
579
+ ? "revive"
580
+ : "followup",
688
581
  target,
689
582
  assignment: args.message,
690
583
  },
@@ -693,9 +586,10 @@ export default function delegation(pi: ExtensionAPI) {
693
586
  ctx,
694
587
  );
695
588
  const next = response.details as ChildSnapshot;
696
- void owner.wait(next).then((state) => {
697
- if (!closing && coordinator === owner) show(compatible(state), state.transcript, true);
698
- });
589
+ if (next.generation !== target.generation)
590
+ void owner.wait(next).then((state) => {
591
+ if (!closing && coordinator === owner) show(compatible(state), state.transcript, true);
592
+ });
699
593
  return result(compatible(next));
700
594
  }
701
595
  return result(compatible(owner.snapshot(target)!));
@@ -1,5 +1,7 @@
1
1
  import type { Skill, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ colorToOklch,
4
+ oklchColor,
3
5
  matchesKey,
4
6
  truncateToWidth,
5
7
  visibleWidth,
@@ -28,7 +30,9 @@ export async function resolvedProfiles(
28
30
  );
29
31
  }
30
32
 
31
- function details(profile: Profile): string[] {
33
+ type DetailLine = { text: string; heading?: boolean };
34
+
35
+ function details(profile: Profile): DetailLine[] {
32
36
  const list = (items: readonly string[] | undefined) =>
33
37
  items?.length ? items.join(", ") : "none";
34
38
  const known = new Set([
@@ -44,30 +48,60 @@ function details(profile: Profile): string[] {
44
48
  "model",
45
49
  "prewalk",
46
50
  ]);
51
+ const heading = (text: string): DetailLine => ({ text, heading: true });
52
+ const value = (text: string): DetailLine => ({ text });
53
+ const structured = (label: string, item: unknown, indent = ""): DetailLine[] => {
54
+ if (Array.isArray(item))
55
+ return [
56
+ value(`${indent}${label}:${item.length ? "" : " none (empty grant)"}`),
57
+ ...item.flatMap((entry, index) =>
58
+ structured(`Selection ${index + 1}`, entry, indent + " "),
59
+ ),
60
+ ];
61
+ if (item && typeof item === "object")
62
+ return [
63
+ value(`${indent}${label}:`),
64
+ ...Object.entries(item).flatMap(([key, entry]) => structured(key, entry, indent + " ")),
65
+ ];
66
+ return [value(`${indent}${label}: ${String(item)}`)];
67
+ };
47
68
  return [
48
- profile.name,
49
- "Description",
50
- profile.description,
51
- "Prompt",
52
- profile.prompt,
53
- `Tools ceiling: ${list(profile.tools)}`,
54
- "Callable tools: unknown in command context; inspect admitted task tools.",
55
- `Skills (exact paths): ${list(profile.skills)}`,
69
+ heading("Description"),
70
+ value(profile.description),
71
+ value(""),
72
+ heading("Prompt"),
73
+ value(profile.prompt),
74
+ value(""),
75
+ heading("Tools and permissions"),
76
+ value(`Tools ceiling: ${list(profile.tools)}`),
77
+ value("Callable tools: unknown in command context; inspect admitted task tools."),
56
78
  ...(profile.name === "general"
57
79
  ? [
58
- "Inheritance: available parent skills and callable builtins; no parent instructions/history.",
80
+ value(
81
+ "Inheritance: available parent skills and callable builtins; no parent instructions/history.",
82
+ ),
59
83
  ]
60
84
  : []),
61
- `Reviewed leaf ceiling: ${list(profile.leafTools)} (writer lease; trusted code, not sandboxed)`,
62
- `Spawn ceiling: ${list(profile.spawn)}`,
63
- `Coordination: ${list(profile.coordination)}`,
64
- `Model default: ${profile.model ? JSON.stringify(profile.model) : "selected parent model"}`,
65
- `Models grant: ${profile.models === undefined ? "no explicit override grant" : JSON.stringify(profile.models)}`,
66
- `Prewalk: ${profile.prewalk ? JSON.stringify(profile.prewalk) : "off"}`,
85
+ value(
86
+ `Reviewed leaf ceiling: ${list(profile.leafTools)} (writer lease; trusted code, not sandboxed)`,
87
+ ),
88
+ value(`Spawn ceiling: ${list(profile.spawn)}`),
89
+ value(`Coordination: ${list(profile.coordination)}`),
90
+ value(""),
91
+ heading("Skills (exact paths)"),
92
+ ...(profile.skills.length ? profile.skills.map(value) : [value("none")]),
93
+ value(""),
94
+ heading("Model policy"),
95
+ ...structured("Model default", profile.model ?? "selected parent model"),
96
+ ...structured("Models grant", profile.models ?? "no explicit override grant"),
97
+ ...structured("Prewalk", profile.prewalk ?? "off"),
67
98
  ...Object.entries(profile)
68
99
  .filter(([key]) => !known.has(key))
69
- .map(([key, value]) => `${key}: ${JSON.stringify(value)}`),
70
- "Admission also applies parent/ancestry, model availability, capacity and writer checks. This viewer grants nothing.",
100
+ .map(([key, item]) => value(`${key}: ${JSON.stringify(item)}`)),
101
+ value(""),
102
+ value(
103
+ "Admission also applies parent/ancestry, model availability, capacity and writer checks. This viewer grants nothing.",
104
+ ),
71
105
  ];
72
106
  }
73
107
 
@@ -93,17 +127,33 @@ export function createProfileViewer(
93
127
  width = Math.max(0, width);
94
128
  const height = Math.max(1, Math.floor(availableRows()));
95
129
  const close = keys.getKeys("tui.select.cancel").join("/") || "Esc";
96
- if (width < 24 || height < 7)
97
- return [`Profiles · ${close}: close`, "Resize for details"]
98
- .slice(0, height)
99
- .map((line) => truncateToWidth(line, width));
130
+ const panelBg = oklchColor(colorToOklch(theme.colors.customMessageBg).l, 0, 0);
131
+ // Match the Hub: foreground helpers may reset both colors inside a painted line.
132
+ const paint = (text: string) => {
133
+ const padded = cell(text, width);
134
+ const restoreBg = theme.style("", { bg: panelBg }).replace("\x1b[49m", "");
135
+ return padded
136
+ .split("\x1b[0m")
137
+ .map((part) => theme.style(part.replaceAll("\x1b[49m", restoreBg), { bg: panelBg }))
138
+ .join("\x1b[0m");
139
+ };
140
+ const fallback = () =>
141
+ [`Profiles · ${safe(close)}: close`, "Resize for details"].slice(0, height).map(paint);
142
+ if (width < 24) return fallback();
100
143
  const inner = width - 4;
101
144
  const wide = width >= 88;
102
145
  const leftWidth = wide ? Math.min(28, Math.floor(inner / 3)) : inner;
103
146
  const rightWidth = wide ? inner - leftWidth - 3 : inner;
104
- const bodyHeight = height - 4;
147
+ const footer = [
148
+ "↑↓ select · j/k scroll · PgUp/Dn · Home/End",
149
+ `${safe(close)}: close · callable tools unknown here`,
150
+ ].flatMap((line) => wrapTextWithAnsi(line, inner));
151
+ // Frames, divider and hints are reserved before assigning any viewport rows.
152
+ const bodyHeight = height - 3 - footer.length;
105
153
  const listHeight = wide ? bodyHeight : Math.min(3, Math.max(1, Math.floor(bodyHeight / 3)));
106
- viewport = wide ? bodyHeight : Math.max(1, bodyHeight - listHeight - 1);
154
+ const detailHeight = wide ? bodyHeight : bodyHeight - listHeight - 1;
155
+ if (detailHeight < 4) return fallback();
156
+ viewport = detailHeight - 3;
107
157
  const start = Math.max(0, selected - listHeight + 1);
108
158
  const roster = profiles
109
159
  .slice(start, start + listHeight)
@@ -113,16 +163,33 @@ export function createProfileViewer(
113
163
  `${start + index === selected ? ">" : " "} ${safe(profile.name).replaceAll("\n", " ")}`,
114
164
  ),
115
165
  );
116
- const lines = (
117
- profiles[selected] ? details(profiles[selected]) : ["No resolved profiles"]
118
- ).flatMap((text) =>
166
+ const profile = profiles[selected];
167
+ const name = profile ? safe(profile.name).replaceAll("\n", " ") : "No selection";
168
+ const content = profile ? details(profile) : [{ text: "No resolved profiles" }];
169
+ // A clipped pinned name still has a complete, scrollable representation.
170
+ if (profile && (visibleWidth(name) > rightWidth || profile.name.includes("\n")))
171
+ content.unshift({ text: profile.name });
172
+ const lines = content.flatMap(({ text, heading }) =>
119
173
  safe(text)
120
174
  .split("\n")
121
- .flatMap((line) => wrapTextWithAnsi(line, rightWidth)),
175
+ .flatMap((line) =>
176
+ wrapTextWithAnsi(line, rightWidth).map((wrapped) =>
177
+ theme.fg(heading ? "accent" : "text", wrapped),
178
+ ),
179
+ ),
122
180
  );
123
181
  maximum = Math.max(0, lines.length - viewport);
124
182
  offset = Math.min(maximum, offset);
125
- const detail = lines.slice(offset, offset + viewport);
183
+ const detail = [
184
+ theme.fg("accent", name),
185
+ theme.fg("muted", "resolved · read-only"),
186
+ ...lines.slice(offset, offset + viewport),
187
+ ...Array(Math.max(0, viewport - (lines.length - offset))).fill(""),
188
+ theme.fg(
189
+ "muted",
190
+ `${profiles.length ? selected + 1 : 0}/${profiles.length} · detail ${offset + 1}–${Math.min(lines.length, offset + viewport)}/${lines.length}`,
191
+ ),
192
+ ];
126
193
  const body = wide
127
194
  ? Array.from(
128
195
  { length: bodyHeight },
@@ -134,20 +201,24 @@ export function createProfileViewer(
134
201
  : [
135
202
  ...roster,
136
203
  ...Array(Math.max(0, listHeight - roster.length)).fill(""),
137
- "─".repeat(inner),
204
+ theme.fg("border", "─".repeat(inner)),
138
205
  ...detail,
139
206
  ];
140
207
  const row = (text: string) =>
141
- theme.style(`│ ${cell(text, inner)} │`, { bg: theme.colors.customMessageBg });
208
+ paint(theme.fg("border", "│") + ` ${cell(text, inner)} ` + theme.fg("border", "│"));
209
+ const frame = (left: string, title: string, right: string) =>
210
+ paint(
211
+ theme.fg("border", left) +
212
+ theme.fg("accent", title) +
213
+ theme.fg("border", "─".repeat(Math.max(0, width - 2 - visibleWidth(title))) + right),
214
+ );
142
215
  return [
143
- row(theme.fg("accent", "Subagent Profiles · resolved · read-only")),
216
+ frame("╭", " Subagent Profiles ", "╮"),
144
217
  ...body.map(row),
145
- row(
146
- `${selected + (profiles.length ? 1 : 0)}/${profiles.length} · detail ${offset + 1}–${Math.min(lines.length, offset + viewport)}/${lines.length}`,
147
- ),
148
- row("↑↓ select · j/k scroll · PgUp/Dn · Home/End"),
149
- row(`${close}: close · callable tools unknown here`),
150
- ].slice(0, height);
218
+ frame("├", "", "┤"),
219
+ ...footer.map((line) => row(theme.fg("muted", line))),
220
+ frame("╰", "", "╯"),
221
+ ];
151
222
  },
152
223
  handleInput(data) {
153
224
  if (keys.matches(data, "tui.select.cancel")) {
package/src/scheduling.ts CHANGED
@@ -20,7 +20,7 @@ export const DEFAULT_POLICY: Readonly<SchedulingPolicy> = {
20
20
  batch: 16,
21
21
  depth: 2,
22
22
  spawns: 8,
23
- deadlineMs: 600000,
23
+ deadlineMs: 1800000,
24
24
  requests: 100,
25
25
  tools: 100,
26
26
  };