@monotykamary/localterm-server 2.31.0 → 2.33.0

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.
Files changed (60) hide show
  1. package/dist/capture-renderer.d.ts +4 -0
  2. package/dist/capture-renderer.d.ts.map +1 -1
  3. package/dist/capture-renderer.js +21 -0
  4. package/dist/capture-renderer.js.map +1 -1
  5. package/dist/cdp/cdp-client.d.ts +58 -0
  6. package/dist/cdp/cdp-client.d.ts.map +1 -1
  7. package/dist/cdp/cdp-client.js +119 -3
  8. package/dist/cdp/cdp-client.js.map +1 -1
  9. package/dist/constants.d.ts +13 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +55 -0
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +201 -12
  15. package/dist/index.js.map +1 -1
  16. package/dist/process-activity-watcher.d.ts +33 -0
  17. package/dist/process-activity-watcher.d.ts.map +1 -0
  18. package/dist/process-activity-watcher.js +93 -0
  19. package/dist/process-activity-watcher.js.map +1 -0
  20. package/dist/protocol.d.ts +2 -2
  21. package/dist/protocol.d.ts.map +1 -1
  22. package/dist/protocol.js +2 -2
  23. package/dist/protocol.js.map +1 -1
  24. package/dist/schemas.d.ts +75 -0
  25. package/dist/schemas.d.ts.map +1 -1
  26. package/dist/schemas.js +157 -2
  27. package/dist/schemas.js.map +1 -1
  28. package/dist/secret-shims.d.ts +2 -2
  29. package/dist/secret-shims.d.ts.map +1 -1
  30. package/dist/secret-shims.js +55 -35
  31. package/dist/secret-shims.js.map +1 -1
  32. package/dist/session-automation.d.ts +53 -0
  33. package/dist/session-automation.d.ts.map +1 -0
  34. package/dist/session-automation.js +230 -0
  35. package/dist/session-automation.js.map +1 -0
  36. package/dist/session-manager.d.ts +21 -0
  37. package/dist/session-manager.d.ts.map +1 -1
  38. package/dist/session-manager.js +123 -1
  39. package/dist/session-manager.js.map +1 -1
  40. package/dist/session.d.ts +1 -0
  41. package/dist/session.d.ts.map +1 -1
  42. package/dist/session.js +8 -0
  43. package/dist/session.js.map +1 -1
  44. package/dist/utils/named-keys.d.ts +4 -0
  45. package/dist/utils/named-keys.d.ts.map +1 -0
  46. package/dist/utils/named-keys.js +82 -0
  47. package/dist/utils/named-keys.js.map +1 -0
  48. package/dist/utils/open-chrome-inspect.d.ts +2 -0
  49. package/dist/utils/open-chrome-inspect.d.ts.map +1 -0
  50. package/dist/utils/open-chrome-inspect.js +65 -0
  51. package/dist/utils/open-chrome-inspect.js.map +1 -0
  52. package/dist/utils/sgr-mouse.d.ts +13 -0
  53. package/dist/utils/sgr-mouse.d.ts.map +1 -0
  54. package/dist/utils/sgr-mouse.js +40 -0
  55. package/dist/utils/sgr-mouse.js.map +1 -0
  56. package/dist/utils/terminal-mode-state.d.ts +1 -0
  57. package/dist/utils/terminal-mode-state.d.ts.map +1 -1
  58. package/dist/utils/terminal-mode-state.js +13 -0
  59. package/dist/utils/terminal-mode-state.js.map +1 -1
  60. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,9 +17,11 @@ import { defaultSnapshotProcesses as defaultCaffeinateSnapshotProcesses, } from
17
17
  import { CdpClient } from "./cdp/cdp-client.js";
18
18
  import { detectWithExplicitPort } from "./cdp/discover-explicit-endpoint.js";
19
19
  import { DaemonConfigStore } from "./daemon-config-store.js";
