takomi 2.1.44 → 2.5.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.
Files changed (50) hide show
  1. package/.pi/README.md +9 -8
  2. package/.pi/agents/architect.md +2 -2
  3. package/.pi/agents/designer.md +2 -2
  4. package/.pi/agents/worker.md +32 -0
  5. package/.pi/extensions/oauth-router/commands.ts +66 -35
  6. package/.pi/extensions/oauth-router/index.ts +51 -7
  7. package/.pi/extensions/oauth-router/report-ui.ts +205 -0
  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 +3 -2
  11. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +50 -117
  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/command-text.ts +5 -4
  20. package/.pi/extensions/takomi-runtime/commands.ts +58 -25
  21. package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
  22. package/.pi/extensions/takomi-runtime/index.ts +385 -124
  23. package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +262 -326
  24. package/.pi/extensions/takomi-runtime/profile.ts +9 -8
  25. package/.pi/extensions/takomi-runtime/routing-policy.ts +97 -66
  26. package/.pi/extensions/takomi-runtime/shared.ts +7 -30
  27. package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
  28. package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
  29. package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
  30. package/.pi/extensions/takomi-subagents/agents.ts +9 -0
  31. package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
  32. package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
  33. package/.pi/extensions/takomi-subagents/index.ts +50 -1
  34. package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
  35. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +62 -47
  36. package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
  37. package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
  38. package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
  39. package/.pi/extensions/takomi-subagents/tool-runner.ts +198 -29
  40. package/.pi/settings.json +39 -36
  41. package/.pi/takomi/model-routing.md +288 -3
  42. package/.pi/takomi-profile.json +54 -50
  43. package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
  44. package/package.json +4 -3
  45. package/src/pi-takomi-core/orchestration.ts +39 -21
  46. package/src/pi-takomi-core/routing.ts +8 -8
  47. package/src/pi-takomi-core/types.ts +35 -5
  48. package/src/pi-takomi-core/workflows.ts +86 -45
  49. package/src/skills-catalog.js +2 -202
  50. package/src/skills-installer.js +4 -1
