pi-ca-leash 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/AGENTS.md +77 -0
  2. package/ARCHITECTURE.md +290 -0
  3. package/CHANGELOG.md +158 -0
  4. package/DEVELOPMENT.md +197 -0
  5. package/KNOWN_LIMITS.md +80 -0
  6. package/LICENSE +21 -0
  7. package/README.md +288 -0
  8. package/extensions/backend-tool-actions.test.ts +59 -0
  9. package/extensions/backend-tool-actions.ts +31 -0
  10. package/extensions/command-drivers.test.ts +37 -0
  11. package/extensions/command-drivers.ts +126 -0
  12. package/extensions/command-parity.test.ts +560 -0
  13. package/extensions/command-visibility.test.ts +21 -0
  14. package/extensions/command-visibility.ts +10 -0
  15. package/extensions/index.ts +3218 -0
  16. package/extensions/llm-tools.test.ts +537 -0
  17. package/extensions/model-catalog.test.ts +34 -0
  18. package/extensions/model-catalog.ts +173 -0
  19. package/extensions/peer-history.test.ts +141 -0
  20. package/extensions/peer-history.ts +90 -0
  21. package/extensions/peer-naming.test.ts +25 -0
  22. package/extensions/peer-naming.ts +129 -0
  23. package/extensions/peer-relay.test.ts +122 -0
  24. package/extensions/peer-relay.ts +83 -0
  25. package/extensions/peer-ux.test.ts +239 -0
  26. package/extensions/peer-ux.ts +327 -0
  27. package/extensions/persistence.test.ts +68 -0
  28. package/extensions/persistence.ts +67 -0
  29. package/extensions/prompts/extension-log-tool.md +5 -0
  30. package/extensions/prompts/peer-ask-tool.md +5 -0
  31. package/extensions/prompts/peer-bridge-system.md +4 -0
  32. package/extensions/prompts/peer-history-tool.md +3 -0
  33. package/extensions/prompts/peer-init-user-help.md +11 -0
  34. package/extensions/prompts/peer-init.md +17 -0
  35. package/extensions/prompts/peer-interrupt-tool.md +2 -0
  36. package/extensions/prompts/peer-list-tool.md +3 -0
  37. package/extensions/prompts/peer-no-babysitting.md +6 -0
  38. package/extensions/prompts/peer-send-tool.md +5 -0
  39. package/extensions/prompts/peer-start-tool.md +7 -0
  40. package/extensions/prompts/peer-stop-tool.md +3 -0
  41. package/extensions/prompts/runtime-models-tool.md +6 -0
  42. package/extensions/prompts/subagent-list-tool.md +3 -0
  43. package/extensions/prompts/subagent-run-tool.md +6 -0
  44. package/extensions/prompts/subagent-status-tool.md +2 -0
  45. package/extensions/prompts/team-list-tool.md +2 -0
  46. package/extensions/prompts/team-message-tool.md +2 -0
  47. package/extensions/prompts/team-spawn-tool.md +5 -0
  48. package/extensions/prompts/team-stop-tool.md +2 -0
  49. package/extensions/prompts/team-task-tool.md +3 -0
  50. package/extensions/prompts.ts +41 -0
  51. package/extensions/runtime-driver.test.ts +38 -0
  52. package/extensions/runtime-driver.ts +33 -0
  53. package/extensions/runtime-safety.test.ts +21 -0
  54. package/extensions/runtime-safety.ts +49 -0
  55. package/extensions/support.test.ts +144 -0
  56. package/extensions/support.ts +205 -0
  57. package/extensions/tool-inputs.test.ts +45 -0
  58. package/extensions/tool-inputs.ts +79 -0
  59. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.d.ts +48 -0
  60. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.js +406 -0
  61. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.js.map +1 -0
  62. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.d.ts +2 -0
  63. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.js +18 -0
  64. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.js.map +1 -0
  65. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.d.ts +5 -0
  66. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.js +5 -0
  67. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.js.map +1 -0
  68. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.d.ts +12 -0
  69. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.js +31 -0
  70. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.js.map +1 -0
  71. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.d.ts +12 -0
  72. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.js +347 -0
  73. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.js.map +1 -0
  74. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.d.ts +103 -0
  75. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.js +2 -0
  76. package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.js.map +1 -0
  77. package/node_modules/@pi-claude-code-agent/intercom-bridge/package.json +32 -0
  78. package/node_modules/@pi-claude-code-agent/runtime/dist/cli.d.ts +2 -0
  79. package/node_modules/@pi-claude-code-agent/runtime/dist/cli.js +26 -0
  80. package/node_modules/@pi-claude-code-agent/runtime/dist/cli.js.map +1 -0
  81. package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.d.ts +4 -0
  82. package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.js +12 -0
  83. package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.js.map +1 -0
  84. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.d.ts +8 -0
  85. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.js +320 -0
  86. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.js.map +1 -0
  87. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.d.ts +24 -0
  88. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.js +266 -0
  89. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.js.map +1 -0
  90. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.d.ts +72 -0
  91. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.js +2 -0
  92. package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.js.map +1 -0
  93. package/node_modules/@pi-claude-code-agent/runtime/dist/index.d.ts +6 -0
  94. package/node_modules/@pi-claude-code-agent/runtime/dist/index.js +5 -0
  95. package/node_modules/@pi-claude-code-agent/runtime/dist/index.js.map +1 -0
  96. package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.d.ts +16 -0
  97. package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.js +94 -0
  98. package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.js.map +1 -0
  99. package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.d.ts +31 -0
  100. package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.js +409 -0
  101. package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.js.map +1 -0
  102. package/node_modules/@pi-claude-code-agent/runtime/dist/types.d.ts +185 -0
  103. package/node_modules/@pi-claude-code-agent/runtime/dist/types.js +2 -0
  104. package/node_modules/@pi-claude-code-agent/runtime/dist/types.js.map +1 -0
  105. package/node_modules/@pi-claude-code-agent/runtime/package.json +32 -0
  106. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.d.ts +34 -0
  107. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.js +327 -0
  108. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.js.map +1 -0
  109. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.d.ts +2 -0
  110. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.js +17 -0
  111. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.js.map +1 -0
  112. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.d.ts +4 -0
  113. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.js +4 -0
  114. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.js.map +1 -0
  115. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.d.ts +12 -0
  116. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.js +81 -0
  117. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.js.map +1 -0
  118. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.d.ts +72 -0
  119. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.js +2 -0
  120. package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.js.map +1 -0
  121. package/node_modules/@pi-claude-code-agent/subagents-backend/package.json +32 -0
  122. package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.d.ts +27 -0
  123. package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.js +194 -0
  124. package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.js.map +1 -0
  125. package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.d.ts +2 -0
  126. package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.js +21 -0
  127. package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.js.map +1 -0
  128. package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.d.ts +4 -0
  129. package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.js +4 -0
  130. package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.js.map +1 -0
  131. package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.d.ts +8 -0
  132. package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.js +66 -0
  133. package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.js.map +1 -0
  134. package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.d.ts +41 -0
  135. package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.js +2 -0
  136. package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.js.map +1 -0
  137. package/node_modules/@pi-claude-code-agent/teams-backend/package.json +33 -0
  138. package/package.json +98 -0
