zerogterm 0.2.0-alpha.1 → 0.2.0-alpha.2

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'); }
@@ -35,6 +35,34 @@ export function validateSshTarget(input) {
35
35
  args.push(destination);
36
36
  return { target: value, args };
37
37
  }
38
+ export function parseWslDistributions(output) {
39
+ return output.split(/\r?\n/).slice(1).map((line) => line.replace(/^\*?\s*/, '').trim())
40
+ .map((line) => line.split(/\s{2,}/)[0]).filter((name) => /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(name));
41
+ }
42
+ export function shellBackendArgs(backend, distribution) {
43
+ if (backend === 'wsl') {
44
+ if (distribution && !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(distribution.trim()))
45
+ throw new Error('Invalid WSL distribution name.');
46
+ return { backend, executable: process.platform === 'win32' ? 'wsl.exe' : 'wsl', args: distribution ? ['-d', distribution.trim()] : [], label: distribution ? `WSL · ${distribution.trim()}` : 'WSL', wslDistribution: distribution?.trim() };
47
+ }
48
+ if (backend === 'powershell')
49
+ return { backend, executable: process.platform === 'win32' ? 'pwsh.exe' : 'pwsh', args: [], label: 'PowerShell' };
50
+ if (backend === 'zsh')
51
+ return { backend, executable: 'zsh', args: [], label: 'zsh' };
52
+ return { backend: 'bash', executable: 'bash', args: [], label: 'bash' };
53
+ }
54
+ export async function discoverShellBackends() {
55
+ const result = [shellBackendArgs('bash')];
56
+ for (const backend of ['powershell', 'wsl']) {
57
+ const candidate = shellBackendArgs(backend);
58
+ try {
59
+ await execFileAsync(candidate.executable, backend === 'wsl' ? ['--status'] : ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()']);
60
+ result.push(candidate);
61
+ }
62
+ catch { /* unavailable */ }
63
+ }
64
+ return result;
65
+ }
38
66
  export function parseScreenList(output) {
39
67
  return output
40
68
  .split('\n')
@@ -54,7 +82,11 @@ export function parseScreenList(output) {
54
82
  cwd: homedir(),
55
83
  status: attached ? 'connected' : 'detached',
56
84
  lastSeen: new Date().toISOString(),
57
- persistence: 'screen'
85
+ persistence: 'screen',
86
+ backend: 'screen',
87
+ scope: 'local',
88
+ source: 'discovered',
89
+ screenName: name
58
90
  }
59
91
  ];
60
92
  });
@@ -71,6 +103,8 @@ export class ScreenService {
71
103
  ptys = new Map();
72
104
  sshSessions = new Map();
73
105
  fallbackLocalSessions = new Map();
106
+ onEvent;
107
+ constructor(options = {}) { this.onEvent = options.onEvent; }
74
108
  async available() {
75
109
  try {
76
110
  await execFileAsync('screen', ['--version']);
@@ -111,33 +145,24 @@ export class ScreenService {
111
145
  return [];
112
146
  }
113
147
  }
114
- async createLocal(name, cwd = homedir()) {
115
- const safeName = validateSessionName(name);
148
+ async createLocal(nameOrRequest, cwd = homedir()) {
149
+ const request = typeof nameOrRequest === 'string' ? { name: nameOrRequest, cwd } : nameOrRequest;
150
+ const safeName = validateSessionName(request.name);
151
+ const backend = request.backend ?? 'bash';
152
+ const shell = shellBackendArgs(backend, request.wslDistribution);
153
+ const requestedCwd = request.cwd ?? homedir();
154
+ if (backend !== 'bash' && !(await executableAvailable(shell.executable)))
155
+ throw new Error(`${shell.label} is not installed or unavailable.`);
116
156
  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
- };
157
+ 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
158
  this.fallbackLocalSessions.set(fallback.id, fallback);
159
+ this.onEvent?.('created', fallback, true);
128
160
  return fallback;
129
161
  }
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
- };
162
+ await execFileAsync('screen', ['-dmS', safeName, shell.executable, ...shell.args], { cwd: requestedCwd });
163
+ 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 };
164
+ this.onEvent?.('created', session, true);
165
+ return session;
141
166
  }
