@almyty/chat 0.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/dist/args.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Command line for `almyty chat`.
3
+ *
4
+ * Parsing lives apart from the entry point so the surface can be tested
5
+ * without a terminal, and so `--help` is generated from the same table
6
+ * the parser reads. A flag that exists and is undocumented, or is
7
+ * documented and does not exist, is a bug either way.
8
+ */
9
+ export interface ChatArgs {
10
+ help: boolean;
11
+ version: boolean;
12
+ /** `<org>/<agent-slug>`, a bare slug, or undefined for the picker. */
13
+ ref?: string;
14
+ resume?: string;
15
+ /** One-shot message. Answers and exits, no REPL. */
16
+ message?: string;
17
+ /** Read the message from stdin rather than a tty. */
18
+ stdin: boolean;
19
+ json: boolean;
20
+ /** False renders the answer once it is complete instead of token by token. */
21
+ stream: boolean;
22
+ /** Explicit colour choice; undefined means "decide from the terminal". */
23
+ color?: boolean;
24
+ maxSteps?: number;
25
+ maxCostCents?: number;
26
+ /** A parse problem worth exiting on, rather than guessing. */
27
+ error?: string;
28
+ }
29
+ export declare function parseArgs(argv: string[]): ChatArgs;
30
+ /**
31
+ * The agent to open: the argument, else $ALMYTY_AGENT.
32
+ *
33
+ * The environment variable is how a compiled terminal app knows which
34
+ * agent it is, since the build compiles this client unchanged and the
35
+ * recipient should not have to type an agent reference.
36
+ */
37
+ export declare function resolveRef(args: ChatArgs, env?: Record<string, string | undefined>): string | undefined;
38
+ /** Split `<org>/<agent>` into its parts; a bare slug leaves org unset. */
39
+ export declare function splitRef(ref: string): {
40
+ orgSlug?: string;
41
+ agentSlug: string;
42
+ };
43
+ /**
44
+ * Whether to run without a terminal UI.
45
+ *
46
+ * A CLI that only works on a tty is half a CLI: piping in a question or
47
+ * out to jq has to work, and ink cannot draw to a pipe.
48
+ */
49
+ export declare function isNonInteractive(args: ChatArgs, io?: {
50
+ stdinTty?: boolean;
51
+ stdoutTty?: boolean;
52
+ }): boolean;
53
+ /**
54
+ * Whether to emit colour.
55
+ *
56
+ * Honours the NO_COLOR convention and FORCE_COLOR, and never colours a
57
+ * pipe unless asked to.
58
+ */
59
+ export declare function useColor(args: ChatArgs, env?: Record<string, string | undefined>, stdoutTty?: boolean): boolean;
60
+ /**
61
+ * `--help`, built from the same command table the REPL resolves
62
+ * against, so a new slash command cannot go undocumented.
63
+ */
64
+ export declare function helpText(version: string): string;
package/dist/args.js ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Command line for `almyty chat`.
3
+ *
4
+ * Parsing lives apart from the entry point so the surface can be tested
5
+ * without a terminal, and so `--help` is generated from the same table
6
+ * the parser reads. A flag that exists and is undocumented, or is
7
+ * documented and does not exist, is a bug either way.
8
+ */
9
+ import { SLASH_COMMANDS, COMMAND_DESCS } from './commands.js';
10
+ import { EXIT_CODE_HELP } from './exit-codes.js';
11
+ /** Flags that take no value. */
12
+ const BOOLEAN_FLAGS = new Set([
13
+ '--help', '-h', '--version', '-v', '--json', '--stdin',
14
+ '--no-stream', '--stream', '--no-color', '--color',
15
+ ]);
16
+ /** Flags that consume the next argument. */
17
+ const VALUE_FLAGS = new Set(['--resume', '--message', '-m', '--max-steps', '--max-cost-cents']);
18
+ function positiveInt(raw, flag) {
19
+ const n = Number(raw);
20
+ if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
21
+ return { error: `${flag} needs a positive whole number, got "${raw}"` };
22
+ }
23
+ return { value: n };
24
+ }
25
+ export function parseArgs(argv) {
26
+ const args = { help: false, version: false, stdin: false, json: false, stream: true };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const arg = argv[i];
29
+ if (VALUE_FLAGS.has(arg)) {
30
+ const value = argv[i + 1];
31
+ if (value === undefined || value.startsWith('-')) {
32
+ return { ...args, error: `${arg} needs a value` };
33
+ }
34
+ i++;
35
+ switch (arg) {
36
+ case '--resume':
37
+ args.resume = value;
38
+ break;
39
+ case '--message':
40
+ case '-m':
41
+ args.message = value;
42
+ break;
43
+ case '--max-steps': {
44
+ const { value: n, error } = positiveInt(value, arg);
45
+ if (error)
46
+ return { ...args, error };
47
+ args.maxSteps = n;
48
+ break;
49
+ }
50
+ case '--max-cost-cents': {
51
+ const { value: n, error } = positiveInt(value, arg);
52
+ if (error)
53
+ return { ...args, error };
54
+ args.maxCostCents = n;
55
+ break;
56
+ }
57
+ }
58
+ continue;
59
+ }
60
+ if (BOOLEAN_FLAGS.has(arg)) {
61
+ switch (arg) {
62
+ case '--help':
63
+ case '-h':
64
+ args.help = true;
65
+ break;
66
+ case '--version':
67
+ case '-v':
68
+ args.version = true;
69
+ break;
70
+ case '--json':
71
+ args.json = true;
72
+ break;
73
+ case '--stdin':
74
+ args.stdin = true;
75
+ break;
76
+ case '--stream':
77
+ args.stream = true;
78
+ break;
79
+ case '--no-stream':
80
+ args.stream = false;
81
+ break;
82
+ case '--color':
83
+ args.color = true;
84
+ break;
85
+ case '--no-color':
86
+ args.color = false;
87
+ break;
88
+ }
89
+ continue;
90
+ }
91
+ if (arg.startsWith('-')) {
92
+ // Silently ignoring an unknown flag is how `--jsonl` spends an
93
+ // afternoon looking like a broken --json.
94
+ return { ...args, error: `Unknown option: ${arg}\nRun with --help to see what this accepts.` };
95
+ }
96
+ if (args.ref === undefined)
97
+ args.ref = arg;
98
+ else
99
+ return { ...args, error: `Unexpected argument: ${arg}. Pass a message with --message instead.` };
100
+ }
101
+ return args;
102
+ }
103
+ /**
104
+ * The agent to open: the argument, else $ALMYTY_AGENT.
105
+ *
106
+ * The environment variable is how a compiled terminal app knows which
107
+ * agent it is, since the build compiles this client unchanged and the
108
+ * recipient should not have to type an agent reference.
109
+ */
110
+ export function resolveRef(args, env = process.env) {
111
+ return args.ref ?? (env.ALMYTY_AGENT || undefined);
112
+ }
113
+ /** Split `<org>/<agent>` into its parts; a bare slug leaves org unset. */
114
+ export function splitRef(ref) {
115
+ const slash = ref.indexOf('/');
116
+ if (slash === -1)
117
+ return { agentSlug: ref };
118
+ return { orgSlug: ref.slice(0, slash), agentSlug: ref.slice(slash + 1) };
119
+ }
120
+ /**
121
+ * Whether to run without a terminal UI.
122
+ *
123
+ * A CLI that only works on a tty is half a CLI: piping in a question or
124
+ * out to jq has to work, and ink cannot draw to a pipe.
125
+ */
126
+ export function isNonInteractive(args, io = {}) {
127
+ if (args.message !== undefined || args.json || args.stdin)
128
+ return true;
129
+ return io.stdinTty === false || io.stdoutTty === false;
130
+ }
131
+ /**
132
+ * Whether to emit colour.
133
+ *
134
+ * Honours the NO_COLOR convention and FORCE_COLOR, and never colours a
135
+ * pipe unless asked to.
136
+ */
137
+ export function useColor(args, env = process.env, stdoutTty = true) {
138
+ if (args.color !== undefined)
139
+ return args.color;
140
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
141
+ return false;
142
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0')
143
+ return true;
144
+ if (env.TERM === 'dumb')
145
+ return false;
146
+ return stdoutTty;
147
+ }
148
+ /**
149
+ * `--help`, built from the same command table the REPL resolves
150
+ * against, so a new slash command cannot go undocumented.
151
+ */
152
+ export function helpText(version) {
153
+ const commands = SLASH_COMMANDS.map((cmd) => ` /${cmd.padEnd(11)}${COMMAND_DESCS[cmd] ?? ''}`).join('\n');
154
+ return `almyty chat v${version} — an interactive REPL for your almyty agents.
155
+
156
+ Usage:
157
+ almyty chat [<org>/<agent-slug>] [options]
158
+
159
+ With no agent reference it lists the agents you can reach and asks.
160
+ A bare slug uses the organization on your credentials.
161
+
162
+ Options:
163
+ -m, --message <text> Ask one question, print the answer, exit.
164
+ --stdin Read the question from stdin (for pipes).
165
+ --resume <id> Continue a previous conversation.
166
+ --json One JSON object per answer. Implies non-interactive.
167
+ --no-stream Wait for the whole answer instead of streaming it.
168
+ --no-color Never colour the output. NO_COLOR is honoured too.
169
+ --max-steps <n> Autonomous runs: cap the number of steps.
170
+ --max-cost-cents <n> Autonomous runs: cap the spend, in cents.
171
+ -h, --help Show this.
172
+ -v, --version Print the version.
173
+
174
+ Slash commands, inside the REPL:
175
+ ${commands}
176
+
177
+ Commands take unique prefixes and aliases: /q for /quit, /sw for /agents.
178
+ Tab completes, up and down walk your input history.
179
+
180
+ Keys:
181
+ Ctrl-C Cancel the running answer (server-side too). Again to exit.
182
+ Ctrl-D Exit.
183
+ Enter Send. End a line with \\ to keep typing on the next one.
184
+
185
+ Non-interactive:
186
+ almyty chat acme/support-bot -m "what is our refund window?"
187
+ echo "summarise today's errors" | almyty chat acme/ops --stdin
188
+ almyty chat acme/ops -m "check the deploy" --json | jq .output
189
+
190
+ The answer goes to stdout and the attribution to stderr, so a pipe
191
+ stays clean.
192
+
193
+ Exit codes:
194
+ ${EXIT_CODE_HELP}
195
+
196
+ Environment:
197
+ ALMYTY_TOKEN Token override, instead of ~/.almyty/credentials.json.
198
+ ALMYTY_URL API URL override.
199
+ ALMYTY_AGENT Default agent reference, used when none is given.
200
+ ALMYTY_APP_URL Dashboard URL used in error messages.
201
+ ALMYTY_CHAT_HISTORY Input history file, instead of ~/.almyty/chat-history.
202
+ NO_COLOR Set to anything to disable colour.
203
+
204
+ Login:
205
+ npx @almyty/auth login
206
+ `;
207
+ }
@@ -1,5 +1,45 @@
1
- export declare const SLASH_COMMANDS: readonly ["agents", "tools", "help", "clear", "quit"];
1
+ import type { RunnerSummary } from '@almyty/client';
2
+ export declare const SLASH_COMMANDS: readonly ["agents", "model", "tools", "cost", "trace", "resume", "new", "runners", "code", "code-stop", "esc", "help", "clear", "quit"];
2
3
  export declare const ALIASES: Record<string, string>;
