zcode-acp-server 0.22.0 → 0.24.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/config/user-config.d.ts +48 -0
- package/dist/config/user-config.d.ts.map +1 -0
- package/dist/config/user-config.js +101 -0
- package/dist/config/user-config.js.map +1 -0
- package/dist/handlers/session.d.ts +16 -2
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +65 -10
- package/dist/handlers/session.js.map +1 -1
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +2 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/remote/config.d.ts +36 -24
- package/dist/remote/config.d.ts.map +1 -1
- package/dist/remote/config.js +70 -36
- package/dist/remote/config.js.map +1 -1
- package/dist/remote/hub-server.d.ts +26 -7
- package/dist/remote/hub-server.d.ts.map +1 -1
- package/dist/remote/hub-server.js +108 -35
- package/dist/remote/hub-server.js.map +1 -1
- package/dist/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +19 -0
- package/dist/server.js.map +1 -1
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +1 -1
- package/docs/REMOTE-CLIENTS.md +25 -18
- package/docs/REMOTE.md +55 -13
- package/package.json +1 -1
package/dist/remote/config.js
CHANGED
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remote access configuration
|
|
2
|
+
* Remote access configuration: user config file first, env fallback.
|
|
3
3
|
*
|
|
4
|
-
* Remote access is opt-in
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Remote access is opt-in and REQUIRES a token — the endpoint is expected to
|
|
5
|
+
* sit behind a public tunnel (Cloudflare Tunnel, frp), so "loopback-only" is
|
|
6
|
+
* never a safe assumption here. A missing token disables the feature with a
|
|
7
|
+
* warning instead of failing the bridge: the stdio link to the editor must
|
|
8
|
+
* keep working no matter what.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
10
|
+
* Sources, in priority order (per field):
|
|
11
|
+
* 1. ~/.config/zcode-acp/config.json `remote` section (XDG_CONFIG_HOME
|
|
12
|
+
* aware) — the authoritative, launch-context-independent source. The
|
|
13
|
+
* hub daemon is idle-exited and re-spawned by arbitrary bridges, so its
|
|
14
|
+
* birth env rotates; user preference must not.
|
|
15
|
+
* 2. Environment variables (unchanged semantics — setups without a file
|
|
16
|
+
* keep working, and env fills any field the file leaves unset):
|
|
17
|
+
* ZCODE_ACP_REMOTE=1 enable the remote endpoint (gate)
|
|
18
|
+
* ZCODE_ACP_REMOTE_TOKEN=<s> auth token (mandatory when enabled)
|
|
19
|
+
* ZCODE_ACP_HUB_PORT=8377 hub's fixed port (the one a tunnel maps)
|
|
20
|
+
* ZCODE_ACP_HUB_HOST=127.0.0.1 hub bind address (e.g. 0.0.0.0 for a
|
|
21
|
+
* containerized tunnel agent)
|
|
22
|
+
* ZCODE_ACP_REMOTE_PORT=8378 bridge endpoint start port (auto-increment
|
|
23
|
+
* when taken; loopback only)
|
|
24
|
+
* ZCODE_ACP_HUB_TERMINAL=0 disable the visible-terminal incubation
|
|
25
|
+
* ZCODE_ACP_HUB_TERMINAL_APP=<name> terminal app for the TUI window
|
|
26
|
+
* ZCODE_ACP_HUB_TERMINAL_COMMAND=<sh> shell command template ({script})
|
|
27
|
+
* 3. Built-in defaults.
|
|
28
|
+
*
|
|
29
|
+
* Process-role plumbing stays env-only by design (never file-configurable):
|
|
30
|
+
* ZCODE_ACP_REMOTE_ORIGIN=serve registration origin override (ADR-0016)
|
|
31
|
+
* ZCODE_ACP_REMOTE_PIN_CWD=1 pin session roots to the process cwd
|
|
32
|
+
* ZCODE_ACP_RESUME_SESSION=<id> per-request boot-resume target (ADR-0017)
|
|
28
33
|
*/
|
|
34
|
+
import { loadUserConfig } from "../config/user-config.js";
|
|
29
35
|
import { warn } from "../utils.js";
|
|
30
36
|
export const DEFAULT_HUB_PORT = 8377;
|
|
31
37
|
export const DEFAULT_BRIDGE_PORT = 8378;
|
|
@@ -40,40 +46,68 @@ function parsePort(raw, fallback, envName) {
|
|
|
40
46
|
}
|
|
41
47
|
return port;
|
|
42
48
|
}
|
|
49
|
+
/** Shared file>env merge for token/ports/host (already validated by the loader). */
|
|
50
|
+
function mergeCommon(env) {
|
|
51
|
+
const file = loadUserConfig(env).remote ?? {};
|
|
52
|
+
return {
|
|
53
|
+
token: file.token ?? (env.ZCODE_ACP_REMOTE_TOKEN ?? "").trim(),
|
|
54
|
+
hubPort: file.hubPort ?? parsePort(env.ZCODE_ACP_HUB_PORT, DEFAULT_HUB_PORT, "ZCODE_ACP_HUB_PORT"),
|
|
55
|
+
hubHost: file.hubHost ?? ((env.ZCODE_ACP_HUB_HOST ?? "").trim() || DEFAULT_HUB_HOST),
|
|
56
|
+
bridgePort: file.bridgePort ??
|
|
57
|
+
parsePort(env.ZCODE_ACP_REMOTE_PORT, DEFAULT_BRIDGE_PORT, "ZCODE_ACP_REMOTE_PORT"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
43
60
|
/** Parse remote config; null = disabled (or misconfigured → warned). */
|
|
44
61
|
export function parseRemoteConfig(env = process.env) {
|
|
45
|
-
const
|
|
46
|
-
|
|
62
|
+
const file = loadUserConfig(env).remote ?? {};
|
|
63
|
+
// Gate: the file is authoritative when it says anything; env decides only
|
|
64
|
+
// when the file is silent (an explicit `enabled: false` wins over env =1).
|
|
65
|
+
const enabled = file.enabled !== undefined
|
|
66
|
+
? file.enabled
|
|
67
|
+
: ["1", "true", "yes", "on"].includes((env.ZCODE_ACP_REMOTE ?? "").trim().toLowerCase());
|
|
68
|
+
if (!enabled)
|
|
47
69
|
return null;
|
|
48
|
-
const token = (env
|
|
70
|
+
const { token, hubPort, hubHost, bridgePort } = mergeCommon(env);
|
|
49
71
|
if (!token) {
|
|
50
|
-
warn("remote:
|
|
72
|
+
warn("remote: enabled but no token (config file or ZCODE_ACP_REMOTE_TOKEN) — " +
|
|
51
73
|
"remote access disabled (stdio unaffected)");
|
|
52
74
|
return null;
|
|
53
75
|
}
|
|
54
76
|
return {
|
|
55
77
|
token,
|
|
56
|
-
hubPort
|
|
57
|
-
hubHost
|
|
58
|
-
bridgePort
|
|
78
|
+
hubPort,
|
|
79
|
+
hubHost,
|
|
80
|
+
bridgePort,
|
|
59
81
|
origin: (env.ZCODE_ACP_REMOTE_ORIGIN ?? "").trim() === "serve" ? "serve" : "editor",
|
|
60
82
|
pinCwd: (env.ZCODE_ACP_REMOTE_PIN_CWD ?? "").trim() === "1",
|
|
61
83
|
};
|
|
62
84
|
}
|
|
63
85
|
/** Parse hub-side config for the standalone hub entry (`zcode-acp hub`). */
|
|
64
86
|
export function parseHubConfig(env = process.env) {
|
|
65
|
-
const token = (env
|
|
87
|
+
const { token, hubPort, hubHost, bridgePort } = mergeCommon(env);
|
|
66
88
|
if (!token) {
|
|
67
|
-
warn("hub:
|
|
89
|
+
warn("hub: no token (config file or ZCODE_ACP_REMOTE_TOKEN) — refusing to start without auth");
|
|
68
90
|
return null;
|
|
69
91
|
}
|
|
70
92
|
return {
|
|
71
93
|
token,
|
|
72
|
-
hubPort
|
|
73
|
-
hubHost
|
|
74
|
-
bridgePort
|
|
94
|
+
hubPort,
|
|
95
|
+
hubHost,
|
|
96
|
+
bridgePort,
|
|
75
97
|
origin: "editor",
|
|
76
98
|
pinCwd: false,
|
|
77
99
|
};
|
|
78
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Terminal incubation preferences for the hub (ADR-0016), merged file > env.
|
|
103
|
+
* Read LIVE by the hub at every incubation so editing the file takes effect
|
|
104
|
+
* without a hub restart — the hub outlives the shells that configured it.
|
|
105
|
+
*/
|
|
106
|
+
export function remoteTerminalPrefs(env = process.env) {
|
|
107
|
+
const file = loadUserConfig(env).remote?.terminal ?? {};
|
|
108
|
+
const app = file.app ?? ((env.ZCODE_ACP_HUB_TERMINAL_APP ?? "").trim() || undefined);
|
|
109
|
+
const command = file.command ?? ((env.ZCODE_ACP_HUB_TERMINAL_COMMAND ?? "").trim() || undefined);
|
|
110
|
+
const enabled = file.enabled ?? !["0", "false", "off"].includes((env.ZCODE_ACP_HUB_TERMINAL ?? "").trim().toLowerCase());
|
|
111
|
+
return { enabled, ...(app ? { app } : {}), ...(command ? { command } : {}) };
|
|
112
|
+
}
|
|
79
113
|
//# sourceMappingURL=config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* next bridge re-spawns it on demand.
|
|
24
24
|
*/
|
|
25
25
|
import { type ChildProcess } from "node:child_process";
|
|
26
|
+
import type { TerminalPrefs } from "../config/user-config.js";
|
|
26
27
|
export interface HubOptions {
|
|
27
28
|
port: number;
|
|
28
29
|
host: string;
|
|
@@ -93,17 +94,27 @@ export type TerminalLaunch =
|
|
|
93
94
|
kind: "openAppArgs";
|
|
94
95
|
app: string;
|
|
95
96
|
args: string[];
|
|
97
|
+
}
|
|
98
|
+
/** Warp: refuses `.command` files and its CLI is agent-only, but its URI
|
|
99
|
+
* scheme EXECUTES a script handed to action/new_tab's path param
|
|
100
|
+
* (app/src/uri/mod.rs → open_file; verified on 0.2026.09.02): the hub opens
|
|
101
|
+
* `<scheme>://action/new_tab?path=<script>` and Warp runs it as a new tab
|
|
102
|
+
* in its default mode. Preview uses the warppreview:// scheme. */
|
|
103
|
+
| {
|
|
104
|
+
kind: "warpUri";
|
|
105
|
+
app: string;
|
|
106
|
+
scheme: string;
|
|
96
107
|
};
|
|
97
108
|
/**
|
|
98
|
-
* Pick how to open the TUI script.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* hatch) → a built-in launcher by app name
|
|
102
|
-
* `.app`-suffix-insensitive) → plain Terminal.app
|
|
103
|
-
*
|
|
109
|
+
* Pick how to open the TUI script. Preferences arrive pre-merged from
|
|
110
|
+
* `remoteTerminalPrefs` (config file first, env fallback — see config.ts);
|
|
111
|
+
* only the app-name normalization lives here. Priority: the explicit command
|
|
112
|
+
* template (the universal escape hatch) → a built-in launcher by app name
|
|
113
|
+
* (aliases are case- and `.app`-suffix-insensitive) → plain Terminal.app
|
|
114
|
+
* (macOS has no default-terminal setting to detect). Any other unmatched
|
|
104
115
|
* name passes through to `open -a` unchanged.
|
|
105
116
|
*/
|
|
106
|
-
export declare function resolveTerminalLaunch(env: NodeJS.ProcessEnv): {
|
|
117
|
+
export declare function resolveTerminalLaunch(env: NodeJS.ProcessEnv, prefs?: TerminalPrefs): {
|
|
107
118
|
launch: TerminalLaunch;
|
|
108
119
|
warning?: string;
|
|
109
120
|
};
|
|
@@ -118,6 +129,14 @@ export declare function resolveTerminalLaunch(env: NodeJS.ProcessEnv): {
|
|
|
118
129
|
export declare function terminalTuiScript(cwd: string, cliJs: string, env: NodeJS.ProcessEnv): string;
|
|
119
130
|
/** Reset the quota cache (test helper). */
|
|
120
131
|
export declare function resetQuotaCacheForTest(): void;
|
|
132
|
+
/**
|
|
133
|
+
* Tab-title sanitizer: the value is printf'd into the terminal as an OSC
|
|
134
|
+
* payload, so control characters (an ESC inside a model-generated summary
|
|
135
|
+
* would inject terminal sequences) become spaces, whitespace collapses, and
|
|
136
|
+
* the length caps at what a tab can usefully show. Empty/non-string →
|
|
137
|
+
* undefined (the caller falls back to the project name).
|
|
138
|
+
*/
|
|
139
|
+
export declare function sanitizeTabTitle(v: unknown): string | undefined;
|
|
121
140
|
/**
|
|
122
141
|
* Start the hub. Resolves once listening; rejects on bind failure (including
|
|
123
142
|
* EADDRINUSE when another hub already owns the port).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-server.d.ts","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"hub-server.d.ts","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAwB9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAM9D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QACvB,sFAAsF;QACtF,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;KACvB,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAsID,sEAAsE;AACtE,MAAM,MAAM,cAAc;AACxB;uEACuE;AACrE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACpC,6EAA6E;GAC3E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAClC;kEACkE;GAChE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE;AACtD;;;;kEAIkE;GAChE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA2BrD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,KAAK,GAAE,aAAwC,GAC9C;IACD,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAUA;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAqB5F;AA6JD,2CAA2C;AAC3C,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAmCD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQ/D;AAyID;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CA22B9F"}
|
|
@@ -35,7 +35,9 @@ import { tmpdir } from "node:os";
|
|
|
35
35
|
import { fileURLToPath } from "node:url";
|
|
36
36
|
import { WebSocket, WebSocketServer } from "ws";
|
|
37
37
|
import { AGENT_INFO, compareVersions, log, warn } from "../utils.js";
|
|
38
|
+
import { remoteTerminalPrefs } from "./config.js";
|
|
38
39
|
import { accountUsageStats } from "../handlers/account.js";
|
|
40
|
+
import { BOOT_RESUME_TRIGGER } from "../handlers/session.js";
|
|
39
41
|
import { listKnownWorkspaces } from "../tasks-index.js";
|
|
40
42
|
const HEARTBEAT_TIMEOUT_MS = 30_000;
|
|
41
43
|
const IDLE_EXIT_MS = 10 * 60_000;
|
|
@@ -128,8 +130,6 @@ function parseOrigin(raw) {
|
|
|
128
130
|
const TUI_REGISTER_TIMEOUT_MS = 20_000;
|
|
129
131
|
/** `open -a <terminal>` must answer fast or the incubation falls back. */
|
|
130
132
|
const TERMINAL_OPEN_TIMEOUT_MS = 3_000;
|
|
131
|
-
/** Set ZCODE_ACP_HUB_TERMINAL=0 to keep remote session-create headless. */
|
|
132
|
-
const TERMINAL_GATE_ENV = "ZCODE_ACP_HUB_TERMINAL";
|
|
133
133
|
/** Single-quote for sh: ' → '\'' . */
|
|
134
134
|
function shQuote(s) {
|
|
135
135
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
@@ -141,11 +141,9 @@ function shQuote(s) {
|
|
|
141
141
|
* WezTerm's `start --` runs an alternative program (wezterm.org/cli/start.html);
|
|
142
142
|
* kitty takes the program as normal positional arguments
|
|
143
143
|
* (sw.kovidgoyal.net/kitty/invocation); Alacritty and Ghostty support the
|
|
144
|
-
* common `-e` flag
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* only; still an open request in #3959/#9083). Hyper is absent for the same
|
|
148
|
-
* reason (#3677).
|
|
144
|
+
* common `-e` flag; Warp rides its new_tab URI action (see warpUri above).
|
|
145
|
+
* The script does its own `cd`, so no per-app cwd flags. Hyper is absent — no
|
|
146
|
+
* programmatic command execution at all (vercel/hyper#3677).
|
|
149
147
|
*/
|
|
150
148
|
const TERMINAL_APP_LAUNCHERS = {
|
|
151
149
|
terminal: { kind: "openApp", app: "Terminal" },
|
|
@@ -156,34 +154,30 @@ const TERMINAL_APP_LAUNCHERS = {
|
|
|
156
154
|
kitty: { kind: "openAppArgs", app: "kitty", args: [] },
|
|
157
155
|
alacritty: { kind: "openAppArgs", app: "Alacritty", args: ["-e"] },
|
|
158
156
|
ghostty: { kind: "openAppArgs", app: "Ghostty", args: ["-e"] },
|
|
157
|
+
warp: { kind: "warpUri", app: "Warp", scheme: "warp" },
|
|
158
|
+
"warp-preview": { kind: "warpUri", app: "Warp Preview", scheme: "warppreview" },
|
|
159
|
+
warp_preview: { kind: "warpUri", app: "Warp Preview", scheme: "warppreview" },
|
|
159
160
|
};
|
|
160
161
|
/**
|
|
161
|
-
* Pick how to open the TUI script.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* hatch) → a built-in launcher by app name
|
|
165
|
-
* `.app`-suffix-insensitive) → plain Terminal.app
|
|
166
|
-
*
|
|
162
|
+
* Pick how to open the TUI script. Preferences arrive pre-merged from
|
|
163
|
+
* `remoteTerminalPrefs` (config file first, env fallback — see config.ts);
|
|
164
|
+
* only the app-name normalization lives here. Priority: the explicit command
|
|
165
|
+
* template (the universal escape hatch) → a built-in launcher by app name
|
|
166
|
+
* (aliases are case- and `.app`-suffix-insensitive) → plain Terminal.app
|
|
167
|
+
* (macOS has no default-terminal setting to detect). Any other unmatched
|
|
167
168
|
* name passes through to `open -a` unchanged.
|
|
168
169
|
*/
|
|
169
|
-
export function resolveTerminalLaunch(env) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
170
|
+
export function resolveTerminalLaunch(env, prefs = remoteTerminalPrefs(env)) {
|
|
171
|
+
if (prefs.command)
|
|
172
|
+
return { launch: { kind: "shell", command: prefs.command } };
|
|
173
|
+
const raw = prefs.app ?? "";
|
|
174
|
+
const name = raw
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.replace(/\.app$/, "")
|
|
177
|
+
.replace(/\s+/g, "-");
|
|
175
178
|
const launcher = TERMINAL_APP_LAUNCHERS[name];
|
|
176
179
|
if (launcher)
|
|
177
180
|
return { launch: launcher };
|
|
178
|
-
if (name === "warp") {
|
|
179
|
-
return {
|
|
180
|
-
launch: { kind: "openApp", app: raw || "Warp" },
|
|
181
|
-
warning: "Warp cannot execute .command scripts (warpdotdev/warp#1917) and has no " +
|
|
182
|
-
"programmatic command execution (warpdotdev/warp#3959, #9083) — the window " +
|
|
183
|
-
"will idle and session-create falls back to a headless bridge after the " +
|
|
184
|
-
"register timeout; set ZCODE_ACP_HUB_TERMINAL_COMMAND to drive Warp another way",
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
181
|
return { launch: { kind: "openApp", app: raw || "Terminal" } };
|
|
188
182
|
}
|
|
189
183
|
/**
|
|
@@ -196,13 +190,22 @@ export function resolveTerminalLaunch(env) {
|
|
|
196
190
|
*/
|
|
197
191
|
export function terminalTuiScript(cwd, cliJs, env) {
|
|
198
192
|
const exports = Object.keys(env)
|
|
199
|
-
|
|
193
|
+
// DSH_TUI_AUTOPROMPT is the one non-ZCODE_ACP_* passenger: the boot-resume
|
|
194
|
+
// banner handshake (martty reads it at its own process start).
|
|
195
|
+
.filter((k) => k.startsWith("ZCODE_ACP_") || k === "DSH_TUI_AUTOPROMPT")
|
|
200
196
|
.map((k) => `export ${k}=${shQuote(String(env[k]))}`);
|
|
201
197
|
return [
|
|
202
198
|
"#!/bin/sh",
|
|
203
199
|
`# Hub-incubated TUI session (ADR-0016): closing this window ends the bridge.`,
|
|
204
200
|
`cd ${shQuote(cwd)} || exit 1`,
|
|
205
201
|
...exports,
|
|
202
|
+
// OSC 0 names the tab after the conversation — without it terminals show
|
|
203
|
+
// the running process ("node"). printf reads the \033/\007 escapes from
|
|
204
|
+
// the format string; %s keeps the value itself shell-safe. martty never
|
|
205
|
+
// sets a terminal title, so this survives until the window closes.
|
|
206
|
+
...(env.ZCODE_ACP_TAB_TITLE !== undefined
|
|
207
|
+
? [`printf '\\033]0;%s\\007' "$ZCODE_ACP_TAB_TITLE"`]
|
|
208
|
+
: []),
|
|
206
209
|
`exec ${shQuote(process.execPath)} ${shQuote(cliJs)}`,
|
|
207
210
|
"",
|
|
208
211
|
].join("\n");
|
|
@@ -222,8 +225,12 @@ export function terminalTuiScript(cwd, cliJs, env) {
|
|
|
222
225
|
async function spawnTerminalTui(opts) {
|
|
223
226
|
if (process.platform !== "darwin")
|
|
224
227
|
return null;
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
// Terminal prefs are read LIVE here (config file first, env fallback): the
|
|
229
|
+
// hub outlives the shells that configured it, and its birth env rotates
|
|
230
|
+
// between GUI editors and interactive shells — only the file is stable.
|
|
231
|
+
// Set remote.terminal.enabled=false in ~/.config/zcode-acp/config.json (or
|
|
232
|
+
// ZCODE_ACP_HUB_TERMINAL=0) to keep remote session-create headless.
|
|
233
|
+
if (!remoteTerminalPrefs(process.env).enabled)
|
|
227
234
|
return null;
|
|
228
235
|
const cliJs = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
229
236
|
const script = path.join(mkdtempSync(path.join(tmpdir(), "zcode-acp-term-")), "tui.command");
|
|
@@ -242,6 +249,16 @@ async function spawnTerminalTui(opts) {
|
|
|
242
249
|
else if (launch.kind === "openApp") {
|
|
243
250
|
argv = ["open", "-a", launch.app, script];
|
|
244
251
|
}
|
|
252
|
+
else if (launch.kind === "warpUri") {
|
|
253
|
+
// new_tab = Warp's default open mode (like Cmd+T: a tab in the focused
|
|
254
|
+
// window; Warp opens a window first if none exists).
|
|
255
|
+
argv = [
|
|
256
|
+
"open",
|
|
257
|
+
"-a",
|
|
258
|
+
launch.app,
|
|
259
|
+
`${launch.scheme}://action/new_tab?path=${encodeURIComponent(script)}`,
|
|
260
|
+
];
|
|
261
|
+
}
|
|
245
262
|
else {
|
|
246
263
|
argv = ["open", "-na", launch.app, "--args", ...launch.args, "/bin/sh", script];
|
|
247
264
|
}
|
|
@@ -371,6 +388,36 @@ const MAX_SESSION_LIST_BYTES = 4 * 1024 * 1024;
|
|
|
371
388
|
* gives its own query 15s).
|
|
372
389
|
*/
|
|
373
390
|
const SESSION_LIST_TIMEOUT_MS = 12_000;
|
|
391
|
+
/** Best-effort budget for the resume tab-title lookup — the window must
|
|
392
|
+
* never wait on a slow bridge (the 12s list budget is for listings, not
|
|
393
|
+
* for cosmetics). */
|
|
394
|
+
const TITLE_LOOKUP_BUDGET_MS = 2_000;
|
|
395
|
+
/**
|
|
396
|
+
* Tab-title sanitizer: the value is printf'd into the terminal as an OSC
|
|
397
|
+
* payload, so control characters (an ESC inside a model-generated summary
|
|
398
|
+
* would inject terminal sequences) become spaces, whitespace collapses, and
|
|
399
|
+
* the length caps at what a tab can usefully show. Empty/non-string →
|
|
400
|
+
* undefined (the caller falls back to the project name).
|
|
401
|
+
*/
|
|
402
|
+
export function sanitizeTabTitle(v) {
|
|
403
|
+
if (typeof v !== "string")
|
|
404
|
+
return undefined;
|
|
405
|
+
const clean = v
|
|
406
|
+
.replace(/[\p{Cc}]/gu, " ")
|
|
407
|
+
.replace(/\s+/g, " ")
|
|
408
|
+
.trim()
|
|
409
|
+
.slice(0, 80);
|
|
410
|
+
return clean.length > 0 ? clean : undefined;
|
|
411
|
+
}
|
|
412
|
+
/** Resolve with undefined after `ms` — for best-effort lookups that must not
|
|
413
|
+
* stall their caller; the losing promise settles into the void (its own
|
|
414
|
+
* rejection is the caller's `.catch`, never unhandled). */
|
|
415
|
+
function withTimeout(p, ms) {
|
|
416
|
+
const timeout = new Promise((resolve) => {
|
|
417
|
+
setTimeout(() => resolve(undefined), ms).unref?.();
|
|
418
|
+
});
|
|
419
|
+
return Promise.race([p, timeout]);
|
|
420
|
+
}
|
|
374
421
|
/**
|
|
375
422
|
* Fetch a bridge's GET /sessions payload (ADR-0015), forwarding the
|
|
376
423
|
* pagination query ("?limit=..&before=.."). Resolves with the parsed JSON;
|
|
@@ -527,7 +574,7 @@ export function startHub(options) {
|
|
|
527
574
|
* requested session (ADR-0017), "serve" the detached headless bridge
|
|
528
575
|
* (background listing, ADR-0015).
|
|
529
576
|
*/
|
|
530
|
-
const incubateServe = async (workspacePath, kind, sessionId) => {
|
|
577
|
+
const incubateServe = async (workspacePath, kind, sessionId, tabTitle) => {
|
|
531
578
|
// Async spawn failures (ENOENT — the dir vanished between the whitelist
|
|
532
579
|
// check and the spawn) arrive as an 'error' event with exitCode still
|
|
533
580
|
// null; without this flag the poll burns the full budget.
|
|
@@ -562,6 +609,19 @@ export function startHub(options) {
|
|
|
562
609
|
// TUI boots into it. The detached serve fallback ignores it; the
|
|
563
610
|
// client attaches to the serve bridge and session/loads there instead.
|
|
564
611
|
env.ZCODE_ACP_RESUME_SESSION = sessionId;
|
|
612
|
+
// Banner handshake (see BOOT_RESUME_TRIGGER): martty auto-submits this
|
|
613
|
+
// text at boot, which drops its welcome banner and reveals the
|
|
614
|
+
// boot-replayed history without waiting for the user's first message.
|
|
615
|
+
// Must ride the env verbatim — martty reads it once at process start.
|
|
616
|
+
env.DSH_TUI_AUTOPROMPT = BOOT_RESUME_TRIGGER;
|
|
617
|
+
}
|
|
618
|
+
if (kind !== "serve") {
|
|
619
|
+
// Tab title (both visible-terminal kinds): a resume carries the
|
|
620
|
+
// conversation's title when the hub resolved it; session-create and a
|
|
621
|
+
// failed lookup fall back to the project name. Rides the env —
|
|
622
|
+
// terminalTuiScript turns it into an OSC 0 before exec'ing the CLI
|
|
623
|
+
// (terminals otherwise name the tab after the process, "node").
|
|
624
|
+
env.ZCODE_ACP_TAB_TITLE = tabTitle ?? path.basename(workspacePath);
|
|
565
625
|
}
|
|
566
626
|
try {
|
|
567
627
|
// Resume shares session-create's visible-terminal surface (and its
|
|
@@ -624,14 +684,14 @@ export function startHub(options) {
|
|
|
624
684
|
* key carries the session id, so resumes of different sessions each get
|
|
625
685
|
* their own window.
|
|
626
686
|
*/
|
|
627
|
-
const joinIncubation = (workspacePath, kind, sessionId) => {
|
|
687
|
+
const joinIncubation = (workspacePath, kind, sessionId, tabTitle) => {
|
|
628
688
|
const mapKey = kind === "resume" ? `${workspacePath}\u0000resume\u0000${sessionId ?? ""}` : workspacePath;
|
|
629
689
|
const inflight = serveIncubations.get(mapKey);
|
|
630
690
|
if (inflight && (inflight.kind === kind || kind === "serve")) {
|
|
631
691
|
log(`hub: joining the incubating serve bridge for ${workspacePath}`);
|
|
632
692
|
return inflight.promise;
|
|
633
693
|
}
|
|
634
|
-
const promise = incubateServe(workspacePath, kind, sessionId);
|
|
694
|
+
const promise = incubateServe(workspacePath, kind, sessionId, tabTitle);
|
|
635
695
|
serveIncubations.set(mapKey, { kind, promise });
|
|
636
696
|
// Drop the entry once settled (identity-checked — a newer incubation may
|
|
637
697
|
// already have replaced it). The catch keeps the DERIVED promise handled;
|
|
@@ -969,13 +1029,26 @@ export function startHub(options) {
|
|
|
969
1029
|
return;
|
|
970
1030
|
}
|
|
971
1031
|
if (resumeSid) {
|
|
1032
|
+
// Tab title, best-effort: the App browsed this conversation through
|
|
1033
|
+
// the serve bridge's /sessions history, which is normally still live
|
|
1034
|
+
// and still holds the title. Every miss (no live instance, id beyond
|
|
1035
|
+
// the page, slow fetch) falls back to the project name inside
|
|
1036
|
+
// incubateServe — the window must never wait on this lookup.
|
|
1037
|
+
let tabTitle;
|
|
1038
|
+
const titleSource = findServeInstance(workspacePath);
|
|
1039
|
+
if (titleSource) {
|
|
1040
|
+
const row = await withTimeout(fetchBridgeSessions(titleSource.port, "?limit=200")
|
|
1041
|
+
.then((payload) => (payload.sessions ?? []).find((s) => s.sessionId === resumeSid))
|
|
1042
|
+
.catch(() => undefined), TITLE_LOOKUP_BUDGET_MS);
|
|
1043
|
+
tabTitle = sanitizeTabTitle(row?.title);
|
|
1044
|
+
}
|
|
972
1045
|
// Resume never reuses the live serve bridge: the window IS the
|
|
973
1046
|
// feature, and the answer must be the NEW instance so the attaching
|
|
974
1047
|
// client and the terminal share one bridge (and one backend process)
|
|
975
1048
|
// for the session. A bogus id still opens the window — the TUI
|
|
976
1049
|
// shows the load failure and falls back to a fresh session, the same
|
|
977
1050
|
// honesty as a window that dies early (ADR-0016 §4).
|
|
978
|
-
const incubation = joinIncubation(workspacePath, "resume", resumeSid);
|
|
1051
|
+
const incubation = joinIncubation(workspacePath, "resume", resumeSid, tabTitle);
|
|
979
1052
|
try {
|
|
980
1053
|
const out = await incubation;
|
|
981
1054
|
res.writeHead(200, { "Content-Type": "application/json" });
|