@agentuity/coder-tui 2.0.10 → 2.0.12

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 (60) hide show
  1. package/dist/agentuity-cli.d.ts +12 -0
  2. package/dist/agentuity-cli.d.ts.map +1 -0
  3. package/dist/agentuity-cli.js +178 -0
  4. package/dist/agentuity-cli.js.map +1 -0
  5. package/dist/auth.d.ts +5 -0
  6. package/dist/auth.d.ts.map +1 -0
  7. package/dist/auth.js +62 -0
  8. package/dist/auth.js.map +1 -0
  9. package/dist/client.d.ts +1 -1
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +5 -6
  12. package/dist/client.js.map +1 -1
  13. package/dist/hub-overlay-state.d.ts.map +1 -1
  14. package/dist/hub-overlay-state.js +3 -1
  15. package/dist/hub-overlay-state.js.map +1 -1
  16. package/dist/hub-overlay.d.ts.map +1 -1
  17. package/dist/hub-overlay.js +18 -12
  18. package/dist/hub-overlay.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +50 -41
  22. package/dist/index.js.map +1 -1
  23. package/dist/local-init-filter.d.ts +5 -0
  24. package/dist/local-init-filter.d.ts.map +1 -0
  25. package/dist/local-init-filter.js +40 -0
  26. package/dist/local-init-filter.js.map +1 -0
  27. package/dist/protocol.d.ts +4 -0
  28. package/dist/protocol.d.ts.map +1 -1
  29. package/dist/remote-runtime.d.ts +16 -0
  30. package/dist/remote-runtime.d.ts.map +1 -0
  31. package/dist/remote-runtime.js +18 -0
  32. package/dist/remote-runtime.js.map +1 -0
  33. package/dist/remote-session.d.ts.map +1 -1
  34. package/dist/remote-session.js +17 -9
  35. package/dist/remote-session.js.map +1 -1
  36. package/dist/remote-tui.d.ts +4 -4
  37. package/dist/remote-tui.d.ts.map +1 -1
  38. package/dist/remote-tui.js +72 -27
  39. package/dist/remote-tui.js.map +1 -1
  40. package/dist/renderers.d.ts.map +1 -1
  41. package/dist/renderers.js +53 -1
  42. package/dist/renderers.js.map +1 -1
  43. package/dist/subagent-tool-selection.d.ts +3 -0
  44. package/dist/subagent-tool-selection.d.ts.map +1 -0
  45. package/dist/subagent-tool-selection.js +22 -0
  46. package/dist/subagent-tool-selection.js.map +1 -0
  47. package/package.json +5 -5
  48. package/src/agentuity-cli.ts +225 -0
  49. package/src/auth.ts +75 -0
  50. package/src/client.ts +6 -6
  51. package/src/hub-overlay-state.ts +4 -1
  52. package/src/hub-overlay.ts +28 -14
  53. package/src/index.ts +53 -41
  54. package/src/local-init-filter.ts +54 -0
  55. package/src/protocol.ts +4 -0
  56. package/src/remote-runtime.ts +45 -0
  57. package/src/remote-session.ts +17 -9
  58. package/src/remote-tui.ts +92 -32
  59. package/src/renderers.ts +61 -1
  60. package/src/subagent-tool-selection.ts +33 -0
