@moxxy/cli 0.0.12 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) 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 +341 -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 +767 -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/skill.js +125 -0
  18. package/src/commands/template.js +237 -0
  19. package/src/commands/uninstall.js +196 -0
  20. package/src/commands/update.js +406 -0
  21. package/src/commands/vault.js +219 -0
  22. package/src/help.js +368 -0
  23. package/src/lib/plugin-registry.js +98 -0
  24. package/src/platform.js +40 -0
  25. package/src/sse-client.js +79 -0
  26. package/src/tui/action-wizards.js +130 -0
  27. package/src/tui/app.jsx +859 -0
  28. package/src/tui/components/action-picker.jsx +86 -0
  29. package/src/tui/components/chat-panel.jsx +120 -0
  30. package/src/tui/components/footer.jsx +13 -0
  31. package/src/tui/components/header.jsx +45 -0
  32. package/src/tui/components/input-area.jsx +384 -0
  33. package/src/tui/components/messages/ask-message.jsx +13 -0
  34. package/src/tui/components/messages/assistant-message.jsx +165 -0
  35. package/src/tui/components/messages/channel-message.jsx +18 -0
  36. package/src/tui/components/messages/event-message.jsx +22 -0
  37. package/src/tui/components/messages/hive-status.jsx +34 -0
  38. package/src/tui/components/messages/skill-message.jsx +31 -0
  39. package/src/tui/components/messages/system-message.jsx +12 -0
  40. package/src/tui/components/messages/thinking.jsx +25 -0
  41. package/src/tui/components/messages/tool-group.jsx +62 -0
  42. package/src/tui/components/messages/tool-message.jsx +66 -0
  43. package/src/tui/components/messages/user-message.jsx +12 -0
  44. package/src/tui/components/model-picker.jsx +138 -0
  45. package/src/tui/components/multiline-input.jsx +72 -0
  46. package/src/tui/events-handler.js +730 -0
  47. package/src/tui/helpers.js +59 -0
  48. package/src/tui/hooks/use-command-handler.js +451 -0
  49. package/src/tui/index.jsx +55 -0
  50. package/src/tui/input-utils.js +26 -0
  51. package/src/tui/markdown-renderer.js +66 -0
  52. package/src/tui/mcp-wizard.js +136 -0
  53. package/src/tui/model-picker.js +174 -0
  54. package/src/tui/slash-commands.js +26 -0
  55. package/src/tui/store.js +12 -0
  56. package/src/tui/theme.js +17 -0
  57. package/src/ui.js +109 -0
  58. package/bin/moxxy.js +0 -2
  59. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  60. package/dist/chunk-2FZEA3NG.mjs +0 -457
  61. package/dist/chunk-3KDPLS22.mjs +0 -1131
  62. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  63. package/dist/chunk-6DZX6EAA.mjs +0 -37
  64. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  65. package/dist/chunk-C46NSEKG.mjs +0 -211
  66. package/dist/chunk-CAUXONEF.mjs +0 -1131
  67. package/dist/chunk-CPL5V56X.mjs +0 -1131
  68. package/dist/chunk-CTBVTTBG.mjs +0 -440
  69. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  70. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  71. package/dist/chunk-GSNMMI3H.mjs +0 -530
  72. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  73. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  74. package/dist/chunk-J33O35WX.mjs +0 -532
  75. package/dist/chunk-N5JTPB6U.mjs +0 -820
  76. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  77. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  78. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  79. package/dist/chunk-QO2JONHP.mjs +0 -1131
  80. package/dist/chunk-RVAPILHA.mjs +0 -1242
  81. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  82. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  83. package/dist/chunk-SOFST2PV.mjs +0 -1242
  84. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  85. package/dist/chunk-TMZWETMH.mjs +0 -1242
  86. package/dist/chunk-TYD7NMMI.mjs +0 -581
  87. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  88. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  89. package/dist/chunk-UQZKODNW.mjs +0 -1124
  90. package/dist/chunk-USC6R2ON.mjs +0 -1242
  91. package/dist/chunk-W32EQCVC.mjs +0 -823
  92. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  93. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  94. package/dist/cli-2AIWTL6F.mjs +0 -8
  95. package/dist/cli-2QKJ5UUL.mjs +0 -8
  96. package/dist/cli-4RIS6DQX.mjs +0 -8
  97. package/dist/cli-5RH4VBBL.mjs +0 -7
  98. package/dist/cli-7MK4YGOP.mjs +0 -7
  99. package/dist/cli-B4KH6MZI.mjs +0 -8
  100. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  101. package/dist/cli-CVP26EL2.mjs +0 -8
  102. package/dist/cli-DDRVVNAV.mjs +0 -8
  103. package/dist/cli-E7U56QVQ.mjs +0 -8
  104. package/dist/cli-EQNRMLL3.mjs +0 -8
  105. package/dist/cli-F5RUHHH4.mjs +0 -8
  106. package/dist/cli-LX6FFSEF.mjs +0 -8
  107. package/dist/cli-LY74GWKR.mjs +0 -6
  108. package/dist/cli-MAT3ZJHI.mjs +0 -8
  109. package/dist/cli-NJXXTQYF.mjs +0 -8
  110. package/dist/cli-O4ZGFAZG.mjs +0 -8
  111. package/dist/cli-ORVLI3UQ.mjs +0 -8
  112. package/dist/cli-PV43ZVKA.mjs +0 -8
  113. package/dist/cli-REVD6ISM.mjs +0 -8
  114. package/dist/cli-TBX76KQX.mjs +0 -8
  115. package/dist/cli-THCGF7SQ.mjs +0 -8
  116. package/dist/cli-TLX5ENVM.mjs +0 -8
  117. package/dist/cli-TMNI5ZYE.mjs +0 -8
  118. package/dist/cli-TNJHCBQA.mjs +0 -6
  119. package/dist/cli-TUX22CZP.mjs +0 -8
  120. package/dist/cli-XJVH7EEP.mjs +0 -8
  121. package/dist/cli-XXOW4VXJ.mjs +0 -8
  122. package/dist/cli-XZ5RESNB.mjs +0 -6
  123. package/dist/cli-YCBYZ76Q.mjs +0 -8
  124. package/dist/cli-ZLMQCU7X.mjs +0 -8
  125. package/dist/dist-2VGKJRBH.mjs +0 -6820
  126. package/dist/dist-37BNX4QG.mjs +0 -7081
  127. package/dist/dist-7LTHRYKA.mjs +0 -11569
  128. package/dist/dist-7XJPQW5C.mjs +0 -6950
  129. package/dist/dist-AYMVOW7T.mjs +0 -7123
  130. package/dist/dist-BHUWCDRS.mjs +0 -7132
  131. package/dist/dist-FAXRJMEN.mjs +0 -6812
  132. package/dist/dist-HQGANM3P.mjs +0 -6976
  133. package/dist/dist-KATLOZQV.mjs +0 -7054
  134. package/dist/dist-KLSB6YHV.mjs +0 -6964
  135. package/dist/dist-LKIOZQ42.mjs +0 -17
  136. package/dist/dist-UYA4RJUH.mjs +0 -2792
  137. package/dist/dist-ZYHCBILM.mjs +0 -6993
  138. package/dist/index.d.mts +0 -23
  139. package/dist/index.d.ts +0 -23
  140. package/dist/index.js +0 -25531
  141. package/dist/index.mjs +0 -18
  142. package/dist/src-APP5P3UD.mjs +0 -1386
  143. package/dist/src-D5HMDDVE.mjs +0 -1324
  144. package/dist/src-EK3WD4AU.mjs +0 -1327
  145. package/dist/src-LSZFLMFN.mjs +0 -1400
  146. package/dist/src-T77DFTFP.mjs +0 -1407
  147. package/dist/src-WIOCZRAC.mjs +0 -1397
  148. 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
+ }