takomi 2.1.43 → 2.1.45

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/.pi/README.md +4 -3
  2. package/.pi/extensions/oauth-router/README.md +3 -3
  3. package/.pi/extensions/oauth-router/commands.ts +66 -39
  4. package/.pi/extensions/oauth-router/config.ts +34 -34
  5. package/.pi/extensions/oauth-router/index.ts +51 -10
  6. package/.pi/extensions/oauth-router/report-ui.ts +205 -0
  7. package/.pi/extensions/oauth-router/scripts/vibe-verify.py +5 -5
  8. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
  9. package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
  10. package/.pi/extensions/takomi-context-manager/index.ts +20 -9
  11. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +36 -15
  12. package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
  13. package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
  14. package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
  15. package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
  16. package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
  17. package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
  18. package/.pi/extensions/takomi-context-manager/types.ts +6 -0
  19. package/.pi/extensions/takomi-runtime/commands.ts +45 -12
  20. package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
  21. package/.pi/extensions/takomi-runtime/index.ts +133 -56
  22. package/.pi/extensions/takomi-runtime/routing-policy.ts +14 -2
  23. package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
  24. package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
  25. package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
  26. package/.pi/extensions/takomi-subagents/agents.ts +4 -0
  27. package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
  28. package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
  29. package/.pi/extensions/takomi-subagents/index.ts +46 -1
  30. package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
  31. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +55 -46
  32. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
  33. package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
  34. package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
  35. package/.pi/extensions/takomi-subagents/tool-runner.ts +158 -28
  36. package/.pi/takomi/model-routing.md +282 -3
  37. package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
  38. package/package.json +4 -3
  39. package/src/pi-harness.js +41 -1
  40. package/src/pi-takomi-core/types.ts +10 -0
  41. package/src/pi-takomi-core/workflows.ts +86 -45
  42. package/src/skills-catalog.js +2 -202
  43. package/src/skills-installer.js +4 -1
  44. package/src/takomi-stats.js +5 -5
package/.pi/README.md CHANGED
@@ -34,8 +34,8 @@ Inside Pi, use:
34
34
  - `/takomi build [prompt]` to implement while cross-checking against approved UI/design artifacts
35
35
  - `/takomi plan [title]` to create a Genesis-first orchestration session that can expand through Design and Build
36
36
  - `/takomi mode direct`, `/takomi mode orchestrate`, or `/takomi mode review` to choose the session operating mode
37
- - `/takomi gate auto` to continue approved plans automatically
38
- - `/takomi gate review` to return to the user after each task with results and verification guidance
37
+ - `/takomi gate auto` to continue approved plans automatically and explicitly authorize project-local agents for this session
38
+ - `/takomi gate review` (or `/takomi gate manual`) to return to the user after each task with results and verification guidance and revoke that authorization
39
39
  - `/takomi subagents on` or `/takomi subagents off` to allow or disable delegated subagents
40
40
  - `/takomi subagents list` to list available Takomi agents without exposing the raw `subagent` tool
41
41
  - `/takomi subagents status` to inspect active subagent state
@@ -109,7 +109,8 @@ So when working on packaging, agents should distinguish between:
109
109
  - Direct `takomi_subagent` calls build a `TakomiDelegationPlan` before launch. In auto mode the plan launches immediately; in manual mode the plan is returned for review until `confirmLaunch=true` is supplied.
110
110
  - The subagent tool supports `conversationId`, so reviewed work can be sent back to the same agent for continuation instead of restarting from scratch.
111
111
  - The subagent tool supports Pi-style single, parallel `tasks`, sequential `chain`, explicit `context=fork|fresh`, `async=true`, and native read/control actions such as `action=list`, `action=status`, `action=doctor`, `action=interrupt`, and `action=resume`.
