deckide 3.5.14 → 3.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/routes/decks.js +22 -1
- package/dist/routes/terminals.js +89 -26
- package/dist/server.js +1 -1
- package/dist/websocket.js +180 -29
- package/package.json +1 -1
- package/web/dist/assets/index-Bw4TZ5S5.css +32 -0
- package/web/dist/assets/index-Dqd8LdlD.js +122 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-B72bb78r.css +0 -32
- package/web/dist/assets/index-BRKvvC_T.js +0 -129
package/dist/routes/decks.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { createHttpError, handleError, readJson } from '../utils/error.js';
|
|
4
4
|
import { requireWorkspace } from './workspaces.js';
|
|
5
|
-
export function createDeckRouter(db, workspaces, decks) {
|
|
5
|
+
export function createDeckRouter(db, workspaces, decks, terminals) {
|
|
6
6
|
const router = new Hono();
|
|
7
7
|
const insertDeck = db.prepare('INSERT INTO decks (id, name, root, workspace_id, created_at) VALUES (?, ?, ?, ?, ?)');
|
|
8
8
|
function createDeck(name, workspaceId) {
|
|
@@ -76,12 +76,33 @@ export function createDeckRouter(db, workspaces, decks) {
|
|
|
76
76
|
});
|
|
77
77
|
const deleteDeckStmt = db.prepare('DELETE FROM decks WHERE id = ?');
|
|
78
78
|
const deleteTerminalsByDeckStmt = db.prepare('DELETE FROM terminals WHERE deck_id = ?');
|
|
79
|
+
function closeDeckTerminalSockets(sockets, reason) {
|
|
80
|
+
sockets.forEach((socket) => {
|
|
81
|
+
try {
|
|
82
|
+
socket.close(1000, reason);
|
|
83
|
+
}
|
|
84
|
+
catch { /* ignore */ }
|
|
85
|
+
});
|
|
86
|
+
sockets.clear();
|
|
87
|
+
}
|
|
79
88
|
router.delete('/:id', (c) => {
|
|
80
89
|
try {
|
|
81
90
|
const deckId = c.req.param('id');
|
|
82
91
|
if (!decks.has(deckId)) {
|
|
83
92
|
throw createHttpError('Deck not found', 404);
|
|
84
93
|
}
|
|
94
|
+
const deckTerminalIds = Array.from(terminals.values())
|
|
95
|
+
.filter((session) => session.deckId === deckId)
|
|
96
|
+
.map((session) => session.id);
|
|
97
|
+
deckTerminalIds.forEach((terminalId) => {
|
|
98
|
+
const session = terminals.get(terminalId);
|
|
99
|
+
if (!session)
|
|
100
|
+
return;
|
|
101
|
+
terminals.delete(terminalId);
|
|
102
|
+
session.resizeOwner = null;
|
|
103
|
+
closeDeckTerminalSockets(session.sockets, 'Deck deleted');
|
|
104
|
+
session.kill();
|
|
105
|
+
});
|
|
85
106
|
deleteTerminalsByDeckStmt.run(deckId);
|
|
86
107
|
deleteDeckStmt.run(deckId);
|
|
87
108
|
decks.delete(deckId);
|
package/dist/routes/terminals.js
CHANGED
|
@@ -5,43 +5,99 @@ import { TERMINAL_BUFFER_LIMIT } from '../config.js';
|
|
|
5
5
|
import { createHttpError, handleError, readJson } from '../utils/error.js';
|
|
6
6
|
import { getDefaultShell } from '../utils/shell.js';
|
|
7
7
|
import { saveTerminal, deleteTerminal as deleteTerminalFromDb } from '../utils/database.js';
|
|
8
|
-
|
|
9
|
-
const
|
|
8
|
+
const DEFAULT_TERMINAL_TITLE = 'ターミナル';
|
|
9
|
+
const MAX_SOCKET_BUFFERED_AMOUNT = 1024 * 1024;
|
|
10
10
|
export function createTerminalRouter(db, decks, terminals) {
|
|
11
11
|
const router = new Hono();
|
|
12
|
+
function toBuffer(data) {
|
|
13
|
+
return Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
14
|
+
}
|
|
12
15
|
function appendToTerminalBuffer(session, data) {
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
session.buffer = newBuffer.slice(excess);
|
|
17
|
-
session.bufferBase += excess;
|
|
16
|
+
const chunk = toBuffer(data);
|
|
17
|
+
if (chunk.length === 0) {
|
|
18
|
+
return;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
if (chunk.length >= TERMINAL_BUFFER_LIMIT) {
|
|
21
|
+
const retainedChunk = Buffer.from(chunk.subarray(chunk.length - TERMINAL_BUFFER_LIMIT));
|
|
22
|
+
session.bufferBase += session.bufferLength + (chunk.length - TERMINAL_BUFFER_LIMIT);
|
|
23
|
+
session.bufferChunks = [retainedChunk];
|
|
24
|
+
session.bufferLength = retainedChunk.length;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
session.bufferChunks.push(Buffer.from(chunk));
|
|
28
|
+
session.bufferLength += chunk.length;
|
|
29
|
+
while (session.bufferLength > TERMINAL_BUFFER_LIMIT && session.bufferChunks.length > 0) {
|
|
30
|
+
const overflow = session.bufferLength - TERMINAL_BUFFER_LIMIT;
|
|
31
|
+
const firstChunk = session.bufferChunks[0];
|
|
32
|
+
if (firstChunk.length <= overflow) {
|
|
33
|
+
session.bufferChunks.shift();
|
|
34
|
+
session.bufferBase += firstChunk.length;
|
|
35
|
+
session.bufferLength -= firstChunk.length;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
session.bufferChunks[0] = Buffer.from(firstChunk.subarray(overflow));
|
|
39
|
+
session.bufferBase += overflow;
|
|
40
|
+
session.bufferLength -= overflow;
|
|
21
41
|
}
|
|
22
42
|
}
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
function getUniqueTerminalTitle(deckId, requestedTitle) {
|
|
44
|
+
const trimmedTitle = requestedTitle?.trim();
|
|
45
|
+
const baseTitle = trimmedTitle && trimmedTitle.length > 0 ? trimmedTitle : null;
|
|
46
|
+
const existingTitles = new Set(Array.from(terminals.values())
|
|
47
|
+
.filter((session) => session.deckId === deckId)
|
|
48
|
+
.map((session) => session.title));
|
|
49
|
+
if (!baseTitle) {
|
|
50
|
+
let index = 1;
|
|
51
|
+
while (existingTitles.has(`${DEFAULT_TERMINAL_TITLE} ${index}`)) {
|
|
52
|
+
index++;
|
|
53
|
+
}
|
|
54
|
+
return `${DEFAULT_TERMINAL_TITLE} ${index}`;
|
|
55
|
+
}
|
|
56
|
+
if (!existingTitles.has(baseTitle)) {
|
|
57
|
+
return baseTitle;
|
|
58
|
+
}
|
|
59
|
+
let suffix = 2;
|
|
60
|
+
while (existingTitles.has(`${baseTitle} ${suffix}`)) {
|
|
61
|
+
suffix++;
|
|
62
|
+
}
|
|
63
|
+
return `${baseTitle} ${suffix}`;
|
|
28
64
|
}
|
|
29
65
|
function broadcastToSockets(session, data) {
|
|
66
|
+
const payload = toBuffer(data);
|
|
30
67
|
const deadSockets = new Set();
|
|
31
68
|
session.sockets.forEach((socket) => {
|
|
32
69
|
try {
|
|
33
|
-
if (socket.readyState
|
|
34
|
-
|
|
70
|
+
if (socket.readyState !== 1) {
|
|
71
|
+
deadSockets.add(socket);
|
|
72
|
+
return;
|
|
35
73
|
}
|
|
36
|
-
|
|
74
|
+
if (socket.bufferedAmount > MAX_SOCKET_BUFFERED_AMOUNT) {
|
|
75
|
+
try {
|
|
76
|
+
socket.close(1009, 'Terminal output overflow');
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore */ }
|
|
37
79
|
deadSockets.add(socket);
|
|
80
|
+
return;
|
|
38
81
|
}
|
|
82
|
+
socket.send(payload, { binary: true }, (error) => {
|
|
83
|
+
if (error) {
|
|
84
|
+
try {
|
|
85
|
+
socket.close(1011, 'Terminal stream error');
|
|
86
|
+
}
|
|
87
|
+
catch { /* ignore */ }
|
|
88
|
+
}
|
|
89
|
+
});
|
|
39
90
|
}
|
|
40
91
|
catch {
|
|
41
92
|
deadSockets.add(socket);
|
|
42
93
|
}
|
|
43
94
|
});
|
|
44
|
-
deadSockets.forEach((s) =>
|
|
95
|
+
deadSockets.forEach((s) => {
|
|
96
|
+
session.sockets.delete(s);
|
|
97
|
+
if (session.resizeOwner === s) {
|
|
98
|
+
session.resizeOwner = null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
45
101
|
}
|
|
46
102
|
function handleTerminalExit(id) {
|
|
47
103
|
const session = terminals.get(id);
|
|
@@ -57,6 +113,7 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
57
113
|
catch { /* ignore */ }
|
|
58
114
|
});
|
|
59
115
|
session.sockets.clear();
|
|
116
|
+
session.resizeOwner = null;
|
|
60
117
|
}
|
|
61
118
|
function createTerminalSession(deck, title, command) {
|
|
62
119
|
const id = crypto.randomUUID();
|
|
@@ -106,11 +163,11 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
106
163
|
cols: 120,
|
|
107
164
|
rows: 32,
|
|
108
165
|
env,
|
|
109
|
-
encoding:
|
|
166
|
+
encoding: null,
|
|
110
167
|
...(isWindows ? { useConpty: true } : {}),
|
|
111
168
|
});
|
|
112
169
|
console.log(`[TERMINAL] Created terminal ${id}: shell=${shell}, cwd=${deck.root}, pid=${term.pid}`);
|
|
113
|
-
const resolvedTitle =
|
|
170
|
+
const resolvedTitle = getUniqueTerminalTitle(deck.id, title);
|
|
114
171
|
const createdAt = new Date().toISOString();
|
|
115
172
|
const session = {
|
|
116
173
|
id,
|
|
@@ -119,13 +176,18 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
119
176
|
command: command || null,
|
|
120
177
|
createdAt,
|
|
121
178
|
sockets: new Set(),
|
|
122
|
-
|
|
179
|
+
resizeOwner: null,
|
|
180
|
+
bufferChunks: [],
|
|
181
|
+
bufferLength: 0,
|
|
123
182
|
bufferBase: 0,
|
|
124
183
|
lastActive: Date.now(),
|
|
125
|
-
write: (data) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
184
|
+
write: (data) => {
|
|
185
|
+
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
186
|
+
try {
|
|
187
|
+
term.write(payload);
|
|
188
|
+
}
|
|
189
|
+
catch { /* terminal may be dying */ }
|
|
190
|
+
},
|
|
129
191
|
resize: (cols, rows) => { try {
|
|
130
192
|
term.resize(cols, rows);
|
|
131
193
|
}
|
|
@@ -136,7 +198,7 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
136
198
|
catch { /* already dead */ } },
|
|
137
199
|
};
|
|
138
200
|
// Wire up PTY output → buffer + WebSocket broadcast
|
|
139
|
-
term.
|
|
201
|
+
term.on('data', (data) => {
|
|
140
202
|
appendToTerminalBuffer(session, data);
|
|
141
203
|
session.lastActive = Date.now();
|
|
142
204
|
broadcastToSockets(session, data);
|
|
@@ -193,6 +255,7 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
193
255
|
catch { /* ignore */ }
|
|
194
256
|
});
|
|
195
257
|
session.sockets.clear();
|
|
258
|
+
session.resizeOwner = null;
|
|
196
259
|
session.kill();
|
|
197
260
|
return c.body(null, 204);
|
|
198
261
|
}
|
package/dist/server.js
CHANGED
|
@@ -65,7 +65,7 @@ export async function createServer() {
|
|
|
65
65
|
// Mount routers
|
|
66
66
|
app.route('/api/settings', createSettingsRouter());
|
|
67
67
|
app.route('/api/workspaces', createWorkspaceRouter(db, workspaces, workspacePathIndex));
|
|
68
|
-
app.route('/api/decks', createDeckRouter(db, workspaces, decks));
|
|
68
|
+
app.route('/api/decks', createDeckRouter(db, workspaces, decks, terminals));
|
|
69
69
|
const terminalRouter = createTerminalRouter(db, decks, terminals);
|
|
70
70
|
app.route('/api/terminals', terminalRouter);
|
|
71
71
|
app.route('/api/git', createGitRouter(workspaces));
|
package/dist/websocket.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
3
|
import { PORT, TRUST_PROXY, CORS_ORIGIN, NODE_ENV } from './config.js';
|
|
4
4
|
import { logSecurityEvent } from './middleware/security.js';
|
|
5
5
|
import { verifyWebSocketAuth } from './middleware/auth.js';
|
|
6
6
|
const MIN_TERMINAL_SIZE = 1;
|
|
7
7
|
const MAX_TERMINAL_SIZE = 500;
|
|
8
8
|
const MAX_MESSAGE_SIZE = 64 * 1024; // 64KB max message size
|
|
9
|
+
const MAX_SOCKET_BUFFERED_AMOUNT = 1024 * 1024;
|
|
10
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
9
11
|
// Configurable connection limit per IP (default: 1000)
|
|
10
12
|
let maxConnectionsPerIP = 1000;
|
|
11
13
|
// Track connections per IP
|
|
@@ -84,8 +86,112 @@ function validateTerminalSize(value) {
|
|
|
84
86
|
}
|
|
85
87
|
return intValue;
|
|
86
88
|
}
|
|
89
|
+
function rawDataByteLength(data) {
|
|
90
|
+
if (typeof data === 'string') {
|
|
91
|
+
return Buffer.byteLength(data, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(data)) {
|
|
94
|
+
return data.reduce((total, chunk) => total + chunk.length, 0);
|
|
95
|
+
}
|
|
96
|
+
if (data instanceof ArrayBuffer) {
|
|
97
|
+
return data.byteLength;
|
|
98
|
+
}
|
|
99
|
+
return data.length;
|
|
100
|
+
}
|
|
101
|
+
function rawDataToBuffer(data) {
|
|
102
|
+
if (typeof data === 'string') {
|
|
103
|
+
return Buffer.from(data, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
if (Array.isArray(data)) {
|
|
106
|
+
return Buffer.concat(data);
|
|
107
|
+
}
|
|
108
|
+
if (data instanceof ArrayBuffer) {
|
|
109
|
+
return Buffer.from(data);
|
|
110
|
+
}
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
function canSendToSocket(socket) {
|
|
114
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
if (socket.bufferedAmount > MAX_SOCKET_BUFFERED_AMOUNT) {
|
|
118
|
+
try {
|
|
119
|
+
socket.close(1009, 'Terminal output overflow');
|
|
120
|
+
}
|
|
121
|
+
catch { /* ignore */ }
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
function sendControl(socket, message) {
|
|
127
|
+
if (!canSendToSocket(socket)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
socket.send(JSON.stringify(message));
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
try {
|
|
136
|
+
socket.close(1011, 'Terminal control send failed');
|
|
137
|
+
}
|
|
138
|
+
catch { /* ignore */ }
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function readBufferedRange(session, startOffset, endOffset) {
|
|
143
|
+
const relativeStart = Math.max(0, startOffset - session.bufferBase);
|
|
144
|
+
const relativeEnd = Math.max(relativeStart, Math.min(session.bufferLength, endOffset - session.bufferBase));
|
|
145
|
+
const totalLength = relativeEnd - relativeStart;
|
|
146
|
+
if (totalLength <= 0 || session.bufferChunks.length === 0) {
|
|
147
|
+
return Buffer.alloc(0);
|
|
148
|
+
}
|
|
149
|
+
if (session.bufferChunks.length === 1) {
|
|
150
|
+
return session.bufferChunks[0].subarray(relativeStart, relativeEnd);
|
|
151
|
+
}
|
|
152
|
+
const slices = [];
|
|
153
|
+
let traversed = 0;
|
|
154
|
+
for (const chunk of session.bufferChunks) {
|
|
155
|
+
const chunkStart = traversed;
|
|
156
|
+
const chunkEnd = traversed + chunk.length;
|
|
157
|
+
traversed = chunkEnd;
|
|
158
|
+
if (chunkEnd <= relativeStart) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (chunkStart >= relativeEnd) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const startInChunk = Math.max(0, relativeStart - chunkStart);
|
|
165
|
+
const endInChunk = Math.min(chunk.length, relativeEnd - chunkStart);
|
|
166
|
+
slices.push(chunk.subarray(startInChunk, endInChunk));
|
|
167
|
+
}
|
|
168
|
+
return slices.length === 1 ? slices[0] : Buffer.concat(slices, totalLength);
|
|
169
|
+
}
|
|
87
170
|
export function setupWebSocketServer(server, terminals) {
|
|
88
|
-
const wss = new WebSocketServer({ server });
|
|
171
|
+
const wss = new WebSocketServer({ server, maxPayload: MAX_MESSAGE_SIZE });
|
|
172
|
+
const heartbeatState = new WeakMap();
|
|
173
|
+
const heartbeatInterval = setInterval(() => {
|
|
174
|
+
for (const socket of wss.clients) {
|
|
175
|
+
if (heartbeatState.get(socket) === false) {
|
|
176
|
+
try {
|
|
177
|
+
socket.terminate();
|
|
178
|
+
}
|
|
179
|
+
catch { /* ignore */ }
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
heartbeatState.set(socket, false);
|
|
183
|
+
try {
|
|
184
|
+
socket.ping();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
try {
|
|
188
|
+
socket.terminate();
|
|
189
|
+
}
|
|
190
|
+
catch { /* ignore */ }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
194
|
+
heartbeatInterval.unref?.();
|
|
89
195
|
const WS_ALLOWED_ORIGINS = new Set([
|
|
90
196
|
`http://localhost:${PORT}`,
|
|
91
197
|
]);
|
|
@@ -101,6 +207,7 @@ export function setupWebSocketServer(server, terminals) {
|
|
|
101
207
|
wss.on('connection', (socket, req) => {
|
|
102
208
|
const socketId = crypto.randomUUID();
|
|
103
209
|
const clientIP = getClientIP(req);
|
|
210
|
+
heartbeatState.set(socket, true);
|
|
104
211
|
// Validate Origin header to prevent Cross-Site WebSocket Hijacking
|
|
105
212
|
// Skip check if CORS_ORIGIN is '*' or unset in development mode
|
|
106
213
|
const skipOriginCheck = CORS_ORIGIN === '*' || (!CORS_ORIGIN && NODE_ENV !== 'production');
|
|
@@ -120,6 +227,9 @@ export function setupWebSocketServer(server, terminals) {
|
|
|
120
227
|
// Socket might already be closed
|
|
121
228
|
}
|
|
122
229
|
});
|
|
230
|
+
socket.on('pong', () => {
|
|
231
|
+
heartbeatState.set(socket, true);
|
|
232
|
+
});
|
|
123
233
|
// Check connection limit per IP
|
|
124
234
|
if (!trackConnection(clientIP, socket)) {
|
|
125
235
|
logSecurityEvent('WS_CONNECTION_LIMIT_EXCEEDED', { ip: clientIP });
|
|
@@ -146,67 +256,101 @@ export function setupWebSocketServer(server, terminals) {
|
|
|
146
256
|
socket.close(1000, 'Terminal not found');
|
|
147
257
|
return;
|
|
148
258
|
}
|
|
259
|
+
const hadNoSockets = session.sockets.size === 0;
|
|
149
260
|
session.sockets.add(socket);
|
|
261
|
+
if (hadNoSockets && !session.resizeOwner) {
|
|
262
|
+
session.resizeOwner = socket;
|
|
263
|
+
}
|
|
150
264
|
session.lastActive = Date.now();
|
|
151
265
|
// Send buffer content if available
|
|
152
|
-
// bufferOffset: absolute
|
|
153
|
-
// bufferBase: absolute position of buffer[0] (
|
|
266
|
+
// bufferOffset: absolute byte count the client already processed
|
|
267
|
+
// bufferBase: absolute byte position of buffer[0] (bytes dropped from start)
|
|
154
268
|
const offsetParam = url.searchParams.get('bufferOffset');
|
|
155
269
|
const clientOffset = offsetParam ? Math.max(0, parseInt(offsetParam, 10) || 0) : 0;
|
|
156
|
-
|
|
270
|
+
const bufferStart = session.bufferBase;
|
|
271
|
+
const bufferEnd = session.bufferBase + session.bufferLength;
|
|
272
|
+
let replayStartOffset = clientOffset;
|
|
273
|
+
let resetTerminal = false;
|
|
274
|
+
if (clientOffset <= bufferStart) {
|
|
275
|
+
replayStartOffset = bufferStart;
|
|
276
|
+
resetTerminal = session.bufferLength > 0;
|
|
277
|
+
}
|
|
278
|
+
else if (clientOffset >= bufferEnd) {
|
|
279
|
+
replayStartOffset = bufferEnd;
|
|
280
|
+
}
|
|
281
|
+
if (!sendControl(socket, { type: 'sync', offsetBase: replayStartOffset, reset: resetTerminal })) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (session.bufferLength > 0) {
|
|
157
285
|
try {
|
|
158
|
-
|
|
159
|
-
const bufferEnd = session.bufferBase + session.buffer.length;
|
|
160
|
-
let bufferToSend;
|
|
286
|
+
let bufferToSend = Buffer.alloc(0);
|
|
161
287
|
if (clientOffset <= bufferStart) {
|
|
162
288
|
// Client's last position is before (or at) what we have — send everything
|
|
163
|
-
bufferToSend = session
|
|
289
|
+
bufferToSend = readBufferedRange(session, bufferStart, bufferEnd);
|
|
164
290
|
}
|
|
165
291
|
else if (clientOffset >= bufferEnd) {
|
|
166
292
|
// Client is fully up to date
|
|
167
|
-
bufferToSend =
|
|
293
|
+
bufferToSend = Buffer.alloc(0);
|
|
168
294
|
}
|
|
169
295
|
else {
|
|
170
296
|
// Send only the delta
|
|
171
|
-
bufferToSend = session
|
|
297
|
+
bufferToSend = readBufferedRange(session, clientOffset, bufferEnd);
|
|
172
298
|
}
|
|
173
|
-
if (bufferToSend) {
|
|
174
|
-
socket.send(bufferToSend);
|
|
299
|
+
if (bufferToSend.length > 0 && canSendToSocket(socket)) {
|
|
300
|
+
socket.send(bufferToSend, { binary: true });
|
|
175
301
|
}
|
|
176
302
|
}
|
|
177
303
|
catch (error) {
|
|
178
304
|
console.error(`Failed to send buffer to socket ${socketId}:`, error);
|
|
179
305
|
}
|
|
180
306
|
}
|
|
307
|
+
sendControl(socket, { type: 'ready' });
|
|
181
308
|
socket.on('message', (data) => {
|
|
182
309
|
try {
|
|
183
|
-
|
|
184
|
-
const message = data.toString('utf8');
|
|
185
|
-
// Check message size
|
|
186
|
-
const messageSize = Buffer.byteLength(message, 'utf8');
|
|
310
|
+
const messageSize = rawDataByteLength(data);
|
|
187
311
|
if (messageSize > MAX_MESSAGE_SIZE) {
|
|
188
312
|
logSecurityEvent('WS_MESSAGE_TOO_LARGE', { ip: clientIP, size: messageSize });
|
|
189
|
-
socket.
|
|
313
|
+
socket.close(1009, 'Message too large');
|
|
190
314
|
return;
|
|
191
315
|
}
|
|
192
316
|
session.lastActive = Date.now();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
317
|
+
if (typeof data === 'string') {
|
|
318
|
+
let control = null;
|
|
319
|
+
try {
|
|
320
|
+
control = JSON.parse(data);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
control = null;
|
|
324
|
+
}
|
|
325
|
+
if (control?.type === 'claim') {
|
|
326
|
+
session.resizeOwner = socket;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (control?.type === 'resize') {
|
|
330
|
+
if (session.resizeOwner && session.resizeOwner !== socket) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
session.resizeOwner = socket;
|
|
334
|
+
const cols = validateTerminalSize(control.cols);
|
|
335
|
+
const rows = validateTerminalSize(control.rows);
|
|
336
|
+
try {
|
|
337
|
+
session.resize(cols, rows);
|
|
338
|
+
}
|
|
339
|
+
catch (resizeError) {
|
|
340
|
+
console.error(`Failed to resize terminal ${id}:`, resizeError);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
199
344
|
try {
|
|
200
|
-
session.
|
|
345
|
+
session.write(Buffer.from(data, 'utf8'));
|
|
201
346
|
}
|
|
202
|
-
catch (
|
|
203
|
-
console.error(`Failed to
|
|
347
|
+
catch (writeError) {
|
|
348
|
+
console.error(`Failed to write text input to terminal ${id}:`, writeError);
|
|
204
349
|
}
|
|
205
350
|
return;
|
|
206
351
|
}
|
|
207
352
|
try {
|
|
208
|
-
|
|
209
|
-
session.write(message);
|
|
353
|
+
session.write(rawDataToBuffer(data));
|
|
210
354
|
}
|
|
211
355
|
catch (writeError) {
|
|
212
356
|
console.error(`Failed to write to terminal ${id}:`, writeError);
|
|
@@ -218,12 +362,19 @@ export function setupWebSocketServer(server, terminals) {
|
|
|
218
362
|
});
|
|
219
363
|
socket.on('close', () => {
|
|
220
364
|
session.sockets.delete(socket);
|
|
365
|
+
if (session.resizeOwner === socket) {
|
|
366
|
+
session.resizeOwner = null;
|
|
367
|
+
}
|
|
221
368
|
session.lastActive = Date.now();
|
|
369
|
+
heartbeatState.delete(socket);
|
|
222
370
|
untrackConnection(clientIP, socket);
|
|
223
371
|
});
|
|
224
372
|
});
|
|
225
373
|
wss.on('error', (error) => {
|
|
226
374
|
console.error('WebSocket server error:', error);
|
|
227
375
|
});
|
|
376
|
+
wss.on('close', () => {
|
|
377
|
+
clearInterval(heartbeatInterval);
|
|
378
|
+
});
|
|
228
379
|
return wss;
|
|
229
380
|
}
|
package/package.json
CHANGED
|
@@ -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-1{margin-bottom:calc(var(--spacing) * 1)}.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-7{height:calc(var(--spacing) * 7)}.h-\[22px\]{height:22px}.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-7{width:calc(var(--spacing) * 7)}.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,.shrink-0{flex-shrink:0}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.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)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.\!overflow-hidden{overflow:hidden!important}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.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}.text-\[15px\]{font-size:15px}.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))}@media (hover:hover){.hover\:bg-\[rgba\(0\,0\,0\,0\.08\)\]:hover{background-color:#00000014}.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}@media (hover:hover){.dark\:hover\:bg-\[rgba\(255\,255\,255\,0\.08\)\]:where([data-theme=dark],[data-theme=dark] *):hover{background-color:#ffffff14}}.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:100%;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{overflow-anchor:none;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{overflow-anchor:none;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{overflow-anchor:none;flex:1;min-height:0;overflow:hidden}.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}
|