3
4
  export declare const COMMAND_DESCS: Record<string, string>;
4
5
  export declare function resolveSlash(input: string): string | null;
5
6
  export declare function getSuggestion(partial: string): string;
7
+ /**
8
+ * Whether a submitted line asks to keep typing.
9
+ *
10
+ * A single-line prompt still has to accept a paragraph, so a trailing
11
+ * backslash continues onto the next line the way a shell does. Returns
12
+ * the text without the continuation marker, or null when the line is
13
+ * complete.
14
+ */
15
+ export declare function continuationOf(value: string): string | null;
16
+ /**
17
+ * A pasted block, as one message.
18
+ *
19
+ * A multi-line paste used to submit on its first newline and hand the
20
+ * rest to the prompt a line at a time — so a pasted stack trace became
21
+ * a dozen messages, and any line of it starting with `/` ran as a
22
+ * command. Newlines inside a submitted value are content.
23
+ */
24
+ export declare function joinSubmission(lines: string[]): string;
25
+ /** Whether a submission should be read as a slash command. */
26
+ export declare function isSlashCommand(value: string): boolean;
27
+ export type InputRoute = 'command' | 'coding' | 'chat';
28
+ /**
29
+ * Where a submitted line goes: slash commands are always commands; when a
30
+ * coding session is active, everything else routes to the session's stdin;
31
+ * otherwise it's a normal chat message.
32
+ */
33
+ export declare function classifyInput(value: string, codingActive: boolean): InputRoute;
34
+ export interface CodeChoice {
35
+ runnerId: string;
36
+ runnerName: string;
37
+ agentId: string;
38
+ agentName: string;
39
+ }
40
+ /**
41
+ * Expand online runners x detected coding CLIs into a flat choice list for
42
+ * the selector. Offline/stale runners and runners without any detected
43
+ * coding CLI are excluded.
44
+ */
45
+ export declare function buildCodeChoices(runners: RunnerSummary[]): CodeChoice[];
package/dist/commands.js CHANGED
@@ -1,17 +1,36 @@
1
1
  // ── Slash command resolution ────────────────────────────────────
