@thegitai/cli 1.0.0-preview.45 → 1.0.0-preview.47
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/README.md +4 -4
- package/dist/src/agent-mode.js +1 -4
- package/dist/src/browser/bridge.js +37 -7
- package/dist/src/browser/native-host.js +11 -1
- package/dist/src/browser/session-bridge.js +15 -3
- package/dist/src/help-text.js +15 -8
- package/dist/src/tools/browser.js +35 -18
- package/dist/src/ui/repl.js +5 -62
- package/dist/src/ui/slash-commands.js +121 -0
- package/dist/src/ui/tui/build-frame.js +10 -47
- package/dist/src/ui/tui/shell-input.js +8 -6
- package/dist/src/update-check.js +92 -8
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ Supports Node.js 22 and newer. Node.js 24 is recommended.
|
|
|
17
17
|
## Updates
|
|
18
18
|
|
|
19
19
|
`ai` checks for a newer release each time it starts, and offers to install it
|
|
20
|
-
for you. TheGitAI
|
|
21
|
-
|
|
22
|
-
you to start `ai` again, so the whole session runs
|
|
23
|
-
**Skip** to go on with the version you have. Skipping lasts for that session
|
|
20
|
+
for you. TheGitAI works best on the latest version, and an older CLI may not
|
|
21
|
+
work optimally. **Update now** is already selected, so Enter takes it — the CLI
|
|
22
|
+
installs the update and asks you to start `ai` again, so the whole session runs
|
|
23
|
+
one version — while ↑/↓ moves to **Skip** to go on with the version you have. Skipping lasts for that session
|
|
24
24
|
only; the next start asks again. Ctrl+C quits without installing anything.
|
|
25
25
|
|
|
26
26
|
## Usage
|
package/dist/src/agent-mode.js
CHANGED
|
@@ -148,12 +148,9 @@ export function agentModeLabel(mode) {
|
|
|
148
148
|
return 'Bypass all permissions';
|
|
149
149
|
if (mode === 'plan')
|
|
150
150
|
return 'Plan · read-only';
|
|
151
|
-
return 'Default ·
|
|
151
|
+
return 'Default · Ask for Approval';
|
|
152
152
|
}
|
|
153
153
|
export function agentModeLabelTiers(mode) {
|
|
154
|
-
if (mode === 'default') {
|
|
155
|
-
return [agentModeLabel(mode), 'Default · asks to edit & run'];
|
|
156
|
-
}
|
|
157
154
|
return [agentModeLabel(mode)];
|
|
158
155
|
}
|
|
159
156
|
export function agentModeShortLabel(mode) {
|
|
@@ -13,6 +13,13 @@ export class BrowserBridgeError extends Error {
|
|
|
13
13
|
this.code = code;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
function rejectDisconnectedRequests(browser) {
|
|
17
|
+
for (const pending of browser.pending.values()) {
|
|
18
|
+
clearTimeout(pending.timer);
|
|
19
|
+
pending.reject(new BrowserBridgeError(`The browser disconnected while ${pending.op} was in flight, so its outcome is unknown. Check the page before doing anything that could repeat.`, 'disconnected'));
|
|
20
|
+
}
|
|
21
|
+
browser.pending.clear();
|
|
22
|
+
}
|
|
16
23
|
function safeUnlink(target) {
|
|
17
24
|
try {
|
|
18
25
|
fs.unlinkSync(target);
|
|
@@ -28,15 +35,20 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
28
35
|
if (socketIsFile())
|
|
29
36
|
safeUnlink(socketPath);
|
|
30
37
|
const browsers = new Map();
|
|
38
|
+
const sockets = new Set();
|
|
31
39
|
const listeners = new Set();
|
|
32
40
|
let selected = null;
|
|
41
|
+
let everConnected = false;
|
|
33
42
|
let closed = false;
|
|
34
43
|
const registration = registerNativeHost({ env });
|
|
35
44
|
const server = net.createServer((socket) => {
|
|
36
45
|
socket.setNoDelay(true);
|
|
46
|
+
sockets.add(socket);
|
|
37
47
|
let buffer = '';
|
|
38
48
|
let key = null;
|
|
39
49
|
socket.on('data', (chunk) => {
|
|
50
|
+
if (socket.destroyed)
|
|
51
|
+
return;
|
|
40
52
|
buffer += chunk.toString('utf8');
|
|
41
53
|
let newline = buffer.indexOf('\n');
|
|
42
54
|
while (newline !== -1) {
|
|
@@ -62,7 +74,12 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
62
74
|
}
|
|
63
75
|
const announced = frame.browser?.key ?? crypto.randomUUID();
|
|
64
76
|
const existing = key ? browsers.get(key) : null;
|
|
65
|
-
|
|
77
|
+
const displaced = browsers.get(announced);
|
|
78
|
+
if (displaced && displaced.socket !== socket) {
|
|
79
|
+
displaced.socket.destroy();
|
|
80
|
+
rejectDisconnectedRequests(displaced);
|
|
81
|
+
}
|
|
82
|
+
if (existing?.socket === socket) {
|
|
66
83
|
if (announced !== key) {
|
|
67
84
|
browsers.delete(key);
|
|
68
85
|
browsers.set(announced, existing);
|
|
@@ -82,13 +99,14 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
82
99
|
selected = announced;
|
|
83
100
|
}
|
|
84
101
|
key = announced;
|
|
102
|
+
everConnected = true;
|
|
85
103
|
for (const listener of listeners) {
|
|
86
104
|
listener('connected', { browser: frame.browser });
|
|
87
105
|
}
|
|
88
106
|
continue;
|
|
89
107
|
}
|
|
90
108
|
const browser = key ? browsers.get(key) : null;
|
|
91
|
-
if (!browser)
|
|
109
|
+
if (!browser || browser.socket !== socket)
|
|
92
110
|
continue;
|
|
93
111
|
if (frame.t === 'res') {
|
|
94
112
|
const pending = browser.pending.get(frame.id);
|
|
@@ -109,16 +127,16 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
109
127
|
}
|
|
110
128
|
});
|
|
111
129
|
const drop = () => {
|
|
130
|
+
sockets.delete(socket);
|
|
112
131
|
if (!key)
|
|
113
132
|
return;
|
|
114
133
|
const browser = browsers.get(key);
|
|
134
|
+
if (!browser || browser.socket !== socket)
|
|
135
|
+
return;
|
|
115
136
|
browsers.delete(key);
|
|
116
137
|
if (selected === key)
|
|
117
138
|
selected = browsers.keys().next().value ?? null;
|
|
118
|
-
|
|
119
|
-
clearTimeout(pending.timer);
|
|
120
|
-
pending.reject(new BrowserBridgeError(`The browser disconnected while ${pending.op} was in flight, so its outcome is unknown. Check the page before doing anything that could repeat.`, 'disconnected'));
|
|
121
|
-
}
|
|
139
|
+
rejectDisconnectedRequests(browser);
|
|
122
140
|
for (const listener of listeners)
|
|
123
141
|
listener('disconnected', { browser: key });
|
|
124
142
|
};
|
|
@@ -152,7 +170,10 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
152
170
|
return;
|
|
153
171
|
closed = true;
|
|
154
172
|
for (const browser of browsers.values())
|
|
155
|
-
browser
|
|
173
|
+
rejectDisconnectedRequests(browser);
|
|
174
|
+
for (const socket of sockets)
|
|
175
|
+
socket.destroy();
|
|
176
|
+
sockets.clear();
|
|
156
177
|
browsers.clear();
|
|
157
178
|
server.close();
|
|
158
179
|
safeUnlink(socketPath);
|
|
@@ -164,12 +185,21 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
164
185
|
status() {
|
|
165
186
|
return {
|
|
166
187
|
connected: browsers.size > 0,
|
|
188
|
+
everConnected,
|
|
167
189
|
browsers: [...browsers.values()].map((browser) => browser.identity),
|
|
168
190
|
selected,
|
|
169
191
|
socketPath,
|
|
170
192
|
registrationErrors: registration.errors,
|
|
171
193
|
};
|
|
172
194
|
},
|
|
195
|
+
resync() {
|
|
196
|
+
if (closed || browsers.size > 0 || sockets.size === 0)
|
|
197
|
+
return false;
|
|
198
|
+
for (const socket of sockets)
|
|
199
|
+
socket.destroy();
|
|
200
|
+
sockets.clear();
|
|
201
|
+
return true;
|
|
202
|
+
},
|
|
173
203
|
selectBrowser(key) {
|
|
174
204
|
if (browsers.has(key))
|
|
175
205
|
selected = key;
|
|
@@ -201,9 +201,19 @@ export function chromiumProcessRunning(platform = process.platform) {
|
|
|
201
201
|
timeout: 2000,
|
|
202
202
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
203
203
|
});
|
|
204
|
-
return
|
|
204
|
+
return output.split('\n').some((line) => isBrowserProcessName(line));
|
|
205
205
|
}
|
|
206
206
|
catch {
|
|
207
207
|
return false;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
+
const BROWSER_PROCESS_NAME = /^(google[ -]?chrome|chrome|chromium|brave|msedge|microsoft[ -]?edge|vivaldi)/i;
|
|
211
|
+
const NOT_A_BROWSER_PROCESS = /crashpad|crashhandler|native|sandbox|driver|updater?\b/i;
|
|
212
|
+
export function isBrowserProcessName(psLine) {
|
|
213
|
+
const name = psLine.trim().split('/').pop()?.trim() ?? '';
|
|
214
|
+
if (!name)
|
|
215
|
+
return false;
|
|
216
|
+
if (NOT_A_BROWSER_PROCESS.test(name))
|
|
217
|
+
return false;
|
|
218
|
+
return BROWSER_PROCESS_NAME.test(name);
|
|
219
|
+
}
|
|
@@ -86,19 +86,31 @@ export function browserAvailability(options) {
|
|
|
86
86
|
if (status.connected) {
|
|
87
87
|
return {
|
|
88
88
|
connected: true,
|
|
89
|
+
everConnected: true,
|
|
89
90
|
browsers: status.browsers,
|
|
90
91
|
selected: status.selected,
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
const hint = status.registrationErrors.length
|
|
94
95
|
? `TheGitAI could not register its browser bridge with Chrome: ${status.registrationErrors[0]}`
|
|
95
|
-
:
|
|
96
|
-
? '
|
|
97
|
-
:
|
|
96
|
+
: status.everConnected
|
|
97
|
+
? 'The browser was connected to this session and dropped. Nothing needs reinstalling — it reconnects on its own, usually within a second or two. If it does not, open the TheGitAI icon in Chrome.'
|
|
98
|
+
: chromiumProcessRunning()
|
|
99
|
+
? 'Chrome is running but the TheGitAI extension has not connected. Install or enable the extension, then reload it from chrome://extensions.'
|
|
100
|
+
: 'Chrome does not appear to be running. Open Chrome with the TheGitAI extension installed.';
|
|
98
101
|
return {
|
|
99
102
|
connected: false,
|
|
103
|
+
everConnected: status.everConnected,
|
|
100
104
|
browsers: [],
|
|
101
105
|
selected: null,
|
|
102
106
|
hint,
|
|
103
107
|
};
|
|
104
108
|
}
|
|
109
|
+
export function resyncBrowserBridge(options) {
|
|
110
|
+
try {
|
|
111
|
+
return ensureBrowserBridge(options).resync();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/src/help-text.js
CHANGED
|
@@ -46,10 +46,12 @@ const HELP_MARKDOWN = [
|
|
|
46
46
|
'## Updates',
|
|
47
47
|
'',
|
|
48
48
|
'- On start, `ai` says when a newer release is published and offers to install',
|
|
49
|
-
' it. TheGitAI
|
|
50
|
-
'
|
|
51
|
-
'-
|
|
52
|
-
'
|
|
49
|
+
' it. TheGitAI works best on the latest version, and an older CLI may not',
|
|
50
|
+
' work optimally.',
|
|
51
|
+
'- **Update now** is selected when the offer appears, so **Enter** takes the',
|
|
52
|
+
' update; **↑/↓** moves to **Skip**, and the number keys still jump straight',
|
|
53
|
+
' to a row. The CLI installs the update and asks you to start `ai` again, so',
|
|
54
|
+
' the whole session runs one version.',
|
|
53
55
|
'- Skipping starts the session on the version you have and asks again next',
|
|
54
56
|
' time. A release cannot be silenced for good — that is what keeps a broken',
|
|
55
57
|
' combination from lasting.',
|
|
@@ -64,10 +66,10 @@ const HELP_MARKDOWN = [
|
|
|
64
66
|
'',
|
|
65
67
|
'## Modes',
|
|
66
68
|
'',
|
|
67
|
-
'- Default ·
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
69
|
+
'- Default · Ask for Approval — asks before creating, editing or deleting',
|
|
70
|
+
' files, before running commands, and before touching a website. Each answer',
|
|
71
|
+
' can be remembered for the rest of the session, and once it is, Default',
|
|
72
|
+
' stops asking for that action; see Safety & approvals.',
|
|
71
73
|
'- Bypass all permissions — approves every command, file edit, deletion and',
|
|
72
74
|
' browser action for the rest of the session, without asking. Shown in red',
|
|
73
75
|
' in the footer. Blocked commands stay blocked.',
|
|
@@ -117,6 +119,11 @@ const HELP_MARKDOWN = [
|
|
|
117
119
|
'',
|
|
118
120
|
'## Chat commands',
|
|
119
121
|
'',
|
|
122
|
+
'- Type `/` to open the command picker. It ranks what you type, so a typo or',
|
|
123
|
+
' a stray plural (`/moddel`, `/models`) still finds the command, and a word',
|
|
124
|
+
' from the description does too (`/bug` finds `/report`). It shows the five',
|
|
125
|
+
' best matches and counts the rest; **↑/↓** chooses, **Tab** or **Enter**',
|
|
126
|
+
' accepts, **Esc** closes it.',
|
|
120
127
|
'- `/help` — show this help',
|
|
121
128
|
'- `/about` — show version and platform info',
|
|
122
129
|
'- `/report` — file a bug privately through the TheGitAI website in your',
|
|
@@ -3,11 +3,13 @@ import path from 'node:path';
|
|
|
3
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
4
4
|
import { BrowserBridgeError } from '../browser/bridge.js';
|
|
5
5
|
import { storeSessionImageBytes } from '../core/session-image-store.js';
|
|
6
|
-
import { browserAvailability, drainBrowserEvents, ensureBrowserBridge, } from '../browser/session-bridge.js';
|
|
6
|
+
import { browserAvailability, drainBrowserEvents, ensureBrowserBridge, resyncBrowserBridge, } from '../browser/session-bridge.js';
|
|
7
7
|
import { ensurePermission, originForUrl } from '../permissions.js';
|
|
8
8
|
const READ_TIMEOUT_MS = 20000;
|
|
9
9
|
const NAVIGATE_TIMEOUT_MS = 45000;
|
|
10
10
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const RECONNECT_GRACE_MS = 2500;
|
|
12
|
+
const SCAN_GRACE_MS = 1200;
|
|
11
13
|
export const READ_ONLY_BROWSER_TOOLS = new Set([
|
|
12
14
|
'browser',
|
|
13
15
|
'browser_tabs',
|
|
@@ -31,13 +33,26 @@ function missing(argument, tool) {
|
|
|
31
33
|
},
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
async function waitForBrowser(options, timeoutMs) {
|
|
37
|
+
const deadline = Date.now() + timeoutMs;
|
|
38
|
+
let availability = browserAvailability(options);
|
|
39
|
+
while (!availability.connected && Date.now() < deadline) {
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
41
|
+
availability = browserAvailability(options);
|
|
42
|
+
}
|
|
43
|
+
return availability;
|
|
44
|
+
}
|
|
45
|
+
async function notConnected(context) {
|
|
46
|
+
const options = { cwd: context.rootDir, env: context.env };
|
|
47
|
+
let availability = browserAvailability(options);
|
|
39
48
|
if (availability.connected)
|
|
40
49
|
return { ok: true };
|
|
50
|
+
if (availability.everConnected) {
|
|
51
|
+
const grace = resyncBrowserBridge(options) ? RECONNECT_GRACE_MS : SCAN_GRACE_MS;
|
|
52
|
+
availability = await waitForBrowser(options, grace);
|
|
53
|
+
if (availability.connected)
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
41
56
|
return {
|
|
42
57
|
ok: false,
|
|
43
58
|
error: `TheGitAI is not connected to a browser. ${availability.hint ?? ''}`.trim(),
|
|
@@ -45,7 +60,9 @@ function notConnected(context) {
|
|
|
45
60
|
failureDetails: {
|
|
46
61
|
category: 'external_service',
|
|
47
62
|
tool: 'browser',
|
|
48
|
-
action:
|
|
63
|
+
action: availability.everConnected
|
|
64
|
+
? 'The link to a browser that was working dropped and did not come back. Nothing is missing and nothing needs installing, so do not send the user to reinstall anything. Say what happened, and that reopening the TheGitAI icon in Chrome restores it. Do not retry in a loop.'
|
|
65
|
+
: 'Report the connection instructions. The extension is a separate installation. Do not retry in a loop or invent a download URL.',
|
|
49
66
|
},
|
|
50
67
|
};
|
|
51
68
|
}
|
|
@@ -106,10 +123,10 @@ function attachEvents(result) {
|
|
|
106
123
|
};
|
|
107
124
|
}
|
|
108
125
|
export async function browserGateway(context) {
|
|
126
|
+
const guard = await notConnected(context);
|
|
127
|
+
if (guard.ok === false)
|
|
128
|
+
return guard;
|
|
109
129
|
const availability = browserAvailability({ cwd: context.rootDir, env: context.env });
|
|
110
|
-
if (!availability.connected) {
|
|
111
|
-
return notConnected(context);
|
|
112
|
-
}
|
|
113
130
|
const platform = availability.browsers[0]?.platform ?? process.platform;
|
|
114
131
|
return {
|
|
115
132
|
ok: true,
|
|
@@ -125,7 +142,7 @@ export async function browserGateway(context) {
|
|
|
125
142
|
};
|
|
126
143
|
}
|
|
127
144
|
export async function browserTabs(context, args) {
|
|
128
|
-
const guard = notConnected(context);
|
|
145
|
+
const guard = await notConnected(context);
|
|
129
146
|
if (guard.ok === false)
|
|
130
147
|
return guard;
|
|
131
148
|
try {
|
|
@@ -139,7 +156,7 @@ export async function browserTabs(context, args) {
|
|
|
139
156
|
}
|
|
140
157
|
}
|
|
141
158
|
export async function browserTabNew(context) {
|
|
142
|
-
const guard = notConnected(context);
|
|
159
|
+
const guard = await notConnected(context);
|
|
143
160
|
if (guard.ok === false)
|
|
144
161
|
return guard;
|
|
145
162
|
try {
|
|
@@ -151,7 +168,7 @@ export async function browserTabNew(context) {
|
|
|
151
168
|
}
|
|
152
169
|
}
|
|
153
170
|
export async function browserTabClose(context, args) {
|
|
154
|
-
const guard = notConnected(context);
|
|
171
|
+
const guard = await notConnected(context);
|
|
155
172
|
if (guard.ok === false)
|
|
156
173
|
return guard;
|
|
157
174
|
const tabId = Number(args.tabId ?? args.tab_id);
|
|
@@ -166,7 +183,7 @@ export async function browserTabClose(context, args) {
|
|
|
166
183
|
}
|
|
167
184
|
}
|
|
168
185
|
export async function browserNavigate(context, args) {
|
|
169
|
-
const guard = notConnected(context);
|
|
186
|
+
const guard = await notConnected(context);
|
|
170
187
|
if (guard.ok === false)
|
|
171
188
|
return guard;
|
|
172
189
|
const url = String(args.url ?? '').trim();
|
|
@@ -199,7 +216,7 @@ export async function browserNavigate(context, args) {
|
|
|
199
216
|
}
|
|
200
217
|
}
|
|
201
218
|
async function gatedTabCall(context, { tool, op, args, summary, timeoutMs = READ_TIMEOUT_MS, }) {
|
|
202
|
-
const guard = notConnected(context);
|
|
219
|
+
const guard = await notConnected(context);
|
|
203
220
|
if (guard.ok === false)
|
|
204
221
|
return guard;
|
|
205
222
|
const tabId = Number(args.tabId ?? args.tab_id);
|
|
@@ -389,7 +406,7 @@ export async function browserDownloads(context, args) {
|
|
|
389
406
|
error: `Plan mode blocked browser_downloads ${action}. Listing downloads is allowed; saving or cancelling one changes the user's disk. Switch to Default mode to decide on a download.`,
|
|
390
407
|
};
|
|
391
408
|
}
|
|
392
|
-
const guard = notConnected(context);
|
|
409
|
+
const guard = await notConnected(context);
|
|
393
410
|
if (guard.ok === false)
|
|
394
411
|
return guard;
|
|
395
412
|
if (action === 'list') {
|
|
@@ -459,7 +476,7 @@ export function browserResize(context, args) {
|
|
|
459
476
|
});
|
|
460
477
|
}
|
|
461
478
|
export async function browserFileUpload(context, args) {
|
|
462
|
-
const guard = notConnected(context);
|
|
479
|
+
const guard = await notConnected(context);
|
|
463
480
|
if (guard.ok === false)
|
|
464
481
|
return guard;
|
|
465
482
|
const ref = String(args.ref ?? '').trim();
|
|
@@ -580,7 +597,7 @@ const BATCH_TOOLS = {
|
|
|
580
597
|
browser_downloads: browserDownloads,
|
|
581
598
|
};
|
|
582
599
|
export async function browserBatch(context, args) {
|
|
583
|
-
const guard = notConnected(context);
|
|
600
|
+
const guard = await notConnected(context);
|
|
584
601
|
if (guard.ok === false)
|
|
585
602
|
return guard;
|
|
586
603
|
const actions = Array.isArray(args.actions) ? args.actions : [];
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -7,7 +7,8 @@ import { backendFor, createNotificationQueue, } from './tui/desktop-notification
|
|
|
7
7
|
import { readPreferences, writePreferences } from '../preferences.js';
|
|
8
8
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
9
9
|
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
10
|
-
|
|
10
|
+
import { getInputCommandToken, getSlashCommandSuggestions, SLASH_COMMANDS, slashCommandMatchesToken, } from './slash-commands.js';
|
|
11
|
+
export { getInputCommandToken, getSlashCommandSuggestions, SLASH_COMMANDS, slashCommandMatchesToken, };
|
|
11
12
|
import { composerInputWidth, frameContentWidth } from './tui/composer-layout.js';
|
|
12
13
|
import { agentModeLabel, nextAgentMode, } from '../agent-mode.js';
|
|
13
14
|
import { chat, models } from '../api/index.js';
|
|
@@ -110,52 +111,6 @@ export function buildTurnFailureHintEntries(cancelled) {
|
|
|
110
111
|
},
|
|
111
112
|
];
|
|
112
113
|
}
|
|
113
|
-
export const SLASH_COMMANDS = [
|
|
114
|
-
{
|
|
115
|
-
command: '/help',
|
|
116
|
-
description: 'Show available chat commands',
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
command: '/about',
|
|
120
|
-
description: 'Show version and platform info',
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
command: '/report',
|
|
124
|
-
description: 'Report a bug privately on the TheGitAI website',
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
command: '/usage',
|
|
128
|
-
description: 'Show account usage percentage and reset times',
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
command: '/model',
|
|
132
|
-
description: 'Switch the active model',
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
command: '/resume',
|
|
136
|
-
description: 'Open the session picker',
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
command: '/jobs',
|
|
140
|
-
description: 'Background jobs: pick to view output or kill',
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
command: '/notify',
|
|
144
|
-
description: 'Toggle desktop notifications when a turn ends (Ctrl+G)',
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
command: '/new',
|
|
148
|
-
description: 'start a new conversation; this session remains saved',
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
command: '/logout',
|
|
152
|
-
description: 'Sign out and quit',
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
command: '/exit',
|
|
156
|
-
description: 'Quit the current session',
|
|
157
|
-
},
|
|
158
|
-
];
|
|
159
114
|
export function composerAttachmentsAfterCancel(queuedAttachments, inFlightAttachments) {
|
|
160
115
|
const seen = new Set();
|
|
161
116
|
const kept = [];
|
|
@@ -1255,16 +1210,6 @@ export const EXIT_CTRL_C_CONFIRM_MS = 3000;
|
|
|
1255
1210
|
export function isExitConfirmActive(exitConfirmUntil, now = Date.now()) {
|
|
1256
1211
|
return exitConfirmUntil != null && now < exitConfirmUntil;
|
|
1257
1212
|
}
|
|
1258
|
-
export function getInputCommandToken(input) {
|
|
1259
|
-
const trimmed = String(input ?? '').trim();
|
|
1260
|
-
if (!trimmed.startsWith('/'))
|
|
1261
|
-
return '';
|
|
1262
|
-
const firstSpaceIndex = trimmed.indexOf(' ');
|
|
1263
|
-
const token = firstSpaceIndex === -1 ? trimmed : trimmed.slice(0, firstSpaceIndex);
|
|
1264
|
-
if (token.indexOf('/', 1) !== -1)
|
|
1265
|
-
return '';
|
|
1266
|
-
return token;
|
|
1267
|
-
}
|
|
1268
1213
|
function shouldShowCommandPalette(state) {
|
|
1269
1214
|
const trimmed = String(state.input ?? '').trim();
|
|
1270
1215
|
return (!state.busy &&
|
|
@@ -1272,8 +1217,9 @@ function shouldShowCommandPalette(state) {
|
|
|
1272
1217
|
!state.modelPickerOpen &&
|
|
1273
1218
|
!state.resumePickerOpen &&
|
|
1274
1219
|
trimmed.startsWith('/') &&
|
|
1275
|
-
!
|
|
1276
|
-
getInputCommandToken(trimmed) !== ''
|
|
1220
|
+
!String(state.input ?? '').includes(' ') &&
|
|
1221
|
+
getInputCommandToken(trimmed) !== '' &&
|
|
1222
|
+
getSlashCommandSuggestions(trimmed).length > 0);
|
|
1277
1223
|
}
|
|
1278
1224
|
export function shouldRemountLiveFrameForComposerInputChange(current, nextInput) {
|
|
1279
1225
|
const currentShowsCommands = shouldShowCommandPalette(current);
|
|
@@ -1296,9 +1242,6 @@ export function applySlashCommandSuggestion(currentInput, suggestion) {
|
|
|
1296
1242
|
input: `${before}${suggestion.command}${after}`,
|
|
1297
1243
|
};
|
|
1298
1244
|
}
|
|
1299
|
-
export function isExactSlashCommandToken(token) {
|
|
1300
|
-
return SLASH_COMMANDS.some((command) => command.command === token);
|
|
1301
|
-
}
|
|
1302
1245
|
export function insertAtCursor(state, text) {
|
|
1303
1246
|
const before = state.input.slice(0, state.cursor);
|
|
1304
1247
|
const after = state.input.slice(state.cursor);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const SLASH_COMMANDS = [
|
|
2
|
+
{ command: '/model', description: 'Switch the active model' },
|
|
3
|
+
{ command: '/help', description: 'Show available chat commands' },
|
|
4
|
+
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
5
|
+
{
|
|
6
|
+
command: '/resume',
|
|
7
|
+
description: 'Open the session picker to resume a previous session',
|
|
8
|
+
},
|
|
9
|
+
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
10
|
+
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
11
|
+
{
|
|
12
|
+
command: '/notify',
|
|
13
|
+
description: 'Toggle desktop notifications when a turn ends (Ctrl+G)',
|
|
14
|
+
},
|
|
15
|
+
{ command: '/report', description: 'Report a bug privately on the TheGitAI website' },
|
|
16
|
+
{ command: '/about', description: 'Show version and platform info' },
|
|
17
|
+
{ command: '/logout', description: 'Sign out and quit' },
|
|
18
|
+
{ command: '/exit', description: 'Quit the current session', aliases: ['/quit'] },
|
|
19
|
+
];
|
|
20
|
+
export const SLASH_COMMAND_SUGGESTION_LIMIT = 5;
|
|
21
|
+
export function getInputCommandToken(input) {
|
|
22
|
+
const trimmed = String(input ?? '').trimStart();
|
|
23
|
+
const match = trimmed.match(/^\/[^\s]*/);
|
|
24
|
+
const token = match?.[0] ?? '';
|
|
25
|
+
if (token.indexOf('/', 1) !== -1)
|
|
26
|
+
return '';
|
|
27
|
+
return token;
|
|
28
|
+
}
|
|
29
|
+
export function slashCommandMatchesToken(option, token) {
|
|
30
|
+
return option.command === token || (option.aliases?.includes(token) ?? false);
|
|
31
|
+
}
|
|
32
|
+
const MATCH_EXACT = 0;
|
|
33
|
+
const MATCH_PREFIX = 100;
|
|
34
|
+
const MATCH_OVERTYPED = 200;
|
|
35
|
+
const MATCH_TYPO = 300;
|
|
36
|
+
const MATCH_IN_NAME = 400;
|
|
37
|
+
const MATCH_IN_DESCRIPTION = 500;
|
|
38
|
+
const MATCH_NONE = 600;
|
|
39
|
+
function typoBudget(body) {
|
|
40
|
+
if (body.length <= 3)
|
|
41
|
+
return 0;
|
|
42
|
+
if (body.length <= 5)
|
|
43
|
+
return 1;
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
function editDistanceWithin(a, b, max) {
|
|
47
|
+
if (a === b)
|
|
48
|
+
return 0;
|
|
49
|
+
if (max <= 0)
|
|
50
|
+
return null;
|
|
51
|
+
if (Math.abs(a.length - b.length) > max)
|
|
52
|
+
return null;
|
|
53
|
+
const width = b.length + 1;
|
|
54
|
+
let beforePrevious = [];
|
|
55
|
+
let previous = Array.from({ length: width }, (_, index) => index);
|
|
56
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
57
|
+
const current = new Array(width);
|
|
58
|
+
current[0] = i;
|
|
59
|
+
let best = i;
|
|
60
|
+
for (let j = 1; j < width; j += 1) {
|
|
61
|
+
const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
62
|
+
let cost = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + substitution);
|
|
63
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
64
|
+
cost = Math.min(cost, beforePrevious[j - 2] + 1);
|
|
65
|
+
}
|
|
66
|
+
current[j] = cost;
|
|
67
|
+
if (cost < best)
|
|
68
|
+
best = cost;
|
|
69
|
+
}
|
|
70
|
+
if (best > max)
|
|
71
|
+
return null;
|
|
72
|
+
beforePrevious = previous;
|
|
73
|
+
previous = current;
|
|
74
|
+
}
|
|
75
|
+
const distance = previous[b.length];
|
|
76
|
+
return distance <= max ? distance : null;
|
|
77
|
+
}
|
|
78
|
+
function scoreSlashCommand(option, token) {
|
|
79
|
+
const command = option.command;
|
|
80
|
+
const body = token.slice(1);
|
|
81
|
+
if (!body)
|
|
82
|
+
return MATCH_PREFIX;
|
|
83
|
+
const spellings = [command, ...(option.aliases ?? [])];
|
|
84
|
+
if (slashCommandMatchesToken(option, token))
|
|
85
|
+
return MATCH_EXACT;
|
|
86
|
+
if (spellings.some((spelling) => spelling.startsWith(token)))
|
|
87
|
+
return MATCH_PREFIX;
|
|
88
|
+
const overtyped = spellings.find((spelling) => token.startsWith(spelling));
|
|
89
|
+
if (overtyped) {
|
|
90
|
+
return MATCH_OVERTYPED + Math.min(token.length - overtyped.length, 99);
|
|
91
|
+
}
|
|
92
|
+
const distances = spellings
|
|
93
|
+
.map((spelling) => editDistanceWithin(body, spelling.slice(1), typoBudget(body)))
|
|
94
|
+
.filter((distance) => distance !== null);
|
|
95
|
+
if (distances.length)
|
|
96
|
+
return MATCH_TYPO + Math.min(...distances);
|
|
97
|
+
if (command.includes(body))
|
|
98
|
+
return MATCH_IN_NAME;
|
|
99
|
+
if (option.description.toLowerCase().includes(body))
|
|
100
|
+
return MATCH_IN_DESCRIPTION;
|
|
101
|
+
return MATCH_NONE;
|
|
102
|
+
}
|
|
103
|
+
function rankSlashCommands(input) {
|
|
104
|
+
const token = getInputCommandToken(input).toLowerCase();
|
|
105
|
+
if (!token)
|
|
106
|
+
return [];
|
|
107
|
+
return SLASH_COMMANDS.map((option, order) => ({
|
|
108
|
+
option,
|
|
109
|
+
order,
|
|
110
|
+
score: scoreSlashCommand(option, token),
|
|
111
|
+
}))
|
|
112
|
+
.filter((entry) => entry.score < MATCH_NONE)
|
|
113
|
+
.sort((a, b) => a.score - b.score || a.order - b.order)
|
|
114
|
+
.map((entry) => entry.option);
|
|
115
|
+
}
|
|
116
|
+
export function getSlashCommandSuggestions(input) {
|
|
117
|
+
return rankSlashCommands(input).slice(0, SLASH_COMMAND_SUGGESTION_LIMIT);
|
|
118
|
+
}
|
|
119
|
+
export function getHiddenSlashCommandCount(input) {
|
|
120
|
+
return Math.max(0, rankSlashCommands(input).length - SLASH_COMMAND_SUGGESTION_LIMIT);
|
|
121
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { agentModeColor, agentModeLabelTiers, agentModeShortLabel, } from '../../agent-mode.js';
|
|
2
2
|
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
|
+
import { getHiddenSlashCommandCount, getSlashCommandSuggestions, } from '../slash-commands.js';
|
|
4
5
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
6
|
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
7
|
import { isDarkTerminalBackground, mutedColor, mutedStyle } from './terminal-theme.js';
|
|
@@ -189,19 +190,6 @@ export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HO
|
|
|
189
190
|
export function formatPromptSessionIdLabel(showSessionId, sessionId) {
|
|
190
191
|
return showSessionId ? String(sessionId ?? '').trim() : '';
|
|
191
192
|
}
|
|
192
|
-
const CLIENT_SLASH_COMMANDS = [
|
|
193
|
-
{ command: '/help', description: 'Show available chat commands' },
|
|
194
|
-
{ command: '/about', description: 'Show version and platform info' },
|
|
195
|
-
{ command: '/usage', description: 'Show account usage percentage and reset times' },
|
|
196
|
-
{ command: '/model', description: 'Switch the active model' },
|
|
197
|
-
{ command: '/resume', description: 'Open the session picker to resume a previous session' },
|
|
198
|
-
{ command: '/jobs', description: 'Background jobs: pick to view output or kill' },
|
|
199
|
-
{ command: '/notify', description: 'Toggle desktop notifications when a turn ends (Ctrl+G)' },
|
|
200
|
-
{ command: '/report', description: 'Report a bug privately on the TheGitAI website' },
|
|
201
|
-
{ command: '/new', description: 'start a new conversation; this session remains saved' },
|
|
202
|
-
{ command: '/logout', description: 'Sign out and quit' },
|
|
203
|
-
{ command: '/exit', description: 'Quit the current session' },
|
|
204
|
-
];
|
|
205
193
|
function buildModelPickerOptions(currentModelId, serverModels) {
|
|
206
194
|
return serverModels.map((model) => ({
|
|
207
195
|
id: model.id,
|
|
@@ -213,38 +201,7 @@ function buildModelPickerOptions(currentModelId, serverModels) {
|
|
|
213
201
|
note: model.description,
|
|
214
202
|
}));
|
|
215
203
|
}
|
|
216
|
-
|
|
217
|
-
const trimmed = String(input ?? '').trimStart();
|
|
218
|
-
const match = trimmed.match(/^\/[^\s]*/);
|
|
219
|
-
const token = match?.[0] ?? '';
|
|
220
|
-
if (token.indexOf('/', 1) !== -1)
|
|
221
|
-
return '';
|
|
222
|
-
return token;
|
|
223
|
-
}
|
|
224
|
-
function scoreSlashCommand(option, token) {
|
|
225
|
-
if (option.command === token)
|
|
226
|
-
return 0;
|
|
227
|
-
if (option.command.startsWith(token))
|
|
228
|
-
return 1;
|
|
229
|
-
if (option.command.includes(token.slice(1)))
|
|
230
|
-
return 2;
|
|
231
|
-
if (option.description.toLowerCase().includes(token.slice(1)))
|
|
232
|
-
return 3;
|
|
233
|
-
return 4;
|
|
234
|
-
}
|
|
235
|
-
export function getSlashCommandSuggestions(input) {
|
|
236
|
-
const token = getInputCommandToken(input).toLowerCase();
|
|
237
|
-
if (!token)
|
|
238
|
-
return [];
|
|
239
|
-
const ranked = [...CLIENT_SLASH_COMMANDS].sort((a, b) => {
|
|
240
|
-
const scoreDiff = scoreSlashCommand(a, token) - scoreSlashCommand(b, token);
|
|
241
|
-
if (scoreDiff !== 0)
|
|
242
|
-
return scoreDiff;
|
|
243
|
-
return a.command.localeCompare(b.command);
|
|
244
|
-
});
|
|
245
|
-
const matches = ranked.filter((option) => scoreSlashCommand(option, token) < 4);
|
|
246
|
-
return (matches.length ? matches : ranked).slice(0, 5);
|
|
247
|
-
}
|
|
204
|
+
export { getSlashCommandSuggestions };
|
|
248
205
|
function formatModelLabel(modelId, serverModels) {
|
|
249
206
|
return serverModels.find((model) => model.id === modelId)?.label ?? 'Unknown model';
|
|
250
207
|
}
|
|
@@ -1061,7 +1018,7 @@ function commandPaletteItemLines(option, selected, innerWidth) {
|
|
|
1061
1018
|
modelPickerPanelSideLine(line(span(`${MODEL_PICKER_META_INDENT}${option.description}`, { color: 'gray' })), innerWidth),
|
|
1062
1019
|
];
|
|
1063
1020
|
}
|
|
1064
|
-
function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
1021
|
+
function buildCommandPalettePanel(suggestions, selectedIndex, hiddenCount, width) {
|
|
1065
1022
|
const panelWidth = Math.max(28, Math.min(width, MODEL_PICKER_PANEL_MAX_WIDTH));
|
|
1066
1023
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
1067
1024
|
const margin = Array.from({ length: MODEL_PICKER_PANEL_MARGIN_LINES }, () => plainLine(''));
|
|
@@ -1080,6 +1037,12 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
1080
1037
|
body.push(commandPaletteSeparatorLine(innerWidth));
|
|
1081
1038
|
}
|
|
1082
1039
|
});
|
|
1040
|
+
if (hiddenCount > 0) {
|
|
1041
|
+
body.push(commandPaletteSeparatorLine(innerWidth), modelPickerPanelSideLine(plainLine(` ${hiddenCount} more • keep typing to narrow`, {
|
|
1042
|
+
color: 'gray',
|
|
1043
|
+
dim: true,
|
|
1044
|
+
}), innerWidth));
|
|
1045
|
+
}
|
|
1083
1046
|
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Tab or Enter accept • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
1084
1047
|
return [...margin, ...body, ...margin];
|
|
1085
1048
|
}
|
|
@@ -1329,7 +1292,7 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1329
1292
|
state.input.trimStart().startsWith('/') &&
|
|
1330
1293
|
!state.input.includes(' ') &&
|
|
1331
1294
|
suggestions.length > 0) {
|
|
1332
|
-
lines.push(...buildCommandPalettePanel(suggestions, state.commandCursor, width));
|
|
1295
|
+
lines.push(...buildCommandPalettePanel(suggestions, state.commandCursor, getHiddenSlashCommandCount(state.input), width));
|
|
1333
1296
|
}
|
|
1334
1297
|
return lines;
|
|
1335
1298
|
}
|
|
@@ -3,7 +3,7 @@ import { moveComposerCursorVertically } from './composer-layout.js';
|
|
|
3
3
|
import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
|
|
4
4
|
import { MAX_IMAGES_PER_MESSAGE, MAX_TOTAL_IMAGE_BYTES_PER_MESSAGE, approximateBase64DecodedBytes, totalAttachmentBytes, } from '../../core/image-limits.js';
|
|
5
5
|
import { tryCacheAttachmentBytes } from '../../core/session-image-store.js';
|
|
6
|
-
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor,
|
|
6
|
+
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, navigatePromptHistory, shouldRemountLiveFrameForComposerInputChange, slashCommandMatchesToken, } from '../repl.js';
|
|
7
7
|
import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
|
|
8
8
|
import { handleUserInputPromptEvent, } from './user-input.js';
|
|
9
9
|
const APPROVAL_PREVIEW_PAGE_ROWS = 3;
|
|
@@ -56,7 +56,9 @@ function shouldShowCommandPalette(state) {
|
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
58
|
const token = getInputCommandToken(state.input);
|
|
59
|
-
|
|
59
|
+
if (!token.startsWith('/') || state.input.includes(' '))
|
|
60
|
+
return false;
|
|
61
|
+
return getSlashCommandSuggestions(state.input).length > 0;
|
|
60
62
|
}
|
|
61
63
|
function insertPastedText(store, handlers, text) {
|
|
62
64
|
const current = store.getState();
|
|
@@ -549,10 +551,10 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
549
551
|
}
|
|
550
552
|
if (key.returnKey) {
|
|
551
553
|
const token = getInputCommandToken(state.input);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
554
|
+
const suggestion = commandSuggestions[Math.min(state.commandCursor, commandSuggestions.length - 1)];
|
|
555
|
+
if (commandPaletteActive &&
|
|
556
|
+
suggestion &&
|
|
557
|
+
!slashCommandMatchesToken(suggestion, token)) {
|
|
556
558
|
store.update((current) => {
|
|
557
559
|
const nextInput = applySlashCommandSuggestion(current.input, suggestion);
|
|
558
560
|
prepareForComposerInputChange(current, nextInput.input);
|
package/dist/src/update-check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { emitKeypressEvents } from 'node:readline';
|
|
4
5
|
import readline from 'node:readline/promises';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import chalk from './colors.js';
|
|
@@ -35,21 +36,55 @@ export function buildUpdateOffer({ currentVersion, latestVersion, moduleDir, })
|
|
|
35
36
|
display: [program, ...args].join(' '),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
|
-
export
|
|
39
|
+
export const UPDATE_OPTIONS = ['update', 'skip'];
|
|
40
|
+
const UPDATE_KEY_HINT = '↑/↓ choose • Enter accepts • Ctrl+C quits';
|
|
41
|
+
function formatUpdateHeader(offer) {
|
|
39
42
|
return [
|
|
40
43
|
'',
|
|
41
44
|
` ${chalk.yellow('✨ Update available')} — ${chalk.dim(offer.currentVersion)} → ${chalk.bold(offer.latestVersion)}`,
|
|
42
45
|
'',
|
|
43
|
-
' TheGitAI
|
|
44
|
-
'
|
|
46
|
+
' TheGitAI works best with the latest version. TheGitAI may not work',
|
|
47
|
+
' optimally if not updated.',
|
|
45
48
|
'',
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
export function formatUpdateOptions(offer, selectedIndex = 0) {
|
|
52
|
+
const rows = [
|
|
53
|
+
{ note: `(runs \`${offer.display}\`)`, text: 'Update now' },
|
|
54
|
+
{ note: '(Agent may not work optimally)', text: 'Skip' },
|
|
55
|
+
];
|
|
56
|
+
return [
|
|
57
|
+
...rows.map(({ note, text }, index) => index === selectedIndex
|
|
58
|
+
? ` ${chalk.cyan(`› ${index + 1}. ${text}`)} ${chalk.dim(note)}`
|
|
59
|
+
: ` ${chalk.dim(`${index + 1}.`)} ${text} ${chalk.dim(note)}`),
|
|
48
60
|
'',
|
|
49
|
-
` ${chalk.dim(
|
|
61
|
+
` ${chalk.dim(UPDATE_KEY_HINT)}`,
|
|
50
62
|
'',
|
|
51
63
|
].join('\n');
|
|
52
64
|
}
|
|
65
|
+
export function formatUpdateScreen(offer, selectedIndex = 0) {
|
|
66
|
+
return `${formatUpdateHeader(offer)}\n${formatUpdateOptions(offer, selectedIndex)}`;
|
|
67
|
+
}
|
|
68
|
+
export function readUpdateKey(key, selectedIndex) {
|
|
69
|
+
const name = key.name ?? '';
|
|
70
|
+
if (key.ctrl && (name === 'c' || name === 'd')) {
|
|
71
|
+
return { kind: 'answer', answer: 'cancel' };
|
|
72
|
+
}
|
|
73
|
+
if (name === 'return' || name === 'enter') {
|
|
74
|
+
return { kind: 'answer', answer: UPDATE_OPTIONS[selectedIndex] ?? 'update' };
|
|
75
|
+
}
|
|
76
|
+
if (name === 'up' || (name === 'tab' && key.sequence === '\u001b[Z')) {
|
|
77
|
+
return { kind: 'select', index: Math.max(selectedIndex - 1, 0) };
|
|
78
|
+
}
|
|
79
|
+
if (name === 'down' || name === 'tab') {
|
|
80
|
+
return { kind: 'select', index: Math.min(selectedIndex + 1, UPDATE_OPTIONS.length - 1) };
|
|
81
|
+
}
|
|
82
|
+
const digit = Number.parseInt(key.sequence ?? '', 10);
|
|
83
|
+
if (Number.isInteger(digit) && digit >= 1 && digit <= UPDATE_OPTIONS.length) {
|
|
84
|
+
return { kind: 'answer', answer: UPDATE_OPTIONS[digit - 1] };
|
|
85
|
+
}
|
|
86
|
+
return { kind: 'ignore' };
|
|
87
|
+
}
|
|
53
88
|
export function readUpdateChoice(answer) {
|
|
54
89
|
const value = answer.trim().toLowerCase();
|
|
55
90
|
if (value === '' || value === '1' || value === 'y' || value === 'yes') {
|
|
@@ -67,7 +102,56 @@ export async function resolveUpdateAnswer(question) {
|
|
|
67
102
|
throw error;
|
|
68
103
|
}
|
|
69
104
|
}
|
|
70
|
-
|
|
105
|
+
export function renderedRowCount(text, columns) {
|
|
106
|
+
const width = Math.max(1, columns);
|
|
107
|
+
const lines = text.split('\n');
|
|
108
|
+
if (lines[lines.length - 1] === '')
|
|
109
|
+
lines.pop();
|
|
110
|
+
return lines.reduce((rows, line) => {
|
|
111
|
+
const visible = [...line.replace(/\x1b\[[0-9;]*m/g, '')].length;
|
|
112
|
+
return rows + Math.max(1, Math.ceil(visible / width));
|
|
113
|
+
}, 0);
|
|
114
|
+
}
|
|
115
|
+
async function askUpdateChoiceWithKeys(offer) {
|
|
116
|
+
let selected = 0;
|
|
117
|
+
const show = (index) => {
|
|
118
|
+
const rows = renderedRowCount(`${formatUpdateOptions(offer, selected)}\n`, output.columns ?? 80);
|
|
119
|
+
selected = index;
|
|
120
|
+
output.write(`\u001b[${rows}A\u001b[0J${formatUpdateOptions(offer, selected)}\n`);
|
|
121
|
+
};
|
|
122
|
+
emitKeypressEvents(input);
|
|
123
|
+
const wasRaw = input.isRaw === true;
|
|
124
|
+
input.setRawMode(true);
|
|
125
|
+
input.resume();
|
|
126
|
+
try {
|
|
127
|
+
return await new Promise((resolve) => {
|
|
128
|
+
const onKeypress = (_text, key) => {
|
|
129
|
+
const action = readUpdateKey(key ?? {}, selected);
|
|
130
|
+
if (action.kind === 'ignore')
|
|
131
|
+
return;
|
|
132
|
+
if (action.kind === 'select') {
|
|
133
|
+
if (action.index !== selected)
|
|
134
|
+
show(action.index);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const chosen = UPDATE_OPTIONS.indexOf(action.answer);
|
|
138
|
+
if (chosen >= 0 && chosen !== selected)
|
|
139
|
+
show(chosen);
|
|
140
|
+
input.off('keypress', onKeypress);
|
|
141
|
+
resolve(action.answer);
|
|
142
|
+
};
|
|
143
|
+
input.on('keypress', onKeypress);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
if (!wasRaw)
|
|
148
|
+
input.setRawMode(false);
|
|
149
|
+
input.pause();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function askUpdateChoice(offer) {
|
|
153
|
+
if (input.isTTY && output.isTTY)
|
|
154
|
+
return await askUpdateChoiceWithKeys(offer);
|
|
71
155
|
const rl = readline.createInterface({ input, output });
|
|
72
156
|
try {
|
|
73
157
|
return await resolveUpdateAnswer(() => rl.question(' ▸ '));
|
|
@@ -93,7 +177,7 @@ export async function offerUpdateAtStartup({ currentVersion, latestVersion, ask
|
|
|
93
177
|
if (!offer)
|
|
94
178
|
return 'none';
|
|
95
179
|
write(`${formatUpdateScreen(offer)}\n`);
|
|
96
|
-
const answer = await ask();
|
|
180
|
+
const answer = await ask(offer);
|
|
97
181
|
if (answer === 'cancel') {
|
|
98
182
|
write(`\n ${chalk.dim('Cancelled. Nothing was installed.')}\n\n`);
|
|
99
183
|
return 'cancelled';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.47",
|
|
4
4
|
"description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic-ai",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
45
45
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
46
46
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
47
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
48
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
49
|
-
"@thegitai/tui-linux-arm64": "1.0.0-preview.
|
|
50
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
51
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
47
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.47",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.47",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.47",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.47",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.47",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|