112
- - The subagent tool supports `agentScope` values of `user`, `project`, and `both`; project-local agents require confirmation by default.
112
+ - The subagent tool supports `agentScope` values of `user`, `project`, and `both`; project-local agents require confirmation by default. The only session authorization bypasses are `TAKOMI_TRUST_PROJECT_AGENTS` and the explicit user `/takomi gate auto` provenance marker; model mode changes plus profile, default, or generic runtime `auto` state do not authorize project agents.
113
+ - The explicit user gate-auto marker is stored in session history so it survives resuming that same session. `/takomi gate review`, `/takomi gate manual`, review mode, and `/takomi-reset` append a revocation; it is not a cross-session or profile setting.
113
114
  - The subagent tool also supports per-run `workflow`, `skills`, `model`, `fallbackModels`, `thinking`, `checklist`, `concurrency`, and `worktree` overrides.
114
115
  - `takomi_board` records session/state/markdown artifacts only; it does not run subagents.
115
116
  - Pi's default `subagent` tool remains owned by the user-level/default subagent extension to avoid tool-name conflicts; Takomi uses `takomi_subagent` as the preferred lifecycle-aware execution interface and renders it with the native Pi-style result surface.
@@ -41,12 +41,12 @@ The extension ships with two default upstream profiles:
41
41
  - auth mode: OAuth
42
42
  - oauth provider: `openai-codex`
43
43
  - api: `openai-codex-responses`
44
- - default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
44
+ - default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
45
45
 
46
46
  2. `openai-compatible`
47
47
  - auth mode: API key fallback
48
48
  - api: `openai-responses`
49
- - default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
49
+ - default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
50
50
 
51
51
  Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. While this is happening, Pi's footer shows the active retry/failover/error state so the UI no longer looks frozen. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
52
52
 
@@ -62,7 +62,7 @@ Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoint
62
62
  - `/router-status`
63
63
  - `/router-usage`
64
64
  5. Select a model:
65
- - `oauth-router/gpt-5.6-sol`
65
+ - `oauth-router/gpt-5.6-sol`
66
66
 
67
67
  ## Account storage format
68
68
 
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { createAccountFromUpstream } from "./oauth-flow.ts";
3
+ import { createRouterReportLines, createRouterReportWidget, ROUTER_REPORT_DISMISS_HINT, ROUTER_REPORT_WIDGET_KEY } from "./report-ui.ts";
3
4
  import type { RouterStatusRow, RouterUsageSummary, RouterUsageWindowSummary, RoutingPolicyName, StoredRouterAccount } from "./types.ts";
4
5
  import { RouterRuntime } from "./provider.ts";
5
6
 
@@ -273,16 +274,32 @@ export function formatUsageRawReport(runtime: RouterRuntime, accountId?: string)
273
274
  return ["# oauth-router usage raw", "", ...rows].join("\n");
274
275
  }
275
276
 
276
- function emitReport(ctx: ExtensionCommandContext, text: string) {
277
- const lines = text.split(/\r?\n/);
278
- const visibleLines = [
279
- "oauth-router report (UI-only; not sent to the agent)",
280
- ...lines,
281
- ];
282
- ctx.ui.setWidget("oauth-router-report", visibleLines, { placement: "belowEditor" });
283
- ctx.ui.notify(lines[0] || "oauth-router report updated", "info");
277
+ export function emitRouterReport(ctx: ExtensionCommandContext, text: string) {
278
+ // A single key gives every report predictable replacement semantics. Add the
279
+ // dismiss affordance at this presentation boundary, not to formatter output,
280
+ // so model/source report text remains unchanged. Omitting options uses Pi's
281
+ // default above-editor placement for both TUI and RPC widgets.
282
+ const reportText = `${ROUTER_REPORT_DISMISS_HINT}\n\n${text}`;
283
+ if (ctx.mode === "tui") {
284
+ ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, createRouterReportWidget(reportText));
285
+ return;
286
+ }
287
+ // RPC accepts string-array widgets only; ctx.hasUI is true there, so ctx.mode
288
+ // is the capability signal that prevents its visible report from being ignored.
289
+ ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, createRouterReportLines(reportText));
290
+ }
291
+
292
+ export function clearRouterReport(ctx: ExtensionCommandContext) {
293
+ ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, undefined);
284
294
  }
285
295
 
296
+ export type RouterReportEmitter = (ctx: ExtensionCommandContext, text: string) => void;
297
+
298
+ export type RouterReportControls = {
299
+ show: RouterReportEmitter;
300
+ clear: (ctx: ExtensionCommandContext) => void;
301
+ };
302
+
286
303
  async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