2
- export const SLASH_COMMANDS = ['agents', 'tools', 'help', 'clear', 'quit'];
2
+ export const SLASH_COMMANDS = [
3
+ 'agents', 'model', 'tools', 'cost', 'trace', 'resume', 'new',
4
+ 'runners', 'code', 'code-stop', 'esc', 'help', 'clear', 'quit',
5
+ ];
3
6
  export const ALIASES = {
4
7
  agent: 'agents', ag: 'agents', switch: 'agents', sw: 'agents',
5
8
  tool: 'tools', t: 'tools',
9
+ usage: 'cost', spend: 'cost',
10
+ steps: 'trace', last: 'trace',
11
+ reset: 'new',
12
+ // `r` predates /resume and kept meaning runners; /res resolves resume.
13
+ runner: 'runners', r: 'runners',
14
+ stop: 'code-stop',
15
+ detach: 'esc',
6
16
  h: 'help', '?': 'help',
7
17
  cls: 'clear', c: 'clear',
8
18
  exit: 'quit', q: 'quit',
9
19
  };
10
20
  export const COMMAND_DESCS = {
11
21
  agents: 'browse and switch agents',
22
+ model: 'show the model and routing policy in use',
12
23
  tools: 'show available tools',
24
+ cost: 'show tokens and spend for this session',
25
+ trace: "show the last run's steps",
26
+ resume: 'print the command that resumes this conversation',
27
+ new: 'start a fresh conversation with the same agent',
28
+ runners: 'list your runners + coding CLIs',
29
+ code: 'run a coding task on a runner',
30
+ 'code-stop': 'stop the active coding session',
31
+ esc: 'leave coding mode (session keeps running)',
13
32
  help: 'show commands',
14
- clear: 'clear conversation',
33
+ clear: 'clear the transcript on screen',
15
34
  quit: 'exit',
16
35
  };