20
- import { AUTOMATION_EVENT_DEBOUNCE_MS, AUTOMATION_RECONCILE_MIN_DOWNTIME_MS, AUTOMATION_RUN_QUERY_PARAM, AUTOMATION_WATCH_DEBOUNCE_MS, AUTOMATION_WATCH_POST_RUN_GRACE_MS, AUTOMATION_WEBHOOK_DEBOUNCE_MS, DEFAULT_HOST, DEFAULT_PORT, FRIENDLY_HOSTNAME, GIT_MAX_REF_LENGTH, HTTP_STATUS_ACCEPTED, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CONFLICT, HTTP_STATUS_CREATED, HTTP_STATUS_NOT_FOUND, MAX_AUTOMATIONS, MAX_PROCESSES, MAX_SECRETS, MS_PER_MINUTE, PROCESSES_FILENAME, SECRETS_FILENAME, SECRETS_SHIMS_DIRNAME, SERVER_STOP_GRACE_MS, SESSION_ID_QUERY_PARAM, WS_BACKPRESSURE_THRESHOLD_BYTES, WS_CLOSE_BACKPRESSURE, WS_CLOSE_CAPACITY_REACHED, WS_CLOSE_POLICY_VIOLATION, WS_HEARTBEAT_GRACE_MS, WS_HEARTBEAT_INTERVAL_MS, WS_HEARTBEAT_TIMEOUT_MS, WS_READY_STATE_OPEN, } from "./constants.js";
20
+ import { z } from "zod";
21
+ import { ACTIVITY_DIRNAME, ACTIVITY_REFRESH_DEBOUNCE_MS, ACTIVITY_WATCHED_PROGRAMS, AUTOMATION_EVENT_DEBOUNCE_MS, AUTOMATION_RECONCILE_MIN_DOWNTIME_MS, AUTOMATION_RUN_QUERY_PARAM, AUTOMATION_WATCH_DEBOUNCE_MS, AUTOMATION_WATCH_POST_RUN_GRACE_MS, AUTOMATION_WEBHOOK_DEBOUNCE_MS, DEFAULT_HOST, DEFAULT_PORT, FRIENDLY_HOSTNAME, GIT_MAX_REF_LENGTH, HTTP_STATUS_ACCEPTED, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CONFLICT, HTTP_STATUS_CREATED, HTTP_STATUS_NOT_FOUND, MAX_AUTOMATIONS, MAX_PROCESSES, MAX_SECRETS, MS_PER_MINUTE, PROCESSES_FILENAME, SECRETS_FILENAME, SECRETS_SHIMS_DIRNAME, SERVER_STOP_GRACE_MS, SESSION_ID_QUERY_PARAM, SESSION_ACTIVITY_WINDOW_MS, WAIT_DEFAULT_TIMEOUT_MS, WS_BACKPRESSURE_THRESHOLD_BYTES, WS_CLOSE_BACKPRESSURE, WS_CLOSE_CAPACITY_REACHED, WS_CLOSE_POLICY_VIOLATION, WS_HEARTBEAT_GRACE_MS, WS_HEARTBEAT_INTERVAL_MS, WS_HEARTBEAT_TIMEOUT_MS, WS_READY_STATE_OPEN, } from "./constants.js";
21
22
  import { getDefaultShell } from "./default-shell.js";
22
23
  import { shellPathForUserShell } from "./utils/shell-path.js";
24
+ import { openChromeInspect } from "./utils/open-chrome-inspect.js";
23
25
  import { ServerErrorException, serverError } from "./errors.js";
24
26
  import { FolderWatchManager } from "./folder-watch-manager.js";
25
27
  import { SessionEventManager } from "./session-event-manager.js";
@@ -30,12 +32,15 @@ import { createDefaultSecretBackend } from "./secret-backend.js";
30
32
  import { SecretStore } from "./secret-store.js";
31
33
  import { ProcessStore } from "./process-store.js";
32
34
  import { regenerateShims } from "./secret-shims.js";
35
+ import { ProcessActivityWatcher } from "./process-activity-watcher.js";
33
36
  import { parseCronExpression } from "./cron-expression.js";
34
37
  import { createGitWorktree, listGitWorktrees, removeGitWorktree } from "./git-worktrees.js";
35
38
  import { defaultSnapshotListeners, isSessionDescendantPid, listSessionListeningPorts, } from "./listening-ports.js";
