@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,384 @@
|
|
|
1
|
+
import hljs from 'highlight.js/lib/common';
|
|
2
|
+
import { marked } from 'marked';
|
|
3
|
+
import { wrapText, truncate, padRight, width as visWidth, strip as stripAnsiText } from '../text.js';
|
|
4
|
+
export function renderMarkdown(src, ctx) {
|
|
5
|
+
// Gemini/OpenRouter escape markdown punctuation (`\*`, `\"`, `\`\`\``, `\|`).
|
|
6
|
+
// Leaving those escapes in place defeats block detection (lists, code fences,
|
|
7
|
+
// tables), so undo the common ones before lexing.
|
|
8
|
+
const tokens = marked.lexer(unescapeModelEscapes(src));
|
|
9
|
+
const rctx = {
|
|
10
|
+
theme: ctx.theme,
|
|
11
|
+
width: ctx.width,
|
|
12
|
+
indent: 0,
|
|
13
|
+
headingLevel: 0,
|
|
14
|
+
};
|
|
15
|
+
const lines = [];
|
|
16
|
+
renderTokens(tokens, rctx, lines);
|
|
17
|
+
return lines;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Undo backslash escapes that language models add to markdown punctuation.
|
|
21
|
+
* Examples: `\*` → `*`, `\"` → `"`, `\`\`\`` → ` ``` `, `\|` → `|`, `\_` → `_`.
|
|
22
|
+
* Lines inside fenced code blocks are left untouched so code samples (e.g.
|
|
23
|
+
* `\d`, `\n`) never get mutated; backslash-letter pairs outside fences also
|
|
24
|
+
* stay intact. Only a backslash directly before markdown punctuation is
|
|
25
|
+
* consumed.
|
|
26
|
+
*/
|
|
27
|
+
function unescapeModelEscapes(src) {
|
|
28
|
+
const escaped = /\\+([\\*_`\[\]()#+\-.!><~|"':])/g;
|
|
29
|
+
const fenceCheck = /^(\s*)(```|~~~)/;
|
|
30
|
+
let inFence = false;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const line of src.split('\n')) {
|
|
33
|
+
if (fenceCheck.test(line)) {
|
|
34
|
+
inFence = !inFence;
|
|
35
|
+
out.push(line);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
out.push(inFence || !line.includes('\\') ? line : line.replace(escaped, (_m, ch) => ch));
|
|
39
|
+
}
|
|
40
|
+
return out.join('\n');
|
|
41
|
+
}
|
|
42
|
+
function renderTokens(tokens, ctx, out) {
|
|
43
|
+
for (const tok of tokens) {
|
|
44
|
+
switch (tok.type) {
|
|
45
|
+
case 'heading': {
|
|
46
|
+
const heading = tok;
|
|
47
|
+
const style = ctx.theme.styles['heading'];
|
|
48
|
+
const prefix = ' '.repeat(ctx.indent);
|
|
49
|
+
// Clean `## …` marker instead of the old solid-█ block.
|
|
50
|
+
const marker = `${'#'.repeat(Math.min(6, heading.depth))} `;
|
|
51
|
+
const text = renderInline(heading.tokens, ctx);
|
|
52
|
+
const raw = stripAnsiSimple(text);
|
|
53
|
+
const wrapped = wrapText(`${marker}${raw}`, ctx.width - ctx.indent);
|
|
54
|
+
for (const ln of wrapped) {
|
|
55
|
+
out.push(`${prefix}${ctx.theme.table.sgr(style)}${ln}\x1b[0m`);
|
|
56
|
+
}
|
|
57
|
+
out.push('');
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'paragraph': {
|
|
61
|
+
const para = tok;
|
|
62
|
+
const text = renderInline(para.tokens, ctx);
|
|
63
|
+
const wrapped = wrapText(text, ctx.width - ctx.indent);
|
|
64
|
+
for (const ln of wrapped)
|
|
65
|
+
out.push(`${' '.repeat(ctx.indent)}${ln}`);
|
|
66
|
+
out.push('');
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case 'space':
|
|
70
|
+
break;
|
|
71
|
+
case 'code': {
|
|
72
|
+
const code = tok;
|
|
73
|
+
const blockWidth = ctx.width - ctx.indent;
|
|
74
|
+
const langLabel = code.lang?.trim() ?? '';
|
|
75
|
+
const borderStyle = ctx.theme.styles['border'];
|
|
76
|
+
const borderSgr = ctx.theme.table.sgr(borderStyle);
|
|
77
|
+
const langStyle = ctx.theme.styles['accent'];
|
|
78
|
+
const codeBgStyle = ctx.theme.styles['codeBg'];
|
|
79
|
+
const codeBgSgr = ctx.theme.table.sgr(codeBgStyle);
|
|
80
|
+
const indent = ' '.repeat(ctx.indent);
|
|
81
|
+
// Top fence — centered language label (no stray backticks)
|
|
82
|
+
const label = langLabel ? ` ${langLabel} ` : '';
|
|
83
|
+
const dashes = Math.max(0, blockWidth - 2 - label.length);
|
|
84
|
+
const leftDashes = Math.floor(dashes / 2);
|
|
85
|
+
out.push(`${indent}${borderSgr}╭${'─'.repeat(leftDashes)}${langLabel ? `${ctx.theme.table.sgr(langStyle)}${label}\x1b[0m${borderSgr}` : ''}${'─'.repeat(dashes - leftDashes)}╮\x1b[0m`);
|
|
86
|
+
// Highlight. Known language: hljs. Otherwise plain text — avoids the
|
|
87
|
+
// slow auto-detect (tries every grammar) while streaming on mobile.
|
|
88
|
+
let ansiCode;
|
|
89
|
+
if (langLabel && hljs.getLanguage(langLabel)) {
|
|
90
|
+
const highlighted = hljs.highlight(code.text, { language: langLabel }).value;
|
|
91
|
+
ansiCode = htmlToAnsi(highlighted, ctx.theme);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
ansiCode = code.text;
|
|
95
|
+
}
|
|
96
|
+
for (const ln of ansiCode.split('\n')) {
|
|
97
|
+
const line = truncate(ln, blockWidth - 4, '…');
|
|
98
|
+
out.push(`${indent}${borderSgr}│\x1b[0m${codeBgSgr} ${padRight(line, blockWidth - 4, ' ')} \x1b[0m${borderSgr}│\x1b[0m`);
|
|
99
|
+
}
|
|
100
|
+
// Bottom fence
|
|
101
|
+
out.push(`${indent}${borderSgr}╰${'─'.repeat(blockWidth - 2)}╯\x1b[0m`);
|
|
102
|
+
out.push('');
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'blockquote': {
|
|
106
|
+
const bq = tok;
|
|
107
|
+
const prevIndent = ctx.indent;
|
|
108
|
+
ctx.indent += 2;
|
|
109
|
+
renderTokens(bq.tokens, ctx, out);
|
|
110
|
+
ctx.indent = prevIndent;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'list': {
|
|
114
|
+
const list = tok;
|
|
115
|
+
for (let idx = 0; idx < list.items.length; idx++) {
|
|
116
|
+
const item = list.items[idx];
|
|
117
|
+
let bullet = list.ordered
|
|
118
|
+
? `${ctx.indent + 1 + idx}.`
|
|
119
|
+
: `${idx === 0 ? '▸' : '·'}`;
|
|
120
|
+
let bulletStyle = ctx.theme.styles['muted'];
|
|
121
|
+
let text = renderInline(item.tokens, ctx);
|
|
122
|
+
// Checkbox tasks: drop the raw "[ ] / [x] " prefix and use a glyph,
|
|
123
|
+
// checked → success, unchecked → dim.
|
|
124
|
+
if (item.task) {
|
|
125
|
+
text = text.replace(/^\[\s*?\][\s:]*/i, '').replace(/^\[[xX]\][\s:]*/, '');
|
|
126
|
+
bullet = item.checked ? '☑' : '☐';
|
|
127
|
+
bulletStyle = item.checked ? ctx.theme.styles['success'] : ctx.theme.styles['dim'];
|
|
128
|
+
}
|
|
129
|
+
const prevIndent = ctx.indent;
|
|
130
|
+
ctx.indent += bullet.length + 1;
|
|
131
|
+
const wrapped = wrapText(text, ctx.width - ctx.indent);
|
|
132
|
+
out.push(`${' '.repeat(prevIndent)}${ctx.theme.table.sgr(bulletStyle)}${bullet}\x1b[0m ${wrapped[0] ?? ''}`);
|
|
133
|
+
for (let i = 1; i < wrapped.length; i++)
|
|
134
|
+
out.push(`${' '.repeat(ctx.indent)}${wrapped[i]}`);
|
|
135
|
+
ctx.indent = prevIndent;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'hr':
|
|
140
|
+
out.push(`${' '.repeat(ctx.indent)}${ctx.theme.table.sgr(ctx.theme.styles['line'])}${'─'.repeat(ctx.width - ctx.indent)}\x1b[0m`);
|
|
141
|
+
out.push('');
|
|
142
|
+
break;
|
|
143
|
+
case 'html':
|
|
144
|
+
break;
|
|
145
|
+
case 'table': {
|
|
146
|
+
renderTable(tok, ctx, out);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
// fallback: render as plain text
|
|
151
|
+
out.push(`${' '.repeat(ctx.indent)}${tok.text ?? ''}`);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Table rendering — bordered grid with per-column alignment.
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
function renderTable(table, ctx, out) {
|
|
160
|
+
const header = table.header;
|
|
161
|
+
const rows = table.rows;
|
|
162
|
+
const colCount = header.length;
|
|
163
|
+
const renderCell = (cell) => renderInline(cell.tokens, ctx);
|
|
164
|
+
const allCells = [header.map(renderCell), ...rows.map((r) => r.map(renderCell))];
|
|
165
|
+
// Column widths from visual width of content (strip ANSI when measuring).
|
|
166
|
+
const maxW = Math.max(8, ctx.width - ctx.indent);
|
|
167
|
+
let total = colCount * 3 + 1; // plus the extra border chars
|
|
168
|
+
const colWidths = [];
|
|
169
|
+
for (let c = 0; c < colCount; c++) {
|
|
170
|
+
let w = 1;
|
|
171
|
+
for (const row of allCells) {
|
|
172
|
+
const text = row[c] ?? '';
|
|
173
|
+
const vw = visWidth(stripAnsiText(text));
|
|
174
|
+
if (vw > w)
|
|
175
|
+
w = Math.min(maxW, vw);
|
|
176
|
+
}
|
|
177
|
+
colWidths.push(w);
|
|
178
|
+
total += w;
|
|
179
|
+
}
|
|
180
|
+
// If the table overflows the terminal, clamp columns proportionally.
|
|
181
|
+
if (total > maxW) {
|
|
182
|
+
const avail = maxW - (colCount * 3 + 1);
|
|
183
|
+
const scale = Math.max(0.15, avail / Math.max(1, total - (colCount * 3 + 1)));
|
|
184
|
+
let used = 0;
|
|
185
|
+
for (let c = 0; c < colCount; c++) {
|
|
186
|
+
colWidths[c] = Math.max(1, Math.floor(colWidths[c] * scale));
|
|
187
|
+
used += colWidths[c];
|
|
188
|
+
}
|
|
189
|
+
// distribute leftover width to the widest column
|
|
190
|
+
let slack = avail - used;
|
|
191
|
+
while (slack > 0) {
|
|
192
|
+
let maxIdx = 0;
|
|
193
|
+
for (let c = 1; c < colCount; c++)
|
|
194
|
+
if (colWidths[c] > colWidths[maxIdx])
|
|
195
|
+
maxIdx = c;
|
|
196
|
+
colWidths[maxIdx] = colWidths[maxIdx] + 1;
|
|
197
|
+
slack--;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const border = ctx.theme.table.sgr(ctx.theme.styles['border']);
|
|
201
|
+
const heading = ctx.theme.table.sgr(ctx.theme.styles['heading']);
|
|
202
|
+
const reset = '\x1b[0m';
|
|
203
|
+
const indent = ' '.repeat(ctx.indent);
|
|
204
|
+
const W = (c) => colWidths[c] + 2; // content + 1 space each side
|
|
205
|
+
const horizontal = (left, mid, right) => {
|
|
206
|
+
let s = `${indent}${border}${left}`;
|
|
207
|
+
for (let c = 0; c < colCount; c++)
|
|
208
|
+
s += `${'─'.repeat(W(c))}${c < colCount - 1 ? mid : right}`;
|
|
209
|
+
out.push(`${s}${reset}`);
|
|
210
|
+
};
|
|
211
|
+
const cellLine = (row, isHeader) => {
|
|
212
|
+
const parts = [];
|
|
213
|
+
for (let c = 0; c < colCount; c++) {
|
|
214
|
+
const raw = truncate(row[c] ?? '', colWidths[c], '…');
|
|
215
|
+
const pad = Math.max(0, colWidths[c] - visWidth(stripAnsiText(raw)));
|
|
216
|
+
const align = table.align[c] ?? 'left';
|
|
217
|
+
let content = `${raw}${' '.repeat(pad)}`; // left (default)
|
|
218
|
+
if (align === 'right')
|
|
219
|
+
content = `${' '.repeat(pad)}${raw}`;
|
|
220
|
+
if (align === 'center') {
|
|
221
|
+
const l = Math.floor(pad / 2);
|
|
222
|
+
content = `${' '.repeat(l)}${raw}${' '.repeat(pad - l)}`;
|
|
223
|
+
}
|
|
224
|
+
parts.push(`${isHeader ? heading : ''} ${content} ${reset}`);
|
|
225
|
+
}
|
|
226
|
+
out.push(`${indent}${border}│${reset}${parts.join(`${border}│${reset}`)}${border}│${reset}`);
|
|
227
|
+
};
|
|
228
|
+
if (colCount > 0) {
|
|
229
|
+
horizontal('┌', '┬', '┐');
|
|
230
|
+
cellLine(header.map(renderCell), true);
|
|
231
|
+
horizontal('├', '┼', '┤');
|
|
232
|
+
for (const row of rows)
|
|
233
|
+
cellLine(row.map(renderCell), false);
|
|
234
|
+
horizontal('└', '┴', '┘');
|
|
235
|
+
out.push('');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function renderInline(tokens, ctx) {
|
|
239
|
+
return tokens.map((t) => renderInlineToken(t, ctx)).join('');
|
|
240
|
+
}
|
|
241
|
+
function renderInlineToken(tok, ctx) {
|
|
242
|
+
switch (tok.type) {
|
|
243
|
+
case 'text': {
|
|
244
|
+
const text = tok;
|
|
245
|
+
return text.text;
|
|
246
|
+
}
|
|
247
|
+
case 'codespan': {
|
|
248
|
+
const cs = tok;
|
|
249
|
+
return `${ctx.theme.table.sgr(ctx.theme.styles['inlineCode'])} ${cs.text} \x1b[0m`;
|
|
250
|
+
}
|
|
251
|
+
case 'strong':
|
|
252
|
+
return `${ctx.theme.table.sgr(ctx.theme.styles['bold'])}${renderInline(tok.tokens, ctx)}\x1b[0m`;
|
|
253
|
+
case 'em':
|
|
254
|
+
return `${ctx.theme.table.sgr(ctx.theme.styles['thinking'])}${renderInline(tok.tokens, ctx)}\x1b[0m`;
|
|
255
|
+
case 'del':
|
|
256
|
+
return `${ctx.theme.table.sgr(ctx.theme.styles['strike'])}${renderInline(tok.tokens, ctx)}\x1b[0m`;
|
|
257
|
+
case 'link': {
|
|
258
|
+
const link = tok;
|
|
259
|
+
return `${ctx.theme.table.sgr(ctx.theme.styles['link'])}${link.text}\x1b[0m`;
|
|
260
|
+
}
|
|
261
|
+
case 'image': {
|
|
262
|
+
const img = tok;
|
|
263
|
+
return img.text ? `[${img.text}]` : '[image]';
|
|
264
|
+
}
|
|
265
|
+
case 'br':
|
|
266
|
+
return '\n';
|
|
267
|
+
case 'escape':
|
|
268
|
+
return tok.text;
|
|
269
|
+
default:
|
|
270
|
+
return tok.text ?? '';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// highlight.js HTML → ANSI
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
function htmlToAnsi(html, theme) {
|
|
277
|
+
// Map hljs CSS classes to theme style names.
|
|
278
|
+
const classMap = {
|
|
279
|
+
'hljs-keyword': theme.styles['accent'],
|
|
280
|
+
'hljs-built_in': theme.styles['info'],
|
|
281
|
+
'hljs-type': theme.styles['secondary'],
|
|
282
|
+
'hljs-literal': theme.styles['warning'],
|
|
283
|
+
'hljs-number': theme.styles['secondary'],
|
|
284
|
+
'hljs-string': theme.styles['success'],
|
|
285
|
+
'hljs-template-variable': theme.styles['success'],
|
|
286
|
+
'hljs-params': theme.styles['text'],
|
|
287
|
+
'hljs-function': theme.styles['info'],
|
|
288
|
+
'hljs-title': theme.styles['info'],
|
|
289
|
+
'hljs-title.function_': theme.styles['info'],
|
|
290
|
+
'hljs-title.class_': theme.styles['secondary'],
|
|
291
|
+
'hljs-variable': theme.styles['text'],
|
|
292
|
+
'hljs-comment': theme.styles['muted'],
|
|
293
|
+
'hljs-doctag': theme.styles['muted'],
|
|
294
|
+
'hljs-meta': theme.styles['muted'],
|
|
295
|
+
'hljs-tag': theme.styles['danger'],
|
|
296
|
+
'hljs-name': theme.styles['danger'],
|
|
297
|
+
'hljs-attr': theme.styles['warning'],
|
|
298
|
+
'hljs-selector-id': theme.styles['info'],
|
|
299
|
+
'hljs-selector-class': theme.styles['info'],
|
|
300
|
+
'hljs-regexp': theme.styles['warning'],
|
|
301
|
+
'hljs-symbol': theme.styles['warning'],
|
|
302
|
+
'hljs-property': theme.styles['info'],
|
|
303
|
+
'hljs-operator': theme.styles['muted'],
|
|
304
|
+
'hljs-punctuation': theme.styles['muted'],
|
|
305
|
+
'hljs-meta.hljs-string': theme.styles['success'],
|
|
306
|
+
};
|
|
307
|
+
const RESET = '\x1b[0m';
|
|
308
|
+
let out = '';
|
|
309
|
+
let i = 0;
|
|
310
|
+
while (i < html.length) {
|
|
311
|
+
if (html[i] !== '<') {
|
|
312
|
+
let end = html.indexOf('<', i);
|
|
313
|
+
if (end === -1)
|
|
314
|
+
end = html.length;
|
|
315
|
+
out += decodeEntities(html.slice(i, end));
|
|
316
|
+
i = end;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// Parse opening tag
|
|
320
|
+
const tagStart = i;
|
|
321
|
+
const tagEnd = html.indexOf('>', i);
|
|
322
|
+
if (tagEnd === -1) {
|
|
323
|
+
out += decodeEntities(html.slice(i));
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
const tag = html.slice(i, tagEnd + 1);
|
|
327
|
+
i = tagEnd + 1;
|
|
328
|
+
if (tag.startsWith('<span')) {
|
|
329
|
+
const classMatch = tag.match(/class="([^"]+)"/);
|
|
330
|
+
if (classMatch) {
|
|
331
|
+
const cls = classMatch[1];
|
|
332
|
+
const styleId = classMap[cls];
|
|
333
|
+
if (styleId != null) {
|
|
334
|
+
out += theme.table.sgr(styleId);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (tag === '</span>') {
|
|
339
|
+
out += RESET;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function stripAnsiSimple(s) {
|
|
345
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
346
|
+
}
|
|
347
|
+
/** Decode HTML entities emitted by highlight.js into plain characters. */
|
|
348
|
+
function decodeEntities(src) {
|
|
349
|
+
if (!src.includes('&'))
|
|
350
|
+
return src;
|
|
351
|
+
const named = {
|
|
352
|
+
amp: '&',
|
|
353
|
+
lt: '<',
|
|
354
|
+
gt: '>',
|
|
355
|
+
quot: '"',
|
|
356
|
+
apos: "'",
|
|
357
|
+
nbsp: ' ',
|
|
358
|
+
bull: '•',
|
|
359
|
+
hellip: '…',
|
|
360
|
+
mdash: '—',
|
|
361
|
+
ndash: '–',
|
|
362
|
+
lsquo: '‘',
|
|
363
|
+
rsquo: '’',
|
|
364
|
+
ldquo: '“',
|
|
365
|
+
rdquo: '”',
|
|
366
|
+
middot: '·',
|
|
367
|
+
};
|
|
368
|
+
return src.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, body) => {
|
|
369
|
+
if (body[0] === '#') {
|
|
370
|
+
const hex = body[1] === 'x' || body[1] === 'X';
|
|
371
|
+
const n = parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);
|
|
372
|
+
if (!Number.isNaN(n) && n >= 0 && n <= 0x10ffff) {
|
|
373
|
+
try {
|
|
374
|
+
return String.fromCodePoint(n);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return m;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return m;
|
|
381
|
+
}
|
|
382
|
+
return named[body] ?? m;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { wrapText, truncate, padRight, width as sw } from '../text.js';
|
|
2
|
+
import { renderMarkdown } from './markdown.js';
|
|
3
|
+
export function messageToLines(msg, theme, width, opts = {}) {
|
|
4
|
+
if (msg.role === 'user')
|
|
5
|
+
return renderUser(msg, theme, width, opts);
|
|
6
|
+
if (msg.role === 'assistant')
|
|
7
|
+
return renderAssistant(msg, theme, width, opts);
|
|
8
|
+
if (msg.role === 'system')
|
|
9
|
+
return renderSystem(msg, theme, width, opts);
|
|
10
|
+
return renderUser({ ...msg, content: `[${msg.role}] ${msg.content}` }, theme, width, opts);
|
|
11
|
+
}
|
|
12
|
+
function renderUser(msg, theme, width, _opts) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
const accentSgr = theme.table.sgr(theme.styles['accent']);
|
|
15
|
+
const dimSgr = theme.table.sgr(theme.styles['dim']);
|
|
16
|
+
const reset = '\x1b[0m';
|
|
17
|
+
lines.push(`${accentSgr}❯ you${reset}`);
|
|
18
|
+
const body = wrapText(msg.content, Math.max(8, width - 4));
|
|
19
|
+
for (const ln of body) {
|
|
20
|
+
lines.push(`${dimSgr} │${reset} ${ln}`);
|
|
21
|
+
}
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
function renderAssistant(msg, theme, width, opts) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const reset = '\x1b[0m';
|
|
27
|
+
const accentSgr = theme.table.sgr(theme.styles['accent']);
|
|
28
|
+
const mutedSgr = theme.table.sgr(theme.styles['muted']);
|
|
29
|
+
const dimSgr = theme.table.sgr(theme.styles['dim']);
|
|
30
|
+
const modelLabel = msg.model ? ` · ${msg.model}` : '';
|
|
31
|
+
lines.push(`${accentSgr}◈ orbit${reset}${mutedSgr}${modelLabel}${reset}`);
|
|
32
|
+
// Thinking block — collapsed by default unless enabled in config.
|
|
33
|
+
if (msg.reasoning && msg.reasoning.length > 0) {
|
|
34
|
+
const open = opts.thinkingDefaultOpen ?? msg.reasoningOpen ?? false;
|
|
35
|
+
const tokenCount = tokenEstimate(msg.reasoning);
|
|
36
|
+
const indicator = open ? '▾' : '▸';
|
|
37
|
+
lines.push(`${mutedSgr}${indicator} thinking · ~${tokenCount} tokens (press t)${reset}`);
|
|
38
|
+
if (open) {
|
|
39
|
+
const wrapped = wrapText(msg.reasoning, Math.max(8, width - 6));
|
|
40
|
+
for (const ln of wrapped) {
|
|
41
|
+
lines.push(`${mutedSgr} ${ln}${reset}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Content
|
|
46
|
+
if (msg.content) {
|
|
47
|
+
const md = renderMarkdown(msg.content, { theme, width: Math.max(8, width - 4) });
|
|
48
|
+
for (const ln of md)
|
|
49
|
+
lines.push(`${ln}`);
|
|
50
|
+
}
|
|
51
|
+
// Tool calls
|
|
52
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
53
|
+
for (const tc of msg.toolCalls) {
|
|
54
|
+
lines.push(...toolCardToLines(tc, theme, width));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Streaming indicator
|
|
58
|
+
if (msg.streaming) {
|
|
59
|
+
lines.push(`${dimSgr}…${reset}`);
|
|
60
|
+
}
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
function toolCardToLines(tc, theme, width) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
const reset = '\x1b[0m';
|
|
66
|
+
const statusIcon = tc.status === 'done'
|
|
67
|
+
? `${theme.table.sgr(theme.styles['toolDone'])}✓`
|
|
68
|
+
: tc.status === 'error'
|
|
69
|
+
? `${theme.table.sgr(theme.styles['toolError'])}✗`
|
|
70
|
+
: tc.status === 'running'
|
|
71
|
+
? `${theme.table.sgr(theme.styles['toolRunning'])}⠋`
|
|
72
|
+
: `${theme.table.sgr(theme.styles['toolPending'])}○`;
|
|
73
|
+
const durationStr = tc.durationMs != null ? `${tc.durationMs}ms` : '';
|
|
74
|
+
const name = truncate(tc.name, Math.max(4, width - 14), '…');
|
|
75
|
+
const innerW = Math.max(2, width - 4);
|
|
76
|
+
const topLeft = `${statusIcon} ${name}${reset}`;
|
|
77
|
+
const topRight = `${theme.table.sgr(theme.styles['muted'])}${durationStr}${reset}`;
|
|
78
|
+
const borderSgr = theme.table.sgr(theme.styles['border']);
|
|
79
|
+
const topText = `┌ ${topLeft}${' '.repeat(Math.max(0, innerW - sw(topLeft) - sw(topRight) - 2))}${topRight}`;
|
|
80
|
+
lines.push(`${borderSgr}${topText} ┐${reset}`);
|
|
81
|
+
// Args preview
|
|
82
|
+
const args = truncate(tc.args, Math.max(4, innerW - 2), '…');
|
|
83
|
+
lines.push(`${borderSgr}│${reset} ${theme.table.sgr(theme.styles['muted'])}${args}${reset}`);
|
|
84
|
+
// Result preview
|
|
85
|
+
if (tc.result) {
|
|
86
|
+
const preview = truncate(tc.result, Math.max(4, innerW - 2), '…');
|
|
87
|
+
lines.push(`${borderSgr}│${reset} ${preview}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push(`${borderSgr}└${padRight('', innerW, '─')}┘${reset}`);
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
function renderSystem(msg, theme, width, _opts) {
|
|
93
|
+
const mutedSgr = theme.table.sgr(theme.styles['info']);
|
|
94
|
+
const reset = '\x1b[0m';
|
|
95
|
+
const wrapped = wrapText(msg.content, Math.max(8, width - 6));
|
|
96
|
+
const out = [`${mutedSgr}ℹ ${wrapped[0] ?? ''}${reset}`];
|
|
97
|
+
for (let i = 1; i < wrapped.length; i++)
|
|
98
|
+
out.push(`${mutedSgr} ${wrapped[i]}${reset}`);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function tokenEstimate(text) {
|
|
102
|
+
const n = Math.ceil(text.length / 4);
|
|
103
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`;
|
|
104
|
+
}
|
|
105
|
+
export { truncate, wrapText };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const StoreContext = createContext(null);
|
|
3
|
+
export function useStore() {
|
|
4
|
+
const store = useContext(StoreContext);
|
|
5
|
+
if (!store)
|
|
6
|
+
throw new Error('Orbit Ink components must be rendered inside the store context');
|
|
7
|
+
return store;
|
|
8
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { wrapText } from './text.js';
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic layout numbers for the fixed-height chrome around the
|
|
4
|
+
* transcript. Terminal row budget = sum of these; the transcript gets the rest.
|
|
5
|
+
*/
|
|
6
|
+
export const HEADER_H = 4; // boxed header: border(1+1) + 2 content rows
|
|
7
|
+
export const TIP_H = 1;
|
|
8
|
+
export const SEP_H = 1;
|
|
9
|
+
export const STATUS_H = 1;
|
|
10
|
+
export const WORKING_H = 1;
|
|
11
|
+
/** In-flow permission panel height (border 2 + 3 content rows). */
|
|
12
|
+
export const ASK_H = 5;
|
|
13
|
+
/** Visual line count of the composer input (soft wraps included). */
|
|
14
|
+
export function composerVisualLines(input, width) {
|
|
15
|
+
const contentW = Math.max(1, width - 2);
|
|
16
|
+
let n = 0;
|
|
17
|
+
for (const line of input.lines) {
|
|
18
|
+
if (line === '')
|
|
19
|
+
n += 1;
|
|
20
|
+
else
|
|
21
|
+
n += wrapText(line, contentW).length;
|
|
22
|
+
}
|
|
23
|
+
return Math.max(1, n);
|
|
24
|
+
}
|
|
25
|
+
/** Rendered composer box height, clamped to [1, 4] rows. */
|
|
26
|
+
export function composerHeight(input, width) {
|
|
27
|
+
return Math.min(4, Math.max(1, composerVisualLines(input, width)));
|
|
28
|
+
}
|
|
29
|
+
export function chromeHeight(args) {
|
|
30
|
+
return (HEADER_H +
|
|
31
|
+
TIP_H +
|
|
32
|
+
(args.working ? WORKING_H : 0) +
|
|
33
|
+
(args.ask ? ASK_H : 0) +
|
|
34
|
+
args.menuLines +
|
|
35
|
+
args.modelLines +
|
|
36
|
+
args.dockLines +
|
|
37
|
+
SEP_H +
|
|
38
|
+
args.composerH +
|
|
39
|
+
STATUS_H);
|
|
40
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-oriented render surface.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old per-cell Screen/blit pipeline with a much simpler model:
|
|
5
|
+
* every frame, components produce one fully-styled string per terminal row,
|
|
6
|
+
* each row carries the theme background baked into its own SGR spans, and the
|
|
7
|
+
* flusher only repaints rows whose text actually changed. This keeps streaming
|
|
8
|
+
* updates cheap and — critically — never leaves half-cleared rows visible, so
|
|
9
|
+
* there is no more duplicated/overlapping text or terminal-default backgrounds.
|
|
10
|
+
*/
|
|
11
|
+
import stringWidth from 'string-width';
|
|
12
|
+
import stripAnsi from 'strip-ansi';
|
|
13
|
+
import { RESET } from './style.js';
|
|
14
|
+
import { write } from './term.js';
|
|
15
|
+
export class VTerm {
|
|
16
|
+
w;
|
|
17
|
+
h;
|
|
18
|
+
rows;
|
|
19
|
+
prev;
|
|
20
|
+
dirtyFlags;
|
|
21
|
+
constructor(w, h) {
|
|
22
|
+
this.w = w;
|
|
23
|
+
this.h = h;
|
|
24
|
+
this.rows = new Array(h).fill('');
|
|
25
|
+
this.prev = new Array(h).fill('\u0000');
|
|
26
|
+
this.dirtyFlags = new Array(h).fill(false);
|
|
27
|
+
}
|
|
28
|
+
resize(w, h) {
|
|
29
|
+
if (w === this.w && h === this.h)
|
|
30
|
+
return;
|
|
31
|
+
this.w = w;
|
|
32
|
+
this.h = h;
|
|
33
|
+
this.rows = new Array(h).fill('');
|
|
34
|
+
// Force a full repaint after resize.
|
|
35
|
+
this.prev = new Array(h).fill('\u0000');
|
|
36
|
+
this.dirtyFlags = new Array(h).fill(true);
|
|
37
|
+
}
|
|
38
|
+
/** Clear every row to empty (next flush repaints them). */
|
|
39
|
+
reset() {
|
|
40
|
+
this.rows = new Array(this.h).fill('');
|
|
41
|
+
for (let y = 0; y < this.h; y++)
|
|
42
|
+
this.dirtyFlags[y] = true;
|
|
43
|
+
}
|
|
44
|
+
setRow(y, line) {
|
|
45
|
+
if (y < 0 || y >= this.h)
|
|
46
|
+
return;
|
|
47
|
+
if (this.rows[y] === line)
|
|
48
|
+
return;
|
|
49
|
+
this.rows[y] = line;
|
|
50
|
+
this.dirtyFlags[y] = true;
|
|
51
|
+
}
|
|
52
|
+
/** Paint a region clear using the theme background, marking changed rows. */
|
|
53
|
+
fillClear(theme) {
|
|
54
|
+
const bg = theme.table.sgr(theme.styles['bg']);
|
|
55
|
+
const pad = theme.table.sgr(theme.styles['pad']);
|
|
56
|
+
const blank = `${bg}${pad}${' '.repeat(this.w)}${RESET}`;
|
|
57
|
+
for (let y = 0; y < this.h; y++)
|
|
58
|
+
this.setRow(y, blank);
|
|
59
|
+
}
|
|
60
|
+
flushLines() {
|
|
61
|
+
let out = '';
|
|
62
|
+
for (let y = 0; y < this.h; y++) {
|
|
63
|
+
if (!this.dirtyFlags[y])
|
|
64
|
+
continue;
|
|
65
|
+
const row = this.rows[y] ?? '';
|
|
66
|
+
// Length-guard: if a row overflows the terminal width it would wrap and
|
|
67
|
+
// desync subsequent rows, so clip to the field width (keeps SGRs intact
|
|
68
|
+
// because composeRow appends padding only when the row fits).
|
|
69
|
+
out += `\x1b[${y + 1};1H${row}`;
|
|
70
|
+
this.prev[y] = row;
|
|
71
|
+
this.dirtyFlags[y] = false;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Flush dirty rows to the terminal. Returns false when nothing changed. */
|
|
76
|
+
flush() {
|
|
77
|
+
if (!this.dirtyFlags.some(Boolean))
|
|
78
|
+
return false;
|
|
79
|
+
const out = this.flushLines();
|
|
80
|
+
if (out.length > 0)
|
|
81
|
+
write(out);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Assemble one terminal row: paint the theme background behind the entire
|
|
87
|
+
* content, re-assert it after every embedded SGR reset, then pad to `width`
|
|
88
|
+
* with the background so trailing cells are always the theme color.
|
|
89
|
+
*/
|
|
90
|
+
export function composeRow(theme, width, content) {
|
|
91
|
+
const bg = theme.table.sgr(theme.styles['bg']);
|
|
92
|
+
const pad = theme.table.sgr(theme.styles['pad']);
|
|
93
|
+
const contentW = stringWidth(stripAnsi(content));
|
|
94
|
+
// Re-apply the background after every reset so unstyled gaps never show the
|
|
95
|
+
// terminal default background mid-row.
|
|
96
|
+
const safe = `${bg}${content.replace(/\x1b\[0m/g, `${RESET}${bg}`)}`;
|
|
97
|
+
const padLen = Math.max(0, width - contentW);
|
|
98
|
+
if (padLen === 0)
|
|
99
|
+
return `${safe}${RESET}`;
|
|
100
|
+
return `${safe}${pad}${' '.repeat(padLen)}${RESET}`;
|
|
101
|
+
}
|
|
102
|
+
/** A fully background-filled, empty row (used at startup / resize). */
|
|
103
|
+
export function blankRow(theme, width) {
|
|
104
|
+
return composeRow(theme, width, '');
|
|
105
|
+
}
|
|
106
|
+
/** Center `content` horizontally on a full-width row. */
|
|
107
|
+
export function centerRow(theme, width, content) {
|
|
108
|
+
const w = stringWidth(stripAnsi(content));
|
|
109
|
+
const side = Math.max(0, Math.floor((width - w) / 2));
|
|
110
|
+
return composeRow(theme, width, `${' '.repeat(side)}${content}`);
|
|
111
|
+
}
|
|
112
|
+
/** A subtle horizontal rule in the border color. */
|
|
113
|
+
export function ruleRow(theme, width, char = '─') {
|
|
114
|
+
const border = theme.table.sgr(theme.styles['border']);
|
|
115
|
+
return composeRow(theme, width, `${border}${char.repeat(width)}\x1b[0m`);
|
|
116
|
+
}
|