@agentuity/coder 2.0.6 → 2.0.8

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 (113) hide show
  1. package/AGENTS.md +43 -0
  2. package/README.md +75 -37
  3. package/dist/index.d.ts +1 -3
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -1577
  6. package/dist/index.js.map +1 -1
  7. package/package.json +9 -12
  8. package/src/index.ts +51 -1885
  9. package/dist/chain-preview.d.ts +0 -55
  10. package/dist/chain-preview.d.ts.map +0 -1
  11. package/dist/chain-preview.js +0 -472
  12. package/dist/chain-preview.js.map +0 -1
  13. package/dist/client.d.ts +0 -43
  14. package/dist/client.d.ts.map +0 -1
  15. package/dist/client.js +0 -405
  16. package/dist/client.js.map +0 -1
  17. package/dist/commands.d.ts +0 -22
  18. package/dist/commands.d.ts.map +0 -1
  19. package/dist/commands.js +0 -99
  20. package/dist/commands.js.map +0 -1
  21. package/dist/footer.d.ts +0 -34
  22. package/dist/footer.d.ts.map +0 -1
  23. package/dist/footer.js +0 -249
  24. package/dist/footer.js.map +0 -1
  25. package/dist/handlers.d.ts +0 -24
  26. package/dist/handlers.d.ts.map +0 -1
  27. package/dist/handlers.js +0 -83
  28. package/dist/handlers.js.map +0 -1
  29. package/dist/hub-overlay-state.d.ts +0 -31
  30. package/dist/hub-overlay-state.d.ts.map +0 -1
  31. package/dist/hub-overlay-state.js +0 -78
  32. package/dist/hub-overlay-state.js.map +0 -1
  33. package/dist/hub-overlay.d.ts +0 -146
  34. package/dist/hub-overlay.d.ts.map +0 -1
  35. package/dist/hub-overlay.js +0 -2354
  36. package/dist/hub-overlay.js.map +0 -1
  37. package/dist/native-remote-ui-context.d.ts +0 -5
  38. package/dist/native-remote-ui-context.d.ts.map +0 -1
  39. package/dist/native-remote-ui-context.js +0 -30
  40. package/dist/native-remote-ui-context.js.map +0 -1
  41. package/dist/output-viewer.d.ts +0 -49
  42. package/dist/output-viewer.d.ts.map +0 -1
  43. package/dist/output-viewer.js +0 -389
  44. package/dist/output-viewer.js.map +0 -1
  45. package/dist/overlay.d.ts +0 -40
  46. package/dist/overlay.d.ts.map +0 -1
  47. package/dist/overlay.js +0 -225
  48. package/dist/overlay.js.map +0 -1
  49. package/dist/protocol.d.ts +0 -605
  50. package/dist/protocol.d.ts.map +0 -1
  51. package/dist/protocol.js +0 -4
  52. package/dist/protocol.js.map +0 -1
  53. package/dist/remote-lifecycle.d.ts +0 -61
  54. package/dist/remote-lifecycle.d.ts.map +0 -1
  55. package/dist/remote-lifecycle.js +0 -190
  56. package/dist/remote-lifecycle.js.map +0 -1
  57. package/dist/remote-session.d.ts +0 -128
  58. package/dist/remote-session.d.ts.map +0 -1
  59. package/dist/remote-session.js +0 -876
  60. package/dist/remote-session.js.map +0 -1
  61. package/dist/remote-tui.d.ts +0 -40
  62. package/dist/remote-tui.d.ts.map +0 -1
  63. package/dist/remote-tui.js +0 -867
  64. package/dist/remote-tui.js.map +0 -1
  65. package/dist/remote-ui-handler.d.ts +0 -5
  66. package/dist/remote-ui-handler.d.ts.map +0 -1
  67. package/dist/remote-ui-handler.js +0 -53
  68. package/dist/remote-ui-handler.js.map +0 -1
  69. package/dist/renderers.d.ts +0 -34
  70. package/dist/renderers.d.ts.map +0 -1
  71. package/dist/renderers.js +0 -669
  72. package/dist/renderers.js.map +0 -1
  73. package/dist/review.d.ts +0 -15
  74. package/dist/review.d.ts.map +0 -1
  75. package/dist/review.js +0 -154
  76. package/dist/review.js.map +0 -1
  77. package/dist/titlebar.d.ts +0 -3
  78. package/dist/titlebar.d.ts.map +0 -1
  79. package/dist/titlebar.js +0 -59
  80. package/dist/titlebar.js.map +0 -1
  81. package/dist/todo/index.d.ts +0 -3
  82. package/dist/todo/index.d.ts.map +0 -1
  83. package/dist/todo/index.js +0 -3
  84. package/dist/todo/index.js.map +0 -1
  85. package/dist/todo/store.d.ts +0 -6
  86. package/dist/todo/store.d.ts.map +0 -1
  87. package/dist/todo/store.js +0 -43
  88. package/dist/todo/store.js.map +0 -1
  89. package/dist/todo/types.d.ts +0 -13
  90. package/dist/todo/types.d.ts.map +0 -1
  91. package/dist/todo/types.js +0 -2
  92. package/dist/todo/types.js.map +0 -1
  93. package/src/chain-preview.ts +0 -621
  94. package/src/client.ts +0 -518
  95. package/src/commands.ts +0 -132
  96. package/src/footer.ts +0 -305
  97. package/src/handlers.ts +0 -113
  98. package/src/hub-overlay-state.ts +0 -127
  99. package/src/hub-overlay.ts +0 -3037
  100. package/src/native-remote-ui-context.ts +0 -41
  101. package/src/output-viewer.ts +0 -480
  102. package/src/overlay.ts +0 -294
  103. package/src/protocol.ts +0 -758
  104. package/src/remote-lifecycle.ts +0 -270
  105. package/src/remote-session.ts +0 -1080
  106. package/src/remote-tui.ts +0 -1020
  107. package/src/remote-ui-handler.ts +0 -86
  108. package/src/renderers.ts +0 -740
  109. package/src/review.ts +0 -201
  110. package/src/titlebar.ts +0 -63
  111. package/src/todo/index.ts +0 -2
  112. package/src/todo/store.ts +0 -49
  113. package/src/todo/types.ts +0 -14