@@ -0,0 +1,3218 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { ClaudeRuntimeIntercomBridge, extractReplyText, type BridgePeer, PiIntercomTransport } from "@pi-claude-code-agent/intercom-bridge";
5
+ import { ClaudeCodeRuntime, type InterruptResult, type RuntimeEvent, type RuntimeMessageBlock } from "@pi-claude-code-agent/runtime";
6
+ import { ClaudeCodeSubagentBackend } from "@pi-claude-code-agent/subagents-backend";
7
+ import { ClaudeCodeTeamsBackend } from "@pi-claude-code-agent/teams-backend";
8
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
9
+ import { Box, Text, truncateToWidth, visibleWidth, type AutocompleteItem } from "@mariozechner/pi-tui";
10
+ import { readAttentionLedger, serializeAttentionLedger, writeAttentionLedger } from "./persistence.js";
11
+ import { ADVANCED_COMMANDS_ENV, LEGACY_COMMANDS_ENV, advancedCommandsEnabled, legacyCommandsEnabled } from "./command-visibility.js";
12
+ import { derivePeerName } from "./peer-naming.js";
13
+ import {
14
+ createPeerRelaySnapshot,
15
+ formatPeerCompletionTurn,
16
+ formatQuotedTextBlock,
17
+ shouldForceRelayPeerCompletion,
18
+ shouldRelayPeerCompletion,
19
+ type PeerRelaySnapshot,
20
+ } from "./peer-relay.js";
21
+ import { formatPeerHistoryPage } from "./peer-history.js";
22
+ import { buildPeerActivityRow, getPeerFirstHealth, isPeerVisibleInWidget, type PeerActivityRow } from "./peer-ux.js";
23
+ import { parseRuntimeDriverName, resolveExtensionRuntimeDriverConfig } from "./runtime-driver.js";
24
+ import { modelCatalogsForDriver, resolveRuntimeModelSelection, type RuntimeDriverModelCatalog } from "./model-catalog.js";
25
+ import { describePromptSize } from "./runtime-safety.js";
26
+ import {
27
+ PEER_ASK_TOOL_PROMPT,
28
+ PEER_BRIDGE_APPEND_SYSTEM_PROMPT,
29
+ EXTENSION_LOG_TOOL_PROMPT,
30
+ PEER_HISTORY_TOOL_PROMPT,
31
+ PEER_INIT_GUIDE,
32
+ PEER_INIT_USER_HELP,
33
+ PEER_INTERRUPT_TOOL_PROMPT,
34
+ PEER_LIST_TOOL_PROMPT,
35
+ PEER_NO_BABYSITTING_GUIDANCE,
36
+ PEER_SEND_TOOL_PROMPT,
37
+ PEER_START_TOOL_PROMPT,
38
+ PEER_STOP_TOOL_PROMPT,
39
+ RUNTIME_MODELS_TOOL_PROMPT,
40
+ SUBAGENT_LIST_TOOL_PROMPT,
41
+ SUBAGENT_RUN_TOOL_PROMPT,
42
+ SUBAGENT_STATUS_TOOL_PROMPT,
43
+ TEAM_LIST_TOOL_PROMPT,
44
+ TEAM_MESSAGE_TOOL_PROMPT,
45
+ TEAM_SPAWN_TOOL_PROMPT,
46
+ TEAM_STOP_TOOL_PROMPT,
47
+ TEAM_TASK_TOOL_PROMPT,
48
+ } from "./prompts.js";
49
+ import { parseSubagentRunToolInput, parseTeamSpawnToolInput } from "./tool-inputs.js";
50
+ import { parsePeerStartCommandInput, parseSubagentRunCommandInput, parseTeamSpawnCommandInput } from "./command-drivers.js";
51
+ import { buildSubagentRunRequest, buildTeamSpawnRequest } from "./backend-tool-actions.js";
52
+ import {
53
+ acknowledgeAttention,
54
+ createAttentionLedger,
55
+ createDashboardState,
56
+ describeAttentionState,
57
+ detectConnectivityTransition,
58
+ hasAttentionNote,
59
+ listAttentionViews,
60
+ recordDashboardEvent,
61
+ recordDashboardRefresh,
62
+ reconcileAttentionLedger,
63
+ computeWidgetSignature,
64
+ shouldRebindTransport,
65
+ shouldSkipBackgroundRefresh,
66
+ snoozeAttention,
67
+ type AttentionLedger,
68
+ type AttentionView,
69
+ type DashboardState,
70
+ } from "./support.js";
71
+
72
+ const EXTENSION_NAME = "pi-ca-leash";
73
+ const extensionRequire = createRequire(import.meta.url);
74
+ const EXTENSION_PACKAGE_PATH = extensionRequire.resolve("../package.json");
75
+ const EXTENSION_PACKAGE_ROOT = dirname(EXTENSION_PACKAGE_PATH);
76
+ const EXTENSION_VERSION = String((extensionRequire("../package.json") as { version?: string }).version ?? "0.0.0");
77
+ const STATE_DIR_NAME = ".pi-ca-leash";
78
+ const BACKGROUND_POLL_INTERVAL_MS = 5_000;
79
+ const BACKGROUND_REFRESH_MIN_INTERVAL_MS = 3_000;
80
+ const DEFAULT_SNOOZE_MINUTES = 15;
81
+
82
+ interface DashboardSnapshot {
83
+ sessions: number;
84
+ peers: number;
85
+ peerBusy: number;
86
+ peerIssues: number;
87
+ peerRows: PeerActivityRow[];
88
+ intercomLive: boolean;
89
+ intercomBoundPeers: number;
90
+ intercomConnectedPeers: number;
91
+ transportDegraded: boolean;
92
+ runs: number;
93
+ runActive: number;
94
+ runAttention: number;
95
+ runIssues: number;
96
+ teammates: number;
97
+ teammateBusy: number;
98
+ teammateIssues: number;
99
+ tasks: number;
100
+ openTasks: number;
101
+ taskIssues: number;
102
+ lastEvent: string;
103
+ lastEventAt: number;
104
+ lastRefreshedAt: number;
105
+ }
106
+
107
+ interface DashboardData {
108
+ snapshot: DashboardSnapshot;
109
+ sessions: Awaited<ReturnType<ClaudeCodeRuntime["list"]>>;
110
+ peers: Awaited<ReturnType<ClaudeRuntimeIntercomBridge["listPeers"]>>;
111
+ runs: Awaited<ReturnType<ClaudeCodeSubagentBackend["listRuns"]>>;
112
+ teammates: Awaited<ReturnType<ClaudeCodeTeamsBackend["listTeammates"]>>;
113
+ tasks: Awaited<ReturnType<ClaudeCodeTeamsBackend["listTasks"]>>;
114
+ attention: AttentionView[];
115
+ }
116
+
117
+ type CommandMessageLevel = "info" | "success" | "warning" | "error";
118
+ type CommandMessageSurface = "custom" | "tool" | "agent";
119
+
120
+ interface CommandMessageDetails {
121
+ level: CommandMessageLevel;
122
+ title: string;
123
+ command?: string;
124
+ timestamp: number;
125
+ surface?: CommandMessageSurface;
126
+ }
127
+
128
+ interface ExtensionLogEntryInput {
129
+ category: "ux" | "agent-guidance" | "tooling" | "runtime" | "model-selection" | "docs" | "bug" | "other";
130
+ severity: "low" | "medium" | "high";
131
+ summary: string;
132
+ observed?: string;
133
+ expected?: string;
134
+ reproduction?: string;
135
+ suggestedFix?: string;
136
+ relatedCommand?: string;
137
+ relatedTool?: string;
138
+ files: string[];
139
+ }
140
+
141
+ let dashboardContextRef: ExtensionContext | ExtensionCommandContext | undefined;
142
+ let operatorContextRef: ExtensionContext | ExtensionCommandContext | undefined;
143
+ let lastWidgetSignature: string | undefined;
144
+
145
+ export default async function piCaLeashExtension(pi: ExtensionAPI) {
146
+ const cwd = process.cwd();
147
+ const rootDir = resolve(cwd, STATE_DIR_NAME);
148
+ const attentionLedgerPath = resolve(rootDir, "extension", "attention-ledger.json");
149
+ const runtimeDriverConfig = resolveExtensionRuntimeDriverConfig();
150
+ const runtime = new ClaudeCodeRuntime({
151
+ storageDir: resolve(rootDir, "runtime"),
152
+ defaultDriver: runtimeDriverConfig.defaultDriver,
153
+ });
154
+ const bridge = new ClaudeRuntimeIntercomBridge({
155
+ runtime,
156
+ storageDir: resolve(rootDir, "bridge"),
157
+ });
158
+ const subagents = new ClaudeCodeSubagentBackend({
159
+ runtime,
160
+ storageDir: resolve(rootDir, "subagents"),
161
+ });
162
+ const teams = new ClaudeCodeTeamsBackend({
163
+ storageDir: resolve(rootDir, "teams"),
164
+ bridge,
165
+ });
166
+
167
+ let intercomTransport: PiIntercomTransport | undefined;
168
+ let intercomReachable: boolean | undefined;
169
+ let backgroundMonitorTimer: NodeJS.Timeout | undefined;
170
+ let attentionLedger: AttentionLedger = await readAttentionLedger(attentionLedgerPath);
171
+ let persistedAttentionLedger = serializeAttentionLedger(attentionLedger);
172
+ const peerRelaySnapshots = new Map<string, PeerRelaySnapshot>();
173
+ const suppressNextPeerRelay = new Set<string>();
174
+ let peerModeActive = false;
175
+ let peerGuideShown = false;
176
+ let runtimeDriverFallbackShown = false;
177
+
178
+ const showAdvancedCommands = advancedCommandsEnabled();
179
+ const showLegacyCommands = legacyCommandsEnabled();
180
+ const noSessionMode = process.argv.includes("--no-session");
181
+ const startupSummary = `${EXTENSION_NAME} v${EXTENSION_VERSION} loaded · default driver ${runtimeDriverConfig.defaultDriver}`;
182
+ const dashboardState: DashboardState = createDashboardState(startupSummary);
183
+
184
+ await bridge.restorePeers();
185
+ await teams.listTeammates();
186
+
187
+ async function startPeerWithoutWaiting(input: {
188
+ name: string;
189
+ prompt: string;
190
+ driver?: "claude-sdk" | "codex-cli";
191
+ cwd?: string;
192
+ model?: string;
193
+ permissionMode?: "default" | "acceptEdits" | "plan" | "dontAsk" | "bypassPermissions";
194
+ }): Promise<BridgePeer> {
195
+ if (await bridge.status(input.name)) {
196
+ throw new Error(`Peer name ${input.name} already registered`);
197
+ }
198
+ const status = await runtime.start({
199
+ prompt: input.prompt,
200
+ driver: input.driver,
201
+ cwd: input.cwd,
202
+ model: input.model,
203
+ name: input.name,
204
+ appendSystemPrompt: PEER_BRIDGE_APPEND_SYSTEM_PROMPT,
205
+ permissionMode: input.permissionMode,
206
+ });
207
+ return bridge.attachPeer({ name: input.name, sessionId: status.sessionId });
208
+ }
209
+
210
+ async function bindIntercomTransport(): Promise<void> {
211
+ intercomTransport = new PiIntercomTransport();
212
+ await bridge.setTransport(intercomTransport);
213
+ }
214
+
215
+ async function unbindIntercomTransport(): Promise<void> {
216
+ await bridge.setTransport(undefined);
217
+ intercomTransport = undefined;
218
+ }
219
+
220
+ async function ensureIntercomTransportHealthy(announce: boolean): Promise<boolean> {
221
+ const reachable = await PiIntercomTransport.canConnect().catch(() => false);
222
+ const transition = detectConnectivityTransition(intercomReachable, reachable);
223
+ intercomReachable = reachable;
224
+
225
+ if (!reachable) {
226
+ if (intercomTransport) {
227
+ await unbindIntercomTransport();
228
+ }
229
+ if (announce && transition === "disconnected") {
230
+ recordDashboardEvent(dashboardState, "Intercom broker disconnected");
231
+ sendCommandMessage(pi, {
232
+ level: "warning",
233
+ title: "Intercom disconnected",
234
+ body: "Broker unreachable. Peers stay local until broker connectivity returns.",
235
+ surface: "custom",
236
+ });
237
+ }
238
+ return false;
239
+ }
240
+
241
+ const transportStatus = await bridge.transportStatus();
242
+ const needsRebind = intercomTransport ? shouldRebindTransport(transportStatus) : false;
243
+ if (!intercomTransport || needsRebind) {
244
+ if (intercomTransport) {
245
+ await unbindIntercomTransport();
246
+ }
247
+ await bindIntercomTransport();
248
+ if (announce && (transition === "connected" || needsRebind)) {
249
+ recordDashboardEvent(dashboardState, needsRebind ? "Intercom transport rebound" : "Intercom broker reconnected");
250
+ sendCommandMessage(pi, {
251
+ level: "success",
252
+ title: needsRebind ? "Intercom transport rebound" : "Intercom reconnected",
253
+ body: needsRebind
254
+ ? "Broker reachable again. Existing peers were rebound to live intercom transport."
255
+ : "Broker reachable again. Peers are back on live intercom transport.",
256
+ surface: "custom",
257
+ });
258
+ }
259
+ }
260
+
261
+ return true;
262
+ }
263
+
264
+ async function persistAttentionLedger(): Promise<void> {
265
+ const nextSerialized = serializeAttentionLedger(attentionLedger);
266
+ if (nextSerialized === persistedAttentionLedger) {
267
+ return;
268
+ }
269
+ await writeAttentionLedger(attentionLedgerPath, attentionLedger);
270
+ persistedAttentionLedger = nextSerialized;
271
+ }
272
+
273
+ async function syncAttentionLedger() {
274
+ const runs = await subagents.listRuns();
275
+ const next = reconcileAttentionLedger(attentionLedger, runs, Date.now());
276
+ attentionLedger = next.ledger;
277
+ await persistAttentionLedger();
278
+ return next;
279
+ }
280
+
281
+ async function refreshVisibleDashboard(lastEvent?: string): Promise<void> {
282
+ if (!peerModeActive) {
283
+ return;
284
+ }
285
+ if (!dashboardContextRef) {
286
+ return;
287
+ }
288
+ if (!lastEvent && shouldSkipBackgroundRefresh(dashboardState, Date.now(), BACKGROUND_REFRESH_MIN_INTERVAL_MS)) {
289
+ return;
290
+ }
291
+ await refreshDashboard(dashboardContextRef, runtime, bridge, subagents, teams, dashboardState, attentionLedger, lastEvent);
292
+ }
293
+
294
+ async function buildPeerRelayInput(peer: BridgePeer): Promise<{ snapshot: PeerRelaySnapshot; row: PeerActivityRow; message?: string }> {
295
+ const events = await runtime.tail(peer.sessionId, 20).catch(() => []);
296
+ const row = buildPeerActivityRow(peer, events);
297
+ const lastMessage = truncate(extractLatestVisibleReplyText(events).trim(), 4_000);
298
+ const fallback = row.activity !== "idle" ? row.activity : undefined;
299
+ const messageText = lastMessage || fallback;
300
+ const snapshot = createPeerRelaySnapshot({
301
+ sessionId: peer.sessionId,
302
+ state: row.state,
303
+ updatedAt: row.lastUpdateAt,
304
+ messageText,
305
+ });
306
+ return {
307
+ snapshot,
308
+ row,
309
+ message: messageText,
310
+ };
311
+ }
312
+
313
+ function extractLatestVisibleReplyText(events: RuntimeEvent[]): string {
314
+ const latestAssistantMessage = [...events]
315
+ .reverse()
316
+ .find((event): event is Extract<RuntimeEvent, { type: "message" }> => event.type === "message" && event.role === "assistant"
317
+ && event.message.blocks.some((block) => Boolean(blockToVisibleText(block)?.trim())));
318
+
319
+ if (latestAssistantMessage) {
320
+ return latestAssistantMessage.message.blocks
321
+ .map(blockToVisibleText)
322
+ .filter((value): value is string => Boolean(value && value.trim()))
323
+ .join("\n\n")
324
+ .trim();
325
+ }
326
+
327
+ const result = [...events]
328
+ .reverse()
329
+ .find((event): event is Extract<RuntimeEvent, { type: "result" }> => event.type === "result");
330
+ return result?.summary ?? "";
331
+ }
332
+
333
+ function blockToVisibleText(block: RuntimeMessageBlock): string | undefined {
334
+ if (block.type === "thinking") {
335
+ return undefined;
336
+ }
337
+ return typeof block.text === "string" ? block.text : undefined;
338
+ }
339
+
340
+ async function resolvePeerHistoryTarget(name: string): Promise<{
341
+ name: string;
342
+ sessionId: string;
343
+ state: string;
344
+ model?: string;
345
+ driver?: string;
346
+ cwd: string;
347
+ source: "active" | "runtime";
348
+ } | undefined> {
349
+ const active = await bridge.status(name);
350
+ if (active) {
351
+ return {
352
+ name: active.name,
353
+ sessionId: active.sessionId,
354
+ state: active.state,
355
+ model: active.model,
356
+ driver: active.driver,
357
+ cwd: active.cwd,
358
+ source: "active",
359
+ };
360
+ }
361
+
362
+ const historical = (await runtime.list()).find((session) => session.name === name);
363
+ if (!historical) {
364
+ return undefined;
365
+ }
366
+
367
+ return {
368
+ name,
369
+ sessionId: historical.sessionId,
370
+ state: historical.state,
371
+ model: historical.model,
372
+ driver: historical.driver,
373
+ cwd: historical.cwd,
374
+ source: "runtime",
375
+ };
376
+ }
377
+
378
+ async function seedPeerRelaySnapshots(): Promise<void> {
379
+ const peers = await bridge.listPeers();
380
+ for (const peer of peers) {
381
+ const { snapshot } = await buildPeerRelayInput(peer);
382
+ peerRelaySnapshots.set(peer.name, snapshot);
383
+ }
384
+ }
385
+
386
+ async function syncPeerRelaySnapshot(peer: BridgePeer): Promise<{ row: PeerActivityRow; message?: string }> {
387
+ const { snapshot, row, message } = await buildPeerRelayInput(peer);
388
+ peerRelaySnapshots.set(peer.name, snapshot);
389
+ suppressNextPeerRelay.delete(peer.name);
390
+ return { row, message };
391
+ }
392
+
393
+ async function interruptPeer(name: string): Promise<{ peer: BridgePeer; interrupt: InterruptResult }> {
394
+ const current = await bridge.status(name);
395
+ if (!current) {
396
+ throw new Error(`Unknown peer ${name}`);
397
+ }
398
+ const interrupt = await runtime.interrupt(current.sessionId);
399
+ return {
400
+ peer: await bridge.status(name) ?? current,
401
+ interrupt,
402
+ };
403
+ }
404
+
405
+ async function relayPeerCompletionToMain(peer: BridgePeer, options: { force?: boolean; reply?: string } = {}): Promise<boolean> {
406
+ if (["stopped", "disconnected"].includes(peer.state)) {
407
+ suppressNextPeerRelay.delete(peer.name);
408
+ return false;
409
+ }
410
+ const previous = peerRelaySnapshots.get(peer.name);
411
+ const { snapshot, row, message } = await buildPeerRelayInput(peer);
412
+ const shouldRelay = options.force
413
+ ? shouldForceRelayPeerCompletion(previous, snapshot)
414
+ : shouldRelayPeerCompletion(previous, snapshot);
415
+
416
+ peerRelaySnapshots.set(peer.name, snapshot);
417
+ if (!shouldRelay) {
418
+ return false;
419
+ }
420
+ if (!options.force && suppressNextPeerRelay.delete(peer.name)) {
421
+ return false;
422
+ }
423
+
424
+ const text = truncate((options.reply ?? message ?? "").trim(), 4_000);
425
+ if (!text) {
426
+ return false;
427
+ }
428
+
429
+ pi.sendUserMessage(formatPeerCompletionTurn({
430
+ peerName: peer.name,
431
+ state: row.state,
432
+ sessionId: peer.sessionId,
433
+ message: text,
434
+ }), { deliverAs: "followUp" });
435
+ return true;
436
+ }
437
+
438
+ async function pollBackground(): Promise<void> {
439
+ await ensureIntercomTransportHealthy(true);
440
+ const attention = await syncAttentionLedger();
441
+
442
+ for (const run of attention.notify) {
443
+ recordDashboardEvent(dashboardState, `Attention ${shortId(run.runId)} · ${run.agentName}`);
444
+ sendCommandMessage(pi, {
445
+ level: "warning",
446
+ title: `Run needs attention: ${shortId(run.runId)}`,
447
+ body: [run.agentName, run.note].filter(Boolean).join("\n\n"),
448
+ surface: "custom",
449
+ });
450
+ }
451
+
452
+ const peers = await bridge.listPeers();
453
+ for (const peer of peers) {
454
+ await relayPeerCompletionToMain(peer);
455
+ }
456
+
457
+ await refreshVisibleDashboard();
458
+ }
459
+
460
+ function startBackgroundMonitor(): void {
461
+ if (backgroundMonitorTimer) {
462
+ return;
463
+ }
464
+ backgroundMonitorTimer = setInterval(() => {
465
+ void pollBackground();
466
+ }, BACKGROUND_POLL_INTERVAL_MS);
467
+ backgroundMonitorTimer.unref?.();
468
+ }
469
+
470
+ const peerNameCache = new Set((await bridge.listPeers()).map((peer) => peer.name));
471
+
472
+ async function activatePeerMode(ctx: ExtensionContext | ExtensionCommandContext, options: { command?: string; showGuide?: boolean; forceGuide?: boolean; reason?: string } = {}): Promise<void> {
473
+ if (!peerModeActive) {
474
+ if (!noSessionMode) {
475
+ await ensureIntercomTransportHealthy(false);
476
+ }
477
+ await syncAttentionLedger();
478
+ await seedPeerRelaySnapshots();
479
+ if (!noSessionMode) {
480
+ startBackgroundMonitor();
481
+ }
482
+ peerModeActive = true;
483
+ dashboardContextRef = ctx;
484
+ if (options.reason) {
485
+ recordDashboardEvent(dashboardState, options.reason);
486
+ }
487
+ }
488
+ if (options.showGuide && (options.forceGuide || !peerGuideShown)) {
489
+ peerGuideShown = true;
490
+ sendCommandMessage(pi, {
491
+ level: "info",
492
+ command: options.command,
493
+ title: "Agent orchestration guide",
494
+ body: PEER_INIT_GUIDE,
495
+ surface: "agent",
496
+ });
497
+ showPeerInitUserHelp(ctx);
498
+ }
499
+ if (runtimeDriverConfig.note && !runtimeDriverFallbackShown) {
500
+ runtimeDriverFallbackShown = true;
501
+ sendCommandMessage(pi, {
502
+ level: "warning",
503
+ command: options.command,
504
+ title: "Runtime driver config fallback",
505
+ body: runtimeDriverConfig.note,
506
+ surface: "custom",
507
+ });
508
+ }
509
+ }
510
+
511
+ pi.registerMessageRenderer("peer-command-result", (message, { expanded }, theme) => {
512
+ const details = (message.details ?? {}) as Partial<CommandMessageDetails>;
513
+ const level = details.level ?? "info";
514
+ const surface = details.surface ?? "tool";
515
+ const title = details.title ?? "Peer command result";
516
+ const body = typeof message.content === "string" ? message.content.trim() : String(message.content ?? "").trim();
517
+ const color = levelColor(level);
518
+ const label = surface === "agent" ? "[peer/agent]" : "[peer]";
519
+
520
+ let text = `${theme.fg("toolTitle", label)} ${theme.fg(color, title)}`;
521
+ if (body) {
522
+ text += `\n${theme.fg("toolOutput", body)}`;
523
+ }
524
+
525
+ if (expanded) {
526
+ const meta = [
527
+ details.command ? `/${details.command}` : undefined,
528
+ details.timestamp ? new Date(details.timestamp).toLocaleTimeString() : undefined,
529
+ ].filter(Boolean).join(" ");
530
+ if (meta) {
531
+ text += `\n${theme.fg("dim", meta)}`;
532
+ }
533
+ }
534
+
535
+ const box = new Box(1, 0, (value) => theme.bg(commandMessageBg(surface, level), value));
536
+ box.addChild(new Text(text, 0, 0));
537
+ return box;
538
+ });
539
+
540
+ pi.registerTool({
541
+ name: "runtime_models",
542
+ label: "Runtime Models",
543
+ description: "List recommended runtime models, or the full bundled Lanista-derived model catalog in verbose mode.",
544
+ promptSnippet: RUNTIME_MODELS_TOOL_PROMPT.snippet,
545
+ promptGuidelines: RUNTIME_MODELS_TOOL_PROMPT.guidelines,
546
+ parameters: {
547
+ type: "object",
548
+ properties: {
549
+ driver: { type: "string", enum: ["claude-sdk", "codex-cli"], description: "Optional runtime driver to filter the model catalog." },
550
+ verbose: { type: "boolean", description: "When true, include every model from the bundled catalog instead of the short recommended list." },
551
+ },
552
+ additionalProperties: false,
553
+ } as any,
554
+ async execute(_toolCallId, params) {
555
+ const input = params as { driver?: unknown; verbose?: unknown };
556
+ const driver = input.driver == null ? undefined : parseRuntimeDriverName(input.driver);
557
+ if (input.driver != null && !driver) {
558
+ throw new Error("driver must be claude-sdk or codex-cli");
559
+ }
560
+ const verbose = input.verbose === true;
561
+ const catalogs = modelCatalogsForDriver(driver);
562
+ return {
563
+ content: [{
564
+ type: "text",
565
+ text: formatModelCatalogReport(catalogs, { verbose }),
566
+ }],
567
+ details: { catalogs },
568
+ };
569
+ },
570
+ });
571
+
572
+ pi.registerTool({
573
+ name: "extension_log",
574
+ label: "Extension Log",
575
+ description: "Append structured pi-ca-leash extension feedback to .pi-ca-leash/log.md.",
576
+ promptSnippet: EXTENSION_LOG_TOOL_PROMPT.snippet,
577
+ promptGuidelines: EXTENSION_LOG_TOOL_PROMPT.guidelines,
578
+ parameters: {
579
+ type: "object",
580
+ properties: {
581
+ category: {
582
+ type: "string",
583
+ enum: ["ux", "agent-guidance", "tooling", "runtime", "model-selection", "docs", "bug", "other"],
584
+ description: "Feedback category.",
585
+ },
586
+ severity: {
587
+ type: "string",
588
+ enum: ["low", "medium", "high"],
589
+ description: "Optional severity. Defaults to medium.",
590
+ },
591
+ summary: { type: "string", description: "Concise actionable summary of the problem or rough edge." },
592
+ observed: { type: "string", description: "What happened or felt rough." },
593
+ expected: { type: "string", description: "What should have happened or felt smoother." },
594
+ reproduction: { type: "string", description: "Minimal reproduction or triggering interaction, if known." },
595
+ suggestedFix: { type: "string", description: "Optional implementation or UX suggestion." },
596
+ relatedCommand: { type: "string", description: "Related slash command or interaction surface, if any." },
597
+ relatedTool: { type: "string", description: "Related LLM-callable tool, if any." },
598
+ files: {
599
+ type: "array",
600
+ items: { type: "string" },
601
+ description: "Relevant local files, if any.",
602
+ },
603
+ },
604
+ required: ["summary"],
605
+ additionalProperties: false,
606
+ } as any,
607
+ async execute(_toolCallId, params) {
608
+ const input = parseExtensionLogInput(params);
609
+ const logPath = resolve(rootDir, "log.md");
610
+ await mkdir(dirname(logPath), { recursive: true });
611
+ await appendFile(logPath, formatExtensionLogEntry(input), "utf8");
612
+ return {
613
+ content: [{
614
+ type: "text",
615
+ text: `Logged extension feedback to ${join(STATE_DIR_NAME, "log.md")}.`,
616
+ }],
617
+ details: { path: logPath, entry: input },
618
+ };
619
+ },
620
+ });
621
+
622
+ pi.registerTool({
623
+ name: "peer_start",
624
+ label: "Start Peer",
625
+ description: "Start a long-lived runtime-backed peer from a prompt, optionally with an explicit name, driver, model, and working directory.",
626
+ promptSnippet: PEER_START_TOOL_PROMPT.snippet,
627
+ promptGuidelines: PEER_START_TOOL_PROMPT.guidelines,
628
+ parameters: {
629
+ type: "object",
630
+ properties: {
631
+ prompt: { type: "string", description: "Task or role prompt for the peer." },
632
+ name: { type: "string", description: "Optional explicit peer name." },
633
+ driver: { type: "string", enum: ["claude-sdk", "codex-cli"], description: "Optional runtime driver for this peer. Defaults to the extension startup driver." },
634
+ model: { type: "string", description: "Optional model to use for this peer session." },
635
+ cwd: { type: "string", description: "Optional working directory for the peer. Relative paths resolve from the current pi working directory." },
636
+ },
637
+ required: ["prompt"],
638
+ additionalProperties: false,
639
+ } as any,
640
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
641
+ await activatePeerMode(ctx, { command: "peer_start", reason: "Peer mode activated" });
642
+ const input = params as { prompt?: unknown; name?: unknown; driver?: unknown; model?: unknown; cwd?: unknown };
643
+ const prompt = String(input.prompt ?? "").trim();
644
+ const explicitName = typeof input.name === "string" ? input.name.trim() : "";
645
+ const driver = input.driver == null ? undefined : parseRuntimeDriverName(input.driver);
646
+ const model = typeof input.model === "string" ? input.model.trim() || undefined : undefined;
647
+ const peerCwd = typeof input.cwd === "string" ? input.cwd.trim() || undefined : undefined;
648
+ if (!prompt) {
649
+ throw new Error("prompt required");
650
+ }
651
+ if (input.driver != null && !driver) {
652
+ throw new Error("driver must be claude-sdk or codex-cli");
653
+ }
654
+ const effectiveDriver = driver ?? runtimeDriverConfig.defaultDriver;
655
+ const modelSelection = resolveRuntimeModelSelection(effectiveDriver, model);
656
+ const modelNote = modelSelection.note;
657
+ const promptSizeNote = describePromptSize("peer prompt", prompt);
658
+
659
+ const existingNames = new Set((await bridge.listPeers()).map((peer) => peer.name));
660
+ const name = explicitName || derivePeerName(prompt, existingNames);
661
+ if (!noSessionMode) {
662
+ await ensureIntercomTransportHealthy(false);
663
+ }
664
+ const peer = await startPeerWithoutWaiting({
665
+ name,
666
+ prompt,
667
+ driver,
668
+ cwd: peerCwd ?? cwd,
669
+ model: modelSelection.runtimeModel,
670
+ permissionMode: "bypassPermissions",
671
+ });
672
+ peerNameCache.add(peer.name);
673
+ const { row, message } = await syncPeerRelaySnapshot(peer);
674
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer started: ${peer.name}`);
675
+ sendCommandMessage(pi, {
676
+ level: "success",
677
+ command: "peer_start",
678
+ title: `Peer started: ${peer.name}`,
679
+ body: [
680
+ !explicitName ? `auto-name ${peer.name}` : undefined,
681
+ `state ${row.state}`,
682
+ `driver ${peer.driver ?? "claude-sdk"}`,
683
+ modelNote,
684
+ promptSizeNote,
685
+ `session ${peer.sessionId}`,
686
+ "",
687
+ PEER_NO_BABYSITTING_GUIDANCE,
688
+ ].filter((line) => line !== undefined).join("\n"),
689
+ });
690
+ return {
691
+ content: [{
692
+ type: "text",
693
+ text: [
694
+ `Peer started: ${peer.name}`,
695
+ PEER_NO_BABYSITTING_GUIDANCE,
696
+ !explicitName ? `auto-name ${peer.name}` : undefined,
697
+ `state ${row.state}`,
698
+ `driver ${peer.driver ?? "claude-sdk"}`,
699
+ `model ${peer.model ?? "-"}`,
700
+ modelNote,
701
+ promptSizeNote,
702
+ `cwd ${peer.cwd}`,
703
+ `session ${peer.sessionId}`,
704
+ message ? `latest peer message\n${formatQuotedTextBlock(message)}` : undefined,
705
+ ].filter(Boolean).join("\n\n"),
706
+ }],
707
+ details: { peerName: peer.name, state: row.state, sessionId: peer.sessionId, driver: peer.driver, model: peer.model, requestedModel: model, modelNote, promptSizeNote, cwd: peer.cwd, guidance: PEER_NO_BABYSITTING_GUIDANCE },
708
+ };
709
+ },
710
+ });
711
+
712
+ pi.registerTool({
713
+ name: "peer_list",
714
+ label: "List Peers",
715
+ description: "List known runtime-backed peers with their current state and latest activity summary.",
716
+ promptSnippet: PEER_LIST_TOOL_PROMPT.snippet,
717
+ promptGuidelines: PEER_LIST_TOOL_PROMPT.guidelines,
718
+ parameters: {
719
+ type: "object",
720
+ properties: {},
721
+ additionalProperties: false,
722
+ } as any,
723
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
724
+ await activatePeerMode(ctx, { command: "peer_list", reason: "Peer mode activated" });
725
+ const data = await collectDashboardData(runtime, bridge, subagents, teams, dashboardState, attentionLedger);
726
+ syncNameCache(peerNameCache, data.peers.map((peer) => peer.name));
727
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peers · ${data.peers.length}`, data);
728
+ return {
729
+ content: [{
730
+ type: "text",
731
+ text: formatPeerList(data.snapshot.peerRows),
732
+ }],
733
+ details: {
734
+ peers: data.snapshot.peerRows.map((row) => {
735
+ const peer = data.peers.find((entry) => entry.sessionId === row.sessionId);
736
+ return {
737
+ name: row.name,
738
+ state: row.state,
739
+ activity: row.activity,
740
+ sessionId: row.sessionId,
741
+ driver: peer?.driver,
742
+ model: peer?.model,
743
+ cwd: peer?.cwd,
744
+ };
745
+ }),
746
+ },
747
+ };
748
+ },
749
+ });
750
+
751
+ pi.registerTool({
752
+ name: "peer_history",
753
+ label: "Peer History",
754
+ description: "Scroll through a peer transcript so the main agent can inspect prior visible messages and tool activity.",
755
+ promptSnippet: PEER_HISTORY_TOOL_PROMPT.snippet,
756
+ promptGuidelines: PEER_HISTORY_TOOL_PROMPT.guidelines,
757
+ parameters: {
758
+ type: "object",
759
+ properties: {
760
+ name: { type: "string", description: "Peer name." },
761
+ cursor: { type: "number", description: "Zero-based visible history cursor. Omit to start from the latest page." },
762
+ limit: { type: "number", description: "Maximum visible history entries to return (default 20, max 200)." },
763
+ },
764
+ required: ["name"],
765
+ additionalProperties: false,
766
+ } as any,
767
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
768
+ await activatePeerMode(ctx, { command: "peer_history", reason: "Peer mode activated" });
769
+ const input = params as { name?: unknown; cursor?: unknown; limit?: unknown };
770
+ const name = String(input.name ?? "").trim();
771
+ const cursor = input.cursor == null ? undefined : Number(input.cursor);
772
+ const limit = input.limit == null ? undefined : Number(input.limit);
773
+ if (!name) {
774
+ throw new Error("name required");
775
+ }
776
+ if (cursor != null && (!Number.isFinite(cursor) || cursor < 0)) {
777
+ throw new Error("cursor must be a non-negative number");
778
+ }
779
+ if (limit != null && (!Number.isFinite(limit) || limit <= 0)) {
780
+ throw new Error("limit must be a positive number");
781
+ }
782
+
783
+ const target = await resolvePeerHistoryTarget(name);
784
+ if (!target) {
785
+ throw new Error(`Unknown peer ${name}`);
786
+ }
787
+
788
+ const transcript = await runtime.readTranscript(target.sessionId);
789
+ const page = formatPeerHistoryPage(transcript.items, {
790
+ cursor: cursor == null ? undefined : Math.trunc(cursor),
791
+ limit: limit == null ? undefined : Math.trunc(limit),
792
+ });
793
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer history: ${name}`);
794
+ return {
795
+ content: [{
796
+ type: "text",
797
+ text: [
798
+ `Peer history: ${name}`,
799
+ `state ${target.state}`,
800
+ `driver ${target.driver ?? "-"}`,
801
+ `model ${target.model ?? "-"}`,
802
+ `cwd ${target.cwd}`,
803
+ `session ${target.sessionId}`,
804
+ `cursor ${page.startCursor}..${page.endCursor} of ${page.total}`,
805
+ `previousCursor ${page.previousCursor ?? "-"}`,
806
+ `nextCursor ${page.nextCursor ?? "-"}`,
807
+ `source ${target.source}`,
808
+ `history\n${formatQuotedTextBlock(page.text)}`,
809
+ ].join("\n\n"),
810
+ }],
811
+ details: {
812
+ peerName: name,
813
+ state: target.state,
814
+ sessionId: target.sessionId,
815
+ driver: target.driver,
816
+ model: target.model,
817
+ cwd: target.cwd,
818
+ source: target.source,
819
+ cursor: page.startCursor,
820
+ endCursor: page.endCursor,
821
+ total: page.total,
822
+ previousCursor: page.previousCursor,
823
+ nextCursor: page.nextCursor,
824
+ },
825
+ };
826
+ },
827
+ });
828
+
829
+ pi.registerTool({
830
+ name: "peer_ask",
831
+ label: "Ask Peer",
832
+ description: "Send a message to an existing peer and wait for its visible reply. Optionally switch the peer to a different model.",
833
+ promptSnippet: PEER_ASK_TOOL_PROMPT.snippet,
834
+ promptGuidelines: PEER_ASK_TOOL_PROMPT.guidelines,
835
+ parameters: {
836
+ type: "object",
837
+ properties: {
838
+ name: { type: "string", description: "Peer name." },
839
+ message: { type: "string", description: "Message to send to the peer." },
840
+ model: { type: "string", description: "Optional model override. When set, the peer keeps using that model for later turns." },
841
+ },
842
+ required: ["name", "message"],
843
+ additionalProperties: false,
844
+ } as any,
845
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
846
+ await activatePeerMode(ctx, { command: "peer_ask", reason: "Peer mode activated" });
847
+ const input = params as { name?: unknown; message?: unknown; model?: unknown };
848
+ const name = String(input.name ?? "").trim();
849
+ const message = String(input.message ?? "").trim();
850
+ const model = typeof input.model === "string" ? input.model.trim() || undefined : undefined;
851
+ if (!name || !message) {
852
+ throw new Error("name and message required");
853
+ }
854
+ const currentPeer = await bridge.status(name).catch(() => undefined);
855
+ const effectiveDriver = currentPeer?.driver ?? runtimeDriverConfig.defaultDriver;
856
+ const modelSelection = model ? resolveRuntimeModelSelection(effectiveDriver, model) : undefined;
857
+ const modelNote = modelSelection?.note;
858
+ const promptSizeNote = describePromptSize("peer message", message);
859
+
860
+ sendCommandMessage(pi, {
861
+ level: "info",
862
+ command: "peer_ask",
863
+ title: `Sent to peer: ${name}`,
864
+ body: [modelNote, promptSizeNote, formatQuotedTextBlock(truncate(message, 4_000))].filter(Boolean).join("\n\n"),
865
+ });
866
+
867
+ suppressNextPeerRelay.delete(name);
868
+ const result = await bridge.ask(name, {
869
+ from: "pi-main-agent",
870
+ text: message,
871
+ model: modelSelection?.runtimeModel,
872
+ });
873
+ peerNameCache.add(name);
874
+ const { row } = await syncPeerRelaySnapshot(result.peer);
875
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer replied: ${name}`);
876
+ const deliveredAndRunning = result.deliveryState === "delivered_and_running";
877
+ const visibleReply = result.reply.trim() || (deliveredAndRunning ? "<peer still running; wait for automated update>" : "<empty reply>");
878
+ return {
879
+ content: [{
880
+ type: "text",
881
+ text: [
882
+ deliveredAndRunning ? `Peer message delivered: ${name}` : `Peer reply: ${name}`,
883
+ "sent prompt shown in an operator notification",
884
+ deliveredAndRunning ? "delivery delivered_and_running" : undefined,
885
+ deliveredAndRunning ? "do not poll; wait for automated peer update" : undefined,
886
+ `state ${row.state}`,
887
+ `driver ${result.peer.driver ?? "claude-sdk"}`,
888
+ `model ${result.peer.model ?? "-"}`,
889
+ modelNote,
890
+ promptSizeNote,
891
+ `quoted peer message\n${formatQuotedTextBlock(visibleReply)}`,
892
+ ].filter(Boolean).join("\n\n"),
893
+ }],
894
+ details: { peerName: name, state: row.state, sessionId: result.peer.sessionId, driver: result.peer.driver, model: result.peer.model, requestedModel: model, modelNote, promptSizeNote, cwd: result.peer.cwd, message, reply: result.reply, deliveryState: result.deliveryState },
895
+ };
896
+ },
897
+ });
898
+
899
+ pi.registerTool({
900
+ name: "peer_send",
901
+ label: "Send to Peer",
902
+ description: "Send a fire-and-forget message to an idle peer. The peer completion will arrive later as an automated follow-up.",
903
+ promptSnippet: PEER_SEND_TOOL_PROMPT.snippet,
904
+ promptGuidelines: PEER_SEND_TOOL_PROMPT.guidelines,
905
+ parameters: {
906
+ type: "object",
907
+ properties: {
908
+ name: { type: "string", description: "Peer name." },
909
+ message: { type: "string", description: "Message to send to the peer." },
910
+ model: { type: "string", description: "Optional model override. When set, the peer keeps using that model for later turns." },
911
+ },
912
+ required: ["name", "message"],
913
+ additionalProperties: false,
914
+ } as any,
915
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
916
+ await activatePeerMode(ctx, { command: "peer_send", reason: "Peer mode activated" });
917
+ const input = params as { name?: unknown; message?: unknown; model?: unknown };
918
+ const name = String(input.name ?? "").trim();
919
+ const message = String(input.message ?? "").trim();
920
+ const model = typeof input.model === "string" ? input.model.trim() || undefined : undefined;
921
+ if (!name || !message) {
922
+ throw new Error("name and message required");
923
+ }
924
+ const currentPeer = await bridge.status(name).catch(() => undefined);
925
+ const effectiveDriver = currentPeer?.driver ?? runtimeDriverConfig.defaultDriver;
926
+ const modelSelection = model ? resolveRuntimeModelSelection(effectiveDriver, model) : undefined;
927
+ const modelNote = modelSelection?.note;
928
+ const promptSizeNote = describePromptSize("peer message", message);
929
+
930
+ sendCommandMessage(pi, {
931
+ level: "info",
932
+ command: "peer_send",
933
+ title: `Sent to peer: ${name}`,
934
+ body: [modelNote, promptSizeNote, formatQuotedTextBlock(truncate(message, 4_000))].filter(Boolean).join("\n\n"),
935
+ });
936
+
937
+ suppressNextPeerRelay.delete(name);
938
+ const peer = await bridge.send(name, {
939
+ from: "pi-main-agent",
940
+ text: message,
941
+ model: modelSelection?.runtimeModel,
942
+ }, { waitForIdle: false });
943
+ peerNameCache.add(name);
944
+ const { row } = await syncPeerRelaySnapshot(peer);
945
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer message sent: ${name}`);
946
+ return {
947
+ content: [{
948
+ type: "text",
949
+ text: [
950
+ `Peer message sent: ${name}`,
951
+ "delivery delivered_and_running",
952
+ "do not poll; wait for automated peer update",
953
+ `state ${row.state}`,
954
+ `driver ${peer.driver ?? "claude-sdk"}`,
955
+ `model ${peer.model ?? "-"}`,
956
+ modelNote,
957
+ promptSizeNote,
958
+ ].filter(Boolean).join("\n\n"),
959
+ }],
960
+ details: { peerName: name, state: row.state, sessionId: peer.sessionId, driver: peer.driver, model: peer.model, requestedModel: model, modelNote, promptSizeNote, cwd: peer.cwd, message, deliveryState: "delivered_and_running" },
961
+ };
962
+ },
963
+ });
964
+
965
+ pi.registerTool({
966
+ name: "peer_interrupt",
967
+ label: "Interrupt Peer",
968
+ description: "Gracefully interrupt a running peer without forgetting its registry entry.",
969
+ promptSnippet: PEER_INTERRUPT_TOOL_PROMPT.snippet,
970
+ promptGuidelines: PEER_INTERRUPT_TOOL_PROMPT.guidelines,
971
+ parameters: {
972
+ type: "object",
973
+ properties: {
974
+ name: { type: "string", description: "Peer name." },
975
+ },
976
+ required: ["name"],
977
+ additionalProperties: false,
978
+ } as any,
979
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
980
+ await activatePeerMode(ctx, { command: "peer_interrupt", reason: "Peer mode activated" });
981
+ const input = params as { name?: unknown };
982
+ const name = String(input.name ?? "").trim();
983
+ if (!name) {
984
+ throw new Error("name required");
985
+ }
986
+
987
+ const { peer, interrupt } = await interruptPeer(name);
988
+ peerNameCache.add(name);
989
+ const { row } = await syncPeerRelaySnapshot(peer);
990
+ const canSendImmediately = !["busy", "starting"].includes(peer.state);
991
+ const interruptLines = [
992
+ `Peer interrupt requested: ${name}`,
993
+ `signal delivered ${interrupt.interrupted ? "yes" : "no"}`,
994
+ `reason ${interrupt.reason}`,
995
+ interrupt.signal ? `signal ${interrupt.signal}` : undefined,
996
+ `resulting state ${peer.state}`,
997
+ row.state !== peer.state ? `dashboard state ${row.state}` : undefined,
998
+ `follow-up send ${canSendImmediately ? "available now" : "wait until peer leaves busy/starting"}`,
999
+ `session ${peer.sessionId}`,
1000
+ ].filter(Boolean) as string[];
1001
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer interrupted: ${name}`);
1002
+ sendCommandMessage(pi, {
1003
+ level: "warning",
1004
+ command: "peer_interrupt",
1005
+ title: `Peer interrupted: ${name}`,
1006
+ body: interruptLines.slice(1).join("\n"),
1007
+ });
1008
+ return {
1009
+ content: [{
1010
+ type: "text",
1011
+ text: interruptLines.join("\n\n"),
1012
+ }],
1013
+ details: {
1014
+ peerName: name,
1015
+ state: row.state,
1016
+ peerState: peer.state,
1017
+ sessionId: peer.sessionId,
1018
+ driver: peer.driver,
1019
+ model: peer.model,
1020
+ cwd: peer.cwd,
1021
+ interruptDelivered: interrupt.interrupted,
1022
+ interruptReason: interrupt.reason,
1023
+ signal: interrupt.signal,
1024
+ canSendImmediately,
1025
+ },
1026
+ };
1027
+ },
1028
+ });
1029
+
1030
+ pi.registerTool({
1031
+ name: "peer_stop",
1032
+ label: "Stop Peer",
1033
+ description: "Stop a named peer, or stop all peers when explicitly confirmed.",
1034
+ promptSnippet: PEER_STOP_TOOL_PROMPT.snippet,
1035
+ promptGuidelines: PEER_STOP_TOOL_PROMPT.guidelines,
1036
+ parameters: {
1037
+ type: "object",
1038
+ properties: {
1039
+ name: { type: "string", description: "Peer name. Optional when using all=true." },
1040
+ all: { type: "boolean", description: "Stop all peers instead of one named peer. Requires confirmAll=true." },
1041
+ confirmAll: { type: "boolean", description: "Required safety confirmation when all=true." },
1042
+ },
1043
+ additionalProperties: false,
1044
+ } as any,
1045
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1046
+ await activatePeerMode(ctx, { command: "peer_stop", reason: "Peer mode activated" });
1047
+ const input = params as { name?: unknown; all?: unknown; confirmAll?: unknown };
1048
+ const name = String(input.name ?? "").trim();
1049
+ const stopAll = input.all === true;
1050
+ const confirmAll = input.confirmAll === true;
1051
+
1052
+ if (stopAll) {
1053
+ if (!confirmAll) {
1054
+ throw new Error("Stopping all peers requires confirmAll=true");
1055
+ }
1056
+ const peers = await bridge.listPeers();
1057
+ if (peers.length === 0) {
1058
+ return {
1059
+ content: [{ type: "text", text: "No peers to stop." }],
1060
+ details: { stoppedPeers: [], count: 0 },
1061
+ };
1062
+ }
1063
+
1064
+ const stoppedPeers = [] as Array<{ peerName: string; state: string; sessionId: string; driver?: string; model?: string; cwd: string }>;
1065
+ for (const peer of peers) {
1066
+ const stopped = await bridge.stop(peer.name);
1067
+ peerNameCache.delete(peer.name);
1068
+ peerRelaySnapshots.delete(peer.name);
1069
+ suppressNextPeerRelay.delete(peer.name);
1070
+ stoppedPeers.push({
1071
+ peerName: stopped.name,
1072
+ state: stopped.state,
1073
+ sessionId: stopped.sessionId,
1074
+ driver: stopped.driver,
1075
+ model: stopped.model,
1076
+ cwd: stopped.cwd,
1077
+ });
1078
+ }
1079
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peers stopped: ${stoppedPeers.length}`);
1080
+ return {
1081
+ content: [{
1082
+ type: "text",
1083
+ text: [
1084
+ `Stopped ${stoppedPeers.length} peer${stoppedPeers.length === 1 ? "" : "s"}.`,
1085
+ stoppedPeers.map((peer) => `${peer.peerName} ${peer.driver ?? runtimeDriverConfig.defaultDriver} ${peer.state}`).join("\n"),
1086
+ ].join("\n\n"),
1087
+ }],
1088
+ details: { stoppedPeers, count: stoppedPeers.length },
1089
+ };
1090
+ }
1091
+
1092
+ if (!name) {
1093
+ throw new Error("name required unless all=true");
1094
+ }
1095
+
1096
+ const peer = await bridge.stop(name);
1097
+ peerNameCache.delete(name);
1098
+ peerRelaySnapshots.delete(name);
1099
+ suppressNextPeerRelay.delete(name);
1100
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer stopped: ${peer.name}`);
1101
+ return {
1102
+ content: [{
1103
+ type: "text",
1104
+ text: [
1105
+ `Peer stopped: ${peer.name}`,
1106
+ `driver ${peer.driver ?? runtimeDriverConfig.defaultDriver}`,
1107
+ `state ${peer.state}`,
1108
+ `session ${peer.sessionId}`,
1109
+ ].join("\n\n"),
1110
+ }],
1111
+ details: {
1112
+ peerName: peer.name,
1113
+ state: peer.state,
1114
+ sessionId: peer.sessionId,
1115
+ driver: peer.driver,
1116
+ model: peer.model,
1117
+ cwd: peer.cwd,
1118
+ },
1119
+ };
1120
+ },
1121
+ });
1122
+
1123
+ if (showAdvancedCommands) {
1124
+ pi.registerTool({
1125
+ name: "subagent_run",
1126
+ label: "Run Subagent",
1127
+ description: "Start a runtime-backed subagent run and return its status or result.",
1128
+ promptSnippet: SUBAGENT_RUN_TOOL_PROMPT.snippet,
1129
+ promptGuidelines: SUBAGENT_RUN_TOOL_PROMPT.guidelines,
1130
+ parameters: {
1131
+ type: "object",
1132
+ properties: {
1133
+ task: { type: "string", description: "Task for the delegated run." },
1134
+ name: { type: "string", description: "Optional agent name for this run." },
1135
+ prompt: { type: "string", description: "Optional agent prompt. Defaults to a concise delegated-worker prompt." },
1136
+ driver: { type: "string", enum: ["claude-sdk", "codex-cli"], description: "Optional runtime driver for this run." },
1137
+ model: { type: "string", description: "Optional model override for this run." },
1138
+ cwd: { type: "string", description: "Optional working directory for this run. Relative paths resolve from the current pi working directory." },
1139
+ async: { type: "boolean", description: "Launch as background run and return immediately." },
1140
+ },
1141
+ required: ["task"],
1142
+ additionalProperties: false,
1143
+ } as any,
1144
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1145
+ await activatePeerMode(ctx, { command: "subagent_run", reason: "Peer mode activated" });
1146
+ const input = parseSubagentRunToolInput(params as { task?: unknown; name?: unknown; prompt?: unknown; driver?: unknown; model?: unknown; cwd?: unknown; async?: unknown });
1147
+ const effectiveDriver = input.driver ?? runtimeDriverConfig.defaultDriver;
1148
+ const modelSelection = resolveRuntimeModelSelection(effectiveDriver, input.model);
1149
+ const modelNote = modelSelection.note;
1150
+ const promptSizeNote = describePromptSize("subagent task", input.task);
1151
+
1152
+ const run = await subagents.startRun(buildSubagentRunRequest({ ...input, model: modelSelection.runtimeModel }, cwd));
1153
+ await syncAttentionLedger();
1154
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Run ${shortId(run.runId)} ${run.state}`);
1155
+ return {
1156
+ content: [{
1157
+ type: "text",
1158
+ text: [
1159
+ `Run ${run.state}: ${shortId(run.runId)}`,
1160
+ `agent ${run.agentName}`,
1161
+ `driver ${run.driver ?? runtimeDriverConfig.defaultDriver}`,
1162
+ modelNote,
1163
+ promptSizeNote,
1164
+ `state ${run.state}`,
1165
+ `cwd ${run.cwd}`,
1166
+ run.sessionId ? `session ${run.sessionId}` : undefined,
1167
+ run.result?.summary ? `summary\n${formatQuotedTextBlock(truncate(run.result.summary, 4000))}` : undefined,
1168
+ ].filter(Boolean).join("\n\n"),
1169
+ }],
1170
+ details: {
1171
+ runId: run.runId,
1172
+ agentName: run.agentName,
1173
+ driver: run.driver,
1174
+ model: run.model,
1175
+ requestedModel: input.model,
1176
+ modelNote,
1177
+ promptSizeNote,
1178
+ state: run.state,
1179
+ cwd: run.cwd,
1180
+ sessionId: run.sessionId,
1181
+ async: input.async,
1182
+ },
1183
+ };
1184
+ },
1185
+ });
1186
+
1187
+ pi.registerTool({
1188
+ name: "subagent_list",
1189
+ label: "List Subagent Runs",
1190
+ description: "List known local subagent runs with driver and state.",
1191
+ promptSnippet: SUBAGENT_LIST_TOOL_PROMPT.snippet,
1192
+ promptGuidelines: SUBAGENT_LIST_TOOL_PROMPT.guidelines,
1193
+ parameters: {
1194
+ type: "object",
1195
+ properties: {},
1196
+ additionalProperties: false,
1197
+ } as any,
1198
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1199
+ await activatePeerMode(ctx, { command: "subagent_list", reason: "Peer mode activated" });
1200
+ const runs = await subagents.listRuns();
1201
+ const attentionViews = listAttentionViews(attentionLedger, runs, Date.now());
1202
+ const attentionByRunId = new Map(attentionViews.map((view) => [view.run.runId, describeAttentionState(view, Date.now())]));
1203
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Runs · ${runs.length}`);
1204
+ return {
1205
+ content: [{
1206
+ type: "text",
1207
+ text: runs.length > 0
1208
+ ? runs.map((run) => {
1209
+ const attention = attentionByRunId.get(run.runId);
1210
+ return `${shortId(run.runId)} ${run.state}${attention ? ` [${attention}]` : ""} ${run.driver ?? "-"} ${run.agentName}`;
1211
+ }).join("\n")
1212
+ : "No runs found.",
1213
+ }],
1214
+ details: {
1215
+ runs: runs.map((run) => ({
1216
+ runId: run.runId,
1217
+ state: run.state,
1218
+ driver: run.driver,
1219
+ agentName: run.agentName,
1220
+ cwd: run.cwd,
1221
+ sessionId: run.sessionId,
1222
+ note: run.note,
1223
+ })),
1224
+ },
1225
+ };
1226
+ },
1227
+ });
1228
+
1229
+ pi.registerTool({
1230
+ name: "subagent_status",
1231
+ label: "Subagent Status",
1232
+ description: "Show status for one local subagent run.",
1233
+ promptSnippet: SUBAGENT_STATUS_TOOL_PROMPT.snippet,
1234
+ promptGuidelines: SUBAGENT_STATUS_TOOL_PROMPT.guidelines,
1235
+ parameters: {
1236
+ type: "object",
1237
+ properties: {
1238
+ runId: { type: "string", description: "Subagent run id." },
1239
+ },
1240
+ required: ["runId"],
1241
+ additionalProperties: false,
1242
+ } as any,
1243
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1244
+ await activatePeerMode(ctx, { command: "subagent_status", reason: "Peer mode activated" });
1245
+ const input = params as { runId?: unknown };
1246
+ const runId = String(input.runId ?? "").trim();
1247
+ if (!runId) {
1248
+ throw new Error("runId required");
1249
+ }
1250
+
1251
+ const resolvedRunId = resolveSubagentRunId(runId, await subagents.listRuns());
1252
+ const run = await subagents.statusRun(resolvedRunId);
1253
+ if (!run) {
1254
+ throw new Error(`Unknown run ${runId}`);
1255
+ }
1256
+
1257
+ await syncAttentionLedger();
1258
+ const attentionView = listAttentionViews(attentionLedger, [run], Date.now())[0];
1259
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Run ${shortId(run.runId)} ${run.state}`);
1260
+ return {
1261
+ content: [{
1262
+ type: "text",
1263
+ text: [
1264
+ `Run ${shortId(run.runId)} · ${run.state}`,
1265
+ `agent ${run.agentName}`,
1266
+ `driver ${run.driver ?? runtimeDriverConfig.defaultDriver}`,
1267
+ `cwd ${run.cwd}`,
1268
+ run.sessionId ? `session ${run.sessionId}` : undefined,
1269
+ attentionView ? `attention ${describeAttentionState(attentionView, Date.now())}` : undefined,
1270
+ run.note ? `note\n${formatQuotedTextBlock(run.note)}` : undefined,
1271
+ run.result?.summary ? `summary\n${formatQuotedTextBlock(truncate(run.result.summary, 4000))}` : undefined,
1272
+ ].filter(Boolean).join("\n\n"),
1273
+ }],
1274
+ details: {
1275
+ runId: run.runId,
1276
+ state: run.state,
1277
+ driver: run.driver,
1278
+ agentName: run.agentName,
1279
+ cwd: run.cwd,
1280
+ sessionId: run.sessionId,
1281
+ note: run.note,
1282
+ summary: run.result?.summary,
1283
+ },
1284
+ };
1285
+ },
1286
+ });
1287
+
1288
+ pi.registerTool({
1289
+ name: "team_spawn",
1290
+ label: "Spawn Teammate",
1291
+ description: "Spawn a persistent local teammate backed by the runtime bridge.",
1292
+ promptSnippet: TEAM_SPAWN_TOOL_PROMPT.snippet,
1293
+ promptGuidelines: TEAM_SPAWN_TOOL_PROMPT.guidelines,
1294
+ parameters: {
1295
+ type: "object",
1296
+ properties: {
1297
+ name: { type: "string", description: "Teammate name." },
1298
+ prompt: { type: "string", description: "Teammate bootstrap prompt." },
1299
+ driver: { type: "string", enum: ["claude-sdk", "codex-cli"], description: "Optional runtime driver for this teammate." },
1300
+ model: { type: "string", description: "Optional model override." },
1301
+ cwd: { type: "string", description: "Optional working directory. Relative paths resolve from the current pi working directory." },
1302
+ },
1303
+ required: ["name", "prompt"],
1304
+ additionalProperties: false,
1305
+ } as any,
1306
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1307
+ await activatePeerMode(ctx, { command: "team_spawn", reason: "Peer mode activated" });
1308
+ const input = parseTeamSpawnToolInput(params as { name?: unknown; prompt?: unknown; driver?: unknown; model?: unknown; cwd?: unknown });
1309
+ const effectiveDriver = input.driver ?? runtimeDriverConfig.defaultDriver;
1310
+ const modelSelection = resolveRuntimeModelSelection(effectiveDriver, input.model);
1311
+ const modelNote = modelSelection.note;
1312
+ const promptSizeNote = describePromptSize("teammate prompt", input.prompt);
1313
+
1314
+ if (!noSessionMode) {
1315
+ await ensureIntercomTransportHealthy(false);
1316
+ }
1317
+ const teammate = await teams.spawnTeammate(buildTeamSpawnRequest({ ...input, model: modelSelection.runtimeModel }, cwd));
1318
+ suppressNextPeerRelay.add(teammate.name);
1319
+ peerNameCache.add(teammate.name);
1320
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team + ${teammate.name}`);
1321
+ return {
1322
+ content: [{
1323
+ type: "text",
1324
+ text: [
1325
+ `Teammate spawned: ${teammate.name}`,
1326
+ `driver ${teammate.driver ?? runtimeDriverConfig.defaultDriver}`,
1327
+ modelNote,
1328
+ promptSizeNote,
1329
+ `state ${teammate.state}`,
1330
+ `cwd ${teammate.cwd}`,
1331
+ `session ${teammate.sessionId ?? "-"}`,
1332
+ ].filter(Boolean).join("\n\n"),
1333
+ }],
1334
+ details: { ...teammate, requestedModel: input.model, modelNote, promptSizeNote },
1335
+ };
1336
+ },
1337
+ });
1338
+
1339
+ pi.registerTool({
1340
+ name: "team_task",
1341
+ label: "Assign Team Task",
1342
+ description: "Assign a task to a persistent teammate and wait for the reply.",
1343
+ promptSnippet: TEAM_TASK_TOOL_PROMPT.snippet,
1344
+ promptGuidelines: TEAM_TASK_TOOL_PROMPT.guidelines,
1345
+ parameters: {
1346
+ type: "object",
1347
+ properties: {
1348
+ name: { type: "string", description: "Teammate name." },
1349
+ title: { type: "string", description: "Task title." },
1350
+ details: { type: "string", description: "Task details." },
1351
+ },
1352
+ required: ["name", "title", "details"],
1353
+ additionalProperties: false,
1354
+ } as any,
1355
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1356
+ await activatePeerMode(ctx, { command: "team_task", reason: "Peer mode activated" });
1357
+ const input = params as { name?: unknown; title?: unknown; details?: unknown };
1358
+ const name = String(input.name ?? "").trim();
1359
+ const title = String(input.title ?? "").trim();
1360
+ const details = String(input.details ?? "").trim();
1361
+ if (!name || !title || !details) {
1362
+ throw new Error("name, title, and details required");
1363
+ }
1364
+
1365
+ suppressNextPeerRelay.delete(name);
1366
+ const task = await teams.assignTask({ assignee: name, title, details });
1367
+ const teammate = await teams.teammateStatus(name);
1368
+ const peer = await bridge.status(name);
1369
+ if (peer) {
1370
+ await syncPeerRelaySnapshot(peer);
1371
+ }
1372
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Task ${shortId(task.taskId)} ${task.state}`);
1373
+ return {
1374
+ content: [{
1375
+ type: "text",
1376
+ text: [
1377
+ `Task ${task.state}: ${shortId(task.taskId)}`,
1378
+ `${task.assignee} · ${task.title}`,
1379
+ `driver ${teammate?.driver ?? runtimeDriverConfig.defaultDriver}`,
1380
+ task.lastReply ? `reply\n${formatQuotedTextBlock(truncate(task.lastReply, 4000))}` : undefined,
1381
+ ].filter(Boolean).join("\n\n"),
1382
+ }],
1383
+ details: { task, teammate },
1384
+ };
1385
+ },
1386
+ });
1387
+
1388
+ pi.registerTool({
1389
+ name: "team_message",
1390
+ label: "Message Teammate",
1391
+ description: "Send a direct message to a persistent teammate and wait for the reply.",
1392
+ promptSnippet: TEAM_MESSAGE_TOOL_PROMPT.snippet,
1393
+ promptGuidelines: TEAM_MESSAGE_TOOL_PROMPT.guidelines,
1394
+ parameters: {
1395
+ type: "object",
1396
+ properties: {
1397
+ name: { type: "string", description: "Teammate name." },
1398
+ message: { type: "string", description: "Direct message to send." },
1399
+ },
1400
+ required: ["name", "message"],
1401
+ additionalProperties: false,
1402
+ } as any,
1403
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1404
+ await activatePeerMode(ctx, { command: "team_message", reason: "Peer mode activated" });
1405
+ const input = params as { name?: unknown; message?: unknown };
1406
+ const name = String(input.name ?? "").trim();
1407
+ const message = String(input.message ?? "").trim();
1408
+ if (!name || !message) {
1409
+ throw new Error("name and message required");
1410
+ }
1411
+
1412
+ suppressNextPeerRelay.delete(name);
1413
+ const result = await teams.sendMessage(name, message);
1414
+ const peer = await bridge.status(name);
1415
+ if (peer) {
1416
+ await syncPeerRelaySnapshot(peer);
1417
+ }
1418
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team ← ${result.teammate.name}`);
1419
+ return {
1420
+ content: [{
1421
+ type: "text",
1422
+ text: [
1423
+ `Teammate reply: ${result.teammate.name}`,
1424
+ `driver ${result.teammate.driver ?? runtimeDriverConfig.defaultDriver}`,
1425
+ `quoted teammate message\n${formatQuotedTextBlock(truncate(result.reply, 4000) || "<empty reply>")}`,
1426
+ ].join("\n\n"),
1427
+ }],
1428
+ details: { teammate: result.teammate, reply: result.reply },
1429
+ };
1430
+ },
1431
+ });
1432
+
1433
+ pi.registerTool({
1434
+ name: "team_list",
1435
+ label: "List Team State",
1436
+ description: "List retained teammates and task records.",
1437
+ promptSnippet: TEAM_LIST_TOOL_PROMPT.snippet,
1438
+ promptGuidelines: TEAM_LIST_TOOL_PROMPT.guidelines,
1439
+ parameters: {
1440
+ type: "object",
1441
+ properties: {},
1442
+ additionalProperties: false,
1443
+ } as any,
1444
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1445
+ await activatePeerMode(ctx, { command: "team_list", reason: "Peer mode activated" });
1446
+ const teammates = await teams.listTeammates();
1447
+ const tasks = await teams.listTasks();
1448
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team/Todo · ${teammates.length}/${tasks.length}`);
1449
+ return {
1450
+ content: [{
1451
+ type: "text",
1452
+ text: [
1453
+ "Teammates",
1454
+ teammates.length > 0 ? teammates.map((teammate) => `${teammate.name} ${teammate.state} ${teammate.driver ?? "-"}`).join("\n") : "No teammates.",
1455
+ "",
1456
+ "Tasks",
1457
+ tasks.length > 0 ? tasks.map((task) => `${shortId(task.taskId)} ${task.state} ${task.assignee} ${task.title}`).join("\n") : "No tasks.",
1458
+ ].join("\n"),
1459
+ }],
1460
+ details: { teammates, tasks },
1461
+ };
1462
+ },
1463
+ });
1464
+
1465
+ pi.registerTool({
1466
+ name: "team_stop",
1467
+ label: "Stop Teammate",
1468
+ description: "Stop a persistent teammate.",
1469
+ promptSnippet: TEAM_STOP_TOOL_PROMPT.snippet,
1470
+ promptGuidelines: TEAM_STOP_TOOL_PROMPT.guidelines,
1471
+ parameters: {
1472
+ type: "object",
1473
+ properties: {
1474
+ name: { type: "string", description: "Teammate name." },
1475
+ },
1476
+ required: ["name"],
1477
+ additionalProperties: false,
1478
+ } as any,
1479
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1480
+ await activatePeerMode(ctx, { command: "team_stop", reason: "Peer mode activated" });
1481
+ const input = params as { name?: unknown };
1482
+ const name = String(input.name ?? "").trim();
1483
+ if (!name) {
1484
+ throw new Error("name required");
1485
+ }
1486
+
1487
+ suppressNextPeerRelay.delete(name);
1488
+ const teammate = await teams.stopTeammate(name);
1489
+ peerNameCache.delete(name);
1490
+ peerRelaySnapshots.delete(name);
1491
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team - ${teammate.name}`);
1492
+ return {
1493
+ content: [{
1494
+ type: "text",
1495
+ text: [
1496
+ `Teammate stopped: ${teammate.name}`,
1497
+ `driver ${teammate.driver ?? runtimeDriverConfig.defaultDriver}`,
1498
+ `state ${teammate.state}`,
1499
+ ].join("\n\n"),
1500
+ }],
1501
+ details: teammate,
1502
+ };
1503
+ },
1504
+ });
1505
+ }
1506
+
1507
+ pi.on("session_start", async (event, ctx) => {
1508
+ if (peerModeActive) {
1509
+ await activatePeerMode(ctx, { reason: `${startupSummary} (${event.reason})` });
1510
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger);
1511
+ ctx.ui.notify(`${startupSummary} (${event.reason})`, "info");
1512
+ }
1513
+ });
1514
+
1515
+ pi.on("session_shutdown", async () => {
1516
+ dashboardContextRef = undefined;
1517
+ operatorContextRef = undefined;
1518
+ lastWidgetSignature = undefined;
1519
+ peerRelaySnapshots.clear();
1520
+ suppressNextPeerRelay.clear();
1521
+ peerModeActive = false;
1522
+ peerGuideShown = false;
1523
+ runtimeDriverFallbackShown = false;
1524
+ if (backgroundMonitorTimer) {
1525
+ clearInterval(backgroundMonitorTimer);
1526
+ backgroundMonitorTimer = undefined;
1527
+ }
1528
+ await bridge.close();
1529
+ intercomTransport = undefined;
1530
+ intercomReachable = undefined;
1531
+ await persistAttentionLedger();
1532
+ attentionLedger = createAttentionLedger();
1533
+ persistedAttentionLedger = serializeAttentionLedger(attentionLedger);
1534
+ });
1535
+
1536
+ async function handleDashboardCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1537
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1538
+ const advanced = args.trim().toLowerCase() === "advanced";
1539
+ await syncAttentionLedger();
1540
+ const data = await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger);
1541
+ const health = getPeerFirstHealth(data.snapshot.peerRows, data.snapshot.transportDegraded);
1542
+ const advancedDiagnosticsWarning = data.snapshot.runIssues > 0
1543
+ || data.snapshot.runAttention > 0
1544
+ || data.snapshot.teammateIssues > 0
1545
+ || data.snapshot.taskIssues > 0;
1546
+ sendCommandMessage(pi, {
1547
+ level: health === "warning" || (advanced && advancedDiagnosticsWarning) ? "warning" : "info",
1548
+ command,
1549
+ title: advanced ? "Peer dashboard · advanced" : "Peer dashboard",
1550
+ body: advanced ? formatAdvancedDashboardReport(data) : formatPeerFirstDashboardReport(data),
1551
+ surface: "custom",
1552
+ });
1553
+ }
1554
+
1555
+ async function handlePeerStartCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1556
+ const parsed = parsePeerStartCommandInput(args);
1557
+ if (!parsed.prompt || hasPlaceholderToken(parsed.name, parsed.prompt)) {
1558
+ showUsage(pi, command, [
1559
+ `/${command} <prompt>`,
1560
+ `/${command} <prompt> | <driver> | <model>`,
1561
+ `/${command} <name> | <prompt>`,
1562
+ `/${command} <name> | <prompt> | <driver> | <model>`,
1563
+ `Example: /${command} Review auth flow and reply briefly.`,
1564
+ `Example: /${command} reviewer | You are a brief worker. Reply briefly.`,
1565
+ ].join("\n"));
1566
+ return;
1567
+ }
1568
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1569
+
1570
+ const existingNames = new Set((await bridge.listPeers()).map((peer) => peer.name));
1571
+ const name = parsed.name ?? derivePeerName(parsed.prompt, existingNames);
1572
+ const effectiveDriver = parsed.driver ?? runtimeDriverConfig.defaultDriver;
1573
+ const modelSelection = resolveRuntimeModelSelection(effectiveDriver, parsed.model);
1574
+ const modelNote = modelSelection.note;
1575
+ const promptSizeNote = describePromptSize("peer prompt", parsed.prompt);
1576
+
1577
+ sendCommandMessage(pi, {
1578
+ level: "info",
1579
+ command,
1580
+ title: `Starting peer: ${name}`,
1581
+ body: [
1582
+ parsed.autoNamed ? `auto-named from prompt\n\n${truncate(parsed.prompt, 160)}` : "Watch Peers widget for live activity.",
1583
+ parsed.driver ? `driver ${parsed.driver}` : undefined,
1584
+ modelNote,
1585
+ promptSizeNote,
1586
+ ].filter(Boolean).join("\n\n"),
1587
+ });
1588
+
1589
+ if (!noSessionMode) {
1590
+ await ensureIntercomTransportHealthy(false);
1591
+ }
1592
+ const peer = await startPeerWithoutWaiting({
1593
+ name,
1594
+ prompt: parsed.prompt,
1595
+ driver: parsed.driver,
1596
+ model: modelSelection.runtimeModel,
1597
+ cwd,
1598
+ permissionMode: "bypassPermissions",
1599
+ });
1600
+ peerNameCache.add(peer.name);
1601
+ const { row } = await syncPeerRelaySnapshot(peer);
1602
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer started: ${peer.name}`);
1603
+ sendCommandMessage(pi, {
1604
+ level: "success",
1605
+ command,
1606
+ title: `Peer started: ${peer.name}`,
1607
+ body: [
1608
+ parsed.autoNamed ? `auto-name ${peer.name}` : undefined,
1609
+ `state ${row.state}`,
1610
+ `driver ${peer.driver ?? "claude-sdk"}`,
1611
+ modelNote,
1612
+ promptSizeNote,
1613
+ `session ${peer.sessionId}`,
1614
+ "",
1615
+ PEER_NO_BABYSITTING_GUIDANCE,
1616
+ ].filter((line) => line !== undefined).join("\n"),
1617
+ });
1618
+ }
1619
+
1620
+ async function handlePeerListCommand(ctx: ExtensionCommandContext, command: string): Promise<void> {
1621
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1622
+ const data = await collectDashboardData(runtime, bridge, subagents, teams, dashboardState, attentionLedger);
1623
+ syncNameCache(peerNameCache, data.peers.map((peer) => peer.name));
1624
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peers · ${data.peers.length}`, data);
1625
+ sendCommandMessage(pi, {
1626
+ level: data.snapshot.peerRows.some((row) => ["error", "offline", "waiting"].includes(row.state)) ? "warning" : "info",
1627
+ command,
1628
+ title: `Peers (${data.peers.length})`,
1629
+ body: formatPeerList(data.snapshot.peerRows),
1630
+ surface: "custom",
1631
+ });
1632
+ }
1633
+
1634
+ async function handlePeerModelsCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1635
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1636
+ const { driver, verbose } = parsePeerModelsArgs(args);
1637
+ const catalogs = modelCatalogsForDriver(driver);
1638
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, "Runtime models");
1639
+ sendCommandMessage(pi, {
1640
+ level: "info",
1641
+ command,
1642
+ title: driver ? `Runtime models: ${driver}` : "Runtime models",
1643
+ body: formatModelCatalogReport(catalogs, { verbose }),
1644
+ surface: "custom",
1645
+ });
1646
+ }
1647
+
1648
+ async function handlePeerAskCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1649
+ const [name, message] = splitArgs(args, 2);
1650
+ if (!name || !message || hasPlaceholderToken(name, message)) {
1651
+ showUsage(pi, command, [
1652
+ `/${command} <name> | <message>`,
1653
+ `Example: /${command} worker1 | Reply with exactly: peer-ok`,
1654
+ ].join("\n"));
1655
+ return;
1656
+ }
1657
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1658
+
1659
+ sendCommandMessage(pi, {
1660
+ level: "info",
1661
+ command,
1662
+ title: `Sent to peer: ${name}`,
1663
+ body: formatQuotedTextBlock(truncate(message, 4_000)),
1664
+ });
1665
+
1666
+ suppressNextPeerRelay.delete(name);
1667
+ const result = await bridge.ask(name, {
1668
+ from: "pi-user",
1669
+ text: message,
1670
+ });
1671
+ peerNameCache.add(name);
1672
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer replied: ${name}`);
1673
+ sendCommandMessage(pi, {
1674
+ level: result.runState === "failed" ? "error" : result.runState === "interrupted" ? "warning" : "success",
1675
+ command,
1676
+ title: `Peer reply: ${name}`,
1677
+ body: [`driver ${result.peer.driver ?? "claude-sdk"}`, truncate(result.reply, 1200) || "<empty reply>"].join("\n\n"),
1678
+ });
1679
+ }
1680
+
1681
+ async function handlePeerSendCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1682
+ const [name, message] = splitArgs(args, 2);
1683
+ if (!name || !message || hasPlaceholderToken(name, message)) {
1684
+ showUsage(pi, command, [
1685
+ `/${command} <name> | <message>`,
1686
+ `Example: /${command} worker1 | Continue with the next batch and report back when done.`,
1687
+ ].join("\n"));
1688
+ return;
1689
+ }
1690
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1691
+
1692
+ sendCommandMessage(pi, {
1693
+ level: "info",
1694
+ command,
1695
+ title: `Sent to peer: ${name}`,
1696
+ body: formatQuotedTextBlock(truncate(message, 4_000)),
1697
+ });
1698
+
1699
+ suppressNextPeerRelay.delete(name);
1700
+ const peer = await bridge.send(name, {
1701
+ from: "pi-user",
1702
+ text: message,
1703
+ }, { waitForIdle: false });
1704
+ peerNameCache.add(name);
1705
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer message sent: ${name}`);
1706
+ sendCommandMessage(pi, {
1707
+ level: "success",
1708
+ command,
1709
+ title: `Peer message delivered: ${name}`,
1710
+ body: [
1711
+ "delivery delivered_and_running",
1712
+ "Do not poll; wait for the automated peer update.",
1713
+ `state ${peer.state}`,
1714
+ `driver ${peer.driver ?? "claude-sdk"}`,
1715
+ `session ${peer.sessionId}`,
1716
+ ].join("\n"),
1717
+ });
1718
+ }
1719
+
1720
+ async function handlePeerHistoryCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1721
+ const [name, cursorToken, limitToken] = args.trim().split(/\s+/, 3).filter(Boolean);
1722
+ const cursor = cursorToken == null ? undefined : Number(cursorToken);
1723
+ const limit = limitToken == null ? undefined : Number(limitToken);
1724
+ if (!name || hasPlaceholderToken(name)) {
1725
+ showUsage(pi, command, [
1726
+ `/${command} <name> [cursor] [limit]`,
1727
+ `Example: /${command} worker1`,
1728
+ `Example: /${command} worker1 0 20`,
1729
+ ].join("\n"));
1730
+ return;
1731
+ }
1732
+ if (cursor != null && (!Number.isFinite(cursor) || cursor < 0)) {
1733
+ throw new Error("cursor must be a non-negative number");
1734
+ }
1735
+ if (limit != null && (!Number.isFinite(limit) || limit <= 0)) {
1736
+ throw new Error("limit must be a positive number");
1737
+ }
1738
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1739
+
1740
+ const target = await resolvePeerHistoryTarget(name);
1741
+ if (!target) {
1742
+ throw new Error(`Unknown peer ${name}`);
1743
+ }
1744
+ const transcript = await runtime.readTranscript(target.sessionId);
1745
+ const page = formatPeerHistoryPage(transcript.items, {
1746
+ cursor: cursor == null ? undefined : Math.trunc(cursor),
1747
+ limit: limit == null ? undefined : Math.trunc(limit),
1748
+ });
1749
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer history: ${name}`);
1750
+ sendCommandMessage(pi, {
1751
+ level: "info",
1752
+ command,
1753
+ title: `Peer history: ${name}`,
1754
+ body: [
1755
+ `state ${target.state}`,
1756
+ `driver ${target.driver ?? "-"}`,
1757
+ `model ${target.model ?? "-"}`,
1758
+ `session ${target.sessionId}`,
1759
+ `cursor ${page.startCursor}..${page.endCursor} of ${page.total}`,
1760
+ `previousCursor ${page.previousCursor ?? "-"}`,
1761
+ `nextCursor ${page.nextCursor ?? "-"}`,
1762
+ `history\n${formatQuotedTextBlock(page.text)}`,
1763
+ ].join("\n\n"),
1764
+ surface: "custom",
1765
+ });
1766
+ }
1767
+
1768
+ async function handlePeerInterruptCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1769
+ const name = args.trim();
1770
+ if (!name || hasPlaceholderToken(name)) {
1771
+ showUsage(pi, command, [
1772
+ `/${command} <name>`,
1773
+ `Example: /${command} worker1`,
1774
+ ].join("\n"));
1775
+ return;
1776
+ }
1777
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1778
+
1779
+ const { peer, interrupt } = await interruptPeer(name);
1780
+ suppressNextPeerRelay.delete(name);
1781
+ peerNameCache.add(name);
1782
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer interrupted: ${peer.name}`);
1783
+ const canSendImmediately = !["busy", "starting"].includes(peer.state);
1784
+ sendCommandMessage(pi, {
1785
+ level: "warning",
1786
+ command,
1787
+ title: `Peer interrupted: ${peer.name}`,
1788
+ body: [
1789
+ `signal delivered ${interrupt.interrupted ? "yes" : "no"}`,
1790
+ `reason ${interrupt.reason}`,
1791
+ interrupt.signal ? `signal ${interrupt.signal}` : undefined,
1792
+ `resulting state ${peer.state}`,
1793
+ `follow-up send ${canSendImmediately ? "available now" : "wait until peer leaves busy/starting"}`,
1794
+ `session ${peer.sessionId}`,
1795
+ ].filter(Boolean).join("\n"),
1796
+ });
1797
+ }
1798
+
1799
+ async function handlePeerStopCommand(args: string, ctx: ExtensionCommandContext, command: string): Promise<void> {
1800
+ await activatePeerMode(ctx, { command, reason: "Peer mode activated" });
1801
+ const trimmed = args.trim();
1802
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
1803
+ const stopAll = tokens.includes("--all");
1804
+ const confirmAll = tokens.includes("--confirm");
1805
+
1806
+ if (stopAll) {
1807
+ const peers = await bridge.listPeers();
1808
+ if (peers.length === 0) {
1809
+ sendCommandMessage(pi, {
1810
+ level: "info",
1811
+ command,
1812
+ title: "No peers to stop",
1813
+ });
1814
+ return;
1815
+ }
1816
+ if (!confirmAll) {
1817
+ showUsage(pi, command, [
1818
+ "This stops all retained peers in this workspace.",
1819
+ `Peers: ${peers.map((peer) => peer.name).join(", ")}`,
1820
+ `/${command} --all --confirm`,
1821
+ ].join("\n"));
1822
+ return;
1823
+ }
1824
+
1825
+ const stopped: string[] = [];
1826
+ for (const peer of peers) {
1827
+ await bridge.stop(peer.name);
1828
+ peerNameCache.delete(peer.name);
1829
+ peerRelaySnapshots.delete(peer.name);
1830
+ suppressNextPeerRelay.delete(peer.name);
1831
+ stopped.push(peer.name);
1832
+ }
1833
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peers stopped: ${stopped.length}`);
1834
+ sendCommandMessage(pi, {
1835
+ level: "warning",
1836
+ command,
1837
+ title: `Stopped ${stopped.length} peer${stopped.length === 1 ? "" : "s"}`,
1838
+ body: stopped.join("\n"),
1839
+ });
1840
+ return;
1841
+ }
1842
+
1843
+ const name = trimmed;
1844
+ if (!name || hasPlaceholderToken(name)) {
1845
+ showUsage(pi, command, [
1846
+ `/${command} <name>`,
1847
+ `/${command} --all --confirm`,
1848
+ `Example: /${command} worker1`,
1849
+ ].join("\n"));
1850
+ return;
1851
+ }
1852
+
1853
+ const peer = await bridge.stop(name);
1854
+ peerNameCache.delete(name);
1855
+ peerRelaySnapshots.delete(name);
1856
+ suppressNextPeerRelay.delete(name);
1857
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Peer stopped: ${peer.name}`);
1858
+ sendCommandMessage(pi, {
1859
+ level: "warning",
1860
+ command,
1861
+ title: `Peer stopped: ${peer.name}`,
1862
+ body: `state ${peer.state}`,
1863
+ });
1864
+ }
1865
+
1866
+ function showPeerUsage(command: string): void {
1867
+ sendCommandMessage(pi, {
1868
+ level: "info",
1869
+ command,
1870
+ title: "Peer help",
1871
+ body: [
1872
+ `${EXTENSION_NAME} v${EXTENSION_VERSION}`,
1873
+ "",
1874
+ "Concepts",
1875
+ "- Peers are long-lived local runtime sessions, not stateless provider calls.",
1876
+ "- Start peers for background work, then keep working while the widget and automatic relays surface updates.",
1877
+ "- Use ask when you need a reply now; use send for fire-and-forget work.",
1878
+ "- Use history to inspect a peer transcript without repeatedly polling for completion.",
1879
+ "- The live intercom broker is optional. When it is offline, peers stay local.",
1880
+ "",
1881
+ "Common flow",
1882
+ `1. /${command} init`,
1883
+ `2. /${command} models`,
1884
+ `3. /${command} start <prompt>`,
1885
+ `4. /${command} dashboard`,
1886
+ `5. /${command} history <name>`,
1887
+ "",
1888
+ "Commands",
1889
+ `/${command} or /${command} dashboard`,
1890
+ `/${command} about`,
1891
+ `/${command} init`,
1892
+ `/${command} dashboard advanced`,
1893
+ `/${command} start <prompt>`,
1894
+ `/${command} start <prompt> | <driver> | <model>`,
1895
+ `/${command} start <name> | <prompt>`,
1896
+ `/${command} start <name> | <prompt> | <driver> | <model>`,
1897
+ `/${command} ask <name> | <message>`,
1898
+ `/${command} send <name> | <message>`,
1899
+ `/${command} list`,
1900
+ `/${command} models [claude-sdk|codex-cli] [all|advanced|verbose]`,
1901
+ `/${command} history <name> [cursor] [limit]`,
1902
+ `/${command} interrupt <name>`,
1903
+ `/${command} stop <name>`,
1904
+ `/${command} stop --all --confirm`,
1905
+ "",
1906
+ "Driver/model forms",
1907
+ `/${command} start <prompt> | <driver> | <model>`,
1908
+ `/${command} start <name> | <prompt> | <driver> | <model>`,
1909
+ "drivers: claude-sdk, codex-cli",
1910
+ "model aliases: sonnet, opus, haiku, mini, spark; exact ids are shown by /peer models",
1911
+ "",
1912
+ "Version/environment",
1913
+ `/${command} about`,
1914
+ ].join("\n"),
1915
+ surface: "custom",
1916
+ });
1917
+ }
1918
+
1919
+ function showPeerAbout(command: string): void {
1920
+ sendCommandMessage(pi, {
1921
+ level: "info",
1922
+ command,
1923
+ title: `${EXTENSION_NAME} v${EXTENSION_VERSION}`,
1924
+ body: [
1925
+ `package ${EXTENSION_NAME}`,
1926
+ `version ${EXTENSION_VERSION}`,
1927
+ `package root ${EXTENSION_PACKAGE_ROOT}`,
1928
+ `state root ${rootDir}`,
1929
+ `default driver ${runtimeDriverConfig.defaultDriver}`,
1930
+ runtimeDriverConfig.note,
1931
+ `no-session ${noSessionMode ? "yes" : "no"}`,
1932
+ ].filter(Boolean).join("\n"),
1933
+ surface: "custom",
1934
+ });
1935
+ }
1936
+
1937
+ registerExtensionCommand(pi, "peer", "Peer dashboard and controls. Args: [about|init|dashboard|start|ask|send|list|models|history|interrupt|stop] ...", async (args, ctx) => {
1938
+ const trimmed = args.trim();
1939
+ if (!trimmed) {
1940
+ await activatePeerMode(ctx, { command: "peer", showGuide: true, reason: "Peer mode activated" });
1941
+ await handleDashboardCommand("", ctx, "peer");
1942
+ return;
1943
+ }
1944
+
1945
+ const [subcommandRaw = "", ...restParts] = trimmed.split(/\s+/);
1946
+ const subcommand = subcommandRaw.toLowerCase();
1947
+ const rest = restParts.join(" ").trim();
1948
+
1949
+ switch (subcommand) {
1950
+ case "about":
1951
+ case "version":
1952
+ case "--version":
1953
+ case "-v":
1954
+ showPeerAbout("peer about");
1955
+ return;
1956
+ case "init":
1957
+ await activatePeerMode(ctx, { command: "peer init", showGuide: true, forceGuide: true, reason: "Peer mode activated" });
1958
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger);
1959
+ return;
1960
+ case "dashboard":
1961
+ await activatePeerMode(ctx, { command: "peer dashboard", showGuide: true, reason: "Peer mode activated" });
1962
+ await handleDashboardCommand(rest, ctx, "peer dashboard");
1963
+ return;
1964
+ case "start":
1965
+ await activatePeerMode(ctx, { command: "peer start", showGuide: true, reason: "Peer mode activated" });
1966
+ await handlePeerStartCommand(rest, ctx, "peer start");
1967
+ return;
1968
+ case "ask":
1969
+ await activatePeerMode(ctx, { command: "peer ask", showGuide: true, reason: "Peer mode activated" });
1970
+ await handlePeerAskCommand(rest, ctx, "peer ask");
1971
+ return;
1972
+ case "send":
1973
+ await activatePeerMode(ctx, { command: "peer send", showGuide: true, reason: "Peer mode activated" });
1974
+ await handlePeerSendCommand(rest, ctx, "peer send");
1975
+ return;
1976
+ case "list":
1977
+ await activatePeerMode(ctx, { command: "peer list", showGuide: true, reason: "Peer mode activated" });
1978
+ await handlePeerListCommand(ctx, "peer list");
1979
+ return;
1980
+ case "models":
1981
+ await activatePeerMode(ctx, { command: "peer models", showGuide: true, reason: "Peer mode activated" });
1982
+ await handlePeerModelsCommand(rest, ctx, "peer models");
1983
+ return;
1984
+ case "history":
1985
+ await activatePeerMode(ctx, { command: "peer history", showGuide: true, reason: "Peer mode activated" });
1986
+ await handlePeerHistoryCommand(rest, ctx, "peer history");
1987
+ return;
1988
+ case "interrupt":
1989
+ await activatePeerMode(ctx, { command: "peer interrupt", showGuide: true, reason: "Peer mode activated" });
1990
+ await handlePeerInterruptCommand(rest, ctx, "peer interrupt");
1991
+ return;
1992
+ case "stop":
1993
+ await activatePeerMode(ctx, { command: "peer stop", showGuide: true, reason: "Peer mode activated" });
1994
+ await handlePeerStopCommand(rest, ctx, "peer stop");
1995
+ return;
1996
+ case "help":
1997
+ case "--help":
1998
+ case "-h":
1999
+ showPeerUsage("peer");
2000
+ return;
2001
+ default:
2002
+ showPeerUsage("peer");
2003
+ }
2004
+ }, (prefix) => completePeerCommand(prefix, peerNameCache));
2005
+
2006
+ if (showLegacyCommands && showAdvancedCommands) {
2007
+ registerExtensionCommand(pi, "claude-dev-ping", `Legacy advanced: proof that ${EXTENSION_NAME} reloaded successfully (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (_args, ctx) => {
2008
+ const sessions = await runtime.list();
2009
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Ping ok · s=${sessions.length}`);
2010
+ sendCommandMessage(pi, {
2011
+ level: "success",
2012
+ command: "claude-dev-ping",
2013
+ title: "Ping ok",
2014
+ body: `sessions ${sessions.length}`,
2015
+ });
2016
+ });
2017
+ }
2018
+
2019
+ if (showLegacyCommands) {
2020
+ registerExtensionCommand(pi, "claude-dashboard", `Legacy: use /peer dashboard instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (args, ctx) => handleDashboardCommand(args, ctx, "claude-dashboard"));
2021
+ registerExtensionCommand(pi, "claude-peer-start", `Legacy: use /peer start instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (args, ctx) => handlePeerStartCommand(args, ctx, "claude-peer-start"));
2022
+ registerExtensionCommand(pi, "claude-peer-list", `Legacy: use /peer list instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (_args, ctx) => handlePeerListCommand(ctx, "claude-peer-list"));
2023
+ registerExtensionCommand(pi, "claude-peer-ask", `Legacy: use /peer ask instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (args, ctx) => handlePeerAskCommand(args, ctx, "claude-peer-ask"), (prefix) => completePeerName(prefix, peerNameCache, true));
2024
+ registerExtensionCommand(pi, "claude-peer-send", `Legacy: use /peer send instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (args, ctx) => handlePeerSendCommand(args, ctx, "claude-peer-send"), (prefix) => completePeerName(prefix, peerNameCache, true));
2025
+ registerExtensionCommand(pi, "claude-peer-interrupt", `Legacy: use /peer interrupt instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (args, ctx) => handlePeerInterruptCommand(args, ctx, "claude-peer-interrupt"), (prefix) => completePeerName(prefix, peerNameCache, false));
2026
+ registerExtensionCommand(pi, "claude-peer-stop", `Legacy: use /peer stop instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, (args, ctx) => handlePeerStopCommand(args, ctx, "claude-peer-stop"), (prefix) => completePeerName(prefix, peerNameCache, false));
2027
+ registerExtensionCommand(pi, "claude-peer-stop-all", `Legacy: use /peer stop --all --confirm instead (set ${LEGACY_COMMANDS_ENV}=1 to enable)`, async (args, ctx) => {
2028
+ const converted = args.trim() === "--yes" ? "--all --confirm" : "--all";
2029
+ await handlePeerStopCommand(converted, ctx, "claude-peer-stop-all");
2030
+ });
2031
+ }
2032
+
2033
+ if (showLegacyCommands && showAdvancedCommands) {
2034
+ registerExtensionCommand(pi, "claude-subagent-run", `Legacy advanced: run Claude subagent job. Args: <task> or <driver> | <task> (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2035
+ const parsed = parseSubagentRunCommandInput(args);
2036
+ if (!parsed.task) {
2037
+ showUsage(pi, "claude-subagent-run", [
2038
+ "/claude-subagent-run <task>",
2039
+ "/claude-subagent-run <driver> | <task>",
2040
+ "Example: /claude-subagent-run codex-cli | Reply with exactly: subagent-ok",
2041
+ ].join("\n"));
2042
+ return;
2043
+ }
2044
+
2045
+ const run = await subagents.startRun({
2046
+ agent: {
2047
+ name: "claude-subagent",
2048
+ runner: "claude-code-agent",
2049
+ prompt: "You are delegated worker. Be concise and execution-focused.",
2050
+ cwd,
2051
+ },
2052
+ task: parsed.task,
2053
+ driver: parsed.driver,
2054
+ });
2055
+ await syncAttentionLedger();
2056
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Run ${shortId(run.runId)} ${run.state}`);
2057
+ sendCommandMessage(pi, {
2058
+ level: runStateLevel(run.state),
2059
+ command: "claude-subagent-run",
2060
+ title: `Run ${run.state}: ${shortId(run.runId)}`,
2061
+ body: [
2062
+ `agent ${run.agentName}`,
2063
+ `driver ${run.driver ?? runtimeDriverConfig.defaultDriver}`,
2064
+ `state ${run.state}`,
2065
+ run.result?.summary ? `summary\n${truncate(run.result.summary, 1200)}` : undefined,
2066
+ ].filter(Boolean).join("\n\n"),
2067
+ });
2068
+ });
2069
+
2070
+ registerExtensionCommand(pi, "claude-subagent-list", `Advanced: list Claude subagent runs (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (_args, ctx) => {
2071
+ const runs = await subagents.listRuns();
2072
+ const attentionViews = listAttentionViews(attentionLedger, runs, Date.now());
2073
+ const attentionByRunId = new Map(attentionViews.map((view) => [view.run.runId, describeAttentionState(view, Date.now())]));
2074
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Runs · ${runs.length}`);
2075
+ sendCommandMessage(pi, {
2076
+ level: runs.some((run) => ["failed", "interrupted"].includes(run.state) || hasAttentionNote(run.note)) ? "warning" : "info",
2077
+ command: "claude-subagent-list",
2078
+ title: `Runs (${runs.length})`,
2079
+ body: runs.length > 0
2080
+ ? runs.map((run) => {
2081
+ const attention = attentionByRunId.get(run.runId);
2082
+ return `${shortId(run.runId)} ${run.state}${attention ? ` [${attention}]` : ""} ${run.driver ?? "-"} ${run.agentName}`;
2083
+ }).join("\n")
2084
+ : "No runs found.",
2085
+ surface: "custom",
2086
+ });
2087
+ });
2088
+
2089
+ registerExtensionCommand(pi, "claude-subagent-status", `Advanced: show Claude subagent run status. Args: <runId> (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2090
+ const runId = args.trim();
2091
+ if (!runId) {
2092
+ showUsage(pi, "claude-subagent-status", "/claude-subagent-status <runId>");
2093
+ return;
2094
+ }
2095
+
2096
+ const resolvedRunId = resolveSubagentRunId(runId, await subagents.listRuns());
2097
+ const run = await subagents.statusRun(resolvedRunId);
2098
+ if (!run) {
2099
+ sendCommandMessage(pi, {
2100
+ level: "error",
2101
+ command: "claude-subagent-status",
2102
+ title: `Unknown run: ${runId}`,
2103
+ });
2104
+ return;
2105
+ }
2106
+
2107
+ await syncAttentionLedger();
2108
+ const attentionView = listAttentionViews(attentionLedger, [run], Date.now())[0];
2109
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Run ${shortId(run.runId)} ${run.state}`);
2110
+ sendCommandMessage(pi, {
2111
+ level: runStateLevel(run.state),
2112
+ command: "claude-subagent-status",
2113
+ title: `Run ${shortId(run.runId)} · ${run.state}`,
2114
+ body: [
2115
+ `agent ${run.agentName}`,
2116
+ `driver ${run.driver ?? runtimeDriverConfig.defaultDriver}`,
2117
+ attentionView ? `attention ${describeAttentionState(attentionView, Date.now())}` : undefined,
2118
+ run.note ? `note\n${run.note}` : undefined,
2119
+ run.result?.summary ? `summary\n${truncate(run.result.summary, 1200)}` : undefined,
2120
+ ].filter(Boolean).join("\n\n"),
2121
+ surface: "custom",
2122
+ });
2123
+ });
2124
+
2125
+ registerExtensionCommand(pi, "claude-attention-list", `Advanced: list runs that currently need attention (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (_args, ctx) => {
2126
+ const attention = await syncAttentionLedger();
2127
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Attention · ${attention.active.length}`);
2128
+ sendCommandMessage(pi, {
2129
+ level: attention.active.length > 0 ? "warning" : "info",
2130
+ command: "claude-attention-list",
2131
+ title: `Attention (${attention.active.length})`,
2132
+ body: formatAttentionReport(attention.active),
2133
+ surface: "custom",
2134
+ });
2135
+ });
2136
+
2137
+ registerExtensionCommand(pi, "claude-attention-ack", `Advanced: acknowledge noisy attention for a run. Args: <runId-prefix> (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2138
+ const token = args.trim();
2139
+ if (!token) {
2140
+ showUsage(pi, "claude-attention-ack", "/claude-attention-ack <runId-prefix>");
2141
+ return;
2142
+ }
2143
+
2144
+ const attention = await syncAttentionLedger();
2145
+ const view = resolveAttentionRun(token, attention.active);
2146
+ attentionLedger = acknowledgeAttention(attentionLedger, view.run.runId);
2147
+ await persistAttentionLedger();
2148
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Attention ack ${shortId(view.run.runId)}`);
2149
+ sendCommandMessage(pi, {
2150
+ level: "success",
2151
+ command: "claude-attention-ack",
2152
+ title: `Acknowledged: ${shortId(view.run.runId)}`,
2153
+ body: [view.run.agentName, view.run.note].filter(Boolean).join("\n\n"),
2154
+ });
2155
+ });
2156
+
2157
+ registerExtensionCommand(pi, "claude-attention-snooze", `Advanced: snooze noisy attention for a run. Args: <runId-prefix> [minutes] (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2158
+ const [token, minutesToken] = args.trim().split(/\s+/, 2).filter(Boolean);
2159
+ if (!token) {
2160
+ showUsage(pi, "claude-attention-snooze", "/claude-attention-snooze <runId-prefix> [minutes]");
2161
+ return;
2162
+ }
2163
+
2164
+ const minutes = minutesToken ? Number(minutesToken) : DEFAULT_SNOOZE_MINUTES;
2165
+ if (!Number.isFinite(minutes) || minutes <= 0) {
2166
+ showUsage(pi, "claude-attention-snooze", "/claude-attention-snooze <runId-prefix> [minutes]");
2167
+ return;
2168
+ }
2169
+
2170
+ const attention = await syncAttentionLedger();
2171
+ const view = resolveAttentionRun(token, attention.active);
2172
+ const snoozedUntil = Date.now() + minutes * 60_000;
2173
+ attentionLedger = snoozeAttention(attentionLedger, view.run.runId, snoozedUntil);
2174
+ await persistAttentionLedger();
2175
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Attention snoozed ${shortId(view.run.runId)}`);
2176
+ sendCommandMessage(pi, {
2177
+ level: "success",
2178
+ command: "claude-attention-snooze",
2179
+ title: `Snoozed: ${shortId(view.run.runId)}`,
2180
+ body: `${view.run.agentName}\n\nuntil ${new Date(snoozedUntil).toLocaleTimeString()}`,
2181
+ });
2182
+ });
2183
+
2184
+ registerExtensionCommand(pi, "claude-team-spawn", `Advanced: spawn Claude teammate. Args: <name> | <prompt> | [driver] (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2185
+ const parsed = parseTeamSpawnCommandInput(args);
2186
+ if (!parsed.name || !parsed.prompt) {
2187
+ showUsage(pi, "claude-team-spawn", [
2188
+ "/claude-team-spawn <name> | <prompt>",
2189
+ "/claude-team-spawn <name> | <prompt> | <driver>",
2190
+ "Example: /claude-team-spawn reviewer | You are teammate. Reply briefly. | codex-cli",
2191
+ ].join("\n"));
2192
+ return;
2193
+ }
2194
+
2195
+ if (!noSessionMode) {
2196
+ await ensureIntercomTransportHealthy(false);
2197
+ }
2198
+ const teammate = await teams.spawnTeammate({
2199
+ name: parsed.name,
2200
+ prompt: parsed.prompt,
2201
+ driver: parsed.driver,
2202
+ cwd,
2203
+ });
2204
+ peerNameCache.add(teammate.name);
2205
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team + ${teammate.name}`);
2206
+ sendCommandMessage(pi, {
2207
+ level: "success",
2208
+ command: "claude-team-spawn",
2209
+ title: `Teammate spawned: ${teammate.name}`,
2210
+ body: [`driver ${teammate.driver ?? runtimeDriverConfig.defaultDriver}`, `state ${teammate.state}`, `session ${teammate.sessionId ?? "-"}`].join("\n"),
2211
+ });
2212
+ });
2213
+
2214
+ registerExtensionCommand(pi, "claude-team-task", `Advanced: assign task to Claude teammate. Args: <name> | <title> | <details> (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2215
+ const [name, title, details] = splitArgs(args, 3);
2216
+ if (!name || !title || !details) {
2217
+ showUsage(pi, "claude-team-task", "/claude-team-task <name> | <title> | <details>");
2218
+ return;
2219
+ }
2220
+
2221
+ const task = await teams.assignTask({
2222
+ assignee: name,
2223
+ title,
2224
+ details,
2225
+ });
2226
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Task ${shortId(task.taskId)} ${task.state}`);
2227
+ sendCommandMessage(pi, {
2228
+ level: task.state === "blocked" ? "warning" : "success",
2229
+ command: "claude-team-task",
2230
+ title: `Task ${task.state}: ${shortId(task.taskId)}`,
2231
+ body: [
2232
+ `${task.assignee} · ${task.title}`,
2233
+ task.lastReply ? `reply\n${truncate(task.lastReply, 1200)}` : undefined,
2234
+ ].filter(Boolean).join("\n\n"),
2235
+ });
2236
+ });
2237
+
2238
+ registerExtensionCommand(pi, "claude-team-message", `Advanced: message Claude teammate. Args: <name> | <message> (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2239
+ const [name, message] = splitArgs(args, 2);
2240
+ if (!name || !message) {
2241
+ showUsage(pi, "claude-team-message", "/claude-team-message <name> | <message>");
2242
+ return;
2243
+ }
2244
+
2245
+ const result = await teams.sendMessage(name, message);
2246
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team ← ${result.teammate.name}`);
2247
+ sendCommandMessage(pi, {
2248
+ level: "success",
2249
+ command: "claude-team-message",
2250
+ title: `Teammate reply: ${result.teammate.name}`,
2251
+ body: truncate(result.reply, 1200) || "<empty reply>",
2252
+ });
2253
+ });
2254
+
2255
+ registerExtensionCommand(pi, "claude-team-list", `Advanced: list Claude teammates and tasks (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (_args, ctx) => {
2256
+ const teammates = await teams.listTeammates();
2257
+ const tasks = await teams.listTasks();
2258
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team/Todo · ${teammates.length}/${tasks.length}`);
2259
+ sendCommandMessage(pi, {
2260
+ level: teammates.some((teammate) => isProblemState(teammate.state)) || tasks.some((task) => ["blocked", "cancelled"].includes(task.state)) ? "warning" : "info",
2261
+ command: "claude-team-list",
2262
+ title: `Teammates ${teammates.length} · Tasks ${tasks.length}`,
2263
+ body: [
2264
+ "Teammates",
2265
+ teammates.length > 0 ? teammates.map((teammate) => `${teammate.name} ${teammate.state} ${teammate.driver ?? "-"}`).join("\n") : "No teammates.",
2266
+ "",
2267
+ "Tasks",
2268
+ tasks.length > 0 ? tasks.map((task) => `${shortId(task.taskId)} ${task.state} ${task.assignee} ${task.title}`).join("\n") : "No tasks.",
2269
+ ].join("\n"),
2270
+ surface: "custom",
2271
+ });
2272
+ });
2273
+
2274
+ registerExtensionCommand(pi, "claude-team-stop", `Advanced: stop Claude teammate. Args: <name> (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (args, ctx) => {
2275
+ const name = args.trim();
2276
+ if (!name) {
2277
+ showUsage(pi, "claude-team-stop", "/claude-team-stop <name>");
2278
+ return;
2279
+ }
2280
+
2281
+ const teammate = await teams.stopTeammate(name);
2282
+ peerNameCache.delete(name);
2283
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Team - ${teammate.name}`);
2284
+ sendCommandMessage(pi, {
2285
+ level: "warning",
2286
+ command: "claude-team-stop",
2287
+ title: `Teammate stopped: ${teammate.name}`,
2288
+ body: `state ${teammate.state}`,
2289
+ });
2290
+ });
2291
+
2292
+ registerExtensionCommand(pi, "claude-runtime-list", `Advanced: list raw runtime sessions (set ${LEGACY_COMMANDS_ENV}=1 and ${ADVANCED_COMMANDS_ENV}=1)`, async (_args, ctx) => {
2293
+ const sessions = await runtime.list();
2294
+ await refreshDashboard(ctx, runtime, bridge, subagents, teams, dashboardState, attentionLedger, `Runtime · ${sessions.length}`);
2295
+ sendCommandMessage(pi, {
2296
+ level: sessions.some((session) => ["failed", "interrupted"].includes(session.state)) ? "warning" : "info",
2297
+ command: "claude-runtime-list",
2298
+ title: `Runtime sessions (${sessions.length})`,
2299
+ body: sessions.length > 0
2300
+ ? sessions.map((session) => `${shortId(session.sessionId)} ${session.state} ${session.name ?? "-"}`).join("\n")
2301
+ : "No runtime sessions.",
2302
+ surface: "custom",
2303
+ });
2304
+ });
2305
+ }
2306
+ }
2307
+
2308
+ function registerExtensionCommand(
2309
+ pi: ExtensionAPI,
2310
+ name: string,
2311
+ description: string,
2312
+ handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>,
2313
+ getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null,
2314
+ ): void {
2315
+ pi.registerCommand(name, {
2316
+ description,
2317
+ getArgumentCompletions,
2318
+ handler: async (args, ctx) => {
2319
+ const previousOperatorContext = operatorContextRef;
2320
+ operatorContextRef = ctx;
2321
+ try {
2322
+ await handler(args, ctx);
2323
+ } catch (error) {
2324
+ const message = error instanceof Error ? error.message : String(error);
2325
+ sendCommandMessage(pi, {
2326
+ level: "error",
2327
+ command: name,
2328
+ title: `/${name} failed`,
2329
+ body: message,
2330
+ });
2331
+ } finally {
2332
+ operatorContextRef = previousOperatorContext;
2333
+ }
2334
+ },
2335
+ });
2336
+ }
2337
+
2338
+ function sendCommandMessage(
2339
+ pi: ExtensionAPI,
2340
+ input: { level: CommandMessageLevel; title: string; body?: string; command?: string; surface?: CommandMessageSurface },
2341
+ ): void {
2342
+ if (input.surface !== "agent") {
2343
+ const ctx = operatorContextRef ?? dashboardContextRef;
2344
+ if (ctx) {
2345
+ ctx.ui.notify(formatOperatorNotification(input), notifyLevel(input.level));
2346
+ }
2347
+ return;
2348
+ }
2349
+
2350
+ pi.sendMessage(
2351
+ {
2352
+ customType: "peer-command-result",
2353
+ content: input.body ?? "",
2354
+ display: true,
2355
+ details: {
2356
+ level: input.level,
2357
+ title: input.title,
2358
+ command: input.command,
2359
+ timestamp: Date.now(),
2360
+ surface: input.surface ?? "tool",
2361
+ } satisfies CommandMessageDetails,
2362
+ },
2363
+ { triggerTurn: false },
2364
+ );
2365
+ }
2366
+
2367
+ function formatOperatorNotification(input: { title: string; body?: string; command?: string }): string {
2368
+ const parts = [`[peer] ${input.title}`];
2369
+ if (input.body?.trim()) {
2370
+ parts.push(input.body.trim());
2371
+ }
2372
+ if (input.command) {
2373
+ parts.push(`/${input.command}`);
2374
+ }
2375
+ return parts.join("\n\n");
2376
+ }
2377
+
2378
+ function notifyLevel(level: CommandMessageLevel): "info" | "warning" | "error" {
2379
+ if (level === "error") {
2380
+ return "error";
2381
+ }
2382
+ if (level === "warning") {
2383
+ return "warning";
2384
+ }
2385
+ return "info";
2386
+ }
2387
+
2388
+ function showPeerInitUserHelp(ctx: ExtensionContext | ExtensionCommandContext): void {
2389
+ ctx.ui.notify(`[peer] ${PEER_INIT_USER_HELP}`, "warning");
2390
+ }
2391
+
2392
+ function showUsage(pi: ExtensionAPI, command: string, usage: string): void {
2393
+ sendCommandMessage(pi, {
2394
+ level: "warning",
2395
+ command,
2396
+ title: `Usage: /${command}`,
2397
+ body: usage,
2398
+ surface: "custom",
2399
+ });
2400
+ }
2401
+
2402
+ async function refreshDashboard(
2403
+ ctx: ExtensionContext | ExtensionCommandContext,
2404
+ runtime: ClaudeCodeRuntime,
2405
+ bridge: ClaudeRuntimeIntercomBridge,
2406
+ subagents: ClaudeCodeSubagentBackend,
2407
+ teams: ClaudeCodeTeamsBackend,
2408
+ state: DashboardState,
2409
+ attentionLedger: AttentionLedger,
2410
+ lastEvent?: string,
2411
+ preCollected?: DashboardData,
2412
+ ): Promise<DashboardData> {
2413
+ dashboardContextRef = ctx;
2414
+ if (lastEvent) {
2415
+ recordDashboardEvent(state, lastEvent);
2416
+ } else {
2417
+ recordDashboardRefresh(state);
2418
+ }
2419
+
2420
+ const data = preCollected ?? await collectDashboardData(runtime, bridge, subagents, teams, state, attentionLedger);
2421
+
2422
+ if (!process.argv.includes("--no-session")) {
2423
+ ctx.ui.setStatus(EXTENSION_NAME, undefined);
2424
+ const newSignature = computeWidgetSignature({
2425
+ peerRows: data.snapshot.peerRows.filter(isPeerVisibleInWidget),
2426
+ transportDegraded: data.snapshot.transportDegraded,
2427
+ lastEvent: data.snapshot.lastEvent,
2428
+ });
2429
+ if (newSignature !== lastWidgetSignature) {
2430
+ lastWidgetSignature = newSignature;
2431
+ ctx.ui.setWidget("peer-dashboard", createDashboardWidget(data.snapshot));
2432
+ }
2433
+ }
2434
+ return data;
2435
+ }
2436
+
2437
+ function createDashboardWidget(snapshot: DashboardSnapshot) {
2438
+ return (_tui: unknown, theme: { fg: (color: string, text: string) => string }) => ({
2439
+ invalidate() {},
2440
+ render(width: number): string[] {
2441
+ const widgetRows = snapshot.peerRows.filter(isPeerVisibleInWidget);
2442
+ const health = getPeerFirstHealth(widgetRows, snapshot.transportDegraded);
2443
+ const summary = formatWidgetSummary(widgetRows, snapshot.transportDegraded);
2444
+ const title = `${theme.fg("accent", "Peers")} ${theme.fg("dim", summary)}`;
2445
+ const badge = health === "warning"
2446
+ ? theme.fg("warning", "● warning")
2447
+ : health === "active"
2448
+ ? theme.fg("success", "● active")
2449
+ : theme.fg("muted", "● idle");
2450
+ const lines = [joinLeftRight(title, badge, width)];
2451
+
2452
+ if (snapshot.peerRows.length === 0) {
2453
+ lines.push(truncateToWidth("no peers yet", width));
2454
+ lines.push(truncateToWidth(theme.fg("dim", "hint /peer start <task or prompt>"), width));
2455
+ } else if (widgetRows.length === 0) {
2456
+ lines.push(truncateToWidth("no active peers", width));
2457
+ lines.push(truncateToWidth(theme.fg("dim", "stopped peers in /peer dashboard"), width));
2458
+ } else {
2459
+ const rows = sortWidgetRows(widgetRows);
2460
+ const layout = createWidgetPeerLayout(widgetRows, width);
2461
+ lines.push(...renderWidgetTable(rows, layout, width, theme));
2462
+ }
2463
+
2464
+ if (snapshot.transportDegraded) {
2465
+ lines.push(truncateToWidth(theme.fg("warning", "broker offline; peers stay local"), width));
2466
+ }
2467
+
2468
+ return lines;
2469
+ },
2470
+ });
2471
+ }
2472
+
2473
+ type WidgetTheme = { fg: (color: string, text: string) => string };
2474
+
2475
+ interface WidgetColumn<T> {
2476
+ label: string;
2477
+ width?: number;
2478
+ color?: string | ((row: T) => string);
2479
+ value: (row: T) => string;
2480
+ }
2481
+
2482
+ interface SizedWidgetColumn<T> extends WidgetColumn<T> {
2483
+ key: string;
2484
+ minWidth: number;
2485
+ maxWidth: number;
2486
+ dropPriority?: number;
2487
+ }
2488
+
2489
+ function createWidgetPeerLayout(rows: PeerActivityRow[], width: number): Array<WidgetColumn<PeerActivityRow>> {
2490
+ const distinctDrivers = new Set(rows.map((row) => row.driver).filter(Boolean)).size;
2491
+ const minActivityWidth = width >= 96 ? 28 : width >= 72 ? 20 : 12;
2492
+ const columns: Array<SizedWidgetColumn<PeerActivityRow> | undefined> = [
2493
+ { key: "peer", label: "peer", minWidth: 8, maxWidth: 24, value: (row) => row.name },
2494
+ { key: "state", label: "state", minWidth: 5, maxWidth: 9, color: widgetStateColor, value: (row) => row.state },
2495
+ distinctDrivers > 1
2496
+ ? { key: "driver", label: "driver", minWidth: 6, maxWidth: 6, dropPriority: 0, color: "dim", value: (row) => row.driver ? compactDriver(row.driver) : "-" }
2497
+ : undefined,
2498
+ rows.some((row) => row.model)
2499
+ ? { key: "model", label: "model", minWidth: 7, maxWidth: 16, dropPriority: 2, color: "dim", value: (row) => row.model ? compactModel(row.model) : "-" }
2500
+ : undefined,
2501
+ rows.some(hasPeerContextUsage)
2502
+ ? { key: "ctx", label: "ctx", minWidth: 4, maxWidth: 6, dropPriority: 1, color: contextUsageColor, value: formatWidgetContextUsage }
2503
+ : undefined,
2504
+ { key: "updated", label: "updated", minWidth: 7, maxWidth: 7, dropPriority: 3, color: "dim", value: (row) => formatWidgetUpdateTime(row.lastUpdateAt) },
2505
+ ];
2506
+
2507
+ let visible = columns.filter((column): column is SizedWidgetColumn<PeerActivityRow> => Boolean(column));
2508
+ while (visible.some((column) => column.dropPriority != null) && widgetTableMinWidth(visible, minActivityWidth) > width) {
2509
+ const dropPriority = Math.min(...visible.filter((column) => column.dropPriority != null).map((column) => column.dropPriority ?? 0));
2510
+ const index = visible.findIndex((column) => column.dropPriority === dropPriority);
2511
+ if (index < 0) {
2512
+ break;
2513
+ }
2514
+ visible = visible.filter((_, currentIndex) => currentIndex !== index);
2515
+ }
2516
+
2517
+ const widths = new Map<string, number>(visible.map((column) => [column.key, column.minWidth]));
2518
+ const gapWidth = widgetTableGapWidth(visible.length + 1);
2519
+ const reservedActivityWidth = widgetTableMinWidth(visible, minActivityWidth) <= width ? minActivityWidth : 1;
2520
+ let extraWidth = Math.max(0, width - gapWidth - reservedActivityWidth - sumWidgetColumnWidths(widths));
2521
+
2522
+ for (const key of ["peer", "model", "state", "driver", "ctx", "updated"]) {
2523
+ const column = visible.find((item) => item.key === key);
2524
+ if (!column || extraWidth <= 0) {
2525
+ continue;
2526
+ }
2527
+ const desiredWidth = measureWidgetColumnWidth(column, rows);
2528
+ const currentWidth = widths.get(column.key) ?? column.minWidth;
2529
+ const nextWidth = Math.min(column.maxWidth, desiredWidth);
2530
+ const growth = Math.min(extraWidth, Math.max(0, nextWidth - currentWidth));
2531
+ if (growth > 0) {
2532
+ widths.set(column.key, currentWidth + growth);
2533
+ extraWidth -= growth;
2534
+ }
2535
+ }
2536
+
2537
+ const activityWidth = Math.max(1, width - gapWidth - sumWidgetColumnWidths(widths));
2538
+ return [
2539
+ ...visible.map((column) => ({
2540
+ label: column.label,
2541
+ width: widths.get(column.key) ?? column.minWidth,
2542
+ color: column.color,
2543
+ value: column.value,
2544
+ })),
2545
+ { label: "activity", width: activityWidth, value: (row) => row.activity },
2546
+ ];
2547
+ }
2548
+
2549
+ function renderWidgetTable<T>(rows: T[], columns: Array<WidgetColumn<T>>, width: number, theme: WidgetTheme): string[] {
2550
+ return [
2551
+ truncateToWidth(theme.fg("dim", columns.map((column) => formatWidgetCell(column.label, column.width)).join(" ")), width),
2552
+ ...rows.map((row) => truncateToWidth(columns.map((column) => {
2553
+ const color = typeof column.color === "function" ? column.color(row) : column.color;
2554
+ const cell = formatWidgetCell(column.value(row), column.width);
2555
+ return color ? theme.fg(color, cell) : cell;
2556
+ }).join(" "), width)),
2557
+ ];
2558
+ }
2559
+
2560
+ function formatWidgetCell(value: string, width?: number): string {
2561
+ if (width == null) {
2562
+ return value;
2563
+ }
2564
+ return truncateToWidth(value, width).padEnd(width);
2565
+ }
2566
+
2567
+ function formatWidgetSummary(rows: PeerActivityRow[], transportDegraded: boolean): string {
2568
+ const busy = rows.filter((row) => row.state === "busy").length;
2569
+ const issues = rows.filter((row) => ["error", "offline", "waiting"].includes(row.state)).length + (transportDegraded ? 1 : 0);
2570
+ return [
2571
+ `${rows.length} peer${rows.length === 1 ? "" : "s"}`,
2572
+ busy > 0 ? `${busy} busy` : undefined,
2573
+ issues > 0 ? `${issues} issue${issues === 1 ? "" : "s"}` : undefined,
2574
+ ].filter((part): part is string => Boolean(part)).join(" · ");
2575
+ }
2576
+
2577
+ function sortWidgetRows(rows: PeerActivityRow[]): PeerActivityRow[] {
2578
+ const priority = (row: PeerActivityRow) => {
2579
+ if (["error", "waiting", "offline"].includes(row.state)) {
2580
+ return 0;
2581
+ }
2582
+ if (row.state === "busy") {
2583
+ return 1;
2584
+ }
2585
+ return 2;
2586
+ };
2587
+ return [...rows].sort((left, right) => priority(left) - priority(right) || left.name.localeCompare(right.name));
2588
+ }
2589
+
2590
+ function widgetStateColor(row: PeerActivityRow): string {
2591
+ if (row.state === "busy") {
2592
+ return "success";
2593
+ }
2594
+ if (["waiting", "offline"].includes(row.state)) {
2595
+ return "warning";
2596
+ }
2597
+ if (row.state === "error") {
2598
+ return "error";
2599
+ }
2600
+ return "text";
2601
+ }
2602
+
2603
+ async function collectDashboardData(
2604
+ runtime: ClaudeCodeRuntime,
2605
+ bridge: ClaudeRuntimeIntercomBridge,
2606
+ subagents: ClaudeCodeSubagentBackend,
2607
+ teams: ClaudeCodeTeamsBackend,
2608
+ state: DashboardState,
2609
+ attentionLedger: AttentionLedger,
2610
+ ): Promise<DashboardData> {
2611
+ const [sessions, peers, runs, teammates, tasks, transportStatus] = await Promise.all([
2612
+ runtime.list(),
2613
+ bridge.listPeers(),
2614
+ subagents.listRuns(),
2615
+ teams.listTeammates(),
2616
+ teams.listTasks(),
2617
+ bridge.transportStatus(),
2618
+ ]);
2619
+ const peerRows = await Promise.all(
2620
+ peers.map(async (peer) => buildPeerActivityRow(peer, await runtime.tail(peer.sessionId, 12).catch(() => []))),
2621
+ );
2622
+ const attention = listAttentionViews(attentionLedger, runs, Date.now());
2623
+ const intercomLive = bridge.hasTransport();
2624
+ const hasNonStoppedPeers = peers.some((peer) => peer.state !== "stopped");
2625
+ const transportDegraded = hasNonStoppedPeers && (!intercomLive || shouldRebindTransport(transportStatus));
2626
+
2627
+ return {
2628
+ snapshot: {
2629
+ sessions: sessions.length,
2630
+ peers: peers.length,
2631
+ peerBusy: peers.filter((peer) => ["busy", "starting"].includes(peer.state)).length,
2632
+ peerIssues: peers.filter((peer) => isProblemState(peer.state)).length,
2633
+ peerRows,
2634
+ intercomLive,
2635
+ intercomBoundPeers: transportStatus?.boundPeers ?? 0,
2636
+ intercomConnectedPeers: transportStatus?.connectedPeers ?? 0,
2637
+ transportDegraded,
2638
+ runs: runs.length,
2639
+ runActive: runs.filter((run) => ["queued", "starting", "running"].includes(run.state)).length,
2640
+ runAttention: attention.length,
2641
+ runIssues: runs.filter((run) => ["failed", "interrupted"].includes(run.state)).length,
2642
+ teammates: teammates.length,
2643
+ teammateBusy: teammates.filter((teammate) => ["starting", "busy"].includes(teammate.state)).length,
2644
+ teammateIssues: teammates.filter((teammate) => isProblemState(teammate.state)).length,
2645
+ tasks: tasks.length,
2646
+ openTasks: tasks.filter((task) => !["done", "cancelled"].includes(task.state)).length,
2647
+ taskIssues: tasks.filter((task) => ["blocked", "cancelled"].includes(task.state)).length,
2648
+ lastEvent: state.lastEvent,
2649
+ lastEventAt: state.lastEventAt,
2650
+ lastRefreshedAt: state.lastRefreshedAt,
2651
+ },
2652
+ sessions,
2653
+ peers,
2654
+ runs,
2655
+ teammates,
2656
+ tasks,
2657
+ attention,
2658
+ };
2659
+ }
2660
+
2661
+ function completePeerCommand(prefix: string, names: Set<string>): AutocompleteItem[] | null {
2662
+ const trimmedStart = prefix.trimStart();
2663
+ const [subcommand = "", ...restParts] = trimmedStart.split(/\s+/);
2664
+ const endsWithSpace = /\s$/.test(trimmedStart);
2665
+ const subcommands = ["init", "dashboard", "start", "ask", "send", "list", "models", "history", "interrupt", "stop", "help"];
2666
+
2667
+ if (!subcommand || (!endsWithSpace && restParts.length === 0)) {
2668
+ const matches = subcommands
2669
+ .filter((item) => item.startsWith(subcommand.toLowerCase()))
2670
+ .map((item) => ({ value: `${item} `, label: item }));
2671
+ return matches.length > 0 ? matches : null;
2672
+ }
2673
+
2674
+ if (["ask", "send"].includes(subcommand)) {
2675
+ return completePeerName(restParts.join(" "), names, true);
2676
+ }
2677
+ if (["history", "interrupt", "stop"].includes(subcommand)) {
2678
+ return completePeerName(restParts.join(" "), names, false);
2679
+ }
2680
+ if (subcommand === "models") {
2681
+ const trimmed = restParts.join(" ").trim();
2682
+ const items = ["claude-sdk", "codex-cli"]
2683
+ .filter((driver) => trimmed.length === 0 || driver.startsWith(trimmed))
2684
+ .map((driver) => ({ value: driver, label: driver }));
2685
+ return items.length > 0 ? items : null;
2686
+ }
2687
+ return null;
2688
+ }
2689
+
2690
+ function completePeerName(prefix: string, names: Set<string>, appendPipe: boolean): AutocompleteItem[] | null {
2691
+ if (prefix.includes("|")) {
2692
+ return null;
2693
+ }
2694
+
2695
+ const trimmed = prefix.trim();
2696
+ const items = [...names]
2697
+ .sort((a, b) => a.localeCompare(b))
2698
+ .filter((name) => trimmed.length === 0 || name.startsWith(trimmed))
2699
+ .map((name) => ({
2700
+ value: appendPipe ? `${name} | ` : name,
2701
+ label: appendPipe ? `${name} | <message>` : name,
2702
+ }));
2703
+
2704
+ return items.length > 0 ? items : null;
2705
+ }
2706
+
2707
+ function hasPlaceholderToken(...values: Array<string | undefined>): boolean {
2708
+ return values.some((value) => Boolean(value && /^<[^>]+>$/.test(value.trim())));
2709
+ }
2710
+
2711
+ function syncNameCache(cache: Set<string>, names: string[]): void {
2712
+ cache.clear();
2713
+ for (const name of names) {
2714
+ cache.add(name);
2715
+ }
2716
+ }
2717
+
2718
+ function splitArgs(args: string, expected: number): string[] {
2719
+ const parts = args.split("|").map((part) => part.trim()).filter(Boolean);
2720
+ return parts.slice(0, expected);
2721
+ }
2722
+
2723
+ function truncate(value: string, max = 240): string {
2724
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
2725
+ }
2726
+
2727
+ function shortId(value: string, size = 8): string {
2728
+ return value.length > size ? value.slice(0, size) : value;
2729
+ }
2730
+
2731
+ function formatTime(timestamp: number): string {
2732
+ const date = new Date(timestamp);
2733
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
2734
+ }
2735
+
2736
+ function joinLeftRight(left: string, right: string, width: number): string {
2737
+ const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
2738
+ return truncateToWidth(`${left}${" ".repeat(gap)}${right}`, width, "");
2739
+ }
2740
+
2741
+ function formatPeerList(rows: PeerActivityRow[]): string {
2742
+ if (rows.length === 0) {
2743
+ return [
2744
+ "No peers yet.",
2745
+ "hint /peer start <task or prompt>",
2746
+ ].join("\n");
2747
+ }
2748
+
2749
+ const hasDriver = rows.some((row) => row.driver);
2750
+ const hasModel = rows.some((row) => row.model);
2751
+ const hasContext = rows.some(hasPeerContextUsage);
2752
+ const headers = [
2753
+ "name",
2754
+ "state",
2755
+ ...(hasDriver ? ["driver"] : []),
2756
+ ...(hasModel ? ["model"] : []),
2757
+ ...(hasContext ? ["ctx"] : []),
2758
+ "activity",
2759
+ "last update",
2760
+ ];
2761
+
2762
+ return formatTable(
2763
+ headers,
2764
+ rows.map((row) => [
2765
+ row.name,
2766
+ row.state,
2767
+ ...(hasDriver ? [row.driver ?? "-"] : []),
2768
+ ...(hasModel ? [row.model ?? "-"] : []),
2769
+ ...(hasContext ? [formatPeerContextUsage(row)] : []),
2770
+ row.activity,
2771
+ formatIsoTime(row.lastUpdateAt),
2772
+ ]),
2773
+ );
2774
+ }
2775
+
2776
+ function formatPeerFirstDashboardReport(data: DashboardData): string {
2777
+ const { snapshot } = data;
2778
+ const health = getPeerFirstHealth(snapshot.peerRows, snapshot.transportDegraded);
2779
+ const sections = [
2780
+ `health ${health}`,
2781
+ `updated ${formatTime(snapshot.lastRefreshedAt)}`,
2782
+ `event ${snapshot.lastEvent}`,
2783
+ ];
2784
+
2785
+ if (snapshot.transportDegraded) {
2786
+ sections.push(
2787
+ "",
2788
+ "Transport",
2789
+ "status broker offline",
2790
+ "impact existing peers remain local; live broker routing unavailable",
2791
+ );
2792
+ }
2793
+
2794
+ sections.push("", "Peers", formatPeerList(snapshot.peerRows));
2795
+ return sections.join("\n");
2796
+ }
2797
+
2798
+ function formatAdvancedDashboardReport(data: DashboardData): string {
2799
+ const now = Date.now();
2800
+ const { snapshot, runs, teammates, tasks, attention } = data;
2801
+ const health = getPeerFirstHealth(snapshot.peerRows, snapshot.transportDegraded);
2802
+ const sections = [
2803
+ `health ${health}`,
2804
+ `updated ${formatTime(snapshot.lastRefreshedAt)}`,
2805
+ `event ${snapshot.lastEvent}`,
2806
+ ];
2807
+
2808
+ if (snapshot.transportDegraded) {
2809
+ sections.push(
2810
+ "",
2811
+ "Transport",
2812
+ "status broker offline",
2813
+ "impact existing peers remain local; live broker routing unavailable",
2814
+ );
2815
+ }
2816
+
2817
+ sections.push(
2818
+ "",
2819
+ "Peers",
2820
+ formatPeerList(snapshot.peerRows),
2821
+ "",
2822
+ "Advanced diagnostics",
2823
+ `runtime sessions ${snapshot.sessions} retained`,
2824
+ `runs ${snapshot.runs} retained`,
2825
+ `attention ${attention.length} active`,
2826
+ `teammates ${snapshot.teammates} retained`,
2827
+ `tasks ${snapshot.openTasks} open`,
2828
+ `broker ${snapshot.transportDegraded ? "offline" : snapshot.intercomLive ? "live" : "local only"}`,
2829
+ );
2830
+
2831
+ if (runs.length > 0) {
2832
+ sections.push(
2833
+ "",
2834
+ "Runs",
2835
+ formatTable(
2836
+ ["run", "state", "driver", "agent", "note"],
2837
+ runs.slice(0, 8).map((run) => [
2838
+ shortId(run.runId, 12),
2839
+ run.state,
2840
+ run.driver ?? "-",
2841
+ run.agentName,
2842
+ truncate(run.note ?? run.result?.summary ?? "", 48),
2843
+ ]),
2844
+ ),
2845
+ );
2846
+ }
2847
+
2848
+ if (attention.length > 0) {
2849
+ sections.push(
2850
+ "",
2851
+ "Attention",
2852
+ formatTable(
2853
+ ["run", "state", "agent", "note"],
2854
+ attention.map((view) => [
2855
+ shortId(view.run.runId, 12),
2856
+ describeAttentionState(view, now),
2857
+ view.run.agentName,
2858
+ truncate(view.run.note ?? "", 72),
2859
+ ]),
2860
+ ),
2861
+ );
2862
+ }
2863
+
2864
+ if (teammates.length > 0) {
2865
+ sections.push(
2866
+ "",
2867
+ "Teammates",
2868
+ formatTable(
2869
+ ["name", "state", "driver", "session"],
2870
+ teammates.map((teammate) => [teammate.name, teammate.state, teammate.driver ?? "-", shortId(teammate.sessionId ?? "-", 12)]),
2871
+ ),
2872
+ );
2873
+ }
2874
+
2875
+ if (tasks.length > 0) {
2876
+ sections.push(
2877
+ "",
2878
+ "Tasks",
2879
+ formatTable(
2880
+ ["task", "state", "assignee", "title"],
2881
+ tasks.slice(0, 8).map((task) => [shortId(task.taskId, 12), task.state, task.assignee, task.title]),
2882
+ ),
2883
+ );
2884
+ }
2885
+
2886
+ return sections.join("\n");
2887
+ }
2888
+
2889
+ function formatAttentionReport(attention: AttentionView[]): string {
2890
+ if (attention.length === 0) {
2891
+ return "No active attention runs.";
2892
+ }
2893
+ const now = Date.now();
2894
+ return formatTable(
2895
+ ["run", "state", "agent", "note"],
2896
+ attention.map((view) => [
2897
+ shortId(view.run.runId, 12),
2898
+ describeAttentionState(view, now),
2899
+ view.run.agentName,
2900
+ truncate(view.run.note ?? "", 72),
2901
+ ]),
2902
+ );
2903
+ }
2904
+
2905
+ function parsePeerModelsArgs(args: string): { driver?: "claude-sdk" | "codex-cli"; verbose: boolean } {
2906
+ let driver: "claude-sdk" | "codex-cli" | undefined;
2907
+ let verbose = false;
2908
+ for (const token of args.trim().split(/\s+/).filter(Boolean)) {
2909
+ const parsedDriver = parseRuntimeDriverName(token);
2910
+ if (parsedDriver) {
2911
+ driver = parsedDriver;
2912
+ continue;
2913
+ }
2914
+ if (["all", "advanced", "verbose", "--all", "--advanced", "--verbose"].includes(token.toLowerCase())) {
2915
+ verbose = true;
2916
+ continue;
2917
+ }
2918
+ throw new Error("usage: /peer models [claude-sdk|codex-cli] [all|advanced|verbose]");
2919
+ }
2920
+ return { driver, verbose };
2921
+ }
2922
+
2923
+ function formatModelCatalogReport(catalogs: RuntimeDriverModelCatalog[], options: { verbose?: boolean } = {}): string {
2924
+ return catalogs.map((catalog) => {
2925
+ const verbose = options.verbose === true;
2926
+ const rows = verbose ? formatFullModelCatalogRows(catalog) : formatRecommendedModelCatalogRows(catalog);
2927
+ const sections = [
2928
+ `${catalog.driver} models`,
2929
+ `provider ${catalog.provider}`,
2930
+ `default ${catalog.defaultModel}`,
2931
+ `aliases ${formatModelAliases(catalog.aliases)}`,
2932
+ `cli ${catalog.flag}`,
2933
+ `source ${catalog.source}`,
2934
+ "columns context window=input token capacity; max output=maximum generated output tokens",
2935
+ "guidance advisory; actual model availability depends on the installed runtime, account, region, and provider rollout",
2936
+ "",
2937
+ verbose
2938
+ ? formatTable(["model", "name", "context window", "max output", "reasoning", "$/M in/out"], rows)
2939
+ : formatTable(["alias", "model", "name", "best for", "context window", "max output", "$/M in/out"], rows),
2940
+ ];
2941
+ if (!verbose) {
2942
+ sections.push(`Use /peer models ${catalog.driver} all or runtime_models({ driver: "${catalog.driver}", verbose: true }) for the full catalog.`);
2943
+ }
2944
+ return sections.join("\n");
2945
+ }).join("\n\n");
2946
+ }
2947
+
2948
+ function formatRecommendedModelCatalogRows(catalog: RuntimeDriverModelCatalog): string[][] {
2949
+ return catalog.recommendations.map((recommendation) => {
2950
+ const model = catalog.models.find((entry) => entry.id === recommendation.model);
2951
+ return [
2952
+ recommendation.alias,
2953
+ recommendation.model === catalog.defaultModel ? `${recommendation.model} *` : recommendation.model,
2954
+ model?.name ?? "-",
2955
+ recommendation.useCase,
2956
+ model ? String(model.contextWindow) : "-",
2957
+ model ? String(model.maxTokens) : "-",
2958
+ model ? `$${model.inputCostPerMillion}/$${model.outputCostPerMillion}` : "-",
2959
+ ];
2960
+ });
2961
+ }
2962
+
2963
+ function formatFullModelCatalogRows(catalog: RuntimeDriverModelCatalog): string[][] {
2964
+ return catalog.models.map((model) => [
2965
+ model.id === catalog.defaultModel ? `${model.id} *` : model.id,
2966
+ model.name,
2967
+ String(model.contextWindow),
2968
+ String(model.maxTokens),
2969
+ model.reasoning ? "yes" : "no",
2970
+ `$${model.inputCostPerMillion}/$${model.outputCostPerMillion}`,
2971
+ ]);
2972
+ }
2973
+
2974
+ function formatModelAliases(aliases: Record<string, string>): string {
2975
+ const entries = Object.entries(aliases);
2976
+ if (entries.length === 0) {
2977
+ return "-";
2978
+ }
2979
+ return entries.map(([alias, target]) => `${alias}->${target}`).join(", ");
2980
+ }
2981
+
2982
+ function parseExtensionLogInput(params: unknown): ExtensionLogEntryInput {
2983
+ const input = (params ?? {}) as Record<string, unknown>;
2984
+ const category = parseStringEnum(input.category, ["ux", "agent-guidance", "tooling", "runtime", "model-selection", "docs", "bug", "other"], "category") ?? "other";
2985
+ const severity = parseStringEnum(input.severity, ["low", "medium", "high"], "severity") ?? "medium";
2986
+ const summary = cleanLogText(input.summary);
2987
+ if (!summary) {
2988
+ throw new Error("summary is required");
2989
+ }
2990
+ const files = Array.isArray(input.files)
2991
+ ? input.files.map((file) => cleanLogText(file)).filter(Boolean)
2992
+ : [];
2993
+ return {
2994
+ category,
2995
+ severity,
2996
+ summary,
2997
+ observed: cleanOptionalLogText(input.observed),
2998
+ expected: cleanOptionalLogText(input.expected),
2999
+ reproduction: cleanOptionalLogText(input.reproduction),
3000
+ suggestedFix: cleanOptionalLogText(input.suggestedFix),
3001
+ relatedCommand: cleanOptionalLogText(input.relatedCommand),
3002
+ relatedTool: cleanOptionalLogText(input.relatedTool),
3003
+ files,
3004
+ };
3005
+ }
3006
+
3007
+ function parseStringEnum<const T extends readonly string[]>(value: unknown, allowed: T, field: string): T[number] | undefined {
3008
+ if (value == null) {
3009
+ return undefined;
3010
+ }
3011
+ if (typeof value !== "string") {
3012
+ throw new Error(`${field} must be a string`);
3013
+ }
3014
+ const trimmed = value.trim();
3015
+ if ((allowed as readonly string[]).includes(trimmed)) {
3016
+ return trimmed as T[number];
3017
+ }
3018
+ throw new Error(`${field} must be one of ${allowed.join(", ")}`);
3019
+ }
3020
+
3021
+ function cleanOptionalLogText(value: unknown): string | undefined {
3022
+ const cleaned = cleanLogText(value);
3023
+ return cleaned || undefined;
3024
+ }
3025
+
3026
+ function cleanLogText(value: unknown): string {
3027
+ if (typeof value !== "string") {
3028
+ return "";
3029
+ }
3030
+ return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").trim();
3031
+ }
3032
+
3033
+ function formatExtensionLogEntry(input: ExtensionLogEntryInput): string {
3034
+ const lines = [
3035
+ `## ${new Date().toISOString()} - ${input.category} - ${input.severity}`,
3036
+ "",
3037
+ `Summary: ${input.summary}`,
3038
+ ];
3039
+ addLogSection(lines, "Observed", input.observed);
3040
+ addLogSection(lines, "Expected", input.expected);
3041
+ addLogSection(lines, "Reproduction", input.reproduction);
3042
+ addLogSection(lines, "Suggested fix", input.suggestedFix);
3043
+
3044
+ const related = [
3045
+ input.relatedCommand ? `- command: ${input.relatedCommand}` : undefined,
3046
+ input.relatedTool ? `- tool: ${input.relatedTool}` : undefined,
3047
+ ...input.files.map((file) => `- file: ${file}`),
3048
+ ].filter(Boolean);
3049
+ if (related.length > 0) {
3050
+ lines.push("", "Related:", ...related);
3051
+ }
3052
+
3053
+ return `${lines.join("\n")}\n\n`;
3054
+ }
3055
+
3056
+ function addLogSection(lines: string[], title: string, value?: string): void {
3057
+ if (!value) {
3058
+ return;
3059
+ }
3060
+ lines.push("", `${title}:`, value);
3061
+ }
3062
+
3063
+ function formatTable(headers: string[], rows: string[][]): string {
3064
+ const matrix = [headers, ...rows].map((row) => row.map((cell) => cell ?? ""));
3065
+ const widths = headers.map((_, index) => Math.max(...matrix.map((row) => row[index]?.length ?? 0)));
3066
+ const formatRow = (row: string[]) => row.map((cell, index) => (cell ?? "").padEnd(widths[index] ?? 0)).join(" ").trimEnd();
3067
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
3068
+ return [formatRow(headers), separator, ...rows.map(formatRow)].join("\n");
3069
+ }
3070
+
3071
+ function formatIsoTime(value: string): string {
3072
+ return formatTime(new Date(value).getTime());
3073
+ }
3074
+
3075
+ function formatWidgetUpdateTime(value: string): string {
3076
+ return formatIsoTime(value).slice(0, 5);
3077
+ }
3078
+
3079
+ function measureWidgetColumnWidth<T>(column: SizedWidgetColumn<T>, rows: T[]): number {
3080
+ return Math.min(
3081
+ column.maxWidth,
3082
+ Math.max(column.minWidth, visibleWidth(column.label), ...rows.map((row) => visibleWidth(column.value(row)))),
3083
+ );
3084
+ }
3085
+
3086
+ function sumWidgetColumnWidths(widths: Map<string, number>): number {
3087
+ return [...widths.values()].reduce((total, value) => total + value, 0);
3088
+ }
3089
+
3090
+ function widgetTableGapWidth(columns: number): number {
3091
+ return Math.max(0, (columns - 1) * 2);
3092
+ }
3093
+
3094
+ function widgetTableMinWidth<T>(columns: Array<SizedWidgetColumn<T>>, minActivityWidth: number): number {
3095
+ return columns.reduce((total, column) => total + column.minWidth, minActivityWidth) + widgetTableGapWidth(columns.length + 1);
3096
+ }
3097
+
3098
+ function compactModel(model: string): string {
3099
+ const stripped = model.replace(/^claude-/, "");
3100
+ return stripped.length > 12 ? `${stripped.slice(0, 11)}…` : stripped;
3101
+ }
3102
+
3103
+ function compactDriver(driver: string): string {
3104
+ switch (driver) {
3105
+ case "claude-sdk":
3106
+ return "claude";
3107
+ case "codex-cli":
3108
+ return "codex";
3109
+ default:
3110
+ return driver.length > 7 ? `${driver.slice(0, 6)}…` : driver;
3111
+ }
3112
+ }
3113
+
3114
+ function hasPeerContextUsage(row: PeerActivityRow): boolean {
3115
+ return row.contextPercentage != null;
3116
+ }
3117
+
3118
+ function formatPeerContextUsage(row: Pick<PeerActivityRow, "contextPercentage">): string {
3119
+ if (row.contextPercentage == null) {
3120
+ return "-";
3121
+ }
3122
+ const percentage = row.contextPercentage > 0 && row.contextPercentage < 1
3123
+ ? "<1"
3124
+ : String(Math.round(row.contextPercentage));
3125
+ return `ctx ${percentage}%`;
3126
+ }
3127
+
3128
+ function formatWidgetContextUsage(row: Pick<PeerActivityRow, "contextPercentage">): string {
3129
+ if (row.contextPercentage == null) {
3130
+ return "-";
3131
+ }
3132
+ if (row.contextPercentage > 999) {
3133
+ return ">999%";
3134
+ }
3135
+ const percentage = row.contextPercentage > 0 && row.contextPercentage < 1
3136
+ ? "<1"
3137
+ : String(Math.round(row.contextPercentage));
3138
+ return `${percentage}%`;
3139
+ }
3140
+
3141
+ function contextUsageColor(row: Pick<PeerActivityRow, "contextPercentage">): string {
3142
+ if (row.contextPercentage == null) {
3143
+ return "dim";
3144
+ }
3145
+ if (row.contextPercentage >= 100) {
3146
+ return "error";
3147
+ }
3148
+ if (row.contextPercentage >= 80) {
3149
+ return "warning";
3150
+ }
3151
+ return "dim";
3152
+ }
3153
+
3154
+ function levelColor(level: CommandMessageLevel): string {
3155
+ switch (level) {
3156
+ case "success":
3157
+ return "success";
3158
+ case "warning":
3159
+ return "warning";
3160
+ case "error":
3161
+ return "error";
3162
+ default:
3163
+ return "text";
3164
+ }
3165
+ }
3166
+
3167
+ function commandMessageBg(surface: CommandMessageSurface, level: CommandMessageLevel): string {
3168
+ if (surface === "custom" || surface === "agent") {
3169
+ return "toolPendingBg";
3170
+ }
3171
+ switch (level) {
3172
+ case "success":
3173
+ return "toolSuccessBg";
3174
+ case "error":
3175
+ return "toolErrorBg";
3176
+ default:
3177
+ return "toolPendingBg";
3178
+ }
3179
+ }
3180
+
3181
+ function runStateLevel(state: string): CommandMessageLevel {
3182
+ if (["completed", "idle"].includes(state)) {
3183
+ return "success";
3184
+ }
3185
+ if (["failed"].includes(state)) {
3186
+ return "error";
3187
+ }
3188
+ if (["interrupted", "stopped", "queued", "starting", "running"].includes(state)) {
3189
+ return "warning";
3190
+ }
3191
+ return "info";
3192
+ }
3193
+
3194
+ function isProblemState(state: string): boolean {
3195
+ return ["errored", "disconnected", "interrupted", "failed"].includes(state);
3196
+ }
3197
+
3198
+ function resolveSubagentRunId(token: string, runs: Array<{ runId: string }>): string {
3199
+ const matches = runs.filter((run) => run.runId.startsWith(token));
3200
+ if (matches.length === 1) {
3201
+ return matches[0]!.runId;
3202
+ }
3203
+ if (matches.length > 1) {
3204
+ throw new Error(`Ambiguous run id prefix ${token}`);
3205
+ }
3206
+ throw new Error(`Unknown run ${token}`);
3207
+ }
3208
+
3209
+ function resolveAttentionRun(token: string, attention: AttentionView[]): AttentionView {
3210
+ const matches = attention.filter((view) => view.run.runId.startsWith(token));
3211
+ if (matches.length === 1) {
3212
+ return matches[0]!;
3213
+ }
3214
+ if (matches.length > 1) {
3215
+ throw new Error(`Ambiguous run id prefix ${token}`);
3216
+ }
3217
+ throw new Error(`No active attention run matches ${token}`);
3218
+ }