@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.
- package/dist/capture-renderer.d.ts +4 -0
- package/dist/capture-renderer.d.ts.map +1 -1
- package/dist/capture-renderer.js +21 -0
- package/dist/capture-renderer.js.map +1 -1
- package/dist/cdp/cdp-client.d.ts +58 -0
- package/dist/cdp/cdp-client.d.ts.map +1 -1
- package/dist/cdp/cdp-client.js +119 -3
- package/dist/cdp/cdp-client.js.map +1 -1
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +55 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -12
- package/dist/index.js.map +1 -1
- package/dist/process-activity-watcher.d.ts +33 -0
- package/dist/process-activity-watcher.d.ts.map +1 -0
- package/dist/process-activity-watcher.js +93 -0
- package/dist/process-activity-watcher.js.map +1 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/protocol.js.map +1 -1
- package/dist/schemas.d.ts +75 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +157 -2
- package/dist/schemas.js.map +1 -1
- package/dist/secret-shims.d.ts +2 -2
- package/dist/secret-shims.d.ts.map +1 -1
- package/dist/secret-shims.js +55 -35
- package/dist/secret-shims.js.map +1 -1
- package/dist/session-automation.d.ts +53 -0
- package/dist/session-automation.d.ts.map +1 -0
- package/dist/session-automation.js +230 -0
- package/dist/session-automation.js.map +1 -0
- package/dist/session-manager.d.ts +21 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +123 -1
- package/dist/session-manager.js.map +1 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +8 -0
- package/dist/session.js.map +1 -1
- package/dist/utils/named-keys.d.ts +4 -0
- package/dist/utils/named-keys.d.ts.map +1 -0
- package/dist/utils/named-keys.js +82 -0
- package/dist/utils/named-keys.js.map +1 -0
- package/dist/utils/open-chrome-inspect.d.ts +2 -0
- package/dist/utils/open-chrome-inspect.d.ts.map +1 -0
- package/dist/utils/open-chrome-inspect.js +65 -0
- package/dist/utils/open-chrome-inspect.js.map +1 -0
- package/dist/utils/sgr-mouse.d.ts +13 -0
- package/dist/utils/sgr-mouse.d.ts.map +1 -0
- package/dist/utils/sgr-mouse.js +40 -0
- package/dist/utils/sgr-mouse.js.map +1 -0
- package/dist/utils/terminal-mode-state.d.ts +1 -0
- package/dist/utils/terminal-mode-state.d.ts.map +1 -1
- package/dist/utils/terminal-mode-state.js +13 -0
- package/dist/utils/terminal-mode-state.js.map +1 -1
- 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 {
|
|
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
|
|
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
|
|
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
|
|
212
|
-
//
|
|
213
|
-
//
|
|
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(
|
|
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
|
|
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
|