@moxxy/cli 0.0.12 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +349 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +954 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/settings.js +224 -0
  18. package/src/commands/skill.js +125 -0
  19. package/src/commands/template.js +237 -0
  20. package/src/commands/uninstall.js +196 -0
  21. package/src/commands/update.js +406 -0
  22. package/src/commands/vault.js +219 -0
  23. package/src/help.js +392 -0
  24. package/src/lib/plugin-registry.js +98 -0
  25. package/src/platform.js +40 -0
  26. package/src/sse-client.js +79 -0
  27. package/src/tui/action-wizards.js +130 -0
  28. package/src/tui/app.jsx +859 -0
  29. package/src/tui/components/action-picker.jsx +86 -0
  30. package/src/tui/components/chat-panel.jsx +120 -0
  31. package/src/tui/components/footer.jsx +13 -0
  32. package/src/tui/components/header.jsx +45 -0
  33. package/src/tui/components/input-area.jsx +384 -0
  34. package/src/tui/components/messages/ask-message.jsx +13 -0
  35. package/src/tui/components/messages/assistant-message.jsx +165 -0
  36. package/src/tui/components/messages/channel-message.jsx +18 -0
  37. package/src/tui/components/messages/event-message.jsx +22 -0
  38. package/src/tui/components/messages/hive-status.jsx +34 -0
  39. package/src/tui/components/messages/skill-message.jsx +31 -0
  40. package/src/tui/components/messages/system-message.jsx +12 -0
  41. package/src/tui/components/messages/thinking.jsx +25 -0
  42. package/src/tui/components/messages/tool-group.jsx +62 -0
  43. package/src/tui/components/messages/tool-message.jsx +66 -0
  44. package/src/tui/components/messages/user-message.jsx +12 -0
  45. package/src/tui/components/model-picker.jsx +138 -0
  46. package/src/tui/components/multiline-input.jsx +72 -0
  47. package/src/tui/events-handler.js +730 -0
  48. package/src/tui/helpers.js +59 -0
  49. package/src/tui/hooks/use-command-handler.js +451 -0
  50. package/src/tui/index.jsx +55 -0
  51. package/src/tui/input-utils.js +26 -0
  52. package/src/tui/markdown-renderer.js +66 -0
  53. package/src/tui/mcp-wizard.js +136 -0
  54. package/src/tui/model-picker.js +174 -0
  55. package/src/tui/slash-commands.js +26 -0
  56. package/src/tui/store.js +12 -0
  57. package/src/tui/theme.js +17 -0
  58. package/src/ui.js +109 -0
  59. package/bin/moxxy.js +0 -2
  60. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  61. package/dist/chunk-2FZEA3NG.mjs +0 -457
  62. package/dist/chunk-3KDPLS22.mjs +0 -1131
  63. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  64. package/dist/chunk-6DZX6EAA.mjs +0 -37
  65. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  66. package/dist/chunk-C46NSEKG.mjs +0 -211
  67. package/dist/chunk-CAUXONEF.mjs +0 -1131
  68. package/dist/chunk-CPL5V56X.mjs +0 -1131
  69. package/dist/chunk-CTBVTTBG.mjs +0 -440
  70. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  71. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  72. package/dist/chunk-GSNMMI3H.mjs +0 -530
  73. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  74. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  75. package/dist/chunk-J33O35WX.mjs +0 -532
  76. package/dist/chunk-N5JTPB6U.mjs +0 -820
  77. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  78. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  79. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  80. package/dist/chunk-QO2JONHP.mjs +0 -1131
  81. package/dist/chunk-RVAPILHA.mjs +0 -1242
  82. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  83. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  84. package/dist/chunk-SOFST2PV.mjs +0 -1242
  85. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  86. package/dist/chunk-TMZWETMH.mjs +0 -1242
  87. package/dist/chunk-TYD7NMMI.mjs +0 -581
  88. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  89. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  90. package/dist/chunk-UQZKODNW.mjs +0 -1124
  91. package/dist/chunk-USC6R2ON.mjs +0 -1242
  92. package/dist/chunk-W32EQCVC.mjs +0 -823
  93. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  94. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  95. package/dist/cli-2AIWTL6F.mjs +0 -8
  96. package/dist/cli-2QKJ5UUL.mjs +0 -8
  97. package/dist/cli-4RIS6DQX.mjs +0 -8
  98. package/dist/cli-5RH4VBBL.mjs +0 -7
  99. package/dist/cli-7MK4YGOP.mjs +0 -7
  100. package/dist/cli-B4KH6MZI.mjs +0 -8
  101. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  102. package/dist/cli-CVP26EL2.mjs +0 -8
  103. package/dist/cli-DDRVVNAV.mjs +0 -8
  104. package/dist/cli-E7U56QVQ.mjs +0 -8
  105. package/dist/cli-EQNRMLL3.mjs +0 -8
  106. package/dist/cli-F5RUHHH4.mjs +0 -8
  107. package/dist/cli-LX6FFSEF.mjs +0 -8
  108. package/dist/cli-LY74GWKR.mjs +0 -6
  109. package/dist/cli-MAT3ZJHI.mjs +0 -8
  110. package/dist/cli-NJXXTQYF.mjs +0 -8
  111. package/dist/cli-O4ZGFAZG.mjs +0 -8
  112. package/dist/cli-ORVLI3UQ.mjs +0 -8
  113. package/dist/cli-PV43ZVKA.mjs +0 -8
  114. package/dist/cli-REVD6ISM.mjs +0 -8
  115. package/dist/cli-TBX76KQX.mjs +0 -8
  116. package/dist/cli-THCGF7SQ.mjs +0 -8
  117. package/dist/cli-TLX5ENVM.mjs +0 -8
  118. package/dist/cli-TMNI5ZYE.mjs +0 -8
  119. package/dist/cli-TNJHCBQA.mjs +0 -6
  120. package/dist/cli-TUX22CZP.mjs +0 -8
  121. package/dist/cli-XJVH7EEP.mjs +0 -8
  122. package/dist/cli-XXOW4VXJ.mjs +0 -8
  123. package/dist/cli-XZ5RESNB.mjs +0 -6
  124. package/dist/cli-YCBYZ76Q.mjs +0 -8
  125. package/dist/cli-ZLMQCU7X.mjs +0 -8
  126. package/dist/dist-2VGKJRBH.mjs +0 -6820
  127. package/dist/dist-37BNX4QG.mjs +0 -7081
  128. package/dist/dist-7LTHRYKA.mjs +0 -11569
  129. package/dist/dist-7XJPQW5C.mjs +0 -6950
  130. package/dist/dist-AYMVOW7T.mjs +0 -7123
  131. package/dist/dist-BHUWCDRS.mjs +0 -7132
  132. package/dist/dist-FAXRJMEN.mjs +0 -6812
  133. package/dist/dist-HQGANM3P.mjs +0 -6976
  134. package/dist/dist-KATLOZQV.mjs +0 -7054
  135. package/dist/dist-KLSB6YHV.mjs +0 -6964
  136. package/dist/dist-LKIOZQ42.mjs +0 -17
  137. package/dist/dist-UYA4RJUH.mjs +0 -2792
  138. package/dist/dist-ZYHCBILM.mjs +0 -6993
  139. package/dist/index.d.mts +0 -23
  140. package/dist/index.d.ts +0 -23
  141. package/dist/index.js +0 -25531
  142. package/dist/index.mjs +0 -18
  143. package/dist/src-APP5P3UD.mjs +0 -1386
  144. package/dist/src-D5HMDDVE.mjs +0 -1324
  145. package/dist/src-EK3WD4AU.mjs +0 -1327
  146. package/dist/src-LSZFLMFN.mjs +0 -1400
  147. package/dist/src-T77DFTFP.mjs +0 -1407
  148. package/dist/src-WIOCZRAC.mjs +0 -1397
  149. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,859 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { Box, useInput, useApp, useStdout } from 'ink';
