@orbit-intelligence/orbit-agent 0.3.12
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/LICENSE +16 -0
- package/README.md +23 -0
- package/bin/orbit +26 -0
- package/dist/prompts/system.js +80 -0
- package/dist/src/cli/args.js +145 -0
- package/dist/src/cli/orchestrate.js +100 -0
- package/dist/src/cli/run.js +393 -0
- package/dist/src/config/config-schema.js +151 -0
- package/dist/src/config/index.js +57 -0
- package/dist/src/core/agent/agent-loop.js +402 -0
- package/dist/src/core/agents/delegate.js +120 -0
- package/dist/src/core/agents/orchestrator.js +58 -0
- package/dist/src/core/agents/prompts.js +82 -0
- package/dist/src/core/agents/types.js +1 -0
- package/dist/src/core/context/context-manager.js +167 -0
- package/dist/src/core/events.js +23 -0
- package/dist/src/core/llm/http.js +207 -0
- package/dist/src/core/llm/index.js +93 -0
- package/dist/src/core/llm/models.js +228 -0
- package/dist/src/core/llm/providers/gemini.js +211 -0
- package/dist/src/core/llm/providers/openai-compat.js +31 -0
- package/dist/src/core/llm/router.js +125 -0
- package/dist/src/core/llm/secrets.js +121 -0
- package/dist/src/core/llm/types.js +10 -0
- package/dist/src/core/orchestration/dispatcher.js +74 -0
- package/dist/src/core/orchestration/messenger.js +139 -0
- package/dist/src/core/orchestration/roles.js +129 -0
- package/dist/src/core/orchestration/runtime.js +122 -0
- package/dist/src/core/orchestration/session.js +204 -0
- package/dist/src/core/orchestration/shared-context.js +88 -0
- package/dist/src/core/orchestration/tools.js +187 -0
- package/dist/src/core/orchestration/types.js +3 -0
- package/dist/src/core/permissions/index.js +58 -0
- package/dist/src/core/project-context.js +115 -0
- package/dist/src/core/skill-loader.js +31 -0
- package/dist/src/core/tools/edit.js +142 -0
- package/dist/src/core/tools/filesystem.js +203 -0
- package/dist/src/core/tools/git.js +138 -0
- package/dist/src/core/tools/registry.js +73 -0
- package/dist/src/core/tools/search.js +90 -0
- package/dist/src/core/tools/shell.js +65 -0
- package/dist/src/core/tools/types.js +6 -0
- package/dist/src/core/types.js +3 -0
- package/dist/src/index.js +11 -0
- package/dist/src/session/event-log.js +55 -0
- package/dist/src/session/store.js +76 -0
- package/dist/src/setup/wizard.js +401 -0
- package/dist/src/tui/InkApp.js +67 -0
- package/dist/src/tui/ansi.js +142 -0
- package/dist/src/tui/app.js +768 -0
- package/dist/src/tui/colors.js +13 -0
- package/dist/src/tui/components/AgentDock.js +46 -0
- package/dist/src/tui/components/Composer.js +35 -0
- package/dist/src/tui/components/Header.js +23 -0
- package/dist/src/tui/components/ModelPicker.js +23 -0
- package/dist/src/tui/components/PermissionModal.js +29 -0
- package/dist/src/tui/components/SlashMenu.js +15 -0
- package/dist/src/tui/components/StatusLine.js +27 -0
- package/dist/src/tui/components/Transcript.js +31 -0
- package/dist/src/tui/components/WorkingStatus.js +29 -0
- package/dist/src/tui/components/input.js +246 -0
- package/dist/src/tui/components/markdown.js +384 -0
- package/dist/src/tui/components/message.js +105 -0
- package/dist/src/tui/context.js +8 -0
- package/dist/src/tui/geometry.js +40 -0
- package/dist/src/tui/renderer.js +116 -0
- package/dist/src/tui/rows.js +247 -0
- package/dist/src/tui/scheduler.js +32 -0
- package/dist/src/tui/store.js +127 -0
- package/dist/src/tui/style.js +151 -0
- package/dist/src/tui/term.js +309 -0
- package/dist/src/tui/text.js +104 -0
- package/dist/src/tui/themes/index.js +15 -0
- package/dist/src/tui/themes/palettes.js +137 -0
- package/dist/src/tui/themes/types.js +1 -0
- package/dist/src/utils/diff.js +161 -0
- package/dist/src/utils/platform.js +71 -0
- package/dist/src/utils/signals.js +26 -0
- package/dist/src/version.js +4 -0
- package/package.json +71 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { wrapText, truncate } from './text.js';
|
|
2
|
+
import { renderMarkdown } from './components/markdown.js';
|
|
3
|
+
// Rendered rows for finalized messages are immutable (they never re-wrap while
|
|
4
|
+
// streaming) but re-created by the app every frame. Cache them per message so
|
|
5
|
+
// the per-token redraw only re-wraps the live streaming message — this removes
|
|
6
|
+
// the markdown/hljs churn that made the transcript fluctuate and lag.
|
|
7
|
+
const MESSAGE_CACHE_MAX = 300;
|
|
8
|
+
const messageCache = new Map();
|
|
9
|
+
// Zero-width and format characters (U+200B–U+200D, U+2060, U+FEFF) that some
|
|
10
|
+
// providers pad model output with. They have no visual width, so a markdown
|
|
11
|
+
// line made only of them would otherwise render as a stray accent `•`.
|
|
12
|
+
const INVISIBLE = /[\u200b-\u200d\u2060\ufeff]/g;
|
|
13
|
+
/** Strip ANSI escapes and zero-width formatting chars — the visible text. */
|
|
14
|
+
function visibleText(s) {
|
|
15
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').replace(INVISIBLE, '');
|
|
16
|
+
}
|
|
17
|
+
export function rowsFromMessages(messages, theme, opts) {
|
|
18
|
+
const rows = [];
|
|
19
|
+
for (let i = 0; i < messages.length; i++) {
|
|
20
|
+
const msg = messages[i];
|
|
21
|
+
const isStreaming = msg.id === opts.streamingId;
|
|
22
|
+
const mr = isStreaming
|
|
23
|
+
? messageRows({ ...msg, content: revealSlice(msg.content, opts.revealChars) }, theme, opts)
|
|
24
|
+
: messageRowsCached(msg, theme, opts);
|
|
25
|
+
rows.push(...mr);
|
|
26
|
+
if (i < messages.length - 1) {
|
|
27
|
+
rows.push({ id: `${msg.id}-gap`, text: '' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return rows;
|
|
31
|
+
}
|
|
32
|
+
function messageRowsCached(msg, theme, opts) {
|
|
33
|
+
const key = messageCacheKey(msg, opts, theme.name);
|
|
34
|
+
const hit = messageCache.get(key);
|
|
35
|
+
if (hit)
|
|
36
|
+
return hit;
|
|
37
|
+
const rows = messageRows(msg, theme, opts);
|
|
38
|
+
if (messageCache.size >= MESSAGE_CACHE_MAX) {
|
|
39
|
+
const first = messageCache.keys().next().value;
|
|
40
|
+
if (first !== undefined)
|
|
41
|
+
messageCache.delete(first);
|
|
42
|
+
}
|
|
43
|
+
messageCache.set(key, rows);
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
function messageCacheKey(msg, opts, themeName) {
|
|
47
|
+
return [
|
|
48
|
+
msg.id,
|
|
49
|
+
msg.content.length,
|
|
50
|
+
opts.width,
|
|
51
|
+
opts.thinkingDefaultOpen ? 1 : 0,
|
|
52
|
+
msg.reasoningOpen ? 1 : 0,
|
|
53
|
+
msg.diffsOpen ? 1 : 0,
|
|
54
|
+
msg.isPlan ? 1 : 0,
|
|
55
|
+
msg.toolCalls?.length ?? 0,
|
|
56
|
+
msg.diffs?.length ?? 0,
|
|
57
|
+
themeName,
|
|
58
|
+
].join(':');
|
|
59
|
+
}
|
|
60
|
+
function revealSlice(content, revealChars) {
|
|
61
|
+
if (revealChars == null || revealChars >= content.length)
|
|
62
|
+
return content;
|
|
63
|
+
const slice = content.slice(0, revealChars);
|
|
64
|
+
const last = slice.charCodeAt(slice.length - 1);
|
|
65
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
66
|
+
return slice.slice(0, -1);
|
|
67
|
+
return slice;
|
|
68
|
+
}
|
|
69
|
+
function messageRows(msg, theme, opts) {
|
|
70
|
+
switch (msg.role) {
|
|
71
|
+
case 'user':
|
|
72
|
+
return userRows(msg, theme, opts);
|
|
73
|
+
case 'assistant':
|
|
74
|
+
return assistantRows(msg, theme, opts);
|
|
75
|
+
case 'system':
|
|
76
|
+
return systemRows(msg, theme, opts);
|
|
77
|
+
default:
|
|
78
|
+
return userRows({ ...msg, content: `[${msg.role}] ${msg.content}` }, theme, opts);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// User — Codex style: `›` + dim text, wrapped continuation lines.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
function userRows(msg, theme, opts) {
|
|
85
|
+
const accent = theme.table.sgr(theme.styles['accent']);
|
|
86
|
+
const dim = theme.table.sgr(theme.styles['dim']);
|
|
87
|
+
const reset = '\x1b[0m';
|
|
88
|
+
const innerW = Math.max(8, opts.width - 2);
|
|
89
|
+
const wrapped = wrapText(msg.content, innerW);
|
|
90
|
+
const rows = [];
|
|
91
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
92
|
+
const line = wrapped[i];
|
|
93
|
+
const prefix = i === 0 ? `${accent}›${reset} ` : `${dim} ${reset}`;
|
|
94
|
+
rows.push({
|
|
95
|
+
id: `${msg.id}-u${i}`,
|
|
96
|
+
text: `${prefix}${dim}${line}${reset}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (rows.length === 0)
|
|
100
|
+
rows.push({ id: `${msg.id}-u0`, text: `${accent}›${reset}` });
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Assistant — `•` body, thinking block, tool lines, streaming marker.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
function assistantRows(msg, theme, opts) {
|
|
107
|
+
const accent = theme.table.sgr(theme.styles['accent']);
|
|
108
|
+
const muted = theme.table.sgr(theme.styles['muted']);
|
|
109
|
+
const dim = theme.table.sgr(theme.styles['dim']);
|
|
110
|
+
const reset = '\x1b[0m';
|
|
111
|
+
const danger = theme.table.sgr(theme.styles['danger']);
|
|
112
|
+
const toolDone = theme.table.sgr(theme.styles['toolDone']);
|
|
113
|
+
const rows = [];
|
|
114
|
+
if (msg.isPlan) {
|
|
115
|
+
rows.push({
|
|
116
|
+
id: `${msg.id}-plan-head`,
|
|
117
|
+
text: `${toolDone}PLAN${reset} ${muted}proposed (approve with /run · reject with /reject)${reset}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (msg.reasoning && msg.reasoning.length > 0) {
|
|
121
|
+
const open = opts.thinkingDefaultOpen ?? msg.reasoningOpen ?? false;
|
|
122
|
+
const tokenCount = tokenEstimate(msg.reasoning);
|
|
123
|
+
const indicator = open ? '▾' : '▸';
|
|
124
|
+
rows.push({
|
|
125
|
+
id: `${msg.id}-th`,
|
|
126
|
+
text: `${muted}${indicator} thinking · ~${tokenCount} tokens (ctrl+t)${reset}`,
|
|
127
|
+
});
|
|
128
|
+
if (open) {
|
|
129
|
+
const innerW = Math.max(8, opts.width - 4);
|
|
130
|
+
for (const line of wrapText(msg.reasoning, innerW)) {
|
|
131
|
+
rows.push({ id: `${msg.id}-th-${rows.length}`, text: `${muted} ${line}${reset}` });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (msg.content) {
|
|
136
|
+
const md = renderMarkdown(msg.content, { theme, width: Math.max(8, opts.width - 2) });
|
|
137
|
+
for (let i = 0; i < md.length; i++) {
|
|
138
|
+
const line = md[i];
|
|
139
|
+
// Skip lines with no visible glyphs (blank or zero-width padding) so
|
|
140
|
+
// they never surface as bare `•` bullets while the turn streams in.
|
|
141
|
+
if (visibleText(line).trim() === '')
|
|
142
|
+
continue;
|
|
143
|
+
const prefix = rows.length === 0 && i === 0 ? `${accent}•${reset} ` : `${dim} ${reset}`;
|
|
144
|
+
rows.push({ id: `${msg.id}-c${i}`, text: `${prefix}${line}${reset}` });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
148
|
+
for (const tc of msg.toolCalls) {
|
|
149
|
+
rows.push(...toolRows(tc, theme, opts, `${msg.id}-tc`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (msg.diffs && msg.diffs.length > 0) {
|
|
153
|
+
const total = msg.diffs.reduce((n, d) => n + d.added, 0);
|
|
154
|
+
const removed = msg.diffs.reduce((n, d) => n + d.removed, 0);
|
|
155
|
+
const state = msg.diffsOpen ? '▾' : '▸';
|
|
156
|
+
rows.push({
|
|
157
|
+
id: `${msg.id}-diff-head`,
|
|
158
|
+
text: `${toolDone}${state}${reset} ${muted}${msg.diffs.length} file(s) changed · +${total} −${removed} · ctrl+d to ${msg.diffsOpen ? 'hide' : 'review'}${reset}`,
|
|
159
|
+
});
|
|
160
|
+
if (msg.diffsOpen) {
|
|
161
|
+
const innerW = Math.max(8, opts.width - 4);
|
|
162
|
+
for (const d of msg.diffs) {
|
|
163
|
+
rows.push({
|
|
164
|
+
id: `${msg.id}-diff-file-${d.path}`,
|
|
165
|
+
text: `${accent}◗ ${d.path}${reset} ${muted}(+${d.added} −${d.removed})${reset}`,
|
|
166
|
+
});
|
|
167
|
+
for (const line of d.diff.split('\n')) {
|
|
168
|
+
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) {
|
|
169
|
+
rows.push({ id: `${msg.id}-diff-${d.path}-${rows.length}`, text: `${dim}${line}${reset}` });
|
|
170
|
+
}
|
|
171
|
+
else if (line.startsWith('+')) {
|
|
172
|
+
rows.push({ id: `${msg.id}-diff-${d.path}-${rows.length}`, text: `${toolDone}${truncate(line, innerW)}${reset}` });
|
|
173
|
+
}
|
|
174
|
+
else if (line.startsWith('-')) {
|
|
175
|
+
rows.push({ id: `${msg.id}-diff-${d.path}-${rows.length}`, text: `${danger}${truncate(line, innerW)}${reset}` });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
rows.push({ id: `${msg.id}-diff-${d.path}-${rows.length}`, text: `${dim}${truncate(line, innerW)}${reset}` });
|
|
179
|
+
}
|
|
180
|
+
if (rows.length > 500) {
|
|
181
|
+
rows.push({ id: `${msg.id}-diff-trunc`, text: `${muted}… diff elided (too large)${reset}` });
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (msg.streaming) {
|
|
189
|
+
rows.push({ id: `${msg.id}-streaming`, text: `${dim}…${reset}` });
|
|
190
|
+
}
|
|
191
|
+
if (rows.length === 0 && visibleText(msg.content).trim() === '') {
|
|
192
|
+
rows.push({ id: `${msg.id}-empty`, text: `${muted}•${reset}` });
|
|
193
|
+
}
|
|
194
|
+
return rows;
|
|
195
|
+
}
|
|
196
|
+
function toolRows(tc, theme, opts, baseId) {
|
|
197
|
+
const reset = '\x1b[0m';
|
|
198
|
+
const icon = tc.status === 'done'
|
|
199
|
+
? { sgr: theme.table.sgr(theme.styles['toolDone']), ch: '✓' }
|
|
200
|
+
: tc.status === 'error'
|
|
201
|
+
? { sgr: theme.table.sgr(theme.styles['toolError']), ch: '✗' }
|
|
202
|
+
: tc.status === 'running'
|
|
203
|
+
? { sgr: theme.table.sgr(theme.styles['toolRunning']), ch: '●' }
|
|
204
|
+
: { sgr: theme.table.sgr(theme.styles['toolPending']), ch: '○' };
|
|
205
|
+
const accent = theme.table.sgr(theme.styles['accent']);
|
|
206
|
+
const dim = theme.table.sgr(theme.styles['dim']);
|
|
207
|
+
const innerW = Math.max(8, opts.width - 4);
|
|
208
|
+
const duration = tc.durationMs != null ? ` · ${tc.durationMs}ms` : '';
|
|
209
|
+
const name = truncate(tc.name, Math.max(6, innerW - 24), '…');
|
|
210
|
+
const argsPreview = truncate(tc.args.replace(/\s+/g, ' ').trim(), Math.max(4, innerW - name.length - 6), '…');
|
|
211
|
+
const rows = [
|
|
212
|
+
{
|
|
213
|
+
id: `${baseId}-${tc.id}-head`,
|
|
214
|
+
text: `${accent}•${reset} ${icon.sgr}${icon.ch}${reset} ${dim}${name}${reset} ${argsPreview}${dim}${duration}${reset}`,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
if (tc.result) {
|
|
218
|
+
const clean = tc.result.replace(INVISIBLE, '').replace(/\s+/g, ' ').trim();
|
|
219
|
+
if (clean) {
|
|
220
|
+
const preview = truncate(clean, innerW, '…');
|
|
221
|
+
rows.push({ id: `${baseId}-${tc.id}-result`, text: `${dim} ${preview}${reset}` });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return rows;
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// System / notes & errors — `ℹ` info lines.
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
function systemRows(msg, theme, opts) {
|
|
230
|
+
const style = msg.error ? 'danger' : 'info';
|
|
231
|
+
const sgr = theme.table.sgr(theme.styles[style]);
|
|
232
|
+
const reset = '\x1b[0m';
|
|
233
|
+
const innerW = Math.max(8, opts.width - 4);
|
|
234
|
+
const prefix = `ℹ `;
|
|
235
|
+
const wrapped = wrapText(msg.content, innerW);
|
|
236
|
+
const rows = [];
|
|
237
|
+
for (let i = 0; i < wrapped.length; i++) {
|
|
238
|
+
const line = wrapped[i];
|
|
239
|
+
const raw = i === 0 ? `${sgr}${prefix}${line}${reset}` : `${sgr} ${line}${reset}`;
|
|
240
|
+
rows.push({ id: `${msg.id}-s${i}`, text: raw });
|
|
241
|
+
}
|
|
242
|
+
return rows;
|
|
243
|
+
}
|
|
244
|
+
function tokenEstimate(text) {
|
|
245
|
+
const n = Math.ceil(text.length / 4);
|
|
246
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`;
|
|
247
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame scheduler — batches redraws to ~30 fps to avoid jank under fast streaming.
|
|
3
|
+
*/
|
|
4
|
+
const FRAME_INTERVAL_MS = 33;
|
|
5
|
+
let renderFn = null;
|
|
6
|
+
let framePending = false;
|
|
7
|
+
let lastFrameTime = 0;
|
|
8
|
+
function flush() {
|
|
9
|
+
framePending = false;
|
|
10
|
+
lastFrameTime = Date.now();
|
|
11
|
+
renderFn?.();
|
|
12
|
+
}
|
|
13
|
+
export function setRenderCallback(fn) {
|
|
14
|
+
renderFn = fn;
|
|
15
|
+
}
|
|
16
|
+
export function requestFrame() {
|
|
17
|
+
if (framePending)
|
|
18
|
+
return;
|
|
19
|
+
framePending = true;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const elapsed = now - lastFrameTime;
|
|
22
|
+
const delay = Math.max(0, FRAME_INTERVAL_MS - elapsed);
|
|
23
|
+
setTimeout(flush, delay);
|
|
24
|
+
}
|
|
25
|
+
export function cancelFrame() {
|
|
26
|
+
framePending = false;
|
|
27
|
+
}
|
|
28
|
+
/** Immediate flush bypassing scheduler (for resize, key events, etc.). */
|
|
29
|
+
export function flushNow() {
|
|
30
|
+
cancelFrame();
|
|
31
|
+
flush();
|
|
32
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { InputState } from './components/input.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mutable UI state shared between the imperative agent/bus wiring and the Ink
|
|
4
|
+
* component tree. The React side mirrors it: components re-read these fields
|
|
5
|
+
* every time `refresh()` fires rather than duplicating event handling.
|
|
6
|
+
*/
|
|
7
|
+
/** Options shown in the centered permission overlay. Index 0 = allow. */
|
|
8
|
+
export const ASK_OPTIONS = ['Allow', 'Deny'];
|
|
9
|
+
const SLASH_COMMANDS = [
|
|
10
|
+
{ cmd: '/model', desc: '[id] select model' },
|
|
11
|
+
{ cmd: '/provider', desc: '<id> switch provider' },
|
|
12
|
+
{ cmd: '/theme', desc: '<name> switch theme' },
|
|
13
|
+
{ cmd: '/plan', desc: '<task> propose a plan (approve with /run)' },
|
|
14
|
+
{ cmd: '/run', desc: 'execute the proposed plan' },
|
|
15
|
+
{ cmd: '/skills', desc: 'list project skills' },
|
|
16
|
+
{ cmd: '/yolo', desc: 'auto-approve tools' },
|
|
17
|
+
{ cmd: '/clear', desc: 'clear transcript' },
|
|
18
|
+
{ cmd: '/agents', desc: 'toggle agent dock (orchestrate)' },
|
|
19
|
+
{ cmd: '/help', desc: 'show this help' },
|
|
20
|
+
{ cmd: '/exit', desc: 'quit' },
|
|
21
|
+
];
|
|
22
|
+
// Word-to-word reveal pacing. Servers stream sentence-sized chunks; the reveal
|
|
23
|
+
// cursor films their contents out at a steady character rate, word-aligned, so
|
|
24
|
+
// output reads "word by word" instead of "sentence by sentence".
|
|
25
|
+
const REVEAL_CPS = 30; // chars/sec baseline (~6-8 words/sec) — brisk typing feel
|
|
26
|
+
const REVEAL_LAG_CAP = 400; // caught-up threshold -> above it, drain 3x faster
|
|
27
|
+
export class AppStore {
|
|
28
|
+
messages = [];
|
|
29
|
+
streaming = false;
|
|
30
|
+
route = null;
|
|
31
|
+
input = new InputState();
|
|
32
|
+
pendingAsk = null;
|
|
33
|
+
inputTokens = 0;
|
|
34
|
+
outputTokens = 0;
|
|
35
|
+
scrollOffset = 0;
|
|
36
|
+
cancelArmed = false;
|
|
37
|
+
greeted = false;
|
|
38
|
+
version = '';
|
|
39
|
+
cwd = '';
|
|
40
|
+
models = [];
|
|
41
|
+
modelPicker = { open: false, index: 0 };
|
|
42
|
+
agents = new Map();
|
|
43
|
+
dockOpen = false;
|
|
44
|
+
yolo = false;
|
|
45
|
+
theme = null;
|
|
46
|
+
thinkingDefaultOpen = true;
|
|
47
|
+
plan = null;
|
|
48
|
+
/** True while a plan proposal is streaming. */
|
|
49
|
+
planningTurn = false;
|
|
50
|
+
/** Project skills available via /skills. */
|
|
51
|
+
skills = [];
|
|
52
|
+
/** Reveal cursor (JS chars) for the streaming message; null = show all. */
|
|
53
|
+
revealChars = null;
|
|
54
|
+
listeners = new Set();
|
|
55
|
+
refresh = () => {
|
|
56
|
+
for (const l of [...this.listeners]) {
|
|
57
|
+
try {
|
|
58
|
+
l();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// never let a listener break the agent loop
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
onRender(l) {
|
|
66
|
+
this.listeners.add(l);
|
|
67
|
+
return () => {
|
|
68
|
+
this.listeners.delete(l);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
findStreaming() {
|
|
72
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
73
|
+
const m = this.messages[i];
|
|
74
|
+
if (m?.role === 'assistant' && (m.streaming || this.streaming))
|
|
75
|
+
return m;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/** Start word-by-word reveal for a new assistant turn. */
|
|
80
|
+
beginStream() {
|
|
81
|
+
this.revealChars = 0;
|
|
82
|
+
}
|
|
83
|
+
/** Show the full streamed content immediately (turn finished/cancelled). */
|
|
84
|
+
revealAll() {
|
|
85
|
+
this.revealChars = null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Advance the reveal cursor by a paced, word-aligned tick. Returns true if
|
|
89
|
+
* the display position moved (callers refresh only then).
|
|
90
|
+
*/
|
|
91
|
+
advanceReveal(dtMs = 120) {
|
|
92
|
+
if (this.revealChars == null)
|
|
93
|
+
return false;
|
|
94
|
+
const tail = this.findStreaming();
|
|
95
|
+
if (!tail)
|
|
96
|
+
return false;
|
|
97
|
+
const len = tail.content.length;
|
|
98
|
+
if (this.revealChars >= len)
|
|
99
|
+
return false;
|
|
100
|
+
const lag = len - this.revealChars;
|
|
101
|
+
const cps = lag > REVEAL_LAG_CAP ? REVEAL_CPS * 3 : REVEAL_CPS;
|
|
102
|
+
const budget = Math.max(1, Math.floor((cps * dtMs) / 1000));
|
|
103
|
+
const target = Math.min(len, this.revealChars + budget);
|
|
104
|
+
const nextGap = tail.content.slice(target).search(/\s/);
|
|
105
|
+
const end = nextGap === -1 ? len : target + nextGap + 1;
|
|
106
|
+
if (end <= this.revealChars)
|
|
107
|
+
return false;
|
|
108
|
+
this.revealChars = end;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
slashQuery() {
|
|
112
|
+
const buf = this.input.currentBuffer();
|
|
113
|
+
if (!buf.startsWith('/'))
|
|
114
|
+
return '';
|
|
115
|
+
const token = buf.split(/\s+/).pop() ?? '';
|
|
116
|
+
return token;
|
|
117
|
+
}
|
|
118
|
+
slashMatches() {
|
|
119
|
+
const q = this.slashQuery().toLowerCase();
|
|
120
|
+
if (!q || q === '/')
|
|
121
|
+
return SLASH_COMMANDS;
|
|
122
|
+
return SLASH_COMMANDS.filter((c) => c.cmd.toLowerCase().startsWith(q));
|
|
123
|
+
}
|
|
124
|
+
static slashCommands() {
|
|
125
|
+
return SLASH_COMMANDS;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { PALETTES } from './themes/palettes.js';
|
|
2
|
+
export function rgbToInt(r, g, b) {
|
|
3
|
+
return (r << 16) | (g << 8) | b;
|
|
4
|
+
}
|
|
5
|
+
export function intToRgb(v) {
|
|
6
|
+
return [(v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff];
|
|
7
|
+
}
|
|
8
|
+
export function sgrFor(style) {
|
|
9
|
+
const parts = [];
|
|
10
|
+
if (style.fg != null) {
|
|
11
|
+
const [r, g, b] = intToRgb(style.fg);
|
|
12
|
+
parts.push(`38;2;${r};${g};${b}`);
|
|
13
|
+
}
|
|
14
|
+
if (style.bg != null) {
|
|
15
|
+
const [r, g, b] = intToRgb(style.bg);
|
|
16
|
+
parts.push(`48;2;${r};${g};${b}`);
|
|
17
|
+
}
|
|
18
|
+
if (style.bold)
|
|
19
|
+
parts.push('1');
|
|
20
|
+
if (style.dim)
|
|
21
|
+
parts.push('2');
|
|
22
|
+
if (style.italic)
|
|
23
|
+
parts.push('3');
|
|
24
|
+
if (style.underline)
|
|
25
|
+
parts.push('4');
|
|
26
|
+
if (style.inverse)
|
|
27
|
+
parts.push('7');
|
|
28
|
+
if (style.strikethrough)
|
|
29
|
+
parts.push('9');
|
|
30
|
+
if (parts.length === 0)
|
|
31
|
+
return '';
|
|
32
|
+
return `\x1b[${parts.join(';')}m`;
|
|
33
|
+
}
|
|
34
|
+
export const RESET = '\x1b[0m';
|
|
35
|
+
export class StyleTable {
|
|
36
|
+
ids = new Map();
|
|
37
|
+
sgrCache = new Map();
|
|
38
|
+
sgrToId = new Map();
|
|
39
|
+
styles = [{}];
|
|
40
|
+
counter = 0;
|
|
41
|
+
constructor() {
|
|
42
|
+
this.ids.set('[plain]', 0);
|
|
43
|
+
this.sgrCache.set(0, '');
|
|
44
|
+
this.sgrToId.set('', 0);
|
|
45
|
+
}
|
|
46
|
+
add(style) {
|
|
47
|
+
const key = JSON.stringify(style);
|
|
48
|
+
const existing = this.ids.get(key);
|
|
49
|
+
if (existing != null)
|
|
50
|
+
return existing;
|
|
51
|
+
const id = ++this.counter;
|
|
52
|
+
this.ids.set(key, id);
|
|
53
|
+
this.sgrCache.set(id, sgrFor(style));
|
|
54
|
+
this.styles[id] = style;
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
sgr(id) {
|
|
58
|
+
return this.sgrCache.get(id) ?? '';
|
|
59
|
+
}
|
|
60
|
+
style(id) {
|
|
61
|
+
return this.styles[id];
|
|
62
|
+
}
|
|
63
|
+
/** Map an SGR code body back to a style index (for blitting ANSI text). */
|
|
64
|
+
styleIdForSgr(code) {
|
|
65
|
+
const cached = this.sgrToId.get(code);
|
|
66
|
+
if (cached != null)
|
|
67
|
+
return cached;
|
|
68
|
+
const id = this.add(parseSgr(code));
|
|
69
|
+
this.sgrToId.set(code, id);
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a set of SGR parameter codes into a Style.
|
|
75
|
+
* Input is the code body without the ESC[ or trailing 'm', e.g. "1;38;2;90;162;255".
|
|
76
|
+
*/
|
|
77
|
+
export function parseSgr(code) {
|
|
78
|
+
const style = {};
|
|
79
|
+
const parts = code.split(';').filter((p) => p !== '');
|
|
80
|
+
for (let i = 0; i < parts.length; i++) {
|
|
81
|
+
const p = parts[i];
|
|
82
|
+
const n = parseInt(p, 10);
|
|
83
|
+
if (Number.isNaN(n))
|
|
84
|
+
continue;
|
|
85
|
+
if (n === 0)
|
|
86
|
+
Object.keys(style).forEach((k) => delete style[k]);
|
|
87
|
+
else if (n === 1)
|
|
88
|
+
style.bold = true;
|
|
89
|
+
else if (n === 2)
|
|
90
|
+
style.dim = true;
|
|
91
|
+
else if (n === 3)
|
|
92
|
+
style.italic = true;
|
|
93
|
+
else if (n === 4)
|
|
94
|
+
style.underline = true;
|
|
95
|
+
else if (n === 7)
|
|
96
|
+
style.inverse = true;
|
|
97
|
+
else if (n === 9)
|
|
98
|
+
style.strikethrough = true;
|
|
99
|
+
else if ((n === 38 || n === 48) && parts[i + 1] === '2') {
|
|
100
|
+
const r = parseInt(parts[i + 2] ?? '0', 10);
|
|
101
|
+
const g = parseInt(parts[i + 3] ?? '0', 10);
|
|
102
|
+
const b = parseInt(parts[i + 4] ?? '0', 10);
|
|
103
|
+
const v = rgbToInt(r, g, b);
|
|
104
|
+
style[n === 38 ? 'fg' : 'bg'] = v;
|
|
105
|
+
i += 4;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return style;
|
|
109
|
+
}
|
|
110
|
+
export function buildTheme(name) {
|
|
111
|
+
const palette = PALETTES[name] ?? PALETTES['tokyonight'];
|
|
112
|
+
const table = new StyleTable();
|
|
113
|
+
const s = (style) => table.add(style);
|
|
114
|
+
// No color backgrounds: rows inherit the terminal's own background. The
|
|
115
|
+
// cursor uses reverse-video so it stays visible without a solid color.
|
|
116
|
+
const fx = (style) => style;
|
|
117
|
+
const styles = {
|
|
118
|
+
text: s(fx({ fg: palette.text })),
|
|
119
|
+
muted: s(fx({ fg: palette.muted })),
|
|
120
|
+
dim: s(fx({ fg: palette.dim })),
|
|
121
|
+
accent: s(fx({ fg: palette.accent })),
|
|
122
|
+
accentBright: s(fx({ fg: palette.accent, bold: true })),
|
|
123
|
+
secondary: s(fx({ fg: palette.secondary })),
|
|
124
|
+
success: s(fx({ fg: palette.success })),
|
|
125
|
+
warning: s(fx({ fg: palette.warning })),
|
|
126
|
+
danger: s(fx({ fg: palette.danger })),
|
|
127
|
+
info: s(fx({ fg: palette.info })),
|
|
128
|
+
border: s(fx({ fg: palette.border })),
|
|
129
|
+
borderBright: s(fx({ fg: palette.borderBright })),
|
|
130
|
+
selection: s({ inverse: true }),
|
|
131
|
+
code: s(fx({ fg: palette.codeText })),
|
|
132
|
+
codeBg: s({}),
|
|
133
|
+
heading: s(fx({ fg: palette.accent, bold: true })),
|
|
134
|
+
bold: s({ bold: true }),
|
|
135
|
+
link: s(fx({ fg: palette.info, underline: true })),
|
|
136
|
+
strike: s(fx({ fg: palette.muted, strikethrough: true })),
|
|
137
|
+
quote: s(fx({ fg: palette.muted, italic: true })),
|
|
138
|
+
inlineCode: s({ fg: palette.accent }),
|
|
139
|
+
prompt: s(fx({ fg: palette.prompt, bold: true })),
|
|
140
|
+
userBubble: s({}),
|
|
141
|
+
error: s(fx({ fg: palette.danger })),
|
|
142
|
+
toolPending: s(fx({ fg: palette.muted })),
|
|
143
|
+
toolRunning: s(fx({ fg: palette.info })),
|
|
144
|
+
toolDone: s(fx({ fg: palette.success })),
|
|
145
|
+
toolError: s(fx({ fg: palette.danger })),
|
|
146
|
+
thinking: s(fx({ fg: palette.muted, italic: true })),
|
|
147
|
+
bg: s({}),
|
|
148
|
+
pad: s({}),
|
|
149
|
+
};
|
|
150
|
+
return { name, label: name, palette, table, styles };
|
|
151
|
+
}
|