287
304
  const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
288
305
  if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
@@ -389,12 +406,11 @@ function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
389
406
  ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
390
407
  }
391
408
 
392
- function showCommandHint(ctx: ExtensionCommandContext, title: string, lines: string[]) {
393
- emitReport(ctx, [`# ${title}`, "", ...lines].join("\n"));
394
- ctx.ui.notify(title, "info");
409
+ function showCommandHint(ctx: ExtensionCommandContext, title: string, lines: string[], showReport: RouterReportEmitter = emitRouterReport) {
410
+ showReport(ctx, [`# ${title}`, "", ...lines].join("\n"));
395
411
  }
396
412
 
397
- async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
413
+ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext, showReport: RouterReportEmitter = emitRouterReport) {
398
414
  const [command = "help", first, ...rest] = parseArgs(args);
399
415
 
400
416
  switch (command) {
@@ -419,18 +435,17 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
419
435
  runtime.clearAccountHealth(duplicate.id);
420
436
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, duplicate.id);
421
437
  setFooterStatus(ctx, runtime);
422
- emitReport(ctx, `Updated existing account ${duplicate.id} (${duplicate.label}) with fresh credentials instead of adding a duplicate.${usageMessage}`);
438
+ showReport(ctx, `Updated existing account ${duplicate.id} (${duplicate.label}) with fresh credentials instead of adding a duplicate.${usageMessage}`);
423
439
  return;
424
440
  }
425
441
  runtime.addAccount(account);
426
442
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, account.id);
427
443
  setFooterStatus(ctx, runtime);
428
- emitReport(ctx, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.${usageMessage}`);
429
- ctx.ui.notify(`Added ${account.id}`, "info");
444
+ showReport(ctx, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.${usageMessage}`);
430
445
  return;
431
446
  }
432
447
  case "list": {
433
- emitReport(ctx, formatAccountsReport(runtime));
448
+ showReport(ctx, formatAccountsReport(runtime));
434
449
  setFooterStatus(ctx, runtime);
435
450
  return;
436
451
  }
@@ -443,7 +458,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
443
458
  }
444
459
  runtime.removeAccount(account.id);
445
460
  setFooterStatus(ctx, runtime);
446
- emitReport(ctx, `Removed account ${account.id}.`);
461
+ showReport(ctx, `Removed account ${account.id}.`);
447
462
  return;
448
463
  }
449
464
  case "rename": {
@@ -451,7 +466,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
451
466
  const label = await getRequiredLabel(ctx, account.label, rest.join(" "));
452
467
  runtime.renameAccount(account.id, label);
453
468
  setFooterStatus(ctx, runtime);
454
- emitReport(ctx, `Renamed account ${account.id} to ${label}.`);
469
+ showReport(ctx, `Renamed account ${account.id} to ${label}.`);
455
470
  return;
456
471
  }
457
472
  case "relogin": {
@@ -476,7 +491,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
476
491
  runtime.clearAccountHealth(existing.id);
477
492
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, existing.id);
478
493
  setFooterStatus(ctx, runtime);
479
- emitReport(ctx, `Re-logged account ${existing.id} (${existing.label}) and cleared auth state.${usageMessage}`);
494
+ showReport(ctx, `Re-logged account ${existing.id} (${existing.label}) and cleared auth state.${usageMessage}`);
480
495
  return;
481
496
  }
482
497
  case "refresh": {
@@ -484,12 +499,12 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
484
499
  const refreshed = await runtime.refreshAccount(account.id);
485
500
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, refreshed.id);
486
501
  setFooterStatus(ctx, runtime);
487
- emitReport(ctx, `Refreshed account ${refreshed.id} (${refreshed.label}).${usageMessage}`);
502
+ showReport(ctx, `Refreshed account ${refreshed.id} (${refreshed.label}).${usageMessage}`);
488
503
  return;
489
504
  }
490
505
  case "help":
