@agentuity/coder-tui 3.0.0-alpha.6 → 3.0.0-alpha.7

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 (40) 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/hub-overlay-state.d.ts.map +1 -1
  6. package/dist/hub-overlay-state.js +3 -1
  7. package/dist/hub-overlay-state.js.map +1 -1
  8. package/dist/hub-overlay.d.ts.map +1 -1
  9. package/dist/hub-overlay.js +12 -3
  10. package/dist/hub-overlay.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +45 -30
  14. package/dist/index.js.map +1 -1
  15. package/dist/local-init-filter.d.ts +5 -0
  16. package/dist/local-init-filter.d.ts.map +1 -0
  17. package/dist/local-init-filter.js +40 -0
  18. package/dist/local-init-filter.js.map +1 -0
  19. package/dist/protocol.d.ts +2 -0
  20. package/dist/protocol.d.ts.map +1 -1
  21. package/dist/remote-session.d.ts.map +1 -1
  22. package/dist/remote-session.js +12 -6
  23. package/dist/remote-session.js.map +1 -1
  24. package/dist/renderers.d.ts.map +1 -1
  25. package/dist/renderers.js +53 -1
  26. package/dist/renderers.js.map +1 -1
  27. package/dist/subagent-tool-selection.d.ts +3 -0
  28. package/dist/subagent-tool-selection.d.ts.map +1 -0
  29. package/dist/subagent-tool-selection.js +22 -0
  30. package/dist/subagent-tool-selection.js.map +1 -0
  31. package/package.json +5 -5
  32. package/src/agentuity-cli.ts +225 -0
  33. package/src/hub-overlay-state.ts +4 -1
  34. package/src/hub-overlay.ts +14 -3
  35. package/src/index.ts +48 -32
  36. package/src/local-init-filter.ts +54 -0
  37. package/src/protocol.ts +2 -0
  38. package/src/remote-session.ts +12 -6
  39. package/src/renderers.ts +61 -1
  40. package/src/subagent-tool-selection.ts +33 -0