3
+ import { Header } from './components/header.jsx';
4
+ import { ChatPanel } from './components/chat-panel.jsx';
5
+ import { InputArea } from './components/input-area.jsx';
6
+ import { ActionPicker } from './components/action-picker.jsx';
7
+ import { ModelPicker } from './components/model-picker.jsx';
8
+ import { EventsHandler } from './events-handler.js';
9
+ import { useEventsStore } from './store.js';
10
+ import { useCommandHandler } from './hooks/use-command-handler.js';
11
+ import {
12
+ buildModelPickerEntries,
13
+ clampPickerScroll,
14
+ findFirstSelectableIndex,
15
+ movePickerSelection,
16
+ } from './model-picker.js';
17
+ import {
18
+ createMcpAddWizard,
19
+ getMcpAddWizardPrompt,
20
+ submitMcpAddWizardValue,
21
+ } from './mcp-wizard.js';
22
+ import {
23
+ buildVaultRemovePickerItems,
24
+ createTemplateAssignWizard,
25
+ createVaultSetWizard,
26
+ getActionWizardPrompt,
27
+ submitActionWizardValue,
28
+ } from './action-wizards.js';
29
+
30
+ const SCROLL_LINES = 3;
31
+
32
+ function useTerminalHeight() {
33
+ const { stdout } = useStdout();
34
+ const [height, setHeight] = useState(stdout?.rows || process.stdout.rows || 24);
35
+
36
+ useEffect(() => {
37
+ const target = stdout || process.stdout;
38
+ const onResize = () => setHeight(target.rows || 24);
39
+ target.on('resize', onResize);
40
+ return () => target.off('resize', onResize);
41
+ }, [stdout]);
42
+
43
+ return height;
44
+ }
45
+
46
+ export function App({ client, agentId, debug, onExit }) {
47
+ const { exit } = useApp();
48
+ const termHeight = useTerminalHeight();
49
+ const handlerRef = useRef(null);
50
+ if (!handlerRef.current) {
51
+ handlerRef.current = new EventsHandler(client, agentId, { debug });
52
+ }
53
+ const eventsHandler = handlerRef.current;
54
+
55
+ const [agent, setAgent] = useState(null);
56
+ const [contextWindow, setContextWindow] = useState(0);
57
+ const [scrollOffset, setScrollOffset] = useState(0);
58
+ const [toolsExpanded, setToolsExpanded] = useState(false);
59
+ const [actionPicker, setActionPicker] = useState(null);
60
+ const [modelPicker, setModelPicker] = useState(null);
61
+ const modelMetaKeyRef = useRef(null);
62
+ const pollRef = useRef(null);
63
+ const agentRef = useRef(null);
64
+ const handleSubmitRef = useRef(null);
65
+
66
+ agentRef.current = agent;
67
+
68
+ const snapshot = useEventsStore(eventsHandler);
69
+ const currentCustomSelection = useCallback((models) => {
70
+ const currentAgent = agentRef.current;
71
+ if (!currentAgent?.provider_id || !currentAgent?.model_id) return null;
72
+ const hasExactMatch = (models || []).some(model =>
73
+ model.provider_id === currentAgent.provider_id && model.model_id === currentAgent.model_id
74
+ );
75
+ if (hasExactMatch) return null;
76
+ return {
77
+ provider_id: currentAgent.provider_id,
78
+ model_id: currentAgent.model_id,
79
+ };
80
+ }, []);
81
+
82
+ const pickerVisibleRows = Math.max(4, Math.min(10, termHeight - 18));
83
+
84
+ const openActionPicker = useCallback((title, items, previousPicker = null) => {
85
+ if (!items || items.length === 0) return;
86
+ setActionPicker({
87
+ mode: 'list',
88
+ title,
89
+ items,
90
+ selected: 0,
91
+ scroll: 0,
92
+ status: null,
93
+ previousPicker,
94
+ });
95
+ }, []);
96
+
97
+ const openMcpTransportPicker = useCallback(() => {
98
+ openActionPicker('MCP Transport', [
99
+ { label: 'stdio', description: 'Local process via stdin/stdout', command: '/mcp add stdio' },
100
+ { label: 'sse', description: 'Remote server via SSE', command: '/mcp add sse' },
101
+ { label: 'streamable_http', description: 'Remote server via Streamable HTTP', command: '/mcp add streamable_http' },
102
+ ]);
103
+ }, [openActionPicker]);
104
+
105
+ const openMcpAddWizard = useCallback((transport, previousPicker = null) => {
106
+ const wizard = createMcpAddWizard(transport);
107
+ const prompt = getMcpAddWizardPrompt(wizard);
108
+ setActionPicker({
109
+ mode: 'input',
110
+ title: prompt.title,
111
+ inputLabel: prompt.label,
112
+ placeholder: prompt.placeholder,
113
+ stepLabel: `${transport} · step 1`,
114
+ value: '',
115
+ status: null,
116
+ flow: 'mcp-add',
117
+ wizard,
118
+ previousPicker,
119
+ });
120
+ }, []);
121
+
122
+ const openInputWizard = useCallback((wizard, previousPicker = null) => {
123
+ const prompt = getActionWizardPrompt(wizard);
124
+ setActionPicker({
125
+ mode: 'input',
126
+ title: prompt.title,
127
+ inputLabel: prompt.label,
128
+ placeholder: prompt.placeholder,
129
+ stepLabel: wizard.flow,
130
+ value: '',
131
+ status: null,
132
+ flow: wizard.flow,
133
+ wizard,
134
+ previousPicker,
135
+ });
136
+ }, []);
137
+
138
+ const openMcpServerPicker = useCallback(async (mode) => {
139
+ try {
140
+ const servers = await client.listMcpServers(agentId);
141
+ if (!Array.isArray(servers) || servers.length === 0) {
142
+ eventsHandler.addSystemMessage(`No MCP servers to ${mode}.`);
143
+ return;
144
+ }
145
+
146
+ openActionPicker(
147
+ mode === 'remove' ? 'Remove MCP Server' : 'Test MCP Server',
148
+ servers.map(server => ({
149
+ label: server.id,
150
+ description: `[${server.transport || 'unknown'}] ${server.enabled === false ? 'disabled' : 'enabled'}`,
151
+ command: `/mcp ${mode} ${server.id}`,
152
+ }))
153
+ );
154
+ } catch (err) {
155
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
156
+ }
157
+ }, [client, agentId, eventsHandler, openActionPicker]);
158
+
159
+ const openVaultRemovePicker = useCallback(async (previousPicker = null) => {
160
+ try {
161
+ const secrets = await client.listSecrets();
162
+ const items = buildVaultRemovePickerItems(secrets);
163
+ if (items.length === 0) {
164
+ eventsHandler.addSystemMessage('No vault secrets found.');
165
+ if (previousPicker) setActionPicker(previousPicker);
166
+ return;
167
+ }
168
+
169
+ openActionPicker('Remove Vault Secret', items, previousPicker);
170
+ } catch (err) {
171
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
172
+ if (previousPicker) setActionPicker(previousPicker);
173
+ }
174
+ }, [client, eventsHandler, openActionPicker]);
175
+
176
+ // Auto-scroll to bottom when new messages arrive
177
+ const prevMsgVersion = useRef(snapshot.messageVersion);
178
+ useEffect(() => {
179
+ if (snapshot.messageVersion !== prevMsgVersion.current) {
180
+ prevMsgVersion.current = snapshot.messageVersion;
181
+ setScrollOffset(0);
182
+ }
183
+ }, [snapshot.messageVersion]);
184
+
185
+ const scrollUp = useCallback(() => {
186
+ setScrollOffset(prev => prev + SCROLL_LINES);
187
+ }, []);
188
+
189
+ const scrollDown = useCallback(() => {
190
+ setScrollOffset(prev => Math.max(0, prev - SCROLL_LINES));
191
+ }, []);
192
+
193
+ const pageUp = useCallback(() => {
194
+ setScrollOffset(prev => prev + Math.max(5, termHeight - 10));
195
+ }, [termHeight]);
196
+
197
+ const pageDown = useCallback(() => {
198
+ setScrollOffset(prev => Math.max(0, prev - Math.max(5, termHeight - 10)));
199
+ }, [termHeight]);
200
+
201
+ const handleExit = useCallback(() => {
202
+ eventsHandler.disconnect();
203
+ if (pollRef.current) clearInterval(pollRef.current);
204
+ if (onExit) onExit();
205
+ else exit();
206
+ }, [eventsHandler, onExit, exit]);
207
+
208
+ const handleStop = useCallback(async () => {
209
+ const a = agentRef.current;
210
+ if (a && a.status === 'running') {
211
+ try {
212
+ await client.stopAgent(a.name);
213
+ setAgent(prev => ({ ...prev, status: 'idle' }));
214
+ eventsHandler._stopThinking();
215
+ eventsHandler.addSystemMessage('Agent stopped.');
216
+ } catch (err) {
217
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
218
+ }
219
+ }
220
+ }, [client, eventsHandler]);
221
+
222
+ const syncContextWindow = useCallback(async (force = false) => {
223
+ const a = agentRef.current;
224
+ if (!a?.provider_id || !a?.model_id) return;
225
+ const key = `${a.provider_id}/${a.model_id}`;
226
+ if (!force && modelMetaKeyRef.current === key) return;
227
+ try {
228
+ const models = await client.listModels(a.provider_id);
229
+ const selected = (models || []).find(m => m.model_id === a.model_id);
230
+ const cw = readContextWindow(selected?.metadata);
231
+ setContextWindow(cw);
232
+ modelMetaKeyRef.current = key;
233
+ } catch {
234
+ setContextWindow(0);
235
+ modelMetaKeyRef.current = null;
236
+ }
237
+ }, [client]);
238
+
239
+ const handleAgentUpdate = useCallback((patch) => {
240
+ setAgent(prev => prev ? { ...prev, ...patch } : prev);
241
+ }, []);
242
+
243
+ const applyModelSelection = useCallback(async (providerId, modelId) => {
244
+ await client.updateAgent(agentId, {
245
+ provider_id: providerId,
246
+ model_id: modelId,
247
+ });
248
+ handleAgentUpdate({ provider_id: providerId, model_id: modelId });
249
+ syncContextWindow(true);
250
+ eventsHandler.addSystemMessage(`Switched to ${providerId}/${modelId}.`);
251
+ }, [client, agentId, handleAgentUpdate, syncContextWindow, eventsHandler]);
252
+
253
+ const refreshModelPicker = useCallback((prev, query) => {
254
+ const entries = buildModelPickerEntries(
255
+ prev.providers,
256
+ prev.models,
257
+ query,
258
+ currentCustomSelection(prev.models)
259
+ );
260
+ const selected = findFirstSelectableIndex(entries);
261
+ return {
262
+ ...prev,
263
+ mode: 'browse',
264
+ query,
265
+ entries,
266
+ selected,
267
+ scroll: clampPickerScroll(selected, 0, pickerVisibleRows),
268
+ status: entries.length === 0 ? 'No models available.' : null,
269
+ };
270
+ }, [currentCustomSelection, pickerVisibleRows]);
271
+
272
+ const openModelPicker = useCallback(async () => {
273
+ try {
274
+ const providers = await client.listProviders();
275
+ if (!providers || providers.length === 0) {
276
+ eventsHandler.addSystemMessage('No providers found.');
277
+ return;
278
+ }
279
+
280
+ const modelGroups = await Promise.all(
281
+ providers.map(async (provider) => {
282
+ try {
283
+ const models = await client.listModels(provider.id);
284
+ return { provider, models: models || [] };
285
+ } catch {
286
+ return { provider, models: [] };
287
+ }
288
+ })
289
+ );
290
+
291
+ const models = modelGroups.flatMap(({ provider, models }) =>
292
+ models.map((model) => ({
293
+ provider_id: provider.id,
294
+ provider_name: provider.display_name || provider.id,
295
+ model_id: model.model_id,
296
+ model_name: model.display_name || model.model_id,
297
+ deployment: readDeployment(provider.id, model),
298
+ is_current:
299
+ provider.id === agentRef.current?.provider_id
300
+ && model.model_id === agentRef.current?.model_id,
301
+ metadata: model.metadata,
302
+ }))
303
+ );
304
+
305
+ const entries = buildModelPickerEntries(
306
+ providers,
307
+ models,
308
+ '',
309
+ currentCustomSelection(models)
310
+ );
311
+ const selected = findFirstSelectableIndex(entries);
312
+
313
+ setModelPicker({
314
+ mode: 'browse',
315
+ providers,
316
+ models,
317
+ query: '',
318
+ focus: 'list',
319
+ entries,
320
+ selected,
321
+ scroll: clampPickerScroll(selected, 0, pickerVisibleRows),
322
+ status: entries.length === 0 ? 'No models available.' : null,
323
+ });
324
+ } catch (err) {
325
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
326
+ }
327
+ }, [client, currentCustomSelection, eventsHandler, pickerVisibleRows]);
328
+
329
+ const { handleSubmit } = useCommandHandler({
330
+ client,
331
+ agent,
332
+ agentId,
333
+ eventsHandler,
334
+ onStop: handleStop,
335
+ onExit: handleExit,
336
+ onAgentUpdate: handleAgentUpdate,
337
+ onContextSync: () => syncContextWindow(true),
338
+ onOpenModelPicker: openModelPicker,
339
+ onOpenVaultPicker: async () => openActionPicker('Vault', [
340
+ { label: '/vault list', description: 'List vault secrets', command: '/vault list' },
341
+ { label: '/vault set', description: 'Set a vault secret', command: '/vault set' },
342
+ { label: '/vault remove', description: 'Remove a vault secret', command: '/vault remove' },
343
+ ]),
344
+ onOpenVaultSetWizard: async () => openInputWizard(createVaultSetWizard(), actionPicker),
345
+ onOpenVaultRemoveWizard: async () => openVaultRemovePicker(actionPicker),
346
+ onOpenMcpPicker: async () => openActionPicker('MCP', [
347
+ { label: '/mcp list', description: 'List MCP servers and tools', command: '/mcp list' },
348
+ { label: '/mcp add', description: 'Add an MCP server', command: '/mcp add' },
349
+ { label: '/mcp remove', description: 'Remove an MCP server', command: '/mcp remove' },
350
+ { label: '/mcp test', description: 'Test MCP server connection', command: '/mcp test' },
351
+ ]),
352
+ onOpenMcpTransportPicker: openMcpTransportPicker,
353
+ onOpenMcpServerPicker: openMcpServerPicker,
354
+ onOpenTemplatePicker: async () => openActionPicker('Template', [
355
+ { label: '/template list', description: 'List available templates', command: '/template list' },
356
+ { label: '/template assign', description: 'Assign a template to the agent', command: '/template assign' },
357
+ { label: '/template clear', description: 'Clear the current template', command: '/template clear' },
358
+ ]),
359
+ onOpenTemplateAssignWizard: async () => openInputWizard(createTemplateAssignWizard(), actionPicker),
360
+ });
361
+ handleSubmitRef.current = handleSubmit;
362
+
363
+ // Load agent + connect SSE on mount
364
+ useEffect(() => {
365
+ let cancelled = false;
366
+
367
+ async function init() {
368
+ try {
369
+ const agentData = await client.getAgent(agentId);
370
+ if (!cancelled) setAgent(agentData);
371
+ } catch (err) {
372
+ if (err.isGatewayDown) {
373
+ eventsHandler.addSystemMessage(err.message);
374
+ } else {
375
+ eventsHandler.addSystemMessage(`Error loading agent: ${err.message}`);
376
+ }
377
+ }
378
+
379
+ await eventsHandler.loadHistory(client, agentId);
380
+ eventsHandler.connect();
381
+ }
382
+
383
+ init();
384
+
385
+ return () => {
386
+ cancelled = true;
387
+ eventsHandler.disconnect();
388
+ if (pollRef.current) clearInterval(pollRef.current);
389
+ };
390
+ }, [client, agentId, eventsHandler]);
391
+
392
+ // Sync context window when agent loads/changes
393
+ useEffect(() => {
394
+ if (agent) syncContextWindow();
395
+ }, [agent?.provider_id, agent?.model_id, syncContextWindow]);
396
+
397
+ // Poll agent status
398
+ useEffect(() => {
399
+ if (!agent || agent.status !== 'running') return;
400
+
401
+ pollRef.current = setInterval(async () => {
402
+ try {
403
+ const updated = await client.getAgent(agentId);
404
+ setAgent(updated);
405
+ const key = `${updated.provider_id}/${updated.model_id}`;
406
+ if (modelMetaKeyRef.current !== key) {
407
+ syncContextWindow(true);
408
+ }
409
+ } catch { /* ignore polling errors */ }
410
+ }, 5000);
411
+
412
+ return () => {
413
+ if (pollRef.current) {
414
+ clearInterval(pollRef.current);
415
+ pollRef.current = null;
416
+ }
417
+ };
418
+ }, [agent?.status, client, agentId, syncContextWindow]);
419
+
420
+ // Global keybindings
421
+ useInput(async (input, key) => {
422
+ if (actionPicker) {
423
+ if (actionPicker.mode === 'input') {
424
+ if (key.escape) {
425
+ setActionPicker(actionPicker.previousPicker || null);
426
+ return;
427
+ }
428
+
429
+ if (key.return) {
430
+ if (actionPicker.flow === 'mcp-add') {
431
+ const result = submitMcpAddWizardValue(actionPicker.wizard, actionPicker.value);
432
+ if (!result.done) {
433
+ if (result.error) {
434
+ setActionPicker(prev => prev ? ({ ...prev, status: result.error }) : prev);
435
+ return;
436
+ }
437
+
438
+ const prompt = getMcpAddWizardPrompt(result.wizard);
439
+ const stepIndex = result.wizard.step === 'server_id' ? 2 : 1;
440
+ setActionPicker(prev => prev ? ({
441
+ ...prev,
442
+ inputLabel: prompt.label,
443
+ placeholder: prompt.placeholder,
444
+ stepLabel: `${result.wizard.transport} · step ${stepIndex}`,
445
+ value: '',
446
+ status: null,
447
+ wizard: result.wizard,
448
+ }) : prev);
449
+ return;
450
+ }
451
+
452
+ setActionPicker(null);
453
+ try {
454
+ await client.addMcpServer(agentId, result.payload);
455
+ eventsHandler.addSystemMessage(`MCP server "${result.payload.id}" added.`);
456
+ } catch (err) {
457
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
458
+ }
459
+ return;
460
+ }
461
+
462
+ if (actionPicker.flow === 'vault-set' || actionPicker.flow === 'vault-remove' || actionPicker.flow === 'template-assign') {
463
+ const result = submitActionWizardValue(actionPicker.wizard, actionPicker.value);
464
+ if (!result.done) {
465
+ if (result.error) {
466
+ setActionPicker(prev => prev ? ({ ...prev, status: result.error }) : prev);
467
+ return;
468
+ }
469
+
470
+ const prompt = getActionWizardPrompt(result.wizard);
471
+ setActionPicker(prev => prev ? ({
472
+ ...prev,
473
+ title: prompt.title,
474
+ inputLabel: prompt.label,
475
+ placeholder: prompt.placeholder,
476
+ value: '',
477
+ status: null,
478
+ wizard: result.wizard,
479
+ }) : prev);
480
+ return;
481
+ }
482
+
483
+ setActionPicker(null);
484
+ try {
485
+ if (actionPicker.flow === 'vault-set') {
486
+ await client.createSecret(result.payload);
487
+ eventsHandler.addSystemMessage(`Secret "${result.payload.key_name}" stored.`);
488
+ } else if (actionPicker.flow === 'vault-remove') {
489
+ const secrets = await client.listSecrets();
490
+ const match = secrets.find(s => s.key_name === result.payload.key_name);
491
+ if (!match) {
492
+ eventsHandler.addSystemMessage(`Secret "${result.payload.key_name}" not found.`);
493
+ } else {
494
+ await client.deleteSecret(match.id);
495
+ eventsHandler.addSystemMessage(`Secret "${result.payload.key_name}" removed.`);
496
+ }
497
+ } else if (actionPicker.flow === 'template-assign') {
498
+ await client.setAgentTemplate(agentId, result.payload.slug);
499
+ eventsHandler.addSystemMessage(`Template "${result.payload.slug}" assigned. Changes take effect on next run.`);
500
+ }
501
+ } catch (err) {
502
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
503
+ }
504
+ return;
505
+ }
506
+ return;
507
+ }
508
+
509
+ if (key.backspace || key.delete) {
510
+ setActionPicker(prev => prev ? ({
511
+ ...prev,
512
+ value: prev.value.slice(0, -1),
513
+ status: null,
514
+ }) : prev);
515
+ return;
516
+ }
517
+
518
+ if (!key.ctrl && !key.meta && !key.escape && input && !key.upArrow && !key.downArrow) {
519
+ setActionPicker(prev => prev ? ({
520
+ ...prev,
521
+ value: prev.value + input,
522
+ status: null,
523
+ }) : prev);
524
+ return;
525
+ }
526
+
527
+ return;
528
+ }
529
+
530
+ if (key.escape) {
531
+ setActionPicker(actionPicker.previousPicker || null);
532
+ return;
533
+ }
534
+
535
+ if (key.return) {
536
+ const item = actionPicker.items[actionPicker.selected];
537
+ if (!item) return;
538
+ if (item.command === '/mcp add stdio' || item.command === '/mcp add sse' || item.command === '/mcp add streamable_http') {
539
+ openMcpAddWizard(item.command.slice('/mcp add '.length).trim(), actionPicker);
540
+ return;
541
+ }
542
+ if (item.command === '/vault set') {
543
+ openInputWizard(createVaultSetWizard(), actionPicker);
544
+ return;
545
+ }
546
+ if (item.command === '/vault remove') {
547
+ await openVaultRemovePicker(actionPicker);
548
+ return;
549
+ }
550
+ if (item.command === '/template assign') {
551
+ openInputWizard(createTemplateAssignWizard(), actionPicker);
552
+ return;
553
+ }
554
+ setActionPicker(null);
555
+ await handleSubmitRef.current(item.command);
556
+ return;
557
+ }
558
+
559
+ if (key.upArrow) {
560
+ setActionPicker(prev => {
561
+ if (!prev) return prev;
562
+ const selected = Math.max(0, movePickerSelection(prev.items, prev.selected, -1));
563
+ return {
564
+ ...prev,
565
+ selected,
566
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
567
+ };
568
+ });
569
+ return;
570
+ }
571
+
572
+ if (key.downArrow) {
573
+ setActionPicker(prev => {
574
+ if (!prev) return prev;
575
+ const selected = Math.min(prev.items.length - 1, movePickerSelection(prev.items, prev.selected, 1));
576
+ return {
577
+ ...prev,
578
+ selected,
579
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
580
+ };
581
+ });
582
+ return;
583
+ }
584
+
585
+ if (key.pageUp) {
586
+ setActionPicker(prev => {
587
+ if (!prev) return prev;
588
+ const selected = Math.max(0, prev.selected - pickerVisibleRows);
589
+ return {
590
+ ...prev,
591
+ selected,
592
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
593
+ };
594
+ });
595
+ return;
596
+ }
597
+
598
+ if (key.pageDown) {
599
+ setActionPicker(prev => {
600
+ if (!prev) return prev;
601
+ const selected = Math.min(prev.items.length - 1, prev.selected + pickerVisibleRows);
602
+ return {
603
+ ...prev,
604
+ selected,
605
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
606
+ };
607
+ });
608
+ return;
609
+ }
610
+
611
+ return;
612
+ }
613
+
614
+ if (modelPicker) {
615
+ if (modelPicker.mode === 'browse') {
616
+ if (key.escape) {
617
+ setModelPicker(null);
618
+ return;
619
+ }
620
+
621
+ if (key.return) {
622
+ const entry = modelPicker.entries[modelPicker.selected];
623
+ if (!entry || entry.type === 'section') return;
624
+
625
+ if (entry.type === 'custom') {
626
+ setModelPicker(prev => prev ? {
627
+ ...prev,
628
+ mode: 'custom',
629
+ providerId: entry.provider_id,
630
+ providerName: entry.provider_name,
631
+ value: entry.current_model_id || '',
632
+ status: null,
633
+ } : prev);
634
+ return;
635
+ }
636
+
637
+ setModelPicker(null);
638
+ try {
639
+ await applyModelSelection(entry.provider_id, entry.model_id);
640
+ } catch (err) {
641
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
642
+ }
643
+ return;
644
+ }
645
+
646
+ if (key.upArrow) {
647
+ setModelPicker(prev => {
648
+ if (!prev) return prev;
649
+ const selected = movePickerSelection(prev.entries, prev.selected, -1);
650
+ return {
651
+ ...prev,
652
+ selected,
653
+ focus: 'list',
654
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
655
+ };
656
+ });
657
+ return;
658
+ }
659
+
660
+ if (key.downArrow) {
661
+ setModelPicker(prev => {
662
+ if (!prev) return prev;
663
+ const selected = movePickerSelection(prev.entries, prev.selected, 1);
664
+ return {
665
+ ...prev,
666
+ selected,
667
+ focus: 'list',
668
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
669
+ };
670
+ });
671
+ return;
672
+ }
673
+
674
+ if (key.pageUp) {
675
+ setModelPicker(prev => {
676
+ if (!prev) return prev;
677
+ let selected = prev.selected;
678
+ for (let i = 0; i < pickerVisibleRows; i++) {
679
+ selected = movePickerSelection(prev.entries, selected, -1);
680
+ }
681
+ return {
682
+ ...prev,
683
+ selected,
684
+ focus: 'list',
685
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
686
+ };
687
+ });
688
+ return;
689
+ }
690
+
691
+ if (key.pageDown) {
692
+ setModelPicker(prev => {
693
+ if (!prev) return prev;
694
+ let selected = prev.selected;
695
+ for (let i = 0; i < pickerVisibleRows; i++) {
696
+ selected = movePickerSelection(prev.entries, selected, 1);
697
+ }
698
+ return {
699
+ ...prev,
700
+ selected,
701
+ focus: 'list',
702
+ scroll: clampPickerScroll(selected, prev.scroll, pickerVisibleRows),
703
+ };
704
+ });
705
+ return;
706
+ }
707
+
708
+ if (key.tab) {
709
+ setModelPicker(prev => prev ? {
710
+ ...prev,
711
+ focus: prev.focus === 'search' ? 'list' : 'search',
712
+ } : prev);
713
+ return;
714
+ }
715
+
716
+ if ((key.backspace || key.delete) && modelPicker.focus === 'search') {
717
+ setModelPicker(prev => prev ? refreshModelPicker(prev, prev.query.slice(0, -1)) : prev);
718
+ return;
719
+ }
720
+
721
+ if (!key.ctrl && !key.meta && !key.escape && input && modelPicker.focus === 'search') {
722
+ setModelPicker(prev => prev ? refreshModelPicker(prev, prev.query + input) : prev);
723
+ return;
724
+ }
725
+
726
+ return;
727
+ }
728
+
729
+ if (modelPicker.mode === 'custom') {
730
+ if (key.escape) {
731
+ setModelPicker(prev => prev ? ({
732
+ ...prev,
733
+ mode: 'browse',
734
+ status: null,
735
+ }) : prev);
736
+ return;
737
+ }
738
+
739
+ if (key.return) {
740
+ const value = modelPicker.value.trim();
741
+ if (!value) {
742
+ setModelPicker(prev => prev ? ({ ...prev, status: 'Custom model ID cannot be empty.' }) : prev);
743
+ return;
744
+ }
745
+
746
+ const providerId = modelPicker.providerId;
747
+ setModelPicker(null);
748
+ try {
749
+ await applyModelSelection(providerId, value);
750
+ } catch (err) {
751
+ eventsHandler.addSystemMessage(`Error: ${err.message}`);
752
+ }
753
+ return;
754
+ }
755
+
756
+ if (key.backspace || key.delete) {
757
+ setModelPicker(prev => prev ? ({
758
+ ...prev,
759
+ value: prev.value.slice(0, -1),
760
+ status: null,
761
+ }) : prev);
762
+ return;
763
+ }
764
+
765
+ if (!key.ctrl && !key.meta && !key.escape && input && !key.upArrow && !key.downArrow) {
766
+ setModelPicker(prev => prev ? ({
767
+ ...prev,
768
+ value: prev.value + input,
769
+ status: null,
770
+ }) : prev);
771
+ return;
772
+ }
773
+
774
+ return;
775
+ }
776
+ }
777
+
778
+ if (key.ctrl && input === 'x') {
779
+ handleStop();
780
+ }
781
+ if (key.ctrl && input === 't') {
782
+ setToolsExpanded(prev => !prev);
783
+ }
784
+ // Shift+Up / Shift+Down for scroll
785
+ if (key.shift && key.upArrow) {
786
+ scrollUp();
787
+ }
788
+ if (key.shift && key.downArrow) {
789
+ scrollDown();
790
+ }
791
+ if (key.pageUp) {
792
+ pageUp();
793
+ }
794
+ if (key.pageDown) {
795
+ pageDown();
796
+ }
797
+ });
798
+
799
+ return (
800
+ <Box flexDirection="column" width="100%">
801
+ <Header agent={agent} />
802
+ <ChatPanel
803
+ messages={snapshot.messages}
804
+ thinking={snapshot.thinking}
805
+ agentName={agent?.name}
806
+ scrollOffset={scrollOffset}
807
+ toolsExpanded={toolsExpanded}
808
+ termHeight={termHeight}
809
+ />
810
+ {actionPicker && (
811
+ <ActionPicker picker={actionPicker} termHeight={termHeight} />
812
+ )}
813
+ {modelPicker && (
814
+ <ModelPicker picker={modelPicker} termHeight={termHeight} />
815
+ )}
816
+ <InputArea
817
+ onSubmit={handleSubmit}
818
+ onExit={handleExit}
819
+ onStop={handleStop}
820
+ pendingAsk={snapshot.pendingAsk}
821
+ agent={agent}
822
+ disabled={Boolean(actionPicker || modelPicker)}
823
+ />
824
+ </Box>
825
+ );
826
+ }
827
+
828
+ function readContextWindow(metadata) {
829
+ if (!metadata || typeof metadata !== 'object') return 0;
830
+ const candidates = [
831
+ metadata.context_window,
832
+ metadata.contextWindow,
833
+ metadata.max_context_tokens,
834
+ metadata.max_input_tokens,
835
+ metadata.input_token_limit,
836
+ ];
837
+ for (const value of candidates) {
838
+ const n = typeof value === 'string' ? Number.parseInt(value, 10) : Number(value);
839
+ if (Number.isFinite(n) && n > 0) return Math.floor(n);
840
+ }
841
+ return 0;
842
+ }
843
+
844
+ function readDeployment(providerId, model) {
845
+ const direct = typeof model?.deployment === 'string' ? model.deployment.trim().toLowerCase() : '';
846
+ if (direct) return direct;
847
+
848
+ const metadataDeployment = typeof model?.metadata?.deployment === 'string'
849
+ ? model.metadata.deployment.trim().toLowerCase()
850
+ : '';
851
+ if (metadataDeployment) return metadataDeployment;
852
+
853
+ if (providerId === 'ollama') {
854
+ const id = String(model?.model_id || '').toLowerCase();
855
+ return id.includes(':cloud') || id.includes('-cloud') ? 'cloud' : 'local';
856
+ }
857
+
858
+ return null;
859
+ }