zerogterm 0.2.0-alpha.1 → 0.3.0-alpha.1

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 CHANGED
@@ -6,6 +6,8 @@ ZeroG Terminal is a Linux-first Electron workspace for persistent `screen` sessi
6
6
  - Session sidebar with create, discover, refresh, and attach actions.
7
7
  - Visible actions for opening a persistent local terminal and connecting to an SSH target (`host`, `user@host`, or `user@host:port`).
8
8
  - xterm.js terminal renderer and a split workspace surface.
9
+ - Pane close button: closing detaches the session and removes the pane from the workspace; screen-backed sessions stay alive and remain discoverable.
10
+ - Newly opened terminal and SSH panes receive keyboard focus so typing works immediately.
9
11
  - Persistent dark/light theme with an accessible sun/moon toggle.
10
12
  - Safe session-name validation and argument-array `screen` invocation.
11
13
  - AI command approval surface; suggestions are visible and require explicit approval before being sent to the terminal.
@@ -1,9 +1,12 @@
1
1
  import { app, BrowserWindow, clipboard, ipcMain, Menu, session } from 'electron';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { ScreenService } from './session-service.js';
4
+ import { ScreenService, discoverShellBackends, parseWslDistributions } from './session-service.js';
5
+ import { SessionHistoryStore, defaultHistoryPath } from './session-history.js';
6
+ import { buildRemoteScreenAttachArgs, buildRemoteScreenDiscoveryArgs, listKnownConnections, parseRemoteScreenList, validateKnownConnection } from './ssh-inventory.js';
5
7
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
6
- const service = new ScreenService();
8
+ const history = new SessionHistoryStore({ filePath: defaultHistoryPath(app.getPath('userData')) });
9
+ const service = new ScreenService({ onEvent: (event, session, available) => { void history.record(event, session, available); } });
7
10
  let win;
8
11
  // GPU is unstable under Toolbox/Wayland on this host; allow override.
