@pablopunk/webr 0.1.1 → 0.1.3
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/README.md +12 -4
- package/dist/client/_astro/App.1PYvyCIo.css +1 -0
- package/dist/client/_astro/App.B3aX6cef.js +2 -0
- package/dist/server/chunks/{App_BmsGAwGf.mjs → App_CmESMUHE.mjs} +15 -14
- package/dist/server/chunks/{_id__u0ueX6GO.mjs → _id__CQzMt2xL.mjs} +7 -6
- package/dist/server/chunks/{_tab__DL6wyr-j.mjs → _tab__D-smYrCo.mjs} +7 -6
- package/dist/server/chunks/_virtual_astro_session-driver_Dp7RXjOf.mjs +123 -0
- package/dist/server/chunks/{compiler_CpXox_xS.mjs → compiler_BiU10Vds.mjs} +1 -1
- package/dist/server/chunks/{console_CucE1SwS.mjs → console_BOi2ismd.mjs} +6 -6
- package/dist/server/chunks/dist_CpTFMtqT.mjs +9199 -0
- package/dist/server/chunks/{index_DyyPce_k.mjs → index_DRfLd22k.mjs} +5 -4
- package/dist/server/chunks/{new_G7XChx5p.mjs → new_NXgn74q9.mjs} +7 -6
- package/dist/server/chunks/{node_BlixNyYV.mjs → node_BcIF-BVp.mjs} +461 -14
- package/dist/server/chunks/rolldown-runtime_BMI-E3GI.mjs +44 -0
- package/dist/server/chunks/{server_DS45LC4q.mjs → server_CnGYDmxV.mjs} +271 -15
- package/dist/server/chunks/{settings_DLV2Js3e.mjs → settings_D1jcCWzt.mjs} +7 -6
- package/dist/server/chunks/{sharp_CgNvPCpY.mjs → sharp_qIaxzsTp.mjs} +2 -2
- package/dist/server/chunks/vnode-children_BSi0H4PL.mjs +213 -0
- package/dist/server/entry.mjs +18980 -5126
- package/package.json +1 -1
- package/server/cli.ts +15 -11
- package/server/plugin/probe.ts +3 -3
- package/server/port.ts +53 -4
- package/server/run.ts +3 -3
- package/server/where.ts +22 -0
- package/src/server/directories.ts +12 -5
- package/src/server/host.ts +3 -4
- package/src/server/runtime/target.ts +1 -0
- package/src/server/transport/herdr-target.ts +21 -7
- package/src/server/transport/registry.ts +1 -1
- package/src/server/transport/saved-machines.ts +41 -0
- package/dist/client/_astro/App.BxlQhLhN.css +0 -1
- package/dist/client/_astro/App.CzxEZ_Dw.js +0 -2
- package/dist/server/chunks/_virtual_astro_session-driver_DS5V7T-N.mjs +0 -6
- package/dist/server/chunks/vnode-children_CRmaSs5G.mjs +0 -24
package/package.json
CHANGED
package/server/cli.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { parseArgs } from 'node:util';
|
|
|
3
3
|
import { renderUnicode } from 'uqr';
|
|
4
4
|
import { webrHome } from './home';
|
|
5
5
|
import { runServer } from './run';
|
|
6
|
-
import {
|
|
6
|
+
import { explicitPort, knownPort } from './port';
|
|
7
7
|
import { defaultDeps, installPlugin, pluginStatus, restartPluginWebr, uninstallWebrPlugin } from './plugin';
|
|
8
8
|
import { checkHerdr } from './herdr-check';
|
|
9
|
+
import { openWebr, statusLines } from './where';
|
|
9
10
|
import { packageInfo } from './package-info';
|
|
10
11
|
import { realUpdateEnvironment, runUpdate } from './update';
|
|
11
12
|
import { refreshLatestVersionCache, updateHint } from './update/hint';
|
|
@@ -18,12 +19,13 @@ Usage
|
|
|
18
19
|
webr plugin uninstall Remove the Herdr plugin
|
|
19
20
|
webr plugin status Show whether the plugin is installed and Webr is running
|
|
20
21
|
webr invite [--port <port>] Print a one-time code and QR to connect a device
|
|
22
|
+
webr status Show whether Webr is running and where
|
|
23
|
+
webr open Open Webr in your browser
|
|
21
24
|
webr update Install the latest version and restart the Herdr plugin
|
|
22
25
|
|
|
23
26
|
Options
|
|
24
|
-
--port <port> Port to listen on (default
|
|
25
|
-
--
|
|
26
|
-
--host <address> Listen on a specific address (default 127.0.0.1)
|
|
27
|
+
--port <port> Port to listen on (default: first free port from 4444, then remembered)
|
|
28
|
+
--host <address> Listen on a specific address (default 0.0.0.0, every interface)
|
|
27
29
|
--origin <url> Public URL when served through a proxy, e.g. https://mac.tailnet.ts.net
|
|
28
30
|
-v, --version Print the version
|
|
29
31
|
-h, --help Print this help
|
|
@@ -33,15 +35,15 @@ Localhost never needs a token. Any other device asks for access and you approve
|
|
|
33
35
|
`;
|
|
34
36
|
|
|
35
37
|
const options = {
|
|
36
|
-
port: { type: 'string' }, host: { type: 'string' }, origin: { type: 'string' },
|
|
38
|
+
port: { type: 'string' }, host: { type: 'string' }, origin: { type: 'string' },
|
|
37
39
|
help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' },
|
|
38
40
|
} as const;
|
|
39
41
|
|
|
40
42
|
function parseCommand(argv: string[]) { return parseArgs({ args: argv, options, allowPositionals: true }); }
|
|
41
43
|
|
|
42
44
|
function applyServerFlags(values: ReturnType<typeof parseCommand>['values']) {
|
|
43
|
-
if (values.port) process.env.PORT = String(
|
|
44
|
-
if (values.
|
|
45
|
+
if (values.port) process.env.PORT = String(explicitPort(['--port', values.port], {}));
|
|
46
|
+
if (values.host) process.env.HOST = values.host;
|
|
45
47
|
if (values.origin) process.env.WEBR_ORIGIN = values.origin;
|
|
46
48
|
process.env.WEBR_DATABASE ??= join(webrHome(), 'gateway.sqlite');
|
|
47
49
|
}
|
|
@@ -64,12 +66,12 @@ async function invite(port: number) {
|
|
|
64
66
|
const { token, urls } = await response.json() as { token: string; urls: string[] };
|
|
65
67
|
if (urls[0]) console.log(`${renderUnicode(urls[0], { border: 1, invert: true })}\n${urls[0]}\n`);
|
|
66
68
|
console.log(`Code: ${token}\nExpires in 5 minutes. Works once.`);
|
|
67
|
-
if (!urls.length) console.log('Webr is only reachable from this computer.
|
|
69
|
+
if (!urls.length) console.log('Webr is only reachable from this computer. It was started with --host set to a local address, or no network interface was found.');
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
const pluginSettings = (values: ReturnType<typeof parseCommand>['values']) => ({
|
|
71
|
-
...(values.port ? { port:
|
|
72
|
-
...(values.
|
|
73
|
+
...(values.port ? { port: explicitPort(['--port', values.port], {}) } : {}),
|
|
74
|
+
...(values.host ? { host: values.host } : {}),
|
|
73
75
|
...(values.origin ? { origin: values.origin } : {}),
|
|
74
76
|
});
|
|
75
77
|
|
|
@@ -87,7 +89,9 @@ export async function main(argv: string[]) {
|
|
|
87
89
|
if (command === 'start') { applyServerFlags(values); await requireHerdr(); return runServer(); }
|
|
88
90
|
if (command === 'update') return void (await runUpdate(realUpdateEnvironment(() => restartPluginWebr(defaultDeps())))).forEach((line) => console.log(line));
|
|
89
91
|
if (command === 'refresh-update-cache') return refreshLatestVersionCache().catch(() => undefined);
|
|
90
|
-
if (command === '
|
|
92
|
+
if (command === 'status') return void (await statusLines(knownPort(values.port ? ['--port', values.port] : []))).forEach((line) => console.log(line));
|
|
93
|
+
if (command === 'open') return void (await openWebr(knownPort(values.port ? ['--port', values.port] : []))).forEach((line) => console.log(line));
|
|
94
|
+
if (command === 'invite') return invite(knownPort(values.port ? ['--port', values.port] : []));
|
|
91
95
|
if (command === 'plugin' && ['install', 'uninstall', 'status'].includes(subcommand ?? '')) return pluginCommand(subcommand!, values);
|
|
92
96
|
throw new Error(`Unknown command: ${positionals.join(' ')}\n\n${HELP}`);
|
|
93
97
|
}
|
package/server/plugin/probe.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createConnection } from 'node:net';
|
|
2
|
+
import { FIRST_PORT, storedPort } from '../port';
|
|
2
3
|
import type { PluginConfig } from './config';
|
|
3
4
|
|
|
4
|
-
const DEFAULT_PORT = 4321;
|
|
5
5
|
const WILDCARD_HOSTS = ['0.0.0.0', '::'];
|
|
6
6
|
|
|
7
|
-
export const probeAddress = ({ port, host }: PluginConfig) => ({
|
|
8
|
-
port: port ??
|
|
7
|
+
export const probeAddress = ({ port, host }: PluginConfig, remembered = storedPort()) => ({
|
|
8
|
+
port: port ?? remembered ?? FIRST_PORT,
|
|
9
9
|
host: !host || WILDCARD_HOSTS.includes(host) ? '127.0.0.1' : host,
|
|
10
10
|
});
|
|
11
11
|
|
package/server/port.ts
CHANGED
|
@@ -1,8 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { webrHome } from './home';
|
|
5
|
+
|
|
6
|
+
export const FIRST_PORT = 4444;
|
|
7
|
+
const LAST_PORT = 65535;
|
|
8
|
+
const PORT_FILE = 'port';
|
|
9
|
+
|
|
2
10
|
const portArgument = (args: string[]) => { const index = args.indexOf('--port'); return index >= 0 ? args[index + 1] : args.find((arg) => arg.startsWith('--port='))?.slice('--port='.length); };
|
|
11
|
+
const isPort = (port: number) => Number.isInteger(port) && port >= 1 && port <= LAST_PORT;
|
|
12
|
+
const portFile = (home: string) => join(home, PORT_FILE);
|
|
13
|
+
|
|
14
|
+
export function explicitPort(args = process.argv.slice(2), env: NodeJS.ProcessEnv = process.env) {
|
|
15
|
+
const value = portArgument(args) ?? env.PORT;
|
|
16
|
+
if (value === undefined) return undefined;
|
|
17
|
+
if (!isPort(Number(value))) throw new Error('The port must be a number from 1 to 65535.');
|
|
18
|
+
return Number(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function storedPort(home = webrHome()) {
|
|
22
|
+
try { const port = Number(readFileSync(portFile(home), 'utf8').trim()); return isPort(port) ? port : undefined; } catch { return undefined; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const canListen = (port: number, host?: string) => new Promise<boolean>((done) => {
|
|
26
|
+
const probe = createServer();
|
|
27
|
+
probe.once('error', () => done(false));
|
|
28
|
+
probe.listen(port, host, () => probe.close(() => done(true)));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const isFree = async (port: number) => (await canListen(port, '127.0.0.1')) && (await canListen(port));
|
|
32
|
+
|
|
33
|
+
async function firstFreePortFrom(start: number) {
|
|
34
|
+
for (let port = start; port <= LAST_PORT; port++) if (await isFree(port)) return port;
|
|
35
|
+
throw new Error('No free port found.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rememberPort(home: string, port: number) {
|
|
39
|
+
mkdirSync(home, { recursive: true });
|
|
40
|
+
writeFileSync(portFile(home), `${port}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function resolvePort(args = process.argv.slice(2), env: NodeJS.ProcessEnv = process.env, home = webrHome(env)) {
|
|
44
|
+
const explicit = explicitPort(args, env);
|
|
45
|
+
if (explicit) return explicit;
|
|
46
|
+
const stored = storedPort(home);
|
|
47
|
+
if (stored) return stored;
|
|
48
|
+
const claimed = await firstFreePortFrom(FIRST_PORT);
|
|
49
|
+
rememberPort(home, claimed);
|
|
50
|
+
return claimed;
|
|
51
|
+
}
|
|
3
52
|
|
|
4
|
-
export function
|
|
5
|
-
const port =
|
|
6
|
-
if (!
|
|
53
|
+
export function knownPort(args = process.argv.slice(2), env: NodeJS.ProcessEnv = process.env, home = webrHome(env)) {
|
|
54
|
+
const port = explicitPort(args, env) ?? storedPort(home);
|
|
55
|
+
if (!port) throw new Error('Webr has not started yet. Run "webr start", or start Herdr with the plugin installed.');
|
|
7
56
|
return port;
|
|
8
57
|
}
|
package/server/run.ts
CHANGED
|
@@ -7,14 +7,14 @@ import { createHost } from '../src/server/host';
|
|
|
7
7
|
import { publicUrls } from '../src/server/public-urls';
|
|
8
8
|
import { detectTailscaleHost } from '../src/server/tailscale-host';
|
|
9
9
|
import { isLoopbackHost } from '../src/server/auth/access';
|
|
10
|
-
import {
|
|
10
|
+
import { resolvePort } from './port';
|
|
11
11
|
import { builtSsr, devSsr } from './ssr';
|
|
12
12
|
|
|
13
13
|
const WILDCARD_HOSTS = ['0.0.0.0', '::'];
|
|
14
14
|
|
|
15
15
|
export async function runServer() {
|
|
16
|
-
const host = process.env.HOST ?? '
|
|
17
|
-
const port =
|
|
16
|
+
const host = process.env.HOST ?? '0.0.0.0';
|
|
17
|
+
const port = await resolvePort();
|
|
18
18
|
const tlsEnabled = !!(process.env.WEBR_TLS_CERT && process.env.WEBR_TLS_KEY);
|
|
19
19
|
const origin = process.env.WEBR_ORIGIN ?? `${tlsEnabled ? 'https' : 'http'}://${WILDCARD_HOSTS.includes(host) ? 'localhost' : host}:${port}`;
|
|
20
20
|
if (new URL(origin).origin !== origin || new URL(origin).username || new URL(origin).password) throw new Error('WEBR_ORIGIN must be a normalized HTTP or HTTPS origin without credentials or a path');
|
package/server/where.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isPortOpen } from './plugin/probe';
|
|
3
|
+
|
|
4
|
+
const localUrl = (port: number) => `http://localhost:${port}`;
|
|
5
|
+
const launcher = (platform = process.platform) => platform === 'darwin' ? ['open'] : platform === 'win32' ? ['cmd', '/c', 'start', ''] : ['xdg-open'];
|
|
6
|
+
|
|
7
|
+
export async function statusLines(port: number) {
|
|
8
|
+
if (await isPortOpen({ host: '127.0.0.1', port })) return [`Webr is running at ${localUrl(port)}`];
|
|
9
|
+
return [`Webr is not running. Its address is ${localUrl(port)}.`, 'It starts with the Herdr server once the plugin is installed ("webr plugin install"), or run "webr start".'];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function openWebr(port: number, open = openInBrowser) {
|
|
13
|
+
const lines = await statusLines(port);
|
|
14
|
+
if (lines.length > 1) return lines;
|
|
15
|
+
open(localUrl(port));
|
|
16
|
+
return [`Opening ${localUrl(port)}`];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function openInBrowser(url: string) {
|
|
20
|
+
const [command, ...args] = launcher();
|
|
21
|
+
spawn(command!, [...args, url], { detached: true, stdio: 'ignore' }).unref();
|
|
22
|
+
}
|
|
@@ -9,13 +9,20 @@ export const expandHome = (path: string) => path === '~' ? homedir() : path.star
|
|
|
9
9
|
const splitAtLastSlash = (prefix: string) => ({ parent: prefix.slice(0, prefix.lastIndexOf('/') + 1), partial: prefix.slice(prefix.lastIndexOf('/') + 1) });
|
|
10
10
|
const startsWithPartial = (partial: string) => (name: string) => name.toLowerCase().startsWith(partial.toLowerCase()) && (partial.startsWith('.') || !name.startsWith('.'));
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export type DirectoryLister = (parent: string) => Promise<string[]>;
|
|
13
|
+
|
|
14
|
+
export const listLocalDirectories: DirectoryLister = async (parent) => {
|
|
15
|
+
const entries = await readdir(expandHome(parent), { withFileTypes: true }).catch(() => []);
|
|
16
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function suggestDirectories(prefix: string, listDirectories: DirectoryLister = listLocalDirectories): Promise<string[]> {
|
|
13
20
|
if (!prefix.startsWith('/') && !prefix.startsWith('~')) return [];
|
|
14
21
|
const { parent, partial } = splitAtLastSlash(prefix.includes('/') ? prefix : `${prefix}/`);
|
|
15
|
-
const
|
|
16
|
-
return
|
|
17
|
-
.filter(
|
|
18
|
-
.map((
|
|
22
|
+
const names = await listDirectories(parent);
|
|
23
|
+
return names
|
|
24
|
+
.filter(startsWithPartial(partial))
|
|
25
|
+
.map((name) => `${parent}${name}/`)
|
|
19
26
|
.sort((a, b) => a.localeCompare(b))
|
|
20
27
|
.slice(0, MAX_SUGGESTIONS);
|
|
21
28
|
}
|
package/src/server/host.ts
CHANGED
|
@@ -8,7 +8,6 @@ import type { RuntimeManager } from './runtime/manager';
|
|
|
8
8
|
import { launchInput, opaqueId } from '../shared/runtime';
|
|
9
9
|
import { registerWebsockets } from './websockets';
|
|
10
10
|
import { createWorkspace } from './runtime/workspace';
|
|
11
|
-
import { expandHome, suggestDirectories } from './directories';
|
|
12
11
|
import { perfLog, perfLogEnabled } from './perf-log';
|
|
13
12
|
import { Auth, type AuthOptions } from './auth/auth';
|
|
14
13
|
import { isTrustedHost } from './auth/access';
|
|
@@ -63,8 +62,8 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
|
|
|
63
62
|
app.get('/api/directories', async (request, reply) => {
|
|
64
63
|
const { machineId, prefix } = z.object({ machineId: opaqueId, prefix: z.string().max(1000).refine((path) => !/[\x00-\x1f]/.test(path)) }).strict().parse(request.query);
|
|
65
64
|
const supervisor = manager.supervisors.get(machineId);
|
|
66
|
-
if (!supervisor?.target.
|
|
67
|
-
return suggestDirectories(prefix);
|
|
65
|
+
if (!supervisor?.connected || !supervisor.target.suggestDirectories) return reply.code(409).send({ error: 'directories_unsupported_target' });
|
|
66
|
+
return supervisor.target.suggestDirectories(prefix);
|
|
68
67
|
});
|
|
69
68
|
app.get('/api/worktrees', async (request) => {
|
|
70
69
|
const { machineId, projectId } = z.object({ machineId: opaqueId, projectId: opaqueId }).strict().parse(request.query);
|
|
@@ -79,7 +78,7 @@ export async function createHost(manager: RuntimeManager, origin: string, ssr?:
|
|
|
79
78
|
const key = z.uuid().parse(request.headers['idempotency-key']);
|
|
80
79
|
const supervisor = manager.supervisors.get(input.machineId);
|
|
81
80
|
if (!supervisor?.connected) return reply.code(409).send({ error: 'machine_disconnected' });
|
|
82
|
-
const result = await createWorkspace(manager.database, supervisor.target, key,
|
|
81
|
+
const result = await createWorkspace(manager.database, supervisor.target, key, input.path, input.label);
|
|
83
82
|
supervisor.invalidate();
|
|
84
83
|
return reply.code(result.state === 'unknown' ? 202 : 201).send(result);
|
|
85
84
|
});
|
|
@@ -22,6 +22,7 @@ export type TargetAdapter = {
|
|
|
22
22
|
icon?(projectId: string): Promise<Icon | undefined>;
|
|
23
23
|
worktrees?(projectId: string): Promise<WorktreeEntry[]>;
|
|
24
24
|
openWorktree?(projectId: string, path: string): Promise<void>;
|
|
25
|
+
suggestDirectories?(prefix: string): Promise<string[]>;
|
|
25
26
|
createWorkspace?(path: string, label: string, requestId: string): Promise<{ workspaceId: string; tabId: string; terminalId: string }>;
|
|
26
27
|
openTerminal(terminalId: string, mode: 'control' | 'observe', cols: number, rows: number, takeover: boolean, onFrame: (frame: TerminalFrame) => void, onClose: (reason: string) => void): TerminalStream;
|
|
27
28
|
create(input: LaunchInput, threadId: string): Promise<{ tabId: string; paneId: string; terminalId: string; workspaceId: string }>;
|
|
@@ -18,6 +18,7 @@ import { scopedProjectId } from '../../shared/projects';
|
|
|
18
18
|
import { validateInstalledSchema } from '../protocol/validate';
|
|
19
19
|
import { ensureLocalSession } from './local-session';
|
|
20
20
|
import { nativeLocations } from './native-locations';
|
|
21
|
+
import { expandHome, listLocalDirectories, suggestDirectories } from '../directories';
|
|
21
22
|
|
|
22
23
|
const MODEL_LISTING_TIMEOUT_MS = 15_000;
|
|
23
24
|
const MODEL_LISTING_LIMIT = 4 * 1024 * 1024;
|
|
@@ -50,7 +51,7 @@ export class HerdrTarget implements TargetAdapter {
|
|
|
50
51
|
if (this.connecting) return this.connecting;
|
|
51
52
|
if (this.api) return this.api;
|
|
52
53
|
this.connecting ??= (async () => {
|
|
53
|
-
if (!this.profile.enabled || !
|
|
54
|
+
if (!this.profile.enabled || !this.profile.automatic && process.env.WEBR_CONNECT !== '1') throw new Error('connection_not_approved');
|
|
54
55
|
if (this.profile.automatic && this.profile.transport === 'local') await ensureLocalSession(this.profile);
|
|
55
56
|
if (this.profile.transport === 'ssh') {
|
|
56
57
|
this.forwarding = new SshForward(this.profile.host!, this.profile.socket!);
|
|
@@ -77,12 +78,12 @@ export class HerdrTarget implements TargetAdapter {
|
|
|
77
78
|
async snapshot() {
|
|
78
79
|
const snapshot = nativeSnapshot.parse((await (await this.connect()).request('session.snapshot')).snapshot);
|
|
79
80
|
if (snapshot.version !== '0.9.3') { this.compatible = false; throw new Error('unsupported_herdr_version'); }
|
|
80
|
-
if (this.profile.automatic
|
|
81
|
+
if (this.profile.automatic) this.locations.splice(0, this.locations.length, ...nativeLocations(this.id, snapshot));
|
|
81
82
|
return snapshot;
|
|
82
83
|
}
|
|
83
84
|
async createWorkspace(path: string, label: string, requestId: string) {
|
|
84
|
-
if (
|
|
85
|
-
const result = await (await this.connect()).request('workspace.create', { cwd: path, label, focus: false }, { timeoutMs: 30_000, requestId });
|
|
85
|
+
if (!this.profile.automatic || !this.compatible) throw new Error('workspace_creation_unavailable');
|
|
86
|
+
const result = await (await this.connect()).request('workspace.create', { cwd: await this.expandHome(path), label, focus: false }, { timeoutMs: 30_000, requestId });
|
|
86
87
|
const workspace = nativeWorkspace.parse(result.workspace); const tab = nativeTab.parse(result.tab); const pane = nativePane.parse(result.root_pane);
|
|
87
88
|
if (tab.workspace_id !== workspace.workspace_id || pane.workspace_id !== workspace.workspace_id || pane.tab_id !== tab.tab_id) throw new Error('workspace_identity_mismatch');
|
|
88
89
|
return { workspaceId: workspace.workspace_id, tabId: tab.tab_id, terminalId: pane.terminal_id };
|
|
@@ -90,15 +91,28 @@ export class HerdrTarget implements TargetAdapter {
|
|
|
90
91
|
async catalog(_projectId?: string): Promise<Machine> {
|
|
91
92
|
const api = await this.connect();
|
|
92
93
|
await api.request('ping');
|
|
93
|
-
const run = (command: string, args: string[]) => this.
|
|
94
|
-
? this.dependencies.process(command, args, undefined, MODEL_LISTING_TIMEOUT_MS, MODEL_LISTING_LIMIT)
|
|
95
|
-
: this.dependencies.process('ssh', [...sshOptions, this.profile.host!, [command, ...args].map(quoteShell).join(' ')], undefined, MODEL_LISTING_TIMEOUT_MS, MODEL_LISTING_LIMIT);
|
|
94
|
+
const run = (command: string, args: string[]) => this.runOnTarget(command, args);
|
|
96
95
|
const isPresent = (kind: string) => run('/bin/sh', ['-c', 'command -v "$1" >/dev/null', 'sh', kind]).then(() => true, () => false);
|
|
97
96
|
const listModels = (kind: string) => modelListings[kind] ? run(kind, modelListings[kind].args).then((output) => parseModelListing(kind, output), () => []) : Promise.resolve([]);
|
|
98
97
|
const present = (await Promise.all(herdrAgentKinds.map(async (kind) => (await isPresent(kind)) ? kind : undefined))).filter((kind): kind is string => !!kind);
|
|
99
98
|
const discovered = await Promise.all(present.map(listModels));
|
|
100
99
|
return { id: this.id, name: this.name, session: this.session, connected: true, writable: this.writable, configVersion: this.configVersion, projectPaths: Object.fromEntries(this.locations.map((location) => [location.projectId, location.path])), harnesses: present.map((id, index) => ({ id, name: harnessName(id), models: ['Default', ...discovered[index]], customModels: acceptsModelFlag(id), launchEnabled: true })) };
|
|
101
100
|
}
|
|
101
|
+
private runOnTarget(command: string, args: string[]) {
|
|
102
|
+
return this.profile.transport === 'local'
|
|
103
|
+
? this.dependencies.process(command, args, undefined, MODEL_LISTING_TIMEOUT_MS, MODEL_LISTING_LIMIT)
|
|
104
|
+
: this.dependencies.process('ssh', [...sshOptions, this.profile.host!, [command, ...args].map(quoteShell).join(' ')], undefined, MODEL_LISTING_TIMEOUT_MS, MODEL_LISTING_LIMIT);
|
|
105
|
+
}
|
|
106
|
+
private async expandHome(path: string) {
|
|
107
|
+
if (this.profile.transport === 'local') return expandHome(path);
|
|
108
|
+
if (path !== '~' && !path.startsWith('~/')) return path;
|
|
109
|
+
return (await this.runOnTarget('/bin/sh', ['-c', 'printf %s "$HOME"'])) + path.slice(1);
|
|
110
|
+
}
|
|
111
|
+
private listRemoteDirectories = async (parent: string) => {
|
|
112
|
+
const script = 'case "$1" in "~") d="$HOME";; "~/"*) d="$HOME/${1#"~/"}";; *) d="$1";; esac; cd "$d" 2>/dev/null && ls -1Ap | grep "/$" | sed "s|/$||"';
|
|
113
|
+
return (await this.runOnTarget('/bin/sh', ['-c', script, 'sh', parent]).catch(() => '')).split('\n').filter(Boolean);
|
|
114
|
+
};
|
|
115
|
+
suggestDirectories(prefix: string) { return suggestDirectories(prefix, this.profile.transport === 'local' ? listLocalDirectories : this.listRemoteDirectories); }
|
|
102
116
|
async icon(projectId: string) {
|
|
103
117
|
const location = this.locations.find((location) => location.projectId === projectId);
|
|
104
118
|
if (this.profile.transport !== 'local' || !location) return;
|
|
@@ -13,7 +13,7 @@ export const profileSchema = z.object({
|
|
|
13
13
|
}).strict().superRefine((value, context) => { if (value.transport === 'ssh' && (!value.host || !value.socket)) context.addIssue({ code: 'custom', message: 'SSH requires a host and a verified absolute socket' }); });
|
|
14
14
|
export type TargetProfile = z.infer<typeof profileSchema>;
|
|
15
15
|
export async function loadRegistry(path?: string): Promise<TargetProfile[]> {
|
|
16
|
-
if (!path) return [await (await import('./local-session')).discoverLocalSession()];
|
|
16
|
+
if (!path) return [await (await import('./local-session')).discoverLocalSession(), ...await (await import('./saved-machines')).discoverSavedMachines()];
|
|
17
17
|
const profiles = z.array(profileSchema).max(32).parse(JSON.parse(await readFile(path, 'utf8')));
|
|
18
18
|
if (new Set(profiles.map((profile) => profile.id)).size !== profiles.length) throw new Error('duplicate_target');
|
|
19
19
|
if (profiles.some((profile) => new Set(profile.locations.map((location) => location.projectId)).size !== profile.locations.length)) throw new Error('duplicate_project_location');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { boundedProcess } from './process';
|
|
6
|
+
import { profileSchema, type TargetProfile } from './registry';
|
|
7
|
+
import { quoteShell, sshOptions } from './ssh';
|
|
8
|
+
|
|
9
|
+
const SESSION_LIST_TIMEOUT_MS = 8000;
|
|
10
|
+
const savedMachines = z.array(z.object({ id: z.string(), label: z.string(), target: z.string(), session: z.string(), enabled: z.boolean() }));
|
|
11
|
+
const sessionsResult = z.object({ sessions: z.array(z.object({ name: z.string(), socket_path: z.string() })) });
|
|
12
|
+
const sshMetadata = z.object({ metadata: z.object({ executable: z.string() }) });
|
|
13
|
+
const herdrState = (env: NodeJS.ProcessEnv) => join(env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'herdr', 'client');
|
|
14
|
+
|
|
15
|
+
async function savedExecutable(id: string, env: NodeJS.ProcessEnv) {
|
|
16
|
+
try { return sshMetadata.parse(JSON.parse(await readFile(join(herdrState(env), 'ssh-metadata', id + '.json'), 'utf8'))).metadata.executable; }
|
|
17
|
+
catch { return undefined; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function remoteSocket(host: string, executable: string, session: string, runProcess: typeof boundedProcess) {
|
|
21
|
+
const command = [executable, 'session', 'list', '--json'].map(quoteShell).join(' ');
|
|
22
|
+
const { sessions } = sessionsResult.parse(JSON.parse(await runProcess('ssh', [...sshOptions, host, command], process.env, SESSION_LIST_TIMEOUT_MS)));
|
|
23
|
+
return sessions.find((candidate) => candidate.name === session)?.socket_path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function profileOf(machine: z.infer<typeof savedMachines>[number], env: NodeJS.ProcessEnv, runProcess: typeof boundedProcess): Promise<TargetProfile | undefined> {
|
|
27
|
+
const executable = await savedExecutable(machine.id, env);
|
|
28
|
+
if (!executable) return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const socket = await remoteSocket(machine.target, executable, machine.session, runProcess);
|
|
31
|
+
return socket ? profileSchema.parse({ id: machine.id, name: machine.label, session: machine.session, enabled: true, automatic: true, transport: 'ssh', host: machine.target, socket, executable, locations: [] }) : undefined;
|
|
32
|
+
} catch { return undefined; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function discoverSavedMachines(env = process.env, runProcess = boundedProcess): Promise<TargetProfile[]> {
|
|
36
|
+
try {
|
|
37
|
+
const machines = savedMachines.parse(JSON.parse(await runProcess('herdr', ['machine', 'list', '--json'], env)));
|
|
38
|
+
const profiles = await Promise.all(machines.filter((machine) => machine.enabled).map((machine) => profileOf(machine, env, runProcess)));
|
|
39
|
+
return profiles.filter((profile): profile is TargetProfile => !!profile);
|
|
40
|
+
} catch { return []; }
|
|
41
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@font-face{font-family:JetBrains Mono;src:url(/_astro/jetbrains-mono.CFJaiWaY.woff2)format("woff2");font-weight:100 800;font-style:normal;font-display:swap}@font-face{font-family:Webr Symbols;src:url(/_astro/symbols-nerd.CEdkKAK8.woff2)format("woff2");font-display:swap;unicode-range:U+E0A0-E0D7,U+E5FA-E6B7,U+E700-E7C5,U+F400-F4A8}:root{color-scheme:light;--page:#f8f8f7;--surface:#fff;--surface-raised:#fff;--surface-muted:#f4f4f3;--surface-hover:#f0f0ef;--surface-active:#eaeae9;--sidebar:#f3f3f2;--card-hover:#fff;--card-edge:#1a1d2017;--card-shadow:inset 0 1px 2px #fffffff2, 0 1px 2px #00000029, 0 3px 7px #00000012;--card-shadow-hover:inset 0 1px 2px #fffffff2, 0 2px 4px #0000002b, 0 5px 10px #00000014;--ink:#1b1c1d;--ink-soft:#5b5d61;--ink-muted:#8b8d90;--line:#e9e9e7;--line-strong:#dedfdd;--accent:#357dba;--action-blue:#345fba;--accent-soft:#e9f3fa;--focus-ring:#579bcf;--working:#58a47a;--blocked:#dca362;--danger:#c9534d;--done:#7196ca;--idle:#9a9fa3;--terminal-bg:#fefefd;--terminal-ink:#333639;--terminal-cursor:#555b60;--terminal-selection:#bbd8e8;--shadow:0 1px 2px #1c202308, 0 10px 36px #1c202308;--ease:cubic-bezier(.22, 1, .36, 1);--font-ui:"DM Sans", Inter, system-ui, sans-serif;--font-mono:"JetBrains Mono", "Webr Symbols", ui-monospace, Menlo, monospace}html[data-theme=dark]{color-scheme:dark;--page:#121313;--surface:#1b1c1c;--surface-raised:#202121;--surface-muted:#242626;--surface-hover:#2a2c2c;--surface-active:#333535;--sidebar:#111212;--card-hover:#2b2d2d;--card-edge:#ffffff14;--card-shadow:inset 0 1px 2px #ffffff2b, 0 1px 2px #0000005c, 0 3px 7px #0000002e;--card-shadow-hover:inset 0 1px 2px #ffffff40, 0 2px 4px #00000061, 0 5px 10px #0000002e;--ink:#f1f2f0;--ink-soft:#b1b3b0;--ink-muted:#777b7b;--line:#292b2b;--line-strong:#393b3b;--accent:#91cae8;--action-blue:#365eb9;--accent-soft:#203239;--focus-ring:#9dd5e9;--working:#85cba2;--blocked:#ecc188;--danger:#e07a74;--done:#a0bdec;--idle:#858d8b;--terminal-bg:#181a1a;--terminal-ink:#d8dcda;--terminal-cursor:#d8dcda;--terminal-selection:#37505a;--shadow:0 1px 2px #00000024, 0 10px 36px #0000001f}*,:before,:after{box-sizing:border-box}html{background:var(--page)}body{color:var(--ink);background:var(--page);font:13px/1.45 var(--font-ui);-webkit-font-smoothing:antialiased;margin:0}button,input,textarea,select{font:inherit}button{cursor:pointer}a{color:inherit;text-decoration:none}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:0}.app-shell{min-height:100dvh;display:flex}.main-panel{flex-direction:column;flex:1;min-width:0;height:100dvh;min-height:100dvh;display:flex}.icon-button{width:30px;height:30px;color:var(--ink-muted);background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;display:inline-flex}.icon-button:hover{background:var(--surface-hover);color:var(--ink)}.not-found{max-width:680px;margin:14vh auto 0;padding:25px}.not-found h1{font-size:26px;font-weight:600}.not-found p{color:var(--ink-muted)}.mobile-controls{display:none}@media (width<=760px){.mobile-controls{z-index:8;top:max(6px, env(safe-area-inset-top));right:max(6px, env(safe-area-inset-right));border:1px solid var(--line);background:var(--sidebar);border-radius:6px;gap:2px;padding:2px;display:flex;position:fixed}.mobile-controls button{width:28px;height:28px;color:var(--ink-soft);background:0 0;border:0;border-radius:4px;place-items:center;display:grid}.mobile-controls button:hover{background:var(--surface-hover)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}@media (pointer:coarse){.app-shell.is-thread{top:var(--app-top,0px);height:var(--app-height,100dvh);min-height:0;position:fixed;left:0;right:0;overflow:hidden}.app-shell.is-thread .main-panel{height:100%;min-height:0;padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right)}html:has(.app-shell.is-thread),html:has(.app-shell.is-thread) body{overscroll-behavior:none}}.sidebar{border-right:1px solid var(--line);background:var(--sidebar);flex-direction:column;flex:0 0 280px;min-width:0;height:100dvh;display:flex;position:sticky;top:0}.sidebar.is-collapsed{flex-basis:44px}.sidebar-header{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;height:48px;padding:0 10px 0 16px;display:flex}.sidebar.is-collapsed .sidebar-header{justify-content:center;padding:0}.brand{letter-spacing:-.02em;align-items:center;gap:7px;font-size:12px;font-weight:700;display:inline-flex}.sidebar-collapse{width:26px;height:26px}.sidebar-actions{flex:none;align-items:center;gap:5px;margin:12px 8px 4px;display:flex}.sidebar-new-thread{border:1px solid var(--card-edge);background:var(--surface-raised);height:34px;box-shadow:var(--card-shadow);color:var(--ink);border-radius:7px;flex:none;justify-content:center;align-items:center;gap:6px;padding:0 10px;font-size:11px;font-weight:600;transition:background-color .2s,box-shadow .2s;display:inline-flex}.sidebar-new-thread span{transform:translateY(1px)}.sidebar-new-thread:hover{background:var(--card-hover);box-shadow:var(--card-shadow-hover)}.sidebar.is-collapsed .sidebar-actions{justify-content:center;margin:10px 0 0}.sidebar.is-collapsed .sidebar-new-thread{width:28px;height:28px;padding:0}.sidebar-open-worktree{border:1px solid var(--card-edge);background:var(--surface-raised);width:34px;height:34px;box-shadow:var(--card-shadow);color:var(--ink-soft);border-radius:7px;flex:none;place-items:center;padding:0;display:grid}.sidebar-open-worktree:hover:not(:disabled){background:var(--card-hover);box-shadow:var(--card-shadow-hover);color:var(--ink)}.sidebar.is-collapsed .sidebar-actions{flex-direction:column}.sidebar.is-collapsed .sidebar-open-worktree{width:28px;height:28px}.sidebar-view-switch{border:1px solid var(--line);background:var(--surface-muted);border-radius:7px;flex:none;gap:2px;width:70px;height:34px;margin-left:auto;padding:2px;display:flex}.sidebar-view-switch button{min-width:0;color:var(--ink-soft);background:0 0;border:1px solid #0000;border-radius:5px;flex:1;place-items:center;padding:0;display:grid}.sidebar-view-switch button:hover{color:var(--ink);background:var(--surface-hover)}.sidebar-view-switch button[aria-pressed=true]{border-color:var(--card-edge);background:var(--surface-raised);box-shadow:var(--card-shadow);color:var(--ink)}.thread-navigation{scrollbar-width:thin;flex:1;padding:8px;overflow-y:auto}.thread-row{min-height:60px;color:var(--ink-soft);border:1px solid #0000;border-radius:7px;grid-template-rows:17px 1fr;grid-template-columns:25px minmax(0,1fr) auto;column-gap:9px;padding:7px 10px;font-size:11.5px;transition:background-color .2s,border-color .2s,box-shadow .2s;display:grid}.thread-item{display:block;position:relative}.thread-item+.thread-item{margin-top:4px}.thread-archive{width:26px;height:26px;color:var(--ink-soft);opacity:0;pointer-events:none;background:0 0;border:0;border-radius:5px;place-items:center;padding:0;transition:opacity .15s,background-color .15s;display:grid;position:absolute;top:50%;right:9px;transform:translateY(-50%)}.thread-item:hover .thread-archive,.thread-archive:focus-visible{opacity:1;pointer-events:auto}.thread-archive:hover{background:var(--surface-hover);color:var(--ink)}.thread-item:hover .thread-time,.thread-item:focus-within .thread-time{visibility:hidden}@media (hover:none){.thread-archive{opacity:1;pointer-events:auto}.thread-item .thread-time{visibility:hidden}}.thread-menu-positioner{z-index:30}.thread-menu{border:1px solid var(--card-edge);background:var(--surface-raised);min-width:180px;color:var(--ink);box-shadow:var(--card-shadow-hover), 0 12px 32px #0000002e;border-radius:8px;outline:0;padding:4px}.thread-menu-item{cursor:pointer;border-radius:5px;outline:0;align-items:center;gap:8px;min-height:32px;padding:6px 8px;font-size:12px;display:flex}.thread-menu-item[data-highlighted]{background:var(--surface-hover)}.worktree-menu{width:260px;padding:0;overflow:hidden}.worktree-menu-list{max-height:min(320px, var(--available-height));padding:4px;overflow-y:auto}.worktree-menu-note{color:var(--ink-soft);margin:0;padding:8px;font-size:12px}.thread-menu-item>svg,.thread-menu-item>.thread-project{flex:none}.worktree-menu-name{overflow-wrap:anywhere;min-width:0;display:grid}.worktree-menu-name small{color:var(--ink-soft);font-size:10.5px}.worktree-menu-chevron{color:var(--ink-soft);margin-left:auto}.thread-menu-item.is-danger{color:var(--danger)}.archived-threads{border-top:1px solid var(--line);scrollbar-width:thin;flex:none;max-height:45%;padding:6px 8px;overflow-y:auto}.archived-threads .group-header{z-index:1;background:var(--sidebar);position:sticky;top:-6px}.group-count{font:10px var(--font-mono);color:var(--ink-muted);margin-left:2px}.archived-delete-all{height:26px;color:var(--ink-soft);background:0 0;border:1px solid #0000;border-radius:5px;flex:none;margin-right:4px;padding:0 8px;font-size:11px}.archived-delete-all:hover:not(:disabled){border-color:var(--line);background:var(--surface-hover);color:var(--danger)}.archived-delete-all:disabled{opacity:.6}.thread-avatar{grid-area:1/1/3;justify-content:center;align-self:center;align-items:center;width:25px;height:25px;display:inline-flex;position:relative}.thread-row:hover,.thread-row.is-current:hover{border-color:var(--card-edge);background:var(--card-hover);box-shadow:var(--card-shadow-hover);color:var(--ink)}.thread-row.needs-attention.status-blocked{border-color:color-mix(in srgb, var(--blocked) 45%, transparent)}.thread-row.needs-attention.status-done{border-color:color-mix(in srgb, var(--done) 45%, transparent)}.thread-row.is-current{border-color:var(--card-edge);background:var(--surface-raised);box-shadow:var(--card-shadow);color:var(--ink)}.thread-row.is-current:hover{background:var(--card-hover);box-shadow:var(--card-shadow-hover)}.thread-copy{flex-direction:column;grid-area:1/2/3;justify-content:center;gap:4px;min-width:0;display:flex}.thread-context{color:var(--ink-soft);align-items:center;gap:5px;font-size:10px;display:flex}.thread-name{white-space:nowrap;text-overflow:ellipsis;color:var(--ink);overflow:hidden}.thread-name-input{border:1px solid var(--focus-ring);background:var(--surface);width:100%;min-width:0;color:var(--ink);font:inherit;border-radius:4px;outline:0;margin:-2px -4px;padding:1px 4px}.thread-time{font:10px var(--font-mono);color:var(--ink-soft);grid-area:1/3;justify-self:end}.thread-status{background:var(--sidebar);color:var(--ink-soft);border-radius:50%;padding:1px;position:absolute;top:-5px;left:-5px}.thread-row.is-current .thread-status{background:var(--surface-raised)}.thread-row:hover .thread-status{background:var(--card-hover)}.thread-status.status-blocked{color:var(--blocked)}.thread-status.status-done{color:var(--done)}.thread-project{background:color-mix(in srgb, var(--project-color) 17%, transparent);width:17px;height:17px;color:var(--project-color);font:700 10px var(--font-mono);border-radius:3px;flex:none;place-items:center;display:grid;position:relative;overflow:hidden}.thread-project img{object-fit:contain;background:var(--surface-raised);opacity:0;width:100%;height:100%;position:absolute;inset:0}.thread-project.has-icon img{opacity:1}.group-header{align-items:center;min-width:0;display:flex}.group-title{min-width:0;color:var(--ink-soft);text-align:left;background:0 0;border:0;border-radius:5px;flex:1;align-items:center;gap:6px;padding:8px;font-size:11px;display:flex}.group-title:hover{background:var(--surface-hover);color:var(--ink)}.group-name{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.group-new-thread{width:28px;height:28px;color:var(--ink-soft);border-radius:5px;flex:none;place-items:center;margin-right:4px;display:grid}.group-new-thread:hover{background:var(--surface-hover);color:var(--ink)}.group-chevron{flex:none;justify-content:center;align-items:center;width:12px;height:17px;display:inline-flex}.thread-group+.thread-group{margin-top:8px}.sidebar-footer{border-top:1px solid var(--line);flex:none;align-items:center;display:flex}.sidebar-settings{height:43px;color:var(--ink-soft);flex:1;align-items:center;gap:10px;padding:0 16px;font-size:11.5px;display:flex}.sidebar-palette{border:1px solid var(--line-strong);color:var(--ink-soft);background:0 0;border-radius:4px;margin-right:12px;padding:3px 6px}.sidebar-palette:hover{color:var(--ink);background:var(--surface-hover)}.sidebar-palette kbd{font:10px var(--font-mono)}.sidebar-settings:hover{color:var(--ink);background:var(--surface-hover)}.sidebar-empty{color:var(--ink-muted);padding:8px;font-size:11px}.sidebar-scrim,.mobile-close{display:none}@media (width<=760px){.sidebar,.sidebar.is-collapsed{z-index:11;flex-basis:auto;width:min(280px,86vw);transition:transform .2s;position:fixed;transform:translate(-100%)}.sidebar.is-mobile-open{transform:translate(0)}.sidebar.is-collapsed .sidebar-header{justify-content:space-between;padding:0 10px 0 16px}.sidebar.is-collapsed .thread-navigation,.sidebar.is-collapsed .sidebar-footer{display:flex}.mobile-close{display:inline-flex}.sidebar-collapse{display:none}.sidebar-scrim{z-index:10;background:#00000073;border:0;display:block;position:fixed;inset:0}}.thread-worktree{opacity:.7;flex:none}.form-page{width:min(520px,100%);margin:8vh auto 0;padding:0 20px 60px}.form-page h1{letter-spacing:-.03em;margin:0 0 26px;font-size:22px;font-weight:600}.form-error{color:#d16a64;font-size:12px}.settings-page section{margin-top:28px}.settings-page h2{margin:18px 0 8px;font-size:12px;font-weight:600}.settings-row{justify-content:space-between;align-items:center;gap:12px;min-height:44px;font-size:12px;display:flex}.settings-section-title{justify-content:space-between;align-items:center;display:flex}.settings-section-title button{color:var(--ink-soft);background:0 0;border:0;border-radius:5px;padding:6px 9px;font-size:11px}.settings-section-title button:hover{background:var(--surface-hover)}.settings-section-title button:hover,.shortcut-key:hover{color:var(--ink)}.shortcut-key{min-height:32px;color:var(--ink-soft);background:0 0;border:0;align-items:center;gap:4px;padding:3px 0;font-size:11px;display:inline-flex}.shortcut-key kbd{background:var(--surface-raised);min-width:25px;height:25px;box-shadow:var(--card-shadow);color:var(--ink-soft);font:11px var(--font-mono);border:0;border-radius:5px;place-items:center;padding:0 5px;display:inline-grid}.shortcut-key:hover kbd,.shortcut-key:focus-visible kbd{color:var(--ink);background:var(--card-hover);box-shadow:var(--card-shadow-hover)}.shortcut-key[data-recording=true]{box-shadow:inset 0 0 0 1px var(--focus-ring);color:var(--ink);border-radius:5px;padding:3px 8px}.settings-note{color:var(--ink-soft);margin:0 0 10px;font-size:11px}.theme-control{background:var(--surface-muted);border-radius:7px;gap:2px;padding:2px;display:flex}.theme-option{color:var(--ink-soft);text-transform:capitalize;background:0 0;border:0;border-radius:5px;align-items:center;gap:5px;padding:6px 8px;font-size:11px;display:flex}.theme-option:hover{color:var(--ink)}.theme-option.is-selected{background:var(--surface-raised);box-shadow:var(--card-shadow);color:var(--ink)}.theme-control--labels .theme-option{text-transform:none}.mobile-menu{display:none}@media (width<=760px){.form-page{margin-top:35px}.mobile-menu{color:var(--ink-soft);background:0 0;border:0;margin-bottom:30px;padding:4px 0;display:block}}.new-thread-page{flex-direction:column;justify-content:center;width:min(800px,100%);min-height:100dvh;margin:0 auto;padding:60px 20px;display:flex;position:relative}.new-thread-page h1{color:var(--ink);letter-spacing:-.035em;text-align:center;margin:0 0 26px;font-size:clamp(24px,2.8vw,32px);font-weight:500;line-height:1.25}.thread-composer{border:1px solid var(--line-strong);background:var(--surface);box-shadow:var(--card-shadow);border-radius:18px;transition:border-color .2s,box-shadow .2s}.thread-composer:focus-within{border-color:color-mix(in srgb, var(--focus-ring) 50%, var(--line-strong));box-shadow:var(--card-shadow-hover)}.workspace-composer{gap:12px;padding:18px;display:grid}.workspace-composer label{color:var(--ink-soft);gap:6px;font-size:12px;display:grid}.workspace-composer input{border:1px solid var(--line-strong);width:100%;color:var(--ink);background:0 0;border-radius:8px;padding:10px}.workspace-composer button{justify-self:end;padding:8px 12px}.workspace-composer .path-field{position:relative}.path-suggestions{z-index:5;border:1px solid var(--card-edge);background:var(--surface-raised);box-shadow:var(--card-shadow-hover);border-radius:8px;max-height:240px;margin:4px 0 0;padding:4px;list-style:none;position:absolute;top:100%;left:0;right:0;overflow-y:auto}.path-suggestions li{color:var(--ink);cursor:pointer;border-radius:5px;padding:6px 8px;font-size:13px}.path-suggestions li[data-highlighted]{background:var(--surface-hover)}.workspace-cancel{color:var(--ink-soft);background:0 0;border:0;border-radius:6px;align-self:center;padding:8px 12px}.workspace-cancel:hover{background:var(--surface-hover);color:var(--ink)}.thread-composer textarea{resize:vertical;width:100%;min-height:94px;max-height:45vh;color:var(--ink);background:0 0;border:0;outline:0;padding:17px 18px 0;font-size:14px;line-height:1.5;display:block}.thread-composer textarea::placeholder{color:var(--ink-muted)}.composer-toolbar{align-items:center;gap:10px;padding:9px 12px 12px;display:flex}.composer-selectors{flex-wrap:wrap;align-items:center;gap:3px;min-width:0;display:flex}.composer-actions{flex:none;align-items:center;gap:5px;margin-left:auto;display:inline-flex}.composer-preview-note{color:var(--ink-soft);text-align:center;margin:12px 0 0;font-size:11px}.worktree-option{width:30px;height:32px;color:var(--ink-soft);cursor:pointer;border-radius:6px;place-items:center;display:inline-grid;position:relative}.worktree-option:hover{color:var(--ink);background:var(--surface-hover)}.worktree-option:focus-within{outline:0}.worktree-option input{opacity:0;width:1px;height:1px;position:absolute}.worktree-option:has(input:not(:checked)){color:var(--ink-muted)}.worktree-option:has(input:checked){background:var(--surface-muted);color:var(--ink)}.composer-send{background:var(--action-blue);color:#fff;border:0;border-radius:50%;flex:none;place-items:center;width:36px;height:36px;display:inline-grid}.composer-send:hover:not(:disabled){filter:brightness(1.12)}.composer-send:disabled{background:var(--surface-active);color:var(--ink-muted);cursor:not-allowed}.new-thread-page .form-error{margin:12px 4px}@media (width<=760px){.new-thread-page{padding:60px 16px}.new-thread-page .mobile-menu{margin:0;position:absolute;top:16px;left:16px}}@media (width<=480px){.composer-toolbar{gap:6px;padding:8px}}.thread-composer.is-dropping{border-style:dashed;border-color:var(--focus-ring);box-shadow:var(--card-shadow-hover)}.composer-images{flex-wrap:wrap;gap:8px;margin:0;padding:14px 18px 0;list-style:none;display:flex}.composer-images li{border:1px solid var(--line-strong);background:var(--surface-raised);border-radius:9px;width:56px;height:56px;position:relative;overflow:hidden}.composer-images img{object-fit:cover;width:100%;height:100%;display:block}.composer-images button{background:color-mix(in srgb, var(--surface) 85%, transparent);width:18px;height:18px;color:var(--ink);cursor:pointer;opacity:0;border:0;border-radius:50%;place-items:center;padding:0;transition:opacity .15s;display:grid;position:absolute;top:3px;right:3px}.composer-images li:hover button,.composer-images button:focus-visible{opacity:1}.composer-images li.is-uploading{background:linear-gradient(100deg, var(--surface-raised) 30%, var(--surface-hover) 50%, var(--surface-raised) 70%) 0 0 / 200% 100%;animation:1.1s linear infinite composer-image-loading}@keyframes composer-image-loading{to{background-position:-200% 0}}@media (hover:none){.composer-images button{opacity:1}}.composer-select{min-width:0;max-width:min(220px,100%);height:32px;color:var(--ink-soft);background:0 0;border:1px solid #0000;border-radius:6px;justify-content:flex-start;align-items:center;gap:5px;padding:0 6px;font-size:11px;display:inline-flex}.composer-select:hover,.composer-select[data-popup-open]{border-color:var(--line);background:var(--surface-hover);color:var(--ink)}.composer-select-icon{flex:none;justify-content:center;align-items:center;display:inline-flex}.composer-select-icon .thread-project{width:14px;height:14px}.composer-select-label{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.composer-select-caret{flex:none;margin-left:8px}.composer-select-positioner{z-index:20}.composer-select-popup{width:min(280px, var(--available-width));max-height:var(--available-height);border:1px solid var(--card-edge);background:var(--surface-raised);color:var(--ink);box-shadow:var(--card-shadow-hover), 0 12px 32px #0000002e;border-radius:9px;overflow:hidden}.composer-select-search{border-bottom:1px solid var(--line);color:var(--ink-soft);align-items:center;gap:8px;padding:10px 12px;display:flex}.composer-select-search input{width:100%;min-width:0;color:var(--ink);background:0 0;border:0;outline:0;padding:0;font-size:16px;line-height:1.4}.composer-select-search input::placeholder{color:var(--ink-soft)}.composer-select-list{max-height:min(240px, calc(var(--available-height) - 50px));overscroll-behavior:contain;padding:5px;overflow-y:auto}.composer-select-option{cursor:pointer;border-radius:5px;align-items:center;gap:8px;min-height:36px;padding:8px;font-size:12px;display:flex}.composer-select-option[data-highlighted]{background:var(--surface-hover);outline:0}.composer-select-option[data-disabled]{opacity:.5;cursor:not-allowed}.composer-select-option-label{overflow-wrap:anywhere;flex:1;min-width:0}.composer-select-check{color:var(--ink-soft);flex:none;display:inline-flex}.composer-select-action{border:0;border-top:1px solid var(--line);width:calc(100% - 10px);min-height:36px;color:var(--ink);text-align:left;background:0 0;border-radius:5px;align-items:center;gap:8px;margin:2px 5px 5px;padding:8px;font-size:12px;display:flex}.composer-select-action:hover{background:var(--surface-hover)}.composer-select-empty{color:var(--ink-soft);font-size:12px}.composer-select-empty:not(:empty){padding:12px}@media (width>=761px){.composer-select-search input{font-size:12px}}@media (width<=480px){.composer-select-icon{display:none}}.pane-layout{background:var(--line-strong);grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;height:100dvh;min-height:0;display:grid}.pane-layout.single-pane{grid-template-columns:minmax(0,1fr)}.terminal-pane{background:var(--terminal-bg);min-width:0;min-height:0;overflow:hidden}.terminal-host{width:100%;height:100%;padding:12px 8px 8px 13px}.terminal-host .xterm{height:100%}.terminal-host .xterm-viewport{scrollbar-width:thin}@media (width<=760px){.pane-layout:not(.single-pane){grid-template-rows:repeat(2,minmax(0,1fr));grid-template-columns:minmax(0,1fr)}}.native-thread-layout{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.native-pane-layout{flex:1;min-height:160px;position:relative}.native-pane-position{padding:2px;position:absolute}.terminal-peek{background:var(--surface-muted);color:var(--ink-soft);border:0;border-radius:6px;justify-content:center;align-items:center;gap:6px;padding:0;font-size:11px;transition:background-color .15s,color .15s;display:flex;position:absolute}.terminal-peek:hover,.terminal-peek:focus-visible{background:var(--surface-hover);color:var(--ink)}.terminal-peek.is-right{flex-direction:column;width:24px;top:2px;bottom:2px;right:2px}.terminal-peek.is-right span{writing-mode:vertical-rl}.terminal-peek.is-bottom{height:24px;bottom:2px;left:2px;right:2px}.native-pane-position .terminal-pane{flex-direction:column;height:100%;display:flex}.native-pane-layout.is-split .terminal-pane{outline:1px solid var(--line);outline-offset:-1px;border-radius:5px;transition:outline-color .15s}.native-pane-layout.is-split .terminal-pane.is-active{outline-color:color-mix(in srgb, var(--focus-ring) 15%, var(--line-strong))}.terminal-host{flex:1;height:auto;min-height:0;overflow:hidden}.terminal-pane{position:relative}.terminal-input-capture{opacity:.01;resize:none;border:0;width:2px;height:2px;padding:0;position:absolute;bottom:8px;left:12px}.terminal-status{clip-path:inset(50%);width:1px;height:1px;position:absolute;overflow:hidden}.terminal-control-conflict{z-index:10;border:1px solid var(--line-strong);background:var(--surface-raised);max-width:calc(100% - 16px);color:var(--ink-soft);box-shadow:var(--card-shadow);white-space:nowrap;border-radius:8px;align-items:center;gap:10px;padding:6px 10px;display:flex;position:absolute;top:8px;left:50%;transform:translate(-50%)}.terminal-control-conflict button{background:var(--surface-hover);color:var(--ink);border:0;border-radius:5px;padding:3px 7px}.adoption-picker{color:var(--ink-soft);gap:8px;padding:16px;font-size:13px;display:grid}.adoption-picker select{margin-left:8px}.runtime-notice{color:var(--ink-muted);padding:10px 16px;font-size:13px}.launch-progress{align-content:center;justify-items:start;gap:18px;width:min(520px,100%);height:100%;margin:0 auto;padding:0 20px;display:grid}.launch-progress h1{letter-spacing:-.03em;margin:0;font-size:22px;font-weight:600}.launch-prompt{border-left:2px solid var(--line-strong);color:var(--ink-soft);white-space:pre-wrap;overflow-wrap:anywhere;max-height:9em;margin:0;padding:10px 14px;font-size:13px;overflow:auto}.launch-steps{gap:10px;margin:0;padding:0;font-size:13px;list-style:none;display:grid}.launch-step{color:var(--ink-muted);align-items:center;gap:10px;display:flex}.launch-step.is-done{color:var(--ink-soft)}.launch-step.is-active{color:var(--ink)}.launch-step.is-active svg{animation:1s linear infinite launch-spin}.launch-step.is-failed{color:#d16a64}.launch-failure{color:var(--ink-soft);gap:10px;font-size:13px;display:grid}.launch-failure p{margin:0}.launch-retry{border:1px solid var(--line-strong);background:var(--surface-raised);color:var(--ink);border-radius:7px;justify-self:start;padding:7px 11px;font-size:12px;text-decoration:none}@keyframes launch-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.launch-step.is-active svg{animation:none}}.terminal-host .xterm{font-variant-ligatures:none;font-feature-settings:"liga" 0, "calt" 0}.terminal-drop-overlay{z-index:15;border:1.5px dashed var(--line-strong);background:color-mix(in srgb, var(--terminal-bg) 82%, transparent);color:var(--ink);pointer-events:none;border-radius:10px;place-content:center;justify-items:center;gap:8px;font-size:13px;display:grid;position:absolute;inset:8px}.terminal-attach-notice{z-index:16;border:1px solid var(--line-strong);background:var(--surface-raised);max-width:calc(100% - 28px);box-shadow:var(--card-shadow);color:var(--ink-soft);border-radius:8px;padding:6px 10px;font-size:12px;position:absolute;bottom:12px;right:14px}.terminal-attach-notice.is-error{color:#d16a64}.terminal-key-bar{padding:6px 8px calc(6px + env(safe-area-inset-bottom));border-top:1px solid var(--line);background:var(--surface-muted);flex:none;gap:6px;display:flex}.terminal-key-bar button{background:var(--surface-raised);min-height:38px;box-shadow:var(--card-shadow);color:var(--ink);touch-action:manipulation;-webkit-user-select:none;user-select:none;border:0;border-radius:8px;flex:1;place-items:center;font-size:13px;display:inline-grid}.terminal-key-bar button.is-armed{background:var(--action-blue);color:#fff}.palette-overlay{z-index:30;background:#0000006e;justify-content:center;align-items:flex-start;padding:min(16vh,140px) 16px 16px;display:flex;position:fixed;inset:0}.palette{border:1px solid var(--card-edge);background:var(--surface);border-radius:9px;width:min(520px,100%);overflow:hidden;box-shadow:inset 0 1px 2px #ffffff1f,0 16px 60px #00000038}.palette>input{border:0;border-bottom:1px solid var(--line);width:100%;height:48px;color:var(--ink);background:0 0;outline:0;padding:0 16px;font-size:13px;display:block}.palette>input::placeholder{color:var(--ink-muted)}.palette-results{max-height:min(400px,55vh);padding:6px;overflow-y:auto}.palette-results button{width:100%;min-height:36px;color:var(--ink);text-align:left;background:0 0;border:1px solid #0000;border-radius:6px;align-items:center;gap:10px;padding:0 10px;font-size:12px;transition:background-color .2s,border-color .2s,box-shadow .2s;display:flex}.palette-icon{width:22px;height:22px;color:var(--ink-soft);flex:none;justify-content:center;align-items:center;display:inline-flex}.palette-label{white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.palette-results button:hover,.palette-results button.is-selected{border-color:var(--card-edge);background:var(--surface-raised);box-shadow:var(--card-shadow)}.palette-results button:hover{background:var(--card-hover);box-shadow:var(--card-shadow-hover)}.palette-results button small{color:var(--ink-soft);white-space:nowrap;font-size:10px}.palette-results button kbd{border:1px solid var(--line-strong);min-width:34px;color:var(--ink-soft);font:10px var(--font-mono);text-align:center;white-space:nowrap;border-radius:4px;margin-left:6px;padding:3px 5px}.palette-results p{color:var(--ink-muted);padding:4px 10px}.palette-help{border-top:1px solid var(--line);color:var(--ink-soft);font:10px var(--font-mono);gap:14px;padding:9px 14px;display:flex}.dialog-backdrop{z-index:40;background:#0000006e;position:fixed;inset:0}.dialog{z-index:41;border:1px solid var(--card-edge);background:var(--surface);width:min(400px,100vw - 32px);color:var(--ink);border-radius:10px;outline:0;padding:18px;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:inset 0 1px 2px #ffffff1f,0 16px 60px #00000038}.dialog-message{margin:0 0 16px;font-size:13px;line-height:1.5}.dialog-actions{justify-content:flex-end;gap:8px;display:flex}.dialog-button{border:1px solid var(--line-strong);background:var(--surface-raised);min-height:32px;color:var(--ink);cursor:pointer;border-radius:6px;padding:0 12px;font-size:12px}.dialog-button:hover{background:var(--surface-hover)}.dialog-button.is-primary{background:var(--action-blue);color:#fff;border-color:#0000}.dialog-button.is-danger{background:var(--danger)}.pairing-title{margin:0 0 6px;font-size:14px;font-weight:600}.pairing-code{font:700 40px/1 var(--font-mono);letter-spacing:.18em;text-align:center;text-indent:.18em;margin:0 0 16px}.pairing-invite .dialog-message code{font-family:var(--font-mono)}.qr-code{aspect-ratio:1;border-radius:8px;width:min(240px,100%);margin:0 auto 14px;display:block}.pairing-token{justify-content:center;align-items:center;gap:8px;margin:0 0 6px;display:flex}.pairing-token code{font:600 14px var(--font-mono);letter-spacing:.08em;user-select:all}.pairing-token .dialog-button{place-items:center;min-width:32px;padding:0;display:inline-grid}.device-tag{background:var(--accent-soft);color:var(--accent);border-radius:4px;margin-left:8px;padding:1px 6px;font-size:10px;font-style:normal}.device-meta{color:var(--ink-muted);font-size:11px;display:block}.crash-report{z-index:100;color:#7a1f1a;max-height:60dvh;font:11px/1.4 var(--font-mono);white-space:pre-wrap;word-break:break-word;background:#fff0ef;margin:0;padding:12px 16px;position:fixed;inset:0 0 auto;overflow:auto}.pairing-invite .theme-control{flex-wrap:wrap;justify-content:center;margin:0 0 12px}.pairing-invite .theme-option{white-space:nowrap}.settings-subtitle{color:var(--ink-soft);margin:16px 0 4px;font-size:11px;font-weight:500}.activity-row{color:var(--ink-muted);justify-content:space-between;gap:12px;margin:0;padding:4px 0;font-size:11px;display:flex}.activity-row time{flex:none}.perf-overlay{z-index:40;color:#d7ffd7;font-variant-numeric:tabular-nums;pointer-events:none;background:#000000c7;border-radius:6px;padding:4px 8px;font:11px/1.5 ui-monospace,monospace;position:fixed;bottom:8px;right:8px}.tooltip-positioner{z-index:40}.tooltip-popup{border:1px solid var(--line);background:var(--surface-raised);box-shadow:var(--card-shadow);color:var(--ink);white-space:nowrap;border-radius:6px;padding:5px 8px;font-size:11px;font-weight:500;transition:opacity .16s,transform .16s}.tooltip-popup[data-starting-style],.tooltip-popup[data-ending-style]{opacity:0;transform:translateY(3px)}::view-transition-group(root),::view-transition-old(root),::view-transition-new(root){mix-blend-mode:normal;animation:none}.astro-route-announcer{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;top:0;left:0;overflow:hidden}.xterm{cursor:text;-webkit-user-select:none;-ms-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:underline double}.xterm-underline-3{text-decoration:underline wavy}.xterm-underline-4{text-decoration:underline dotted}.xterm-underline-5{text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}
|