@moxxy/cli 0.0.11 → 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 (141) 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-N5JTPB6U.mjs +0 -820
  74. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  75. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  76. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  77. package/dist/chunk-QO2JONHP.mjs +0 -1131
  78. package/dist/chunk-RVAPILHA.mjs +0 -1242
  79. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  80. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  81. package/dist/chunk-SOFST2PV.mjs +0 -1242
  82. package/dist/chunk-TMZWETMH.mjs +0 -1242
  83. package/dist/chunk-TYD7NMMI.mjs +0 -581
  84. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  85. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  86. package/dist/chunk-UQZKODNW.mjs +0 -1124
  87. package/dist/chunk-USC6R2ON.mjs +0 -1242
  88. package/dist/chunk-W32EQCVC.mjs +0 -823
  89. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  90. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  91. package/dist/cli-2AIWTL6F.mjs +0 -8
  92. package/dist/cli-2QKJ5UUL.mjs +0 -8
  93. package/dist/cli-4RIS6DQX.mjs +0 -8
  94. package/dist/cli-5RH4VBBL.mjs +0 -7
  95. package/dist/cli-7MK4YGOP.mjs +0 -7
  96. package/dist/cli-B4KH6MZI.mjs +0 -8
  97. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  98. package/dist/cli-CVP26EL2.mjs +0 -8
  99. package/dist/cli-DDRVVNAV.mjs +0 -8
  100. package/dist/cli-E7U56QVQ.mjs +0 -8
  101. package/dist/cli-EQNRMLL3.mjs +0 -8
  102. package/dist/cli-F5RUHHH4.mjs +0 -8
  103. package/dist/cli-LX6FFSEF.mjs +0 -8
  104. package/dist/cli-LY74GWKR.mjs +0 -6
  105. package/dist/cli-MAT3ZJHI.mjs +0 -8
  106. package/dist/cli-NJXXTQYF.mjs +0 -8
  107. package/dist/cli-O4ZGFAZG.mjs +0 -8
  108. package/dist/cli-ORVLI3UQ.mjs +0 -8
  109. package/dist/cli-PV43ZVKA.mjs +0 -8
  110. package/dist/cli-REVD6ISM.mjs +0 -8
  111. package/dist/cli-TBX76KQX.mjs +0 -8
  112. package/dist/cli-TLX5ENVM.mjs +0 -8
  113. package/dist/cli-TNJHCBQA.mjs +0 -6
  114. package/dist/cli-TUX22CZP.mjs +0 -8
  115. package/dist/cli-XJVH7EEP.mjs +0 -8
  116. package/dist/cli-XXOW4VXJ.mjs +0 -8
  117. package/dist/cli-XZ5RESNB.mjs +0 -6
  118. package/dist/cli-YCBYZ76Q.mjs +0 -8
  119. package/dist/cli-ZLMQCU7X.mjs +0 -8
  120. package/dist/dist-2VGKJRBH.mjs +0 -6820
  121. package/dist/dist-37BNX4QG.mjs +0 -7081
  122. package/dist/dist-7LTHRYKA.mjs +0 -11569
  123. package/dist/dist-7XJPQW5C.mjs +0 -6950
  124. package/dist/dist-AYMVOW7T.mjs +0 -7123
  125. package/dist/dist-FAXRJMEN.mjs +0 -6812
  126. package/dist/dist-HQGANM3P.mjs +0 -6976
  127. package/dist/dist-KATLOZQV.mjs +0 -7054
  128. package/dist/dist-KLSB6YHV.mjs +0 -6964
  129. package/dist/dist-LKIOZQ42.mjs +0 -17
  130. package/dist/dist-UYA4RJUH.mjs +0 -2792
  131. package/dist/dist-ZYHCBILM.mjs +0 -6993
  132. package/dist/index.d.mts +0 -23
  133. package/dist/index.d.ts +0 -23
  134. package/dist/index.js +0 -25512
  135. package/dist/index.mjs +0 -18
  136. package/dist/src-APP5P3UD.mjs +0 -1386
  137. package/dist/src-D5HMDDVE.mjs +0 -1324
  138. package/dist/src-EK3WD4AU.mjs +0 -1327
  139. package/dist/src-LSZFLMFN.mjs +0 -1400
  140. package/dist/src-WIOCZRAC.mjs +0 -1397
  141. 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
+ }