package/.pi/README.md CHANGED
@@ -33,9 +33,9 @@ Inside Pi, use:
33
33
  - `/takomi design [prompt]` to run UI/UX design against the agreed project direction
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
- - `/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
36
+ - `/takomi mode idle|code|review|orchestrate` to choose the main-agent mode; Genesis, Design, and Build remain separate lifecycle stages
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
@@ -100,16 +100,17 @@ So when working on packaging, agents should distinguish between:
100
100
  - Design stage includes a Gemini-oriented hint for model selection.
101
101
  - Build is treated as a workflow/stage, not as a separate specialist agent.
102
102
  - A fresh orchestration session starts with a Genesis foundation task, then expands Design and Build only when the scope justifies it.
103
- - The main execution roles remain things like `orchestrator`, `coder`, `designer`, `architect`, and `reviewer`.
103
+ - The only public Takomi personas are `architect`, `designer`, `coder`, `worker`, `reviewer`, and `orchestrator`. Overlapping upstream built-ins remain hidden behind the execution engine.
104
104
  - Agent discovery prefers `project/.pi/agents/`, also supports legacy `project/.agents/`, and falls back to Pi's configured user agent directory so new projects can reuse the global Takomi agent pack without hard-coding `~/.pi` assumptions.
105
105
  - Orchestrator sessions run in hybrid mode:
106
106
  - human-readable docs live under `docs/tasks/orchestrator-sessions/<sessionId>/`
107
107
  - machine state lives under `.pi/takomi/orchestrator/<sessionId>.json`
108
- - Task packets can carry `workflow`, `skills`, `preferredModel`, `fallbackModels`, `preferredThinking`, `executionHint`, `conversationId`, and `checklist` metadata.
108
+ - Task packets use canonical persona names and can carry `workflow`, `skills`, confirmed `preferredModel`, explicit `fallbackModels`, `requiredCapabilities`, `preferredThinking`, `executionHint`, `conversationId`, and `checklist` metadata.
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.
@@ -118,9 +119,9 @@ So when working on packaging, agents should distinguish between:
118
119
  - Use Pi's native result expansion, `Alt+T`, or `/takomi subagents expand` to inspect detailed subagent output.
119
120
  - Takomi still tracks active runs internally for status and review continuity, but board synchronization is explicit: run with `takomi_subagent`, then update the board.
120
121
  - `takomi-context-manager` reduces prompt bloat by replacing the always-on skill description dump with a names-only Skill Index plus progressive `skill_manifest`/`skill_load` tools.
121
- - `takomi-context-manager` treats `/takomi routing` as the source of truth for model-routing policy via `.pi/settings.json -> takomi.modelRoutingPolicyFile`.
122
+ - `model-routing.md` is advisory model-facing guidance. Executable provider allowlists and persona defaults live only under `takomi.routing` in global/project settings.
122
123
  - `takomi-context-manager` gates `takomi_subagent` when model-routing context has not been loaded, provides the routing policy, and tells the agent to retry.
123
- - `takomi-context-manager` can correct safe wrong-provider model requests, block or pause on policy violations, and ask the user whether to retry with an approved model or stop.
124
+ - `takomi-context-manager` never performs provider-family substitution. It blocks unapproved exact IDs and changes models only after explicit user selection.
124
125
  - `takomi-context-manager` detects known duplicate global/project Takomi extension paths in `context_report` to help diagnose tool registration conflicts.
125
126
  - `context_report` restores context-manager snapshots and Pi tool-result history after reload/restart and explicitly labels gaps; it is not a replacement for Pi's Alt-C token/session stats.
126
127
  - `context_report` defaults to compact `mode: "summary"`; use `mode: "verbose"` for full diagnostics or `mode: "problems"` for attention-only output. `verbose: true` remains a compatibility alias. `/context-report` exposes `summary`, `verbose`, and `problems` as slash-command argument completions.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: architect
3
3
  description: Gather requirements, design architecture, and create implementation-ready plans.
4
- tools: read,bash,grep,find,ls
4
+ tools: read,bash,write,grep,find,ls
5
5
  ---
6
6
  You are the Takomi Architect.
7
7
 
@@ -46,7 +46,7 @@ Produce implementation-ready structure when relevant:
46
46
 
47
47
  This is technical planning, not the Takomi Design lifecycle stage. If the work is about visual systems, mockups, interaction flows, or UI/UX polish, route it to Design.
48
48
 
49
- Stay at architecture level; do not write product code.
49
+ Stay at architecture level; do not write product code. You may create or replace planning Markdown artifacts when the task requires them.
50
50
 
51
51
  ## Phase 4: Planning
52
52
  Create a practical plan with:
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: designer
3
3
  description: Translate requirements into build-ready UI, UX, visual systems, and interaction direction.
4
- tools: read,bash,grep,find,ls
4
+ tools: read,bash,write,grep,find,ls
5
5
  ---
6
6
  You are the Takomi Design Specialist.
7
7
 
@@ -64,7 +64,7 @@ Report:
64
64
  - pages/components ready for implementation
65
65
  - open design risks or follow-up
66
66
 
67
- Update builder guidance when mockups should be treated as implementation source of truth.
67
+ Update builder guidance when mockups should be treated as implementation source of truth. You may create or replace UI/UX Markdown artifacts when the task requires them.
68
68
 
69
69
  ## Anti-Patterns
70
70
  - do not stay vague or inspirational only
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: worker
3
+ description: Execute write-capable non-coding tasks and produce repository artifacts with controlled scope.
4
+ tools: read,bash,edit,write,grep,find,ls
5
+ ---
6
+ You are the Takomi Worker.
7
+
8
+ Your mode pattern is:
9
+ READ -> UNDERSTAND -> EXECUTE -> VERIFY -> HANDOFF.
10
+
11
+ ## Role Scope
12
+ - documentation and repository artifact creation
13
+ - configuration and content updates
14
+ - mechanical or operational tasks that do not require the Coder persona
15
+ - execution of an approved, well-scoped direction
16
+
17
+ Use Coder for application implementation, debugging, refactoring, and tests. Use Architect for requirements, architecture, and planning decisions. Use Designer for UI/UX work. Worker executes a clear non-coding task; it does not invent product or architecture decisions.
18
+
19
+ ## Working Rules
20
+ - Read the task, relevant files, and project instructions before editing.
21
+ - Make the smallest complete change within scope.
22
+ - Create or modify the requested artifacts directly.
23
+ - Escalate missing product or architecture decisions instead of guessing.
24
+ - Verify outputs with the strongest practical checks.
25
+ - Do not expand scope or perform unrelated cleanup.
26
+
27
+ ## Handoff
28
+ Report:
29
+ - artifacts created or changed
30
+ - validation performed
31
+ - anything left incomplete
32
+ - decisions still needed
@@ -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,12 +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
- ctx.ui.setWidget("oauth-router-report", lines, { placement: "belowEditor" });
279
- 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);
280
294
  }
281
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
+
282
303
  async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
283
304
  const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
284
305
  if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
@@ -385,12 +406,11 @@ function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
385
406
  ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
386
407
  }
387
408
 
388
- function showCommandHint(ctx: ExtensionCommandContext, title: string, lines: string[]) {
389
- emitReport(ctx, [`# ${title}`, "", ...lines].join("\n"));
390
- ctx.ui.notify(title, "info");
409
+ function showCommandHint(ctx: ExtensionCommandContext, title: string, lines: string[], showReport: RouterReportEmitter = emitRouterReport) {
410
+ showReport(ctx, [`# ${title}`, "", ...lines].join("\n"));
391
411
  }