package/dist/index.js CHANGED
@@ -1,1578 +1,2 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
- import { Type } from '@sinclair/typebox';
10
- import { createRequire } from 'node:module';
11
- import { HubClient } from "./client.js";
12
- import { processActions } from "./handlers.js";
13
- import { getToolRenderers } from "./renderers.js";
14
- import { setupCoderFooter } from "./footer.js";
15
- import { setupTitlebar } from "./titlebar.js";
16
- import { registerAgentCommands } from "./commands.js";
17
- import { AgentManagerOverlay } from "./overlay.js";
18
- import { ChainEditorOverlay } from "./chain-preview.js";
19
- import { HubOverlay } from "./hub-overlay.js";
20
- import { OutputViewerOverlay } from "./output-viewer.js";
21
- import { setNativeRemoteExtensionContext } from "./native-remote-ui-context.js";
22
- import { handleRemoteUiRequest } from "./remote-ui-handler.js";
23
- import { setupRemoteMode, } from "./remote-session.js";
24
- // ESM doesn't have require() — create one for synchronous child_process access
25
- const _require = createRequire(import.meta.url);
26
- const HUB_URL_ENV = 'AGENTUITY_CODER_HUB_URL';
27
- const AGENT_ENV = 'AGENTUITY_CODER_AGENT';
28
- const REMOTE_SESSION_ENV = 'AGENTUITY_CODER_REMOTE_SESSION';
29
- const NATIVE_REMOTE_ENV = 'AGENTUITY_CODER_NATIVE_REMOTE';
30
- // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
31
- const API_KEY_ENV = 'AGENTUITY_CODER_API_KEY';
32
- const API_KEY_HEADER = 'x-agentuity-auth-api-key';
33
- const RECONNECT_WAIT_TIMEOUT_MS = 120_000;
34
- // Recent agent results for full-screen viewer (Ctrl+Shift+V / Alt+Shift+V)
35
- const recentResults = [];
36
- const MAX_STORED_RESULTS = 20;
37
- function startStreamingResult(agentName, description, prompt) {
38
- const result = {
39
- agentName,
40
- text: '',
41
- thinking: '',
42
- timestamp: Date.now(),
43
- isStreaming: true,
44
- description,
45
- prompt,
46
- };
47
- recentResults.unshift(result);
48
- if (recentResults.length > MAX_STORED_RESULTS)
49
- recentResults.pop();
50
- return result;
51
- }
52
- // ══════════════════════════════════════════════
53
- // Sub-Agent Output Limits (prevents context bloat in parent)
54
- // Inspired by pi-subagents (200KB/5K lines) and oh-my-pi (500KB/5K lines)
55
- // ══════════════════════════════════════════════
56
- const MAX_OUTPUT_BYTES = 200_000;
57
- const MAX_OUTPUT_LINES = 5_000;
58
- // All Pi events we subscribe to
59
- const PROXY_EVENTS = [
60
- 'session_shutdown',
61
- 'session_before_switch',
62
- 'session_switch',
63
- 'session_before_fork',
64
- 'session_fork',
65
- 'session_before_compact',
66
- 'session_compact',
67
- 'before_agent_start',
68
- 'agent_start',
69
- 'agent_end',
70
- 'turn_start',
71
- 'turn_end',
72
- 'tool_call',
73
- 'tool_result',
74
- 'tool_execution_start',
75
- 'tool_execution_update',
76
- 'tool_execution_end',
77
- 'message_start',
78
- 'message_update',
79
- 'message_end',
80
- 'input',
81
- 'model_select',
82
- 'context',
83
- ];
84
- const DEBUG = !!process.env['AGENTUITY_DEBUG'];
85
- function log(msg) {
86
- if (DEBUG)
87
- console.error(`[agentuity-coder] ${msg}`);
88
- }
89
- /** Build headers object with API key if available. Merges with any existing headers. */
90
- // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
91
- function authHeaders(extra) {
92
- const apiKey = process.env[API_KEY_ENV];
93
- const headers = { ...extra };
94
- if (apiKey)
95
- headers[API_KEY_HEADER] = apiKey;
96
- return headers;
97
- }
98
- // ══════════════════════════════════════════════
99
- // Synchronous Bootstrap — fetch InitMessage from Hub REST endpoint
100
- // This runs BEFORE tool registration so we know what tools/agents
101
- // the server actually provides. No hardcoded schemas.
102
- // ══════════════════════════════════════════════
103
- function buildInitUrl(hubUrl, agentRole) {
104
- let httpUrl = hubUrl.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
105
- if (httpUrl.includes('/api/ws')) {
106
- httpUrl = httpUrl.replace('/api/ws', '/api/hub/init');
107
- }
108
- else if (/\/ws\b/.test(httpUrl)) {
109
- httpUrl = httpUrl.replace(/\/ws\b/, '/api/hub/init');
110
- }
111
- else {
112
- httpUrl = httpUrl.replace(/\/?$/, '/api/hub/init');
113
- }
114
- if (agentRole && agentRole !== 'lead') {
115
- httpUrl += `?agent=${encodeURIComponent(agentRole)}`;
116
- }
117
- return httpUrl;
118
- }
119
- function getHubHttpBaseUrl(hubUrl) {
120
- let httpUrl = hubUrl.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
121
- httpUrl = httpUrl.replace(/\/api\/ws\b.*$/, '');
122
- httpUrl = httpUrl.replace(/\/ws\b.*$/, '');
123
- return httpUrl.replace(/\/+$/, '');
124
- }
125
- /**
126
- * Synchronously fetch the InitMessage from Hub's REST endpoint.
127
- *
128
- * Uses `curl` via `execFileSync` because Pi's extension registration is synchronous —
129
- * we need tools/agents BEFORE the extension returns. Node's `fetch()` is async-only,
130
- * and `Bun.spawnSync` isn't available in Pi's Node.js runtime.
131
- *
132
- * Requires `curl` binary (available on macOS, Linux, Windows 10+).
133
- */
134
- function fetchInitMessageSync(hubUrl, agentRole) {
135
- const httpUrl = buildInitUrl(hubUrl, agentRole);
136
- try {
137
- const { execFileSync } = _require('node:child_process');
138
- const apiKey = process.env[API_KEY_ENV];
139
- const curlArgs = ['-s', '--connect-timeout', '3', '--max-time', '5'];
140
- // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
141
- if (apiKey)
142
- curlArgs.push('-H', `${API_KEY_HEADER}: ${apiKey}`);
143
- curlArgs.push(httpUrl);
144
- const result = execFileSync('curl', curlArgs, { encoding: 'utf-8' });
145
- const parsed = JSON.parse(result);
146
- if (parsed && parsed.type === 'init') {
147
- return parsed;
148
- }
149
- return null;
150
- }
151
- catch {
152
- return null;
153
- }
154
- }
155
- /**
156
- * Fetch session snapshot from Hub REST endpoint.
157
- * Extracts observer count and session label for the footer display.
158
- * Best-effort, non-blocking — failures are silently ignored.
159
- */
160
- async function fetchSessionSnapshot(hubUrl, sessionId, observerState) {
161
- const baseUrl = getHubHttpBaseUrl(hubUrl);
162
- const httpUrl = sessionId
163
- ? `${baseUrl}/api/hub/session/${encodeURIComponent(sessionId)}`
164
- : `${baseUrl}/api/hub/sessions`;
165
- const controller = new AbortController();
166
- const timeout = setTimeout(() => controller.abort(), 5_000);
167
- try {
168
- const response = await fetch(httpUrl, {
169
- signal: controller.signal,
170
- headers: authHeaders({ accept: 'application/json' }),
171
- });
172
- if (!response.ok)
173
- return;
174
- if (sessionId) {
175
- const snapshot = (await response.json());
176
- if (observerState) {
177
- if (snapshot.label)
178
- observerState.label = snapshot.label;
179
- if (Array.isArray(snapshot.participants)) {
180
- observerState.count = snapshot.participants.filter((p) => p.role === 'observer').length;
181
- }
182
- }
183
- return;
184
- }
185
- const data = (await response.json());
186
- const first = data.sessions?.websocket?.[0];
187
- if (first && observerState) {
188
- if (first.label)
189
- observerState.label = first.label;
190
- if (typeof first.observerCount === 'number')
191
- observerState.count = first.observerCount;
192
- }
193
- }
194
- catch {
195
- // Ignore — best effort
196
- }
197
- finally {
198
- clearTimeout(timeout);
199
- }
200
- }
201
- async function fetchInitMessage(hubUrl, agentRole) {
202
- const httpUrl = buildInitUrl(hubUrl, agentRole);
203
- const controller = new AbortController();
204
- const timeout = setTimeout(() => controller.abort(), 5_000);
205
- try {
206
- const response = await fetch(httpUrl, {
207
- signal: controller.signal,
208
- headers: authHeaders({ accept: 'application/json' }),
209
- });
210
- if (!response.ok)
211
- return null;
212
- const parsed = (await response.json());
213
- if (parsed.type === 'init') {
214
- return parsed;
215
- }
216
- return null;
217
- }
218
- catch {
219
- return null;
220
- }
221
- finally {
222
- clearTimeout(timeout);
223
- }
224
- }
225
- export function agentuityCoderHub(pi) {
226
- const hubUrl = process.env[HUB_URL_ENV];
227
- if (!hubUrl)
228
- return;
229
- // ── Remote mode detection ──
230
- // If AGENTUITY_CODER_REMOTE_SESSION is set, the TUI connects as a controller
231
- // to an existing sandbox session. The full UI is set up (tools, commands, /hub)
232
- // but user input is relayed to the remote sandbox instead of the local Pi agent.
233
- const remoteSessionId = process.env[REMOTE_SESSION_ENV] || null;
234
- const isNativeRemote = !!process.env[NATIVE_REMOTE_ENV];
235
- if (remoteSessionId) {
236
- log(`Remote mode: will connect as controller to session ${remoteSessionId}`);
237
- }
238
- const isSubAgent = !!process.env[AGENT_ENV];
239
- const agentRole = process.env[AGENT_ENV] || 'lead';
240
- log(`Hub URL: ${hubUrl} (role: ${agentRole})`);
241
- // ══════════════════════════════════════════════
242
- // Fetch InitMessage from Hub REST endpoint (synchronous)
243
- // This is how we discover what tools/agents the server provides.
244
- // ══════════════════════════════════════════════
245
- const initMsg = fetchInitMessageSync(hubUrl, agentRole);
246
- if (!initMsg) {
247
- log('Hub not reachable — no tools or agents registered');
248
- log('Make sure the Hub server is running');
249
- return;
250
- }
251
- const serverTools = initMsg.tools || [];
252
- const serverAgents = initMsg.agents || [];
253
- let hubConfig = initMsg.config;
254
- const openChainEditor = async (ctx, initialAgents = []) => {
255
- if (!ctx.hasUI)
256
- return;
257
- const result = await ctx.ui.custom((_tui, theme, _keybindings, done) => new ChainEditorOverlay(theme, serverAgents, done, initialAgents), {
258
- overlay: true,
259
- overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
260
- });
261
- if (!result || result.steps.length === 0)
262
- return;
263
- const instructions = result.steps
264
- .map((step, index) => `${index + 1}) @${step.agent}: ${step.task || '(no task provided)'}`)
265
- .join(', ');
266
- const message = result.mode === 'parallel'
267
- ? `@lead Execute these tasks in parallel: ${instructions}`
268
- : `@lead Execute this plan in order: ${instructions}`;
269
- pi.sendUserMessage(message, { deliverAs: 'followUp' });
270
- };
271
- const openAgentManager = async (ctx) => {
272
- if (!ctx.hasUI)
273
- return;
274
- const result = await ctx.ui.custom((_tui, theme, _keybindings, done) => new AgentManagerOverlay(theme, serverAgents, done), {
275
- overlay: true,
276
- overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
277
- });
278
- // TODO: chain action from Agent Manager overlay (multi-select + Ctrl+R) not yet implemented
279
- if (result?.action === 'chain' && Array.isArray(result.agents)) {
280
- await openChainEditor(ctx, result.agents);
281
- return;
282
- }
283
- if (result?.action === 'run' && result.agent) {
284
- const task = await ctx.ui.input(`Task for ${result.agent}`, 'What should this agent do?');
285
- const trimmed = task?.trim();
286
- if (trimmed) {
287
- pi.sendUserMessage(`@${result.agent} ${trimmed}`, { deliverAs: 'followUp' });
288
- }
289
- }
290
- };
291
- const openHubOverlay = async (ctx, activeSessionId, detailSessionId) => {
292
- if (!ctx.hasUI)
293
- return;
294
- if (hubOverlayOpen)
295
- return;
296
- hubOverlayOpen = true;
297
- try {
298
- await ctx.ui.custom((tui, theme, _keybindings, done) => new HubOverlay(tui, theme, {
299
- baseUrl: getHubHttpBaseUrl(hubUrl),
300
- currentSessionId: activeSessionId ?? undefined,
301
- initialSessionId: detailSessionId ?? undefined,
302
- startInDetail: !!detailSessionId,
303
- done,
304
- }), {
305
- overlay: true,
306
- overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
307
- });
308
- }
309
- finally {
310
- hubOverlayOpen = false;
311
- }
312
- };
313
- const buildActionContext = (ctx) => ({
314
- ui: ctx.hasUI ? ctx.ui : undefined,
315
- sendUserMessage: (message, options) => {
316
- pi.sendUserMessage(message, { deliverAs: options?.deliverAs ?? 'followUp' });
317
- },
318
- });
319
- log(`Hub connected. Tools: ${serverTools.length}, Agents: ${serverAgents.length}`);
320
- // Titlebar: branding + spinner (registers its own event handlers)
321
- setupTitlebar(pi);
322
- // ══════════════════════════════════════════════
323
- // WebSocket client for runtime communication (tool execution + events)
324
- // ══════════════════════════════════════════════
325
- const client = new HubClient();
326
- // TODO: Remove/Change when we get Agentuity service level auth enabled, this is just temporary
327
- client.apiKey = process.env[API_KEY_ENV] || null;
328
- let cachedInitMessage = initMsg;
329
- let currentSessionId = initMsg.sessionId ?? null;
330
- let systemPromptApplied = false;
331
- let connectPromise = null;
332
- // In native remote mode, remote-tui.ts owns the Hub connection — show as connected
333
- let hubUiStatus = process.env[NATIVE_REMOTE_ENV] ? 'connected' : 'offline';
334
- let footerCtx = null;
335
- let hubOverlayOpen = false;
336
- // Observer awareness state — tracks who's watching this session.
337
- // Updated via broadcast events from the Hub (session_join, session_leave).
338
- const observerState = { count: 0, label: '' };
339
- const observerParticipantIds = new Set();
340
- function getHubUiStatus() {
341
- return hubUiStatus;
342
- }
343
- function getObserverState() {
344
- return observerState;
345
- }
346
- function mapConnectionStateToUiStatus(state) {
347
- if (state === 'connected')
348
- return 'connected';
349
- if (state === 'reconnecting')
350
- return 'reconnecting';
351
- return 'offline';
352
- }
353
- function updateHubUiStatus(state) {
354
- hubUiStatus = mapConnectionStateToUiStatus(state);
355
- if (footerCtx?.hasUI) {
356
- footerCtx.ui.setStatus('hub_connection', hubUiStatus);
357
- }
358
- }
359
- function applyInitMessage(nextInit) {
360
- cachedInitMessage = nextInit;
361
- if (nextInit.sessionId)
362
- currentSessionId = nextInit.sessionId;
363
- if (nextInit.config)
364
- hubConfig = nextInit.config;
365
- }
366
- client.onInitMessage = (nextInit) => {
367
- applyInitMessage(nextInit);
368
- };
369
- client.onConnectionStateChange = (state) => {
370
- updateHubUiStatus(state);
371
- log(`Hub connection state: ${state}`);
372
- };
373
- client.onBeforeReconnect = async () => {
374
- const refreshedInit = await fetchInitMessage(hubUrl, agentRole);
375
- if (refreshedInit) {
376
- applyInitMessage(refreshedInit);
377
- log('Refreshed Hub init payload before reconnect');
378
- }
379
- };
380
- // Handle unsolicited server messages (broadcast, presence)
381
- // Updates observer state for footer display
382
- client.onServerMessage = (message) => {
383
- const msgType = message.type;
384
- if (msgType === 'broadcast') {
385
- const event = message.event;
386
- if (event === 'session_join') {
387
- const participant = message.data?.participant;
388
- if (participant?.role === 'observer' && typeof participant.id === 'string') {
389
- observerParticipantIds.add(participant.id);
390
- observerState.count = observerParticipantIds.size;
391
- log(`Observer joined: ${observerState.count} observers`);
392
- }
393
- }
394
- else if (event === 'session_label_updated') {
395
- const label = message.data?.label;
396
- if (label) {
397
- observerState.label = label;
398
- log(`Session label updated: ${label}`);
399
- }
400
- }
401
- else if (event === 'session_leave') {
402
- const participant = message.data?.participant;
403
- if (participant?.role === 'observer' && typeof participant.id === 'string') {
404
- observerParticipantIds.delete(participant.id);
405
- observerState.count = observerParticipantIds.size;
406
- log(`Observer left: ${observerState.count} observers`);
407
- }
408
- }
409
- }
410
- else if (msgType === 'presence') {
411
- // Full presence update — may include participant list
412
- const participants = message.participants;
413
- if (participants) {
414
- observerParticipantIds.clear();
415
- for (const participant of participants) {
416
- if (participant.role === 'observer' && typeof participant.id === 'string') {
417
- observerParticipantIds.add(participant.id);
418
- }
419
- }
420
- observerState.count = observerParticipantIds.size;
421
- log(`Presence update: ${observerState.count} observers`);
422
- }
423
- }
424
- };
425
- // Lazy WebSocket connect — returns cached InitMessage
426
- // In native remote mode, remote-tui.ts owns the controller WebSocket via RemoteSession.
427
- // Skip the extension's own HubClient connection to avoid duplicate controllers.
428
- function ensureConnected() {
429
- if (isNativeRemote) {
430
- log('Native remote mode — skipping HubClient WebSocket (remote-tui owns controller)');
431
- return Promise.resolve(cachedInitMessage);
432
- }
433
- if (client.connected && cachedInitMessage)
434
- return Promise.resolve(cachedInitMessage);
435
- if (client.connectionState === 'reconnecting' || client.connectionState === 'disconnected') {
436
- return client
437
- .waitUntilConnected(RECONNECT_WAIT_TIMEOUT_MS)
438
- .then(() => cachedInitMessage)
439
- .catch(() => null);
440
- }
441
- if (connectPromise)
442
- return connectPromise;
443
- connectPromise = (async () => {
444
- log('Connecting WebSocket to Hub...');
445
- try {
446
- // In remote mode, connect as a controller to the existing session
447
- const connectOpts = remoteSessionId
448
- ? { sessionId: remoteSessionId, role: 'controller' }
449
- : undefined;
450
- const wsInitMsg = await client.connect(hubUrl, connectOpts);
451
- log('WebSocket connected');
452
- applyInitMessage(wsInitMsg);
453
- connectPromise = null; // Clear so future disconnects can reconnect
454
- return wsInitMsg;
455
- }
456
- catch (err) {
457
- log(`WebSocket failed: ${err instanceof Error ? err.message : String(err)}`);
458
- connectPromise = null;
459
- return null;
460
- }
461
- })();
462
- return connectPromise;
463
- }
464
- // ══════════════════════════════════════════════
465
- // Register Hub tools from server's InitMessage
466
- // Tools come from the server — NOT hardcoded in the extension.
467
- // ══════════════════════════════════════════════
468
- for (const toolDef of serverTools) {
469
- log(`Registering tool: ${toolDef.name}`);
470
- const renderers = getToolRenderers(toolDef.name);
471
- pi.registerTool({
472
- name: toolDef.name,
473
- label: toolDef.label || toolDef.name,
474
- description: toolDef.description,
475
- // Server sends JSON Schema; TypeBox schemas are JSON Schema at runtime
476
- parameters: toolDef.parameters,
477
- ...(toolDef.promptSnippet ? { promptSnippet: toolDef.promptSnippet } : {}),
478
- ...(toolDef.promptGuidelines
479
- ? {
480
- promptGuidelines: Array.isArray(toolDef.promptGuidelines)
481
- ? toolDef.promptGuidelines
482
- : [toolDef.promptGuidelines],
483
- }
484
- : {}),
485
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
486
- // Ensure WebSocket is connected before executing
487
- await ensureConnected();
488
- if (!client.connected) {
489
- return {
490
- content: [{ type: 'text', text: 'Error: Hub WebSocket not connected' }],
491
- details: undefined,
492
- };
493
- }
494
- const id = client.nextId();
495
- let response;
496
- try {
497
- response = await client.send({
498
- id,
499
- type: 'tool',
500
- name: toolDef.name,
501
- toolCallId,
502
- params: (params ?? {}),
503
- });
504
- }
505
- catch {
506
- return {
507
- content: [{ type: 'text', text: 'Error: Hub connection lost' }],
508
- details: undefined,
509
- };
510
- }
511
- // Process ALL Hub actions (NOTIFY, STATUS, RETURN, etc.)
512
- const result = await processActions(response.actions, buildActionContext(ctx));
513
- // If there's a return value from processActions, use it
514
- if (result.returnValue !== undefined) {
515
- const text = typeof result.returnValue === 'string'
516
- ? result.returnValue
517
- : JSON.stringify(result.returnValue, null, 2);
518
- return {
519
- content: [{ type: 'text', text }],
520
- details: undefined,
521
- };
522
- }
523
- // Fallback — check for RETURN action directly (backward compat)
524
- const returnAction = response.actions.find((a) => a.action === 'RETURN');
525
- if (returnAction && 'result' in returnAction) {
526
- const text = typeof returnAction.result === 'string'
527
- ? returnAction.result
528
- : JSON.stringify(returnAction.result, null, 2);
529
- return {
530
- content: [{ type: 'text', text }],
531
- details: undefined,
532
- };
533
- }
534
- return {
535
- content: [{ type: 'text', text: 'Done' }],
536
- details: undefined,
537
- };
538
- },
539
- // TUI renderers — optional, only for known Hub tools.
540
- // Cast needed: SimpleText satisfies Component, but TS can't verify cross-package structural match.
541
- ...(renderers?.renderCall && {
542
- renderCall: renderers.renderCall,
543
- }),
544
- ...(renderers?.renderResult && {
545
- renderResult: renderers.renderResult,
546
- }),
547
- });
548
- }
549
- // ══════════════════════════════════════════════
550
- // Register task tools (LEAD only) from server's agent list
551
- // Agent names and configs come from the Hub, not hardcoded.
552
- // ══════════════════════════════════════════════
553
- if (!isSubAgent && serverAgents.length > 0) {
554
- pi.registerShortcut('ctrl+shift+a', {
555
- description: 'Open Agent Manager',
556
- handler: async (ctx) => {
557
- await openAgentManager(ctx);
558
- },
559
- });
560
- const openOutputViewer = async (ctx) => {
561
- if (!ctx.hasUI || recentResults.length === 0)
562
- return;
563
- await ctx.ui.custom((tui, theme, _keybindings, done) => new OutputViewerOverlay(tui, theme, recentResults, done), {
564
- overlay: true,
565
- overlayOptions: { width: '95%', maxHeight: '95%', anchor: 'center', margin: 1 },
566
- });
567
- };
568
- pi.registerShortcut('ctrl+shift+v', {
569
- description: 'View full agent output',
570
- handler: openOutputViewer,
571
- });
572
- // Tmux/terminal environments often cannot emit Ctrl+Shift+V consistently.
573
- pi.registerShortcut('alt+shift+v', {
574
- description: 'View full agent output',
575
- handler: openOutputViewer,
576
- });
577
- pi.registerShortcut('ctrl+shift+c', {
578
- description: 'Open Chain Editor',
579
- handler: async (ctx) => {
580
- await openChainEditor(ctx);
581
- },
582
- });
583
- pi.registerShortcut('ctrl+h', {
584
- description: 'Open Hub overlay',
585
- handler: async (ctx) => {
586
- if (!ctx.hasUI)
587
- return;
588
- await openHubOverlay(ctx, currentSessionId);
589
- },
590
- });
591
- const agentRegistry = new Map(serverAgents.map((a) => [a.name, a]));
592
- const agentNames = serverAgents.map((a) => a.name);
593
- log(`Registering task tools. Agents: ${agentNames.join(', ')}`);
594
- const taskRenderers = getToolRenderers('task');
595
- pi.registerTool({
596
- name: 'task',
597
- label: 'Delegate Task to Agent',
598
- description: `Delegate a task to a specialized agent on your team. ` +
599
- `Available agents: ${agentNames.join(', ')}. ` +
600
- `Each agent runs independently with its own context window.`,
601
- promptSnippet: 'Use task({ description, prompt, subagent_type }) to delegate one focused sub-task to a specialist agent.',
602
- parameters: Type.Object({
603
- description: Type.String({ description: 'Short 3-5 word task description' }),
604
- prompt: Type.String({ description: 'Detailed task instructions for the agent' }),
605
- subagent_type: Type.String({
606
- description: `Agent: ${agentNames.join(', ')}`,
607
- }),
608
- }),
609
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
610
- const { description, prompt, subagent_type } = params;
611
- if (signal?.aborted) {
612
- return {
613
- content: [{ type: 'text', text: 'Cancelled' }],
614
- details: undefined,
615
- };
616
- }
617
- const agent = agentRegistry.get(subagent_type);
618
- if (!agent) {
619
- return {
620
- content: [
621
- {
622
- type: 'text',
623
- text: `Unknown agent: ${subagent_type}. Available: ${agentNames.join(', ')}`,
624
- },
625
- ],
626
- details: undefined,
627
- };
628
- }
629
- log(`Task: ${description} → ${subagent_type}`);
630
- const startTime = Date.now();
631
- const formatElapsed = () => {
632
- const s = Math.floor((Date.now() - startTime) / 1000);
633
- if (s < 60)
634
- return `${s}s`;
635
- return `${Math.floor(s / 60)}m ${s % 60}s`;
636
- };
637
- let elapsedTimer = null;
638
- // ── Single-agent status via working message ──
639
- let lastWidgetTool;
640
- let lastWidgetToolArgs;
641
- function updateWidget(status, tool, toolArgs) {
642
- if (!ctx.hasUI)
643
- return;
644
- let msg = '';
645
- if (status === 'running') {
646
- msg = '\u25CF ' + subagent_type; // ● name
647
- if (tool) {
648
- const toolInfo = toolArgs ? `${tool} ${toolArgs}` : tool;
649
- msg += ' ' + toolInfo.slice(0, 40);
650
- }
651
- msg += ' ' + formatElapsed();
652
- }
653
- else if (status === 'completed') {
654
- msg = '\u2713 ' + subagent_type + ' ' + formatElapsed(); // ✓ name Xs
655
- }
656
- else if (status === 'failed') {
657
- msg = '\u2717 ' + subagent_type + ' failed'; // ✗ name failed
658
- }
659
- ctx.ui.setWorkingMessage(msg);
660
- }
661
- if (ctx.hasUI) {
662
- ctx.ui.setStatus('active_agent', subagent_type);
663
- updateWidget('running');
664
- elapsedTimer = setInterval(() => {
665
- updateWidget('running', lastWidgetTool, lastWidgetToolArgs);
666
- }, 1000);
667
- }
668
- // Create live streaming result before starting sub-agent
669
- const liveResult = startStreamingResult(subagent_type, description, prompt);
670
- sendEventNoWait('task_start', {
671
- taskId: toolCallId,
672
- agent: subagent_type,
673
- prompt,
674
- description,
675
- });
676
- try {
677
- const result = await runSubAgent(agent, prompt, client, ctx.hasUI
678
- ? (progress) => {
679
- // Update TUI working message with live tool activity
680
- try {
681
- if (progress.status === 'thinking_delta' && progress.delta) {
682
- liveResult.thinking += progress.delta;
683
- updateWidget('running', 'thinking...');
684
- }
685
- else if (progress.status === 'text_delta' && progress.delta) {
686
- liveResult.text += progress.delta;
687
- updateWidget('running', 'writing...');
688
- }
689
- else if (progress.status === 'tool_start' && progress.currentTool) {
690
- lastWidgetTool = progress.currentTool;
691
- lastWidgetToolArgs = progress.currentToolArgs;
692
- updateWidget('running', progress.currentTool, progress.currentToolArgs);
693
- }
694
- }
695
- catch {
696
- // Best-effort live widget updates.
697
- }
698
- // Forward progress to Hub (fire-and-forget, queued while disconnected)
699
- sendEventNoWait('agent_progress', {
700
- agentName: progress.agentName,
701
- status: progress.status,
702
- taskId: toolCallId,
703
- delta: progress.delta,
704
- currentTool: progress.currentTool,
705
- currentToolArgs: progress.currentToolArgs,
706
- elapsed: progress.elapsed,
707
- });
708
- }
709
- : undefined, signal);
710
- // Flash completed state briefly before clearing
711
- updateWidget('completed');
712
- // Finalize the live result instead of creating a new one
713
- liveResult.isStreaming = false;
714
- liveResult.text = result.output || liveResult.text || '(no output)';
715
- await sendEvent('task_complete', {
716
- taskId: toolCallId,
717
- agent: subagent_type,
718
- duration: result.duration,
719
- result: result.output.slice(0, 10000),
720
- }, ctx);
721
- let output = result.output;
722
- let tokenInfoStr;
723
- if (result.tokens && (result.tokens.input > 0 || result.tokens.output > 0)) {
724
- tokenInfoStr = `${subagent_type}: ${result.duration}ms | ${result.tokens.input} in ${result.tokens.output} out | $${result.tokens.cost.toFixed(4)}`;
725
- output += `\n\n---\n_${subagent_type}: ${result.duration}ms | ${result.tokens.input} in ${result.tokens.output} out tokens | $${result.tokens.cost.toFixed(4)}_`;
726
- }
727
- if (tokenInfoStr)
728
- liveResult.tokenInfo = tokenInfoStr;
729
- return {
730
- content: [{ type: 'text', text: output }],
731
- details: undefined,
732
- };
733
- }
734
- catch (err) {
735
- const errorMsg = err instanceof Error ? err.message : String(err);
736
- liveResult.isStreaming = false;
737
- liveResult.text = liveResult.text || `Agent ${subagent_type} failed: ${errorMsg}`;
738
- await sendEvent('task_error', {
739
- taskId: toolCallId,
740
- agent: subagent_type,
741
- error: errorMsg,
742
- }, ctx);
743
- updateWidget('failed');
744
- return {
745
- content: [
746
- { type: 'text', text: `Agent ${subagent_type} failed: ${errorMsg}` },
747
- ],
748
- details: undefined,
749
- };
750
- }
751
- finally {
752
- if (elapsedTimer)
753
- clearInterval(elapsedTimer);
754
- if (ctx.hasUI) {
755
- ctx.ui.setStatus('active_agent', undefined);
756
- ctx.ui.setWorkingMessage(); // Restore Pi's default working message
757
- }
758
- }
759
- },
760
- ...(taskRenderers?.renderCall && {
761
- renderCall: taskRenderers.renderCall,
762
- }),
763
- ...(taskRenderers?.renderResult && {
764
- renderResult: taskRenderers.renderResult,
765
- }),
766
- });
767
- const parallelRenderers = getToolRenderers('parallel_tasks');
768
- pi.registerTool({
769
- name: 'parallel_tasks',
770
- label: 'Delegate Parallel Tasks',
771
- description: `Run multiple agent tasks concurrently (max 4). ` +
772
- `Available agents: ${agentNames.join(', ')}.`,
773
- promptSnippet: 'Use parallel_tasks({ tasks: [...] }) when multiple independent specialist tasks can run at the same time.',
774
- parameters: Type.Object({
775
- tasks: Type.Array(Type.Object({
776
- description: Type.String({ description: 'Short task description' }),
777
- prompt: Type.String({ description: 'Detailed instructions' }),
778
- subagent_type: Type.String({ description: 'Agent to delegate to' }),
779
- }), { maxItems: 4 }),
780
- }),
781
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
782
- const { tasks } = params;
783
- if (signal?.aborted) {
784
- return {
785
- content: [{ type: 'text', text: 'Cancelled' }],
786
- details: undefined,
787
- };
788
- }
789
- if (tasks.length > 4) {
790
- return {
791
- content: [{ type: 'text', text: 'Maximum 4 concurrent tasks allowed.' }],
792
- details: undefined,
793
- };
794
- }
795
- log(`Parallel tasks: ${tasks.map((t) => `${t.subagent_type}:${t.description}`).join(', ')}`);
796
- let elapsedTimer = null;
797
- const agentStatuses = tasks.map((t) => ({
798
- name: t.subagent_type,
799
- status: 'pending',
800
- }));
801
- function updateWidget() {
802
- if (!ctx.hasUI)
803
- return;
804
- const parts = agentStatuses
805
- .filter((s) => s.status !== 'pending')
806
- .map((s) => {
807
- const elapsed = s.startTime
808
- ? Math.floor((Date.now() - s.startTime) / 1000)
809
- : 0;
810
- const timeStr = elapsed < 60
811
- ? `${elapsed}s`
812
- : `${Math.floor(elapsed / 60)}m${elapsed % 60}s`;
813
- if (s.status === 'running') {
814
- let info = `\u25CF ${s.name}`;
815
- if (s.currentTool)
816
- info += ` ${s.currentTool.slice(0, 15)}`;
817
- return info + ` ${timeStr}`;
818
- }
819
- if (s.status === 'completed') {
820
- return `\u2713 ${s.name} ${timeStr}`;
821
- }
822
- if (s.status === 'failed') {
823
- return `\u2717 ${s.name}`;
824
- }
825
- return `\u25CB ${s.name}`;
826
- });
827
- ctx.ui.setWorkingMessage(parts.join(' '));
828
- }
829
- if (ctx.hasUI) {
830
- ctx.ui.setStatus('active_agent', 'agents');
831
- updateWidget();
832
- elapsedTimer = setInterval(() => {
833
- updateWidget(); // Refresh elapsed times in widget
834
- }, 1000);
835
- }
836
- // Create live streaming results for each parallel task
837
- const liveResults = tasks.map((task) => startStreamingResult(task.subagent_type, task.description, task.prompt));
838
- const promises = tasks.map(async (task, index) => {
839
- const taskId = `${toolCallId}-${index}-${task.subagent_type}`;
840
- const agent = agentRegistry.get(task.subagent_type);
841
- if (!agent) {
842
- agentStatuses[index].status = 'failed';
843
- liveResults[index].isStreaming = false;
844
- liveResults[index].text = `Unknown agent: ${task.subagent_type}`;
845
- await sendEvent('task_error', {
846
- taskId,
847
- agent: task.subagent_type,
848
- error: `Unknown agent: ${task.subagent_type}`,
849
- }, ctx);
850
- updateWidget();
851
- return {
852
- agent: task.subagent_type,
853
- error: `Unknown agent: ${task.subagent_type}`,
854
- };
855
- }
856
- agentStatuses[index].status = 'running';
857
- agentStatuses[index].startTime = Date.now();
858
- sendEventNoWait('task_start', {
859
- taskId,
860
- agent: task.subagent_type,
861
- prompt: task.prompt,
862
- description: task.description,
863
- });
864
- updateWidget();
865
- try {
866
- const result = await runSubAgent(agent, task.prompt, client, ctx.hasUI
867
- ? (progress) => {
868
- // Handle streaming deltas
869
- if (progress.status === 'thinking_delta' && progress.delta) {
870
- liveResults[index].thinking += progress.delta;
871
- }
872
- else if (progress.status === 'text_delta' && progress.delta) {
873
- liveResults[index].text += progress.delta;
874
- }
875
- // Update per-agent widget with tool activity
876
- agentStatuses[index].currentTool = progress.currentTool;
877
- agentStatuses[index].currentToolArgs = progress.currentToolArgs;
878
- updateWidget();
879
- // Forward progress to Hub (fire-and-forget, queued while disconnected)
880
- sendEventNoWait('agent_progress', {
881
- agentName: progress.agentName,
882
- status: progress.status,
883
- taskId,
884
- delta: progress.delta,
885
- currentTool: progress.currentTool,
886
- currentToolArgs: progress.currentToolArgs,
887
- elapsed: progress.elapsed,
888
- });
889
- }
890
- : undefined, signal);
891
- agentStatuses[index].status = 'completed';
892
- agentStatuses[index].duration = result.duration;
893
- agentStatuses[index].currentTool = undefined;
894
- agentStatuses[index].currentToolArgs = undefined;
895
- // Finalize the live result
896
- liveResults[index].isStreaming = false;
897
- liveResults[index].text =
898
- result.output || liveResults[index].text || '(no output)';
899
- await sendEvent('task_complete', {
900
- taskId,
901
- agent: task.subagent_type,
902
- duration: result.duration,
903
- result: result.output.slice(0, 10000),
904
- }, ctx);
905
- updateWidget();
906
- return {
907
- agent: task.subagent_type,
908
- output: result.output,
909
- duration: result.duration,
910
- tokens: result.tokens,
911
- };
912
- }
913
- catch (err) {
914
- const errorMsg = err instanceof Error ? err.message : String(err);
915
- agentStatuses[index].status = 'failed';
916
- agentStatuses[index].currentTool = undefined;
917
- agentStatuses[index].currentToolArgs = undefined;
918
- liveResults[index].isStreaming = false;
919
- liveResults[index].text = liveResults[index].text || `Failed: ${errorMsg}`;
920
- await sendEvent('task_error', {
921
- taskId,
922
- agent: task.subagent_type,
923
- error: errorMsg,
924
- }, ctx);
925
- updateWidget();
926
- return { agent: task.subagent_type, error: errorMsg };
927
- }
928
- });
929
- try {
930
- const results = await Promise.all(promises);
931
- // Finalize live results with token info
932
- results.forEach((r, idx) => {
933
- if ('output' in r && r.output && !('error' in r && r.error)) {
934
- if ('tokens' in r && r.tokens && (r.tokens.input > 0 || r.tokens.output > 0)) {
935
- liveResults[idx].tokenInfo =
936
- `${r.agent}: ${'duration' in r ? r.duration : 0}ms | ${r.tokens.input} in ${r.tokens.output} out | $${r.tokens.cost.toFixed(4)}`;
937
- }
938
- }
939
- });
940
- const output = results
941
- .map((r) => {
942
- if ('error' in r && r.error)
943
- return `### ${r.agent} (FAILED)\n${r.error}`;
944
- let text = `### ${r.agent} (${'duration' in r ? r.duration : 0}ms)\n${'output' in r ? r.output : ''}`;
945
- if ('tokens' in r && r.tokens && (r.tokens.input > 0 || r.tokens.output > 0)) {
946
- text += `\n\n---\n_${r.agent}: ${'duration' in r ? r.duration : 0}ms | ${r.tokens.input} in ${r.tokens.output} out tokens | $${r.tokens.cost.toFixed(4)}_`;
947
- }
948
- return text;
949
- })
950
- .join('\n\n---\n\n');
951
- return {
952
- content: [{ type: 'text', text: output }],
953
- details: undefined,
954
- };
955
- }
956
- finally {
957
- if (elapsedTimer)
958
- clearInterval(elapsedTimer);
959
- if (ctx.hasUI) {
960
- ctx.ui.setStatus('active_agent', undefined);
961
- ctx.ui.setWorkingMessage(); // Restore Pi's default working message
962
- }
963
- }
964
- },
965
- ...(parallelRenderers?.renderCall && {
966
- renderCall: parallelRenderers.renderCall,
967
- }),
968
- ...(parallelRenderers?.renderResult && {
969
- renderResult: parallelRenderers.renderResult,
970
- }),
971
- });
972
- }
973
- log('Tool registration complete');
974
- // ══════════════════════════════════════════════
975
- // Register slash commands for agent routing (LEAD only)
976
- // When user types /memory, /scout, etc., the message is routed
977
- // to that specific agent via a routing prefix.
978
- // ══════════════════════════════════════════════
979
- if (!isSubAgent && serverAgents.length > 0) {
980
- registerAgentCommands(pi, serverAgents, getHubUiStatus, openAgentManager, openChainEditor);
981
- }
982
- // ══════════════════════════════════════════════
983
- // /hub command — Hub session overview (LEAD only)
984
- // ══════════════════════════════════════════════
985
- if (!isSubAgent) {
986
- pi.registerCommand('hub', {
987
- description: 'Open Coder Hub overlay (sessions, detail, feed)',
988
- handler: async (_args, ctx) => {
989
- if (!ctx.hasUI)
990
- return;
991
- await openHubOverlay(ctx, currentSessionId);
992
- },
993
- });
994
- pi.registerCommand('todos', {
995
- description: 'Open session todo board for current Hub session',
996
- handler: async (args, ctx) => {
997
- if (!ctx.hasUI)
998
- return;
999
- const targetSessionId = args.trim().length > 0 ? args.trim() : currentSessionId;
1000
- if (!targetSessionId) {
1001
- ctx.ui.notify('No active Hub session id available yet.', 'warning');
1002
- return;
1003
- }
1004
- await openHubOverlay(ctx, currentSessionId, targetSessionId);
1005
- },
1006
- });
1007
- pi.registerCommand('rename-session', {
1008
- description: 'Rename the current Hub session (max 30 chars)',
1009
- handler: async (args, ctx) => {
1010
- const label = args.trim().slice(0, 30);
1011
- if (!label) {
1012
- if (ctx.hasUI)
1013
- ctx.ui.notify('Usage: /rename-session <label>', 'warning');
1014
- return;
1015
- }
1016
- if (!client.connected) {
1017
- if (ctx.hasUI)
1018
- ctx.ui.notify('Not connected to Hub', 'warning');
1019
- return;
1020
- }
1021
- try {
1022
- client.send({ type: 'rename_session', label });
1023
- observerState.label = label;
1024
- if (ctx.hasUI)
1025
- ctx.ui.notify(`Session renamed to "${label}"`, 'info');
1026
- log(`Session renamed to "${label}"`);
1027
- }
1028
- catch (err) {
1029
- if (ctx.hasUI)
1030
- ctx.ui.notify(`Failed to rename: ${err?.message || err}`, 'error');
1031
- }
1032
- },
1033
- });
1034
- pi.registerCommand('sync-hub-skills', {
1035
- description: 'Sync skills from Coder Hub to local .agents/skills/ directory',
1036
- handler: async (_args, ctx) => {
1037
- const baseUrl = getHubHttpBaseUrl(hubUrl);
1038
- const url = `${baseUrl}/api/hub/skills`;
1039
- try {
1040
- const resp = await fetch(url, { headers: authHeaders() });
1041
- if (!resp.ok) {
1042
- const msg = `Hub skills fetch failed: ${resp.status} ${resp.statusText}`;
1043
- if (ctx.hasUI)
1044
- ctx.ui.notify(msg, 'error');
1045
- return;
1046
- }
1047
- const data = (await resp.json());
1048
- if (!data.ok || !data.skills?.length) {
1049
- if (ctx.hasUI)
1050
- ctx.ui.notify('No skills available from Hub.', 'info');
1051
- return;
1052
- }
1053
- const { mkdirSync, writeFileSync } = _require('node:fs');
1054
- const { dirname, resolve, relative } = _require('node:path');
1055
- const cwd = process.cwd();
1056
- const synced = [];
1057
- for (const skill of data.skills) {
1058
- if (skill.path.includes('\0')) {
1059
- log(`Skipping skill with null byte in path: ${skill.path}`);
1060
- continue;
1061
- }
1062
- const fullPath = resolve(cwd, skill.path);
1063
- const rel = relative(cwd, fullPath);
1064
- if (rel.startsWith('..') || resolve(cwd, rel) !== fullPath) {
1065
- log(`Skipping skill with path traversal: ${skill.path}`);
1066
- continue;
1067
- }
1068
- mkdirSync(dirname(fullPath), { recursive: true });
1069
- writeFileSync(fullPath, skill.content, 'utf-8');
1070
- synced.push(skill.path);
1071
- }
1072
- const msg = `Synced ${synced.length} skill files:\n${synced.map((p) => ` ${p}`).join('\n')}`;
1073
- if (ctx.hasUI)
1074
- ctx.ui.notify(msg, 'info');
1075
- log(msg);
1076
- }
1077
- catch (err) {
1078
- const msg = `Failed to sync skills: ${err?.message || err}`;
1079
- if (ctx.hasUI)
1080
- ctx.ui.notify(msg, 'error');
1081
- }
1082
- },
1083
- });
1084
- }
1085
- // ══════════════════════════════════════════════
1086
- // Event Handlers
1087
- // ══════════════════════════════════════════════
1088
- function serializeEvent(event) {
1089
- const data = {};
1090
- if (event && typeof event === 'object') {
1091
- for (const [key, value] of Object.entries(event)) {
1092
- if (typeof value !== 'function' && key !== 'signal') {
1093
- try {
1094
- JSON.stringify(value);
1095
- data[key] = value;
1096
- }
1097
- catch {
1098
- /* skip */
1099
- }
1100
- }
1101
- }
1102
- }
1103
- return data;
1104
- }
1105
- async function sendEvent(eventName, data, ctx) {
1106
- const id = client.nextId();
1107
- try {
1108
- const response = await client.send({
1109
- id,
1110
- type: 'event',
1111
- event: eventName,
1112
- data: { ...data, agentRole },
1113
- });
1114
- const result = await processActions(response.actions, buildActionContext(ctx));
1115
- if (result.block)
1116
- return result.block;
1117
- if (result.returnValue !== undefined)
1118
- return result.returnValue;
1119
- }
1120
- catch {
1121
- /* ignore */
1122
- }
1123
- return undefined;
1124
- }
1125
- function sendEventNoWait(eventName, data) {
1126
- client.sendNoWait({
1127
- id: client.nextId(),
1128
- type: 'event',
1129
- event: eventName,
1130
- data: { ...data, agentRole },
1131
- });
1132
- }
1133
- const onEvent = pi.on.bind(pi);
1134
- // session_start: establish WebSocket connection to Hub + set up footer
1135
- onEvent('session_start', async (event, ctx) => {
1136
- footerCtx = ctx;
1137
- if (isNativeRemote && remoteSessionId) {
1138
- setNativeRemoteExtensionContext(ctx);
1139
- }
1140
- await ensureConnected();
1141
- if (ctx.hasUI) {
1142
- ctx.ui.setStatus('hub_connection', getHubUiStatus());
1143
- }
1144
- // Set up Coder footer (powerline: model or active agent > branch > status + observer count)
1145
- setupCoderFooter(ctx, getHubUiStatus, getObserverState);
1146
- // Fire-and-forget: fetch session snapshot for label + initial observer count.
1147
- // Uses the Hub REST endpoint — non-blocking, best-effort.
1148
- if (!isSubAgent) {
1149
- fetchSessionSnapshot(hubUrl, currentSessionId, observerState).catch(() => { });
1150
- }
1151
- return sendEvent('session_start', serializeEvent(event), ctx);
1152
- });
1153
- // before_agent_start: inject system prompt from Hub
1154
- onEvent('before_agent_start', async (event, ctx) => {
1155
- const eventData = event;
1156
- let systemPrompt = eventData.systemPrompt || '';
1157
- const id = client.nextId();
1158
- try {
1159
- const response = await client.send({
1160
- id,
1161
- type: 'event',
1162
- event: 'before_agent_start',
1163
- data: { ...serializeEvent(event), agentRole },
1164
- });
1165
- const result = await processActions(response.actions, buildActionContext(ctx));
1166
- if (result.block)
1167
- return result.block;
1168
- if (result.systemPrompt) {
1169
- const mode = result.systemPromptMode || 'suffix';
1170
- if (mode === 'prefix') {
1171
- systemPrompt = result.systemPrompt + '\n\n' + systemPrompt;
1172
- }
1173
- else if (mode === 'suffix') {
1174
- systemPrompt = systemPrompt + '\n\n' + result.systemPrompt;
1175
- }
1176
- else {
1177
- systemPrompt = result.systemPrompt;
1178
- }
1179
- }
1180
- }
1181
- catch {
1182
- /* ignore */
1183
- }
1184
- // Apply config prefix/suffix — LEAD ONLY
1185
- if (!isSubAgent) {
1186
- if (hubConfig?.systemPromptPrefix && !systemPromptApplied) {
1187
- systemPrompt = hubConfig.systemPromptPrefix + '\n\n' + systemPrompt;
1188
- systemPromptApplied = true;
1189
- }
1190
- if (hubConfig?.systemPromptSuffix) {
1191
- systemPrompt = systemPrompt + '\n\n' + hubConfig.systemPromptSuffix;
1192
- }
1193
- }
1194
- return { systemPrompt };
1195
- });
1196
- // Proxy all other events
1197
- for (const eventName of PROXY_EVENTS) {
1198
- if (eventName === 'before_agent_start')
1199
- continue;
1200
- onEvent(eventName, async (event, ctx) => {
1201
- return sendEvent(eventName, serializeEvent(event), ctx);
1202
- });
1203
- }
1204
- // ── Remote mode: input interception + remote event rendering ──
1205
- // Two sub-modes:
1206
- // 1. Native remote (AGENTUITY_CODER_NATIVE_REMOTE=1): remote-tui.ts drives rendering
1207
- // via Agent.emit(). Extension only provides Hub UI (footer, /hub, commands).
1208
- // No pi.sendMessage() rendering, no setupRemoteMode() event handlers.
1209
- // 2. Legacy remote: Extension handles all rendering via pi.sendMessage({ customType }).
1210
- if (remoteSessionId) {
1211
- let remoteSession = null;
1212
- // Register custom message renderers (used only in legacy mode, harmless in native)
1213
- try {
1214
- pi.registerMessageRenderer('remote_message', () => undefined);
1215
- pi.registerMessageRenderer('remote_history', () => undefined);
1216
- }
1217
- catch {
1218
- /* not available in this Pi version */
1219
- }
1220
- if (!isNativeRemote) {
1221
- // Legacy remote: intercept input and render events via pi.sendMessage()
1222
- pi.on('input', async (event, ctx) => {
1223
- const inputEvent = event;
1224
- const userMessage = inputEvent.text || inputEvent.message;
1225
- if (!userMessage)
1226
- return;
1227
- if (!remoteSession?.isConnected) {
1228
- if (ctx.hasUI)
1229
- ctx.ui.notify('Not connected to remote session yet');
1230
- return { action: 'handled' };
1231
- }
1232
- pi.sendMessage({
1233
- customType: 'remote_message',
1234
- content: `**You:** ${userMessage}`,
1235
- display: true,
1236
- });
1237
- remoteSession.prompt(userMessage, inputEvent.images);
1238
- log(`Sent prompt to remote: ${userMessage.slice(0, 100)}`);
1239
- if (ctx.hasUI) {
1240
- ctx.ui.setWorkingMessage('Sending to remote agent…');
1241
- }
1242
- return { action: 'handled' };
1243
- });
1244
- // Connect the remote session with legacy event rendering
1245
- (async () => {
1246
- try {
1247
- remoteSession = await setupRemoteMode(pi, hubUrl, remoteSessionId);
1248
- log(`Remote session connected: ${remoteSessionId}`);
1249
- if (footerCtx) {
1250
- remoteSession._setExtensionCtx?.(footerCtx);
1251
- }
1252
- pi.sendMessage({
1253
- customType: 'remote_message',
1254
- content: `Connected to remote session **${remoteSessionId}**`,
1255
- display: true,
1256
- });
1257
- remoteSession.setUiHandler(async (request) => {
1258
- if (!footerCtx)
1259
- return null;
1260
- return await handleRemoteUiRequest(footerCtx, request);
1261
- });
1262
- }
1263
- catch (err) {
1264
- const msg = err instanceof Error ? err.message : String(err);
1265
- log(`Remote connection failed: ${msg}`);
1266
- pi.sendMessage({
1267
- customType: 'remote_message',
1268
- content: `Failed to connect to remote session: ${msg}`,
1269
- display: true,
1270
- });
1271
- }
1272
- })();
1273
- }
1274
- // In native remote mode: no input interception, no event rendering.
1275
- // remote-tui.ts handles Agent.emit() for native rendering and
1276
- // monkey-patches Agent.prompt/steer/abort for input relay.
1277
- }
1278
- // Clean up on shutdown
1279
- pi.on('session_shutdown', async (_event, _ctx) => {
1280
- log('Shutting down — closing Hub connection');
1281
- if (isNativeRemote && remoteSessionId) {
1282
- setNativeRemoteExtensionContext(null);
1283
- }
1284
- try {
1285
- client.close();
1286
- }
1287
- catch {
1288
- /* pending promises rejected on close — safe to ignore */
1289
- }
1290
- });
1291
- }
1292
- function truncateOutput(text) {
1293
- let result = text;
1294
- const lines = result.split('\n');
1295
- if (lines.length > MAX_OUTPUT_LINES) {
1296
- result =
1297
- lines.slice(0, MAX_OUTPUT_LINES).join('\n') +
1298
- `\n\n[Output truncated — ${lines.length - MAX_OUTPUT_LINES} lines omitted]`;
1299
- }
1300
- if (result.length > MAX_OUTPUT_BYTES) {
1301
- result =
1302
- result.slice(0, MAX_OUTPUT_BYTES) +
1303
- `\n\n[Output truncated — exceeded ${MAX_OUTPUT_BYTES} bytes]`;
1304
- }
1305
- return result;
1306
- }
1307
- /** Cache resolved Pi SDK modules to avoid repeated dynamic import resolution */
1308
- let _piSdkCache = null;
1309
- /**
1310
- * Load Pi SDK packages at runtime.
1311
- * The extension runs inside Pi's process, but @mariozechner/pi-ai isn't in
1312
- * our node_modules — resolve it from Pi's install directory via process.argv[1].
1313
- */
1314
- async function loadPiSdk() {
1315
- if (_piSdkCache)
1316
- return _piSdkCache;
1317
- // Try direct import first (works if packages are in module resolution path)
1318
- try {
1319
- const piSdk = await import('@mariozechner/pi-coding-agent');
1320
- // @ts-expect-error pi-ai is a runtime dependency available inside Pi's process
1321
- const piAi = await import('@mariozechner/pi-ai');
1322
- _piSdkCache = { piSdk, piAi };
1323
- return _piSdkCache;
1324
- }
1325
- catch {
1326
- /* fall through to argv[1] resolution */
1327
- }
1328
- // Resolve from Pi CLI binary (process.argv[1] → pi-coding-agent package root)
1329
- const { realpathSync } = _require('node:fs');
1330
- const { pathToFileURL } = _require('node:url');
1331
- const { dirname, join } = _require('node:path');
1332
- const piRealPath = realpathSync(process.argv[1] || '');
1333
- const piPkgDir = dirname(dirname(piRealPath));
1334
- const piSdkEntry = pathToFileURL(join(piPkgDir, 'dist', 'index.js')).href;
1335
- const piAiEntry = pathToFileURL(join(piPkgDir, 'node_modules', '@mariozechner', 'pi-ai', 'dist', 'index.js')).href;
1336
- const piSdk = await import(__rewriteRelativeImportExtension(piSdkEntry));
1337
- const piAi = await import(__rewriteRelativeImportExtension(piAiEntry));
1338
- _piSdkCache = { piSdk, piAi };
1339
- return _piSdkCache;
1340
- }
1341
- /**
1342
- * Create a Pi-compatible tool that proxies execution to the Hub via WebSocket.
1343
- * Used to give sub-agents access to Hub tools (memory, context7, etc.).
1344
- */
1345
- function createHubToolProxy(toolDef, hubClient) {
1346
- return {
1347
- name: toolDef.name,
1348
- label: toolDef.label || toolDef.name,
1349
- description: toolDef.description,
1350
- parameters: toolDef.parameters,
1351
- async execute(toolCallId, params) {
1352
- if (!hubClient.connected) {
1353
- return {
1354
- content: [
1355
- { type: 'text', text: `Hub not connected — cannot execute ${toolDef.name}` },
1356
- ],
1357
- details: undefined,
1358
- };
1359
- }
1360
- const id = hubClient.nextId();
1361
- try {
1362
- const response = await hubClient.send({
1363
- id,
1364
- type: 'tool',
1365
- name: toolDef.name,
1366
- toolCallId,
1367
- params: (params ?? {}),
1368
- });
1369
- // Extract RETURN action result
1370
- const returnAction = response.actions.find((a) => a.action === 'RETURN');
1371
- if (returnAction && 'result' in returnAction) {
1372
- const text = typeof returnAction.result === 'string'
1373
- ? returnAction.result
1374
- : JSON.stringify(returnAction.result, null, 2);
1375
- return { content: [{ type: 'text', text }], details: undefined };
1376
- }
1377
- return { content: [{ type: 'text', text: 'Done' }], details: undefined };
1378
- }
1379
- catch (err) {
1380
- const msg = err instanceof Error ? err.message : String(err);
1381
- return {
1382
- content: [{ type: 'text', text: `Hub tool error: ${msg}` }],
1383
- details: undefined,
1384
- };
1385
- }
1386
- },
1387
- };
1388
- }
1389
- /**
1390
- * Run a sub-agent in-process using Pi's createAgentSession().
1391
- * Sub-agents are created with noExtensions=true so they can't recursively
1392
- * spawn further sub-agents (no task tool registered).
1393
- * Sub-agents DO get Hub tools (memory, context7, etc.) via extensionFactories.
1394
- * Only returns the final assistant text, not intermediate events.
1395
- */
1396
- async function runSubAgent(agentConfig, task, hubClient, onProgress, signal) {
1397
- const startTime = Date.now();
1398
- const { piSdk, piAi } = await loadPiSdk();
1399
- // Runtime-resolved dynamic imports — exact types unavailable statically
1400
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1401
- const { createAgentSession, DefaultResourceLoader, SessionManager, createCodingTools, createReadOnlyTools, } = piSdk;
1402
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1403
- const { getModel } = piAi;
1404
- // Model — use agent's configured model (sub-agents typically use haiku for speed)
1405
- const modelId = agentConfig.model || 'claude-haiku-4-5';
1406
- const [provider, id] = modelId.includes('/')
1407
- ? modelId.split('/', 2)
1408
- : ['anthropic', modelId];
1409
- const subModel = getModel(provider, id);
1410
- if (!subModel) {
1411
- throw new Error(`Model "${modelId}" not available. ` +
1412
- `Check that the ${provider} API key is configured ` +
1413
- `(e.g. ${provider.toUpperCase().replace(/[^A-Z]/g, '_')}_API_KEY).`);
1414
- }
1415
- // Hub tools for this sub-agent (shared WebSocket connection)
1416
- // Sub-agents get Hub tools (memory, context7, etc.) via extensionFactories
1417
- // so they work in both driver and TUI mode.
1418
- const hubTools = agentConfig.hubTools ?? [];
1419
- // Resource loader — no extensions (prevents recursive task tool registration),
1420
- // no skills, agent's system prompt injected directly.
1421
- // Hub tools are injected via extensionFactories so sub-agents can use
1422
- // memory_recall, context7_search, etc.
1423
- const subLoader = new DefaultResourceLoader({
1424
- cwd: process.cwd(),
1425
- noExtensions: true,
1426
- extensionFactories: hubTools.length > 0
1427
- ? [
1428
- (pi) => {
1429
- for (const toolDef of hubTools) {
1430
- // Proxy object has the correct shape; cast needed because return type is Record<string, unknown>
1431
- pi.registerTool(createHubToolProxy(toolDef, hubClient));
1432
- }
1433
- },
1434
- ]
1435
- : [],
1436
- systemPromptOverride: () => agentConfig.systemPrompt,
1437
- });
1438
- await subLoader.reload();
1439
- // Select tools based on readOnly flag
1440
- const cwd = process.cwd();
1441
- const tools = agentConfig.readOnly ? createReadOnlyTools(cwd) : createCodingTools(cwd);
1442
- const { session } = await createAgentSession({
1443
- // subModel is already untyped (from dynamic import) — createAgentSession is also dynamically imported
1444
- model: subModel,
1445
- thinkingLevel: (agentConfig.thinkingLevel || 'xhigh'),
1446
- tools,
1447
- resourceLoader: subLoader,
1448
- sessionManager: SessionManager.inMemory('/tmp'),
1449
- });
1450
- await session.bindExtensions({});
1451
- // Subscribe to sub-agent events for live progress tracking
1452
- if (onProgress) {
1453
- try {
1454
- session.subscribe?.((event) => {
1455
- try {
1456
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1457
- const evt = event;
1458
- const elapsed = Date.now() - startTime;
1459
- // Handle streaming message updates (thinking + text tokens)
1460
- if (evt.type === 'message_update' && evt.assistantMessageEvent) {
1461
- const ame = evt.assistantMessageEvent;
1462
- if (ame.type === 'thinking_delta' && ame.delta) {
1463
- onProgress({
1464
- agentName: agentConfig.name,
1465
- status: 'thinking_delta',
1466
- delta: ame.delta,
1467
- elapsed,
1468
- });
1469
- }
1470
- else if (ame.type === 'text_delta' && ame.delta) {
1471
- onProgress({
1472
- agentName: agentConfig.name,
1473
- status: 'text_delta',
1474
- delta: ame.delta,
1475
- elapsed,
1476
- });
1477
- }
1478
- return;
1479
- }
1480
- if (evt.type === 'tool_execution_start') {
1481
- const toolName = evt.toolName || evt.name || evt.tool || 'unknown';
1482
- let toolArgs = '';
1483
- if (evt.args && typeof evt.args === 'object') {
1484
- const args = evt.args;
1485
- if (args.command)
1486
- toolArgs = String(args.command).slice(0, 60);
1487
- else if (args.filePath || args.path)
1488
- toolArgs = String(args.filePath || args.path);
1489
- else if (args.pattern)
1490
- toolArgs = String(args.pattern).slice(0, 40);
1491
- else {
1492
- const first = Object.values(args)[0];
1493
- if (first)
1494
- toolArgs = String(first).slice(0, 40);
1495
- }
1496
- }
1497
- onProgress({
1498
- agentName: agentConfig.name,
1499
- status: 'tool_start',
1500
- toolCallId: typeof evt.toolCallId === 'string' ? evt.toolCallId : undefined,
1501
- currentTool: toolName,
1502
- currentToolArgs: toolArgs,
1503
- elapsed,
1504
- });
1505
- }
1506
- else if (evt.type === 'tool_execution_end') {
1507
- onProgress({
1508
- agentName: agentConfig.name,
1509
- status: 'tool_end',
1510
- toolCallId: typeof evt.toolCallId === 'string' ? evt.toolCallId : undefined,
1511
- elapsed,
1512
- });
1513
- }
1514
- }
1515
- catch {
1516
- /* ignore — progress tracking is best-effort */
1517
- }
1518
- });
1519
- }
1520
- catch {
1521
- /* ignore — subscribe may not be available */
1522
- }
1523
- }
1524
- // Abort signal support — cancel sub-agent when user presses Esc
1525
- if (signal) {
1526
- if (signal.aborted) {
1527
- throw new Error('Aborted');
1528
- }
1529
- const onAbort = () => {
1530
- log(`Sub-agent ${agentConfig.name} aborted by signal`);
1531
- try {
1532
- session.abort?.();
1533
- }
1534
- catch {
1535
- /* ignore */
1536
- }
1537
- };
1538
- signal.addEventListener('abort', onAbort, { once: true });
1539
- }
1540
- log(`Sub-agent started: ${agentConfig.name} (model: ${modelId})`);
1541
- try {
1542
- await session.prompt(task);
1543
- // Only return the final assistant text — NOT intermediate JSONL events
1544
- const output = session.getLastAssistantText?.() || '(no output)';
1545
- const duration = Date.now() - startTime;
1546
- log(`Sub-agent ${agentConfig.name} completed in ${duration}ms`);
1547
- // Best-effort token extraction from sub-agent session messages
1548
- const subTokens = { input: 0, output: 0, cost: 0 };
1549
- try {
1550
- const branch = session.sessionManager?.getBranch?.() || [];
1551
- for (const entry of branch) {
1552
- if (entry.type === 'message') {
1553
- const msg = entry.message;
1554
- if (msg.role === 'assistant' && msg.usage) {
1555
- subTokens.input += msg.usage.input;
1556
- subTokens.output += msg.usage.output;
1557
- subTokens.cost += msg.usage.cost.total;
1558
- }
1559
- }
1560
- }
1561
- }
1562
- catch {
1563
- /* ignore — token extraction is best-effort */
1564
- }
1565
- return { output: truncateOutput(output.trim()), duration, tokens: subTokens };
1566
- }
1567
- catch (err) {
1568
- try {
1569
- session.abort?.();
1570
- }
1571
- catch {
1572
- /* ignore */
1573
- }
1574
- throw err;
1575
- }
1576
- }
1577
- export default agentuityCoderHub;
1
+ export { CoderClient, CoderClientOptionsSchema, } from '@agentuity/core/coder';
1578
2
  //# sourceMappingURL=index.js.map