491
506
  default: {
492
- emitReport(
507
+ showReport(
493
508
  ctx,
494
509
  [
495
510
  "# oauth-router commands",
@@ -510,6 +525,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
510
525
  "- /router-disable <id>",
511
526
  "- /router-policy <round-robin|weighted-round-robin>",
512
527
  "- /router-weight <id> <n>",
528
+ "- /router-clear (dismiss the current report)",
513
529
  ].join("\n"),
514
530
  );
515
531
  return;
@@ -517,16 +533,27 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
517
533
  }
518
534
  }
519
535
 
520
- export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime) {
536
+ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime, reportControls?: RouterReportControls) {
537
+ const showReport = reportControls?.show ?? emitRouterReport;
538
+ const clearReport = reportControls?.clear ?? clearRouterReport;
539
+
521
540
  pi.registerCommand("router-login", {
522
541
  description: "Manage oauth-router accounts",
523
- handler: async (args, ctx) => handleRouterLogin(runtime, args || "", ctx),
542
+ handler: async (args, ctx) => handleRouterLogin(runtime, args || "", ctx, showReport),
543
+ });
544
+
545
+ pi.registerCommand("router-clear", {
546
+ description: "Dismiss the current oauth-router report widget",
547
+ handler: async (_args, ctx) => {
548
+ clearReport(ctx);
549
+ setFooterStatus(ctx, runtime);
550
+ },
524
551
  });
525
552
 
526
553
  pi.registerCommand("router-status", {
527
554
  description: "Show oauth-router health and routing state",
528
555
  handler: async (_args, ctx) => {
529
- emitReport(ctx, formatStatusReport(runtime));
556
+ showReport(ctx, formatStatusReport(runtime));
530
557
  setFooterStatus(ctx, runtime);
531
558
  },
532
559
  });
@@ -534,7 +561,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
534
561
  pi.registerCommand("router-accounts", {
535
562
  description: "Show compact oauth-router account list",
536
563
  handler: async (_args, ctx) => {
537
- emitReport(ctx, formatAccountsReport(runtime));
564
+ showReport(ctx, formatAccountsReport(runtime));
538
565
  setFooterStatus(ctx, runtime);
539
566
  },
540
567
  });
@@ -543,7 +570,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
543
570
  description: "Show oauth-router visual provider quota and local usage",
544
571
  handler: async (args, ctx) => {
545
572
  const [id] = parseArgs(args || "");
546
- emitReport(ctx, formatUsageReport(runtime, id));
573
+ showReport(ctx, formatUsageReport(runtime, id));
547
574
  setFooterStatus(ctx, runtime);
548
575
  },
549
576
  });
@@ -552,7 +579,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
552
579
  description: "Show raw oauth-router usage/provider quota details",
553
580
  handler: async (args, ctx) => {
554
581
  const [id] = parseArgs(args || "");
555
- emitReport(ctx, formatUsageRawReport(runtime, id));
582
+ showReport(ctx, formatUsageRawReport(runtime, id));
556
583
  setFooterStatus(ctx, runtime);
557
584
  },
558
585
  });
@@ -561,7 +588,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
561
588
  description: "Alias for visual oauth-router usage/quota",
562
589
  handler: async (args, ctx) => {
563
590
  const [id] = parseArgs(args || "");
564
- emitReport(ctx, formatUsageReport(runtime, id));
591
+ showReport(ctx, formatUsageReport(runtime, id));
565
592
  setFooterStatus(ctx, runtime);
566
593
  },
567
594
  });
@@ -584,7 +611,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
584
611
  }
585
612
 
586
613
  const report = formatUsageReport(runtime, target === "all" ? undefined : target);
587
- emitReport(ctx, failures.length ? [report, "", "## Refresh failures", ...failures].join("\n") : report);
614
+ showReport(ctx, failures.length ? [report, "", "## Refresh failures", ...failures].join("\n") : report);
588
615
  if (failures.length) ctx.ui.notify(`oauth-router usage refreshed with ${failures.length} failure(s)`, "error");
589
616
  setFooterStatus(ctx, runtime);
590
617
  },
@@ -598,7 +625,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
598
625
  const label = await getRequiredLabel(ctx, account.label, labelParts.join(" "));
599
626
  runtime.renameAccount(account.id, label);
