jaitus 0.3.0 → 0.3.2
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 +155 -17
- package/src/commands/login.js +50 -32
- package/src/commands/update.js +167 -9
- package/src/errors.js +2 -2
- package/src/index.js +40 -10
- package/src/tui/mouse.js +150 -125
- package/src/tui-ink.js +1998 -1927
package/src/index.js
CHANGED
|
@@ -41,7 +41,7 @@ const createTui = process.env.JAITUS_RENDERER === 'classic' ? createClassicTui :
|
|
|
41
41
|
import login from './commands/login.js';
|
|
42
42
|
import logout from './commands/logout.js';
|
|
43
43
|
import doctor from './commands/doctor.js';
|
|
44
|
-
import update from './commands/update.js';
|
|
44
|
+
import update, { updateNotice, autoUpdate } from './commands/update.js';
|
|
45
45
|
import init from './commands/init.js';
|
|
46
46
|
import research from './commands/research.js';
|
|
47
47
|
import explain from './commands/explain.js';
|
|
@@ -522,6 +522,12 @@ export function ensureUtf8Codepage() {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
async function session({ flags }) {
|
|
525
|
+
// Signed out: sign in here, in this window, and go straight on into the session —
|
|
526
|
+
// rather than stopping to tell someone to run another command first.
|
|
527
|
+
if (!signedIn() && process.stdin.isTTY && process.stdout.isTTY) {
|
|
528
|
+
const code = await login([], { then: 'continue' });
|
|
529
|
+
if (code !== 0) return code;
|
|
530
|
+
}
|
|
525
531
|
const client = requireAuth();
|
|
526
532
|
// Classic conhost speaks an OEM codepage by default, so every ›•─│ the CLI
|
|
527
533
|
// paints arrives as mojibake (the whole UI looks broken, not old). One
|
|
@@ -786,9 +792,11 @@ async function session({ flags }) {
|
|
|
786
792
|
spent.turns += 1;
|
|
787
793
|
// opencode assistant metadata: ▣ Mode · model · duration.
|
|
788
794
|
const secs = ((Date.now() - turnStarted) / 1000).toFixed(1);
|
|
789
|
-
|
|
795
|
+
// The same words the header uses for the mode. "Off" on its own read as though
|
|
796
|
+
// something was broken; it meant approvals were off.
|
|
797
|
+
const posture = permissions.mode === 'plan' ? 'Plan'
|
|
790
798
|
: permissions.mode === 'acceptEdits' ? 'Auto-edit'
|
|
791
|
-
: permissions.mode === 'bypassPermissions' ? '
|
|
799
|
+
: permissions.mode === 'bypassPermissions' ? 'Bypass' : 'Ask';
|
|
792
800
|
const meta = ` ${c.brand(glyph('▣', '*'))} ${posture} ${c.dim(`· ${model} · ${secs}s`)}`;
|
|
793
801
|
if (tui) tui.print(meta);
|
|
794
802
|
else out(meta);
|
|
@@ -1039,6 +1047,9 @@ async function session({ flags }) {
|
|
|
1039
1047
|
: true;
|
|
1040
1048
|
if (process.stdout.isTTY && wantFullscreen) {
|
|
1041
1049
|
tui.start({ onLine: handleLine, history: sessionHistory() });
|
|
1050
|
+
// A global install never updates itself; say so when there is something newer.
|
|
1051
|
+
// Background and cached for a day — the app never waits on the network for this.
|
|
1052
|
+
void updateNotice().then((line) => { if (line) tui.notice(line, 'info'); }).catch(() => {});
|
|
1042
1053
|
// Fresh sessions open on the home moment, opencode-style: the centered
|
|
1043
1054
|
// two-tone mark, then a tip — the composer below is the whole welcome.
|
|
1044
1055
|
// The mark is vertically centered in the free space above the pinned
|
|
@@ -1122,7 +1133,7 @@ async function session({ flags }) {
|
|
|
1122
1133
|
const { columns: termColumns } = await import('./ui.js');
|
|
1123
1134
|
const approval = permissions.mode === 'plan' ? 'read-only'
|
|
1124
1135
|
: permissions.mode === 'acceptEdits' ? 'auto-edit'
|
|
1125
|
-
: permissions.mode === 'bypassPermissions' ? '
|
|
1136
|
+
: permissions.mode === 'bypassPermissions' ? 'bypass' : 'ask';
|
|
1126
1137
|
for (const line of renderClassicOpen({
|
|
1127
1138
|
version: VERSION, model, approval, cwd: flags.cwd,
|
|
1128
1139
|
account: creds?.user?.email ? `${creds.user.email}${creds.user.isPaid ? '' : ' (free plan)'}` : null,
|
|
@@ -1992,9 +2003,24 @@ async function slashCommand(verb, args, ctx) {
|
|
|
1992
2003
|
return null;
|
|
1993
2004
|
}
|
|
1994
2005
|
|
|
1995
|
-
case '/login':
|
|
1996
|
-
|
|
2006
|
+
case '/login': {
|
|
2007
|
+
// In the app view the steps go to the transcript; the session carries on after,
|
|
2008
|
+
// on the new account — every request reads the saved sign-in fresh.
|
|
2009
|
+
const t = ctx.tui;
|
|
2010
|
+
if (!t) { await login(['--force'], { then: 'none' }); return null; }
|
|
2011
|
+
const { signIn } = await import('./commands/login.js');
|
|
2012
|
+
const result = await signIn({
|
|
2013
|
+
onUrl: (url) => {
|
|
2014
|
+
t.notice('Opening your browser to sign in. If it does not open, paste this link:', 'info');
|
|
2015
|
+
t.notice(url, 'dim');
|
|
2016
|
+
},
|
|
2017
|
+
onStatus: (s) => t.setStatus(s === 'waiting' ? 'Waiting for you to approve in the browser… (Esc does not cancel; close the tab to give up)' : 'Finishing sign-in…'),
|
|
2018
|
+
});
|
|
2019
|
+
t.clearStatus?.();
|
|
2020
|
+
if (result.ok) t.notice(`Signed in as ${result.user?.email || 'your account'}${result.user?.isPaid ? '' : ' (free plan)'}.`, 'ok');
|
|
2021
|
+
else t.notice(result.message, 'warn');
|
|
1997
2022
|
return null;
|
|
2023
|
+
}
|
|
1998
2024
|
|
|
1999
2025
|
case '/logout': {
|
|
2000
2026
|
const { default: logout } = await import('./commands/logout.js');
|
|
@@ -2031,14 +2057,14 @@ async function slashCommand(verb, args, ctx) {
|
|
|
2031
2057
|
return null;
|
|
2032
2058
|
|
|
2033
2059
|
case '/update':
|
|
2034
|
-
await
|
|
2060
|
+
await update([], { json: false, interactive: false });
|
|
2035
2061
|
return null;
|
|
2036
2062
|
|
|
2037
2063
|
case '/banner': {
|
|
2038
2064
|
// Session header on demand, codex-style.
|
|
2039
2065
|
const modeApproval = ctx.permissions.mode === 'plan' ? 'read-only'
|
|
2040
2066
|
: ctx.permissions.mode === 'acceptEdits' ? 'auto-edit'
|
|
2041
|
-
: ctx.permissions.mode === 'bypassPermissions' ? '
|
|
2067
|
+
: ctx.permissions.mode === 'bypassPermissions' ? 'bypass' : 'ask';
|
|
2042
2068
|
ctx.tui.print(` ${c.dim(`jaitus v${VERSION}`)}`);
|
|
2043
2069
|
ctx.tui.print(` ${c.dim(`model ${ctx.model} · approval ${modeApproval} · ${ctx.flags.cwd}`)}`);
|
|
2044
2070
|
return null;
|
|
@@ -2288,7 +2314,7 @@ const commands = {
|
|
|
2288
2314
|
run: (argv, { json }) => {
|
|
2289
2315
|
const [key, value] = argv;
|
|
2290
2316
|
const settings = loadSettings();
|
|
2291
|
-
const ONOFF = ['copyOnSelect', 'wheelAccel', 'autoScroll', 'animations', 'autoCompact', 'mouse'];
|
|
2317
|
+
const ONOFF = ['copyOnSelect', 'wheelAccel', 'autoScroll', 'animations', 'autoCompact', 'mouse', 'autoUpdate'];
|
|
2292
2318
|
const SECRET = ['braveSearchKey', 'tavilyKey', 'serperKey'];
|
|
2293
2319
|
if (!key) {
|
|
2294
2320
|
const e = endpoints();
|
|
@@ -2321,7 +2347,7 @@ const commands = {
|
|
|
2321
2347
|
['Auto-scroll', data.autoScroll ? 'on' : 'off'],
|
|
2322
2348
|
['Animations', data.animations ? 'on' : 'off'],
|
|
2323
2349
|
['Auto-compact', data.autoCompact ? 'on (summarises near the context limit)' : 'off'],
|
|
2324
|
-
['Mouse', data.mouse ? 'on (wheel scrolls, clicks act
|
|
2350
|
+
['Mouse', data.mouse ? 'on (wheel scrolls, clicks act, drag or double-click copies)' : 'off (the terminal selects text itself)'],
|
|
2325
2351
|
['Search API', data.searchApi ? `${data.searchApi} (set)` : 'none — using free engines + npm/GitHub/MDN/Stack Overflow'],
|
|
2326
2352
|
['Status line', data.statusLine || '(unset — set a command to render one)'],
|
|
2327
2353
|
['Config', data.configDir],
|
|
@@ -2456,6 +2482,10 @@ export default async function main(argv) {
|
|
|
2456
2482
|
|
|
2457
2483
|
if (flags.version) { out(VERSION); return 0; }
|
|
2458
2484
|
|
|
2485
|
+
// Always on the newest version: install it and reopen before doing anything else.
|
|
2486
|
+
const relaunched = await autoUpdate({ argv, settings: loadSettings() }).catch(() => null);
|
|
2487
|
+
if (relaunched !== null) return relaunched;
|
|
2488
|
+
|
|
2459
2489
|
const [first, ...others] = rest;
|
|
2460
2490
|
const cmd = commands[first];
|
|
2461
2491
|
|
package/src/tui/mouse.js
CHANGED
|
@@ -1,125 +1,150 @@
|
|
|
1
|
-
// ─── Mouse ──────────────────────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// Ink has no mouse support, so this is the whole of it: turn the terminal's reporting
|
|
4
|
-
// on, recognise the sequences when they arrive through the ordinary key handler, and
|
|
5
|
-
// turn it off again on the way out.
|
|
6
|
-
//
|
|
7
|
-
// Two things make that safe to do:
|
|
8
|
-
//
|
|
9
|
-
// • **SGR extended mode (1006) or nothing.** The original X10 encoding packs the
|
|
10
|
-
// coordinates into single bytes offset by 32, so it cannot report a click past
|
|
11
|
-
// column 223 — on a wide terminal the right-hand half of the screen silently stops
|
|
12
|
-
// responding. Every terminal worth enabling this for has supported 1006 for a
|
|
13
|
-
// decade; the ones that do not simply get no mouse, which is the honest outcome.
|
|
14
|
-
// • **The sequences arrive as ordinary input.** Ink's key parser does not recognise
|
|
15
|
-
// them, so it hands them through as a plain string with the leading escape stripped.
|
|
16
|
-
// That is lucky rather than designed, and it is why the guard below runs before
|
|
17
|
-
// anything can insert them into the composer — without it, scrolling the wheel types
|
|
18
|
-
// `[<64;10;5M` into your prompt.
|
|
19
|
-
//
|
|
20
|
-
// Enabling mouse reporting takes over the terminal's own text selection. That is a real
|
|
21
|
-
// cost, not a detail: people select and copy out of a terminal constantly. Hold Shift to
|
|
22
|
-
// get the native behaviour back in every terminal that supports this, and `/mouse off`
|
|
23
|
-
// turns it off for good.
|
|
24
|
-
|
|
25
|
-
// Button-event tracking (1002) rather than any-event (1003): 1003 reports every pixel of
|
|
26
|
-
// pointer motion, which on a busy screen is thousands of writes a second for information
|
|
27
|
-
// nothing here uses.
|
|
28
|
-
const ON = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
|
|
29
|
-
const OFF = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
|
|
30
|
-
|
|
31
|
-
// Ink strips the leading escape, so both spellings have to match.
|
|
32
|
-
const SGR = /^\x1b?\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
33
|
-
|
|
34
|
-
// The X10 encoding: `CSI M` followed by three raw bytes. A terminal
|
|
35
|
-
// understand the 1006 request still honours 1000 and reports in this
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
const X10 = /^\x1b?\[M[\s\S]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
enabled =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
x
|
|
80
|
-
y
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
1
|
+
// ─── Mouse ──────────────────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Ink has no mouse support, so this is the whole of it: turn the terminal's reporting
|
|
4
|
+
// on, recognise the sequences when they arrive through the ordinary key handler, and
|
|
5
|
+
// turn it off again on the way out.
|
|
6
|
+
//
|
|
7
|
+
// Two things make that safe to do:
|
|
8
|
+
//
|
|
9
|
+
// • **SGR extended mode (1006) or nothing.** The original X10 encoding packs the
|
|
10
|
+
// coordinates into single bytes offset by 32, so it cannot report a click past
|
|
11
|
+
// column 223 — on a wide terminal the right-hand half of the screen silently stops
|
|
12
|
+
// responding. Every terminal worth enabling this for has supported 1006 for a
|
|
13
|
+
// decade; the ones that do not simply get no mouse, which is the honest outcome.
|
|
14
|
+
// • **The sequences arrive as ordinary input.** Ink's key parser does not recognise
|
|
15
|
+
// them, so it hands them through as a plain string with the leading escape stripped.
|
|
16
|
+
// That is lucky rather than designed, and it is why the guard below runs before
|
|
17
|
+
// anything can insert them into the composer — without it, scrolling the wheel types
|
|
18
|
+
// `[<64;10;5M` into your prompt.
|
|
19
|
+
//
|
|
20
|
+
// Enabling mouse reporting takes over the terminal's own text selection. That is a real
|
|
21
|
+
// cost, not a detail: people select and copy out of a terminal constantly. Hold Shift to
|
|
22
|
+
// get the native behaviour back in every terminal that supports this, and `/mouse off`
|
|
23
|
+
// turns it off for good.
|
|
24
|
+
|
|
25
|
+
// Button-event tracking (1002) rather than any-event (1003): 1003 reports every pixel of
|
|
26
|
+
// pointer motion, which on a busy screen is thousands of writes a second for information
|
|
27
|
+
// nothing here uses.
|
|
28
|
+
const ON = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
|
|
29
|
+
const OFF = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
|
|
30
|
+
|
|
31
|
+
// Ink strips the leading escape, so both spellings have to match.
|
|
32
|
+
const SGR = /^\x1b?\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
33
|
+
|
|
34
|
+
// The X10 encoding: `CSI M` followed by three raw bytes, each offset by 32. A terminal
|
|
35
|
+
// that does not understand the 1006 request still honours 1000 and reports in this
|
|
36
|
+
// form. It is decoded rather than swallowed: its coordinates stop at column 223, but
|
|
37
|
+
// below that it is a perfectly good report, and swallowing it meant a terminal without
|
|
38
|
+
// 1006 had no selection at all AND no native one, because reporting was still on.
|
|
39
|
+
const X10 = /^\x1b?\[M([\s\S])([\s\S])([\s\S])$/;
|
|
40
|
+
|
|
41
|
+
// A report the input layer delivered in pieces. Ink holds an unfinished escape sequence
|
|
42
|
+
// for only 20ms before passing it through as typed text, so a report split across two
|
|
43
|
+
// reads — a busy machine, SSH, a fast trackpad drag — arrived as two halves: the drag
|
|
44
|
+
// was lost and the second half was typed into the prompt as `[<32;15;13M`.
|
|
45
|
+
const SGR_FRAGMENT = /^\x1b?\[<[\d;]*$/;
|
|
46
|
+
const X10_FRAGMENT = /^\x1b?\[M[\s\S]{0,2}$/;
|
|
47
|
+
|
|
48
|
+
/** True once, per process: never write the disable sequence for something never enabled. */
|
|
49
|
+
let enabled = false;
|
|
50
|
+
|
|
51
|
+
export function enableMouse(stream = process.stdout) {
|
|
52
|
+
if (enabled || !stream?.isTTY) return false;
|
|
53
|
+
try { stream.write(ON); enabled = true; } catch { /* not a real terminal */ }
|
|
54
|
+
return enabled;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function disableMouse(stream = process.stdout) {
|
|
58
|
+
if (!enabled) return;
|
|
59
|
+
try { stream.write(OFF); } catch { /* going away anyway */ }
|
|
60
|
+
enabled = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const mouseEnabled = () => enabled;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One input string → a mouse event, or null.
|
|
67
|
+
*
|
|
68
|
+
* The button field is a bitfield: the low two bits are which button, bit 5 (32) means
|
|
69
|
+
* the pointer was moving with a button held, and bit 6 (64) means the wheel. Shift, Alt
|
|
70
|
+
* and Ctrl ride in bits 2–4 and are reported so a Shift-click can mean something
|
|
71
|
+
* different from a click.
|
|
72
|
+
*/
|
|
73
|
+
export function parseMouse(input) {
|
|
74
|
+
const s = String(input ?? '');
|
|
75
|
+
let code, x, y, pressed;
|
|
76
|
+
const m = SGR.exec(s);
|
|
77
|
+
if (m) {
|
|
78
|
+
code = Number(m[1]);
|
|
79
|
+
x = Number(m[2]);
|
|
80
|
+
y = Number(m[3]);
|
|
81
|
+
pressed = m[4] === 'M';
|
|
82
|
+
} else {
|
|
83
|
+
const o = X10.exec(s);
|
|
84
|
+
if (!o) return null;
|
|
85
|
+
code = o[1].charCodeAt(0) - 32;
|
|
86
|
+
x = o[2].charCodeAt(0) - 32;
|
|
87
|
+
y = o[3].charCodeAt(0) - 32;
|
|
88
|
+
// X10 has no release suffix: a release is button 3, and it does not say which.
|
|
89
|
+
pressed = (code & 3) !== 3 || (code & 64) !== 0;
|
|
90
|
+
if (x < 1 || y < 1) return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const wheel = (code & 64) !== 0;
|
|
94
|
+
const motion = (code & 32) !== 0;
|
|
95
|
+
const button = code & 3;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
x,
|
|
99
|
+
y,
|
|
100
|
+
shift: (code & 4) !== 0,
|
|
101
|
+
meta: (code & 8) !== 0,
|
|
102
|
+
ctrl: (code & 16) !== 0,
|
|
103
|
+
// A wheel "button" of 0 is up and 1 is down; 2 and 3 are horizontal tilt, which is
|
|
104
|
+
// reported so a future horizontal scroll has somewhere to go rather than being
|
|
105
|
+
// silently read as a vertical one.
|
|
106
|
+
kind: wheel
|
|
107
|
+
? (button === 0 ? 'wheelUp' : button === 1 ? 'wheelDown' : button === 2 ? 'wheelLeft' : 'wheelRight')
|
|
108
|
+
: motion ? 'drag'
|
|
109
|
+
: pressed ? 'down' : 'up',
|
|
110
|
+
button: wheel ? null : button === 0 ? 'left' : button === 1 ? 'middle' : button === 2 ? 'right' : null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The start of a mouse report whose remainder has not arrived yet. */
|
|
115
|
+
export const isMouseFragment = (input) => {
|
|
116
|
+
const s = String(input ?? '');
|
|
117
|
+
return (SGR_FRAGMENT.test(s) || X10_FRAGMENT.test(s)) && !SGR.test(s) && !X10.test(s);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Does this input look like a mouse report we should swallow rather than type? */
|
|
121
|
+
export const isMouse = (input) => {
|
|
122
|
+
const s = String(input ?? '');
|
|
123
|
+
return SGR.test(s) || X10.test(s);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A hit map for one frame.
|
|
128
|
+
*
|
|
129
|
+
* The layout is computed in one place, so the regions can be computed there too rather
|
|
130
|
+
* than measured afterwards. Rows and columns are 1-based to match what the terminal
|
|
131
|
+
* reports, and later registrations win — which is what makes an overlay's buttons
|
|
132
|
+
* reachable while the header underneath it is not.
|
|
133
|
+
*/
|
|
134
|
+
export function createHitMap() {
|
|
135
|
+
let regions = [];
|
|
136
|
+
return {
|
|
137
|
+
reset() { regions = []; },
|
|
138
|
+
add(region) { if (region && region.action) regions.push(region); },
|
|
139
|
+
/** The topmost region containing this cell, or null. */
|
|
140
|
+
at(x, y) {
|
|
141
|
+
for (let i = regions.length - 1; i >= 0; i--) {
|
|
142
|
+
const r = regions[i];
|
|
143
|
+
if (x >= r.x1 && x <= r.x2 && y >= r.y1 && y <= r.y2) return r;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
},
|
|
147
|
+
get size() { return regions.length; },
|
|
148
|
+
all() { return regions.slice(); },
|
|
149
|
+
};
|
|
150
|
+
}
|