17
36
  export function resolveSlash(input) {
@@ -34,3 +53,73 @@ export function getSuggestion(partial) {
34
53
  const match = SLASH_COMMANDS.find(c => c.startsWith(p) && c !== p);
35
54
  return match ? `/${match}` : '';
36
55
  }
56
+ // ── Multi-line input ────────────────────────────────────────────
57
+ /**
58
+ * Whether a submitted line asks to keep typing.
59
+ *
60
+ * A single-line prompt still has to accept a paragraph, so a trailing
61
+ * backslash continues onto the next line the way a shell does. Returns
62
+ * the text without the continuation marker, or null when the line is
63
+ * complete.
64
+ */
65
+ export function continuationOf(value) {
66
+ if (!/\\$/.test(value))
67
+ return null;
68
+ // An escaped backslash at the end is a literal one, not a hinge.
69
+ const trailing = value.length - value.replace(/\\+$/, '').length;
70
+ if (trailing % 2 === 0)
71
+ return null;
72
+ return value.slice(0, -1);
73
+ }
74
+ /**
75
+ * A pasted block, as one message.
76
+ *
77
+ * A multi-line paste used to submit on its first newline and hand the
78
+ * rest to the prompt a line at a time — so a pasted stack trace became
79
+ * a dozen messages, and any line of it starting with `/` ran as a
80
+ * command. Newlines inside a submitted value are content.
81
+ */
82
+ export function joinSubmission(lines) {
83
+ return lines.join('\n').replace(/\s+$/, '');
84
+ }
85
+ /** Whether a submission should be read as a slash command. */
86
+ export function isSlashCommand(value) {
87
+ const trimmed = value.trim();
88
+ // Only a single-line submission can be a command: a pasted block that
89
+ // happens to begin with a slash is text.
90
+ return trimmed.startsWith('/') && !trimmed.includes('\n');
91
+ }
92
+ /**
93
+ * Where a submitted line goes: slash commands are always commands; when a
94
+ * coding session is active, everything else routes to the session's stdin;
95
+ * otherwise it's a normal chat message.
96
+ */
97
+ export function classifyInput(value, codingActive) {
98
+ if (isSlashCommand(value))
99
+ return 'command';
100
+ return codingActive ? 'coding' : 'chat';
101
+ }
102
+ // ── /code target selection ──────────────────────────────────────
103
+ /** Runner states that can accept a coding dispatch. */
104
+ const DISPATCHABLE_STATES = new Set(['online', 'busy']);
105
+ /**
106
+ * Expand online runners x detected coding CLIs into a flat choice list for
107
+ * the selector. Offline/stale runners and runners without any detected
108
+ * coding CLI are excluded.
109
+ */
110
+ export function buildCodeChoices(runners) {
111
+ const choices = [];
112
+ for (const runner of runners) {
113
+ if (!DISPATCHABLE_STATES.has(runner.state ?? ''))
114
+ continue;
115
+ for (const agent of runner.codingAgents ?? []) {
116
+ choices.push({
117
+ runnerId: runner.id,
118
+ runnerName: runner.name,
119
+ agentId: agent.id,
120
+ agentName: agent.displayName || agent.id,
121
+ });
122
+ }
123
+ }
124
+ return choices;
125
+ }
@@ -1,37 +1,61 @@
1
1
  import React from 'react';
2
2
  import type { AgentInfo } from '@almyty/client';
3
3
  export interface Message {
4
- role: 'user' | 'agent' | 'error' | 'info' | 'tool';
4
+ role: 'user' | 'agent' | 'error' | 'info' | 'tool' | 'coding';
5
5
  text: string;
6
6
  }
7
7
  export declare function MarkdownText({ children, prefix, prefixColor }: {
8
8
  children: string;
9
9
  prefix?: string;
10
10
  prefixColor?: string;
11
- }): import("react/jsx-runtime").JSX.Element;
11
+ }): React.JSX.Element;
12
12
  export declare function InlineFormat({ text }: {
13
13
  text: string;
14
- }): import("react/jsx-runtime").JSX.Element;
14
+ }): React.JSX.Element;
15
15
  export declare function boldify(text: string): React.ReactElement;