600
627
  setFooterStatus(ctx, runtime);
601
- emitReport(ctx, `Renamed account ${account.id} to ${label}.`);
628
+ showReport(ctx, `Renamed account ${account.id} to ${label}.`);
602
629
  },
603
630
  });
604
631
 
@@ -613,13 +640,13 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
613
640
  }
614
641
  runtime.removeAccount(account.id);
615
642
  setFooterStatus(ctx, runtime);
616
- emitReport(ctx, `Deleted account ${account.id}.`);
643
+ showReport(ctx, `Deleted account ${account.id}.`);
617
644
  },
618
645
  });
619
646
 
620
647
  pi.registerCommand("router-relogin", {
621
648
  description: "Re-login and recover an oauth-router account",
622
- handler: async (args, ctx) => handleRouterLogin(runtime, `relogin ${args || ""}`, ctx),
649
+ handler: async (args, ctx) => handleRouterLogin(runtime, `relogin ${args || ""}`, ctx, showReport),
623
650
  });
624
651
 
625
652
  pi.registerCommand("router-enable", {
@@ -629,7 +656,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
629
656
  const account = await pickAccount(runtime, ctx, id, "Choose account to enable");
630
657
  runtime.setEnabled(account.id, true);
631
658
  setFooterStatus(ctx, runtime);
632
- emitReport(ctx, `Enabled account ${account.id}.`);
659
+ showReport(ctx, `Enabled account ${account.id}.`);
633
660
  },
634
661
  });
635
662
 
@@ -640,7 +667,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
640
667
  const account = await pickAccount(runtime, ctx, id, "Choose account to disable");
641
668
  runtime.setEnabled(account.id, false);
642
669
  setFooterStatus(ctx, runtime);
643
- emitReport(ctx, `Disabled account ${account.id}.`);
670
+ showReport(ctx, `Disabled account ${account.id}.`);
644
671
  },
645
672
  });
646
673
 
@@ -655,7 +682,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
655
682
  "",
656
683
  "Usage: /router-policy <round-robin|weighted-round-robin>",
657
684
  "Aliases: rr, wrr, weighted",
658
- ]);
685
+ ], showReport);
659
686
  return;
660
687
  }
661
688
  if (!policy) {
@@ -664,12 +691,12 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
664
691
  "",
665
692
  `Current policy: ${runtime.getPolicy()}`,
666
693
  "Valid values: round-robin, weighted-round-robin",
667
- ]);
694
+ ], showReport);
668
695
  return;
669
696
  }
670
697
  runtime.setPolicy(policy);
671
698
  setFooterStatus(ctx, runtime);
672
- emitReport(ctx, `Routing policy set to ${policy}.`);
699
+ showReport(ctx, `Routing policy set to ${policy}.`);
673
700
  },
674
701
  });
675
702
 
@@ -684,12 +711,12 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
684
711
  weight = Number(response);
685
712
  }
686
713
  if (!Number.isFinite(weight)) {
687
- showCommandHint(ctx, "router-weight", ["Usage: /router-weight <id> <n>", "Example: /router-weight acct_ab12cd34 3"]);
714
+ showCommandHint(ctx, "router-weight", ["Usage: /router-weight <id> <n>", "Example: /router-weight acct_ab12cd34 3"], showReport);
688
715
  return;
689
716
  }
690
717
  runtime.setWeight(account.id, weight);
691
718
  setFooterStatus(ctx, runtime);
692
- emitReport(ctx, `Updated weight for ${account.id} to ${Math.max(1, Math.floor(weight))}.`);
719
+ showReport(ctx, `Updated weight for ${account.id} to ${Math.max(1, Math.floor(weight))}.`);
693
720
  },
694
721
  });
695
722
  }
@@ -11,7 +11,7 @@ export const STATE_PATH = join(DATA_ROOT, "state.json");
11
11
 
12
12
  const LEGACY_CODEX_CONTEXT_WINDOW = 272000;
13
13
  const SAFE_CODEX_CONTEXT_WINDOW = 240000;
