jaitus 0.3.22 → 0.3.24
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/api.js +1 -1
- package/src/errors.js +126 -124
- package/src/index.js +3198 -3189
- package/src/serviceNotice.js +9 -0
- package/src/tui-ink.js +110 -9
- package/src/updatePolicy.js +54 -0
package/src/serviceNotice.js
CHANGED
|
@@ -75,6 +75,15 @@ export function lineFor(item, where = statusAddress()) {
|
|
|
75
75
|
if (until) return `Jaitus is down for maintenance until ${until}. Updates: ${where}`;
|
|
76
76
|
return `Jaitus is down for maintenance. Updates: ${where}`;
|
|
77
77
|
}
|
|
78
|
+
if (item.kind === 'offer') {
|
|
79
|
+
// An offer from Admin → Offers: its headline, the short line under it, the code if it
|
|
80
|
+
// has one, and where to take it.
|
|
81
|
+
if (!title) return null;
|
|
82
|
+
const details = dropStop(clean(item.details, 90));
|
|
83
|
+
const code = clean(item.code, 30);
|
|
84
|
+
const billing = where.replace(/\/status$/, '/billing');
|
|
85
|
+
return `${title}${details ? ` — ${details}` : ''}.${code ? ` Code ${code}.` : ''} ${billing}`;
|
|
86
|
+
}
|
|
78
87
|
if (item.kind === 'maintenance' && item.phase === 'upcoming') {
|
|
79
88
|
const when = clean(item.window, 60);
|
|
80
89
|
if (!when) return null;
|
package/src/tui-ink.js
CHANGED
|
@@ -39,6 +39,7 @@ import { badgeText, badgeTop, turnShareText } from './planView.js';
|
|
|
39
39
|
|
|
40
40
|
const h = React.createElement;
|
|
41
41
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
42
|
+
|
|
42
43
|
const ascii = () => process.env.JAITUS_ASCII === '1';
|
|
43
44
|
const g = (uni, plainText) => (ascii() ? plainText : uni);
|
|
44
45
|
|
|
@@ -419,8 +420,9 @@ function layoutTranscript(state, width, height, top) {
|
|
|
419
420
|
|
|
420
421
|
// The drag handler needs the text under the pointer, and it must be the text actually
|
|
421
422
|
// on screen rather than the transcript's idea of it.
|
|
422
|
-
state.screen = { top: screenTop, left: 2, rows: visible };
|
|
423
|
+
state.screen = { top: screenTop, left: 2, rows: visible, first, all: rows, height };
|
|
423
424
|
state.totalRows = rows.length;
|
|
425
|
+
anchorSelection(state);
|
|
424
426
|
|
|
425
427
|
// The sticky header names the prompt whose answer you are looking at — the last one
|
|
426
428
|
// that has scrolled off the top. Scroll far enough that the prompt itself is back on
|
|
@@ -554,6 +556,41 @@ const URL_IN_TEXT = /https?:\/\/[^\s<>"'`]+/g;
|
|
|
554
556
|
// background colour, then CSI ? … c device attributes — alone or run together.
|
|
555
557
|
export const TERMINAL_REPLY = /^(?=[\x1b\][])(?:\x1b?\]11;rgb:[0-9a-f]{1,4}\/[0-9a-f]{1,4}\/[0-9a-f]{1,4}(?:\x07|\x1b\\)?)?(?:\x1b?\[\?[0-9;]*c)?$/i;
|
|
556
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Keep a drag selection on the TEXT while the view scrolls.
|
|
561
|
+
*
|
|
562
|
+
* A selection that starts in the transcript records the row of the whole transcript it
|
|
563
|
+
* started on, so scrolling (the wheel, or holding the pointer past an edge) moves the
|
|
564
|
+
* highlight with the text instead of leaving it on the same screen cells over different
|
|
565
|
+
* text. While the button is held the other end follows the pointer: whichever row is under
|
|
566
|
+
* it now, clamped to what is on screen — so with the pointer above the conversation it is
|
|
567
|
+
* the top row, and the selection grows as more scrolls in. Screen positions are derived
|
|
568
|
+
* from rows here, every frame, so painting and copying read the same thing.
|
|
569
|
+
*/
|
|
570
|
+
function selectionFirstRow(state) {
|
|
571
|
+
const s = state.screen;
|
|
572
|
+
if (!s?.all) return 0;
|
|
573
|
+
const total = s.all.length;
|
|
574
|
+
const offset = state.viewingTranscript ? clamp(state.transcriptOffset || 0, 0, Math.max(0, total - s.height)) : 0;
|
|
575
|
+
return Math.max(0, total - offset - s.height);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function anchorSelection(state) {
|
|
579
|
+
const sel = state.selection;
|
|
580
|
+
const s = state.screen;
|
|
581
|
+
if (!sel?.anchor || !sel.focus || sel.anchor.row == null || !s?.all) return;
|
|
582
|
+
const first = selectionFirstRow(state);
|
|
583
|
+
const shown = Math.min(s.height, s.all.length - first);
|
|
584
|
+
if (sel.dragging && sel.pointer) {
|
|
585
|
+
const lastRow = first + Math.max(0, shown - 1);
|
|
586
|
+
const row = clamp(first + (sel.pointer.y - s.top), first, lastRow);
|
|
587
|
+
const x = sel.pointer.y < s.top ? s.left : sel.pointer.y > s.top + shown - 1 ? 1e4 : sel.pointer.x;
|
|
588
|
+
sel.focus = { x, row };
|
|
589
|
+
}
|
|
590
|
+
sel.anchor.y = s.top + sel.anchor.row - first;
|
|
591
|
+
if (sel.focus.row != null) sel.focus.y = s.top + sel.focus.row - first;
|
|
592
|
+
}
|
|
593
|
+
|
|
557
594
|
/**
|
|
558
595
|
* Draw the drag selection.
|
|
559
596
|
*
|
|
@@ -1772,6 +1809,43 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
|
|
|
1772
1809
|
notifyNow();
|
|
1773
1810
|
}
|
|
1774
1811
|
|
|
1812
|
+
/** The row of the whole transcript at screen line `y`, or null off the transcript. */
|
|
1813
|
+
function transcriptRowAt(y) {
|
|
1814
|
+
const s = state.screen;
|
|
1815
|
+
if (!s?.all || state.dialog || state.panel) return null;
|
|
1816
|
+
if (y < s.top || y > s.top + s.rows.length - 1) return null;
|
|
1817
|
+
return selectionFirstRow(state) + (y - s.top);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Holding a drag past the top or bottom of the conversation scrolls it, faster the
|
|
1821
|
+
// further out the pointer is, until the button is released or the pointer comes back —
|
|
1822
|
+
// what every terminal does. Terminals only report the mouse when it moves, so a pointer
|
|
1823
|
+
// held still past the edge needs this timer to keep going.
|
|
1824
|
+
let edgeTimer = null;
|
|
1825
|
+
function stopEdgeScroll() { if (edgeTimer) { clearInterval(edgeTimer); edgeTimer = null; } }
|
|
1826
|
+
function edgeOut() {
|
|
1827
|
+
const sel = state.selection;
|
|
1828
|
+
const s = state.screen;
|
|
1829
|
+
if (!sel?.dragging || !sel.pointer || !s?.rows?.length) return 0;
|
|
1830
|
+
const bottom = s.top + s.rows.length - 1;
|
|
1831
|
+
if (sel.pointer.y < s.top) return s.top - sel.pointer.y; // above: positive = back into history
|
|
1832
|
+
if (sel.pointer.y > bottom) return -(sel.pointer.y - bottom); // below: towards the newest
|
|
1833
|
+
return 0;
|
|
1834
|
+
}
|
|
1835
|
+
function edgeScroll() {
|
|
1836
|
+
if (!edgeOut()) return stopEdgeScroll();
|
|
1837
|
+
if (edgeTimer) return;
|
|
1838
|
+
edgeTimer = setInterval(() => {
|
|
1839
|
+
const out = edgeOut();
|
|
1840
|
+
if (!out) return stopEdgeScroll();
|
|
1841
|
+
const step = Math.min(4, Math.abs(out));
|
|
1842
|
+
scroll(out > 0 ? step : -step);
|
|
1843
|
+
anchorSelection(state);
|
|
1844
|
+
notifyNow();
|
|
1845
|
+
}, 60);
|
|
1846
|
+
edgeTimer.unref?.();
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1775
1849
|
// ── selecting text in the composer ──
|
|
1776
1850
|
//
|
|
1777
1851
|
// The composer is a text field, so it selects like one: drag with the mouse (copied on
|
|
@@ -1857,15 +1931,28 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
|
|
|
1857
1931
|
// out or replaced by what it stands for, and a wrapped line rejoins the line it was
|
|
1858
1932
|
// wrapped from (tui/ansi.js copyText / softJoin).
|
|
1859
1933
|
const pieces = [];
|
|
1860
|
-
screen
|
|
1861
|
-
|
|
1862
|
-
|
|
1934
|
+
// `startX` / `endX` are screen columns of the first and last selected row (or null for
|
|
1935
|
+
// "the whole row").
|
|
1936
|
+
const take = (row, startX, endX) => {
|
|
1863
1937
|
if (isDecorationRow(row)) return;
|
|
1864
1938
|
const len = rowText(row).length;
|
|
1865
|
-
const start =
|
|
1866
|
-
const end =
|
|
1939
|
+
const start = startX == null ? 0 : Math.max(0, startX - screen.left);
|
|
1940
|
+
const end = endX == null ? len : Math.min(len, Math.max(0, endX - screen.left + 1));
|
|
1867
1941
|
pieces.push({ text: copyText(row, start, end), join: pieces.length ? softJoin(row) : null });
|
|
1868
|
-
}
|
|
1942
|
+
};
|
|
1943
|
+
if (a.row != null && b.row != null && screen.all) {
|
|
1944
|
+
// Anchored to the text: everything selected, including what has scrolled off screen.
|
|
1945
|
+
const [f, t] = (a.row < b.row || (a.row === b.row && a.x <= b.x)) ? [a, b] : [b, a];
|
|
1946
|
+
for (let r = f.row; r <= t.row; r++) {
|
|
1947
|
+
if (screen.all[r]) take(screen.all[r], r === f.row ? f.x : null, r === t.row ? t.x : null);
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
screen.rows.forEach((row, index) => {
|
|
1951
|
+
const y = screen.top + index;
|
|
1952
|
+
if (y < from.y || y > to.y) return;
|
|
1953
|
+
take(row, y === from.y ? from.x : null, y === to.y ? to.x : null);
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1869
1956
|
let out = '';
|
|
1870
1957
|
pieces.forEach((piece, i) => {
|
|
1871
1958
|
// A word cut mid-way (join '') keeps every character on both sides; anywhere else
|
|
@@ -1912,6 +1999,7 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
|
|
|
1912
1999
|
}
|
|
1913
2000
|
|
|
1914
2001
|
function clearSelection() {
|
|
2002
|
+
stopEdgeScroll();
|
|
1915
2003
|
if (!state.selection) return;
|
|
1916
2004
|
state.selection = null;
|
|
1917
2005
|
notifyNow();
|
|
@@ -2141,20 +2229,33 @@ export function createTui({ slashCommands = [], fileProvider = null, meta = {},
|
|
|
2141
2229
|
// A press starts a possible drag AND a possible click. Which it was is only
|
|
2142
2230
|
// known on release, so nothing is dispatched yet — dispatching here made every
|
|
2143
2231
|
// attempt to select text also press whatever was under the cursor.
|
|
2144
|
-
pressedAt = { x: event.x, y: event.y };
|
|
2232
|
+
pressedAt = { x: event.x, y: event.y, row: transcriptRowAt(event.y) };
|
|
2145
2233
|
dragged = false;
|
|
2146
2234
|
clearSelection();
|
|
2147
2235
|
return;
|
|
2148
2236
|
}
|
|
2149
2237
|
if (event.kind === 'drag' && pressedAt) {
|
|
2150
2238
|
if (event.x !== pressedAt.x || event.y !== pressedAt.y) dragged = true;
|
|
2151
|
-
|
|
2239
|
+
if (pressedAt.row == null) {
|
|
2240
|
+
state.selection = { anchor: { x: pressedAt.x, y: pressedAt.y }, focus: { x: event.x, y: event.y } };
|
|
2241
|
+
return notifyNow();
|
|
2242
|
+
}
|
|
2243
|
+
// Anchored to the text, the other end on the pointer (anchorSelection), and the
|
|
2244
|
+
// view scrolls on its own while the pointer is held past either edge.
|
|
2245
|
+
const sel = state.selection?.dragging ? state.selection
|
|
2246
|
+
: { anchor: { x: pressedAt.x, y: pressedAt.y, row: pressedAt.row }, focus: { x: event.x, y: event.y }, dragging: true };
|
|
2247
|
+
sel.pointer = { x: event.x, y: event.y };
|
|
2248
|
+
state.selection = sel;
|
|
2249
|
+
anchorSelection(state);
|
|
2250
|
+
edgeScroll();
|
|
2152
2251
|
return notifyNow();
|
|
2153
2252
|
}
|
|
2154
2253
|
if (event.kind === 'up') {
|
|
2155
2254
|
const start = pressedAt;
|
|
2156
2255
|
pressedAt = null;
|
|
2256
|
+
stopEdgeScroll();
|
|
2157
2257
|
if (!start) return;
|
|
2258
|
+
if (state.selection?.dragging) { anchorSelection(state); state.selection.dragging = false; }
|
|
2158
2259
|
if (dragged) { dragged = false; void finishSelection(); return; }
|
|
2159
2260
|
const now = Date.now();
|
|
2160
2261
|
const again = lastClick && now - lastClick.at < 450 && lastClick.y === start.y && Math.abs(lastClick.x - start.x) <= 1;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ─── "Update Jaitus to continue", even mid-task ─────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The admin can require a newer CLI (backend services/cliUpdate.js). Every request is
|
|
4
|
+
// checked there, but a CLI sitting idle — or deep in a long tool run — would not hear about
|
|
5
|
+
// it until its next request. So the CLI asks once at start and every two minutes while it
|
|
6
|
+
// is open. The question goes through the same gate: a CLI that is too old is refused with
|
|
7
|
+
// jaitus_update_required, and that refusal is the answer. When it comes, the running task
|
|
8
|
+
// is stopped and the reason is shown once, short.
|
|
9
|
+
//
|
|
10
|
+
// Any failure (offline, timeout, an older server) means "nothing to say": this never
|
|
11
|
+
// blocks or slows the CLI on its own.
|
|
12
|
+
|
|
13
|
+
import { endpoints, version } from './config.js';
|
|
14
|
+
|
|
15
|
+
const EVERY_MS = 2 * 60 * 1000;
|
|
16
|
+
const TIMEOUT_MS = 6000;
|
|
17
|
+
|
|
18
|
+
/** null when this CLI may keep working; otherwise { message } to show. */
|
|
19
|
+
export async function checkPolicy({ fetchImpl = fetch, baseUrl = endpoints().baseUrl } = {}) {
|
|
20
|
+
const ctrl = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetchImpl(`${baseUrl}/api/public/cli-policy`, {
|
|
24
|
+
headers: { accept: 'application/json', 'user-agent': `jaitus-cli/${version()}`, 'x-jaitus-features': 'usage-window plan-expired update-required' },
|
|
25
|
+
signal: ctrl.signal,
|
|
26
|
+
});
|
|
27
|
+
if (res.status !== 426) return null;
|
|
28
|
+
const body = await res.json().catch(() => null);
|
|
29
|
+
if (body?.error?.code !== 'jaitus_update_required') return null;
|
|
30
|
+
return { message: String(body.error.message || '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 240) };
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
} finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check now and every two minutes; call onBlocked({ message }) once, the first time this
|
|
40
|
+
* CLI is refused. Returns a stop function. The timer never keeps the process alive.
|
|
41
|
+
*/
|
|
42
|
+
export function watchPolicy(onBlocked, { everyMs = EVERY_MS, check = checkPolicy } = {}) {
|
|
43
|
+
let done = false;
|
|
44
|
+
let timer = null;
|
|
45
|
+
const run = async () => {
|
|
46
|
+
if (done) return;
|
|
47
|
+
const r = await check();
|
|
48
|
+
if (r && !done) { done = true; clearInterval(timer); onBlocked(r); }
|
|
49
|
+
};
|
|
50
|
+
void run();
|
|
51
|
+
timer = setInterval(() => { void run(); }, everyMs);
|
|
52
|
+
timer.unref?.();
|
|
53
|
+
return () => { done = true; clearInterval(timer); };
|
|
54
|
+
}
|