@almyty/chat 1.2.0 → 1.3.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.
- package/README.md +163 -24
- package/dist/app.d.ts +3 -1
- package/dist/app.js +283 -177
- package/dist/args.d.ts +64 -0
- package/dist/args.js +207 -0
- package/dist/commands.d.ts +21 -1
- package/dist/commands.js +50 -4
- package/dist/components.d.ts +7 -1
- package/dist/components.js +18 -26
- package/dist/errors.d.ts +49 -0
- package/dist/errors.js +144 -0
- package/dist/exit-codes.d.ts +32 -0
- package/dist/exit-codes.js +65 -0
- package/dist/headless.d.ts +44 -0
- package/dist/headless.js +106 -0
- package/dist/history.d.ts +51 -0
- package/dist/history.js +146 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +112 -40
- package/dist/stream.d.ts +95 -0
- package/dist/stream.js +276 -0
- package/dist/turn.d.ts +53 -0
- package/dist/turn.js +147 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +27 -0
- package/dist/viewport.d.ts +61 -0
- package/dist/viewport.js +118 -0
- package/package.json +9 -5
package/dist/app.js
CHANGED
|
@@ -2,13 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
5
|
-
import { Header,
|
|
6
|
-
import { SLASH_COMMANDS, COMMAND_DESCS, resolveSlash,
|
|
5
|
+
import { Header, MessageWindow, AgentSelector, CodingModeIndicator, ChoiceSelector, } from './components.js';
|
|
6
|
+
import { SLASH_COMMANDS, COMMAND_DESCS, resolveSlash, ALIASES, classifyInput, continuationOf, buildCodeChoices, } from './commands.js';
|
|
7
|
+
import { explainError } from './errors.js';
|
|
8
|
+
import { addUsage, formatUsage } from './stream.js';
|
|
9
|
+
import { runTurn } from './turn.js';
|
|
10
|
+
import { appendHistory, loadHistory, walkHistory } from './history.js';
|
|
11
|
+
import { usableRows } from './viewport.js';
|
|
7
12
|
// Mutable module-level variable; written by ChatApp, read by main() after exit
|
|
8
13
|
export let exitMessage = '';
|
|
14
|
+
/** Rows the header, separator, prompt and status bar take off the top. */
|
|
15
|
+
const CHROME_ROWS = 10;
|
|
9
16
|
// ── Chat app ────────────────────────────────────────────────────
|
|
10
|
-
export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
17
|
+
export function ChatApp({ client, initialAgent, gw, resumeConversationId, errorContext }) {
|
|
11
18
|
const { exit } = useApp();
|
|
19
|
+
const agentRef = `${gw.orgSlug}/${gw.agentSlug}`;
|
|
20
|
+
const errCtx = { agentRef, ...errorContext };
|
|
12
21
|
const [state, setState] = useState({
|
|
13
22
|
agent: initialAgent,
|
|
14
23
|
messages: [],
|
|
@@ -17,6 +26,15 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
17
26
|
conversationId: resumeConversationId ?? null,
|
|
18
27
|
pendingRunId: null,
|
|
19
28
|
});
|
|
29
|
+
// Assistant text arriving right now, drawn below the transcript and
|
|
30
|
+
// committed as a message when the turn ends.
|
|
31
|
+
const [streaming, setStreaming] = useState('');
|
|
32
|
+
const [sessionUsage, setSessionUsage] = useState({ cost: 0, tokens: 0, steps: 0 });
|
|
33
|
+
const [lastRunId, setLastRunId] = useState(null);
|
|
34
|
+
// Lines of a message being typed across several Enter presses.
|
|
35
|
+
const [draft, setDraft] = useState([]);
|
|
36
|
+
// Redrawn on resize so the visible window matches the new size.
|
|
37
|
+
const [size, setSize] = useState({ rows: process.stdout.rows, columns: process.stdout.columns });
|
|
20
38
|
// Load conversation history on resume
|
|
21
39
|
useEffect(() => {
|
|
22
40
|
if (!resumeConversationId)
|
|
@@ -33,15 +51,27 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
33
51
|
}));
|
|
34
52
|
setState(s => ({ ...s, messages: msgs, loading: false }));
|
|
35
53
|
}
|
|
36
|
-
catch {
|
|
37
|
-
setState(s => ({
|
|
54
|
+
catch (err) {
|
|
55
|
+
setState(s => ({
|
|
56
|
+
...s,
|
|
57
|
+
loading: false,
|
|
58
|
+
messages: [...s.messages, { role: 'error', text: explainError(err, { ...errCtx, what: 'history' }) }],
|
|
59
|
+
}));
|
|
38
60
|
}
|
|
39
61
|
})();
|
|
40
62
|
}, [resumeConversationId]);
|
|
63
|
+
// ink redraws on resize, but the window arithmetic needs the new
|
|
64
|
+
// numbers in state to recompute with them.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const onResize = () => setSize({ rows: process.stdout.rows, columns: process.stdout.columns });
|
|
67
|
+
process.stdout.on('resize', onResize);
|
|
68
|
+
return () => { process.stdout.off('resize', onResize); };
|
|
69
|
+
}, []);
|
|
41
70
|
const [input, setInput] = useState('');
|
|
42
71
|
const [paletteCursor, setPaletteCursor] = useState(0);
|
|
43
|
-
// Input history
|
|
44
|
-
|
|
72
|
+
// Input history persisted across sessions, so up-arrow works in a
|
|
73
|
+
// fresh session and survives /clear.
|
|
74
|
+
const [inputHistory, setInputHistory] = useState(() => loadHistory());
|
|
45
75
|
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
46
76
|
const [showPicker, setShowPicker] = useState(false);
|
|
47
77
|
const [pickerAgents, setPickerAgents] = useState([]);
|
|
@@ -50,12 +80,54 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
50
80
|
// Pending runner x CLI pick for /code when multiple targets exist.
|
|
51
81
|
const [codeChoices, setCodeChoices] = useState(null);
|
|
52
82
|
const codingAbortRef = useRef(null);
|
|
83
|
+
// The in-flight turn, so Ctrl-C can stop it server-side.
|
|
84
|
+
const runAbortRef = useRef(null);
|
|
53
85
|
// Command palette matches
|
|
54
86
|
const slashMatches = input.startsWith('/') && !input.includes(' ')
|
|
55
87
|
? SLASH_COMMANDS.filter(c => c.startsWith(input.slice(1).toLowerCase()))
|
|
56
88
|
: [];
|
|
57
89
|
const paletteOpen = input.startsWith('/') && slashMatches.length > 0;
|
|
90
|
+
const addMessage = useCallback((msg) => {
|
|
91
|
+
setState(s => ({ ...s, messages: [...s.messages, msg] }));
|
|
92
|
+
}, []);
|
|
93
|
+
const quit = useCallback(() => {
|
|
94
|
+
setState(s => {
|
|
95
|
+
exitMessage = s.conversationId
|
|
96
|
+
? `\nTo resume: almyty chat ${agentRef} --resume ${s.conversationId}\n`
|
|
97
|
+
: '';
|
|
98
|
+
return s;
|
|
99
|
+
});
|
|
100
|
+
exit();
|
|
101
|
+
}, [exit, agentRef]);
|
|
58
102
|
useInput((ch, key) => {
|
|
103
|
+
// Ctrl-C and Ctrl-D come first: they have to work while a run is in
|
|
104
|
+
// flight, which is the only moment cancelling means anything.
|
|
105
|
+
if (key.ctrl && ch === 'c') {
|
|
106
|
+
if (runAbortRef.current) {
|
|
107
|
+
// Cancels the run where it runs, not just where it is watched.
|
|
108
|
+
runAbortRef.current.abort();
|
|
109
|
+
runAbortRef.current = null;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (codingAbortRef.current) {
|
|
113
|
+
codingAbortRef.current.abort();
|
|
114
|
+
codingAbortRef.current = null;
|
|
115
|
+
setCoding(null);
|
|
116
|
+
addMessage({ role: 'info', text: 'left coding mode (the session keeps running)' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (input) {
|
|
120
|
+
setInput('');
|
|
121
|
+
setDraft([]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
quit();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (key.ctrl && ch === 'd') {
|
|
128
|
+
quit();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
59
131
|
if (state.loading)
|
|
60
132
|
return;
|
|
61
133
|
// Command palette navigation
|
|
@@ -76,22 +148,17 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
76
148
|
return;
|
|
77
149
|
}
|
|
78
150
|
// Input history — up/down arrows always, like Claude Code
|
|
79
|
-
if (key.upArrow
|
|
80
|
-
const
|
|
81
|
-
setHistoryIdx(
|
|
82
|
-
|
|
151
|
+
if (key.upArrow) {
|
|
152
|
+
const walked = walkHistory(inputHistory, historyIdx, 'up');
|
|
153
|
+
setHistoryIdx(walked.idx);
|
|
154
|
+
if (walked.value)
|
|
155
|
+
setInput(walked.value);
|
|
83
156
|
return;
|
|
84
157
|
}
|
|
85
158
|
if (key.downArrow) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
setInput(inputHistory[inputHistory.length - 1 - newIdx]);
|
|
90
|
-
}
|
|
91
|
-
else if (historyIdx === 0) {
|
|
92
|
-
setHistoryIdx(-1);
|
|
93
|
-
setInput('');
|
|
94
|
-
}
|
|
159
|
+
const walked = walkHistory(inputHistory, historyIdx, 'down');
|
|
160
|
+
setHistoryIdx(walked.idx);
|
|
161
|
+
setInput(walked.value);
|
|
95
162
|
return;
|
|
96
163
|
}
|
|
97
164
|
});
|
|
@@ -100,9 +167,6 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
100
167
|
setInput(val);
|
|
101
168
|
setPaletteCursor(0);
|
|
102
169
|
}, []);
|
|
103
|
-
const addMessage = useCallback((msg) => {
|
|
104
|
-
setState(s => ({ ...s, messages: [...s.messages, msg] }));
|
|
105
|
-
}, []);
|
|
106
170
|
// ── Coding session helpers (chat-to-runner bridge) ──────────────
|
|
107
171
|
/**
|
|
108
172
|
* Append streamed CLI output to the transcript, merging into the last
|
|
@@ -161,14 +225,14 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
161
225
|
}, ac.signal).catch((err) => {
|
|
162
226
|
if (ac.signal.aborted)
|
|
163
227
|
return;
|
|
164
|
-
addMessage({ role: 'error', text: `coding stream lost: ${err
|
|
228
|
+
addMessage({ role: 'error', text: `coding stream lost: ${explainError(err, errCtx)}` });
|
|
165
229
|
codingAbortRef.current = null;
|
|
166
230
|
setCoding(null);
|
|
167
231
|
});
|
|
168
232
|
}
|
|
169
233
|
catch (err) {
|
|
170
234
|
setState(s => ({ ...s, loading: false }));
|
|
171
|
-
addMessage({ role: 'error', text: err
|
|
235
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
172
236
|
}
|
|
173
237
|
}, [client, addMessage, appendCodingOutput]);
|
|
174
238
|
/** Detach from coding mode (abort the stream; the remote session is untouched). */
|
|
@@ -178,19 +242,89 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
178
242
|
setCoding(null);
|
|
179
243
|
addMessage({ role: 'info', text: note });
|
|
180
244
|
}, [addMessage]);
|
|
245
|
+
// ── Turns ───────────────────────────────────────────────────────
|
|
246
|
+
/** Send one message and stream the answer into the transcript. */
|
|
247
|
+
const sendMessage = useCallback(async (text) => {
|
|
248
|
+
addMessage({ role: 'user', text });
|
|
249
|
+
setStreaming('');
|
|
250
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Thinking' }));
|
|
251
|
+
const ac = new AbortController();
|
|
252
|
+
runAbortRef.current = ac;
|
|
253
|
+
let result;
|
|
254
|
+
try {
|
|
255
|
+
result = await runTurn(gw, text, {
|
|
256
|
+
mode: state.agent.mode,
|
|
257
|
+
conversationId: state.conversationId ?? undefined,
|
|
258
|
+
pendingRunId: state.pendingRunId ?? undefined,
|
|
259
|
+
signal: ac.signal,
|
|
260
|
+
hooks: {
|
|
261
|
+
partial: setStreaming,
|
|
262
|
+
activity: (activity) => addMessage(activity),
|
|
263
|
+
label: (label) => setState(s => ({ ...s, loadingLabel: label })),
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
runAbortRef.current = null;
|
|
269
|
+
setStreaming('');
|
|
270
|
+
setState(s => ({ ...s, loading: false, pendingRunId: null }));
|
|
271
|
+
addMessage({ role: 'error', text: explainError(err, { ...errCtx, what: 'run' }) });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
runAbortRef.current = null;
|
|
275
|
+
setStreaming('');
|
|
276
|
+
setState(s => ({
|
|
277
|
+
...s,
|
|
278
|
+
loading: false,
|
|
279
|
+
conversationId: result.conversationId ?? s.conversationId,
|
|
280
|
+
pendingRunId: result.pendingRunId ?? null,
|
|
281
|
+
}));
|
|
282
|
+
if (result.runId)
|
|
283
|
+
setLastRunId(result.runId);
|
|
284
|
+
setSessionUsage(u => addUsage(u, result.usage));
|
|
285
|
+
if (result.text)
|
|
286
|
+
addMessage({ role: 'agent', text: result.text });
|
|
287
|
+
const attribution = formatUsage(result.usage);
|
|
288
|
+
if (attribution)
|
|
289
|
+
addMessage({ role: 'info', text: attribution });
|
|
290
|
+
if (result.status === 'failed') {
|
|
291
|
+
addMessage({ role: 'error', text: result.error ?? 'The run failed' });
|
|
292
|
+
}
|
|
293
|
+
else if (result.status === 'cancelled') {
|
|
294
|
+
addMessage({ role: 'info', text: 'cancelled — the run was stopped server-side too' });
|
|
295
|
+
}
|
|
296
|
+
else if (result.status === 'waiting_input') {
|
|
297
|
+
addMessage({ role: 'info', text: 'the agent is waiting for your answer' });
|
|
298
|
+
}
|
|
299
|
+
}, [state.agent.mode, state.conversationId, state.pendingRunId, gw, addMessage]);
|
|
181
300
|
const handleSubmit = useCallback(async (value) => {
|
|
182
|
-
|
|
183
|
-
|
|
301
|
+
// A line ending in a backslash keeps the message open.
|
|
302
|
+
const continuation = continuationOf(value);
|
|
303
|
+
if (continuation !== null) {
|
|
304
|
+
setDraft(d => [...d, continuation]);
|
|
305
|
+
setInput('');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const submitted = draft.length ? [...draft, value].join('\n') : value;
|
|
309
|
+
if (draft.length)
|
|
310
|
+
setDraft([]);
|
|
311
|
+
const trimmed = submitted.trim();
|
|
312
|
+
if (!trimmed) {
|
|
313
|
+
setInput('');
|
|
184
314
|
return;
|
|
315
|
+
}
|
|
185
316
|
setInput('');
|
|
186
317
|
setHistoryIdx(-1);
|
|
187
|
-
|
|
188
|
-
|
|
318
|
+
appendHistory(trimmed);
|
|
319
|
+
setInputHistory(h => (h[h.length - 1] === trimmed ? h : [...h, trimmed]));
|
|
320
|
+
// Slash commands. A multi-line paste is never one, even if its
|
|
321
|
+
// first character is a slash.
|
|
322
|
+
if (classifyInput(trimmed, coding !== null) === 'command') {
|
|
189
323
|
const [raw, ...args] = trimmed.slice(1).split(/\s+/);
|
|
190
324
|
const cmd = resolveSlash(raw);
|
|
191
325
|
if (!cmd) {
|
|
192
326
|
addMessage({ role: 'error', text: `Unknown command: /${raw}` });
|
|
193
|
-
addMessage({ role: 'info', text:
|
|
327
|
+
addMessage({ role: 'info', text: '/help lists every command.' });
|
|
194
328
|
return;
|
|
195
329
|
}
|
|
196
330
|
if (cmd !== raw.toLowerCase() && !Object.entries(ALIASES).some(([k, v]) => k === raw.toLowerCase() && v === cmd)) {
|
|
@@ -198,26 +332,89 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
198
332
|
}
|
|
199
333
|
switch (cmd) {
|
|
200
334
|
case 'quit':
|
|
201
|
-
|
|
202
|
-
exitMessage = state.conversationId
|
|
203
|
-
? `\nTo resume: npx @almyty/chat ${agentRef} --resume ${state.conversationId}\n`
|
|
204
|
-
: '';
|
|
205
|
-
exit();
|
|
335
|
+
quit();
|
|
206
336
|
return;
|
|
207
337
|
case 'clear':
|
|
338
|
+
// Honest about what it does: the server-side conversation is
|
|
339
|
+
// untouched, so the agent still remembers. /new forgets.
|
|
208
340
|
setState(s => ({ ...s, messages: [] }));
|
|
341
|
+
addMessage({ role: 'info', text: 'transcript cleared on screen — the agent still has this conversation. /new starts a fresh one.' });
|
|
342
|
+
return;
|
|
343
|
+
case 'new':
|
|
344
|
+
setState(s => ({ ...s, messages: [], conversationId: null, pendingRunId: null }));
|
|
345
|
+
setSessionUsage({ cost: 0, tokens: 0, steps: 0 });
|
|
346
|
+
setLastRunId(null);
|
|
347
|
+
addMessage({ role: 'info', text: 'new conversation — the agent has no memory of the last one' });
|
|
348
|
+
return;
|
|
349
|
+
case 'resume':
|
|
350
|
+
if (!state.conversationId) {
|
|
351
|
+
addMessage({ role: 'info', text: 'No conversation yet — send a message first.' });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
addMessage({ role: 'info', text: `almyty chat ${agentRef} --resume ${state.conversationId}` });
|
|
355
|
+
return;
|
|
356
|
+
case 'cost': {
|
|
357
|
+
const line = formatUsage(sessionUsage);
|
|
358
|
+
addMessage({ role: 'info', text: line ? `this session: ${line}` : 'Nothing spent yet this session.' });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case 'model': {
|
|
362
|
+
const config = (state.agent.modelConfig ?? {});
|
|
363
|
+
const routing = config.routing;
|
|
364
|
+
if (routing) {
|
|
365
|
+
const summary = [routing.role, routing.rationale, routing.strategy].filter(Boolean).join(' · ');
|
|
366
|
+
addMessage({ role: 'info', text: `routed per run${summary ? ` — ${summary}` : ''}; the answering model is shown after each turn` });
|
|
367
|
+
}
|
|
368
|
+
else if (config.model) {
|
|
369
|
+
addMessage({ role: 'info', text: `${config.model}${config.providerId ? ` · provider ${String(config.providerId).slice(0, 8)}` : ''}` });
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
addMessage({ role: 'info', text: 'No model configured on this agent. It will not answer until one is chosen in the dashboard.' });
|
|
373
|
+
}
|
|
374
|
+
if (sessionUsage.model)
|
|
375
|
+
addMessage({ role: 'info', text: `last answered by ${sessionUsage.model}` });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
case 'trace': {
|
|
379
|
+
if (!lastRunId) {
|
|
380
|
+
addMessage({ role: 'info', text: 'No run to trace yet.' });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading trace' }));
|
|
384
|
+
try {
|
|
385
|
+
const run = await gw.getRun(lastRunId);
|
|
386
|
+
setState(s => ({ ...s, loading: false }));
|
|
387
|
+
const steps = Array.isArray(run.steps) ? run.steps : [];
|
|
388
|
+
if (!steps.length) {
|
|
389
|
+
addMessage({ role: 'info', text: `run ${lastRunId.slice(0, 8)} · ${run.status} · no steps recorded` });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
addMessage({ role: 'info', text: `run ${lastRunId.slice(0, 8)} · ${run.status} · ${steps.length} step${steps.length === 1 ? '' : 's'}` });
|
|
393
|
+
steps.forEach((step, i) => {
|
|
394
|
+
const bits = [step.type];
|
|
395
|
+
if (step.input?.tool)
|
|
396
|
+
bits.push(step.input.tool);
|
|
397
|
+
if (typeof step.cost === 'number' && step.cost)
|
|
398
|
+
bits.push(`$${step.cost.toFixed(4)}`);
|
|
399
|
+
if (step.duration)
|
|
400
|
+
bits.push(`${Math.round(step.duration)}ms`);
|
|
401
|
+
if (step.error)
|
|
402
|
+
bits.push(`error: ${step.error}`);
|
|
403
|
+
addMessage({ role: step.error ? 'error' : 'tool', text: `${i + 1}. ${bits.join(' · ')}` });
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
setState(s => ({ ...s, loading: false }));
|
|
408
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
409
|
+
}
|
|
209
410
|
return;
|
|
411
|
+
}
|
|
210
412
|
case 'help':
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
addMessage({ role: 'info', text: '/
|
|
215
|
-
addMessage({ role: 'info', text: '
|
|
216
|
-
addMessage({ role: 'info', text: '/esc leave coding mode (session keeps running)' });
|
|
217
|
-
addMessage({ role: 'info', text: '/clear clear conversation' });
|
|
218
|
-
addMessage({ role: 'info', text: '/help show this help' });
|
|
219
|
-
addMessage({ role: 'info', text: '/quit exit' });
|
|
220
|
-
addMessage({ role: 'info', text: 'Tab to autocomplete commands.' });
|
|
413
|
+
for (const name of SLASH_COMMANDS) {
|
|
414
|
+
addMessage({ role: 'info', text: `/${name.padEnd(11)}${COMMAND_DESCS[name] ?? ''}` });
|
|
415
|
+
}
|
|
416
|
+
addMessage({ role: 'info', text: 'Tab completes · up/down walk history · end a line with \\ to keep typing' });
|
|
417
|
+
addMessage({ role: 'info', text: 'Ctrl-C cancels the running answer, again to exit · Ctrl-D exits' });
|
|
221
418
|
return;
|
|
222
419
|
case 'tools': {
|
|
223
420
|
const tools = state.agent.tools;
|
|
@@ -236,26 +433,38 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
236
433
|
const target = args.join(' ').trim();
|
|
237
434
|
if (target) {
|
|
238
435
|
setState(s => ({ ...s, loading: true, loadingLabel: 'Switching' }));
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
436
|
+
try {
|
|
437
|
+
const found = await client.findAgentByNameOrId(target);
|
|
438
|
+
setState(s => ({ ...s, loading: false }));
|
|
439
|
+
if (!found) {
|
|
440
|
+
addMessage({ role: 'error', text: `No agent called "${target}". /agents with no argument lists them.` });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
setState(s => ({
|
|
444
|
+
...s,
|
|
445
|
+
agent: found,
|
|
446
|
+
messages: [],
|
|
447
|
+
conversationId: null,
|
|
448
|
+
pendingRunId: null,
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
setState(s => ({ ...s, loading: false }));
|
|
453
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
244
454
|
}
|
|
245
|
-
setState(s => ({
|
|
246
|
-
...s,
|
|
247
|
-
agent: found,
|
|
248
|
-
messages: [],
|
|
249
|
-
conversationId: null,
|
|
250
|
-
pendingRunId: null,
|
|
251
|
-
}));
|
|
252
455
|
return;
|
|
253
456
|
}
|
|
254
457
|
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading' }));
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
458
|
+
try {
|
|
459
|
+
const agents = await client.listAgents();
|
|
460
|
+
setState(s => ({ ...s, loading: false }));
|
|
461
|
+
setPickerAgents(agents);
|
|
462
|
+
setShowPicker(true);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
setState(s => ({ ...s, loading: false }));
|
|
466
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
467
|
+
}
|
|
259
468
|
return;
|
|
260
469
|
}
|
|
261
470
|
case 'runners': {
|
|
@@ -274,7 +483,7 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
274
483
|
}
|
|
275
484
|
catch (err) {
|
|
276
485
|
setState(s => ({ ...s, loading: false }));
|
|
277
|
-
addMessage({ role: 'error', text: err
|
|
486
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
278
487
|
}
|
|
279
488
|
return;
|
|
280
489
|
}
|
|
@@ -305,7 +514,7 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
305
514
|
}
|
|
306
515
|
catch (err) {
|
|
307
516
|
setState(s => ({ ...s, loading: false }));
|
|
308
|
-
addMessage({ role: 'error', text: err
|
|
517
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
309
518
|
}
|
|
310
519
|
return;
|
|
311
520
|
}
|
|
@@ -318,7 +527,7 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
318
527
|
await client.stopCodingSession(coding.runnerId, coding.sessionId);
|
|
319
528
|
}
|
|
320
529
|
catch (err) {
|
|
321
|
-
addMessage({ role: 'error', text: err
|
|
530
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
322
531
|
}
|
|
323
532
|
leaveCodingMode('coding session stopped');
|
|
324
533
|
return;
|
|
@@ -342,117 +551,12 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
342
551
|
await client.sendCodingInput(coding.runnerId, coding.sessionId, trimmed);
|
|
343
552
|
}
|
|
344
553
|
catch (err) {
|
|
345
|
-
addMessage({ role: 'error', text: err
|
|
554
|
+
addMessage({ role: 'error', text: explainError(err, errCtx) });
|
|
346
555
|
}
|
|
347
556
|
return;
|
|
348
557
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
setState(s => ({ ...s, loading: true, loadingLabel: 'Thinking' }));
|
|
352
|
-
try {
|
|
353
|
-
if (state.agent.mode === 'autonomous') {
|
|
354
|
-
let runId;
|
|
355
|
-
if (state.pendingRunId) {
|
|
356
|
-
await gw.sendRunInput(state.pendingRunId, trimmed);
|
|
357
|
-
runId = state.pendingRunId;
|
|
358
|
-
setState(s => ({ ...s, pendingRunId: null }));
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
const run = await gw.startRun(trimmed, {
|
|
362
|
-
conversationId: state.conversationId ?? undefined,
|
|
363
|
-
});
|
|
364
|
-
runId = run.id;
|
|
365
|
-
if (run.conversationId) {
|
|
366
|
-
setState(s => ({ ...s, conversationId: run.conversationId }));
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
// Stream events — show tool calls, sub-agents, chunks in real time
|
|
370
|
-
let streamedContent = '';
|
|
371
|
-
const result = await gw.streamRun(runId, (event) => {
|
|
372
|
-
switch (event.type) {
|
|
373
|
-
case 'llm.started':
|
|
374
|
-
setState(s => ({ ...s, loadingLabel: 'Thinking' }));
|
|
375
|
-
break;
|
|
376
|
-
case 'llm.chunk': {
|
|
377
|
-
const chunk = event.data.content;
|
|
378
|
-
if (chunk)
|
|
379
|
-
streamedContent += chunk;
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
case 'llm.response':
|
|
383
|
-
setState(s => ({ ...s, loadingLabel: 'Processing' }));
|
|
384
|
-
break;
|
|
385
|
-
case 'tool.started': {
|
|
386
|
-
const toolName = event.data.tool;
|
|
387
|
-
if (toolName) {
|
|
388
|
-
setState(s => ({ ...s, loading: false }));
|
|
389
|
-
addMessage({ role: 'tool', text: toolName });
|
|
390
|
-
setState(s => ({ ...s, loading: true, loadingLabel: `Running ${toolName}` }));
|
|
391
|
-
}
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
case 'tool.result': {
|
|
395
|
-
const tool = event.data.tool;
|
|
396
|
-
const success = event.data.success;
|
|
397
|
-
if (tool) {
|
|
398
|
-
addMessage({ role: 'info', text: `${tool} ${success ? 'completed' : 'failed'}` });
|
|
399
|
-
}
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
case 'step.completed':
|
|
403
|
-
break;
|
|
404
|
-
case 'run.completed': {
|
|
405
|
-
const output = event.data.output;
|
|
406
|
-
if (output) {
|
|
407
|
-
const text = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
408
|
-
streamedContent = text;
|
|
409
|
-
}
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
case 'run.failed': {
|
|
413
|
-
const error = event.data.error;
|
|
414
|
-
addMessage({ role: 'error', text: error || 'Run failed' });
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
setState(s => ({ ...s, loading: false }));
|
|
420
|
-
// If still running after stream, poll to completion
|
|
421
|
-
let finalResult = result;
|
|
422
|
-
if (result.status === 'running') {
|
|
423
|
-
finalResult = await gw.pollRun(runId);
|
|
424
|
-
}
|
|
425
|
-
// Show final output
|
|
426
|
-
if (finalResult.status === 'completed') {
|
|
427
|
-
const text = streamedContent
|
|
428
|
-
|| (finalResult.output != null ? (typeof finalResult.output === 'string' ? finalResult.output : JSON.stringify(finalResult.output, null, 2)) : '');
|
|
429
|
-
if (text)
|
|
430
|
-
addMessage({ role: 'agent', text });
|
|
431
|
-
}
|
|
432
|
-
else if (finalResult.status === 'waiting_input') {
|
|
433
|
-
setState(s => ({ ...s, pendingRunId: runId }));
|
|
434
|
-
addMessage({ role: 'info', text: 'Waiting for your input' });
|
|
435
|
-
}
|
|
436
|
-
else if (finalResult.status === 'failed' && !streamedContent) {
|
|
437
|
-
addMessage({ role: 'error', text: finalResult.error || 'Run failed' });
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
// Workflow: synchronous invoke
|
|
442
|
-
const result = await gw.invoke({ message: trimmed });
|
|
443
|
-
setState(s => ({ ...s, loading: false }));
|
|
444
|
-
const output = result?.output ?? result?.data?.output ?? result;
|
|
445
|
-
if (output != null) {
|
|
446
|
-
const text = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
447
|
-
addMessage({ role: 'agent', text });
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
catch (err) {
|
|
452
|
-
setState(s => ({ ...s, loading: false }));
|
|
453
|
-
addMessage({ role: 'error', text: err.message });
|
|
454
|
-
}
|
|
455
|
-
}, [state.agent, state.conversationId, state.pendingRunId, client, addMessage, exit, coding, startCoding, leaveCodingMode]);
|
|
558
|
+
await sendMessage(trimmed);
|
|
559
|
+
}, [state.agent, state.conversationId, state.pendingRunId, client, gw, addMessage, quit, coding, startCoding, leaveCodingMode, sendMessage, draft, sessionUsage, lastRunId, agentRef]);
|
|
456
560
|
const handlePickerSelect = useCallback((agent) => {
|
|
457
561
|
setShowPicker(false);
|
|
458
562
|
if (agent.id === state.agent.id)
|
|
@@ -465,7 +569,6 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
465
569
|
pendingRunId: null,
|
|
466
570
|
}));
|
|
467
571
|
}, [state.agent.id]);
|
|
468
|
-
const suggestion = getSuggestion(input);
|
|
469
572
|
if (showPicker) {
|
|
470
573
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsx(AgentSelector, { agents: pickerAgents, onSelect: handlePickerSelect })] }));
|
|
471
574
|
}
|
|
@@ -483,13 +586,16 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
483
586
|
void startCoding(choice, task);
|
|
484
587
|
} })] }));
|
|
485
588
|
}
|
|
486
|
-
|
|
589
|
+
const transcriptRows = usableRows(size.rows, CHROME_ROWS + (paletteOpen ? slashMatches.length : 0));
|
|
590
|
+
const separatorWidth = Math.min(Math.max(size.columns || 80, 20), 120);
|
|
591
|
+
const sessionLine = formatUsage(sessionUsage);
|
|
592
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsx(Box, { flexDirection: "column", paddingRight: 2, children: _jsx(MessageWindow, { messages: state.messages, loading: state.loading, loadingLabel: state.loadingLabel, maxRows: transcriptRows, streaming: streaming }) }), paletteOpen && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: slashMatches.map((cmd, i) => {
|
|
487
593
|
const active = i === paletteCursor;
|
|
488
|
-
const padded = `/${cmd}`.padEnd(
|
|
594
|
+
const padded = `/${cmd}`.padEnd(12);
|
|
489
595
|
const desc = COMMAND_DESCS[cmd] ?? '';
|
|
490
596
|
const line = `${active ? '❯' : ' '} ${padded} ${desc}`;
|
|
491
597
|
return _jsx(Text, { color: active ? '#8b5cf6' : undefined, bold: active, wrap: "truncate", children: line }, cmd);
|
|
492
|
-
}) })), coding && _jsx(CodingModeIndicator, { agent: coding.agent, runner: coding.runnerName }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '─'.repeat(
|
|
598
|
+
}) })), coding && _jsx(CodingModeIndicator, { agent: coding.agent, runner: coding.runnerName }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '─'.repeat(separatorWidth) }) }), _jsxs(Box, { paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: coding ? '#22d3ee' : '#8b5cf6', children: draft.length ? '… ' : '❯ ' }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: input, onChange: handleInputChange, onSubmit: (val) => {
|
|
493
599
|
if (paletteOpen && slashMatches.length > 0) {
|
|
494
600
|
const selected = `/${slashMatches[paletteCursor]}`;
|
|
495
601
|
setInput('');
|
|
@@ -498,5 +604,5 @@ export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
|
498
604
|
return;
|
|
499
605
|
}
|
|
500
606
|
handleSubmit(val);
|
|
501
|
-
}, placeholder: coding ? 'Type input for the coding session or / for commands' : 'Type a message or / for commands' }) })] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { dimColor: true, children: [state.agent.name, state.agent.tools?.length ? ` · ${state.agent.tools.length} tools` : '', state.conversationId ? ` · ${state.conversationId.slice(0, 8)}` : '', coding ? ` · coding:${coding.agent}@${coding.runnerName}` : ''] }) })] }));
|
|
607
|
+
}, placeholder: coding ? 'Type input for the coding session or / for commands' : 'Type a message or / for commands' }) })] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [state.agent.name, state.agent.tools?.length ? ` · ${state.agent.tools.length} tools` : '', state.conversationId ? ` · ${state.conversationId.slice(0, 8)}` : '', sessionLine ? ` · ${sessionLine}` : '', coding ? ` · coding:${coding.agent}@${coding.runnerName}` : '', state.loading ? ' · ctrl-c cancels' : ''] }) })] }));
|
|
502
608
|
}
|