@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 CHANGED
@@ -1,42 +1,53 @@
1
1
  # @almyty/chat
2
2
 
3
- Interactive chat REPL for almyty agents. Supports both workflow and autonomous agent modes.
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
- Run without arguments to pick an agent from an interactive menu, or pass a name or ID to connect directly.
16
-
17
- ```bash
18
- $ npx @almyty/chat # interactive agent picker
19
- $ npx @almyty/chat <name|id> # connect to a specific agent
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
- ## REPL commands
28
+ ## Features
23
29
 
24
- | Command | Description |
25
- |---------|-------------|
26
- | `/switch <agent>` | Switch to a different agent |
27
- | `/agents` | List agents (current one marked) |
28
- | `/clear` | Clear the screen |
29
- | `/help` | Show available commands |
30
- | `/quit` | Exit the REPL |
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
- ## Docs
43
+ ## About almyty
37
44
 
38
- https://almyty.com/docs
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
- ## License
49
+ - Website — https://almyty.com
50
+ - Docs — https://docs.almyty.com
51
+ - Source — https://github.com/almyty-inc/almyty
41
52
 
42
- BSL-1.1
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[];