@@ -0,0 +1,54 @@
1
+ import type { AgentDefinition, HubToolDefinition, InitMessage } from './protocol.ts';
2
+
3
+ const LOCAL_TUI_HIDDEN_HUB_TOOL_NAMES = new Set(['sandbox_exec']);
4
+
5
+ function filterHubToolsForLocalTui(tools?: HubToolDefinition[]): HubToolDefinition[] | undefined {
6
+ if (!tools) return tools;
7
+
8
+ const filtered = tools.filter((tool) => !LOCAL_TUI_HIDDEN_HUB_TOOL_NAMES.has(tool.name));
9
+ return filtered.length === tools.length ? tools : filtered;
10
+ }
11
+
12
+ function filterAgentHubToolsForLocalTui(agents?: AgentDefinition[]): AgentDefinition[] | undefined {
13
+ if (!agents) return agents;
14
+
15
+ let changed = false;
16
+ const filteredAgents = agents.map((agent) => {
17
+ const filteredHubTools = filterHubToolsForLocalTui(agent.hubTools);
18
+ if (filteredHubTools === agent.hubTools) {
19
+ return agent;
20
+ }
21
+
22
+ changed = true;
23
+ return {
24
+ ...agent,
25
+ hubTools: filteredHubTools && filteredHubTools.length > 0 ? filteredHubTools : undefined,
26
+ };
27
+ });
28
+
29
+ return changed ? filteredAgents : agents;
30
+ }
31
+
32
+ export function adaptInitMessageForLocalTui(
33
+ init: InitMessage,
34
+ options: {
35
+ isRemoteSession: boolean;
36
+ }
37
+ ): InitMessage {
38
+ if (options.isRemoteSession) {
39
+ return init;
40
+ }
41
+
42
+ const tools = filterHubToolsForLocalTui(init.tools);
43
+ const agents = filterAgentHubToolsForLocalTui(init.agents);
44
+
45
+ if (tools === init.tools && agents === init.agents) {
46
+ return init;
47
+ }
48
+
49
+ return {
50
+ ...init,
51
+ tools,
52
+ agents,
53
+ };
54
+ }
package/src/protocol.ts CHANGED
@@ -75,6 +75,7 @@ export type HubAction =
75
75
  export interface AgentDefinition {
76
76
  name: string;
77
77
  displayName?: string;
78
+ source?: 'builtin' | 'custom';
78
79
  description: string;
79
80
  systemPrompt: string;
80
81
  model?: string;
@@ -84,6 +85,7 @@ export interface AgentDefinition {
84
85
  readOnly?: boolean;
85
86
  hubTools?: HubToolDefinition[];
86
87
  capabilities?: string[];
88
+ strictToolSelection?: boolean;
87
89
  status?: 'available' | 'busy' | 'offline';
88
90
  }
89
91
 
@@ -381,6 +383,7 @@ export interface SessionListItem {
381
383
  participantCount: number;
382
384
  tags: string[];
383
385
  skills: SessionSkillRef[];
386
+ enabledAgents: string[];
384
387
  defaultAgent?: string;
385
388
  bucket: SessionBucket;
386
389
  runtimeAvailable: boolean;
@@ -529,6 +532,7 @@ export interface SessionSnapshotMetadataExtensions {
529
532
  product?: SessionProductProjection;
530
533
  tags: string[];
531
534
  skills: SessionSkillRef[];
535
+ enabledAgents: string[];
532
536
  defaultAgent?: string;
533
537
  workers?: SessionListItem[];
534
538
  }
@@ -0,0 +1,45 @@
1
+ import type { RpcEvent } from './remote-session.ts';
2
+
3
+ export const REMOTE_RUNTIME_OPERATION_MESSAGE =
4
+ 'Remote controller mode only supports the current sandbox session.';
5
+
6
+ type RemoteRuntimeOperation = '/new' | '/resume' | '/fork' | '/import';
7
+
8
+ interface RemoteRuntimeGuards {
9
+ newSession?: () => Promise<unknown>;
10
+ switchSession?: () => Promise<unknown>;
11
+ fork?: () => Promise<unknown>;
12
+ importFromJsonl?: () => Promise<unknown>;
13
+ }
14
+
15
+ interface RemoteSessionEventTarget {
16
+ _handleAgentEvent?: (event: RpcEvent) => void;
17
+ }
18
+
19
+ export function dispatchRemoteSessionEvent(
20
+ session: RemoteSessionEventTarget,
21
+ event: RpcEvent
22
+ ): void {
23
+ if (typeof session._handleAgentEvent !== 'function') {
24
+ throw new Error(
25
+ 'Pi AgentSession no longer exposes _handleAgentEvent; remote TUI event dispatch needs an update.'
26
+ );
27
+ }
28
+
29
+ session._handleAgentEvent(event);
30
+ }
31
+
32
+ export function installRemoteRuntimeOperationGuards(
33
+ runtime: RemoteRuntimeGuards,
34
+ onBlocked: (operation: RemoteRuntimeOperation) => void
35
+ ): void {
36
+ const block = (operation: RemoteRuntimeOperation) => async (): Promise<{ cancelled: true }> => {
37
+ onBlocked(operation);
38
+ return { cancelled: true };
39
+ };
40
+
41
+ runtime.newSession = block('/new');
42
+ runtime.switchSession = block('/resume');
43
+ runtime.fork = block('/fork');
44
+ runtime.importFromJsonl = block('/import');
45
+ }
@@ -22,6 +22,8 @@ import {
22
22
  syncRemoteLifecycleWorkingMessage,
23
23
  type RemoteLifecycleState,
24
24
  } from './remote-lifecycle.ts';
25
+ import { resolveCoderOrgId } from './auth.ts';
26
+ import { formatToolDisplay } from './agentuity-cli.ts';
25
27
 
26
28
  const DEBUG = !!process.env['AGENTUITY_DEBUG'];
27
29
 
@@ -267,9 +269,10 @@ export class RemoteSession {
267
269
  wsHeaders['Authorization'] = `Bearer ${this.apiKey}`;
268
270
  }
269
271
  }
270
- if (this.orgId) {
271
- url.searchParams.set('orgId', this.orgId);
272
- wsHeaders['x-agentuity-orgid'] = this.orgId;
272
+ const orgId = resolveCoderOrgId(this.orgId);
273
+ if (orgId) {
274
+ url.searchParams.set('orgId', orgId);
275
+ wsHeaders['x-agentuity-orgid'] = orgId;
273
276
  }
274
277
 
275
278
  log(`${isReconnect ? 'Reconnecting' : 'Connecting'} to ${url.toString()}`);
@@ -999,18 +1002,23 @@ export async function setupRemoteMode(
999
1002
  break;
1000
1003
 
1001
1004
  case 'tool_execution_start': {
1002
- const tool = (event as { toolName?: string }).toolName ?? 'tool';
1003
- currentTool = tool;
1005
+ const rawTool = (event as { toolName?: string }).toolName ?? 'tool';
1006
+ const rawArgs = (event as { args?: string | Record<string, unknown> }).args;
1007
+ const display = formatToolDisplay(rawTool, rawArgs);
1008
+ currentTool = display.toolName;
1004
1009
  if (extensionCtxRef?.hasUI) {
1005
- setNonLifecycleWorkingMessage(`Running ${tool}...`);
1006
- extensionCtxRef.ui.setStatus('remote_activity', `Running ${tool}...`);
1010
+ setNonLifecycleWorkingMessage(`Running ${display.fullLabel}...`);
1011
+ extensionCtxRef.ui.setStatus('remote_activity', `Running ${display.fullLabel}...`);
1007
1012
  }
1008
- log(`Tool: ${tool}`);
1013
+ log(`Tool: ${display.fullLabel}`);
1009
1014
  break;
1010
1015
  }
1011
1016
 
1012
1017
  case 'tool_execution_end': {
1013
- const tool = (event as { toolName?: string }).toolName ?? currentTool ?? 'tool';
1018
+ const rawTool = (event as { toolName?: string }).toolName ?? currentTool ?? 'tool';
1019
+ const rawArgs = (event as { args?: string | Record<string, unknown> }).args;
1020
+ const display = rawArgs ? formatToolDisplay(rawTool, rawArgs) : null;
1021
+ const tool = display?.fullLabel ?? currentTool ?? rawTool;
1014
1022
  currentTool = null;
1015
1023
  if (extensionCtxRef?.hasUI) {
1016
1024
  clearWorkingMessage();
package/src/remote-tui.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Remote TUI — Native Pi Coding Agent Renderer for Remote Sessions
3
3
  *
4
- * Creates a real AgentSession + InteractiveMode backed by a remote sandbox
4
+ * Creates a real AgentSessionRuntime + InteractiveMode backed by a remote sandbox
5
5
  * via Hub WebSocket, with the coder extension loaded for Hub UI (footer,
6
6
  * /hub overlay, commands, titlebar).
7
7
  *
8
8
  * Architecture:
9
9
  * Remote TUI → Hub WebSocket (controller) → Sandbox (Pi RPC mode)
10
10
  * - User input → agent.prompt() (monkey-patched) → RPC `prompt` → Hub → sandbox
11
- * - Sandbox Pi → AgentEvent stream → Hub broadcast → Agent.emit() → InteractiveMode renders natively
11
+ * - Sandbox Pi → AgentEvent stream → Hub broadcast → AgentSession event handling → InteractiveMode renders natively
12
12
  * - Hub UI → coder extension (loaded via DefaultResourceLoader) provides footer, /hub, commands
13
13
  *
14
14
  * The local Agent never calls an LLM. Its prompt/steer/abort are monkey-patched
@@ -22,14 +22,16 @@
22
22
  *
23
23
  * IMPORTANT: Initialization order matters!
24
24
  * 1. Create RemoteSession (no connection yet)
25
- * 2. Create AgentSession, patch Agent/Session methods
25
+ * 2. Create AgentSessionRuntime, patch Agent/Session methods
26
26
  * 3. Register ALL event handlers on RemoteSession
27
27
  * 4. THEN connect — so hydration + replay events are captured
28
28
  */
29
29
 
30
30
  import {
31
- createAgentSession,
32
- DefaultResourceLoader,
31
+ createAgentSessionFromServices,
32
+ createAgentSessionRuntime,
33
+ createAgentSessionServices,
34
+ getAgentDir,
33
35
  InteractiveMode,
34
36
  SessionManager,
35
37
  } from '@mariozechner/pi-coding-agent';
@@ -49,6 +51,11 @@ import { RemoteSession } from './remote-session.ts';
49
51
  import type { RpcEvent } from './remote-session.ts';
50
52
  import { agentuityCoderHub } from './index.ts';
51
53
  import { handleRemoteUiRequest, REMOTE_FIRE_AND_FORGET_UI_METHODS } from './remote-ui-handler.ts';
54
+ import {
55
+ dispatchRemoteSessionEvent,
56
+ installRemoteRuntimeOperationGuards,
57
+ REMOTE_RUNTIME_OPERATION_MESSAGE,
58
+ } from './remote-runtime.ts';
52
59
 
53
60
  const DEBUG = !!process.env['AGENTUITY_DEBUG'];
54
61
 
@@ -60,7 +67,7 @@ function log(msg: string): void {
60
67
  * Run the native Pi TUI connected to a remote sandbox session.
61
68
  *
62
69
  * This is the entry point for `agentuity coder start --remote <sessionId>`.
63
- * Creates an AgentSession with the coder extension loaded (Hub UI), then
70
+ * Creates an AgentSessionRuntime with the coder extension loaded (Hub UI), then
64
71
  * monkey-patches the Agent for remote-backed execution.
65
72
  */
66
73
  export async function runRemoteTui(options: {
@@ -90,33 +97,74 @@ export async function runRemoteTui(options: {
90
97
  let hydrationStreamingDetected = false;
91
98
  let sessionResumeSeen = false;
92
99
 
93
- // ── 2. Create AgentSession with coder extension loaded ──
100
+ // ── 2. Create AgentSessionRuntime with coder extension loaded ──
94
101
  // The extension provides Hub UI (footer, /hub overlay, commands, titlebar).
95
102
  // AGENTUITY_CODER_NATIVE_REMOTE=1 tells it to skip legacy event rendering.
96
103
  const cwd = process.cwd();
97
- const resourceLoader = new DefaultResourceLoader({
98
- cwd,
99
- noExtensions: true, // Skip file-system extension discovery
100
- extensionFactories: [agentuityCoderHub], // Load coder extension directly
101
- });
102
- await resourceLoader.reload();
104
+ const runtime = await createAgentSessionRuntime(
105
+ async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
106
+ const services = await createAgentSessionServices({
107
+ cwd,
108
+ agentDir,
109
+ resourceLoaderOptions: {
110
+ noExtensions: true, // Skip file-system extension discovery
111
+ extensionFactories: [agentuityCoderHub], // Load coder extension directly
112
+ },
113
+ });
114
+
115
+ return {
116
+ ...(await createAgentSessionFromServices({
117
+ services,
118
+ sessionManager,
119
+ sessionStartEvent,
120
+ tools: [], // No local tools — sandbox has all the tools
121
+ })),
122
+ services,
123
+ diagnostics: services.diagnostics,
124
+ };
125
+ },
126
+ {
127
+ cwd,
128
+ agentDir: getAgentDir(),
129
+ sessionManager: SessionManager.inMemory(),
130
+ }
131
+ );
132
+ const session = runtime.session;
133
+ const agent: any = session.agent;
134
+ let runtimeDisposed = false;
135
+ log('AgentSessionRuntime created');
103
136
 
104
- const { session } = await createAgentSession({
105
- sessionManager: SessionManager.inMemory(),
106
- tools: [], // No local tools — sandbox has all the tools
107
- resourceLoader,
108
- });
109
- log('AgentSession created');
137
+ function notifyBlockedRuntimeOperation(operation: string): void {
138
+ const message = `${REMOTE_RUNTIME_OPERATION_MESSAGE} (${operation})`;
139
+ const ctx = getNativeRemoteExtensionContext();
140
+ if (ctx?.hasUI) {
141
+ ctx.ui.notify(message, 'warning');
142
+ ctx.ui.setStatus('remote_runtime', operation);
143
+ return;
144
+ }
145
+ log(message);
146
+ }
147
+
148
+ installRemoteRuntimeOperationGuards(runtime as any, (operation) =>
149
+ notifyBlockedRuntimeOperation(operation)
150
+ );
151
+
152
+ async function disposeRuntime(): Promise<void> {
153
+ if (runtimeDisposed) return;
154
+ runtimeDisposed = true;
155
+ await runtime.dispose();
156
+ }
110
157
 
111
158
  // NOTE: Do NOT call session.bindExtensions() here.
112
159
  // InteractiveMode.initExtensions() calls it with the proper uiContext.
113
160
  // Calling it early fires session_start twice, duplicating extension init.
114
-
115
- // Access the Agent instance (typed as `any` for monkey-patching)
116
- const agent: any = session.agent;
117
161
  let lifecycleState = remote.getLifecycleState();
118
162
  let lifecycleOwnsWorkingMessage = false;
119
163
 
164
+ function emitSessionEvent(event: RpcEvent): void {
165
+ dispatchRemoteSessionEvent(session as any, event);
166
+ }
167
+
120
168
  function applyLifecycleUi(state: RemoteLifecycleState): void {
121
169
  const ctx = getNativeRemoteExtensionContext();
122
170
  if (!ctx?.hasUI) return;
@@ -174,7 +222,7 @@ export async function runRemoteTui(options: {
174
222
 
175
223
  // Emit synthetic agent_start so InteractiveMode shows "working" immediately
176
224
  syntheticAgentStartEmitted = true;
177
- agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() });
225
+ emitSessionEvent({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
178
226
 
179
227
  // Send RPC command to sandbox
180
228
  remote.prompt(text);
@@ -268,7 +316,7 @@ export async function runRemoteTui(options: {
268
316
  //
269
317
  // Events that arrive before InteractiveMode is initialized are buffered
270
318
  // and flushed after init (InteractiveMode registers listeners during init,
271
- // so agent.emit() before that fires into the void).
319
+ // so session event dispatch before that fires into the void).
272
320
  let interactiveModeReady = false;
273
321
  let eventBuffer: RpcEvent[] = [];
274
322
  let seenMessageStart = false;
@@ -315,7 +363,7 @@ export async function runRemoteTui(options: {
315
363
  }
316
364
 
317
365
  for (const event of syntheticEvents) {
318
- agent.emit(event);
366
+ emitSessionEvent(event);
319
367
  }
320
368
  }
321
369
 
@@ -507,10 +555,14 @@ export async function runRemoteTui(options: {
507
555
  ) {
508
556
  log(`Live ${rpcEvent.type} without prior message_start — injecting synthetics`);
509
557
  if (!hadSeenAgentStart) {
510
- agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
558
+ emitSessionEvent({
559
+ type: 'agent_start',
560
+ agentName: 'lead',
561
+ timestamp: Date.now(),
562
+ } as any);
511
563
  seenAgentStart = true;
512
564
  }
513
- agent.emit({
565
+ emitSessionEvent({
514
566
  type: 'message_start',
515
567
  message: {
516
568
  role: 'assistant',
@@ -535,7 +587,7 @@ export async function runRemoteTui(options: {
535
587
  }
536
588
 
537
589
  // Emit to subscribers — InteractiveMode.handleEvent processes this
538
- agent.emit(rpcEvent);
590
+ emitSessionEvent(rpcEvent);
539
591
  if (injectedSyntheticMessageStart && rpcEvent.type === 'message_end') {
540
592
  seenMessageStart = false;
541
593
  seenAgentStart = hadSeenAgentStart;
@@ -787,7 +839,7 @@ export async function runRemoteTui(options: {
787
839
 
788
840
  // ── 9. Start InteractiveMode — full native Pi TUI ──
789
841
  log('Creating InteractiveMode');
790
- const interactive = new InteractiveMode(session);
842
+ const interactive = new InteractiveMode(runtime);
791
843
  log('InteractiveMode created, calling init...');
792
844
  await interactive.init();
793
845
 
@@ -803,8 +855,8 @@ export async function runRemoteTui(options: {
803
855
  // the streaming indicator right away, before any buffered events flush.
804
856
  // This prevents the blank screen gap between connect and first event.
805
857
  log('Hydration detected streaming — emitting immediate synthetics');
806
- agent.emit({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
807
- agent.emit({
858
+ emitSessionEvent({ type: 'agent_start', agentName: 'lead', timestamp: Date.now() } as any);
859
+ emitSessionEvent({
808
860
  type: 'message_start',
809
861
  message: {
810
862
  role: 'assistant',
@@ -836,7 +888,7 @@ export async function runRemoteTui(options: {
836
888
  if (eventBuffer.length > 0) {
837
889
  log(`Flushing ${eventBuffer.length} events: ${eventBuffer.map((e) => e.type).join(', ')}`);
838
890
  for (const buffered of eventBuffer) {
839
- agent.emit(buffered);
891
+ emitSessionEvent(buffered);
840
892
  if (buffered.type === 'agent_end') {
841
893
  resolveRunningPrompt();
842
894
  }
@@ -851,6 +903,11 @@ export async function runRemoteTui(options: {
851
903
  remote.close();
852
904
  setNativeRemoteExtensionContext(null);
853
905
  interactive.stop();
906
+ void disposeRuntime().catch((err) => {
907
+ log(
908
+ `Runtime dispose error during cleanup: ${err instanceof Error ? err.message : String(err)}`
909
+ );
910
+ });
854
911
  };
855
912
  process.on('SIGINT', cleanup);
856
913
  process.on('SIGTERM', cleanup);
@@ -863,6 +920,9 @@ export async function runRemoteTui(options: {
863
920
  } finally {
864
921
  remote.close();
865
922
  setNativeRemoteExtensionContext(null);
923
+ await disposeRuntime().catch((err) => {
924
+ log(`Runtime dispose error: ${err instanceof Error ? err.message : String(err)}`);
925
+ });
866
926
  log('Remote TUI exited');
867
927
  }
868
928
 
package/src/renderers.ts CHANGED
@@ -12,6 +12,7 @@ import type {
12
12
  AgentToolResult,
13
13
  } from '@mariozechner/pi-coding-agent';
14
14
  import { Box, Text, Container, type Component } from '@mariozechner/pi-tui';
15
+ import { formatToolDisplay } from './agentuity-cli.ts';
15
16
 
16
17
  // ──────────────────────────────────────────────
17
18
  // Line-safety helper — must be declared before SimpleText so
@@ -125,6 +126,32 @@ function truncate(str: string, max: number): string {
125
126
  return str.slice(0, max - 1) + '\u2026';
126
127
  }
127
128
 
129
+ function toSingleLinePreview(value: string): string {
130
+ return value.replace(/\s+/g, ' ').trim();
131
+ }
132
+
133
+ function isCommandToolName(toolName: string): boolean {
134
+ const normalized = toolName.trim().toLowerCase();
135
+ return normalized === 'bash' || normalized === 'execute_command' || normalized.includes('shell');
136
+ }
137
+
138
+ function getCommandArg(args: Record<string, unknown>): string | undefined {
139
+ const value = args['command'] ?? args['cmd'];
140
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
141
+ }
142
+
143
+ function getCommandTimeoutLabel(args: Record<string, unknown>): string | undefined {
144
+ const timeout = args['timeout'];
145
+ if (typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0) {
146
+ return `${timeout}s`;
147
+ }
148
+ if (typeof timeout === 'string' && timeout.trim()) {
149
+ const value = timeout.trim();
150
+ return /s$/i.test(value) ? value : `${value}s`;
151
+ }
152
+ return undefined;
153
+ }
154
+
128
155
  // ──────────────────────────────────────────────
129
156
  // Individual tool renderers
130
157
  // ──────────────────────────────────────────────
@@ -709,6 +736,37 @@ function parallelTasksRenderers(): ToolRenderers {
709
736
  };
710
737
  }
711
738
 
739
+ function commandToolRenderers(toolName: string): ToolRenderers {
740
+ return {
741
+ renderCall(args, theme) {
742
+ const display = formatToolDisplay(toolName, args);
743
+ const timeout = getCommandTimeoutLabel(args);
744
+
745
+ if (display.branded) {
746
+ let text = theme.fg('toolTitle', theme.bold(display.toolName));
747
+ if (display.toolArgs) {
748
+ text += theme.fg('accent', ` ${display.toolArgs}`);
749
+ }
750
+ if (timeout) {
751
+ text += theme.fg('dim', ` (timeout ${timeout})`);
752
+ }
753
+ return new SimpleText(text);
754
+ }
755
+
756
+ let text = theme.fg('toolTitle', theme.bold('$ '));
757
+ const commandPreview = getCommandArg(args);
758
+ text += theme.fg(
759
+ 'accent',
760
+ truncate(commandPreview ? toSingleLinePreview(commandPreview) : display.toolName, 80)
761
+ );
762
+ if (timeout) {
763
+ text += theme.fg('dim', ` (timeout ${timeout})`);
764
+ }
765
+ return new SimpleText(text);
766
+ },
767
+ };
768
+ }
769
+
712
770
  // ──────────────────────────────────────────────
713
771
 
714
772
  const RENDERERS: Record<string, () => ToolRenderers> = {
@@ -736,5 +794,7 @@ const RENDERERS: Record<string, () => ToolRenderers> = {
736
794
  */
737
795
  export function getToolRenderers(toolName: string): ToolRenderers | undefined {
738
796
  const factory = RENDERERS[toolName];
739
- return factory?.();
797
+ if (factory) return factory();
798
+ if (isCommandToolName(toolName)) return commandToolRenderers(toolName);
799
+ return undefined;
740
800
  }
@@ -0,0 +1,33 @@
1
+ import type { AgentDefinition } from './protocol.ts';
2
+
3
+ const READ_ONLY_TOOL_NAMES = ['read', 'grep', 'find', 'ls'] as const;
4
+ const CODING_TOOL_NAMES = ['read', 'bash', 'edit', 'write'] as const;
5
+
6
+ function normalizeToolName(name: string): string {
7
+ return name.trim().toLowerCase();
8
+ }
9
+
10
+ export function selectSubAgentToolNames(agentConfig: AgentDefinition): string[] {
11
+ const declared = new Set(
12
+ (agentConfig.tools ?? [])
13
+ .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)
14
+ .map(normalizeToolName)
15
+ );
16
+ const needsBash = declared.has('bash');
17
+ const baseToolNames =
18
+ agentConfig.readOnly && !needsBash ? READ_ONLY_TOOL_NAMES : CODING_TOOL_NAMES;
19
+ const allowLegacyFallback =
20
+ agentConfig.strictToolSelection !== true && agentConfig.source === 'builtin';
21
+
22
+ if (declared.size === 0) {
23
+ return allowLegacyFallback ? [...baseToolNames] : [];
24
+ }
25
+
26
+ const filtered = baseToolNames.filter((toolName) => declared.has(toolName));
27
+
28
+ if (filtered.length > 0) {
29
+ return filtered;
30
+ }
31
+
32
+ return allowLegacyFallback ? [...baseToolNames] : [];
33
+ }