jaitus 0.3.14 → 0.3.16
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/package.json +2 -2
- package/src/agent/loop.js +40 -4
- package/src/api.js +177 -174
- package/src/commands/update.js +8 -3
- package/src/compact.js +224 -0
- package/src/errors.js +2 -0
- package/src/index.js +220 -119
- package/src/machine.js +192 -0
- package/src/tui/ansi.js +65 -1
- package/src/tui/entries.js +15 -9
- package/src/tui/markdown.js +40 -15
- package/src/tui-ink.js +2922 -2841
package/src/tui/ansi.js
CHANGED
|
@@ -188,6 +188,12 @@ export function truncateRow(row, width, ellipsis = '…') {
|
|
|
188
188
|
* bullet or a wrapped tool row stay visually attached to its marker.
|
|
189
189
|
*/
|
|
190
190
|
export function wrapRow(row, width, indent = '') {
|
|
191
|
+
// A newline inside a span is a line break, never a character to print: Ink would draw
|
|
192
|
+
// it as an extra screen line the layout never counted, and every row below it — and
|
|
193
|
+
// the selection — would be off by one. Pasted multi-line text arrives exactly so.
|
|
194
|
+
if (row.some((span) => typeof span.t === 'string' && span.t.includes('\n'))) {
|
|
195
|
+
return splitAtNewlines(row).flatMap((line) => wrapRow(line, width, indent));
|
|
196
|
+
}
|
|
191
197
|
if (width < 4) return [row];
|
|
192
198
|
if (rowWidth(row) <= width) return [row];
|
|
193
199
|
|
|
@@ -200,10 +206,15 @@ export function wrapRow(row, width, indent = '') {
|
|
|
200
206
|
// Every line after the first opens with the hanging indent, so a wrapped bullet or a
|
|
201
207
|
// wrapped tool row stays visually attached to its marker instead of resetting to
|
|
202
208
|
// column zero and reading as a new item.
|
|
209
|
+
// A continuation line is the same line of text, so copying rejoins it (SOFT): the
|
|
210
|
+
// marker carries what the break swallowed — a space between words, nothing in a word
|
|
211
|
+
// cut mid-way. The hanging indent is layout, so it is never copied.
|
|
212
|
+
let breakJoin = ' ';
|
|
203
213
|
const flush = () => {
|
|
204
214
|
lines.push(current.length ? current : [{ t: '' }]);
|
|
205
|
-
current = indent ? [...indentSpan] : [];
|
|
215
|
+
current = [{ t: '', soft: breakJoin }, ...(indent ? [{ ...indentSpan[0], copy: '' }] : [])];
|
|
206
216
|
used = indent ? indentWidth : 0;
|
|
217
|
+
breakJoin = ' ';
|
|
207
218
|
};
|
|
208
219
|
|
|
209
220
|
for (const span of row) {
|
|
@@ -228,6 +239,7 @@ export function wrapRow(row, width, indent = '') {
|
|
|
228
239
|
// `cut` marks a word split mid-way, so a URL cut here can be rejoined when it is
|
|
229
240
|
// clicked (the renderer ignores the field).
|
|
230
241
|
current.push(piece ? { ...span, t: piece, cut: true } : { ...span, t: piece });
|
|
242
|
+
breakJoin = '';
|
|
231
243
|
flush();
|
|
232
244
|
piece = '';
|
|
233
245
|
pieceWidth = 0;
|
|
@@ -255,6 +267,58 @@ export function wrapRows(rows, width, indent = '') {
|
|
|
255
267
|
/** The plain text of a row of spans, for selection, search and the clipboard. */
|
|
256
268
|
export const rowText = (row) => (row || []).map((span) => span.t).join('');
|
|
257
269
|
|
|
270
|
+
/** One row with newlines in its spans → one row per line, styles kept. */
|
|
271
|
+
export function splitAtNewlines(row) {
|
|
272
|
+
const lines = [[]];
|
|
273
|
+
for (const span of row) {
|
|
274
|
+
const parts = String(span.t ?? '').split('\n');
|
|
275
|
+
parts.forEach((part, i) => {
|
|
276
|
+
if (i > 0) lines.push([]);
|
|
277
|
+
if (part) lines[lines.length - 1].push({ ...span, t: part });
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return lines.map((line) => (line.length ? line : [{ t: '' }]));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Copying what is on screen ──────────────────────────────────────────────────
|
|
284
|
+
//
|
|
285
|
+
// A row on screen is source text plus decoration: the bar beside a message, a code
|
|
286
|
+
// block's gutter and frame, a quote mark, a table's column rules, a bullet glyph, the
|
|
287
|
+
// indent under a wrapped line. A span says what it contributes to a COPY with `copy`
|
|
288
|
+
// (absent = its own text; '' = nothing; '- ' for a bullet), and a wrapped line says how
|
|
289
|
+
// it rejoins the line above with a `soft` marker. Copying a command out of an answer
|
|
290
|
+
// used to bring the gutter's ▐ (| in plain terminals) and the bullet with it.
|
|
291
|
+
|
|
292
|
+
/** True for a row that is decoration only (a code block's frame, a rule). */
|
|
293
|
+
export const isDecorationRow = (row) => (row || []).length > 0
|
|
294
|
+
&& row.every((span) => span.copy === '' || !span.t)
|
|
295
|
+
&& row.some((span) => span.t && span.copy === '');
|
|
296
|
+
|
|
297
|
+
/** How a row joins the row above when both are copied: null = a new line. */
|
|
298
|
+
export const softJoin = (row) => {
|
|
299
|
+
const marker = (row || []).find((span) => span.soft !== undefined);
|
|
300
|
+
return marker ? marker.soft : null;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** The copy text of the cells [from, to) of a row (string offsets, as selection uses). */
|
|
304
|
+
export function copyText(row, from = 0, to = Infinity) {
|
|
305
|
+
let at = 0;
|
|
306
|
+
let out = '';
|
|
307
|
+
for (const span of row || []) {
|
|
308
|
+
const t = String(span.t ?? '');
|
|
309
|
+
const start = at;
|
|
310
|
+
const end = at + t.length;
|
|
311
|
+
at = end;
|
|
312
|
+
if (end <= from || start >= to) {
|
|
313
|
+
// A zero-width marker inside the range still counts (it is how bullets survive).
|
|
314
|
+
if (!(t.length === 0 && start >= from && start < to)) continue;
|
|
315
|
+
}
|
|
316
|
+
if (span.copy !== undefined) { out += span.copy; continue; }
|
|
317
|
+
out += t.slice(Math.max(0, from - start), Math.min(t.length, to - start));
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
258
322
|
/**
|
|
259
323
|
* Invert the cells between two columns of a row, splitting spans where the range falls
|
|
260
324
|
* inside one. Used to show a drag selection: the terminal's own highlight is gone once
|
package/src/tui/entries.js
CHANGED
|
@@ -137,12 +137,18 @@ const BUILDERS = {
|
|
|
137
137
|
// ── What the user said ───────────────────────────────────────────────────────
|
|
138
138
|
user(data, width) {
|
|
139
139
|
const p = palette();
|
|
140
|
-
|
|
140
|
+
// One mark beside the message, on its first row — not a bar on every row: a pasted
|
|
141
|
+
// command or log used to arrive striped with ▌ (| in plain terminals) down its whole
|
|
142
|
+
// length. Later rows sit under the text, indented. Neither is ever copied.
|
|
143
|
+
const bar = { t: pick(['▌', '|']), color: p.brand, copy: '' };
|
|
144
|
+
const gap = { t: ' ', copy: '' };
|
|
145
|
+
const indent = { t: ' ', copy: '' };
|
|
141
146
|
const inner = Math.max(12, width - 2);
|
|
142
|
-
const
|
|
143
|
-
const
|
|
147
|
+
const text = String(data.text || '').replace(/\r\n?/g, '\n').replace(/\t/g, ' ');
|
|
148
|
+
const body = wrapRow([{ t: text, color: p.body, bold: true }], inner);
|
|
149
|
+
const rows = body.map((row, index) => (index === 0 ? [bar, gap, ...row] : [indent, ...row]));
|
|
144
150
|
if (data.attachments?.length) {
|
|
145
|
-
rows.push([
|
|
151
|
+
rows.push([indent, { t: data.attachments.map((a) => `⧉ ${a}`).join(' '), color: p.purple }]);
|
|
146
152
|
}
|
|
147
153
|
return rows;
|
|
148
154
|
},
|
|
@@ -156,8 +162,8 @@ const BUILDERS = {
|
|
|
156
162
|
// A lead mark on the first row only: it identifies the speaker without spending a
|
|
157
163
|
// whole line on a name label, and the indent keeps the column under it aligned.
|
|
158
164
|
return body.map((row, index) => (index === 0
|
|
159
|
-
? [{ t: pick(['● ', '* ']), color: p.teal }, ...row]
|
|
160
|
-
: [{ t: ' ' }, ...row]));
|
|
165
|
+
? [{ t: pick(['● ', '* ']), color: p.teal, copy: '' }, ...row]
|
|
166
|
+
: [{ t: ' ', copy: '' }, ...row]));
|
|
161
167
|
},
|
|
162
168
|
|
|
163
169
|
// ── A tool call ──────────────────────────────────────────────────────────────
|
|
@@ -197,11 +203,11 @@ const BUILDERS = {
|
|
|
197
203
|
const tinted = highlightLine(line.replace(/\t/g, ' '), language, state);
|
|
198
204
|
state = tinted.state;
|
|
199
205
|
const spans = tinted.spans.length ? tinted.spans : [{ t: '' }];
|
|
200
|
-
rows.push([{ t: ` ${pick(['│', '|'])} `, color: p.lineSoft }, ...truncateRow(spans, Math.max(8, width - 6))]);
|
|
206
|
+
rows.push([{ t: ` ${pick(['│', '|'])} `, color: p.lineSoft, copy: '' }, ...truncateRow(spans, Math.max(8, width - 6))]);
|
|
201
207
|
}
|
|
202
208
|
if (preview.more > 0) {
|
|
203
209
|
const hint = openable && !data.expanded ? ' — click the row to show all' : '';
|
|
204
|
-
rows.push([{ t: ` ${pick(['│', '|'])} `, color: p.lineSoft }, { t: `+${preview.more} more line${preview.more === 1 ? '' : 's'}${hint}`, color: p.faint, italic: true }]);
|
|
210
|
+
rows.push([{ t: ` ${pick(['│', '|'])} `, color: p.lineSoft, copy: '' }, { t: `+${preview.more} more line${preview.more === 1 ? '' : 's'}${hint}`, color: p.faint, italic: true }]);
|
|
205
211
|
}
|
|
206
212
|
}
|
|
207
213
|
return rows;
|
|
@@ -262,7 +268,7 @@ const BUILDERS = {
|
|
|
262
268
|
error(data, width) {
|
|
263
269
|
const p = palette();
|
|
264
270
|
const rows = [];
|
|
265
|
-
const bar = { t: pick(['▌', '|']), color: p.red };
|
|
271
|
+
const bar = { t: pick(['▌', '|']), color: p.red, copy: '' };
|
|
266
272
|
const inner = Math.max(12, width - 2);
|
|
267
273
|
|
|
268
274
|
rows.push([bar, { t: ' ' }, { t: `${pick(['✗', 'x'])} ${data.title || 'Error'}`, color: p.red, bold: true }]);
|
package/src/tui/markdown.js
CHANGED
|
@@ -122,14 +122,14 @@ export function renderMarkdown(text, width, { compact = false } = {}) {
|
|
|
122
122
|
for (const row of wrapRow(content, w)) rows.push(row);
|
|
123
123
|
if (level <= 2) {
|
|
124
124
|
const ruleWidth = Math.min(w, Math.max(8, contentWidth(content)));
|
|
125
|
-
rows.push([{ t: (level === 1 ? g('━', '=') : g('─', '-')).repeat(ruleWidth), color: level === 1 ? p.brandDeep : p.line }]);
|
|
125
|
+
rows.push([{ t: (level === 1 ? g('━', '=') : g('─', '-')).repeat(ruleWidth), color: level === 1 ? p.brandDeep : p.line, copy: '' }]);
|
|
126
126
|
}
|
|
127
127
|
i += 1;
|
|
128
128
|
continue;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// ── horizontal rule ────────────────────────────────────────────────────────
|
|
132
|
-
if (RULE.test(line)) { rows.push([{ t: g('─', '-').repeat(w), color: p.line }]); i += 1; continue; }
|
|
132
|
+
if (RULE.test(line)) { rows.push([{ t: g('─', '-').repeat(w), color: p.line, copy: '' }]); i += 1; continue; }
|
|
133
133
|
|
|
134
134
|
// ── blockquote ─────────────────────────────────────────────────────────────
|
|
135
135
|
const quote = QUOTE.exec(line);
|
|
@@ -138,7 +138,7 @@ export function renderMarkdown(text, width, { compact = false } = {}) {
|
|
|
138
138
|
while (i < lines.length && QUOTE.test(lines[i])) { body.push(QUOTE.exec(lines[i])[1]); i += 1; }
|
|
139
139
|
if (!compact) blank();
|
|
140
140
|
for (const inner of renderMarkdown(body.join('\n'), w - 2, { compact: true })) {
|
|
141
|
-
rows.push([{ t: g('▏ ', '| '), color: p.teal }, ...inner]);
|
|
141
|
+
rows.push([{ t: g('▏ ', '| '), color: p.teal, copy: '> ' }, ...inner]);
|
|
142
142
|
}
|
|
143
143
|
if (!compact) rows.push([]);
|
|
144
144
|
continue;
|
|
@@ -159,10 +159,10 @@ export function renderMarkdown(text, width, { compact = false } = {}) {
|
|
|
159
159
|
const done = !!task && /[xX]/.test(task[1]);
|
|
160
160
|
let marker;
|
|
161
161
|
if (task) {
|
|
162
|
-
marker = { t: done ? g('✔ ', '[x] ') : g('○ ', '[ ] '), color: done ? p.green : p.dim };
|
|
162
|
+
marker = { t: done ? g('✔ ', '[x] ') : g('○ ', '[ ] '), color: done ? p.green : p.dim, copy: done ? '- [x] ' : '- [ ] ' };
|
|
163
163
|
content = task[2];
|
|
164
164
|
} else if (b) {
|
|
165
|
-
marker = { t: `${(ascii() ? ['-', '*', '+'] : ['•', '◦', '▪'])[depth % 3]} `, color: p.brand };
|
|
165
|
+
marker = { t: `${(ascii() ? ['-', '*', '+'] : ['•', '◦', '▪'])[depth % 3]} `, color: p.brand, copy: '- ' };
|
|
166
166
|
} else {
|
|
167
167
|
marker = { t: `${o[2]}. `, color: p.brand };
|
|
168
168
|
}
|
|
@@ -206,6 +206,28 @@ export function renderMarkdown(text, width, { compact = false } = {}) {
|
|
|
206
206
|
|
|
207
207
|
const contentWidth = (spans) => spans.reduce((sum, s) => sum + stringWidth(s.t), 0);
|
|
208
208
|
|
|
209
|
+
/** Spans cut into rows of at most `width` cells, character by character, styles kept. */
|
|
210
|
+
function hardWrap(spans, width) {
|
|
211
|
+
const rows = [[]];
|
|
212
|
+
let used = 0;
|
|
213
|
+
for (const span of spans) {
|
|
214
|
+
let piece = '';
|
|
215
|
+
for (const ch of String(span.t ?? '')) {
|
|
216
|
+
const cw = stringWidth(ch);
|
|
217
|
+
if (used + cw > width && used > 0) {
|
|
218
|
+
if (piece) rows[rows.length - 1].push({ ...span, t: piece });
|
|
219
|
+
rows.push([]);
|
|
220
|
+
piece = '';
|
|
221
|
+
used = 0;
|
|
222
|
+
}
|
|
223
|
+
piece += ch;
|
|
224
|
+
used += cw;
|
|
225
|
+
}
|
|
226
|
+
if (piece) rows[rows.length - 1].push({ ...span, t: piece });
|
|
227
|
+
}
|
|
228
|
+
return rows.map((r) => (r.length ? r : [{ t: '' }]));
|
|
229
|
+
}
|
|
230
|
+
|
|
209
231
|
/**
|
|
210
232
|
* A fenced block: a language tag, a tinted body, and a left gutter that makes the code
|
|
211
233
|
* a distinct object on the page without the cost of a full border (which would eat two
|
|
@@ -214,14 +236,14 @@ const contentWidth = (spans) => spans.reduce((sum, s) => sum + stringWidth(s.t),
|
|
|
214
236
|
function codeBlock(body, language, width) {
|
|
215
237
|
const p = palette();
|
|
216
238
|
const rows = [];
|
|
217
|
-
const gutter = { t: g('▐ ', '| '), color: p.tealDeep };
|
|
239
|
+
const gutter = { t: g('▐ ', '| '), color: p.tealDeep, copy: '' };
|
|
218
240
|
const label = language && isKnownLanguage(language) ? language : language || '';
|
|
219
241
|
const inner = Math.max(8, width - 2);
|
|
220
242
|
|
|
221
243
|
rows.push([
|
|
222
|
-
{ t: g('▛', '+'), color: p.tealDeep },
|
|
223
|
-
{ t: label ? `${g('─', '-')} ${label} ` : g('─', '-'), color: p.dim },
|
|
224
|
-
{ t: g('─', '-').repeat(Math.max(0, width - 2 - (label ? label.length + 3 : 1))), color: p.lineSoft },
|
|
244
|
+
{ t: g('▛', '+'), color: p.tealDeep, copy: '' },
|
|
245
|
+
{ t: label ? `${g('─', '-')} ${label} ` : g('─', '-'), color: p.dim, copy: '' },
|
|
246
|
+
{ t: g('─', '-').repeat(Math.max(0, width - 2 - (label ? label.length + 3 : 1))), color: p.lineSoft, copy: '' },
|
|
225
247
|
]);
|
|
226
248
|
|
|
227
249
|
let state = {};
|
|
@@ -231,11 +253,13 @@ function codeBlock(body, language, width) {
|
|
|
231
253
|
state = result.state;
|
|
232
254
|
const spans = result.spans.length ? result.spans : [{ t: '' }];
|
|
233
255
|
// Code is not prose: wrapping it on spaces would move a token onto a line where it
|
|
234
|
-
// does not belong
|
|
235
|
-
//
|
|
236
|
-
|
|
256
|
+
// does not belong — so a long line is cut at the edge, character by character, and
|
|
257
|
+
// carries on under a fainter gutter. It used to be cut off with › and the rest was
|
|
258
|
+
// gone: a long command copied out of an answer came back truncated.
|
|
259
|
+
const pieces = hardWrap(spans, inner);
|
|
260
|
+
pieces.forEach((piece, k) => rows.push(k === 0 ? [gutter, ...piece] : [{ ...gutter, t: g('┆ ', ': '), color: p.faint }, { t: '', soft: '' }, ...piece]));
|
|
237
261
|
}
|
|
238
|
-
rows.push([{ t: g('▙', '+'), color: p.tealDeep }, { t: g('─', '-').repeat(Math.max(0, width - 2)), color: p.lineSoft }]);
|
|
262
|
+
rows.push([{ t: g('▙', '+'), color: p.tealDeep, copy: '' }, { t: g('─', '-').repeat(Math.max(0, width - 2)), color: p.lineSoft, copy: '' }]);
|
|
239
263
|
return rows;
|
|
240
264
|
}
|
|
241
265
|
|
|
@@ -265,7 +289,8 @@ function table(block, width) {
|
|
|
265
289
|
const header = index === 0;
|
|
266
290
|
const spans = [];
|
|
267
291
|
for (let col = 0; col < columns; col++) {
|
|
268
|
-
|
|
292
|
+
// Copied as a tab: a table pasted into a sheet lands in cells.
|
|
293
|
+
if (col) spans.push({ t: g(' │ ', ' | '), color: p.line, copy: '\t' });
|
|
269
294
|
const content = truncateRow(inlineSpans(row[col] || '', header ? { bold: true, color: p.tealSoft } : {}), widths[col]);
|
|
270
295
|
spans.push(...content);
|
|
271
296
|
const gap = widths[col] - contentWidth(content);
|
|
@@ -273,7 +298,7 @@ function table(block, width) {
|
|
|
273
298
|
}
|
|
274
299
|
rows.push(spans);
|
|
275
300
|
if (header) {
|
|
276
|
-
rows.push(widths.flatMap((w, col) => (col ? [{ t: g('─┼─', '-+-'), color: p.line }, { t: g('─', '-').repeat(w), color: p.line }] : [{ t: g('─', '-').repeat(w), color: p.line }])));
|
|
301
|
+
rows.push(widths.flatMap((w, col) => (col ? [{ t: g('─┼─', '-+-'), color: p.line, copy: '' }, { t: g('─', '-').repeat(w), color: p.line, copy: '' }] : [{ t: g('─', '-').repeat(w), color: p.line, copy: '' }])));
|
|
277
302
|
}
|
|
278
303
|
});
|
|
279
304
|
return rows;
|