@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
- if (existing) {
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
- for (const pending of browser?.pending.values() ?? []) {
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.socket.destroy();
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 /(google[- ]?chrome|chromium|brave|msedge|microsoft edge|vivaldi)/i.test(output);
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
- : chromiumProcessRunning()
96
- ? 'Chrome is running but the TheGitAI extension has not connected. Install or enable the extension, then reload it from chrome://extensions.'
97
- : 'Chrome does not appear to be running. Open Chrome with the TheGitAI extension installed.';
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 notConnected(context) {
35
- const availability = browserAvailability({
36
- cwd: context.rootDir,
37
- env: context.env,
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: 'Report the connection instructions. The extension is a separate installation. Do not retry in a loop or invent a download URL.',
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.46",
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.46",
48
- "@thegitai/tui-darwin-x64": "1.0.0-preview.46",
49
- "@thegitai/tui-linux-arm64": "1.0.0-preview.46",
50
- "@thegitai/tui-linux-x64": "1.0.0-preview.46",
51
- "@thegitai/tui-win32-x64": "1.0.0-preview.46",
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": {