16
+ /**
17
+ * Re-exported so callers do not have to know the arithmetic moved.
18
+ * Takes a message rather than text, which is how it has always read.
19
+ */
16
20
  export declare function estimateLines(msg: Message, cols: number): number;
17
- export declare function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset }: {
21
+ export declare function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset, streaming }: {
18
22
  messages: Message[];
19
23
  loading: boolean;
20
24
  loadingLabel: string;
21
25
  maxRows: number;
22
26
  scrollOffset?: number;
23
- }): import("react/jsx-runtime").JSX.Element;
27
+ /** Assistant text arriving right now, drawn below the transcript. */
28
+ streaming?: string;
29
+ }): React.JSX.Element;
24
30
  export declare function Header({ agent, conversationId }: {
25
31
  agent: AgentInfo;
26
32
  conversationId?: string | null;
27
- }): import("react/jsx-runtime").JSX.Element;
33
+ }): React.JSX.Element;
28
34
  export declare function MessageView({ msg }: {
29
35
  msg: Message;
30
- }): import("react/jsx-runtime").JSX.Element | null;
36
+ }): React.JSX.Element | null;
31
37
  export declare function LoadingIndicator({ label }: {
32
38
  label: string;
33
- }): import("react/jsx-runtime").JSX.Element;
39
+ }): React.JSX.Element;
34
40
  export declare function AgentSelector({ agents, onSelect }: {
35
41
  agents: AgentInfo[];
36
42
  onSelect: (a: AgentInfo) => void;
37
- }): import("react/jsx-runtime").JSX.Element;
43
+ }): React.JSX.Element;
44
+ /**
45
+ * Visible mode banner while a coding session is active: input routes to the
46
+ * session, /esc detaches, /code-stop kills it.
47
+ */
48
+ export declare function CodingModeIndicator({ agent, runner }: {
49
+ agent: string;
50
+ runner: string;
51
+ }): React.JSX.Element;
52
+ export interface Choice {
53
+ key: string;
54
+ label: string;
55
+ hint?: string;
56
+ }
57
+ export declare function ChoiceSelector({ title, choices, onSelect }: {
58
+ title: string;
59
+ choices: Choice[];
60
+ onSelect: (c: Choice) => void;
61
+ }): React.JSX.Element;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
+ import { columns, estimateLines as estimateMessageLines, selectWindow } from './viewport.js';
5
6
  // ── Simple markdown rendering ───────────────────────────────────