36
- import { clientToServerMessageSchema, createAutomationInputSchema, createSessionInputSchema, createWorktreeInputSchema, execInputSchema, execOneShotInputSchema, launchInputSchema, resetAutomationInputSchema, secretEntrySchema, secretSetInputSchema, sessionInputSchema, sessionResizeSchema, processNameSchema, processSetInputSchema, updateAutomationInputSchema, updateDaemonConfigInputSchema, updateSessionInputSchema, updateWorktreeConfigInputSchema, worktreeIncludeFileInputSchema, } from "./schemas.js";
39
+ import { clientToServerMessageSchema, createAutomationInputSchema, createSessionInputSchema, createWorktreeInputSchema, execInputSchema, execOneShotInputSchema, launchInputSchema, resetAutomationInputSchema, secretEntrySchema, secretSetInputSchema, sessionInputSchema, sessionResizeSchema, processNameSchema, processSetInputSchema, updateAutomationInputSchema, updateDaemonConfigInputSchema, updateSessionInputSchema, updateWorktreeConfigInputSchema, worktreeIncludeFileInputSchema, waitInputSchema, mouseInputSchema, } from "./schemas.js";
37
40
  import { createNetworkPolicyMiddleware, isAllowedSourceIp, isLoopbackHost } from "./security.js";
38
41
  import { SessionManager, } from "./session-manager.js";
42
+ import { capturePanePng, sendMouse } from "./session-automation.js";
43
+ import { encodeClick, encodeDrag, encodeMove, encodeScroll } from "./utils/sgr-mouse.js";
39
44
  import { getBufferedAmount } from "./utils/ws-socket.js";
40
45
  import { resolveStaticAsset } from "./static-resolver.js";
41
46
  import { resolveImageAsset } from "./utils/resolve-image-asset.js";
@@ -103,9 +108,86 @@ const safeSend = (ws, payload) => {
103
108
  /* socket closed between readyState check and send */
104
109
  }
105
110
  };
