jaitus 0.3.21 → 0.3.23
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 +2 -0
- package/src/serviceNotice.js +9 -0
- package/src/tui/orb.js +41 -0
- package/src/tui/widgets.js +14 -0
- package/src/tui-ink.js +112 -11
- package/src/ui.js +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jaitus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "Jaitus — a coding agent in your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node tests/source-sanity.js && node tests/react-build.js && node tests/stalls.js && node tests/clipboard-write.js && node tests/rewind-images.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js && node tests/service-notices.js && node tests/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js && node tests/context-trim.js && node tests/compaction.js && node tests/copy-clean.js && node tests/parallel-tools.js && node tests/plan-percent.js && node tests/project-identity.js",
|
|
19
|
+
"test": "node tests/source-sanity.js && node tests/react-build.js && node tests/stalls.js && node tests/clipboard-write.js && node tests/rewind-images.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js && node tests/service-notices.js && node tests/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js && node tests/context-trim.js && node tests/compaction.js && node tests/copy-clean.js && node tests/parallel-tools.js && node tests/plan-percent.js && node tests/orb.js && node tests/update-required.js && node tests/select-scroll.js && node tests/project-identity.js",
|
|
20
20
|
"preview": "node scripts/tui-preview.js",
|
|
21
21
|
"prepublishOnly": "npm test"
|
|
22
22
|
},
|
package/src/api.js
CHANGED
|
@@ -94,7 +94,7 @@ export function createClient() {
|
|
|
94
94
|
|
|
95
95
|
// x-jaitus-features: what this build understands, so the gateway can use newer error
|
|
96
96
|
// codes (a used-up usage window) without an older CLI reading them as "went wrong".
|
|
97
|
-
const base = { 'Content-Type': 'application/json', 'user-agent': `jaitus-cli/${version()}`, 'x-jaitus-features': 'usage-window plan-expired' };
|
|
97
|
+
const base = { 'Content-Type': 'application/json', 'user-agent': `jaitus-cli/${version()}`, 'x-jaitus-features': 'usage-window plan-expired update-required' };
|
|
98
98
|
// x-jaitus-device: this computer's hashed identifiers (machine.js), which the free
|
|
99
99
|
// tier is limited by. Built once per process.
|
|
100
100
|
const headers = (token) => ({ ...base, 'x-jaitus-device': machineHeader(), authorization: `Bearer ${token}` });
|
package/src/errors.js
CHANGED
|
@@ -16,6 +16,8 @@ const CATALOG = {
|
|
|
16
16
|
free_machine_limit: { title: 'No free prompts on this account', hint: 'Subscribe at jaitus.com/billing to keep using Jaitus on this account.' },
|
|
17
17
|
free_needs_check: { title: 'Unlock your free prompts', hint: 'A one-time ₹1 UPI check at jaitus.com/billing (refunded). Or subscribe.' },
|
|
18
18
|
free_needs_cli: { title: 'Update Jaitus for free prompts', hint: 'Run npm i -g jaitus, then try again.' },
|
|
19
|
+
// This version is below what Jaitus now requires; the message names both versions.
|
|
20
|
+
update_required: { title: 'Update Jaitus to continue', hint: 'Run npm i -g jaitus@latest, then start jaitus again.' },
|
|
19
21
|
free_request_too_large: { title: 'Too large for the free plan', hint: 'Run /compact or start a new session, or subscribe at jaitus.com/billing.' },
|
|
20
22
|
plan_limit_reached: { title: 'Monthly usage used up', hint: 'To keep going now, switch plans at jaitus.com/billing — what is left of this plan carries over. Buying another 30 days of the same plan adds days but does not refill this month. /usage shows where you are.' },
|
|
21
23
|
rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
|
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/orb.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── The orb ────────────────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The power source the website draws its energy from, in two terminal cells: a ring of
|
|
4
|
+
// braille dots spinning round a core that crackles, flaring white every couple of
|
|
5
|
+
// seconds. It replaces the one-cell spinner wherever work is running, so the terminal
|
|
6
|
+
// shows the same thing the site does — the work is being powered by the orb.
|
|
7
|
+
//
|
|
8
|
+
// Two cells are a 4×4 grid of dots. The ring is the eight dots on the edge; a two-dot
|
|
9
|
+
// gap travels round it, which is what reads as spin. The four centre dots are the core.
|
|
10
|
+
// Pure function of the tick, so it costs nothing beyond the shared animation timer, and
|
|
11
|
+
// frame 0 is a sensible still for reduced motion.
|
|
12
|
+
|
|
13
|
+
// Braille bit for column c (0..1) and row r (0..3) inside one cell.
|
|
14
|
+
const BIT = [[0x01, 0x02, 0x04, 0x40], [0x08, 0x10, 0x20, 0x80]];
|
|
15
|
+
|
|
16
|
+
// The ring, clockwise from the top: [column 0..3, row 0..3] on the 4×4 grid.
|
|
17
|
+
const RING = [[1, 0], [2, 0], [3, 1], [3, 2], [2, 3], [1, 3], [0, 2], [0, 1]];
|
|
18
|
+
const CORE = [[1, 1], [2, 1], [1, 2], [2, 2]];
|
|
19
|
+
|
|
20
|
+
function cells(dots) {
|
|
21
|
+
const bits = [0, 0];
|
|
22
|
+
for (const [x, y] of dots) bits[x >> 1] |= BIT[x & 1][y];
|
|
23
|
+
return String.fromCharCode(0x2800 + bits[0], 0x2800 + bits[1]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The orb at a given tick. `level` is 0 (steady), 1 (the core crackling) or 2 (a surge:
|
|
28
|
+
* every dot lit) so the caller can colour it — brand, lighter, near-white.
|
|
29
|
+
*/
|
|
30
|
+
export function orbFrame(tick) {
|
|
31
|
+
const n = Math.max(0, Math.floor(tick));
|
|
32
|
+
if (n % 30 < 2 && n >= 30) return { text: cells([...RING, ...CORE]), level: 2 };
|
|
33
|
+
const gap = n % RING.length;
|
|
34
|
+
const ring = RING.filter((_, i) => i !== gap && i !== (gap + 1) % RING.length);
|
|
35
|
+
// The core crackles on an irregular beat — a fixed pattern of primes rather than
|
|
36
|
+
// random, so a frame is reproducible in tests and previews.
|
|
37
|
+
const crackle = n % 11 === 3 || n % 11 === 4 || n % 7 === 0;
|
|
38
|
+
return { text: cells(crackle ? [...ring, ...CORE] : ring), level: crackle ? 1 : 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ORB_STILL = orbFrame(0).text;
|
package/src/tui/widgets.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Box, Text, useAnimation } from 'ink';
|
|
15
15
|
import { palette, mix, ramp } from './theme.js';
|
|
16
|
+
import { orbFrame } from './orb.js';
|
|
16
17
|
|
|
17
18
|
const h = React.createElement;
|
|
18
19
|
|
|
@@ -59,6 +60,19 @@ export function Spinner({ kind = 'braille', color, interval = 80 }) {
|
|
|
59
60
|
return h(Text, { color: color || p.brand }, active ? frames[frame % frames.length] : frames[0]);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The orb (tui/orb.js) as the working indicator: two cells, a spinning ring round a
|
|
65
|
+
* crackling core that surges white. Same shared timer as every other widget.
|
|
66
|
+
*/
|
|
67
|
+
export function OrbSpinner({ interval = 90 }) {
|
|
68
|
+
const p = palette();
|
|
69
|
+
const active = motionOn();
|
|
70
|
+
const { frame } = useAnimation({ interval, isActive: active });
|
|
71
|
+
const f = orbFrame(active ? frame : 0);
|
|
72
|
+
const color = f.level === 2 ? mix(p.brand, p.text, 0.75) : f.level === 1 ? p.brandSoft : p.brand;
|
|
73
|
+
return h(Text, { color }, f.text);
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* Text with a highlight travelling through it.
|
|
64
78
|
*
|
package/src/tui-ink.js
CHANGED
|
@@ -28,7 +28,7 @@ import { Box, Spacer, Text, render, useAnimation, useApp, useCursor, useInput, u
|
|
|
28
28
|
import { palette, resetPalette, mix, ramp } from './tui/theme.js';
|
|
29
29
|
import { makeEntry, rowsFor, invalidate, expandable } from './tui/entries.js';
|
|
30
30
|
import { plain, stringWidth, truncateRow, wrapRow, rowText, inverseRange, copyText, softJoin, isDecorationRow } from './tui/ansi.js';
|
|
31
|
-
import { Spinner, Shimmer, GradientText, FlowingRule, Meter, Elapsed, CursorRow, Rows, Row, Reveal, motionOn, publishSettings } from './tui/widgets.js';
|
|
31
|
+
import { Spinner, OrbSpinner, Shimmer, GradientText, FlowingRule, Meter, Elapsed, CursorRow, Rows, Row, Reveal, motionOn, publishSettings } from './tui/widgets.js';
|
|
32
32
|
import { wordRows, wordSize } from './tui/wordmark.js';
|
|
33
33
|
import { enableMouse, disableMouse, parseMouse, isMouse, isMouseFragment, createHitMap } from './tui/mouse.js';
|
|
34
34
|
import { openUrl } from './open.js';
|
|
@@ -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
|
*
|
|
@@ -590,7 +627,7 @@ function WorkBar({ state, width }) {
|
|
|
590
627
|
const label = plain(state.status) || 'Working';
|
|
591
628
|
const narrow = width < 70;
|
|
592
629
|
return h(Box, { flexDirection: 'row', paddingX: 1 },
|
|
593
|
-
h(Spinner, { kind:
|
|
630
|
+
ascii() ? h(Spinner, { kind: 'ascii' }) : h(OrbSpinner),
|
|
594
631
|
h(Text, null, ' '),
|
|
595
632
|
h(Shimmer, { from: p.brand, to: p.brandSoft }, truncatePlain(label, Math.max(12, width - (narrow ? 22 : 46)))),
|
|
596
633
|
h(Spacer),
|
|
@@ -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;
|
package/src/ui.js
CHANGED
|
@@ -18,6 +18,7 @@ import path from 'node:path';
|
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import { loadSettings, HOME } from './config.js';
|
|
20
20
|
import { terminalBackground } from './terminal.js';
|
|
21
|
+
import { orbFrame } from './tui/orb.js';
|
|
21
22
|
|
|
22
23
|
// Read without re-entering config's write paths; failures mean defaults.
|
|
23
24
|
function loadSettingsFile() { return loadSettings(); }
|
|
@@ -266,9 +267,8 @@ export function spinner(text) {
|
|
|
266
267
|
out(`${c.dim(glyph('…', '...'))} ${text}`);
|
|
267
268
|
return { stop: () => {}, update: () => {} };
|
|
268
269
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
: ['|', '/', '-', '\\'];
|
|
270
|
+
// The orb (tui/orb.js) where the terminal can draw braille; a plain bar where it can't.
|
|
271
|
+
const frames = unicode() ? null : ['|', '/', '-', '\\'];
|
|
272
272
|
let i = 0;
|
|
273
273
|
let label = text;
|
|
274
274
|
// stop() is called more than once per spinner: runAgent stops it when the first
|
|
@@ -285,7 +285,11 @@ export function spinner(text) {
|
|
|
285
285
|
const tick = () => {
|
|
286
286
|
if (stopped) return;
|
|
287
287
|
const n = i++;
|
|
288
|
-
|
|
288
|
+
const glyphNow = frames ? c.brand(frames[n % frames.length]) : (() => {
|
|
289
|
+
const f = orbFrame(n);
|
|
290
|
+
return f.level ? c.shimmer(f.text) : c.brand(f.text);
|
|
291
|
+
})();
|
|
292
|
+
write(`\r\x1b[2K${glyphNow} ${sweep(label, n)}`);
|
|
289
293
|
};
|
|
290
294
|
tick();
|
|
291
295
|
const timer = setInterval(tick, 80);
|