9
12
  if (process.env.ZEROG_ENABLE_GPU !== '1') {
@@ -53,12 +56,28 @@ function isRecord(value) {
53
56
  return typeof value === 'object' && value !== null;
54
57
  }
55
58
  ipcMain.handle('sessions:list', () => service.list());
59
+ ipcMain.handle('sessions:history', () => history.list());
60
+ ipcMain.handle('sessions:historyRemove', (_event, entryId) => history.remove(entryId));
61
+ ipcMain.handle('sessions:backends', () => discoverShellBackends());
62
+ ipcMain.handle('sessions:wslDistributions', async () => {
63
+ try {
64
+ const { execFile } = await import('node:child_process');
65
+ const { promisify } = await import('node:util');
66
+ const { stdout } = await promisify(execFile)(process.platform === 'win32' ? 'wsl.exe' : 'wsl', ['--list', '--quiet']);
67
+ return parseWslDistributions(stdout);
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ });
56
73
  ipcMain.handle('sessions:createLocal', async (_event, request) => {
57
74
  if (!isRecord(request) || typeof request.name !== 'string') {
58
75
  throw new Error('createLocalSession requires { name: string }');
59
76
  }
60
77
  const cwd = typeof request.cwd === 'string' ? request.cwd : undefined;
61
- return service.createLocal(request.name, cwd);
78
+ const backend = request.backend === 'bash' || request.backend === 'zsh' || request.backend === 'powershell' || request.backend === 'wsl' ? request.backend : undefined;
79
+ const wslDistribution = typeof request.wslDistribution === 'string' ? request.wslDistribution : undefined;
80
+ return service.createLocal({ name: request.name, cwd, ...(backend ? { backend } : {}), wslDistribution });
62
81
  });
63
82
  ipcMain.handle('sessions:createSsh', async (_event, request) => {
64
83
  if (!isRecord(request) || typeof request.target !== 'string') {
@@ -67,6 +86,26 @@ ipcMain.handle('sessions:createSsh', async (_event, request) => {
67
86
  const name = typeof request.name === 'string' ? request.name : undefined;
68
87
  return service.createSsh(request.target, name);
69
88
  });
89
+ ipcMain.handle('connections:listKnown', () => listKnownConnections());
90
+ ipcMain.handle('screens:discoverRemote', async (_event, input) => {
91
+ const connection = validateKnownConnection(input);
92
+ const command = buildRemoteScreenDiscoveryArgs(connection);
93
+ const { execFile } = await import('node:child_process');
94
+ const { promisify } = await import('node:util');
95
+ try {
96
+ const { stdout } = await promisify(execFile)(command.file, command.args);
97
+ return parseRemoteScreenList(stdout, connection.alias);
98
+ }
99
+ catch (error) {
100
+ return { status: 'unavailable', host: connection.alias, reason: error?.code === 'ENOENT' ? 'ssh-unavailable' : 'host-unreachable', sessions: [] };
101
+ }
102
+ });
103
+ ipcMain.handle('screens:attachRemote', (_event, input, screenName) => {
104
+ const connection = validateKnownConnection(input);
105
+ if (typeof screenName !== 'string')
106
+ throw new Error('screenName is required');
107
+ return buildRemoteScreenAttachArgs(connection, screenName);
108
+ });
70
109
  ipcMain.handle('sessions:attach', (_event, id) => {
71
110
  if (typeof id !== 'string' || !id)
72
111
  throw new Error('attachSession requires a session id');
@@ -2,8 +2,15 @@ const { contextBridge, ipcRenderer } = require('electron');
2
2
 
3
3
  const api = {
4
4
  listSessions: () => ipcRenderer.invoke('sessions:list'),
5
+ listHistory: () => ipcRenderer.invoke('sessions:history'),
6
+ removeHistory: (entryId) => ipcRenderer.invoke('sessions:historyRemove', entryId),
7
+ listBackends: () => ipcRenderer.invoke('sessions:backends'),
8
+ listWslDistributions: () => ipcRenderer.invoke('sessions:wslDistributions'),
5
9
  createLocalSession: (request) => ipcRenderer.invoke('sessions:createLocal', request),
6
10
  createSshSession: (request) => ipcRenderer.invoke('sessions:createSsh', request),
11
+ listKnownConnections: () => ipcRenderer.invoke('connections:listKnown'),
12
+ discoverRemoteScreens: (connection) => ipcRenderer.invoke('screens:discoverRemote', connection),
13
+ buildRemoteScreenAttach: (connection, screenName) => ipcRenderer.invoke('screens:attachRemote', connection, screenName),
7
14
  attachSession: (id) => ipcRenderer.invoke('sessions:attach', id),
8
15
  closeSession: (id) => ipcRenderer.invoke('sessions:close', id),
9
16
  write: (sessionId, data) => ipcRenderer.send('terminal:write', sessionId, data),
@@ -0,0 +1,105 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ const SCHEMA_VERSION = 1;
5
+ const DEFAULT_LIMIT = 100;
6
+ /** Main-process-only, best-effort history persistence. Never stores cwd, args, or credentials. */
7
+ export class SessionHistoryStore {
8
+ filePath;
9
+ limit;
10
+ now;
11
+ entries = [];
12
+ loaded = false;
13
+ writeQueue = Promise.resolve();
14
+ constructor(options) {
15
+ this.filePath = options.filePath;
16
+ this.limit = Math.max(1, Math.floor(options.limit ?? DEFAULT_LIMIT));
17
+ this.now = options.now ?? (() => new Date());
18
+ }
19
+ async list() {
20
+ await this.ensureLoaded();
21
+ return this.entries.map((entry) => ({ ...entry, session: { ...entry.session } }));
22
+ }
23
+ async record(event, session, available) {
24
+ await this.ensureLoaded();
25
+ const entry = {
26
+ id: randomUUID(),
27
+ timestamp: this.now().toISOString(),
28
+ event,
29
+ session: redactSession(session),
30
+ available: Boolean(available)
31
+ };
32
+ this.entries = [entry, ...this.entries].slice(0, this.limit);
33
+ await this.persist();
34
+ return entry;
35
+ }
36
+ async remove(entryId) {
37
+ await this.ensureLoaded();
38
+ const index = this.entries.findIndex((item) => item.id === entryId);
39
+ if (index < 0)
40
+ return false;
41
+ this.entries = this.entries.filter((item) => item.id !== entryId);
42
+ await this.persist();
43
+ return true;
44
+ }
45
+ async ensureLoaded() {
46
+ if (this.loaded)
47
+ return;
48
+ this.loaded = true;
49
+ try {
50
+ const parsed = JSON.parse(await readFile(this.filePath, 'utf8'));
51
+ if (!isHistoryFile(parsed))
52
+ return;
53
+ this.entries = parsed.entries.slice(0, this.limit).map(normalizeEntry).filter((entry) => entry !== undefined);
54
+ }
55
+ catch {
56
+ this.entries = [];
57
+ }
58
+ }
59
+ async persist() {
60
+ const snapshot = { version: SCHEMA_VERSION, entries: this.entries };
61
+ this.writeQueue = this.writeQueue.then(async () => {
62
+ try {
63
+ await mkdir(dirname(this.filePath), { recursive: true });
64
+ const temp = join(dirname(this.filePath), `.history.tmp-${process.pid}-${randomUUID()}`);
65
+ await writeFile(temp, JSON.stringify(snapshot, null, 2), { encoding: 'utf8', mode: 0o600 });
66
+ await rename(temp, this.filePath);
67
+ }
68
+ catch {
69
+ // History must never affect terminal operation.
70
+ }
71
+ });
72
+ await this.writeQueue;
73
+ }
74
+ }
75
+ function redactSession(session) {
76
+ const result = {
77
+ id: safeText(session.id), name: safeText(session.name), kind: session.kind, host: safeText(session.host), backend: session.backend, scope: session.scope, screenName: session.screenName, sshTarget: session.sshTarget, wslDistribution: session.wslDistribution
78
+ };
79
+ if (typeof session.backend === 'string')
80
+ result.backend = safeText(session.backend);
81
+ if (typeof session.scope === 'string')
82
+ result.scope = safeText(session.scope);
83
+ if (typeof session.screenName === 'string')
84
+ result.screenName = safeText(session.screenName);
85
+ if (typeof session.sshTarget === 'string')
86
+ result.sshTarget = safeText(session.sshTarget);
87
+ if (typeof session.wslDistribution === 'string')
88
+ result.wslDistribution = safeText(session.wslDistribution);
89
+ return result;
90
+ }
91
+ function safeText(value) { return value.replace(/[\u0000-\u001f\u007f]/g, '').slice(0, 256); }
92
+ function isHistoryFile(value) {
93
+ return typeof value === 'object' && value !== null && value.version === SCHEMA_VERSION && Array.isArray(value.entries);
94
+ }
95
+ function normalizeEntry(value) {
96
+ if (!value || typeof value !== 'object')
97
+ return undefined;
98
+ const item = value;
99
+ if (typeof item.id !== 'string' || typeof item.timestamp !== 'string' || !['created', 'attached', 'detached', 'closed', 'reconnect-failed'].includes(item.event) || typeof item.available !== 'boolean' || !item.session || typeof item.session !== 'object')
100
+ return undefined;
101
+ if (!['local', 'ssh'].includes(item.session.kind) || typeof item.session.id !== 'string' || typeof item.session.name !== 'string' || typeof item.session.host !== 'string')
102
+ return undefined;
103
+ return { id: safeText(item.id), timestamp: item.timestamp, event: item.event, available: item.available, session: redactSession(item.session) };
104
+ }
105
+ export function defaultHistoryPath(userDataPath) { return join(userDataPath, 'session-history.json'); }
@@ -6,8 +6,13 @@ import { promisify } from 'node:util';
6
6
  const execFileAsync = promisify(execFile);
7
7
  const require = createRequire(import.meta.url);
8
8
  const NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,48}$/;
9
- /** host, user@host, host:port, user@host:port — no shell metacharacters. */
10
- const SSH_TARGET = /^(?:([A-Za-z0-9._-]+)@)?([A-Za-z0-9.-]+)(?::(\d{1,5}))?$/;
9
+ /**
10
+ * host, user@host, host:port, user@host:port — no shell metacharacters.
11
+ * Host and user must start alphanumeric: a leading '-' would be parsed by
12
+ * ssh's getopt as an option, and `-Fsome.cfg` can point ssh at an attacker
13
+ * -chosen config file (hence ProxyCommand) without any shell involvement.
14
+ */
15
+ const SSH_TARGET = /^(?:([A-Za-z0-9][A-Za-z0-9._-]*)@)?([A-Za-z0-9][A-Za-z0-9.-]*)(?::(\d{1,5}))?$/;
11
16
  export function validateSessionName(name) {
12
17
  const value = name.trim();
13
18
  if (!NAME.test(value)) {
@@ -32,9 +37,38 @@ export function validateSshTarget(input) {
32
37
  const args = ['-tt'];
33
38
  if (portText)
34
39
  args.push('-p', portText);
35
- args.push(destination);
40
+ // '--' ends option parsing, so the destination can never be read as a flag.
41
+ args.push('--', destination);
36
42
  return { target: value, args };
37
43
  }
44
+ export function parseWslDistributions(output) {
45
+ return output.split(/\r?\n/).slice(1).map((line) => line.replace(/^\*?\s*/, '').trim())
46
+ .map((line) => line.split(/\s{2,}/)[0]).filter((name) => /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(name));
47
+ }
48
+ export function shellBackendArgs(backend, distribution) {
49
+ if (backend === 'wsl') {
50
+ if (distribution && !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(distribution.trim()))
51
+ throw new Error('Invalid WSL distribution name.');
52
+ return { backend, executable: process.platform === 'win32' ? 'wsl.exe' : 'wsl', args: distribution ? ['-d', distribution.trim()] : [], label: distribution ? `WSL · ${distribution.trim()}` : 'WSL', wslDistribution: distribution?.trim() };
53
+ }
54
+ if (backend === 'powershell')
55
+ return { backend, executable: process.platform === 'win32' ? 'pwsh.exe' : 'pwsh', args: [], label: 'PowerShell' };
56
+ if (backend === 'zsh')
57
+ return { backend, executable: 'zsh', args: [], label: 'zsh' };
58
+ return { backend: 'bash', executable: 'bash', args: [], label: 'bash' };
59
+ }
60
+ export async function discoverShellBackends() {
61
+ const result = [shellBackendArgs('bash')];
62
+ for (const backend of ['powershell', 'wsl']) {
63
+ const candidate = shellBackendArgs(backend);
64
+ try {
65
+ await execFileAsync(candidate.executable, backend === 'wsl' ? ['--status'] : ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()']);
66
+ result.push(candidate);
67
+ }
68
+ catch { /* unavailable */ }
69
+ }
70
+ return result;
71
+ }
38
72
  export function parseScreenList(output) {
39
73
  return output
40
74
  .split('\n')
@@ -54,7 +88,11 @@ export function parseScreenList(output) {
54
88
  cwd: homedir(),
55
89
  status: attached ? 'connected' : 'detached',
56
90
  lastSeen: new Date().toISOString(),
57
- persistence: 'screen'
91
+ persistence: 'screen',
92
+ backend: 'screen',
93
+ scope: 'local',
94
+ source: 'discovered',
95
+ screenName: name
58
96
  }
59
97
  ];
60
98
  });
@@ -71,6 +109,8 @@ export class ScreenService {
71
109
  ptys = new Map();
72
110
  sshSessions = new Map();
73
111
  fallbackLocalSessions = new Map();
112
+ onEvent;
113
+ constructor(options = {}) { this.onEvent = options.onEvent; }
74
114
  async available() {
75
115
  try {
76
116
  await execFileAsync('screen', ['--version']);
@@ -111,33 +151,24 @@ export class ScreenService {
111
151
  return [];
112
152
  }
113
153
  }
114
- async createLocal(name, cwd = homedir()) {
115
- const safeName = validateSessionName(name);
154
+ async createLocal(nameOrRequest, cwd = homedir()) {
155
+ const request = typeof nameOrRequest === 'string' ? { name: nameOrRequest, cwd } : nameOrRequest;
156
+ const safeName = validateSessionName(request.name);
157
+ const backend = request.backend ?? 'bash';
158
+ const shell = shellBackendArgs(backend, request.wslDistribution);
159
+ const requestedCwd = request.cwd ?? homedir();
160
+ if (backend !== 'bash' && !(await executableAvailable(shell.executable)))
161
+ throw new Error(`${shell.label} is not installed or unavailable.`);
116
162
  if (!(await this.available())) {
117
- const fallback = {
118
- id: `local:${safeName}`,
119
- name: safeName,
120
- kind: 'local',
121
- host: 'local',
122
- cwd,
123
- status: 'detached',
124
- lastSeen: new Date().toISOString(),
125
- persistence: 'process'
126
- };
163
+ const fallback = { id: `local:${safeName}`, name: safeName, kind: 'local', host: 'local', cwd: requestedCwd, status: 'detached', lastSeen: new Date().toISOString(), persistence: 'process', backend, scope: 'local', source: 'active', wslDistribution: shell.wslDistribution };
127
164
  this.fallbackLocalSessions.set(fallback.id, fallback);
165
+ this.onEvent?.('created', fallback, true);
128
166
  return fallback;
129
167
  }
130
- await execFileAsync('screen', ['-dmS', safeName, 'bash'], { cwd });
131
- return {
132
- id: `local:${safeName}`,
133
- name: safeName,
134
- kind: 'local',
135
- host: 'local',
136
- cwd,
137
- status: 'detached',
138
- lastSeen: new Date().toISOString(),
139
- persistence: 'screen'
140
- };
168
+ await execFileAsync('screen', ['-dmS', safeName, shell.executable, ...shell.args], { cwd: requestedCwd });
169
+ const session = { id: `local:${safeName}`, name: safeName, kind: 'local', host: 'local', cwd: requestedCwd, status: 'detached', lastSeen: new Date().toISOString(), persistence: 'screen', backend: 'screen', scope: 'local', source: 'active', screenName: safeName, wslDistribution: shell.wslDistribution };
170
+ this.onEvent?.('created', session, true);
171
+ return session;
141
172
  }
142
173
  async createSsh(targetInput, name) {
143
174
  const { target } = validateSshTarget(targetInput);
@@ -150,9 +181,13 @@ export class ScreenService {
150
181
  cwd: '~',
151
182
  status: 'detached',
152
183
  lastSeen: new Date().toISOString(),
153
- sshTarget: target
184
+ sshTarget: target,
185
+ backend: 'ssh',
186
+ scope: 'remote',
187
+ source: 'active'
154
188
  };
155
189
  this.sshSessions.set(session.id, session);
190
+ this.onEvent?.('created', session, true);
156
191
  return session;
157
192
  }
158
193
  attach(id, onData, onExit) {
@@ -164,11 +199,16 @@ export class ScreenService {
164
199
  if (fallback) {
165
200
  fallback.status = 'connected';
166
201
  fallback.lastSeen = new Date().toISOString();
167
- this.spawnCommand(id, 'bash', [], onData, onExit, fallback.cwd);
202
+ const shell = shellBackendArgs(fallback.backend ?? 'bash', fallback.wslDistribution);
203
+ this.spawnCommand(id, shell.executable, shell.args, onData, onExit, fallback.cwd);
204
+ this.onEvent?.('attached', fallback, true);
168
205
  return { ...fallback };
169
206
  }
170
- const name = id.slice('local:'.length);
207
+ // Session ids cross the IPC boundary from the renderer, so re-validate
208
+ // rather than trusting that createLocal produced this one.
209
+ const name = validateSessionName(id.slice('local:'.length));
171
210
  this.spawnCommand(id, 'screen', ['-x', name], onData, onExit);
211
+ this.onEvent?.('attached', existing, true);
172
212
  return { ...existing, status: 'connected', persistence: 'screen' };
173
213
  }
174
214
  const session = this.sshSessions.get(id);
@@ -178,7 +218,15 @@ export class ScreenService {
178
218
  const { args } = validateSshTarget(session.sshTarget);
179
219
  session.status = 'connected';
180
220
  session.lastSeen = new Date().toISOString();
181
- this.spawnCommand(id, 'ssh', args, onData, onExit);
221
+ try {
222
+ this.spawnCommand(id, 'ssh', args, onData, onExit);
223
+ }
224
+ catch (error) {
225
+ session.status = 'error';
226
+ this.onEvent?.('reconnect-failed', session, false);
227
+ throw error;
228
+ }
229
+ this.onEvent?.('attached', session, true);
182
230
  return { ...session };
183
231
  }
184
232
  getSession(id) {
@@ -186,7 +234,7 @@ export class ScreenService {
186
234
  const fallback = this.fallbackLocalSessions.get(id);
187
235
  if (fallback)
188
236
  return fallback;
189
- const name = id.slice('local:'.length);
237
+ const name = validateSessionName(id.slice('local:'.length));
190
238
  return {
191
239
  id,
192
240
  name,
@@ -229,11 +277,15 @@ export class ScreenService {
229
277
  proc.onExit(() => {
230
278
  this.ptys.delete(sessionId);
231
279
  const fallback = this.fallbackLocalSessions.get(sessionId);
232
- if (fallback)
280
+ if (fallback) {
233
281
  fallback.status = 'detached';
282
+ this.onEvent?.('detached', fallback, false);
283
+ }
234
284
  const ssh = this.sshSessions.get(sessionId);
235
- if (ssh)
285
+ if (ssh) {
236
286
  ssh.status = 'detached';
287
+ this.onEvent?.('detached', ssh, false);
288
+ }
237
289
  onExit(fallback
238
290
  ? 'Terminal detached. This process-only local session will not survive app exit; install screen for persistence.'
239
291
  : 'Terminal detached. Local screen sessions remain alive; SSH sessions end with the connection.');
@@ -259,8 +311,17 @@ export class ScreenService {
259
311
  * discoverable via `screen -ls`; SSH and process-only sessions end.
260
312
  */
261
313
  close(sessionId) {
314
+ const session = this.getSession(sessionId);
262
315
  this.detach(sessionId);
263
316
  this.sshSessions.delete(sessionId);
264
317
  this.fallbackLocalSessions.delete(sessionId);
318
+ this.onEvent?.('closed', session, false);
265
319
  }
266
320
  }
321
+ async function executableAvailable(executable) { try {
322
+ await execFileAsync(executable, ['--version']);
323
+ return true;
324
+ }
325
+ catch {
326
+ return false;
327
+ } }
@@ -0,0 +1,119 @@
1
+ import { homedir } from 'node:os';
2
+ import { readFile } from 'node:fs/promises';
3
+ const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4
+ const HOST = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,253}$/;
5
+ // Must start alphanumeric like TOKEN/HOST above: the user is concatenated into
6
+ // `user@host`, so a leading '-' makes the whole destination look like an option
7
+ // to ssh's getopt (`-Fevil.cfg@host` reads an attacker-chosen config file).
8
+ const USER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
9
+ const SCREEN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,48}$/;
10
+ function words(value) {
11
+ const result = [];
12
+ const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'|(\S+)/g;
13
+ for (let match; (match = re.exec(value));)
14
+ result.push((match[1] ?? match[2] ?? match[3]).replace(/\\([\\"'])/g, '$1'));
15
+ return result;
16
+ }
17
+ export function parseSshConfig(text, source) {
18
+ const entries = [];
19
+ let current = [];
20
+ const flush = () => { entries.push(...current); current = []; };
21
+ for (const raw of text.split(/\r?\n/)) {
22
+ const line = raw.replace(/\s+#.*$/, '').trim();
23
+ if (!line || line.startsWith('#'))
24
+ continue;
25
+ const match = line.match(/^([^\s=]+)\s*(?:=\s*|\s+)(.*)$/);
26
+ if (!match)
27
+ continue;
28
+ const key = match[1].toLowerCase();
29
+ const args = words(match[2].trim());
30
+ if (key === 'host') {
31
+ flush();
32
+ const aliases = args.filter((alias) => !alias.includes('*') && !alias.includes('?') && TOKEN.test(alias));
33
+ current = aliases.map((alias) => ({ alias, source }));
34
+ continue;
35
+ }
36
+ if (!current.length || !args[0])
37
+ continue;
38
+ const value = args[0];
39
+ for (const entry of current) {
40
+ if (key === 'hostname' && !entry.hostName)
41
+ entry.hostName = value;
42
+ else if (key === 'user' && !entry.user && USER.test(value))
43
+ entry.user = value;
44
+ else if (key === 'port' && !entry.port && /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535)
45
+ entry.port = Number(value);
46
+ else if (key === 'identityfile' && !entry.identityFile)
47
+ entry.identityFile = value;
48
+ }
49
+ }
50
+ flush();
51
+ return entries;
52
+ }
53
+ export async function listKnownConnections(path = `${homedir()}/.ssh/config`) {
54
+ try {
55
+ return parseSshConfig(await readFile(path, 'utf8'), path);
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ export function validateKnownConnection(input) {
62
+ if (!input || typeof input !== 'object')
63
+ throw new Error('Invalid SSH connection.');
64
+ const value = input;
65
+ if (typeof value.alias !== 'string' || !TOKEN.test(value.alias))
66
+ throw new Error('Invalid SSH alias.');
67
+ const result = { alias: value.alias };
68
+ if (value.hostName !== undefined) {
69
+ if (typeof value.hostName !== 'string' || !HOST.test(value.hostName))
70
+ throw new Error('Invalid SSH hostname.');
71
+ result.hostName = value.hostName;
72
+ }
73
+ if (value.user !== undefined) {
74
+ if (typeof value.user !== 'string' || !USER.test(value.user))
75
+ throw new Error('Invalid SSH user.');
76
+ result.user = value.user;
77
+ }
78
+ if (value.port !== undefined) {
79
+ if (!Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
80
+ throw new Error('Invalid SSH port.');
81
+ result.port = value.port;
82
+ }
83
+ if (value.identityFile !== undefined) {
84
+ if (typeof value.identityFile !== 'string' || value.identityFile.length > 4096 || /[\0\r\n]/.test(value.identityFile))
85
+ throw new Error('Invalid identity file metadata.');
86
+ result.identityFile = value.identityFile;
87
+ }
88
+ if (value.source !== undefined && typeof value.source === 'string')
89
+ result.source = value.source;
90
+ return result;
91
+ }
92
+ function sshArgs(connection) {
93
+ const c = validateKnownConnection(connection);
94
+ const destination = c.user ? `${c.user}@${c.hostName ?? c.alias}` : (c.hostName ?? c.alias);
95
+ const args = [];
96
+ if (c.port)
97
+ args.push('-p', String(c.port));
98
+ args.push(destination, '--');
99
+ return args;
100
+ }
101
+ export function buildRemoteScreenDiscoveryArgs(connection) {
102
+ return { file: 'ssh', args: [...sshArgs(connection), 'screen', '-ls'] };
103
+ }
104
+ export function buildRemoteScreenAttachArgs(connection, screenName) {
105
+ if (!SCREEN.test(screenName))
106
+ throw new Error('Invalid screen session name.');
107
+ return { file: 'ssh', args: [...sshArgs(connection), 'screen', '-x', screenName] };
108
+ }
109
+ export function parseRemoteScreenList(output, host) {
110
+ if (!TOKEN.test(host))
111
+ throw new Error('Invalid remote host identity.');
112
+ return output.split(/\r?\n/).flatMap((line) => {
113
+ const match = line.trim().match(/^\d+\.(\S+)\s+\(([^)]+)\)/);
114
+ if (!match || !SCREEN.test(match[1]))
115
+ return [];
116
+ const name = match[1];
117
+ return [{ id: `remote:${host}:${name}`, name, kind: 'ssh', host, cwd: '~', status: /attached/i.test(match[2]) ? 'connected' : 'detached', lastSeen: new Date().toISOString(), persistence: 'screen', sshTarget: host, backend: 'screen', scope: 'remote', source: 'discovered', screenName: name }];
118
+ });
119
+ }
@@ -0,0 +1 @@
1
+ .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;inset:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.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{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.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;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#d8dee9;background:#0a0c10;font-size:13px;line-height:1.4;color-scheme:dark}*{box-sizing:border-box}body{margin:0;min-width:980px;overflow:hidden;background:#0a0c10}button,input{font:inherit}button{color:inherit;border:0;cursor:pointer}button:focus-visible,input:focus-visible{outline:1px solid #7aa2f7;outline-offset:2px}.app-shell{height:100vh;background:#0a0c10}.window-bar{height:48px;display:flex;align-items:center;border-bottom:1px solid #202630;background:#0f1218;padding:0 12px;gap:14px}.window-brand{width:120px;display:flex;align-items:center;gap:9px;flex:none}.brand-mark{display:grid;place-items:center;width:25px;height:25px;border:1px solid #3b82f6;border-radius:5px;color:#7aa2f7;font:700 9px JetBrains Mono,monospace}.brand-name{font-size:13px;font-weight:650;letter-spacing:.01em}.workspace-tabs{display:flex;align-items:center;gap:6px;min-width:0;overflow:auto}.workspace-tab{height:32px;display:flex;align-items:center;gap:8px;padding:0 12px;border:1px solid transparent;border-radius:5px;color:#8b93a3;background:transparent;white-space:nowrap}.workspace-tab.active{background:#151a22;border-color:#252d39;color:#d8dee9}.tab-dot,.connection-dot,.pane-live{width:6px;height:6px;border-radius:50%;background:#9ece6a;display:inline-block;flex:none}.tab-meta{font:11px JetBrains Mono,monospace;color:#697386;margin-left:2px}.window-actions{margin-left:auto;display:flex;align-items:center;gap:4px;flex:none}.bar-button,.avatar{height:30px;display:flex;align-items:center;gap:7px;padding:0 9px;border-radius:4px;color:#8b93a3;background:transparent}.bar-button:hover{background:#1a202a;color:#d8dee9}.icon{font-size:14px;line-height:1;color:currentColor}.icon-svg{width:14px;height:14px;display:block;flex:none}.square-button .icon-svg,.rail-button .icon-svg,.layout-button .icon-svg,.inline-restore .icon-svg{width:15px;height:15px}.avatar{width:28px;justify-content:center;border:1px solid #2a3340;color:#9ece6a;padding:0;font-size:11px;margin-left:6px}.body-shell{height:calc(100vh - 48px);display:flex}.rail{width:48px;display:flex;flex-direction:column;align-items:center;gap:5px;padding:10px 0;border-right:1px solid #202630;background:#0d1015;flex:none}.rail-button{width:32px;height:32px;display:grid;place-items:center;border-radius:5px;color:#697386;background:transparent}.rail-button:hover,.rail-button.active{background:#1a202a;color:#d8dee9}.rail-button.active{box-shadow:inset 2px 0 #7aa2f7}.rail-spacer{flex:1}.session-drawer{width:238px;display:flex;flex-direction:column;border-right:1px solid #202630;background:#0f1218;flex:none}.drawer-head{display:flex;align-items:center;justify-content:space-between;padding:18px 14px 12px;gap:8px}.drawer-head-actions{display:flex;align-items:center;gap:6px}.eyebrow{display:block;color:#697386;font-size:10px;font-weight:700;letter-spacing:.1em}.drawer-head h1{margin:3px 0 0;font-size:16px;font-weight:600;letter-spacing:-.01em}.square-button{width:27px;height:27px;display:grid;place-items:center;border:1px solid #2a3340;border-radius:4px;background:#151a22;color:#9aa4b2}.square-button:hover{border-color:#7aa2f7;color:#d8dee9}.drawer-tools{display:flex;gap:2px;padding:0 10px 10px;border-bottom:1px solid #202630}.drawer-tool{padding:5px 7px;border-radius:4px;color:#697386;background:transparent;font-size:11px}.drawer-tool span{margin-left:5px;color:#4e5969}.drawer-tool:hover,.drawer-tool.active{background:#1a202a;color:#d8dee9}.session-list{padding:8px 7px;overflow:auto;flex:1}.session-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 8px;text-align:left;border-radius:5px;background:transparent;color:#8b93a3}.session-row:hover{background:#151a22}.session-row.active{background:#1c2531;color:#d8dee9;box-shadow:inset 2px 0 #7aa2f7}.status-dot{width:6px;height:6px;border-radius:50%;background:#4e5969;flex:none}.status-dot.connected{background:#9ece6a}.status-dot.detached{background:#e0af68}.status-dot.error{background:#f7768e}.session-copy{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1}.session-copy b{font-size:12px;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-copy small{font:10px JetBrains Mono,monospace;color:#697386;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.session-state{font:11px JetBrains Mono,monospace;color:#697386}.session-empty{display:flex;flex-direction:column;gap:6px;padding:18px 10px;color:#697386}.session-empty b{color:#8b93a3;font-size:12px;font-weight:550}.session-empty small{font-size:11px;line-height:1.45}.session-group-label{padding:8px 10px 4px;color:#697386;font:10px JetBrains Mono,monospace;letter-spacing:.04em}.drawer-bottom{margin-top:auto;padding:10px 9px;border-top:1px solid #202630}.drawer-action{width:100%;display:flex;align-items:center;gap:8px;padding:7px 5px;color:#8b93a3;background:transparent;text-align:left;border-radius:4px;font-size:11px}.drawer-action:hover{background:#151a22;color:#d8dee9}.drawer-action kbd{margin-left:auto;color:#4e5969;font:9px JetBrains Mono,monospace}.drawer-status{display:flex;align-items:center;gap:8px;margin-top:13px;padding:5px;color:#697386;font:10px JetBrains Mono,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.workspace{display:flex;flex:1;flex-direction:column;min-width:0;background:#0a0c10}.workspace-head{height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-bottom:1px solid #202630;background:#0d1015}.location{display:flex;align-items:center;gap:8px;color:#8b93a3;font:11px JetBrains Mono,monospace;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.inline-restore{width:24px;height:24px;display:grid;place-items:center;border-radius:4px;color:#8b93a3;background:#151a22;border:1px solid #2a3340}.inline-restore:hover{color:#d8dee9;border-color:#7aa2f7}.slash{color:#3d4654}.muted-text{color:#697386}.layout-controls{display:flex;align-items:center;gap:2px}.control-label{font-size:9px;color:#4e5969;letter-spacing:.08em;margin-right:6px}.layout-button{width:26px;height:25px;display:grid;place-items:center;border-radius:4px;background:transparent;color:#697386}.layout-button:hover,.layout-button.active{background:#1a202a;color:#9ece6a}.layout-button .icon-svg{width:15px;height:15px}.pane-grid{display:grid;flex:1;min-height:0;gap:1px;background:#202630;padding:0}.pane-grid.split-v{grid-template-columns:1fr 1fr}.pane-grid.split-h{grid-template-rows:1fr 1fr}.pane-grid.grid{grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr}.pane-grid.maximized-pane-grid{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.pane-grid:not(.maximized-pane-grid) .overflow-pane,.pane-grid.maximized-pane-grid .pane:not(.maximized-pane){display:none}.pane{min-width:0;min-height:0;display:flex;flex-direction:column;background:#0a0c10}.terminal-pane{outline:1px solid #202630;outline-offset:-1px}.terminal-pane.focused{outline:1px solid #7aa2f7;outline-offset:-1px}.pane-title{height:29px;display:flex;align-items:center;justify-content:space-between;padding:0 9px;border-bottom:1px solid #202630;color:#8b93a3;font:10px JetBrains Mono,monospace}.pane-actions,.pane-dim{color:#4e5969;font-size:9px}.pane-actions{display:flex;align-items:center;gap:5px}.pane-nav,.pane-maximize,.pane-close,.pane-mic{width:19px;height:19px;display:grid;place-items:center;padding:0;border:1px solid transparent;border-radius:3px;color:#697386;background:transparent}.pane-nav:hover,.pane-maximize:hover,.pane-mic:hover{color:#d8dee9;background:#1a202a;border-color:#2a3340}.pane-close:hover{color:#f7768e;background:#f7768e24;border-color:#f7768e66}.pane-mic:disabled{opacity:.4;cursor:default}.pane-mic.listening{color:#f7768e;background:#f7768e29;border-color:#f7768e8c;animation:mic-pulse 1.2s ease-in-out infinite}.pane-mic.transcribing{color:#e0af68;background:#e0af6824;border-color:#e0af6880;animation:mic-pulse .8s ease-in-out infinite}@keyframes mic-pulse{0%,to{transform:scale(1)}50%{transform:scale(1.15)}}.pane-nav .icon-svg,.pane-maximize .icon-svg,.pane-close .icon-svg,.pane-mic .icon-svg{width:13px;height:13px}.pane-live{width:5px;height:5px;margin-right:5px}.terminal{flex:1;min-height:0;min-width:0;padding:0;overflow:visible;position:relative;background:#0a0c10;line-height:1.2;font-variant-ligatures:none}.terminal .xterm{position:absolute;inset:0;height:100%!important;width:100%!important;padding:0;box-sizing:border-box}.terminal .xterm-viewport{overflow-y:auto!important}.terminal .xterm-screen{position:relative}.terminal .xterm-rows{line-height:inherit}.empty-pane{background:#0d1015}.empty-pane-body{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;height:100%;color:#697386}.empty-glyph{display:grid;place-items:center;width:30px;height:30px;border:1px dashed #344050;border-radius:5px;color:#697386;font-size:18px}.empty-pane-body strong{font-size:11px;color:#8b93a3;font-weight:550}.empty-pane-body small{font-size:10px}.empty-pane-body button{margin-top:8px;padding:6px 9px;border:1px solid #2a3340;border-radius:4px;background:#151a22;color:#8b93a3;font-size:10px}.empty-pane-body button:hover{border-color:#7aa2f7;color:#d8dee9}.status-bar{height:25px;display:flex;align-items:center;gap:8px;padding:0 10px;border-top:1px solid #202630;background:#0f1218;color:#697386;font:10px JetBrains Mono,monospace}.status-separator{width:1px;height:11px;background:#2a3340}.status-spacer{flex:1}.overview-layer,.modal-layer{position:fixed;inset:0;z-index:20;display:grid;place-items:center;background:#07090dcc}.overview{width:min(780px,calc(100vw - 100px));background:#11161e;border:1px solid #303b4b;border-radius:8px;box-shadow:0 20px 80px #0009}.overview-head,.modal-head{display:flex;justify-content:space-between;align-items:flex-start;padding:18px 20px 14px;border-bottom:1px solid #202630}.overview-head h2,.modal-head h2{margin:4px 0 0;font-size:17px;font-weight:600}.close-button{padding:4px 7px;border:1px solid #2a3340;border-radius:4px;background:#151a22;color:#697386;font:10px JetBrains Mono,monospace}.close-button:hover{color:#d8dee9;border-color:#7aa2f7}.thumbnail-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:18px 20px}.thumbnail{padding:0;text-align:left;background:#0d1015;border:1px solid #252f3c;border-radius:6px;overflow:hidden}.thumbnail:hover,.thumbnail.active{border-color:#7aa2f7}.thumbnail.active{box-shadow:0 0 0 1px #7aa2f733}.thumbnail-preview{height:100px;padding:13px;background:#0a0c10;border-bottom:1px solid #202630;display:flex;flex-direction:column;gap:6px;position:relative}.thumbnail-line{height:3px;width:70%;background:#4e5969;border-radius:2px}.thumbnail-line.short{width:43%}.thumbnail-line.cyan{background:#7dcfff}.thumbnail-line.green{background:#9ece6a;width:56%}.thumbnail-cursor{position:absolute;left:13px;bottom:15px;width:5px;height:9px;background:#9ece6a}.thumbnail-label{display:flex;flex-direction:column;gap:3px;padding:9px 10px}.thumbnail-label b{font-size:11px;font-weight:550;color:#d8dee9}.thumbnail-label small{font:10px JetBrains Mono,monospace;color:#697386}.overview-empty{grid-column:1 / -1;display:flex;flex-direction:column;align-items:center;gap:7px;padding:36px;color:#697386}.overview-empty span{font-size:24px}.overview-empty b{color:#d8dee9;font-size:12px}.overview-empty small{font-size:11px}.overview-foot{display:flex;align-items:center;gap:8px;padding:12px 20px;border-top:1px solid #202630}.overview-foot button{padding:7px 9px;border:1px solid #2a3340;border-radius:4px;background:#151a22;color:#8b93a3;font-size:10px}.overview-foot button:hover{color:#d8dee9;border-color:#7aa2f7}.overview-hint{margin-left:auto;color:#4e5969;font:10px JetBrains Mono,monospace}.modal-card{width:390px;padding:0 20px 18px;background:#11161e;border:1px solid #303b4b;border-radius:8px;box-shadow:0 20px 80px #0009}.modal-card p{color:#8b93a3;font-size:12px;line-height:1.5;margin:17px 0}.modal-card label{display:block;margin-top:14px;color:#8b93a3;font-size:11px}.modal-card input{display:block;width:100%;margin-top:6px;padding:9px 10px;border:1px solid #2a3340;border-radius:4px;background:#0a0c10;color:#d8dee9;font:12px JetBrains Mono,monospace}.modal-card input:focus{border-color:#7aa2f7;outline:0}.modal-actions{display:flex;justify-content:flex-end;gap:7px;margin-top:20px}.modal-actions button{padding:8px 11px;border:1px solid #2a3340;border-radius:4px;background:#151a22;color:#8b93a3;font-size:11px}.modal-actions button:hover{color:#d8dee9;border-color:#7aa2f7}.primary-button{background:#9ece6a!important;border-color:#9ece6a!important;color:#0a0c10!important;font-weight:600}.primary-button:disabled{opacity:.55;cursor:wait}.approval-card code{display:block;padding:11px;border:1px solid #2a3340;border-radius:4px;background:#0a0c10;color:#7dcfff;font:12px JetBrains Mono,monospace}.warning-text{color:#e0af68}@media(max-width:1100px){.session-drawer{width:210px}.window-brand{width:90px}.brand-name,.bar-button span{display:none}.thumbnail-grid{grid-template-columns:repeat(2,1fr)}}.history-button{color:#7aa2f7}.session-ghosted{opacity:.58;border:1px dashed #344050}.session-ghosted:hover{opacity:.9}.session-error b{color:#f7768e}.session-empty button{align-self:flex-start;padding:5px 8px;border:1px solid #2a3340;border-radius:4px;background:#151a22;color:#8b93a3;font-size:10px}.history-empty button:hover{border-color:#7aa2f7;color:#d8dee9}.history-popover{position:fixed;z-index:30;top:58px;left:245px;width:285px;max-height:min(440px,calc(100vh - 76px));overflow:auto;padding:10px;border:1px solid #303b4b;border-radius:7px;background:#11161e;box-shadow:0 18px 55px #0009}.history-layer{position:fixed;inset:0;z-index:25}.history-head{display:flex;justify-content:space-between;align-items:center;padding:3px 4px 9px;color:#d8dee9}.history-head button{background:transparent;color:#697386;font-size:18px}.history-item{width:100%;display:flex;align-items:center;justify-content:space-between;gap:9px;padding:8px 5px;text-align:left;border-top:1px solid #202630;background:transparent;color:#8b93a3}.history-main{display:flex;align-items:center;gap:9px;min-width:0}.history-text{display:flex;flex-direction:column;align-items:flex-start;min-width:0}.history-remove{margin-left:8px;background:transparent;color:#536176;border:none;cursor:pointer;font-size:16px;line-height:1}.history-item:hover .history-remove{color:#253044}:root[data-theme=light] .history-remove{color:#68768a}.history-item:hover{background:#1a202a;color:#d8dee9}.history-item b,.history-item small{display:block}.history-item small,.history-note,.history-empty{color:#697386;font-size:10px}.history-kind{min-width:43px;color:#7aa2f7;font:9px JetBrains Mono,monospace}.history-empty{padding:13px 5px}.history-note{display:block;padding:9px 4px 2px;line-height:1.4}:root[data-theme=light] .history-popover{background:#fff;border-color:#c7d2e1;color:#253044}:root[data-theme=light] .history-head{color:#253044}:root[data-theme=light] .history-head button{color:#536176}:root[data-theme=light] .history-item{border-top-color:#dbe2ec;color:#536176;background:transparent}:root[data-theme=light] .history-item:hover{background:#e1e9f4;color:#253044}:root[data-theme=light] .history-item small,:root[data-theme=light] .history-note,:root[data-theme=light] .history-empty{color:#68768a}:root[data-theme=light] .history-kind{color:#2459a6}:root[data-theme=light]{color:#253044;background:#f7f9fc;color-scheme:light}:root[data-theme=light] body,:root[data-theme=light] .app-shell,:root[data-theme=light] .workspace,:root[data-theme=light] .pane,:root[data-theme=light] .terminal,:root[data-theme=light] .thumbnail-preview{background:#f7f9fc}:root[data-theme=light] .window-bar,:root[data-theme=light] .session-drawer,:root[data-theme=light] .status-bar{background:#fff;border-color:#dbe2ec}:root[data-theme=light] .rail,:root[data-theme=light] .workspace-head,:root[data-theme=light] .empty-pane{background:#eef2f7;border-color:#dbe2ec}:root[data-theme=light] .pane-grid{background:#dbe2ec}:root[data-theme=light] .workspace-tab,:root[data-theme=light] .bar-button,:root[data-theme=light] .avatar,:root[data-theme=light] .rail-button,:root[data-theme=light] .drawer-tool,:root[data-theme=light] .drawer-action,:root[data-theme=light] .location,:root[data-theme=light] .layout-button{color:#5c687b}:root[data-theme=light] .workspace-tab.active,:root[data-theme=light] .bar-button:hover,:root[data-theme=light] .rail-button:hover,:root[data-theme=light] .rail-button.active,:root[data-theme=light] .drawer-tool:hover,:root[data-theme=light] .drawer-tool.active,:root[data-theme=light] .session-row:hover,:root[data-theme=light] .inline-restore,:root[data-theme=light] .layout-button:hover,:root[data-theme=light] .layout-button.active{background:#e1e9f4;color:#253044}:root[data-theme=light] .workspace-tab.active,:root[data-theme=light] .session-row.active{border-color:#c7d5e8;background:#dbe7f5;color:#253044}:root[data-theme=light] .session-row,:root[data-theme=light] .pane-title,:root[data-theme=light] .drawer-head h1,:root[data-theme=light] .empty-pane-body strong,:root[data-theme=light] .thumbnail-label b,:root[data-theme=light] .overview-empty b{color:#253044}:root[data-theme=light] .eyebrow,:root[data-theme=light] .tab-meta,:root[data-theme=light] .session-copy small,:root[data-theme=light] .session-state,:root[data-theme=light] .session-empty,:root[data-theme=light] .drawer-status,:root[data-theme=light] .muted-text,:root[data-theme=light] .pane-actions,:root[data-theme=light] .pane-dim,:root[data-theme=light] .status-bar,:root[data-theme=light] .overview-hint,:root[data-theme=light] .thumbnail-label small,:root[data-theme=light] .overview-empty,:root[data-theme=light] .modal-card p,:root[data-theme=light] .modal-card label{color:#68768a}:root[data-theme=light] .brand-mark{color:#2459a6;border-color:#6d9bd6}:root[data-theme=light] .avatar,:root[data-theme=light] .square-button,:root[data-theme=light] .inline-restore,:root[data-theme=light] .empty-pane-body button,:root[data-theme=light] .overview-foot button,:root[data-theme=light] .close-button,:root[data-theme=light] .modal-actions button,:root[data-theme=light] .modal-card input,:root[data-theme=light] .approval-card code{background:#fff;border-color:#c7d2e1;color:#536176}:root[data-theme=light] .session-row.active{box-shadow:inset 2px 0 #2459a6}:root[data-theme=light] .rail-button.active{box-shadow:inset 2px 0 #2459a6}:root[data-theme=light] .layout-button.active{color:#16803c}:root[data-theme=light] .pane-close:hover{color:#c53030}:root[data-theme=light] .pane-mic.listening{color:#c53030}:root[data-theme=light] .pane-mic.transcribing{color:#b7791f}:root[data-theme=light] .overview,:root[data-theme=light] .modal-card{background:#fff;border-color:#c7d2e1;box-shadow:0 20px 80px #53617633}:root[data-theme=light] .overview-head,:root[data-theme=light] .modal-head,:root[data-theme=light] .overview-foot,:root[data-theme=light] .pane-title{border-color:#dbe2ec}:root[data-theme=light] .thumbnail{background:#eef2f7;border-color:#c7d2e1}:root[data-theme=light] .thumbnail-preview,:root[data-theme=light] .approval-card code,:root[data-theme=light] .modal-card input{background:#f7f9fc;border-color:#dbe2ec}:root[data-theme=light] .modal-layer,:root[data-theme=light] .overview-layer{background:#25304433}:root[data-theme=light] .empty-glyph{border-color:#b8c6d8}:root[data-theme=light] .primary-button{background:#16803c!important;border-color:#16803c!important;color:#fff!important}