6
7
  export function MarkdownText({ children, prefix, prefixColor }) {
7
8
  const lines = children.split('\n');
@@ -90,35 +91,26 @@ export function boldify(text) {
90
91
  }
91
92
  return _jsx(_Fragment, { children: parts });
92
93
  }
93
- // ── Message window (manual scroll) ──────────────────────────────
94
+ // ── Message window (bounded to the terminal) ────────────────────
95
+ /**
96
+ * Re-exported so callers do not have to know the arithmetic moved.
97
+ * Takes a message rather than text, which is how it has always read.
98
+ */
94
99
  export function estimateLines(msg, cols) {
95
- const textWidth = Math.max(cols - 10, 20);
96
- const lines = msg.text.split('\n');
97
- let total = 1; // margin
98
- for (const line of lines) {
99
- total += Math.max(1, Math.ceil(Math.max(line.length, 1) / textWidth));
100
- }
101
- return total;
100
+ return estimateMessageLines(msg.text, cols);
102
101
  }
103
- export function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset = 0 }) {
104
- const cols = process.stdout.columns || 80;
105
- const available = Math.max(maxRows, 5);
106
- // Calculate the end of the visible window (shifted by scrollOffset)
107
- const endIdx = Math.max(0, messages.length - scrollOffset);
108
- // Walk backwards from endIdx to find how many messages fit
109
- let usedRows = loading && scrollOffset === 0 ? 2 : 0;
110
- let startIdx = endIdx;
111
- for (let i = endIdx - 1; i >= 0; i--) {
112
- const est = estimateLines(messages[i], cols);
113
- if (usedRows + est > available && i < endIdx - 1)
114
- break;
115
- usedRows += est;
116
- startIdx = i;
117
- }
102
+ export function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset = 0, streaming }) {
103
+ const cols = columns(process.stdout.columns);
104
+ const streamRows = streaming ? estimateMessageLines(streaming, cols) : 0;
105
+ const { startIdx, endIdx, hiddenBefore, hiddenAfter } = selectWindow(messages, {
106
+ rows: maxRows,
107
+ cols,
108
+ // The spinner and the text still arriving both need room.
109
+ reservedRows: (loading && scrollOffset === 0 ? 2 : 0) + streamRows,
110
+ scrollOffset,
111
+ });
118
112
  const visible = messages.slice(startIdx, endIdx);
119
- const hasEarlier = startIdx > 0;
120
- const hasLater = endIdx < messages.length;
121
- return (_jsxs(Box, { flexDirection: "column", children: [hasEarlier && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIdx, " earlier \u00B7 scroll to see more"] }) })), visible.map((msg, i) => (_jsx(MessageView, { msg: msg }, startIdx + i))), loading && scrollOffset === 0 && _jsx(LoadingIndicator, { label: loadingLabel }), hasLater && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2193 ", messages.length - endIdx, " newer \u00B7 scroll to see more"] }) }))] }));
113
+ return (_jsxs(Box, { flexDirection: "column", children: [hiddenBefore > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2191 ", hiddenBefore, " earlier \u00B7 scroll to see more"] }) })), visible.map((msg, i) => (_jsx(MessageView, { msg: msg }, startIdx + i))), streaming && scrollOffset === 0 && _jsx(MessageView, { msg: { role: 'agent', text: streaming } }), loading && scrollOffset === 0 && _jsx(LoadingIndicator, { label: loadingLabel }), hiddenAfter > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2193 ", hiddenAfter, " newer \u00B7 scroll to see more"] }) }))] }));
122
114
  }