392
412
 
393
- async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
413
+ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext, showReport: RouterReportEmitter = emitRouterReport) {
394
414
  const [command = "help", first, ...rest] = parseArgs(args);
395
415
 
396
416
  switch (command) {
@@ -415,18 +435,17 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
415
435
  runtime.clearAccountHealth(duplicate.id);
416
436
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, duplicate.id);
417
437
  setFooterStatus(ctx, runtime);
418
- 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}`);
419
439
  return;
420
440
  }
421
441
  runtime.addAccount(account);
422
442
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, account.id);
423
443
  setFooterStatus(ctx, runtime);
424
- emitReport(ctx, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.${usageMessage}`);
425
- ctx.ui.notify(`Added ${account.id}`, "info");
444
+ showReport(ctx, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.${usageMessage}`);
426
445
  return;
427
446
  }
428
447
  case "list": {
429
- emitReport(ctx, formatAccountsReport(runtime));
448
+ showReport(ctx, formatAccountsReport(runtime));
430
449
  setFooterStatus(ctx, runtime);
431
450
  return;
432
451
  }
@@ -439,7 +458,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
439
458
  }
440
459
  runtime.removeAccount(account.id);
441
460
  setFooterStatus(ctx, runtime);
442
- emitReport(ctx, `Removed account ${account.id}.`);
461
+ showReport(ctx, `Removed account ${account.id}.`);
443
462
  return;
444
463
  }
445
464
  case "rename": {
@@ -447,7 +466,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
447
466
  const label = await getRequiredLabel(ctx, account.label, rest.join(" "));
448
467
  runtime.renameAccount(account.id, label);
449
468
  setFooterStatus(ctx, runtime);
450
- emitReport(ctx, `Renamed account ${account.id} to ${label}.`);
469
+ showReport(ctx, `Renamed account ${account.id} to ${label}.`);
451
470
  return;
452
471
  }
453
472
  case "relogin": {
@@ -472,7 +491,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
472
491
  runtime.clearAccountHealth(existing.id);
473
492
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, existing.id);
474
493
  setFooterStatus(ctx, runtime);
475
- 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}`);
476
495
  return;