111
+ // Build the predicate the session manager polls against the flushed capture
112
+ // renderer. Text matches substring (case-insensitive by default); regex compiles
113
+ // (a bad pattern yields invalid_body); idle returns a no-op test since the
114
+ // manager resolves it from output recency, not the pane text.
115
+ const buildWaitPredicate = (input) => {
116
+ if (input.mode === "text") {
117
+ const needle = input.text;
118
+ const caseSensitive = input.caseSensitive ?? false;
119
+ return {
120
+ kind: "text",
121
+ test: caseSensitive
122
+ ? (text) => text.includes(needle)
123
+ : (text) => text.toLowerCase().includes(needle.toLowerCase()),
124
+ };
125
+ }
126
+ if (input.mode === "regex") {
127
+ let pattern;
128
+ try {
129
+ pattern = new RegExp(input.regex);
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ return { kind: "regex", test: (text) => pattern.test(text) };
135
+ }
136
+ return { kind: "idle", test: () => false };
137
+ };
138
+ // Normalize the parsed mouse schema into the MouseAction union the automation
139
+ // layer consumes, applying defaults (left button, 1 click, 3 scroll lines).
140
+ const normalizeMouseAction = (input) => {
141
+ if (input.action === "click") {
142
+ const button = input.button ?? "left";
143
+ const clicks = input.clicks ?? 1;
144
+ if (input.onText !== undefined)
145
+ return { action: "click", onText: input.onText, button, clicks };
146
+ if (input.col !== undefined && input.row !== undefined)
147
+ return { action: "click", col: input.col, row: input.row, button, clicks };
148
+ return null;
149
+ }
150
+ if (input.action === "drag")
151
+ return {
152
+ action: "drag",
153
+ fromCol: input.fromCol,
154
+ fromRow: input.fromRow,
155
+ toCol: input.toCol,
156
+ toRow: input.toRow,
157
+ button: input.button ?? "left",
158
+ };
159
+ if (input.action === "move")
160
+ return { action: "move", col: input.col, row: input.row };
161
+ return {
162
+ action: "scroll",
163
+ direction: input.direction,
164
+ amount: input.amount ?? 3,
165
+ col: input.col ?? 0,
166
+ row: input.row ?? 0,
167
+ };
168
+ };
106
169
  const buildApiRoutes = (ctx) => {
107
170
  const api = new Hono();
108
- const { registry, cdpClient, secretBackend, secretStore, shimsDir, processStore, syncSecretShims, automationStore, broadcastAutomations, syncFolderWatchers, syncSessionEventListeners, webhookTriggerManager, worktreeConfigStore, portsSnapshotProcesses, portsSnapshotListeners, toAutomationWithNextRun, listAutomationsWithNextRun, tryLaunch, getCdpPort, applyCdpPort, getGraceSeconds, applyGraceSeconds, connectCdpNow, } = ctx;
171
+ const { registry, cdpClient, secretBackend, secretStore, shimsDir, processStore, syncSecretShims, automationStore, broadcastAutomations, syncFolderWatchers, syncSessionEventListeners, webhookTriggerManager, worktreeConfigStore, portsSnapshotProcesses, portsSnapshotListeners, toAutomationWithNextRun, listAutomationsWithNextRun, tryLaunch, getCdpPort, applyCdpPort, getGraceSeconds, applyGraceSeconds, connectCdpNow, buildTabUrl, } = ctx;
172
+ // Headless SGR-1006 fallback for `mouse` when no CDP tab is reachable:
173
+ // encode the gesture as SGR bytes and write them straight to the PTY. Closes
174
+ // over `registry` so the automation layer stays CDP-agnostic. Coords arrive
175
+ // 0-indexed (viewport cells); SGR is 1-indexed.
176
+ const writeSgrMouseFallback = (id, action, col, row) => {
177
+ const c = col + 1;
178
+ const r = row + 1;
179
+ const button = action.action === "click" || action.action === "drag" ? action.button : "left";
180
+ let bytes;
181
+ if (action.action === "click")
182
+ bytes = encodeClick(c, r, button, action.clicks);
183
+ else if (action.action === "drag")
184
+ bytes = encodeDrag(action.fromCol + 1, action.fromRow + 1, action.toCol + 1, action.toRow + 1, button);
185
+ else if (action.action === "move")
186
+ bytes = encodeMove(c, r);
187
+ else
188
+ bytes = encodeScroll(c, r, action.direction, action.amount);
189
+ return registry.writeInputById(id, bytes);
190
+ };
109
191
  api.get("/health", (context) => context.json({
110
192
  ok: true,
111
193
  sessions: registry.size(),
@@ -186,15 +268,19 @@ const buildApiRoutes = (ctx) => {
186
268
  return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
187
269
  return context.json({ session });
188
270
  });
189
- // send-keys: write raw input to a session by id. Bytes go straight to the
190
- // PTY (no pending handshake — there's no WebSocket client). To execute a
271
+ // send-keys / press: write input to a session by id. Bytes go straight to
272
+ // the PTY (no pending handshake — there's no WebSocket client). To execute a
191
273
  // command, include a trailing newline; for a blocking command+output+exit
192
- // in one call, use `exec` instead.
274
+ // in one call, use `exec` instead. `named:true` resolves space-separated key
275
+ // names (`F2`, `Ctrl-C`, literal text) to xterm bytes — the `press` path.
193
276
  api.post("/sessions/:id/input", async (context) => {
194
277
  const parsed = sessionInputSchema.safeParse(await readJsonBody(context));
195
278
  if (!parsed.success)
196
279
  return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
197
- const written = registry.writeInputById(context.req.param("id"), parsed.data.data);
280
+ const id = context.req.param("id");
281
+ const written = parsed.data.named
282
+ ? registry.pressKeysById(id, parsed.data.data)
283
+ : registry.writeInputById(id, parsed.data.data);
198
284
  if (!written)
199
285
  return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
200
286
  return context.json({ ok: true });
@@ -208,20 +294,76 @@ const buildApiRoutes = (ctx) => {
208
294
  return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
209
295
  return context.json({ ok: true });
210
296
  });
211
- // capture-pane: the session's rendered screen as clean text (ANSI processed by
212
- // the headless emulator). `lines` defaults to the viewport and may extend into
213
- // scrollback. Returns the tmux `capture-pane -p` equivalent.
297
+ // capture-pane: the session's rendered screen. `format=png` returns the
298
+ // terminal rasterized to a PNG by the browser over the daemon's existing
299
+ // CDP socket (the viewer is reused, or an ephemeral background tab is
300
+ // opened and closed) — no new image dependency. Text (the default) is the
301
+ // headless capture renderer's grid, which works with no browser at all.
214
302
  api.get("/sessions/:id/pane", async (context) => {
303
+ const id = context.req.param("id");
304
+ const format = context.req.query("format");
305
+ if (format === "png") {
306
+ const png = await capturePanePng({ cdpClient, buildTabUrl }, registry, id);
307
+ if (!png)
308
+ return context.json({ error: "no_browser" }, HTTP_STATUS_CONFLICT);
309
+ return new Response(png, { headers: { "content-type": "image/png" } });
310
+ }
215
311
  const linesParam = context.req.query("lines");
216
312
  const lines = linesParam ? Number(linesParam) : undefined;
217
313
  if (lines !== undefined && (!Number.isInteger(lines) || lines <= 0)) {
218
314
  return context.json({ error: "invalid_lines" }, HTTP_STATUS_BAD_REQUEST);
219
315
  }
220
- const text = await registry.capturePane(context.req.param("id"), lines);
316
+ const text = await registry.capturePane(id, lines);
221
317
  if (text === null)
222
318
  return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
223
319
  return context.json({ text });
224
320
  });
321
+ // wait: block until the session's rendered pane matches a text/regex
322
+ // predicate or goes idle for a window. The blocking `wait` primitive for
323
+ // interactive apps so an agent doesn't poll. Reuses the capture renderer as
324
+ // the source of truth (flushed per frame). Exits on match, timeout, or exit.
325
+ api.post("/sessions/:id/wait", async (context) => {
326
+ const parsed = waitInputSchema.safeParse(await readJsonBody(context));
327
+ if (!parsed.success)
328
+ return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
329
+ const id = context.req.param("id");
330
+ const timeoutMs = parsed.data.timeoutMs ?? WAIT_DEFAULT_TIMEOUT_MS;
331
+ const predicate = buildWaitPredicate(parsed.data);
332
+ if (!predicate)
333
+ return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
334
+ const idleMs = parsed.data.mode === "idle" ? (parsed.data.idleMs ?? SESSION_ACTIVITY_WINDOW_MS) : undefined;
335
+ const result = await registry.waitFor(id, predicate, timeoutMs, idleMs);
336
+ if (!result)
337
+ return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
338
+ return context.json(result);
339
+ });
340
+ // mouse: drive a TUI with the mouse. Dispatches a real event through the
341
+ // tab's xterm.js (SGR generated natively) over the existing CDP socket, or
342
+ // falls back to direct SGR-1006 bytes when no browser is reachable.
343
+ api.post("/sessions/:id/mouse", async (context) => {
344
+ const parsed = mouseInputSchema.safeParse(await readJsonBody(context));
345
+ if (!parsed.success)
346
+ return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
347
+ const id = context.req.param("id");
348
+ const action = normalizeMouseAction(parsed.data);
349
+ if (!action)
350
+ return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
351
+ const result = await sendMouse({ cdpClient, buildTabUrl }, registry, id, action, writeSgrMouseFallback);
352
+ return context.json(result);
353
+ });
354
+ // mouse state: whether the session's foreground app enabled mouse tracking
355
+ // (gates the SGR fallback) plus the viewport size.
356
+ api.get("/sessions/:id/mouse/state", (context) => {
357
+ const id = context.req.param("id");
358
+ const managed = registry.list().find((session) => session.id === id);
359
+ if (!managed)
360
+ return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
361
+ return context.json({
362
+ enabled: registry.mouseEnabledFor(id),
363
+ cols: registry.sessionSizeFor(id).cols,
364
+ rows: registry.sessionSizeFor(id).rows,
365
+ });
366
+ });
225
367
  // In-session exec: run a single command line inside a persistent session,
226
368
  // capture its rendered output, and return its exit code. The session's
227
369
  // cwd/env/history survive across calls (the tmux send-keys+capture-pane
@@ -885,6 +1027,16 @@ const buildApiRoutes = (ctx) => {
885
1027
  // or the error that explains a failure), unlike the fire-and-forget connect
886
1028
  // kicked by `PUT /api/config` and daemon start.
887
1029
  api.post("/cdp/connect", async (context) => context.json(await connectCdpNow()));
1030
+ // Open chrome://inspect in the user's browser. This is the bootstrap path
1031
+ // for users who haven't enabled remote debugging yet — they open the inspect
1032
+ // page to toggle "Discover network targets" — so it must NOT go through CDP
1033
+ // (CDP isn't available to those users). chrome:// URLs can't be navigated to
1034
+ // from a web page, so the daemon opens it (AppleScript `open location` on
1035
+ // macOS to reuse the running profile; OS opener elsewhere).
1036
+ api.post("/cdp/open-inspect", async (context) => {
1037
+ await openChromeInspect();
1038
+ return context.json({ ok: true });
1039
+ });
888
1040
  // Fire a webhook-triggered automation. The :id is the automation's webhook
889
1041
  // capability token (Discord-style: anyone with the URL can fire it). The body
890
1042
  // is intentionally ignored — the command/cwd are fixed at create time, so a
@@ -1016,8 +1168,39 @@ export const createServer = async (options = {}) => {
1016
1168
  shimsDir,
1017
1169
  });
1018
1170
  const processStore = new ProcessStore(path.join(stateDirectory, PROCESSES_FILENAME));
1019
- const syncSecretShims = () => regenerateShims(processStore.list(), secretStore.envVarByName(), shimsDir, secretBackend);
1171
+ const activityDir = path.join(stateDirectory, ACTIVITY_DIRNAME);
1172
+ const syncSecretShims = () => regenerateShims(processStore.list(), secretStore.envVarByName(), shimsDir, secretBackend, activityDir);
1020
1173
  syncSecretShims();
1174
+ // Detect short-lived CLI invocations the process-tree walker can't catch
1175
+ // (they exit before a `ps` snapshot observes them). Each activity-watched
1176
+ // program's PATH shim overwrites <activityDir>/<program> with $PWD after the
1177
+ // real binary exits; this watcher reacts via fs.watch (no polling). The
1178
+ // built-in set starts with `gh` so running it in a viewed repo refreshes the
1179
+ // PR lease for that cwd without the user manually refreshing — the same role
1180
+ // the working-tree git-dirty signal plays for the diff summary. Gated on
1181
+ // the secret backend because the activity shim is only generated where the
1182
+ // shim feature is supported (darwin); elsewhere there is nothing to watch.
1183
+ let processActivityWatcher = null;
1184
+ if (secretBackend.supported && ACTIVITY_WATCHED_PROGRAMS.length > 0) {
1185
+ processActivityWatcher = new ProcessActivityWatcher({
1186
+ activityDir,
1187
+ programs: ACTIVITY_WATCHED_PROGRAMS,
1188
+ debounceMs: ACTIVITY_REFRESH_DEBOUNCE_MS,
1189
+ });
1190
+ processActivityWatcher.on("activity", (program, cwd) => {
1191
+ if (program !== "gh")
1192
+ return;
1193
+ // No subscribers in this cwd → no toolbar to update, so skip the GitHub
1194
+ // call. getGitBranchPr's own per-(cwd, branch) in-flight dedup handles
1195
+ // any overlap with a concurrent manual refresh.
1196
+ if (!registry.hasCoordinatorFor(cwd))
1197
+ return;
1198
+ void (async () => {
1199
+ const pr = await getGitBranchPr(cwd);
1200
+ registry.broadcastGitBranchPr(cwd, pr);
1201
+ })();
1202
+ });
1203
+ }
1021
1204
  const caffeinateManager = new CaffeinateManager({
1022
1205
  controller: caffeinateController,
1023
1206
  store: caffeinatePreferencesStore,
@@ -1353,6 +1536,11 @@ export const createServer = async (options = {}) => {
1353
1536
  getGraceSeconds,
1354
1537
  applyGraceSeconds,
1355
1538
  connectCdpNow,
1539
+ buildTabUrl: (sessionId) => {
1540
+ const url = new URL(localOrigin ?? publicOrigin ?? `http://${FRIENDLY_HOSTNAME}:${actualPort}`);
1541
+ url.searchParams.set(SESSION_ID_QUERY_PARAM, sessionId);
1542
+ return url.toString();
1543
+ },
1356
1544
  };
1357
1545
  const api = buildApiRoutes(ctx);
1358
1546
  app.route("/api", api);
@@ -1730,6 +1918,7 @@ export const createServer = async (options = {}) => {
1730
1918
  folderWatchManager.dispose();
1731
1919
  webhookTriggerManager.dispose();
1732
1920
  caffeinateManager.dispose();
1921
+ processActivityWatcher?.dispose();
1733
1922
  cdpClient?.close();
1734
1923
  registry.disposeAll();
1735
1924
  // Forcibly tear down every WS first. node-pty + ws upgraded sockets