123
115
  // ── Components ──────────────────────────────────────────────────
124
116
  export function Header({ agent, conversationId }) {
@@ -137,6 +129,10 @@ export function MessageView({ msg }) {
137
129
  return (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: "red", children: ["\u2502 ", msg.text] }) }));
138
130
  case 'info':
139
131
  return (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2502 ", msg.text] }) }));
132
+ case 'coding':
133
+ // Coding-session output: distinct cyan gutter so streamed CLI output
134
+ // reads apart from agent/chat messages.
135
+ return (_jsx(Box, { paddingLeft: 2, flexDirection: "column", children: msg.text.replace(/\n+$/, '').split('\n').map((line, i) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "#22d3ee", children: "\u258D " }), _jsx(Text, { dimColor: true, children: line || ' ' })] }, i))) }));
140
136
  default:
141
137
  return null;
142
138
  }
@@ -157,3 +153,23 @@ export function AgentSelector({ agents, onSelect }) {
157
153
  });
158
154
  return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Text, { dimColor: true, children: " Select an agent:" }), _jsx(Text, { children: " " }), agents.map((ag, i) => (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: i === cursor ? '#8b5cf6' : undefined, children: [i === cursor ? '❯' : ' ', ' '] }), _jsx(Text, { bold: i === cursor, children: ag.name }), _jsxs(Text, { dimColor: true, children: [" ", ag.mode] })] }, ag.id))), agents[cursor]?.description && (_jsx(Box, { paddingLeft: 4, paddingTop: 1, children: _jsx(Text, { dimColor: true, children: agents[cursor].description }) }))] }));