@@ -0,0 +1,225 @@
1
+ const SHELL_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)$/;
2
+
3
+ export const AGENTUITY_CLI_MARK = '⨺';
4
+
5
+ type ToolArgsInput = string | Record<string, unknown> | undefined;
6
+
7
+ export interface ToolDisplayDescriptor {
8
+ toolName: string;
9
+ toolArgs?: string;
10
+ fullLabel: string;
11
+ branded: boolean;
12
+ }
13
+
14
+ function stripShellQuotes(value: string): string {
15
+ if (
16
+ (value.startsWith('"') && value.endsWith('"')) ||
17
+ (value.startsWith("'") && value.endsWith("'"))
18
+ ) {
19
+ return value.slice(1, -1);
20
+ }
21
+ return value;
22
+ }
23
+
24
+ function tokenizeShellLike(command: string): string[] {
25
+ return (command.match(/"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|&&|\|\||[;|()]|[^\s;|()]+/g) ?? []).map(
26
+ stripShellQuotes
27
+ );
28
+ }
29
+
30
+ function splitShellSegments(command: string): string[] {
31
+ return command
32
+ .split(/(?:\r?\n|&&|\|\||;|\|)/)
33
+ .map((segment) => segment.trim())
34
+ .filter(Boolean);
35
+ }
36
+
37
+ function optionConsumesValue(token: string): boolean {
38
+ return (
39
+ token === '-c' ||
40
+ token === '-g' ||
41
+ token === '-h' ||
42
+ token === '-p' ||
43
+ token === '-r' ||
44
+ token === '-t' ||
45
+ token === '-u' ||
46
+ token === '--chdir' ||
47
+ token === '--config' ||
48
+ token === '--group' ||
49
+ token === '--host' ||
50
+ token === '--package' ||
51
+ token === '--registry' ||
52
+ token === '--user'
53
+ );
54
+ }
55
+
56
+ function trimShellPrefix(tokens: string[]): string[] {
57
+ let index = 0;
58
+
59
+ while (index < tokens.length) {
60
+ const token = tokens[index];
61
+ if (!token || token === '!' || token === '(' || token === ')') {
62
+ index += 1;
63
+ continue;
64
+ }
65
+
66
+ if (token === 'env' || token === 'sudo') {
67
+ index += 1;
68
+ while (tokens[index]?.startsWith('-')) {
69
+ const option = tokens[index] ?? '';
70
+ index += optionConsumesValue(option) ? 2 : 1;
71
+ }
72
+ continue;
73
+ }
74
+
75
+ if (SHELL_ASSIGNMENT_PATTERN.test(token)) {
76
+ index += 1;
77
+ continue;
78
+ }
79
+
80
+ break;
81
+ }
82
+
83
+ return tokens.slice(index);
84
+ }
85
+
86
+ function extractAgentuityCliRemainderFromTokens(tokens: string[]): string | null {
87
+ const trimmed = trimShellPrefix(tokens);
88
+ if (trimmed.length === 0) return null;
89
+
90
+ const first = trimmed[0]?.toLowerCase();
91
+ if (first === 'agentuity') {
92
+ return trimmed.slice(1).join(' ').trim();
93
+ }
94
+
95
+ if (first === 'bunx' || first === 'npx') {
96
+ let index = 1;
97
+ while (trimmed[index]?.startsWith('-')) {
98
+ const option = trimmed[index] ?? '';
99
+ index += optionConsumesValue(option) ? 2 : 1;
100
+ }
101
+ if (trimmed[index] === '@agentuity/cli') {
102
+ return trimmed
103
+ .slice(index + 1)
104
+ .join(' ')
105
+ .trim();
106
+ }
107
+ }
108
+
109
+ if (first === 'pnpm' && trimmed[1]?.toLowerCase() === 'dlx') {
110
+ let index = 2;
111
+ while (trimmed[index]?.startsWith('-')) {
112
+ const option = trimmed[index] ?? '';
113
+ index += optionConsumesValue(option) ? 2 : 1;
114
+ }
115
+ if (trimmed[index] === '@agentuity/cli') {
116
+ return trimmed
117
+ .slice(index + 1)
118
+ .join(' ')
119
+ .trim();
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ function normalizeDisplayWhitespace(value: string): string {
127
+ return value.replace(/\s+/g, ' ').trim();
128
+ }
129
+
130
+ function trimDisplay(value: string, max: number): string {
131
+ const normalized = normalizeDisplayWhitespace(value);
132
+ if (normalized.length <= max) return normalized;
133
+ return `${normalized.slice(0, max - 3)}...`;
134
+ }
135
+
136
+ function getCommandArg(rawArgs?: ToolArgsInput): string | undefined {
137
+ if (typeof rawArgs === 'string' && rawArgs.trim()) {
138
+ return rawArgs.trim();
139
+ }
140
+
141
+ if (!rawArgs || typeof rawArgs !== 'object') {
142
+ return undefined;
143
+ }
144
+
145
+ const value = rawArgs.command ?? rawArgs.cmd;
146
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
147
+ }
148
+
149
+ function getGenericToolArgsPreview(rawArgs?: ToolArgsInput): string | undefined {
150
+ if (typeof rawArgs === 'string' && rawArgs.trim()) {
151
+ return trimDisplay(rawArgs.trim(), 60);
152
+ }
153
+
154
+ if (!rawArgs || typeof rawArgs !== 'object') {
155
+ return undefined;
156
+ }
157
+
158
+ if (typeof rawArgs.command === 'string' && rawArgs.command.trim()) {
159
+ return trimDisplay(rawArgs.command.trim(), 60);
160
+ }
161
+ if (typeof rawArgs.filePath === 'string' && rawArgs.filePath.trim()) {
162
+ return rawArgs.filePath.trim();
163
+ }
164
+ if (typeof rawArgs.path === 'string' && rawArgs.path.trim()) {
165
+ return rawArgs.path.trim();
166
+ }
167
+ if (typeof rawArgs.pattern === 'string' && rawArgs.pattern.trim()) {
168
+ return trimDisplay(rawArgs.pattern.trim(), 40);
169
+ }
170
+
171
+ const first = Object.values(rawArgs)[0];
172
+ if (typeof first === 'string' && first.trim()) {
173
+ return trimDisplay(first.trim(), 40);
174
+ }
175
+
176
+ return undefined;
177
+ }
178
+
179
+ function isCommandToolName(toolName: string): boolean {
180
+ const normalized = toolName.trim().toLowerCase();
181
+ return normalized === 'bash' || normalized === 'execute_command' || normalized.includes('shell');
182
+ }
183
+
184
+ export function getAgentuityCliCommandRemainder(command?: string): string | null {
185
+ if (!command?.trim()) return null;
186
+
187
+ for (const segment of splitShellSegments(command)) {
188
+ const remainder = extractAgentuityCliRemainderFromTokens(tokenizeShellLike(segment));
189
+ if (remainder !== null) {
190
+ return remainder;
191
+ }
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ export function formatToolDisplay(
198
+ toolName: string,
199
+ rawArgs?: ToolArgsInput
200
+ ): ToolDisplayDescriptor {
201
+ const normalizedToolName = normalizeDisplayWhitespace(toolName) || 'tool';
202
+ const command = getCommandArg(rawArgs);
203
+
204
+ if (isCommandToolName(normalizedToolName) && command) {
205
+ const remainder = getAgentuityCliCommandRemainder(command);
206
+ if (remainder !== null) {
207
+ const brandedToolName = `${AGENTUITY_CLI_MARK} agentuity`;
208
+ const toolArgs = remainder ? trimDisplay(remainder, 60) : undefined;
209
+ return {
210
+ toolName: brandedToolName,
211
+ toolArgs,
212
+ fullLabel: toolArgs ? `${brandedToolName} ${toolArgs}` : brandedToolName,
213
+ branded: true,
214
+ };
215
+ }
216
+ }
217
+
218
+ const toolArgs = getGenericToolArgsPreview(rawArgs);
219
+ return {
220
+ toolName: normalizedToolName,
221
+ toolArgs,
222
+ fullLabel: toolArgs ? `${normalizedToolName} ${toolArgs}` : normalizedToolName,
223
+ branded: false,
224
+ };
225
+ }
@@ -1,3 +1,5 @@
1
+ import { formatToolDisplay } from './agentuity-cli.ts';
2
+
1
3
  export interface StreamBuffer {
2
4
  output: string;
3
5
  thinking: string;
@@ -78,7 +80,8 @@ export function buildProjectionFromEntries(
78
80
  const type = typeof entry.type === 'string' ? entry.type : '';
79
81
  if (type === 'tool_call') {
80
82
  const toolName = typeof entry.toolName === 'string' ? entry.toolName : 'tool';
81
- append('output', `[tool_call] ${toolName}\n\n`, entry.taskId);
83
+ const display = formatToolDisplay(toolName, entry.toolArgs);
84
+ append('output', `[tool_call] ${display.fullLabel}\n\n`, entry.taskId);
82
85
  continue;
83
86
  }
84
87
 
@@ -10,6 +10,7 @@ import {
10
10
  type StreamProjectionSource,
11
11
  } from './hub-overlay-state.ts';
12
12
  import { applyCoderAuthHeaders } from './auth.ts';
13
+ import { formatToolDisplay } from './agentuity-cli.ts';
13
14
  import { truncateToWidth } from './renderers.ts';
14
15
  import type {
15
16
  ConversationEntry as HubConversationEntry,
@@ -2128,6 +2129,13 @@ export class HubOverlay implements Component, Focusable {
2128
2129
  ? data.toolName
2129
2130
  : 'tool';
2130
2131
  const input = data?.args ?? data?.input;
2132
+ const display = formatToolDisplay(
2133
+ name,
2134
+ typeof input === 'string' || (input && typeof input === 'object')
2135
+ ? (input as string | Record<string, unknown>)
2136
+ : undefined
2137
+ );
2138
+ if (display.branded) return `tool_call ${display.fullLabel}`;
2131
2139
  const summarized = summarizeToolCall(name, input);
2132
2140
  if (summarized) return summarized;
2133
2141
  const argsPreview = summarizeArgs(input, 90);
@@ -2171,15 +2179,18 @@ export class HubOverlay implements Component, Focusable {
2171
2179
  const failed = data?.isError === true || details?.error === true;
2172
2180
  return `${header}\n${failed ? 'failed' : `done${duration}`}`;
2173
2181
  }
2174
- return `tool_result ${name}`;
2182
+ const display = formatToolDisplay(name, input);
2183
+ return `tool_result ${display.toolName}`;
2175
2184
  }
2176
2185
 
2177
2186
  if (eventName === 'agent_progress') {
2178
2187
  const agent = typeof data?.agentName === 'string' ? data.agentName : 'agent';
2179
2188
  const status = typeof data?.status === 'string' ? data.status : 'progress';
2180
- const toolName = typeof data?.currentTool === 'string' ? data.currentTool : '';
2189
+ const rawToolName = typeof data?.currentTool === 'string' ? data.currentTool : '';
2181
2190
  const toolArgsRaw = typeof data?.currentToolArgs === 'string' ? data.currentToolArgs : '';
2182
- const toolArgs = toolArgsRaw ? truncateToWidth(normalize(toolArgsRaw), 80) : '';
2191
+ const display = formatToolDisplay(rawToolName, toolArgsRaw || undefined);
2192
+ const toolName = display.toolName;
2193
+ const toolArgs = display.toolArgs ? truncateToWidth(normalize(display.toolArgs), 80) : '';
2183
2194
 
2184
2195
  // Deltas are already represented in rendered stream mode; skip them in event mode
2185
2196
  // to avoid noisy, low-signal token lines.
package/src/index.ts CHANGED
@@ -1,11 +1,12 @@
1
- import type {
1
+ import {
2
+ createBashToolDefinition,
2
3
  AgentToolResult,
3
4
  ExtensionAPI,
4
5
  ExtensionContext,
5
6
  ExtensionCommandContext,
6
7
  ToolDefinition,
7
8
  } from '@mariozechner/pi-coding-agent';
8
- import { Type, type TSchema } from '@sinclair/typebox';
9
+ import { Type, type TSchema } from 'typebox';
9
10
  import { createRequire } from 'node:module';
10
11
  import { HubClient } from './client.ts';
11
12
  import type { ConnectionState } from './client.ts';
@@ -22,6 +23,9 @@ import { setNativeRemoteExtensionContext } from './native-remote-ui-context.ts';
22
23
  import { handleRemoteUiRequest } from './remote-ui-handler.ts';
23
24
  import { buildInboundRpcPromptText, getInboundRpcDeliverAs } from './inbound-rpc.ts';
24
25
  import { applyCoderAuthHeaders, getCoderAuthCurlArgs } from './auth.ts';
26
+ import { formatToolDisplay } from './agentuity-cli.ts';
27
+ import { adaptInitMessageForLocalTui } from './local-init-filter.ts';
28
+ import { selectSubAgentToolNames } from './subagent-tool-selection.ts';
25
29
  import type {
26
30
  HubAction,
27
31
  HubResponse,
@@ -283,6 +287,7 @@ export function agentuityCoderHub(pi: ExtensionAPI) {
283
287
  // to an existing sandbox session. The full UI is set up (tools, commands, /hub)
284
288
  // but user input is relayed to the remote sandbox instead of the local Pi agent.
285
289
  const remoteSessionId = process.env[REMOTE_SESSION_ENV] || null;
290
+ const isRemoteSession = Boolean(remoteSessionId);
286
291
  const isNativeRemote = !!process.env[NATIVE_REMOTE_ENV];
287
292
  if (remoteSessionId) {
288
293
  log(`Remote mode: will connect as controller to session ${remoteSessionId}`);
@@ -298,7 +303,10 @@ export function agentuityCoderHub(pi: ExtensionAPI) {
298
303
  // This is how we discover what tools/agents the server provides.
299
304
  // ══════════════════════════════════════════════
300
305
 
301
- const initMsg = fetchInitMessageSync(hubUrl, agentRole);
306
+ const initialInitMsg = fetchInitMessageSync(hubUrl, agentRole);
307
+ const initMsg = initialInitMsg
308
+ ? adaptInitMessageForLocalTui(initialInitMsg, { isRemoteSession })
309
+ : null;
302
310
 
303
311
  if (!initMsg) {
304
312
  log('Hub not reachable — no tools or agents registered');
@@ -411,6 +419,18 @@ export function agentuityCoderHub(pi: ExtensionAPI) {
411
419
  // Titlebar: branding + spinner (registers its own event handlers)
412
420
  setupTitlebar(pi);
413
421
 
422
+ // Override Pi's built-in bash call-row rendering so local transcript rows
423
+ // can brand Agentuity CLI invocations without changing bash execution/result behavior.
424
+ const hasBashTool = serverTools.some((tool) => tool.name === 'bash');
425
+ const localBashRenderers = hasBashTool ? getToolRenderers('bash') : undefined;
426
+ if (hasBashTool && localBashRenderers?.renderCall) {
427
+ const bashToolDefinition = createBashToolDefinition(process.cwd());
428
+ pi.registerTool({
429
+ ...bashToolDefinition,
430
+ renderCall: localBashRenderers.renderCall as ToolDefinition['renderCall'],
431
+ });
432
+ }
433
+
414
434
  // ══════════════════════════════════════════════
415
435
  // WebSocket client for runtime communication (tool execution + events)
416
436
  // ══════════════════════════════════════════════
@@ -453,9 +473,10 @@ export function agentuityCoderHub(pi: ExtensionAPI) {
453
473
  }
454
474
 
455
475
  function applyInitMessage(nextInit: InitMessage): void {
456
- cachedInitMessage = nextInit;
457
- if (nextInit.sessionId) currentSessionId = nextInit.sessionId;
458
- if (nextInit.config) hubConfig = nextInit.config;
476
+ const effectiveInit = adaptInitMessageForLocalTui(nextInit, { isRemoteSession });
477
+ cachedInitMessage = effectiveInit;
478
+ if (effectiveInit.sessionId) currentSessionId = effectiveInit.sessionId;
479
+ if (effectiveInit.config) hubConfig = effectiveInit.config;
459
480
  }
460
481
 
461
482
  client.onInitMessage = (nextInit) => {
@@ -1755,13 +1776,7 @@ async function runSubAgent(
1755
1776
  const { piSdk, piAi } = await loadPiSdk();
1756
1777
  // Runtime-resolved dynamic imports — exact types unavailable statically
1757
1778
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1758
- const {
1759
- createAgentSession,
1760
- DefaultResourceLoader,
1761
- SessionManager,
1762
- createCodingTools,
1763
- createReadOnlyTools,
1764
- } = piSdk as any;
1779
+ const { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager } = piSdk as any;
1765
1780
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1766
1781
  const { getModel } = piAi as any;
1767
1782
 
@@ -1783,13 +1798,16 @@ async function runSubAgent(
1783
1798
  // Sub-agents get Hub tools (memory, context7, etc.) via extensionFactories
1784
1799
  // so they work in both driver and TUI mode.
1785
1800
  const hubTools = agentConfig.hubTools ?? [];
1801
+ const cwd = process.cwd();
1802
+ const agentDir = getAgentDir();
1786
1803
 
1787
1804
  // Resource loader — no extensions (prevents recursive task tool registration),
1788
1805
  // no skills, agent's system prompt injected directly.
1789
1806
  // Hub tools are injected via extensionFactories so sub-agents can use
1790
1807
  // memory_recall, context7_search, etc.
1791
1808
  const subLoader = new DefaultResourceLoader({
1792
- cwd: process.cwd(),
1809
+ cwd,
1810
+ agentDir,
1793
1811
  noExtensions: true,
1794
1812
  extensionFactories:
1795
1813
  hubTools.length > 0
@@ -1808,9 +1826,12 @@ async function runSubAgent(
1808
1826
  });
1809
1827
  await subLoader.reload();
1810
1828
 
1811
- // Select tools based on readOnly flag
1812
- const cwd = process.cwd();
1813
- const tools = agentConfig.readOnly ? createReadOnlyTools(cwd) : createCodingTools(cwd);
1829
+ // Pi v0.68.x uses a name allowlist for both built-in and extension/custom tools.
1830
+ const builtInToolNames = selectSubAgentToolNames(agentConfig);
1831
+ const hubToolNames = hubTools
1832
+ .map((tool) => (typeof tool.name === 'string' ? tool.name.trim() : ''))
1833
+ .filter((name): name is string => name.length > 0);
1834
+ const tools = Array.from(new Set([...builtInToolNames, ...hubToolNames]));
1814
1835
 
1815
1836
  const { session } = await createAgentSession({
1816
1837
  // subModel is already untyped (from dynamic import) — createAgentSession is also dynamically imported
@@ -1824,7 +1845,8 @@ async function runSubAgent(
1824
1845
  | 'xhigh',
1825
1846
  tools,
1826
1847
  resourceLoader: subLoader,
1827
- sessionManager: SessionManager.inMemory('/tmp'),
1848
+ // Pi now tracks cwd per session, so bind in-memory sub-agents to the actual repo cwd.
1849
+ sessionManager: SessionManager.inMemory(cwd),
1828
1850
  });
1829
1851
  await session.bindExtensions({});
1830
1852
 
@@ -1860,25 +1882,19 @@ async function runSubAgent(
1860
1882
 
1861
1883
  if (evt.type === 'tool_execution_start') {
1862
1884
  const toolName = evt.toolName || evt.name || evt.tool || 'unknown';
1863
- let toolArgs = '';
1864
- if (evt.args && typeof evt.args === 'object') {
1865
- const args = evt.args as Record<string, unknown>;
1866
- if (args.command) toolArgs = String(args.command).slice(0, 60);
1867
- else if (args.filePath || args.path)
1868
- toolArgs = String(args.filePath || args.path);
1869
- else if (args.pattern) toolArgs = String(args.pattern).slice(0, 40);
1870
- else {
1871
- const first = Object.values(args)[0];
1872
- if (first) toolArgs = String(first).slice(0, 40);
1873
- }
1874
- }
1885
+ const display = formatToolDisplay(
1886
+ toolName,
1887
+ typeof evt.args === 'string' || (evt.args && typeof evt.args === 'object')
1888
+ ? (evt.args as string | Record<string, unknown>)
1889
+ : undefined
1890
+ );
1875
1891
 
1876
1892
  onProgress({
1877
1893
  agentName: agentConfig.name,
1878
1894
  status: 'tool_start',
1879
1895
  toolCallId: typeof evt.toolCallId === 'string' ? evt.toolCallId : undefined,
1880
- currentTool: toolName,
1881
- currentToolArgs: toolArgs,
1896
+ currentTool: display.toolName,
1897
+ currentToolArgs: display.toolArgs,
1882
1898
  elapsed,
1883
1899
  });
1884
1900
  } else if (evt.type === 'tool_execution_end') {
@@ -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
 
@@ -23,6 +23,7 @@ import {
23
23
  type RemoteLifecycleState,
24
24
  } from './remote-lifecycle.ts';
25
25
  import { resolveCoderOrgId } from './auth.ts';
26
+ import { formatToolDisplay } from './agentuity-cli.ts';
26
27
 
27
28
  const DEBUG = !!process.env['AGENTUITY_DEBUG'];
28
29
 
@@ -1001,18 +1002,23 @@ export async function setupRemoteMode(
1001
1002
  break;
1002
1003
 
1003
1004
  case 'tool_execution_start': {
1004
- const tool = (event as { toolName?: string }).toolName ?? 'tool';
1005
- 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;
1006
1009
  if (extensionCtxRef?.hasUI) {
1007
- setNonLifecycleWorkingMessage(`Running ${tool}...`);
1008
- extensionCtxRef.ui.setStatus('remote_activity', `Running ${tool}...`);
1010
+ setNonLifecycleWorkingMessage(`Running ${display.fullLabel}...`);
1011
+ extensionCtxRef.ui.setStatus('remote_activity', `Running ${display.fullLabel}...`);
1009
1012
  }
1010
- log(`Tool: ${tool}`);
1013
+ log(`Tool: ${display.fullLabel}`);
1011
1014
  break;
1012
1015
  }
1013
1016
 
1014
1017
  case 'tool_execution_end': {
1015
- 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;
1016
1022
  currentTool = null;
1017
1023
  if (extensionCtxRef?.hasUI) {
1018
1024
  clearWorkingMessage();
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
+ }