142
167
  async createSsh(targetInput, name) {
143
168
  const { target } = validateSshTarget(targetInput);
@@ -150,9 +175,13 @@ export class ScreenService {
150
175
  cwd: '~',
151
176
  status: 'detached',
152
177
  lastSeen: new Date().toISOString(),
153
- sshTarget: target
178
+ sshTarget: target,
179
+ backend: 'ssh',
180
+ scope: 'remote',
181
+ source: 'active'
154
182
  };
155
183
  this.sshSessions.set(session.id, session);
184
+ this.onEvent?.('created', session, true);
156
185
  return session;
157
186
  }
158
187
  attach(id, onData, onExit) {
@@ -164,11 +193,14 @@ export class ScreenService {
164
193
  if (fallback) {
165
194
  fallback.status = 'connected';
166
195
  fallback.lastSeen = new Date().toISOString();
167
- this.spawnCommand(id, 'bash', [], onData, onExit, fallback.cwd);
196
+ const shell = shellBackendArgs(fallback.backend ?? 'bash', fallback.wslDistribution);
197
+ this.spawnCommand(id, shell.executable, shell.args, onData, onExit, fallback.cwd);
198
+ this.onEvent?.('attached', fallback, true);
168
199
  return { ...fallback };
169
200
  }
170
201
  const name = id.slice('local:'.length);
171
202
  this.spawnCommand(id, 'screen', ['-x', name], onData, onExit);
203
+ this.onEvent?.('attached', existing, true);
172
204
  return { ...existing, status: 'connected', persistence: 'screen' };
173
205
  }
174
206
  const session = this.sshSessions.get(id);
@@ -178,7 +210,15 @@ export class ScreenService {
178
210
  const { args } = validateSshTarget(session.sshTarget);
179
211
  session.status = 'connected';
180
212
  session.lastSeen = new Date().toISOString();
181
- this.spawnCommand(id, 'ssh', args, onData, onExit);
213
+ try {
214
+ this.spawnCommand(id, 'ssh', args, onData, onExit);
215
+ }
216
+ catch (error) {
217
+ session.status = 'error';
218
+ this.onEvent?.('reconnect-failed', session, false);
219
+ throw error;
220
+ }
221
+ this.onEvent?.('attached', session, true);
182
222
  return { ...session };
183
223
  }
184
224
  getSession(id) {
@@ -229,11 +269,15 @@ export class ScreenService {
229
269
  proc.onExit(() => {
230
270
  this.ptys.delete(sessionId);
231
271
  const fallback = this.fallbackLocalSessions.get(sessionId);
232
- if (fallback)
272
+ if (fallback) {
233
273
  fallback.status = 'detached';
274
+ this.onEvent?.('detached', fallback, false);
275
+ }
234
276
  const ssh = this.sshSessions.get(sessionId);
235
- if (ssh)
277
+ if (ssh) {
236
278
  ssh.status = 'detached';
279
+ this.onEvent?.('detached', ssh, false);
280
+ }
237
281
  onExit(fallback
238
282
  ? 'Terminal detached. This process-only local session will not survive app exit; install screen for persistence.'
239
283
  : 'Terminal detached. Local screen sessions remain alive; SSH sessions end with the connection.');
@@ -259,8 +303,17 @@ export class ScreenService {
259
303
  * discoverable via `screen -ls`; SSH and process-only sessions end.
260
304
  */
261
305
  close(sessionId) {
306
+ const session = this.getSession(sessionId);
262
307
  this.detach(sessionId);
263
308
  this.sshSessions.delete(sessionId);
264
309
  this.fallbackLocalSessions.delete(sessionId);
310
+ this.onEvent?.('closed', session, false);
265
311
  }
266
312
  }
313
+ async function executableAvailable(executable) { try {
314
+ await execFileAsync(executable, ['--version']);
315
+ return true;
316
+ }
317
+ catch {
318
+ return false;
319
+ } }
@@ -0,0 +1,116 @@
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
+ const USER = /^[A-Za-z0-9._-]{1,64}$/;
6
+ const SCREEN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,48}$/;
7
+ function words(value) {
8
+ const result = [];
9
+ const re = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'|(\S+)/g;
10
+ for (let match; (match = re.exec(value));)
11
+ result.push((match[1] ?? match[2] ?? match[3]).replace(/\\([\\"'])/g, '$1'));
12
+ return result;
13
+ }
14
+ export function parseSshConfig(text, source) {
15
+ const entries = [];
16
+ let current = [];
17
+ const flush = () => { entries.push(...current); current = []; };
18
+ for (const raw of text.split(/\r?\n/)) {
19
+ const line = raw.replace(/\s+#.*$/, '').trim();
20
+ if (!line || line.startsWith('#'))
21
+ continue;
22
+ const match = line.match(/^([^\s=]+)\s*(?:=\s*|\s+)(.*)$/);
23
+ if (!match)
24
+ continue;
25
+ const key = match[1].toLowerCase();
26
+ const args = words(match[2].trim());
27
+ if (key === 'host') {
28
+ flush();
29
+ const aliases = args.filter((alias) => !alias.includes('*') && !alias.includes('?') && TOKEN.test(alias));
30
+ current = aliases.map((alias) => ({ alias, source }));
31
+ continue;
32
+ }
33
+ if (!current.length || !args[0])
34
+ continue;
35
+ const value = args[0];
36
+ for (const entry of current) {
37
+ if (key === 'hostname' && !entry.hostName)
38
+ entry.hostName = value;
39
+ else if (key === 'user' && !entry.user && USER.test(value))
40
+ entry.user = value;
41
+ else if (key === 'port' && !entry.port && /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535)
42
+ entry.port = Number(value);
43
+ else if (key === 'identityfile' && !entry.identityFile)
44
+ entry.identityFile = value;
45
+ }
46
+ }
47
+ flush();
48
+ return entries;
49
+ }
50
+ export async function listKnownConnections(path = `${homedir()}/.ssh/config`) {
51
+ try {
52
+ return parseSshConfig(await readFile(path, 'utf8'), path);
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ export function validateKnownConnection(input) {
59
+ if (!input || typeof input !== 'object')
60
+ throw new Error('Invalid SSH connection.');
61
+ const value = input;
62
+ if (typeof value.alias !== 'string' || !TOKEN.test(value.alias))
63
+ throw new Error('Invalid SSH alias.');
64
+ const result = { alias: value.alias };
65
+ if (value.hostName !== undefined) {
66
+ if (typeof value.hostName !== 'string' || !HOST.test(value.hostName))
67
+ throw new Error('Invalid SSH hostname.');
68
+ result.hostName = value.hostName;
69
+ }
70
+ if (value.user !== undefined) {
71
+ if (typeof value.user !== 'string' || !USER.test(value.user))
72
+ throw new Error('Invalid SSH user.');
73
+ result.user = value.user;
74
+ }
75
+ if (value.port !== undefined) {
76
+ if (!Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
77
+ throw new Error('Invalid SSH port.');
78
+ result.port = value.port;
79
+ }
80
+ if (value.identityFile !== undefined) {
81
+ if (typeof value.identityFile !== 'string' || value.identityFile.length > 4096 || /[\0\r\n]/.test(value.identityFile))
82
+ throw new Error('Invalid identity file metadata.');
83
+ result.identityFile = value.identityFile;
84
+ }
85
+ if (value.source !== undefined && typeof value.source === 'string')
86
+ result.source = value.source;
87
+ return result;
88
+ }
89
+ function sshArgs(connection) {
90
+ const c = validateKnownConnection(connection);
91
+ const destination = c.user ? `${c.user}@${c.hostName ?? c.alias}` : (c.hostName ?? c.alias);
92
+ const args = [];
93
+ if (c.port)
94
+ args.push('-p', String(c.port));
95
+ args.push(destination, '--');
96
+ return args;
97
+ }
98
+ export function buildRemoteScreenDiscoveryArgs(connection) {
99
+ return { file: 'ssh', args: [...sshArgs(connection), 'screen', '-ls'] };
100
+ }
101
+ export function buildRemoteScreenAttachArgs(connection, screenName) {
102
+ if (!SCREEN.test(screenName))
103
+ throw new Error('Invalid screen session name.');
104
+ return { file: 'ssh', args: [...sshArgs(connection), 'screen', '-x', screenName] };
105
+ }
106
+ export function parseRemoteScreenList(output, host) {
107
+ if (!TOKEN.test(host))
108
+ throw new Error('Invalid remote host identity.');
109
+ return output.split(/\r?\n/).flatMap((line) => {
110
+ const match = line.trim().match(/^\d+\.(\S+)\s+\(([^)]+)\)/);
111
+ if (!match || !SCREEN.test(match[1]))
112
+ return [];
113
+ const name = match[1];
114
+ 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 }];
115
+ });
116
+ }
@@ -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}