159
155
  }
156
+ // ── Coding mode ─────────────────────────────────────────────────
157
+ /**
158
+ * Visible mode banner while a coding session is active: input routes to the
159
+ * session, /esc detaches, /code-stop kills it.
160
+ */
161
+ export function CodingModeIndicator({ agent, runner }) {
162
+ return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { backgroundColor: "#0891B2", color: "black", children: " coding " }), _jsxs(Text, { color: "#22d3ee", children: [" ", agent, "@", runner] }), _jsx(Text, { dimColor: true, children: " input goes to the session \u00B7 /esc detach \u00B7 /code-stop stop" })] }));
163
+ }
164
+ export function ChoiceSelector({ title, choices, onSelect }) {
165
+ const [cursor, setCursor] = useState(0);
166
+ useInput((input, key) => {
167
+ if (key.upArrow)
168
+ setCursor(c => (c - 1 + choices.length) % choices.length);
169
+ if (key.downArrow)
170
+ setCursor(c => (c + 1) % choices.length);
171
+ if (key.return)
172
+ onSelect(choices[cursor]);
173
+ });
174
+ return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsxs(Text, { dimColor: true, children: [" ", title] }), _jsx(Text, { children: " " }), choices.map((choice, i) => (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: i === cursor ? '#8b5cf6' : undefined, children: [i === cursor ? '❯' : ' ', ' '] }), _jsx(Text, { bold: i === cursor, children: choice.label }), choice.hint && _jsxs(Text, { dimColor: true, children: [" ", choice.hint] })] }, choice.key)))] }));
175
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * One sentence a user can act on, for every way a chat session fails.
3
+ *
4
+ * A REPL that answers "API error 400: {"success":false,...}" has told the
5
+ * user nothing. Every failure a session can reach has a cause and a next
6
+ * step, and both are known here rather than left for the reader to infer
7
+ * from a status code.
8
+ *
9
+ * Kept free of ink and of the network client so it can be tested as the
10
+ * pure mapping it is.
11
+ */
12
+ /** What the session knows about itself when something fails. */
13
+ export interface ErrorContext {
14
+ /** `<org>/<agent-slug>`, as the user typed it. */
15
+ agentRef?: string;
16
+ /** API base URL, for "cannot reach" messages. */
17
+ apiUrl?: string;
18
+ /** Dashboard URL, for "fix it here" messages. */
19
+ appUrl?: string;
20
+ /** What the session was doing: shapes the timeout wording. */
21
+ what?: 'run' | 'info' | 'history' | 'stream' | 'cancel';
22
+ }
23
+ export declare const DEFAULT_APP_URL = "https://app.almyty.com";
24
+ interface Failure {
25
+ status?: number;
26
+ code?: string;
27
+ serverMessage?: string;
28
+ message: string;
29
+ network: boolean;
30
+ aborted: boolean;
31
+ pollTimeout: boolean;
32
+ }
33
+ /** Pull everything useful off a thrown value, whatever shape it has. */
34
+ export declare function inspectError(err: unknown): Failure;
35
+ /** The model name out of a MODEL_NOT_FOUND message, when it names one. */
36
+ export declare function extractModelName(message: string): string | null;
37
+ /**
38
+ * Turn a thrown value into one sentence, plus the command or URL that
39
+ * fixes it.
40
+ */
41
+ export declare function explainError(err: unknown, ctx?: ErrorContext): string;
42
+ /**
43
+ * Why `<org>/<agent>` could not be opened at startup.
44
+ *
45
+ * Startup is the one place a bad reference and a bad login look the
46
+ * same, and where "Agent not found" was printed for both.
47
+ */
48
+ export declare function explainStartupFailure(err: unknown, ctx?: ErrorContext): string;
49
+ export {};