@almyty/chat 0.1.4 → 1.2.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 +31 -20
- package/dist/app.d.ts +25 -0
- package/dist/app.js +502 -0
- package/dist/commands.d.ts +25 -0
- package/dist/commands.js +79 -0
- package/dist/components.d.ts +55 -0
- package/dist/components.js +183 -0
- package/dist/index.d.ts +1 -8
- package/dist/index.js +64 -367
- package/package.json +22 -4
package/README.md
CHANGED
|
@@ -1,42 +1,53 @@
|
|
|
1
1
|
# @almyty/chat
|
|
2
2
|
|
|
3
|
-
Interactive chat REPL for almyty agents.
|
|
3
|
+
Interactive chat REPL for almyty agents. Built with [ink](https://github.com/vadimdemedes/ink) (React for CLI).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
$ npx @almyty/auth login
|
|
9
|
-
$ npx @almyty/chat
|
|
10
|
-
$ npx @almyty/chat my-research-bot
|
|
9
|
+
$ npx @almyty/chat acme/support-bot
|
|
11
10
|
```
|
|
12
11
|
|
|
13
12
|
## Usage
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
```
|
|
15
|
+
Usage:
|
|
16
|
+
npx @almyty/chat <org>/<agent-slug>
|
|
17
|
+
npx @almyty/chat <org>/<agent-slug> --resume <conversation-id>
|
|
18
|
+
npx @almyty/chat # interactive agent picker
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
/agents browse and switch agents
|
|
22
|
+
/tools show available tools
|
|
23
|
+
/help show commands
|
|
24
|
+
/clear clear conversation
|
|
25
|
+
/quit exit (shows resume command)
|
|
20
26
|
```
|
|
21
27
|
|
|
22
|
-
##
|
|
28
|
+
## Features
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
- **Gateway routing** -- agents addressed as `<org>/<agent-slug>`
|
|
31
|
+
- **Resume conversations** -- `--resume <conversation-id>` picks up where you left off; `/quit` prints the resume command
|
|
32
|
+
- **Arrow-key agent picker** -- run without arguments to browse and select
|
|
33
|
+
- **Slash commands** -- tab autocomplete with fuzzy prefix matching
|
|
34
|
+
- **Command palette** -- arrow-key navigation through matching commands
|
|
35
|
+
- **Input history** -- up/down arrows cycle through previous messages (derived from conversation, including resumed history)
|
|
36
|
+
- **SSE streaming** -- real-time tool calls and agent responses via server-sent events
|
|
37
|
+
- **Markdown rendering** -- bold, inline code, code blocks, lists, headers
|
|
31
38
|
|
|
32
39
|
## Authentication
|
|
33
40
|
|
|
34
41
|
Requires `npx @almyty/auth login` first. Reads credentials from `~/.almyty/credentials.json`. Override with `ALMYTY_TOKEN` and `ALMYTY_URL` environment variables.
|
|
35
42
|
|
|
36
|
-
##
|
|
43
|
+
## About almyty
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
almyty is the full-stack platform for AI agents, agnostic by design: any LLM, any
|
|
46
|
+
API turned into tools, served over MCP, A2A, UTCP, and Agent Skills. Open source,
|
|
47
|
+
no lock-in.
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
- Website — https://almyty.com
|
|
50
|
+
- Docs — https://docs.almyty.com
|
|
51
|
+
- Source — https://github.com/almyty-inc/almyty
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
Apache-2.0 © Almyty Inc.
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AlmytyClient, GatewayClient, AgentInfo } from '@almyty/client';
|
|
3
|
+
import type { Message } from './components.js';
|
|
4
|
+
export interface AppState {
|
|
5
|
+
agent: AgentInfo;
|
|
6
|
+
messages: Message[];
|
|
7
|
+
loading: boolean;
|
|
8
|
+
loadingLabel: string;
|
|
9
|
+
conversationId: string | null;
|
|
10
|
+
pendingRunId: string | null;
|
|
11
|
+
}
|
|
12
|
+
/** Active coding session driven from the REPL (chat-to-runner bridge). */
|
|
13
|
+
export interface CodingSessionState {
|
|
14
|
+
runnerId: string;
|
|
15
|
+
runnerName: string;
|
|
16
|
+
agent: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
}
|
|
19
|
+
export declare let exitMessage: string;
|
|
20
|
+
export declare function ChatApp({ client, initialAgent, gw, resumeConversationId }: {
|
|
21
|
+
client: AlmytyClient;
|
|
22
|
+
initialAgent: AgentInfo;
|
|
23
|
+
gw: GatewayClient;
|
|
24
|
+
resumeConversationId?: string;
|
|
25
|
+
}): React.JSX.Element;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Header, MessageView, LoadingIndicator, AgentSelector, CodingModeIndicator, ChoiceSelector, } from './components.js';
|
|
6
|
+
import { SLASH_COMMANDS, COMMAND_DESCS, resolveSlash, getSuggestion, ALIASES, classifyInput, buildCodeChoices, } from './commands.js';
|
|
7
|
+
// Mutable module-level variable; written by ChatApp, read by main() after exit
|
|
8
|
+
export let exitMessage = '';
|
|
9
|
+
// ── Chat app ────────────────────────────────────────────────────
|
|
10
|
+
export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
const [state, setState] = useState({
|
|
13
|
+
agent: initialAgent,
|
|
14
|
+
messages: [],
|
|
15
|
+
loading: false,
|
|
16
|
+
loadingLabel: 'Thinking',
|
|
17
|
+
conversationId: resumeConversationId ?? null,
|
|
18
|
+
pendingRunId: null,
|
|
19
|
+
});
|
|
20
|
+
// Load conversation history on resume
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!resumeConversationId)
|
|
23
|
+
return;
|
|
24
|
+
(async () => {
|
|
25
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading history' }));
|
|
26
|
+
try {
|
|
27
|
+
const history = await gw.getConversationMessages(resumeConversationId);
|
|
28
|
+
const msgs = history
|
|
29
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
30
|
+
.map(m => ({
|
|
31
|
+
role: m.role === 'user' ? 'user' : 'agent',
|
|
32
|
+
text: m.content,
|
|
33
|
+
}));
|
|
34
|
+
setState(s => ({ ...s, messages: msgs, loading: false }));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
setState(s => ({ ...s, loading: false }));
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
}, [resumeConversationId]);
|
|
41
|
+
const [input, setInput] = useState('');
|
|
42
|
+
const [paletteCursor, setPaletteCursor] = useState(0);
|
|
43
|
+
// Input history derived from conversation — includes resumed messages
|
|
44
|
+
const inputHistory = state.messages.filter(m => m.role === 'user').map(m => m.text);
|
|
45
|
+
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
46
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
47
|
+
const [pickerAgents, setPickerAgents] = useState([]);
|
|
48
|
+
// Active coding session (input routes to the runner while set).
|
|
49
|
+
const [coding, setCoding] = useState(null);
|
|
50
|
+
// Pending runner x CLI pick for /code when multiple targets exist.
|
|
51
|
+
const [codeChoices, setCodeChoices] = useState(null);
|
|
52
|
+
const codingAbortRef = useRef(null);
|
|
53
|
+
// Command palette matches
|
|
54
|
+
const slashMatches = input.startsWith('/') && !input.includes(' ')
|
|
55
|
+
? SLASH_COMMANDS.filter(c => c.startsWith(input.slice(1).toLowerCase()))
|
|
56
|
+
: [];
|
|
57
|
+
const paletteOpen = input.startsWith('/') && slashMatches.length > 0;
|
|
58
|
+
useInput((ch, key) => {
|
|
59
|
+
if (state.loading)
|
|
60
|
+
return;
|
|
61
|
+
// Command palette navigation
|
|
62
|
+
if (paletteOpen) {
|
|
63
|
+
if (key.upArrow) {
|
|
64
|
+
setPaletteCursor(c => (c - 1 + slashMatches.length) % slashMatches.length);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.downArrow) {
|
|
68
|
+
setPaletteCursor(c => (c + 1) % slashMatches.length);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.tab) {
|
|
72
|
+
setInput(`/${slashMatches[paletteCursor]}`);
|
|
73
|
+
setPaletteCursor(0);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Input history — up/down arrows always, like Claude Code
|
|
79
|
+
if (key.upArrow && inputHistory.length > 0) {
|
|
80
|
+
const newIdx = Math.min(historyIdx + 1, inputHistory.length - 1);
|
|
81
|
+
setHistoryIdx(newIdx);
|
|
82
|
+
setInput(inputHistory[inputHistory.length - 1 - newIdx]);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.downArrow) {
|
|
86
|
+
if (historyIdx > 0) {
|
|
87
|
+
const newIdx = historyIdx - 1;
|
|
88
|
+
setHistoryIdx(newIdx);
|
|
89
|
+
setInput(inputHistory[inputHistory.length - 1 - newIdx]);
|
|
90
|
+
}
|
|
91
|
+
else if (historyIdx === 0) {
|
|
92
|
+
setHistoryIdx(-1);
|
|
93
|
+
setInput('');
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// Reset palette cursor when input changes
|
|
99
|
+
const handleInputChange = useCallback((val) => {
|
|
100
|
+
setInput(val);
|
|
101
|
+
setPaletteCursor(0);
|
|
102
|
+
}, []);
|
|
103
|
+
const addMessage = useCallback((msg) => {
|
|
104
|
+
setState(s => ({ ...s, messages: [...s.messages, msg] }));
|
|
105
|
+
}, []);
|
|
106
|
+
// ── Coding session helpers (chat-to-runner bridge) ──────────────
|
|
107
|
+
/**
|
|
108
|
+
* Append streamed CLI output to the transcript, merging into the last
|
|
109
|
+
* coding message so a burst of chunks doesn't explode into hundreds of
|
|
110
|
+
* transcript entries. Caps a single coding message before rolling over.
|
|
111
|
+
*/
|
|
112
|
+
const appendCodingOutput = useCallback((text) => {
|
|
113
|
+
if (!text)
|
|
114
|
+
return;
|
|
115
|
+
setState(s => {
|
|
116
|
+
const msgs = [...s.messages];
|
|
117
|
+
const last = msgs[msgs.length - 1];
|
|
118
|
+
if (last && last.role === 'coding' && last.text.length < 4000) {
|
|
119
|
+
msgs[msgs.length - 1] = { ...last, text: last.text + text };
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
msgs.push({ role: 'coding', text });
|
|
123
|
+
}
|
|
124
|
+
return { ...s, messages: msgs };
|
|
125
|
+
});
|
|
126
|
+
}, []);
|
|
127
|
+
const startCoding = useCallback(async (choice, task) => {
|
|
128
|
+
setState(s => ({ ...s, loading: true, loadingLabel: `Starting ${choice.agentName} on ${choice.runnerName}` }));
|
|
129
|
+
try {
|
|
130
|
+
const session = await client.startCodingSession(choice.runnerId, {
|
|
131
|
+
agent: choice.agentId,
|
|
132
|
+
task,
|
|
133
|
+
});
|
|
134
|
+
setState(s => ({ ...s, loading: false }));
|
|
135
|
+
setCoding({
|
|
136
|
+
runnerId: choice.runnerId,
|
|
137
|
+
runnerName: choice.runnerName,
|
|
138
|
+
agent: choice.agentId,
|
|
139
|
+
sessionId: session.sessionId,
|
|
140
|
+
});
|
|
141
|
+
addMessage({
|
|
142
|
+
role: 'info',
|
|
143
|
+
text: `coding session started — ${choice.agentId}@${choice.runnerName} (cwd ${session.cwd ?? '~'})`,
|
|
144
|
+
});
|
|
145
|
+
// Background stream: coding.output chunks land in the transcript,
|
|
146
|
+
// coding.exit closes the mode. /esc aborts the stream client-side.
|
|
147
|
+
const ac = new AbortController();
|
|
148
|
+
codingAbortRef.current = ac;
|
|
149
|
+
void client.streamCodingEvents(choice.runnerId, session.sessionId, (event) => {
|
|
150
|
+
if (event.type === 'coding.output') {
|
|
151
|
+
const chunk = event.data.data;
|
|
152
|
+
if (chunk)
|
|
153
|
+
appendCodingOutput(String(chunk));
|
|
154
|
+
}
|
|
155
|
+
else if (event.type === 'coding.exit') {
|
|
156
|
+
const code = event.data.exitCode;
|
|
157
|
+
addMessage({ role: 'info', text: `coding session exited${code != null ? ` (exit ${code})` : ''}` });
|
|
158
|
+
codingAbortRef.current = null;
|
|
159
|
+
setCoding(null);
|
|
160
|
+
}
|
|
161
|
+
}, ac.signal).catch((err) => {
|
|
162
|
+
if (ac.signal.aborted)
|
|
163
|
+
return;
|
|
164
|
+
addMessage({ role: 'error', text: `coding stream lost: ${err.message}` });
|
|
165
|
+
codingAbortRef.current = null;
|
|
166
|
+
setCoding(null);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
setState(s => ({ ...s, loading: false }));
|
|
171
|
+
addMessage({ role: 'error', text: err.message });
|
|
172
|
+
}
|
|
173
|
+
}, [client, addMessage, appendCodingOutput]);
|
|
174
|
+
/** Detach from coding mode (abort the stream; the remote session is untouched). */
|
|
175
|
+
const leaveCodingMode = useCallback((note) => {
|
|
176
|
+
codingAbortRef.current?.abort();
|
|
177
|
+
codingAbortRef.current = null;
|
|
178
|
+
setCoding(null);
|
|
179
|
+
addMessage({ role: 'info', text: note });
|
|
180
|
+
}, [addMessage]);
|
|
181
|
+
const handleSubmit = useCallback(async (value) => {
|
|
182
|
+
const trimmed = value.trim();
|
|
183
|
+
if (!trimmed)
|
|
184
|
+
return;
|
|
185
|
+
setInput('');
|
|
186
|
+
setHistoryIdx(-1);
|
|
187
|
+
// Slash commands
|
|
188
|
+
if (trimmed.startsWith('/')) {
|
|
189
|
+
const [raw, ...args] = trimmed.slice(1).split(/\s+/);
|
|
190
|
+
const cmd = resolveSlash(raw);
|
|
191
|
+
if (!cmd) {
|
|
192
|
+
addMessage({ role: 'error', text: `Unknown command: /${raw}` });
|
|
193
|
+
addMessage({ role: 'info', text: `Commands: /help /agents /runners /code /clear /quit` });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (cmd !== raw.toLowerCase() && !Object.entries(ALIASES).some(([k, v]) => k === raw.toLowerCase() && v === cmd)) {
|
|
197
|
+
addMessage({ role: 'info', text: `→ /${cmd}` });
|
|
198
|
+
}
|
|
199
|
+
switch (cmd) {
|
|
200
|
+
case 'quit':
|
|
201
|
+
const agentRef = `${gw.orgSlug}/${gw.agentSlug}`;
|
|
202
|
+
exitMessage = state.conversationId
|
|
203
|
+
? `\nTo resume: npx @almyty/chat ${agentRef} --resume ${state.conversationId}\n`
|
|
204
|
+
: '';
|
|
205
|
+
exit();
|
|
206
|
+
return;
|
|
207
|
+
case 'clear':
|
|
208
|
+
setState(s => ({ ...s, messages: [] }));
|
|
209
|
+
return;
|
|
210
|
+
case 'help':
|
|
211
|
+
addMessage({ role: 'info', text: '/agents browse and switch agents' });
|
|
212
|
+
addMessage({ role: 'info', text: '/tools show available tools' });
|
|
213
|
+
addMessage({ role: 'info', text: '/runners list your runners + coding CLIs' });
|
|
214
|
+
addMessage({ role: 'info', text: '/code run a coding task on a runner' });
|
|
215
|
+
addMessage({ role: 'info', text: '/code-stop stop the active coding session' });
|
|
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.' });
|
|
221
|
+
return;
|
|
222
|
+
case 'tools': {
|
|
223
|
+
const tools = state.agent.tools;
|
|
224
|
+
if (!tools?.length) {
|
|
225
|
+
addMessage({ role: 'info', text: 'No tools configured for this agent.' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
addMessage({ role: 'info', text: `${tools.length} tool${tools.length > 1 ? 's' : ''} available:` });
|
|
229
|
+
for (const tool of tools) {
|
|
230
|
+
const desc = tool.description ? ` — ${tool.description}` : '';
|
|
231
|
+
addMessage({ role: 'tool', text: `${tool.name}${desc}` });
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
case 'agents': {
|
|
236
|
+
const target = args.join(' ').trim();
|
|
237
|
+
if (target) {
|
|
238
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Switching' }));
|
|
239
|
+
const found = await client.findAgentByNameOrId(target);
|
|
240
|
+
setState(s => ({ ...s, loading: false }));
|
|
241
|
+
if (!found) {
|
|
242
|
+
addMessage({ role: 'error', text: `Agent "${target}" not found` });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
setState(s => ({
|
|
246
|
+
...s,
|
|
247
|
+
agent: found,
|
|
248
|
+
messages: [],
|
|
249
|
+
conversationId: null,
|
|
250
|
+
pendingRunId: null,
|
|
251
|
+
}));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading' }));
|
|
255
|
+
const agents = await client.listAgents();
|
|
256
|
+
setState(s => ({ ...s, loading: false }));
|
|
257
|
+
setPickerAgents(agents);
|
|
258
|
+
setShowPicker(true);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
case 'runners': {
|
|
262
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading runners' }));
|
|
263
|
+
try {
|
|
264
|
+
const runners = await client.listRunners();
|
|
265
|
+
setState(s => ({ ...s, loading: false }));
|
|
266
|
+
if (!runners.length) {
|
|
267
|
+
addMessage({ role: 'info', text: 'No runners registered. Start one: npx @almyty/runner start' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
for (const r of runners) {
|
|
271
|
+
const clis = (r.codingAgents ?? []).map(a => a.id).join(', ') || 'no coding CLIs detected';
|
|
272
|
+
addMessage({ role: 'info', text: `${r.name} · ${r.state ?? 'unknown'} · ${clis}` });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
setState(s => ({ ...s, loading: false }));
|
|
277
|
+
addMessage({ role: 'error', text: err.message });
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
case 'code': {
|
|
282
|
+
const task = args.join(' ').trim();
|
|
283
|
+
if (!task) {
|
|
284
|
+
addMessage({ role: 'error', text: 'Usage: /code <task>' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (coding) {
|
|
288
|
+
addMessage({ role: 'error', text: 'A coding session is already active — /code-stop or /esc first.' });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Finding runners' }));
|
|
292
|
+
try {
|
|
293
|
+
const runners = await client.listRunners();
|
|
294
|
+
setState(s => ({ ...s, loading: false }));
|
|
295
|
+
const choices = buildCodeChoices(runners);
|
|
296
|
+
if (!choices.length) {
|
|
297
|
+
addMessage({ role: 'error', text: 'No online runner with a detected coding CLI. Start one: npx @almyty/runner start' });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (choices.length === 1) {
|
|
301
|
+
await startCoding(choices[0], task);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
setCodeChoices({ choices, task });
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
setState(s => ({ ...s, loading: false }));
|
|
308
|
+
addMessage({ role: 'error', text: err.message });
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
case 'code-stop': {
|
|
313
|
+
if (!coding) {
|
|
314
|
+
addMessage({ role: 'info', text: 'No active coding session.' });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await client.stopCodingSession(coding.runnerId, coding.sessionId);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
addMessage({ role: 'error', text: err.message });
|
|
322
|
+
}
|
|
323
|
+
leaveCodingMode('coding session stopped');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
case 'esc': {
|
|
327
|
+
if (!coding) {
|
|
328
|
+
addMessage({ role: 'info', text: 'Not in coding mode.' });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
leaveCodingMode(`left coding mode — session keeps running on ${coding.runnerName} (/code-stop to kill it)`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Coding mode: anything that isn't a slash command routes to the
|
|
338
|
+
// session's stdin, not to the chat agent.
|
|
339
|
+
if (classifyInput(trimmed, coding !== null) === 'coding' && coding) {
|
|
340
|
+
addMessage({ role: 'user', text: trimmed });
|
|
341
|
+
try {
|
|
342
|
+
await client.sendCodingInput(coding.runnerId, coding.sessionId, trimmed);
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
addMessage({ role: 'error', text: err.message });
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Regular message
|
|
350
|
+
addMessage({ role: 'user', text: trimmed });
|
|
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]);
|
|
456
|
+
const handlePickerSelect = useCallback((agent) => {
|
|
457
|
+
setShowPicker(false);
|
|
458
|
+
if (agent.id === state.agent.id)
|
|
459
|
+
return;
|
|
460
|
+
setState(s => ({
|
|
461
|
+
...s,
|
|
462
|
+
agent,
|
|
463
|
+
messages: [],
|
|
464
|
+
conversationId: null,
|
|
465
|
+
pendingRunId: null,
|
|
466
|
+
}));
|
|
467
|
+
}, [state.agent.id]);
|
|
468
|
+
const suggestion = getSuggestion(input);
|
|
469
|
+
if (showPicker) {
|
|
470
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsx(AgentSelector, { agents: pickerAgents, onSelect: handlePickerSelect })] }));
|
|
471
|
+
}
|
|
472
|
+
if (codeChoices) {
|
|
473
|
+
const items = codeChoices.choices.map(c => ({
|
|
474
|
+
key: `${c.runnerId}:${c.agentId}`,
|
|
475
|
+
label: c.agentName,
|
|
476
|
+
hint: `on ${c.runnerName}`,
|
|
477
|
+
}));
|
|
478
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsx(ChoiceSelector, { title: "Pick a coding CLI + runner:", choices: items, onSelect: (picked) => {
|
|
479
|
+
const choice = codeChoices.choices.find(c => `${c.runnerId}:${c.agentId}` === picked.key);
|
|
480
|
+
const task = codeChoices.task;
|
|
481
|
+
setCodeChoices(null);
|
|
482
|
+
if (choice)
|
|
483
|
+
void startCoding(choice, task);
|
|
484
|
+
} })] }));
|
|
485
|
+
}
|
|
486
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 2, overflow: "hidden", children: [state.messages.map((msg, i) => (_jsx(MessageView, { msg: msg }, i))), state.loading && _jsx(LoadingIndicator, { label: state.loadingLabel })] }), paletteOpen && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: slashMatches.map((cmd, i) => {
|
|
487
|
+
const active = i === paletteCursor;
|
|
488
|
+
const padded = `/${cmd}`.padEnd(10);
|
|
489
|
+
const desc = COMMAND_DESCS[cmd] ?? '';
|
|
490
|
+
const line = `${active ? '❯' : ' '} ${padded} ${desc}`;
|
|
491
|
+
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(Math.min(process.stdout.columns || 80, 120)) }) }), _jsxs(Box, { paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: coding ? '#22d3ee' : '#8b5cf6', children: "\u276F " }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: input, onChange: handleInputChange, onSubmit: (val) => {
|
|
493
|
+
if (paletteOpen && slashMatches.length > 0) {
|
|
494
|
+
const selected = `/${slashMatches[paletteCursor]}`;
|
|
495
|
+
setInput('');
|
|
496
|
+
setPaletteCursor(0);
|
|
497
|
+
handleSubmit(selected);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
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}` : ''] }) })] }));
|
|
502
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RunnerSummary } from '@almyty/client';
|
|
2
|
+
export declare const SLASH_COMMANDS: readonly ["agents", "tools", "runners", "code", "code-stop", "esc", "help", "clear", "quit"];
|
|
3
|
+
export declare const ALIASES: Record<string, string>;
|
|
4
|
+
export declare const COMMAND_DESCS: Record<string, string>;
|
|
5
|
+
export declare function resolveSlash(input: string): string | null;
|
|
6
|
+
export declare function getSuggestion(partial: string): string;
|
|
7
|
+
export type InputRoute = 'command' | 'coding' | 'chat';
|
|
8
|
+
/**
|
|
9
|
+
* Where a submitted line goes: slash commands are always commands; when a
|
|
10
|
+
* coding session is active, everything else routes to the session's stdin;
|
|
11
|
+
* otherwise it's a normal chat message.
|
|
12
|
+
*/
|
|
13
|
+
export declare function classifyInput(value: string, codingActive: boolean): InputRoute;
|
|
14
|
+
export interface CodeChoice {
|
|
15
|
+
runnerId: string;
|
|
16
|
+
runnerName: string;
|
|
17
|
+
agentId: string;
|
|
18
|
+
agentName: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Expand online runners x detected coding CLIs into a flat choice list for
|
|
22
|
+
* the selector. Offline/stale runners and runners without any detected
|
|
23
|
+
* coding CLI are excluded.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildCodeChoices(runners: RunnerSummary[]): CodeChoice[];
|