477
496
  }
478
497
  case "refresh": {
@@ -480,12 +499,12 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
480
499
  const refreshed = await runtime.refreshAccount(account.id);
481
500
  const usageMessage = await refreshUsageAfterCredentialChange(runtime, refreshed.id);
482
501
  setFooterStatus(ctx, runtime);
483
- emitReport(ctx, `Refreshed account ${refreshed.id} (${refreshed.label}).${usageMessage}`);
502
+ showReport(ctx, `Refreshed account ${refreshed.id} (${refreshed.label}).${usageMessage}`);
484
503
  return;
485
504
  }
486
505
  case "help":
487
506
  default: {
488
- emitReport(
507
+ showReport(
489
508
  ctx,
490
509
  [
491
510
  "# oauth-router commands",
@@ -506,6 +525,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
506
525
  "- /router-disable <id>",
507
526
  "- /router-policy <round-robin|weighted-round-robin>",
508
527
  "- /router-weight <id> <n>",
528
+ "- /router-clear (dismiss the current report)",
509
529
  ].join("\n"),
510
530
  );
511
531
  return;
@@ -513,16 +533,27 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
513
533
  }
514
534
  }
515
535
 
516
- 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
+
517
540
  pi.registerCommand("router-login", {
518
541
  description: "Manage oauth-router accounts",
519
- 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
+ },
520
551
  });
521
552
 
522
553
  pi.registerCommand("router-status", {
523
554
  description: "Show oauth-router health and routing state",
524
555
  handler: async (_args, ctx) => {
525
- emitReport(ctx, formatStatusReport(runtime));
556
+ showReport(ctx, formatStatusReport(runtime));
526
557
  setFooterStatus(ctx, runtime);
527
558
  },
528
559
  });
@@ -530,7 +561,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
530
561
  pi.registerCommand("router-accounts", {
531
562
  description: "Show compact oauth-router account list",
532
563
  handler: async (_args, ctx) => {
533
- emitReport(ctx, formatAccountsReport(runtime));
564
+ showReport(ctx, formatAccountsReport(runtime));
534
565
  setFooterStatus(ctx, runtime);
535
566
  },
536
567
  });
@@ -539,7 +570,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
539
570
  description: "Show oauth-router visual provider quota and local usage",
540
571
  handler: async (args, ctx) => {
541
572
  const [id] = parseArgs(args || "");
542
- emitReport(ctx, formatUsageReport(runtime, id));
573
+ showReport(ctx, formatUsageReport(runtime, id));
543
574
  setFooterStatus(ctx, runtime);
544
575
  },
545
576
  });
@@ -548,7 +579,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
548
579
  description: "Show raw oauth-router usage/provider quota details",
549
580
  handler: async (args, ctx) => {
550
581
  const [id] = parseArgs(args || "");
551
- emitReport(ctx, formatUsageRawReport(runtime, id));
582
+ showReport(ctx, formatUsageRawReport(runtime, id));
552
583
  setFooterStatus(ctx, runtime);
553
584
  },
554
585
  });
@@ -557,7 +588,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
557
588
  description: "Alias for visual oauth-router usage/quota",
558
589
  handler: async (args, ctx) => {
559
590
  const [id] = parseArgs(args || "");
560
- emitReport(ctx, formatUsageReport(runtime, id));
591
+ showReport(ctx, formatUsageReport(runtime, id));
561
592
  setFooterStatus(ctx, runtime);
562
593
  },
563
594
  });
@@ -580,7 +611,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
580
611
  }
581
612
 
582
613
  const report = formatUsageReport(runtime, target === "all" ? undefined : target);
