@thegitai/cli 1.0.0-preview.46 → 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.
|
@@ -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
|
+
}
|
|
@@ -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/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": {
|