deckide 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/apps/server/dist/config.js +77 -0
- package/apps/server/dist/index.js +5 -0
- package/apps/server/dist/middleware/auth.js +78 -0
- package/apps/server/dist/middleware/cors.js +26 -0
- package/apps/server/dist/middleware/security.js +16 -0
- package/apps/server/dist/pty-client.js +177 -0
- package/apps/server/dist/pty-daemon.js +246 -0
- package/apps/server/dist/routes/decks.js +95 -0
- package/apps/server/dist/routes/files.js +221 -0
- package/apps/server/dist/routes/git.js +775 -0
- package/apps/server/dist/routes/settings.js +95 -0
- package/apps/server/dist/routes/terminals.js +239 -0
- package/apps/server/dist/routes/workspaces.js +83 -0
- package/apps/server/dist/server.js +257 -0
- package/apps/server/dist/types.js +1 -0
- package/apps/server/dist/utils/database.js +136 -0
- package/apps/server/dist/utils/error.js +28 -0
- package/apps/server/dist/utils/path.js +98 -0
- package/apps/server/dist/utils/shell.js +4 -0
- package/apps/server/dist/websocket.js +207 -0
- package/apps/server/package.json +26 -0
- package/apps/web/dist/assets/index-C-usl0y0.css +32 -0
- package/apps/web/dist/assets/index-CibzlLP5.js +129 -0
- package/apps/web/dist/index.html +13 -0
- package/bin/deckide.js +79 -0
- package/package.json +77 -0
- package/packages/shared/dist/types.d.ts +124 -0
- package/packages/shared/dist/types.d.ts.map +1 -0
- package/packages/shared/dist/types.js +3 -0
- package/packages/shared/dist/types.js.map +1 -0
- package/packages/shared/dist/utils-node.d.ts +22 -0
- package/packages/shared/dist/utils-node.d.ts.map +1 -0
- package/packages/shared/dist/utils-node.js +35 -0
- package/packages/shared/dist/utils-node.js.map +1 -0
- package/packages/shared/dist/utils.d.ts +90 -0
- package/packages/shared/dist/utils.d.ts.map +1 -0
- package/packages/shared/dist/utils.js +186 -0
- package/packages/shared/dist/utils.js.map +1 -0
- package/packages/shared/package.json +16 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { getWorkspaceKey } from './path.js';
|
|
4
|
+
export function checkDatabaseIntegrity(dbPath) {
|
|
5
|
+
try {
|
|
6
|
+
const tempDb = new DatabaseSync(dbPath);
|
|
7
|
+
const result = tempDb.prepare('PRAGMA integrity_check').get();
|
|
8
|
+
tempDb.close();
|
|
9
|
+
return result && typeof result === 'object' && 'integrity_check' in result && result.integrity_check === 'ok';
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function handleDatabaseCorruption(dbPath) {
|
|
16
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
17
|
+
const corruptedPath = `${dbPath}.corrupted-${timestamp}`;
|
|
18
|
+
console.error('CRITICAL: Database corruption detected!');
|
|
19
|
+
try {
|
|
20
|
+
fsSync.renameSync(dbPath, corruptedPath);
|
|
21
|
+
console.log(`Corrupted database moved to: ${corruptedPath}`);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error('Failed to move corrupted database:', err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function initializeDatabase(db) {
|
|
28
|
+
// Enable WAL mode for better concurrent access
|
|
29
|
+
db.exec('PRAGMA journal_mode = WAL;');
|
|
30
|
+
db.exec('PRAGMA synchronous = FULL;');
|
|
31
|
+
db.exec(`
|
|
32
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
name TEXT NOT NULL,
|
|
35
|
+
path TEXT NOT NULL,
|
|
36
|
+
normalized_path TEXT NOT NULL UNIQUE,
|
|
37
|
+
created_at TEXT NOT NULL
|
|
38
|
+
);
|
|
39
|
+
`);
|
|
40
|
+
db.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS decks (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
name TEXT NOT NULL,
|
|
44
|
+
root TEXT NOT NULL,
|
|
45
|
+
workspace_id TEXT NOT NULL,
|
|
46
|
+
created_at TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
`);
|
|
49
|
+
db.exec(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS terminals (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
deck_id TEXT NOT NULL,
|
|
53
|
+
title TEXT NOT NULL,
|
|
54
|
+
command TEXT,
|
|
55
|
+
buffer TEXT NOT NULL DEFAULT '',
|
|
56
|
+
created_at TEXT NOT NULL
|
|
57
|
+
);
|
|
58
|
+
`);
|
|
59
|
+
// Migration: add sort_order column to decks
|
|
60
|
+
try {
|
|
61
|
+
db.exec('ALTER TABLE decks ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// column already exists
|
|
65
|
+
}
|
|
66
|
+
// Create indexes for better query performance
|
|
67
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_decks_workspace_id ON decks(workspace_id);`);
|
|
68
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_terminals_deck_id ON terminals(deck_id);`);
|
|
69
|
+
}
|
|
70
|
+
export function loadPersistedState(db, workspaces, workspacePathIndex, decks) {
|
|
71
|
+
const workspaceRows = db
|
|
72
|
+
.prepare('SELECT id, name, path, created_at FROM workspaces ORDER BY created_at ASC')
|
|
73
|
+
.all();
|
|
74
|
+
workspaceRows.forEach((row) => {
|
|
75
|
+
const id = String(row.id);
|
|
76
|
+
const name = String(row.name);
|
|
77
|
+
const workspacePath = String(row.path);
|
|
78
|
+
const createdAt = String(row.created_at);
|
|
79
|
+
const workspace = {
|
|
80
|
+
id,
|
|
81
|
+
name,
|
|
82
|
+
path: workspacePath,
|
|
83
|
+
createdAt
|
|
84
|
+
};
|
|
85
|
+
workspaces.set(id, workspace);
|
|
86
|
+
workspacePathIndex.set(getWorkspaceKey(workspacePath), id);
|
|
87
|
+
});
|
|
88
|
+
const deckRows = db
|
|
89
|
+
.prepare('SELECT id, name, root, workspace_id, created_at FROM decks ORDER BY sort_order ASC, created_at ASC')
|
|
90
|
+
.all();
|
|
91
|
+
deckRows.forEach((row) => {
|
|
92
|
+
const workspaceId = String(row.workspace_id);
|
|
93
|
+
if (!workspaces.has(workspaceId))
|
|
94
|
+
return;
|
|
95
|
+
const deck = {
|
|
96
|
+
id: String(row.id),
|
|
97
|
+
name: String(row.name),
|
|
98
|
+
root: String(row.root),
|
|
99
|
+
workspaceId,
|
|
100
|
+
createdAt: String(row.created_at)
|
|
101
|
+
};
|
|
102
|
+
decks.set(deck.id, deck);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Terminal persistence functions
|
|
106
|
+
export function loadPersistedTerminals(db, decks) {
|
|
107
|
+
const rows = db
|
|
108
|
+
.prepare('SELECT id, deck_id, title, command, created_at FROM terminals ORDER BY created_at ASC')
|
|
109
|
+
.all();
|
|
110
|
+
const terminals = [];
|
|
111
|
+
rows.forEach((row) => {
|
|
112
|
+
const deckId = String(row.deck_id);
|
|
113
|
+
// Only load terminals for existing decks
|
|
114
|
+
if (!decks.has(deckId)) {
|
|
115
|
+
// Clean up orphaned terminal
|
|
116
|
+
db.prepare('DELETE FROM terminals WHERE id = ?').run(String(row.id));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
terminals.push({
|
|
120
|
+
id: String(row.id),
|
|
121
|
+
deckId,
|
|
122
|
+
title: String(row.title),
|
|
123
|
+
command: row.command ? String(row.command) : null,
|
|
124
|
+
createdAt: String(row.created_at)
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return terminals;
|
|
128
|
+
}
|
|
129
|
+
export function saveTerminal(db, id, deckId, title, command, createdAt) {
|
|
130
|
+
const stmt = db.prepare('INSERT OR REPLACE INTO terminals (id, deck_id, title, command, created_at) VALUES (?, ?, ?, ?, ?)');
|
|
131
|
+
stmt.run(id, deckId, title, command, createdAt);
|
|
132
|
+
}
|
|
133
|
+
export function deleteTerminal(db, id) {
|
|
134
|
+
const stmt = db.prepare('DELETE FROM terminals WHERE id = ?');
|
|
135
|
+
stmt.run(id);
|
|
136
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NODE_ENV } from '../config.js';
|
|
2
|
+
import { createHttpError as sharedCreateHttpError, getErrorMessage as sharedGetErrorMessage } from '@deck-ide/shared/utils-node';
|
|
3
|
+
export function createHttpError(message, status) {
|
|
4
|
+
return sharedCreateHttpError(message, status);
|
|
5
|
+
}
|
|
6
|
+
export function getErrorMessage(error) {
|
|
7
|
+
return sharedGetErrorMessage(error);
|
|
8
|
+
}
|
|
9
|
+
export function handleError(c, error) {
|
|
10
|
+
const status = (error?.status ?? 500);
|
|
11
|
+
const message = getErrorMessage(error) || 'Unexpected error';
|
|
12
|
+
if (NODE_ENV === 'production' && status === 500) {
|
|
13
|
+
return c.json({ error: 'Internal server error' }, status);
|
|
14
|
+
}
|
|
15
|
+
return c.json({ error: message }, status);
|
|
16
|
+
}
|
|
17
|
+
export async function readJson(c) {
|
|
18
|
+
try {
|
|
19
|
+
return await c.req.json();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Log parse errors in development for debugging
|
|
23
|
+
if (NODE_ENV === 'development') {
|
|
24
|
+
console.warn('JSON parse error:', getErrorMessage(error));
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_ROOT } from '../config.js';
|
|
4
|
+
import { createHttpError } from './error.js';
|
|
5
|
+
import { normalizeWorkspacePath as sharedNormalizeWorkspacePath, getWorkspaceKey as sharedGetWorkspaceKey, getWorkspaceName as sharedGetWorkspaceName } from '@deck-ide/shared/utils-node';
|
|
6
|
+
export function normalizeWorkspacePath(inputPath = '') {
|
|
7
|
+
return sharedNormalizeWorkspacePath(inputPath || '', DEFAULT_ROOT);
|
|
8
|
+
}
|
|
9
|
+
export function getWorkspaceKey(workspacePath) {
|
|
10
|
+
return sharedGetWorkspaceKey(workspacePath);
|
|
11
|
+
}
|
|
12
|
+
export function getWorkspaceName(workspacePath, index) {
|
|
13
|
+
return sharedGetWorkspaceName(workspacePath, index);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates and resolves a path safely within a workspace root.
|
|
17
|
+
* Prevents path traversal and symlink escape attacks.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveSafePath(workspacePath, inputPath = '') {
|
|
20
|
+
// Normalize the input to prevent directory traversal
|
|
21
|
+
const normalizedInput = path.normalize(inputPath);
|
|
22
|
+
// Check for obvious path traversal attempts
|
|
23
|
+
if (normalizedInput.includes('..') || path.isAbsolute(normalizedInput)) {
|
|
24
|
+
throw createHttpError('Invalid path: path traversal not allowed', 400);
|
|
25
|
+
}
|
|
26
|
+
// Resolve paths
|
|
27
|
+
const root = path.resolve(workspacePath);
|
|
28
|
+
const resolved = path.join(root, normalizedInput);
|
|
29
|
+
// Double-check the resolved path is within root
|
|
30
|
+
const relative = path.relative(root, resolved);
|
|
31
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
32
|
+
throw createHttpError('Path escapes workspace root', 400);
|
|
33
|
+
}
|
|
34
|
+
// Try to resolve the real path (following symlinks)
|
|
35
|
+
try {
|
|
36
|
+
const realPath = await fs.realpath(resolved);
|
|
37
|
+
const realRoot = await fs.realpath(root);
|
|
38
|
+
// Ensure the real path is still within the real root
|
|
39
|
+
const realRelative = path.relative(realRoot, realPath);
|
|
40
|
+
if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
|
|
41
|
+
throw createHttpError('Symlink target escapes workspace root', 400);
|
|
42
|
+
}
|
|
43
|
+
return realPath;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// If it's an HttpError we threw, re-throw it
|
|
47
|
+
if (error?.status) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
// File might not exist yet (e.g., for new file creation)
|
|
51
|
+
// Verify the parent directory is safe
|
|
52
|
+
const parentDir = path.dirname(resolved);
|
|
53
|
+
try {
|
|
54
|
+
const realParent = await fs.realpath(parentDir);
|
|
55
|
+
const realRoot = await fs.realpath(root);
|
|
56
|
+
const parentRelative = path.relative(realRoot, realParent);
|
|
57
|
+
if (parentRelative.startsWith('..') || path.isAbsolute(parentRelative)) {
|
|
58
|
+
throw createHttpError('Parent directory escapes workspace root', 400);
|
|
59
|
+
}
|
|
60
|
+
// Parent is safe, return the unresolved path for the new file
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
catch (parentError) {
|
|
64
|
+
// If parent doesn't exist, check if we're still within root
|
|
65
|
+
if (parentError?.status) {
|
|
66
|
+
throw parentError;
|
|
67
|
+
}
|
|
68
|
+
// Verify the resolved path is still within root bounds
|
|
69
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
70
|
+
throw createHttpError('Path escapes workspace root', 400);
|
|
71
|
+
}
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validates a file path without resolving it.
|
|
78
|
+
* Useful for quick validation before more expensive operations.
|
|
79
|
+
*/
|
|
80
|
+
export function validatePathSyntax(inputPath) {
|
|
81
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// Check for null bytes
|
|
85
|
+
if (inputPath.includes('\0')) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Check for path traversal
|
|
89
|
+
const normalized = path.normalize(inputPath);
|
|
90
|
+
if (normalized.includes('..') || path.isAbsolute(normalized)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Check for excessive length
|
|
94
|
+
if (inputPath.length > 1000) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import { PORT, TRUST_PROXY } from './config.js';
|
|
4
|
+
import { logSecurityEvent } from './middleware/security.js';
|
|
5
|
+
import { verifyWebSocketAuth } from './middleware/auth.js';
|
|
6
|
+
const MIN_TERMINAL_SIZE = 1;
|
|
7
|
+
const MAX_TERMINAL_SIZE = 500;
|
|
8
|
+
const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB max message size
|
|
9
|
+
// Configurable connection limit per IP (default: 1000)
|
|
10
|
+
let maxConnectionsPerIP = 1000;
|
|
11
|
+
// Track connections per IP
|
|
12
|
+
const connectionsByIP = new Map();
|
|
13
|
+
// Export for API access
|
|
14
|
+
export function getConnectionLimit() {
|
|
15
|
+
return maxConnectionsPerIP;
|
|
16
|
+
}
|
|
17
|
+
export function setConnectionLimit(limit) {
|
|
18
|
+
maxConnectionsPerIP = Math.max(1, Math.floor(limit));
|
|
19
|
+
}
|
|
20
|
+
export function getConnectionStats() {
|
|
21
|
+
const stats = [];
|
|
22
|
+
for (const [ip, connections] of connectionsByIP) {
|
|
23
|
+
stats.push({ ip, count: connections.size });
|
|
24
|
+
}
|
|
25
|
+
return stats;
|
|
26
|
+
}
|
|
27
|
+
export function clearAllConnections() {
|
|
28
|
+
let closedCount = 0;
|
|
29
|
+
for (const [ip, connections] of connectionsByIP) {
|
|
30
|
+
for (const socket of connections) {
|
|
31
|
+
try {
|
|
32
|
+
socket.close(1000, 'Connection cleared by admin');
|
|
33
|
+
closedCount++;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Socket might already be closed
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
connectionsByIP.clear();
|
|
41
|
+
return closedCount;
|
|
42
|
+
}
|
|
43
|
+
function getClientIP(req) {
|
|
44
|
+
if (TRUST_PROXY) {
|
|
45
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
46
|
+
if (typeof forwarded === 'string') {
|
|
47
|
+
return forwarded.split(',')[0].trim();
|
|
48
|
+
}
|
|
49
|
+
const realIp = req.headers['x-real-ip'];
|
|
50
|
+
if (typeof realIp === 'string') {
|
|
51
|
+
return realIp;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return req.socket?.remoteAddress || 'unknown';
|
|
55
|
+
}
|
|
56
|
+
function trackConnection(ip, socket) {
|
|
57
|
+
let connections = connectionsByIP.get(ip);
|
|
58
|
+
if (!connections) {
|
|
59
|
+
connections = new Set();
|
|
60
|
+
connectionsByIP.set(ip, connections);
|
|
61
|
+
}
|
|
62
|
+
if (connections.size >= maxConnectionsPerIP) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
connections.add(socket);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function untrackConnection(ip, socket) {
|
|
69
|
+
const connections = connectionsByIP.get(ip);
|
|
70
|
+
if (connections) {
|
|
71
|
+
connections.delete(socket);
|
|
72
|
+
if (connections.size === 0) {
|
|
73
|
+
connectionsByIP.delete(ip);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function validateTerminalSize(value) {
|
|
78
|
+
const intValue = Math.floor(value);
|
|
79
|
+
if (!Number.isFinite(intValue) || intValue < MIN_TERMINAL_SIZE) {
|
|
80
|
+
return MIN_TERMINAL_SIZE;
|
|
81
|
+
}
|
|
82
|
+
if (intValue > MAX_TERMINAL_SIZE) {
|
|
83
|
+
return MAX_TERMINAL_SIZE;
|
|
84
|
+
}
|
|
85
|
+
return intValue;
|
|
86
|
+
}
|
|
87
|
+
export function setupWebSocketServer(server, terminals) {
|
|
88
|
+
const wss = new WebSocketServer({ server });
|
|
89
|
+
const WS_ALLOWED_ORIGINS = [
|
|
90
|
+
`http://localhost:${PORT}`,
|
|
91
|
+
'http://localhost:5173',
|
|
92
|
+
'http://localhost:3000',
|
|
93
|
+
];
|
|
94
|
+
wss.on('connection', (socket, req) => {
|
|
95
|
+
const socketId = crypto.randomUUID();
|
|
96
|
+
const clientIP = getClientIP(req);
|
|
97
|
+
// Validate Origin header to prevent Cross-Site WebSocket Hijacking
|
|
98
|
+
const origin = req.headers['origin'];
|
|
99
|
+
if (origin && !WS_ALLOWED_ORIGINS.includes(origin)) {
|
|
100
|
+
logSecurityEvent('WS_INVALID_ORIGIN', { ip: clientIP, origin });
|
|
101
|
+
socket.close(1008, 'Invalid origin');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Add error handler for socket
|
|
105
|
+
socket.on('error', (error) => {
|
|
106
|
+
console.error(`WebSocket error for socket ${socketId}:`, error.message);
|
|
107
|
+
try {
|
|
108
|
+
socket.close(1011, 'Internal error');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Socket might already be closed
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// Check connection limit per IP
|
|
115
|
+
if (!trackConnection(clientIP, socket)) {
|
|
116
|
+
logSecurityEvent('WS_CONNECTION_LIMIT_EXCEEDED', { ip: clientIP });
|
|
117
|
+
socket.close(1013, 'Too many connections');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!verifyWebSocketAuth(req)) {
|
|
121
|
+
logSecurityEvent('WS_AUTH_FAILED', { ip: clientIP });
|
|
122
|
+
untrackConnection(clientIP, socket);
|
|
123
|
+
socket.close(1008, 'Unauthorized');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const url = new URL(req.url || '', `http://localhost:${PORT}`);
|
|
127
|
+
const match = url.pathname.match(/\/api\/terminals\/(.+)/);
|
|
128
|
+
if (!match) {
|
|
129
|
+
untrackConnection(clientIP, socket);
|
|
130
|
+
socket.close(1002, 'Invalid path');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const id = match[1];
|
|
134
|
+
const session = terminals.get(id);
|
|
135
|
+
if (!session) {
|
|
136
|
+
untrackConnection(clientIP, socket);
|
|
137
|
+
socket.close(1002, 'Terminal not found');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
session.sockets.add(socket);
|
|
141
|
+
session.lastActive = Date.now();
|
|
142
|
+
// Send buffer content if available
|
|
143
|
+
// bufferOffset: client sends how many bytes it already received, so we only send the delta
|
|
144
|
+
const offsetParam = url.searchParams.get('bufferOffset');
|
|
145
|
+
const clientBufferOffset = offsetParam ? Math.max(0, parseInt(offsetParam, 10) || 0) : 0;
|
|
146
|
+
if (session.buffer) {
|
|
147
|
+
try {
|
|
148
|
+
const bufferToSend = clientBufferOffset > 0
|
|
149
|
+
? session.buffer.slice(clientBufferOffset)
|
|
150
|
+
: session.buffer;
|
|
151
|
+
if (bufferToSend) {
|
|
152
|
+
socket.send(bufferToSend);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error(`Failed to send buffer to socket ${socketId}:`, error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
socket.on('message', (data) => {
|
|
160
|
+
try {
|
|
161
|
+
// Convert to string
|
|
162
|
+
const message = data.toString('utf8');
|
|
163
|
+
// Check message size
|
|
164
|
+
const messageSize = Buffer.byteLength(message, 'utf8');
|
|
165
|
+
if (messageSize > MAX_MESSAGE_SIZE) {
|
|
166
|
+
logSecurityEvent('WS_MESSAGE_TOO_LARGE', { ip: clientIP, size: messageSize });
|
|
167
|
+
socket.send('\r\n\x1b[31mMessage too large. Maximum size is 64KB.\x1b[0m\r\n');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
session.lastActive = Date.now();
|
|
171
|
+
// Check for resize message
|
|
172
|
+
if (message.startsWith('\u0000resize:')) {
|
|
173
|
+
const payload = message.slice('\u0000resize:'.length);
|
|
174
|
+
const [colsRaw, rowsRaw] = payload.split(',');
|
|
175
|
+
const cols = validateTerminalSize(Number(colsRaw));
|
|
176
|
+
const rows = validateTerminalSize(Number(rowsRaw));
|
|
177
|
+
try {
|
|
178
|
+
session.resize(cols, rows);
|
|
179
|
+
}
|
|
180
|
+
catch (resizeError) {
|
|
181
|
+
console.error(`Failed to resize terminal ${id}:`, resizeError);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
// Write user input to terminal
|
|
187
|
+
session.write(message);
|
|
188
|
+
}
|
|
189
|
+
catch (writeError) {
|
|
190
|
+
console.error(`Failed to write to terminal ${id}:`, writeError);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error(`Error handling WebSocket message for socket ${socketId}:`, error);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
socket.on('close', () => {
|
|
198
|
+
session.sockets.delete(socket);
|
|
199
|
+
session.lastActive = Date.now();
|
|
200
|
+
untrackConnection(clientIP, socket);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
wss.on('error', (error) => {
|
|
204
|
+
console.error('WebSocket server error:', error);
|
|
205
|
+
});
|
|
206
|
+
return wss;
|
|
207
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deck-ide-server",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx src/index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"serve": "node dist/index.js",
|
|
10
|
+
"start": "node dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@deck-ide/shared": "file:../../packages/shared",
|
|
14
|
+
"@hono/node-server": "^1.12.2",
|
|
15
|
+
"hono": "^4.5.10",
|
|
16
|
+
"node-pty": "^1.0.0",
|
|
17
|
+
"simple-git": "^3.27.0",
|
|
18
|
+
"ws": "^8.17.0",
|
|
19
|
+
"zod": "^4.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.10.9",
|
|
23
|
+
"tsx": "^4.19.2",
|
|
24
|
+
"typescript": "^5.6.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Noto+Sans+JP:wght@400;500;600&display=swap";/**
|
|
2
|
+
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
|
+
* https://github.com/chjj/term.js
|
|
5
|
+
* @license MIT
|
|
6
|
+
*
|
|
7
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
* in the Software without restriction, including without limitation the rights
|
|
10
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
* furnished to do so, subject to the following conditions:
|
|
13
|
+
*
|
|
14
|
+
* The above copyright notice and this permission notice shall be included in
|
|
15
|
+
* all copies or substantial portions of the Software.
|
|
16
|
+
*
|
|
17
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
* THE SOFTWARE.
|
|
24
|
+
*
|
|
25
|
+
* Originally forked from (with the author's permission):
|
|
26
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
27
|
+
* http://bellard.org/jslinux/
|
|
28
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
29
|
+
* The original design remains. The terminal itself
|
|
30
|
+
* has been extended to include xterm CSI codes, among
|
|
31
|
+
* other features.
|
|
32
|
+
*/.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;right:0;left:0;top:0;bottom: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,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.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}/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"JetBrains Mono", monospace;--color-red-400:oklch(70.4% .191 22.216);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#f3f3f3;--color-bg-soft:#e8e8e8;--color-panel:#fff;--color-sidebar:#f3f3f3;--color-activitybar:#2c2c2c;--color-activitybar-fg:#fff;--color-activitybar-inactive:#fff9;--color-border:#e5e5e5;--color-ink:#333;--color-ink-muted:#6e6e6e;--color-muted:#717171;--color-accent:#007acc;--color-statusbar:#007acc;--color-statusbar-fg:#fff;--color-list-hover:#0000000a;--color-list-active:#0078d41a;--color-focus:#0090f1;--color-title-bar:#ddd;--color-git-modified:#cca700;--color-git-untracked:#4ec9b0;--color-git-deleted:#f14c4c;--color-git-staged:#73c991;--color-git-renamed:#4fc1ff;--color-git-conflicted:#ff6b6b;--font-ui:"Noto Sans JP", sans-serif}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*,:before,:after{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font-family:var(--font-ui);background:var(--color-bg);color:var(--color-ink);font-size:13px;line-height:1.4;overflow:hidden}button,input,select,textarea{color:inherit;font-family:inherit}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--color-focus);outline-offset:2px}@supports (height:100dvh){html,body,#root{height:100dvh}}*{scrollbar-width:thin;scrollbar-color:#8080804d transparent}:root[data-theme=dark] *{scrollbar-color:#ffffff26 transparent}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#8080804d;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#80808080}:root[data-theme=dark] ::-webkit-scrollbar-thumb{background:#ffffff26}:root[data-theme=dark] ::-webkit-scrollbar-thumb:hover{background:#ffffff4d}::-webkit-scrollbar-corner{background:0 0}}@layer components{.activity-bar-item.active:before{content:"";background:var(--color-activitybar-fg);width:2px;position:absolute;top:0;bottom:0;left:0}.workspace-editor-grid .activity-bar-item.active:before{background:var(--color-accent)}.app>nav.activity-bar .activity-bar-item.active:before{width:auto;height:2px;inset:0 0 auto}.git-tab.active:after{content:"";background:var(--color-accent);height:2px;position:absolute;bottom:0;left:0;right:0}.editor-tab:hover .editor-tab-close,.editor-tab.active .editor-tab-close{opacity:1}.editor-tab.dirty .editor-tab-close{opacity:0}.editor-tab.dirty:hover .editor-tab-close{opacity:1}.editor-tab.dirty:hover .editor-tab-dirty{display:none}.deck-tab:hover .deck-tab-close,.deck-tab.active .deck-tab-close{opacity:.5}.deck-tab-close:hover{color:var(--color-ink);background:#ffffff1f;opacity:1!important}.git-file-row:hover .git-file-actions{opacity:1}.tree-row.is-open .tree-chevron-icon{transform:rotate(90deg)}.terminal-split-container.is-drop-target:after{content:"デッキをここにドロップして分割";border:2px dashed var(--color-accent);color:var(--color-ink-muted);pointer-events:none;z-index:2;background:#0078d414;border-radius:6px;justify-content:center;align-items:center;font-size:12px;font-weight:600;display:flex;position:absolute;top:10px;right:10px;bottom:10px;left:10px}.git-tree-modified .tree-label{color:var(--color-git-modified)}.git-tree-untracked .tree-label{color:var(--color-git-untracked)}.git-tree-deleted .tree-label{color:var(--color-git-deleted)}.git-tree-staged .tree-label{color:var(--color-git-staged)}.git-tree-renamed .tree-label{color:var(--color-git-renamed)}.git-tree-conflicted .tree-label{color:var(--color-git-conflicted)}.sidebar-content .panel{box-shadow:none;background:0 0;border:none;border-radius:0}.sidebar-content .panel-header{background:var(--color-sidebar);z-index:1;margin-bottom:0;padding:8px 12px;position:sticky;top:0}.sidebar-content .panel-body{padding:0}}@layer utilities{.fixed{position:fixed}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.z-50{z-index:50}.z-\[500\]{z-index:500}.z-\[999\]{z-index:999}.z-\[1000\]{z-index:1000}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing) * 0)}.my-1{margin-block:calc(var(--spacing) * 1)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-\[22px\]{height:22px}.h-\[280px\]{height:280px}.h-full{height:100%}.h-px{height:1px}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-\[28px\]{min-height:28px}.min-h-\[40px\]{min-height:40px}.min-h-\[44px\]{min-height:44px}.min-h-\[50px\]{min-height:50px}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-\[22px\]{width:22px}.w-\[500px\]{width:500px}.w-full{width:100%}.max-w-\[500px\]{max-width:500px}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[44px\]{min-width:44px}.min-w-\[140px\]{min-width:140px}.min-w-\[160px\]{min-width:160px}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.grid-rows-\[35px_minmax\(0\,1fr\)\]{grid-template-rows:35px minmax(0,1fr)}.flex-col{flex-direction:column}.place-items-center{place-items:center}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-\[2px\]{border-radius:2px}.rounded-\[3px\]{border-radius:3px}.rounded-\[6px\]{border-radius:6px}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-accent{border-color:var(--color-accent)}.border-border{border-color:var(--color-border)}.bg-\[\#f14c4c\]{background-color:#f14c4c}.bg-accent{background-color:var(--color-accent)}.bg-accent\/20{background-color:#007acc33}@supports (color:color-mix(in lab,red,red)){.bg-accent\/20{background-color:color-mix(in oklab,var(--color-accent) 20%,transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-bg-soft{background-color:var(--color-bg-soft)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-border{background-color:var(--color-border)}.bg-list-active{background-color:var(--color-list-active)}.bg-panel{background-color:var(--color-panel)}.bg-sidebar{background-color:var(--color-sidebar)}.bg-title-bar{background-color:var(--color-title-bar)}.bg-transparent{background-color:#0000}.fill-current{fill:currentColor}.p-0{padding:calc(var(--spacing) * 0)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.font-\[inherit\]{font-family:inherit}.font-mono{font-family:var(--font-mono)}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.75rem\]{font-size:.75rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.5px\]{--tw-tracking:.5px;letter-spacing:.5px}.break-words{overflow-wrap:break-word}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#f14c4c\]{color:#f14c4c}.text-accent{color:var(--color-accent)}.text-ink{color:var(--color-ink)}.text-ink-muted{color:var(--color-ink-muted)}.text-muted{color:var(--color-muted)}.text-red-400{color:var(--color-red-400)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.shadow-\[0_4px_12px_rgba\(0\,0\,0\,0\.15\)\]{--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.focus-within\:bg-list-hover:focus-within{background-color:var(--color-list-hover)}@media (hover:hover){.hover\:bg-\[rgba\(241\,76\,76\,0\.1\)\]:hover{background-color:#f14c4c1a}.hover\:bg-list-hover:hover{background-color:var(--color-list-hover)}.hover\:bg-red-400\/10:hover{background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-400\/10:hover{background-color:color-mix(in oklab,var(--color-red-400) 10%,transparent)}}.hover\:text-ink:hover{color:var(--color-ink)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-focus:focus{border-color:var(--color-focus)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.scrollbar-none{scrollbar-width:none}.scrollbar-none::-webkit-scrollbar{display:none}.spin{animation:1s linear infinite spin}.slide-in{animation:.2s slideIn}}:root[data-theme=dark]{--color-bg:#1e1e1e;--color-bg-soft:#252526;--color-panel:#1e1e1e;--color-sidebar:#252526;--color-activitybar:#333;--color-activitybar-inactive:#fff6;--color-border:#3c3c3c;--color-ink:#ccc;--color-ink-muted:#9e9e9e;--color-muted:#8b8b8b;--color-accent:#0078d4;--color-list-hover:#ffffff0a;--color-list-active:#0078d433;--color-focus:#007fd4;--color-title-bar:#3c3c3c}@keyframes spin{to{transform:rotate(360deg)}}@keyframes slideIn{0%{transform:translate(-100%)}to{transform:translate(0)}}.app{grid-template-rows:minmax(0,1fr) 22px;grid-template-columns:48px minmax(0,1fr);height:100vh;display:grid;overflow:hidden}.activity-bar{background:var(--color-activitybar);flex-direction:column;grid-row:1/3;width:48px;display:flex}.activity-bar-top{flex-direction:column;display:flex}.activity-bar-bottom{flex-direction:column;margin-top:auto;display:flex}.activity-bar-item{width:48px;height:48px;color:var(--color-activitybar-inactive);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;transition:color .15s;display:flex;position:relative}.activity-bar-item:hover,.activity-bar-item.active{color:var(--color-activitybar-fg)}.activity-bar-item svg{width:24px;height:24px}.activity-bar-badge{text-align:center;background:var(--color-accent);color:#fff;border-radius:8px;min-width:16px;height:16px;padding:0 4px;font-size:10px;font-weight:600;line-height:16px;position:absolute;top:8px;right:6px}.main{background:var(--color-bg);flex-direction:column;min-height:0;display:flex}.workspace-editor-overlay{background:var(--color-bg);z-index:30;grid-template-rows:35px minmax(0,1fr);display:grid;position:fixed;top:0;right:0;bottom:0;left:0}.workspace-editor-header{background:var(--color-title-bar);border-bottom:1px solid var(--color-border);align-items:center;gap:12px;padding:0 12px;display:flex}.workspace-meta{color:var(--color-muted);font-size:12px;font-family:var(--font-mono);text-overflow:ellipsis;white-space:nowrap;text-align:center;flex:1;overflow:hidden}.workspace-editor-grid{grid-template-rows:minmax(0,1fr);grid-template-columns:48px 220px minmax(0,1fr);height:100%;min-height:0;display:grid}.sidebar-panel{background:var(--color-sidebar);border-right:1px solid var(--color-border);flex-direction:column;height:100%;min-height:0;display:flex}.sidebar-content{flex:1;min-height:0;overflow:auto}.workspace-editor-grid>.activity-bar{background:var(--color-activitybar);border-right:1px solid var(--color-border);grid-row:unset;flex-direction:column;width:48px;display:flex}.workspace-editor-grid .activity-bar-item{color:var(--color-activitybar-fg);opacity:.6;transition:opacity .15s}.workspace-editor-grid .activity-bar-item:hover,.workspace-editor-grid .activity-bar-item.active{opacity:1}.panel{background:var(--color-panel);border:1px solid var(--color-border);border-radius:0;flex-direction:column;min-height:0;padding:0;display:flex}.panel-header{border-bottom:1px solid var(--color-border);background:var(--color-sidebar);justify-content:space-between;align-items:center;gap:8px;padding:8px 12px;display:flex}.panel-title{text-transform:uppercase;letter-spacing:.5px;color:var(--color-ink-muted);font-size:11px;font-weight:600}.panel-subtitle{color:var(--color-muted);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.panel-body{flex:1;min-height:0;overflow:auto}.modal{background:var(--color-panel);border:1px solid var(--color-border);border-radius:4px;width:480px;max-width:96vw;max-height:90dvh;padding:16px;overflow-y:auto;box-shadow:0 8px 32px #0000004d}.modal-close-btn{color:var(--color-muted);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;width:32px;height:32px;padding:0;font-size:24px;line-height:1;display:flex}.modal-close-btn:hover{color:var(--color-ink);background:var(--color-list-hover)}.tree-body{padding:4px 0}.tree-row{color:var(--color-ink);text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:4px;width:100%;min-height:22px;padding:2px 8px 2px 4px;font-size:13px;transition:background .1s;display:flex}.tree-row:hover{background:var(--color-list-hover)}.tree-row:focus-visible{outline:2px solid var(--color-focus);outline-offset:-2px}.tree-chevron{width:16px;height:16px;color:var(--color-ink-muted);flex-shrink:0;justify-content:center;align-items:center;display:flex}.tree-chevron-icon{width:12px;height:12px;transition:transform .1s}.tree-icon{flex-shrink:0;justify-content:center;align-items:center;width:16px;height:16px;display:flex}.tree-svg{width:16px;height:16px}.tree-label{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.tree-meta{color:var(--color-muted);font-size:11px;font-family:var(--font-mono);margin-left:auto}.tree-state{color:var(--color-muted);padding:8px 12px;font-size:12px}.tree-actions{align-items:center;gap:4px;display:flex}.tree-input-row{align-items:center;gap:4px;min-height:22px;padding:2px 8px 2px 4px;display:flex}.tree-input{border:1px solid var(--color-accent);background:var(--color-panel);min-width:0;color:var(--color-ink);border-radius:2px;outline:none;flex:1;padding:2px 6px;font-size:13px}.tree-input::placeholder{color:var(--color-muted)}.editor-container{background:var(--color-panel);flex-direction:column;height:100%;display:flex;overflow:hidden}.editor-container.editor-empty{justify-content:center;align-items:center;display:flex}.editor-welcome{color:var(--color-ink-muted);text-align:center;flex-direction:column;align-items:center;gap:16px;padding:60px 40px;display:flex}.editor-welcome-icon{opacity:.2;width:80px;height:80px}.editor-welcome-icon svg{width:100%;height:100%;stroke:var(--color-ink)}.editor-welcome-text{color:var(--color-ink-muted);font-size:18px;font-weight:400}.editor-welcome-hint{opacity:.6;font-size:13px}.editor-tabs{background:var(--color-bg-soft);border-bottom:1px solid var(--color-border);flex-shrink:0;align-items:stretch;min-height:35px;display:flex}.editor-tabs-list{scrollbar-width:none;display:flex;overflow-x:auto}.editor-tabs-list::-webkit-scrollbar{display:none}.editor-tab{background:var(--color-bg-soft);border:none;border-right:1px solid var(--color-border);min-width:100px;max-width:180px;height:35px;color:var(--color-ink-muted);cursor:pointer;align-items:center;gap:6px;padding:0 10px;font-size:13px;transition:background .1s,color .1s;display:flex;position:relative}.editor-tab:hover{background:var(--color-panel);color:var(--color-ink)}.editor-tab.active{background:var(--color-panel);color:var(--color-ink);border-bottom:1px solid var(--color-panel);margin-bottom:-1px}.editor-tab-icon{font-size:10px;font-weight:600;font-family:var(--font-mono);flex-shrink:0}.editor-tab-name{text-overflow:ellipsis;white-space:nowrap;text-align:left;flex:1;overflow:hidden}.editor-tab-dirty{color:var(--color-ink);flex-shrink:0;font-size:18px;line-height:1}.editor-tab-saving{flex-shrink:0;width:14px;height:14px}.editor-tab-saving svg{width:100%;height:100%;stroke:var(--color-ink-muted)}.editor-tab-close{width:20px;height:20px;color:var(--color-ink-muted);cursor:pointer;opacity:0;background:0 0;border:none;border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:opacity .1s,background .1s;display:flex}.editor-tab-close svg{width:16px;height:16px}.editor-tab-close:hover{background:var(--color-list-hover);color:var(--color-ink)}.editor-breadcrumb{background:var(--color-panel);border-bottom:1px solid var(--color-border);color:var(--color-ink-muted);flex-shrink:0;align-items:center;height:22px;padding:0 12px;font-size:12px;display:flex}.editor-breadcrumb-path{text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);overflow:hidden}.editor-content{flex:1;min-height:0;position:relative}.editor-no-file{height:100%;color:var(--color-ink-muted);justify-content:center;align-items:center;display:flex}.editor-statusbar{background:var(--color-statusbar);height:22px;color:var(--color-statusbar-fg);flex-shrink:0;justify-content:space-between;align-items:center;padding:0 12px;font-size:12px;display:flex}.editor-statusbar-left,.editor-statusbar-right{align-items:center;gap:14px;display:flex}.editor-status-item{cursor:default;align-items:center;gap:4px;display:flex}.editor-status-item svg{width:14px;height:14px}.terminal-layout{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex;overflow:hidden}.terminal-topbar{background:var(--color-sidebar);border-bottom:1px solid var(--color-border);flex-shrink:0;align-items:stretch;min-width:0;display:flex;overflow:hidden}.topbar-left{flex:1;align-items:stretch;min-width:0;display:flex;overflow:hidden}.deck-tabs{scrollbar-width:none;flex:1;align-items:stretch;min-width:0;display:flex;overflow-x:auto}.deck-tabs::-webkit-scrollbar{display:none}.deck-tab{border:none;border-top:2px solid #0000;border-right:1px solid var(--color-border);min-width:80px;max-width:200px;color:var(--color-muted);cursor:pointer;white-space:nowrap;background:0 0;flex-shrink:0;align-items:center;gap:4px;padding:0 8px 0 12px;font-size:12px;font-weight:500;transition:background .12s,color .12s;display:flex}.deck-tab[draggable=true]{cursor:grab}.deck-tab.is-dragging{opacity:.5}.deck-tab.is-drag-over{background:var(--color-list-active);color:var(--color-ink)}.deck-tab:hover{background:var(--color-list-hover);color:var(--color-ink)}.deck-tab.active{background:var(--color-panel);color:var(--color-ink);border-top-color:var(--color-accent)}.deck-tab-name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.deck-tab-close{opacity:0;width:16px;height:16px;color:var(--color-muted);border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;font-size:13px;line-height:1;transition:opacity .12s,background .12s;display:flex}.deck-tab-add{border-top:none;justify-content:center;min-width:35px;max-width:35px;padding:0;font-size:16px;font-weight:300}.deck-tab-add:hover{background:var(--color-list-hover);color:var(--color-ink)}.terminal-split-container{flex:1;gap:4px;width:100%;min-width:0;max-width:100%;min-height:0;padding:4px;display:grid;position:relative}.deck-split-pane{border:1px solid var(--color-border);border-radius:4px;flex-direction:column;min-width:0;min-height:0;display:flex;overflow:hidden}.topbar-btn-sm{border:1px solid var(--color-border);color:var(--color-muted);cursor:pointer;background:0 0;border-radius:3px;padding:2px 6px;font-size:11px;font-weight:600;transition:all .15s}.topbar-btn-sm:hover{background:var(--color-list-hover);color:var(--color-ink);border-color:var(--color-accent)}.topbar-btn-sm.topbar-btn-claude{color:#d97757}.topbar-btn-sm.topbar-btn-claude:hover{background:#d9775726;border-color:#d97757}.topbar-btn-sm.topbar-btn-codex{color:#00a86b}.topbar-btn-sm.topbar-btn-codex:hover{background:#00a86b26;border-color:#00a86b}.terminal-pane{flex-direction:column;flex:1;min-width:0;min-height:0;padding:4px;display:flex}.terminal-grid{flex:1;gap:4px;width:100%;min-width:0;min-height:0;display:grid;overflow:hidden}.terminal-empty{background:var(--color-bg-soft);border:1px dashed var(--color-border);color:var(--color-muted);border-radius:4px;flex:1;justify-content:center;align-items:center;font-size:12px;display:flex}.terminal-tile{border:1px solid var(--color-border);color:#d4d4d4;background:#1e1e1e;border-radius:4px;flex-direction:column;min-width:0;min-height:0;display:flex;overflow:hidden}.terminal-tile-header{background:#2d2d2d;border-bottom:1px solid #404040;justify-content:space-between;align-items:center;gap:8px;padding:6px 10px;font-size:12px;display:flex}.terminal-tile-header span{text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);flex:1;overflow:hidden}.terminal-close-btn{color:#858585;cursor:pointer;background:0 0;border:none;border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;width:20px;height:20px;transition:background .1s,color .1s;display:flex}.terminal-close-btn:hover{color:#fff;background:#404040}.terminal-tile-body{flex:1;min-height:0}.git-modified{color:var(--color-git-modified)}.git-untracked{color:var(--color-git-untracked)}.git-deleted{color:var(--color-git-deleted)}.git-staged{color:var(--color-git-staged)}.git-renamed{color:var(--color-git-renamed)}.git-conflicted{color:var(--color-git-conflicted)}.git-tabs{border-bottom:1px solid var(--color-border);background:var(--color-sidebar);flex-shrink:0;display:flex}.git-tab{color:var(--color-ink-muted);cursor:pointer;background:0 0;border:none;align-items:center;gap:6px;padding:6px 12px;font-size:12px;transition:color .1s,background .1s;display:flex;position:relative}.git-tab:hover{color:var(--color-ink);background:var(--color-list-hover)}.git-tab.active{color:var(--color-ink)}.git-file-row{align-items:center;gap:4px;transition:background .1s;display:flex}.git-file-row:hover{background:var(--color-list-hover)}.git-file-main{color:var(--color-ink);cursor:pointer;text-align:left;background:0 0;border:none;flex:1;align-items:center;gap:6px;min-width:0;min-height:22px;padding:4px 12px;display:flex}.git-status-badge{width:16px;height:16px;font-size:11px;font-weight:600;font-family:var(--font-mono);flex-shrink:0;justify-content:center;align-items:center;display:flex}.git-file-path{text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.git-file-actions{opacity:0;gap:2px;padding-right:8px;transition:opacity .1s;display:flex}.git-action-btn{width:22px;height:22px;color:var(--color-ink-muted);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;padding:0;transition:background .1s,color .1s;display:flex}.git-action-btn:hover{background:var(--color-list-hover);color:var(--color-ink)}.git-action-btn svg{fill:currentColor;width:14px;height:14px}.status-float{right:20px;bottom:calc(40px + env(safe-area-inset-bottom,0px));background:var(--color-panel);border:1px solid var(--color-border);color:var(--color-ink);border-radius:4px;max-width:min(400px,100vw - 40px);padding:8px 14px;font-size:12px;position:fixed;box-shadow:0 4px 16px #0003}.sidebar-toggle-btn,.sidebar-overlay{display:none}@media (max-width:1024px){.workspace-editor-grid{grid-template-rows:200px minmax(0,1fr);grid-template-columns:48px 1fr}.workspace-editor-grid>.activity-bar{grid-row:1/-1}.sidebar-panel{border-right:none;border-bottom:1px solid var(--color-border)}}@media (max-width:720px){.app{padding-bottom:env(safe-area-inset-bottom);grid-template-rows:minmax(0,1fr) 48px 22px;grid-template-columns:1fr}.app>nav.activity-bar{width:auto;height:48px;padding:0 max(8px,env(safe-area-inset-right,0px)) 0 max(8px,env(safe-area-inset-left,0px));border-top:1px solid var(--color-border);flex-direction:row;grid-row:2;justify-content:space-between;gap:8px}.activity-bar-top,.activity-bar-bottom{flex-direction:row;align-items:center;gap:4px}.activity-bar-bottom{margin-top:0;margin-left:0}.main{grid-row:1;min-width:0;overflow:hidden}.sidebar-toggle-btn{justify-content:center;align-items:center;display:flex}.activity-bar-item{min-width:44px;min-height:44px}.deck-tab{min-height:40px;padding:0 8px 0 14px;font-size:13px}.topbar-btn-sm{min-width:44px;min-height:40px;padding:0 10px}.tree-row,.editor-tab{min-height:36px}.modal-close-btn{min-width:44px;min-height:44px}.modal{border-radius:8px;max-width:calc(100vw - 16px);max-height:90dvh;margin:8px}.deck-tabs{-webkit-overflow-scrolling:touch;scrollbar-width:none;flex-wrap:nowrap;overflow-x:auto}.deck-tabs::-webkit-scrollbar{display:none}.terminal-split-container{scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain;touch-action:pan-x;scrollbar-width:none;width:100%;max-width:100%;overflow:scroll hidden;flex-direction:row!important;gap:0!important;padding:0!important;display:flex!important}.terminal-split-container::-webkit-scrollbar{display:none}.deck-split-pane{scroll-snap-align:start;border-left:none;border-right:none;border-radius:0;flex:0 0 100%;width:100%;max-width:100%;overflow:hidden}.terminal-pane{scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;overscroll-behavior-y:contain;touch-action:pan-y;scrollbar-width:none;width:100%;max-width:100%;height:100%;overflow:hidden scroll;padding:0!important}.terminal-pane::-webkit-scrollbar{display:none}.terminal-grid{width:100%;height:100%;grid-template-columns:unset!important;grid-template-rows:unset!important;flex-direction:column!important;flex:none!important;gap:0!important;display:flex!important;overflow:visible!important}.terminal-tile{scroll-snap-align:start;border-left:none;border-right:none;border-radius:0;flex:none;width:100%;max-width:100%;height:100%}.status-float{right:12px;bottom:calc(78px + env(safe-area-inset-bottom,0px));max-width:calc(100vw - 24px)}.workspace-editor-grid{grid-template-rows:minmax(0,1fr);grid-template-columns:1fr;position:relative}.workspace-editor-grid>.activity-bar{z-index:101;border-right:1px solid var(--color-border);border-top:none;flex-direction:column;width:48px;height:auto;transition:transform .2s;position:absolute;top:0;bottom:0;left:0;transform:translate(-100%)}.sidebar-panel{z-index:100;border-bottom:none;border-right:1px solid var(--color-border);width:260px;transition:transform .2s;position:absolute;top:0;bottom:0;left:48px;overflow-y:auto;transform:translate(calc(-100% - 48px))}.drawer-open .workspace-editor-grid>.activity-bar,.drawer-open .sidebar-panel{transform:translate(0)}.sidebar-overlay{z-index:99;background:#0006;display:none;position:absolute;top:0;right:0;bottom:0;left:0}.drawer-open .sidebar-overlay{display:block}.workspace-editor-header{gap:8px;padding:0 8px}.workspace-editor-header .sidebar-toggle-btn{order:3;margin-left:auto}.workspace-editor-header .workspace-meta{text-align:left;min-width:0}}@media (pointer:coarse){.modal-close-btn:hover,.tree-row:hover,.editor-tab:hover,.deck-tab:hover,.deck-tab-add:hover,.topbar-btn-sm:hover,.terminal-close-btn:hover,.git-file-row:hover,.git-action-btn:hover,.git-tab:hover{border-color:inherit;color:inherit;opacity:1;background:0 0}.editor-tab:hover{color:var(--color-ink-muted);background:0 0}.git-file-actions{opacity:1}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|