583
- emitReport(ctx, failures.length ? [report, "", "## Refresh failures", ...failures].join("\n") : report);
614
+ showReport(ctx, failures.length ? [report, "", "## Refresh failures", ...failures].join("\n") : report);
584
615
  if (failures.length) ctx.ui.notify(`oauth-router usage refreshed with ${failures.length} failure(s)`, "error");
585
616
  setFooterStatus(ctx, runtime);
586
617
  },
@@ -594,7 +625,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
594
625
  const label = await getRequiredLabel(ctx, account.label, labelParts.join(" "));
595
626
  runtime.renameAccount(account.id, label);
596
627
  setFooterStatus(ctx, runtime);
597
- emitReport(ctx, `Renamed account ${account.id} to ${label}.`);
628
+ showReport(ctx, `Renamed account ${account.id} to ${label}.`);
598
629
  },
599
630
  });
600
631
 
@@ -609,13 +640,13 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
609
640
  }
610
641
  runtime.removeAccount(account.id);
611
642
  setFooterStatus(ctx, runtime);
612
- emitReport(ctx, `Deleted account ${account.id}.`);
643
+ showReport(ctx, `Deleted account ${account.id}.`);
613
644
  },
614
645
  });
615
646
 
616
647
  pi.registerCommand("router-relogin", {
617
648
  description: "Re-login and recover an oauth-router account",
618
- handler: async (args, ctx) => handleRouterLogin(runtime, `relogin ${args || ""}`, ctx),
649
+ handler: async (args, ctx) => handleRouterLogin(runtime, `relogin ${args || ""}`, ctx, showReport),
619
650
  });
620
651
 
621
652
  pi.registerCommand("router-enable", {
@@ -625,7 +656,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
625
656
  const account = await pickAccount(runtime, ctx, id, "Choose account to enable");
626
657
  runtime.setEnabled(account.id, true);
627
658
  setFooterStatus(ctx, runtime);
628
- emitReport(ctx, `Enabled account ${account.id}.`);
659
+ showReport(ctx, `Enabled account ${account.id}.`);
629
660
  },
630
661
  });
631
662
 
@@ -636,7 +667,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
636
667
  const account = await pickAccount(runtime, ctx, id, "Choose account to disable");
637
668
  runtime.setEnabled(account.id, false);
638
669
  setFooterStatus(ctx, runtime);
639
- emitReport(ctx, `Disabled account ${account.id}.`);
670
+ showReport(ctx, `Disabled account ${account.id}.`);
640
671
  },
641
672
  });
642
673
 
@@ -651,7 +682,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
651
682
  "",
652
683
  "Usage: /router-policy <round-robin|weighted-round-robin>",
653
684
  "Aliases: rr, wrr, weighted",
654
- ]);
685
+ ], showReport);
655
686
  return;
656
687
  }
657
688
  if (!policy) {
@@ -660,12 +691,12 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
660
691
  "",
661
692
  `Current policy: ${runtime.getPolicy()}`,
662
693
  "Valid values: round-robin, weighted-round-robin",
663
- ]);
694
+ ], showReport);
664
695
  return;
665
696
  }
666
697
  runtime.setPolicy(policy);
667
698
  setFooterStatus(ctx, runtime);
668
- emitReport(ctx, `Routing policy set to ${policy}.`);
699
+ showReport(ctx, `Routing policy set to ${policy}.`);
669
700
  },
670
701
  });
671
702
 
@@ -680,12 +711,12 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
680
711
  weight = Number(response);
681
712
  }
682
713
  if (!Number.isFinite(weight)) {
683
- 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);
684
715
  return;
685
716
  }
686
717
  runtime.setWeight(account.id, weight);
687
718
  setFooterStatus(ctx, runtime);
688
- 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))}.`);
689
720
  },
690
721
  });
691
722
  }
@@ -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,23 +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", report.split(/\r?\n/), { placement: "belowEditor" });
124
- ctx.ui.notify("oauth-router debug report updated", "info");
168
+ reportControls.show(ctx, formatStatusReport(runtime));
125
169
  },
126
170
  });
127
171
  }