14
- const CODEX_MODEL_IDS = new Set(["gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
14
+ const CODEX_MODEL_IDS = new Set(["gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
15
15
 
16
16
  const DEFAULT_MODELS: RouterModelConfig[] = [
17
17
  {
@@ -32,42 +32,42 @@ const DEFAULT_MODELS: RouterModelConfig[] = [
32
32
  contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
33
33
  maxTokens: 128000,
34
34
  },
35
- {
36
- id: "gpt-5.5",
35
+ {
36
+ id: "gpt-5.5",
37
37
  name: "GPT-5.5",
38
38
  reasoning: true,
39
39
  input: ["text", "image"],
40
40
  cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
41
41
  contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
42
- maxTokens: 128000,
43
- },
44
- {
45
- id: "gpt-5.6-luna",
46
- name: "GPT-5.6 Luna",
47
- reasoning: true,
48
- input: ["text", "image"],
49
- cost: { input: 1.00, output: 6.00, cacheRead: 0.10, cacheWrite: 1.25 },
50
- contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
51
- maxTokens: 128000,
52
- },
53
- {
54
- id: "gpt-5.6-sol",
55
- name: "GPT-5.6 Sol",
56
- reasoning: true,
57
- input: ["text", "image"],
58
- cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
59
- contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
60
- maxTokens: 128000,
61
- },
62
- {
63
- id: "gpt-5.6-terra",
64
- name: "GPT-5.6 Terra",
65
- reasoning: true,
66
- input: ["text", "image"],
67
- cost: { input: 2.50, output: 15.00, cacheRead: 0.25, cacheWrite: 3.125 },
68
- contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
69
- maxTokens: 128000,
70
- },
42
+ maxTokens: 128000,
43
+ },
44
+ {
45
+ id: "gpt-5.6-luna",
46
+ name: "GPT-5.6 Luna",
47
+ reasoning: true,
48
+ input: ["text", "image"],
49
+ cost: { input: 1.00, output: 6.00, cacheRead: 0.10, cacheWrite: 1.25 },
50
+ contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
51
+ maxTokens: 128000,
52
+ },
53
+ {
54
+ id: "gpt-5.6-sol",
55
+ name: "GPT-5.6 Sol",
56
+ reasoning: true,
57
+ input: ["text", "image"],
58
+ cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
59
+ contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
60
+ maxTokens: 128000,
61
+ },
62
+ {
63
+ id: "gpt-5.6-terra",
64
+ name: "GPT-5.6 Terra",
65
+ reasoning: true,
66
+ input: ["text", "image"],
67
+ cost: { input: 2.50, output: 15.00, cacheRead: 0.25, cacheWrite: 3.125 },
68
+ contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
69
+ maxTokens: 128000,
70
+ },
71
71
  ];
72
72
 
73
73
  const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
@@ -79,7 +79,7 @@ const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
79
79
  api: "openai-responses",
80
80
  authMode: "api-key",
81
81
  enabled: true,
82
- modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
82
+ modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
83
83
  },
84
84
  {
85
85
  id: "chatgpt-codex",
@@ -90,7 +90,7 @@ const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
90
90
  authMode: "oauth",
91
91
  oauthProviderId: "openai-codex",
92
92
  enabled: true,
93
- modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
93
+ modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
94
94
  usageProbe: {
95
95
  enabled: true,
96
96
  timeoutMs: 8_000,
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { registerRouterCommands, formatStatusReport } from "./commands.ts";
2
+ import { emitRouterReport, formatStatusReport, registerRouterCommands, type RouterReportControls } from "./commands.ts";
3
+ import { ROUTER_REPORT_WIDGET_KEY } from "./report-ui.ts";
3
4
  import { registerRouterProvider, RouterRuntime } from "./provider.ts";
4
5
  import type { RouterUiEvent } from "./types.ts";
5
6
 
@@ -64,8 +65,33 @@ function notifyActivity(ctx: ExtensionContext, event: RouterUiEvent) {
64
65
  ctx.ui.notify(message, level);
65
66
  }
66
67
 
67
- function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyOnLoad = false) {
68
+ function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyOnLoad = false): RouterReportControls {
68
69
  let activeCtx: ExtensionContext | undefined;
70
+ let reportGeneration = 0;
71
+ let activeReport: { generation: number; ctx: ExtensionContext } | undefined;
72
+
73
+ const clearReport = (fallbackCtx?: ExtensionContext, clearFallback = false) => {
74
+ const report = activeReport;
75
+ const ctx = report?.ctx ?? (clearFallback ? fallbackCtx : undefined);
76
+ if (!ctx) return;
77
+
78
+ try {
79
+ ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, undefined);
80
+ } catch {
81
+ // Widget lifecycle is best-effort and must not interrupt agent activity.
82
+ } finally {
83
+ // A re-entrant command can replace the report while the old widget clears.
84
+ // Its newer generation remains active instead of being erased by this clear.
85
+ if (report && activeReport?.generation === report.generation) activeReport = undefined;
86
+ }
87
+ };
88
+
89
+ const showReport: RouterReportControls["show"] = (ctx, text) => {
90
+ const report = { generation: ++reportGeneration, ctx };
91
+ activeCtx = ctx;
92
+ activeReport = report;
93
+ emitRouterReport(ctx, text);
94
+ };
69
95
 
70
96
  const setBaseStatus = (ctx: ExtensionContext) => {
71
97
  activeCtx = ctx;
@@ -84,14 +110,29 @@ function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyO
84
110
  });
85
111
 
86
112
  pi.on("session_start", async (_event, ctx) => {
113
+ // A session replacement owns a fresh transport, so clear the old transport
114
+ // before accepting reports for the new one.
115
+ clearReport(activeCtx, true);
87
116
  setBaseStatus(ctx);
88
117
  if (notifyOnLoad) ctx.ui.notify("oauth-router loaded", "info");
89
118
  });
90
119
 
120
+ pi.on("input", async (event, ctx) => {
121
+ // Registered slash commands run before Pi emits input and return without an
122
+ // input event. Ordinary text/image input reaches this handler before its
123
+ // agent starts, which is the intended transient-dismissal boundary.
124
+ if (event.text.trim() || event.images?.length) clearReport(ctx);
125
+ });
126
+
91
127
  pi.on("agent_start", async (_event, ctx) => {
128
+ clearReport(ctx);
92
129
  setBaseStatus(ctx);
93
130
  });
94
131
 
132
+ pi.on("tool_execution_start", async (_event, ctx) => {
133
+ clearReport(ctx);
134
+ });
135
+
95
136
  pi.on("turn_start", async (_event, ctx) => {
96
137
  setBaseStatus(ctx);
97
138
  });
@@ -105,26 +146,26 @@ function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyO
105
146
  });
106
147
 
107
148
  pi.on("session_shutdown", async () => {
149
+ // The old context owns the RPC transport. Clear its report before releasing
150
+ // it so clients do not retain a widget across a session replacement.
151
+ clearReport(activeCtx, true);
108
152
  activeCtx = undefined;
109
153
  });
154
+
155
+ return { show: showReport, clear: (ctx) => clearReport(ctx, true) };
110
156
  }
111
157
 
112
158
  export default function (pi: ExtensionAPI) {
113
159
  const runtime = new RouterRuntime();
114
160
 
115
161
  registerRouterProvider(pi, runtime);
116
- registerRouterCommands(pi, runtime);
117
- installRouterUiBridge(pi, runtime, false);
162
+ const reportControls = installRouterUiBridge(pi, runtime, false);
163
+ registerRouterCommands(pi, runtime, reportControls);
118
164
 
119
165
  pi.registerCommand("router-debug-report", {
120
166
  description: "Show a detailed oauth-router report in the UI only",
121
167
  handler: async (_args, ctx) => {
122
- const report = formatStatusReport(runtime);
123
- ctx.ui.setWidget("oauth-router-report", [
124
- "oauth-router debug report (UI-only; not sent to the agent)",
125
- ...report.split(/\r?\n/),
126
- ], { placement: "belowEditor" });
127
- ctx.ui.notify("oauth-router debug report updated", "info");
168
+ reportControls.show(ctx, formatStatusReport(runtime));
128
169
  },
129
170
  });
130
171
  }