dsh-terminal-panel 1.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/client.js ADDED
@@ -0,0 +1,1315 @@
1
+ /**
2
+ * terminal-dock — Client half.
3
+ *
4
+ * Adds an in-page terminal panel: a `sidebar.panellist` icon addresses a `main`
5
+ * panel (the same mechanism the Conversation panel uses), where every open
6
+ * terminal is a tab and the active one renders a live screen. The
7
+ * `shell.overlay` dock in the left column is the management surface — the
8
+ * "new terminal" action plus the terminal list.
9
+ *
10
+ * The screen is rendered by the small VT engine below (no xterm.js is available
11
+ * to a plain-JS client module), fed by the PTY output the host streams over
12
+ * Server-Sent Events.
13
+ */
14
+ window.__ModuleLoader__.load({
15
+ id: 'dsh-terminal-panel',
16
+ factory(require) {
17
+ const React = require('react');
18
+ const h = React.createElement;
19
+
20
+ const NS = 'dsh-terminal-panel';
21
+ const API = '/system-terminals/api';
22
+ const PANEL_KEY = 'terminal-panel';
23
+ const POLL_MS = 4000;
24
+ const MAX_COLS = 400;
25
+ const MAX_ROWS = 120;
26
+
27
+ const DICTS = {
28
+ zh: {
29
+ title: '终端',
30
+ newTerminal: '新建终端',
31
+ empty: '还没有终端',
32
+ emptyHint: '点击“新建终端”在面板里打开一个终端',
33
+ running: '运行中',
34
+ exited: '已退出',
35
+ close: '关闭该终端',
36
+ remove: '从列表移除',
37
+ rename: '重命名',
38
+ renameHint: '双击标签重命名,或点 ✎',
39
+ selectMode: '选择文本(暂停键盘输入)',
40
+ inputMode: '返回键盘输入',
41
+ activeLabel: '活跃终端',
42
+ hostMissing: '宿主插件未响应,请刷新页面后重试',
43
+ },
44
+ en: {
45
+ title: 'Terminals',
46
+ newTerminal: 'New Terminal',
47
+ empty: 'No terminals yet',
48
+ emptyHint: 'Use “New Terminal” to open one in this panel',
49
+ running: 'Running',
50
+ exited: 'Exited',
51
+ close: 'Close this terminal',
52
+ remove: 'Remove from list',
53
+ rename: 'Rename',
54
+ renameHint: 'Double-click the tab to rename, or use ✎',
55
+ selectMode: 'Select text (keyboard paused)',
56
+ inputMode: 'Back to keyboard input',
57
+ activeLabel: 'Active terminals',
58
+ hostMissing: 'The host plugin did not respond: reload the page and try again',
59
+ },
60
+ };
61
+
62
+ const runtime = {
63
+ t: (key) => (DICTS.en[key] !== undefined ? DICTS.en[key] : key),
64
+ ctx: null,
65
+ locale: null,
66
+ scheme: 'dark',
67
+ };
68
+
69
+ /** Size the next terminal is allocated with, measured from the panel. */
70
+ let panelSize = { cols: 100, rows: 28 };
71
+
72
+ // ------------------------------------------------------------------ api
73
+
74
+ async function api(method, payload) {
75
+ const init = { method: 'GET', credentials: 'same-origin' };
76
+ if (payload !== undefined) {
77
+ init.method = 'POST';
78
+ init.headers = { 'content-type': 'application/json' };
79
+ init.body = JSON.stringify(payload);
80
+ }
81
+ let response;
82
+ try {
83
+ response = await fetch(API + '/' + method, init);
84
+ } catch (err) {
85
+ throw new Error(runtime.t('hostMissing'));
86
+ }
87
+ let parsed = null;
88
+ try {
89
+ parsed = await response.json();
90
+ } catch (err) {
91
+ parsed = null;
92
+ }
93
+ if (!parsed || parsed.ok !== true) {
94
+ const message = (parsed && parsed.error && parsed.error.message) || 'HTTP ' + response.status;
95
+ throw new Error(message);
96
+ }
97
+ return parsed.value;
98
+ }
99
+
100
+ function messageOf(error) {
101
+ return error && error.message ? error.message : String(error);
102
+ }
103
+
104
+ // ---------------------------------------------------------------- store
105
+
106
+ const store = {
107
+ snapshot: { items: [], activeId: null, error: null, notice: null, cwd: null, ready: false, selectMode: false, scheme: 'dark' },
108
+ listeners: new Set(),
109
+ subscribe: (fn) => {
110
+ store.listeners.add(fn);
111
+ return () => {
112
+ store.listeners.delete(fn);
113
+ };
114
+ },
115
+ getSnapshot: () => store.snapshot,
116
+ patch(next) {
117
+ store.snapshot = Object.assign({}, store.snapshot, next);
118
+ for (const fn of Array.from(store.listeners)) fn();
119
+ },
120
+ };
121
+
122
+ function useStore() {
123
+ return React.useSyncExternalStore(store.subscribe, store.getSnapshot);
124
+ }
125
+
126
+ async function refresh() {
127
+ if (runtime.syncScheme) runtime.syncScheme();
128
+ try {
129
+ const value = await api('list');
130
+ const items = value && Array.isArray(value.items) ? value.items : [];
131
+ let activeId = store.snapshot.activeId;
132
+ if (!activeId || !items.some((item) => item.id === activeId)) {
133
+ const running = items.find((item) => item.status === 'running');
134
+ activeId = running ? running.id : items.length ? items[0].id : null;
135
+ }
136
+ store.patch({ items, activeId, cwd: (value && value.cwd) || null, error: null, ready: true });
137
+ } catch (error) {
138
+ store.patch({ error: messageOf(error), ready: true });
139
+ }
140
+ }
141
+
142
+ function selectPanel() {
143
+ const layout = runtime.ctx && runtime.ctx.get ? runtime.ctx.get('layout') : null;
144
+ if (layout && typeof layout.selectPanel === 'function') {
145
+ try {
146
+ layout.selectPanel(PANEL_KEY);
147
+ } catch (err) {
148
+ /* the shell may not accept an unknown panel id; the icon still works */
149
+ }
150
+ }
151
+ }
152
+
153
+ async function createTerminal() {
154
+ store.patch({ notice: null });
155
+ try {
156
+ const value = await api('create', { cols: panelSize.cols, rows: panelSize.rows });
157
+ await refresh();
158
+ if (value && value.item) store.patch({ activeId: value.item.id });
159
+ selectPanel();
160
+ } catch (error) {
161
+ store.patch({ notice: messageOf(error) });
162
+ }
163
+ }
164
+
165
+ async function closeTerminal(id) {
166
+ store.patch({ notice: null });
167
+ try {
168
+ await api('close', { id });
169
+ } catch (error) {
170
+ store.patch({ notice: messageOf(error) });
171
+ }
172
+ await refresh();
173
+ }
174
+
175
+ async function removeTerminal(id) {
176
+ store.patch({ notice: null });
177
+ try {
178
+ await api('remove', { id });
179
+ } catch (error) {
180
+ store.patch({ notice: messageOf(error) });
181
+ }
182
+ await refresh();
183
+ }
184
+
185
+ async function renameTerminal(id, title) {
186
+ store.patch({ notice: null });
187
+ try {
188
+ await api('rename', { id, title });
189
+ } catch (error) {
190
+ store.patch({ notice: messageOf(error) });
191
+ }
192
+ await refresh();
193
+ }
194
+
195
+ function writeData(id, data) {
196
+ if (!data) return;
197
+ api('write', { id, data }).catch(() => undefined);
198
+ }
199
+
200
+ // ------------------------------------------------------------ VT engine
201
+
202
+ const ATTR_BOLD = 1;
203
+ const ATTR_DIM = 2;
204
+ const ATTR_ITALIC = 4;
205
+ const ATTR_UNDERLINE = 8;
206
+ const ATTR_INVERSE = 16;
207
+ const ATTR_STRIKE = 32;
208
+
209
+ /**
210
+ * Two ANSI palettes. The light one is not the dark one on a white sheet:
211
+ * SGR 37 ("white") and 93 ("bright yellow") must stay readable on a light
212
+ * background, so they map to dark grey / dark yellow there.
213
+ */
214
+ const PALETTES = {
215
+ dark: [
216
+ '#000000', '#cd3131', '#0dbc79', '#e5e510', '#2472c8', '#bc3fbc', '#11a8cd', '#e5e5e5',
217
+ '#666666', '#f14c4c', '#23d18b', '#f5f543', '#3b8eea', '#d670d6', '#29b8db', '#ffffff',
218
+ ],
219
+ light: [
220
+ '#000000', '#cd3131', '#00bc00', '#949800', '#0451a5', '#bc05bc', '#0598bc', '#555555',
221
+ '#666666', '#cd3131', '#14ce14', '#b5ba00', '#0451a5', '#bc05bc', '#0598bc', '#a5a5a5',
222
+ ],
223
+ };
224
+
225
+ function palette() {
226
+ return runtime.scheme === 'light' ? PALETTES.light : PALETTES.dark;
227
+ }
228
+
229
+ function createScreen(cols, rows) {
230
+ const screen = {
231
+ cols,
232
+ rows,
233
+ lines: [],
234
+ x: 0,
235
+ y: 0,
236
+ savedX: 0,
237
+ savedY: 0,
238
+ fg: null,
239
+ bg: null,
240
+ attr: 0,
241
+ top: 0,
242
+ bottom: rows - 1,
243
+ cursorVisible: true,
244
+ dirty: true,
245
+ alt: null,
246
+ };
247
+ blank(screen);
248
+ return screen;
249
+ }
250
+
251
+ function blank(screen) {
252
+ screen.lines = [];
253
+ for (let y = 0; y < screen.rows; y += 1) screen.lines.push(new Array(screen.cols).fill(null));
254
+ }
255
+
256
+ /** Keep the overlapping region when the grid is rebuilt at another size. */
257
+ function adoptScreen(previous, next) {
258
+ for (let y = 0; y < Math.min(previous.rows, next.rows); y += 1) {
259
+ for (let x = 0; x < Math.min(previous.cols, next.cols); x += 1) {
260
+ next.lines[y][x] = previous.lines[y][x];
261
+ }
262
+ }
263
+ next.x = Math.min(previous.x, next.cols - 1);
264
+ next.y = Math.min(previous.y, next.rows - 1);
265
+ next.fg = previous.fg;
266
+ next.bg = previous.bg;
267
+ next.attr = previous.attr;
268
+ }
269
+
270
+ function cellAt(screen, x, y) {
271
+ const line = screen.lines[y];
272
+ return line ? line[x] : null;
273
+ }
274
+
275
+ function writeCell(screen, x, y, ch, wide) {
276
+ const line = screen.lines[y];
277
+ if (!line || x < 0 || x >= screen.cols) return;
278
+ line[x] = { ch, fg: screen.fg, bg: screen.bg, a: screen.attr };
279
+ if (wide && x + 1 < screen.cols) line[x + 1] = { ch: '', fg: screen.fg, bg: screen.bg, a: screen.attr, wide: true };
280
+ }
281
+
282
+ function scrollUp(screen, top, bottom, count) {
283
+ for (let n = 0; n < count; n += 1) {
284
+ screen.lines.splice(top, 1);
285
+ screen.lines.splice(bottom, 0, new Array(screen.cols).fill(null));
286
+ }
287
+ }
288
+
289
+ function scrollDown(screen, top, bottom, count) {
290
+ for (let n = 0; n < count; n += 1) {
291
+ screen.lines.splice(bottom, 1);
292
+ screen.lines.splice(top, 0, new Array(screen.cols).fill(null));
293
+ }
294
+ }
295
+
296
+ function index(screen) {
297
+ if (screen.y === screen.bottom) scrollUp(screen, screen.top, screen.bottom, 1);
298
+ else if (screen.y < screen.rows - 1) screen.y += 1;
299
+ }
300
+
301
+ function isWide(cp) {
302
+ return (
303
+ (cp >= 0x1100 && cp <= 0x115f) ||
304
+ (cp >= 0x2e80 && cp <= 0x303e) ||
305
+ (cp >= 0x3041 && cp <= 0x33ff) ||
306
+ (cp >= 0x3400 && cp <= 0x4dbf) ||
307
+ (cp >= 0x4e00 && cp <= 0x9fff) ||
308
+ (cp >= 0xa000 && cp <= 0xa4cf) ||
309
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
310
+ (cp >= 0xf900 && cp <= 0xfaff) ||
311
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
312
+ (cp >= 0xff00 && cp <= 0xff60) ||
313
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
314
+ (cp >= 0x1f300 && cp <= 0x1f9ff) ||
315
+ (cp >= 0x20000 && cp <= 0x3fffd)
316
+ );
317
+ }
318
+
319
+ function put(screen, ch, cp) {
320
+ const wide = isWide(cp);
321
+ if (screen.x >= screen.cols) {
322
+ screen.x = 0;
323
+ index(screen);
324
+ }
325
+ if (wide && screen.x === screen.cols - 1) {
326
+ screen.x = 0;
327
+ index(screen);
328
+ }
329
+ writeCell(screen, screen.x, screen.y, ch, wide);
330
+ screen.x += wide ? 2 : 1;
331
+ }
332
+
333
+ function applySgr(screen, params) {
334
+ const list = params.length ? params : [0];
335
+ for (let i = 0; i < list.length; i += 1) {
336
+ const code = list[i];
337
+ if (code === 0) {
338
+ screen.fg = null;
339
+ screen.bg = null;
340
+ screen.attr = 0;
341
+ } else if (code === 1) screen.attr |= ATTR_BOLD;
342
+ else if (code === 2) screen.attr |= ATTR_DIM;
343
+ else if (code === 3) screen.attr |= ATTR_ITALIC;
344
+ else if (code === 4) screen.attr |= ATTR_UNDERLINE;
345
+ else if (code === 7) screen.attr |= ATTR_INVERSE;
346
+ else if (code === 9) screen.attr |= ATTR_STRIKE;
347
+ else if (code === 22) screen.attr &= ~(ATTR_BOLD | ATTR_DIM);
348
+ else if (code === 23) screen.attr &= ~ATTR_ITALIC;
349
+ else if (code === 24) screen.attr &= ~ATTR_UNDERLINE;
350
+ else if (code === 27) screen.attr &= ~ATTR_INVERSE;
351
+ else if (code === 29) screen.attr &= ~ATTR_STRIKE;
352
+ else if (code >= 30 && code <= 37) screen.fg = code - 30;
353
+ else if (code === 39) screen.fg = null;
354
+ else if (code >= 40 && code <= 47) screen.bg = code - 40;
355
+ else if (code === 49) screen.bg = null;
356
+ else if (code >= 90 && code <= 97) screen.fg = code - 90 + 8;
357
+ else if (code >= 100 && code <= 107) screen.bg = code - 100 + 8;
358
+ else if (code === 38 || code === 48) {
359
+ const target = code === 38 ? 'fg' : 'bg';
360
+ const mode = list[i + 1];
361
+ if (mode === 5) {
362
+ screen[target] = list[i + 2];
363
+ i += 2;
364
+ } else if (mode === 2) {
365
+ const r = clamp255(list[i + 2]);
366
+ const g = clamp255(list[i + 3]);
367
+ const b = clamp255(list[i + 4]);
368
+ screen[target] = '#' + hex(r) + hex(g) + hex(b);
369
+ i += 4;
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ function clamp255(value) {
376
+ const number = typeof value === 'number' ? value : 0;
377
+ return Math.max(0, Math.min(255, Math.round(number)));
378
+ }
379
+
380
+ function hex(value) {
381
+ const text = clamp255(value).toString(16);
382
+ return text.length === 1 ? '0' + text : text;
383
+ }
384
+
385
+ function eraseInLine(screen, mode) {
386
+ const line = screen.lines[screen.y];
387
+ if (!line) return;
388
+ const from = mode === 0 ? screen.x : 0;
389
+ const to = mode === 1 ? screen.x + 1 : screen.cols;
390
+ for (let x = from; x < to; x += 1) line[x] = null;
391
+ }
392
+
393
+ function eraseInDisplay(screen, mode) {
394
+ if (mode === 0 || mode === 2 || mode === 3) {
395
+ const startY = mode === 0 ? screen.y : 0;
396
+ const line = screen.lines[screen.y];
397
+ if (mode === 0 && line) for (let x = screen.x; x < screen.cols; x += 1) line[x] = null;
398
+ for (let y = startY + (mode === 0 ? 1 : 0); y < screen.rows; y += 1) {
399
+ screen.lines[y] = new Array(screen.cols).fill(null);
400
+ }
401
+ }
402
+ if (mode === 1) {
403
+ for (let y = 0; y < screen.y; y += 1) screen.lines[y] = new Array(screen.cols).fill(null);
404
+ const line = screen.lines[screen.y];
405
+ if (line) for (let x = 0; x <= screen.x && x < screen.cols; x += 1) line[x] = null;
406
+ }
407
+ if (mode === 2 || mode === 3) {
408
+ for (let y = 0; y < screen.rows; y += 1) screen.lines[y] = new Array(screen.cols).fill(null);
409
+ }
410
+ }
411
+
412
+ function switchAlt(screen, on) {
413
+ if (on) {
414
+ if (screen.alt) return;
415
+ screen.alt = { lines: screen.lines, x: screen.x, y: screen.y };
416
+ blank(screen);
417
+ screen.x = 0;
418
+ screen.y = 0;
419
+ screen.top = 0;
420
+ screen.bottom = screen.rows - 1;
421
+ } else if (screen.alt) {
422
+ screen.lines = screen.alt.lines;
423
+ screen.x = screen.alt.x;
424
+ screen.y = screen.alt.y;
425
+ screen.alt = null;
426
+ screen.top = 0;
427
+ screen.bottom = screen.rows - 1;
428
+ }
429
+ }
430
+
431
+ function escapeAt(screen, text, start) {
432
+ const next = text[start + 1];
433
+ if (next === undefined) return text.length;
434
+ if (next === '[') {
435
+ let i = start + 2;
436
+ let body = '';
437
+ while (i < text.length && !/[@-~]/.test(text[i])) {
438
+ body += text[i];
439
+ i += 1;
440
+ }
441
+ const final = text[i];
442
+ if (final !== undefined) csi(screen, body, final);
443
+ return i + 1;
444
+ }
445
+ if (next === ']') {
446
+ let i = start + 2;
447
+ while (i < text.length) {
448
+ if (text[i] === '\x07') return i + 1;
449
+ if (text[i] === '\x1b' && text[i + 1] === '\\') return i + 2;
450
+ i += 1;
451
+ }
452
+ return i;
453
+ }
454
+ if (next === '7') {
455
+ screen.savedX = screen.x;
456
+ screen.savedY = screen.y;
457
+ return start + 2;
458
+ }
459
+ if (next === '8') {
460
+ screen.x = screen.savedX;
461
+ screen.y = screen.savedY;
462
+ return start + 2;
463
+ }
464
+ if (next === 'c') {
465
+ screen.fg = null;
466
+ screen.bg = null;
467
+ screen.attr = 0;
468
+ blank(screen);
469
+ screen.x = 0;
470
+ screen.y = 0;
471
+ return start + 2;
472
+ }
473
+ if (next === '(' || next === ')' || next === '*' || next === '+') return start + 3;
474
+ if (next === '=' || next === '>' || next === 'M' || next === 'D' || next === 'E' || next === 'H') return start + 2;
475
+ return start + 2;
476
+ }
477
+
478
+ function paramsOf(body) {
479
+ const clean = body.replace(/^[?>!]+/, '');
480
+ if (!clean) return [];
481
+ return clean.split(';').map((part) => {
482
+ const value = Number.parseInt(part, 10);
483
+ return Number.isFinite(value) ? value : 0;
484
+ });
485
+ }
486
+
487
+ function csi(screen, body, final) {
488
+ const privateMode = body[0] === '?';
489
+ const params = paramsOf(body);
490
+ const first = params.length ? params[0] : 0;
491
+ const amount = first === 0 ? 1 : first;
492
+ switch (final) {
493
+ case 'A':
494
+ screen.y = Math.max(screen.top, screen.y - amount);
495
+ break;
496
+ case 'B':
497
+ screen.y = Math.min(screen.bottom, screen.y + amount);
498
+ break;
499
+ case 'C':
500
+ screen.x = Math.min(screen.cols - 1, screen.x + amount);
501
+ break;
502
+ case 'D':
503
+ screen.x = Math.max(0, screen.x - amount);
504
+ break;
505
+ case 'E':
506
+ screen.x = 0;
507
+ screen.y = Math.min(screen.bottom, screen.y + amount);
508
+ break;
509
+ case 'F':
510
+ screen.x = 0;
511
+ screen.y = Math.max(screen.top, screen.y - amount);
512
+ break;
513
+ case 'G':
514
+ screen.x = Math.max(0, Math.min(screen.cols - 1, (first || 1) - 1));
515
+ break;
516
+ case 'd':
517
+ screen.y = Math.max(0, Math.min(screen.rows - 1, (first || 1) - 1));
518
+ break;
519
+ case 'H':
520
+ case 'f': {
521
+ const row = params.length > 0 && params[0] ? params[0] : 1;
522
+ const col = params.length > 1 && params[1] ? params[1] : 1;
523
+ screen.y = Math.max(0, Math.min(screen.rows - 1, row - 1));
524
+ screen.x = Math.max(0, Math.min(screen.cols - 1, col - 1));
525
+ break;
526
+ }
527
+ case 'J':
528
+ eraseInDisplay(screen, first);
529
+ break;
530
+ case 'K':
531
+ eraseInLine(screen, first);
532
+ break;
533
+ case 'L':
534
+ if (screen.y >= screen.top && screen.y <= screen.bottom) scrollDown(screen, screen.y, screen.bottom, amount);
535
+ break;
536
+ case 'M':
537
+ if (screen.y >= screen.top && screen.y <= screen.bottom) scrollUp(screen, screen.y, screen.bottom, amount);
538
+ break;
539
+ case 'P': {
540
+ const line = screen.lines[screen.y];
541
+ if (line) {
542
+ line.splice(screen.x, amount);
543
+ while (line.length < screen.cols) line.push(null);
544
+ }
545
+ break;
546
+ }
547
+ case 'S':
548
+ scrollUp(screen, screen.top, screen.bottom, amount);
549
+ break;
550
+ case 'T':
551
+ scrollDown(screen, screen.top, screen.bottom, amount);
552
+ break;
553
+ case 'X': {
554
+ const line = screen.lines[screen.y];
555
+ if (line) for (let x = screen.x; x < Math.min(screen.cols, screen.x + amount); x += 1) line[x] = null;
556
+ break;
557
+ }
558
+ case 'm':
559
+ applySgr(screen, params);
560
+ break;
561
+ case 'n':
562
+ break;
563
+ case 'r':
564
+ screen.top = params.length > 0 && params[0] ? params[0] - 1 : 0;
565
+ screen.bottom = params.length > 1 && params[1] ? params[1] - 1 : screen.rows - 1;
566
+ screen.top = Math.max(0, Math.min(screen.rows - 1, screen.top));
567
+ screen.bottom = Math.max(screen.top, Math.min(screen.rows - 1, screen.bottom));
568
+ screen.x = 0;
569
+ screen.y = screen.top;
570
+ break;
571
+ case 's':
572
+ screen.savedX = screen.x;
573
+ screen.savedY = screen.y;
574
+ break;
575
+ case 'u':
576
+ screen.x = screen.savedX;
577
+ screen.y = screen.savedY;
578
+ break;
579
+ case 'h':
580
+ case 'l':
581
+ if (privateMode) {
582
+ const on = final === 'h';
583
+ for (const param of params) {
584
+ if (param === 25) screen.cursorVisible = on;
585
+ else if (param === 1049 || param === 47 || param === 1047) switchAlt(screen, on);
586
+ }
587
+ }
588
+ break;
589
+ default:
590
+ break;
591
+ }
592
+ }
593
+
594
+ function feed(screen, text) {
595
+ if (!text) return;
596
+ let i = 0;
597
+ while (i < text.length) {
598
+ const code = text.charCodeAt(i);
599
+ if (code === 27) {
600
+ i = escapeAt(screen, text, i);
601
+ continue;
602
+ }
603
+ if (code === 13) {
604
+ screen.x = 0;
605
+ i += 1;
606
+ continue;
607
+ }
608
+ if (code === 10 || code === 11 || code === 12) {
609
+ index(screen);
610
+ i += 1;
611
+ continue;
612
+ }
613
+ if (code === 8) {
614
+ if (screen.x > 0) screen.x -= 1;
615
+ i += 1;
616
+ continue;
617
+ }
618
+ if (code === 9) {
619
+ screen.x = Math.min(screen.cols - 1, (Math.floor(screen.x / 8) + 1) * 8);
620
+ i += 1;
621
+ continue;
622
+ }
623
+ if (code < 32 || code === 127) {
624
+ i += 1;
625
+ continue;
626
+ }
627
+ const cp = text.codePointAt(i);
628
+ const ch = String.fromCodePoint(cp);
629
+ i += ch.length;
630
+ put(screen, ch, cp);
631
+ }
632
+ screen.dirty = true;
633
+ }
634
+
635
+ const styleCache = new Map();
636
+
637
+ function colorOf(value) {
638
+ if (value === null || value === undefined) return null;
639
+ if (typeof value === 'string') return value;
640
+ if (value < 16) return palette()[value];
641
+ if (value < 232) {
642
+ const n = value - 16;
643
+ const steps = [0, 95, 135, 175, 215, 255];
644
+ const r = steps[Math.floor(n / 36) % 6];
645
+ const g = steps[Math.floor(n / 6) % 6];
646
+ const b = steps[n % 6];
647
+ return '#' + hex(r) + hex(g) + hex(b);
648
+ }
649
+ const level = 8 + (value - 232) * 10;
650
+ return '#' + hex(level) + hex(level) + hex(level);
651
+ }
652
+
653
+ function cellKey(cell) {
654
+ if (!cell) return 'd';
655
+ return String(cell.fg) + '/' + String(cell.bg) + '/' + cell.a;
656
+ }
657
+
658
+ function cellStyle(cell) {
659
+ const key = cellKey(cell) + '@' + runtime.scheme;
660
+ const cached = styleCache.get(key);
661
+ if (cached !== undefined) return cached;
662
+ const style = {};
663
+ if (!cell) {
664
+ styleCache.set(key, style);
665
+ return style;
666
+ }
667
+ let fg = colorOf(cell.fg);
668
+ let bg = colorOf(cell.bg);
669
+ if (cell.a & ATTR_INVERSE) {
670
+ const swap = fg;
671
+ fg = bg || 'var(--dt-fg)';
672
+ bg = swap || 'var(--dt-bg)';
673
+ }
674
+ if (cell.a & ATTR_BOLD) style.fontWeight = 600;
675
+ if (cell.a & ATTR_DIM) style.opacity = 0.65;
676
+ if (cell.a & ATTR_ITALIC) style.fontStyle = 'italic';
677
+ if (cell.a & ATTR_UNDERLINE) style.textDecoration = 'underline';
678
+ if (cell.a & ATTR_STRIKE) style.textDecoration = 'line-through';
679
+ if (fg) style.color = fg;
680
+ if (bg) style.backgroundColor = bg;
681
+ styleCache.set(key, style);
682
+ return style;
683
+ }
684
+
685
+ /** Split one row into styled runs, splitting again at the cursor cell. */
686
+ function runsFor(screen, y) {
687
+ const line = screen.lines[y] || [];
688
+ const cursorHere = screen.cursorVisible && screen.y === y;
689
+ const runs = [];
690
+ let current = null;
691
+ for (let x = 0; x < screen.cols; x += 1) {
692
+ const cell = line[x];
693
+ const isCursor = cursorHere && x === screen.x;
694
+ const key = cellKey(cell) + (isCursor ? '+' : '');
695
+ const text = cell ? cell.ch : ' ';
696
+ if (!current || current.key !== key) {
697
+ current = { key, text, cell: cell || null, cursor: isCursor };
698
+ runs.push(current);
699
+ } else {
700
+ current.text += text;
701
+ }
702
+ }
703
+ while (runs.length) {
704
+ const last = runs[runs.length - 1];
705
+ if (!last.cursor && last.key === 'd' && /^ +$/.test(last.text)) runs.pop();
706
+ else break;
707
+ }
708
+ return runs;
709
+ }
710
+
711
+ function measurePanel(holder) {
712
+ if (!holder) return;
713
+ const probe = holder.parentElement ? holder.parentElement.querySelector('.dt-probe') : null;
714
+ const target = probe || holder.querySelector('.dt-probe');
715
+ const rect = target ? target.getBoundingClientRect() : null;
716
+ const cellW = rect && rect.width ? rect.width / 20 : 7.8;
717
+ const cellH = rect && rect.height ? rect.height / 2 : 17;
718
+ const cols = Math.max(20, Math.min(MAX_COLS, Math.floor((holder.clientWidth - 20) / cellW)));
719
+ const rows = Math.max(5, Math.min(MAX_ROWS, Math.floor((holder.clientHeight - 34) / cellH)));
720
+ panelSize = { cols, rows };
721
+ }
722
+
723
+ function decodeBase64(text) {
724
+ const binary = atob(text);
725
+ const bytes = new Uint8Array(binary.length);
726
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
727
+ return new TextDecoder('utf-8').decode(bytes);
728
+ }
729
+
730
+ function keyToData(event) {
731
+ const key = event.key;
732
+ if (event.ctrlKey && !event.altKey && !event.metaKey && key.length === 1) {
733
+ if (key === ' ') return '\x00';
734
+ const upper = key.toUpperCase();
735
+ const code = upper.charCodeAt(0);
736
+ if (code >= 65 && code <= 90) return String.fromCharCode(code - 64);
737
+ if (key === '[') return '\x1b';
738
+ if (key === '\\') return '\x1c';
739
+ if (key === ']') return '\x1d';
740
+ if (key === '^') return '\x1e';
741
+ if (key === '_') return '\x1f';
742
+ return null;
743
+ }
744
+ if (event.metaKey) return null;
745
+ if (event.altKey && key.length === 1) return '\x1b' + key;
746
+ switch (key) {
747
+ case 'Enter':
748
+ return '\r';
749
+ case 'Backspace':
750
+ return '\x7f';
751
+ case 'Tab':
752
+ return '\t';
753
+ case 'Escape':
754
+ return '\x1b';
755
+ case 'ArrowUp':
756
+ return '\x1b[A';
757
+ case 'ArrowDown':
758
+ return '\x1b[B';
759
+ case 'ArrowRight':
760
+ return '\x1b[C';
761
+ case 'ArrowLeft':
762
+ return '\x1b[D';
763
+ case 'Home':
764
+ return '\x1b[H';
765
+ case 'End':
766
+ return '\x1b[F';
767
+ case 'PageUp':
768
+ return '\x1b[5~';
769
+ case 'PageDown':
770
+ return '\x1b[6~';
771
+ case 'Insert':
772
+ return '\x1b[2~';
773
+ case 'Delete':
774
+ return '\x1b[3~';
775
+ default:
776
+ return null;
777
+ }
778
+ }
779
+
780
+ // ----------------------------------------------------------- components
781
+
782
+ function TerminalView({ item, select }) {
783
+ const holderRef = React.useRef(null);
784
+ const inputRef = React.useRef(null);
785
+ const screenRef = React.useRef(null);
786
+ const frameRef = React.useRef(0);
787
+ const [, setFrame] = React.useState(0);
788
+
789
+ const cols = item.cols || 100;
790
+ const rows = item.rows || 28;
791
+ if (!screenRef.current || screenRef.current.cols !== cols || screenRef.current.rows !== rows) {
792
+ const previous = screenRef.current;
793
+ const next = createScreen(cols, rows);
794
+ if (previous) adoptScreen(previous, next);
795
+ screenRef.current = next;
796
+ }
797
+ const screen = screenRef.current;
798
+
799
+ const scheduleFrame = React.useCallback(() => {
800
+ if (frameRef.current) return;
801
+ frameRef.current = window.requestAnimationFrame(() => {
802
+ frameRef.current = 0;
803
+ setFrame((value) => value + 1);
804
+ });
805
+ }, []);
806
+
807
+ React.useEffect(() => {
808
+ const source = new EventSource(API + '/stream?id=' + encodeURIComponent(item.id));
809
+ const onData = (event) => {
810
+ feed(screenRef.current, decodeBase64(event.data));
811
+ scheduleFrame();
812
+ };
813
+ const onStatus = () => scheduleFrame();
814
+ source.addEventListener('data', onData);
815
+ source.addEventListener('history', onData);
816
+ source.addEventListener('status', onStatus);
817
+ source.addEventListener('exit', onStatus);
818
+ return () => source.close();
819
+ }, [item.id, scheduleFrame]);
820
+
821
+ React.useEffect(() => {
822
+ const holder = holderRef.current;
823
+ if (!holder) return undefined;
824
+ if (inputRef.current) inputRef.current.focus({ preventScroll: true });
825
+ measurePanel(holder);
826
+ let lastSent = 0;
827
+ const observer = new ResizeObserver(() => {
828
+ if (!holder.clientWidth || holder.clientWidth < 80 || holder.clientHeight < 60) return;
829
+ measurePanel(holder);
830
+ const screenNow = screenRef.current;
831
+ if (!item.resizable || !screenNow) return;
832
+ if (panelSize.cols === screenNow.cols && panelSize.rows === screenNow.rows) return;
833
+ const now = Date.now();
834
+ if (now - lastSent < 350) return;
835
+ lastSent = now;
836
+ const next = createScreen(panelSize.cols, panelSize.rows);
837
+ adoptScreen(screenNow, next);
838
+ screenRef.current = next;
839
+ scheduleFrame();
840
+ api('resize', { id: item.id, cols: panelSize.cols, rows: panelSize.rows }).catch(() => undefined);
841
+ });
842
+ observer.observe(holder);
843
+ return () => observer.disconnect();
844
+ }, [item.id, item.resizable, scheduleFrame]);
845
+
846
+ const focus = React.useCallback(() => {
847
+ if (inputRef.current) inputRef.current.focus({ preventScroll: true });
848
+ }, []);
849
+
850
+ const send = React.useCallback(
851
+ (data) => {
852
+ writeData(item.id, data);
853
+ },
854
+ [item.id],
855
+ );
856
+
857
+ const onKeyDown = (event) => {
858
+ const data = keyToData(event);
859
+ if (data !== null) {
860
+ event.preventDefault();
861
+ send(data);
862
+ }
863
+ };
864
+
865
+ const onInput = (event) => {
866
+ const value = event.target.value;
867
+ if (value) {
868
+ send(value);
869
+ event.target.value = '';
870
+ }
871
+ };
872
+
873
+ const onPaste = (event) => {
874
+ const text = event.clipboardData ? event.clipboardData.getData('text') : '';
875
+ if (text) {
876
+ event.preventDefault();
877
+ send(text);
878
+ }
879
+ };
880
+
881
+ const rowNodes = [];
882
+ for (let y = 0; y < screen.rows; y += 1) {
883
+ const runs = runsFor(screen, y);
884
+ rowNodes.push(
885
+ h(
886
+ 'div',
887
+ { className: 'dt-row', key: y },
888
+ runs.map((run, index) =>
889
+ h(
890
+ 'span',
891
+ { key: index, className: run.cursor ? 'dt-cursor' : undefined, style: cellStyle(run.cell) },
892
+ run.text,
893
+ ),
894
+ ),
895
+ ),
896
+ );
897
+ }
898
+
899
+ return h(
900
+ 'div',
901
+ { className: select ? 'dt-view plain' : 'dt-view', ref: holderRef, onMouseDown: select ? undefined : focus },
902
+ h('div', { className: 'dt-screen', style: { width: screen.cols + 'ch' } }, rowNodes),
903
+ h('textarea', {
904
+ className: select ? 'dt-input off' : 'dt-input',
905
+ ref: inputRef,
906
+ spellCheck: false,
907
+ autoCapitalize: 'off',
908
+ autoCorrect: 'off',
909
+ autoComplete: 'off',
910
+ onKeyDown,
911
+ onInput,
912
+ onPaste,
913
+ }),
914
+ );
915
+ }
916
+
917
+ function TerminalTab({ item, active }) {
918
+ const t = runtime.t;
919
+ const [editing, setEditing] = React.useState(false);
920
+ const [draft, setDraft] = React.useState(item.title);
921
+ const inputRef = React.useRef(null);
922
+
923
+ React.useEffect(() => {
924
+ if (editing && inputRef.current) {
925
+ inputRef.current.focus();
926
+ inputRef.current.select();
927
+ }
928
+ }, [editing]);
929
+
930
+ const startEditing = () => {
931
+ setDraft(item.title);
932
+ setEditing(true);
933
+ };
934
+
935
+ const commit = () => {
936
+ setEditing(false);
937
+ const next = draft.trim();
938
+ if (next && next !== item.title) renameTerminal(item.id, next);
939
+ };
940
+
941
+ if (editing) {
942
+ return h(
943
+ 'div',
944
+ { className: active ? 'dt-tab on' : 'dt-tab' },
945
+ h('span', { className: item.status === 'running' ? 'dt-dot on' : 'dt-dot' }),
946
+ h('input', {
947
+ className: 'dt-tab-input',
948
+ ref: inputRef,
949
+ value: draft,
950
+ spellCheck: false,
951
+ onChange: (event) => setDraft(event.target.value),
952
+ onBlur: commit,
953
+ onKeyDown: (event) => {
954
+ if (event.key === 'Enter') {
955
+ event.preventDefault();
956
+ commit();
957
+ } else if (event.key === 'Escape') {
958
+ event.preventDefault();
959
+ setEditing(false);
960
+ }
961
+ },
962
+ }),
963
+ );
964
+ }
965
+
966
+ return h(
967
+ 'div',
968
+ {
969
+ className: active ? 'dt-tab on' : 'dt-tab',
970
+ onClick: () => store.patch({ activeId: item.id }),
971
+ onDoubleClick: startEditing,
972
+ title: (item.cwd ? item.cwd + ' — ' : '') + t('renameHint'),
973
+ },
974
+ h('span', { className: item.status === 'running' ? 'dt-dot on' : 'dt-dot' }),
975
+ h('span', { className: 'dt-tab-name' }, item.title),
976
+ h(
977
+ 'button',
978
+ {
979
+ type: 'button',
980
+ className: 'dt-tab-x',
981
+ title: t('rename'),
982
+ onClick: (event) => {
983
+ event.stopPropagation();
984
+ startEditing();
985
+ },
986
+ },
987
+ '✎',
988
+ ),
989
+ h(
990
+ 'button',
991
+ {
992
+ type: 'button',
993
+ className: 'dt-tab-x',
994
+ title: item.status === 'running' ? t('close') : t('remove'),
995
+ onClick: (event) => {
996
+ event.stopPropagation();
997
+ if (item.status === 'running') closeTerminal(item.id);
998
+ else removeTerminal(item.id);
999
+ },
1000
+ },
1001
+ '✕',
1002
+ ),
1003
+ );
1004
+ }
1005
+
1006
+ function TerminalTabs({ state }) {
1007
+ const t = runtime.t;
1008
+ return h(
1009
+ 'div',
1010
+ { className: 'dt-head' },
1011
+ h(
1012
+ 'div',
1013
+ { className: 'dt-tabs' },
1014
+ state.items.map((item) => h(TerminalTab, { key: item.id, item, active: item.id === state.activeId })),
1015
+ h(
1016
+ 'button',
1017
+ { type: 'button', className: 'dt-new', onClick: createTerminal, title: t('newTerminal') },
1018
+ '+ ' + t('newTerminal'),
1019
+ ),
1020
+ state.items.length
1021
+ ? h(
1022
+ 'button',
1023
+ {
1024
+ type: 'button',
1025
+ className: state.selectMode ? 'dt-new on' : 'dt-new',
1026
+ title: state.selectMode ? t('inputMode') : t('selectMode'),
1027
+ onClick: () => store.patch({ selectMode: !state.selectMode }),
1028
+ },
1029
+ state.selectMode ? '⌨' : '⧉',
1030
+ )
1031
+ : null,
1032
+ ),
1033
+ h('div', { className: 'dt-meta' }, state.cwd || ''),
1034
+ );
1035
+ }
1036
+
1037
+ function TerminalPanel() {
1038
+ const t = runtime.t;
1039
+ const state = useStore();
1040
+ React.useEffect(() => {
1041
+ if (runtime.syncScheme) runtime.syncScheme();
1042
+ }, []);
1043
+ const active = state.items.find((item) => item.id === state.activeId) || null;
1044
+ return h(
1045
+ 'div',
1046
+ { className: 'dt-panel' },
1047
+ h('span', { className: 'dt-probe', 'aria-hidden': true }, 'MMMMMMMMMMMMMMMMMMMM\nM'),
1048
+ state.items.length ? h(TerminalTabs, { state }) : null,
1049
+ state.notice ? h('div', { className: 'dt-notice' }, state.notice) : null,
1050
+ state.error ? h('div', { className: 'dt-notice err' }, state.error) : null,
1051
+ active
1052
+ ? h(TerminalView, { key: active.id, item: active, select: state.selectMode })
1053
+ : h(
1054
+ 'div',
1055
+ { className: 'dt-empty' },
1056
+ h('div', { className: 'dt-empty-title' }, t('empty')),
1057
+ h('div', { className: 'dt-empty-hint' }, t('emptyHint')),
1058
+ h('button', { type: 'button', className: 'dt-empty-btn', onClick: createTerminal }, '+ ' + t('newTerminal')),
1059
+ ),
1060
+ );
1061
+ }
1062
+
1063
+ function PanelIcon(props) {
1064
+ const t = runtime.t;
1065
+ const state = useStore();
1066
+ const size = props && props.size ? props.size : 18;
1067
+ const active = Boolean(props && props.active);
1068
+ const running = state.items.filter((item) => item.status === 'running').length;
1069
+ return h(
1070
+ 'span',
1071
+ { className: 'dt-icon' },
1072
+ h(
1073
+ 'svg',
1074
+ {
1075
+ width: size,
1076
+ height: size,
1077
+ viewBox: '0 0 16 16',
1078
+ fill: 'none',
1079
+ stroke: 'currentColor',
1080
+ strokeWidth: 1.4,
1081
+ strokeLinecap: 'round',
1082
+ strokeLinejoin: 'round',
1083
+ 'aria-hidden': true,
1084
+ style: { display: 'block', opacity: active ? 1 : 0.85 },
1085
+ },
1086
+ h('rect', { x: 1.6, y: 2.4, width: 12.8, height: 11.2, rx: 2 }),
1087
+ h('path', { d: 'M4.4 6.2l2 1.8-2 1.8' }),
1088
+ h('path', { d: 'M8.4 10.2h3.2' }),
1089
+ ),
1090
+ running
1091
+ ? h('span', { className: 'dt-badge', title: t('activeLabel') + ': ' + running }, String(running))
1092
+ : null,
1093
+ );
1094
+ }
1095
+
1096
+ // ---------------------------------------------------------------- style
1097
+
1098
+ const CSS = [
1099
+ '.dt-panel{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#1f2329);font-size:13px}',
1100
+ '.dt-head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(15,23,42,.1));background:var(--dsw-alias-bg-layer-1,#fafafa)}',
1101
+ '.dt-tabs{display:flex;align-items:center;gap:4px;overflow-x:auto;min-width:0;flex:1 1 auto}',
1102
+ '.dt-tab{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:7px;cursor:pointer;white-space:nowrap;color:var(--dsw-alias-label-secondary,#6b7280)}',
1103
+ '.dt-tab:hover{background:var(--dsw-alias-bg-layer-2,rgba(15,23,42,.06))}',
1104
+ '.dt-tab.on{background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#1f2329);box-shadow:0 0 0 1px var(--dsw-alias-border-l1,rgba(15,23,42,.12))}',
1105
+ '.dt-tab-name{max-width:160px;overflow:hidden;text-overflow:ellipsis}',
1106
+ '.dt-tab-x{border:0;background:transparent;color:inherit;cursor:pointer;border-radius:4px;font-size:11px;line-height:1;padding:1px 3px;opacity:.6}',
1107
+ '.dt-tab-x:hover{opacity:1;color:var(--dsw-alias-state-error-primary,#dc2626)}',
1108
+ '.dt-new{border:1px solid var(--dsw-alias-border-l1,rgba(15,23,42,.14));background:transparent;color:var(--dsw-alias-label-secondary,#6b7280);border-radius:7px;padding:4px 8px;cursor:pointer;font:inherit;white-space:nowrap}',
1109
+ '.dt-new:hover{color:var(--dsw-alias-brand-primary,#4d6bfe);border-color:currentColor}',
1110
+ '.dt-meta{flex:0 0 auto;max-width:32%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px}',
1111
+ '.dt-notice{margin:6px 10px 0;padding:5px 8px;border-radius:6px;background:rgba(220,38,38,.1);color:var(--dsw-alias-state-error-primary,#dc2626);font-size:12px;word-break:break-word}',
1112
+ '.dt-notice.err{background:rgba(220,38,38,.1)}',
1113
+ '.dt-view{position:relative;flex:1 1 auto;min-height:0;overflow:auto;padding:8px 10px;cursor:text;background:var(--dsw-alias-bg-base,#fff)}',
1114
+ '.dt-screen{--dt-fg:var(--dsw-alias-label-primary,#1f2329);--dt-bg:var(--dsw-alias-bg-base,#fff);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;font-size:13px;line-height:1.32;white-space:pre;tab-size:8}',
1115
+ '.dt-row{height:1.32em;white-space:pre}',
1116
+ '.dt-cursor{outline:1px solid var(--dsw-alias-brand-primary,#4d6bfe);background:rgba(77,107,254,.14)}',
1117
+ '.dt-probe{position:absolute;left:-9999px;top:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;font-size:13px;line-height:1.32;white-space:pre}',
1118
+ '.dt-input{position:absolute;inset:0;width:100%;height:100%;opacity:0;border:0;resize:none;background:transparent;color:transparent;caret-color:transparent;font:inherit;padding:0;outline:none}',
1119
+ '.dt-input.off{pointer-events:none}',
1120
+ '.dt-new.on{color:var(--dsw-alias-brand-primary,#4d6bfe);border-color:currentColor}',
1121
+ '.dt-view.plain{cursor:text;user-select:text}',
1122
+ '.dt-view.plain .dt-screen{user-select:text}',
1123
+ '.dt-empty{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--dsw-alias-label-secondary,#6b7280)}',
1124
+ '.dt-empty-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2329)}',
1125
+ '.dt-empty-hint{font-size:12px}',
1126
+ '.dt-empty-btn{margin-top:4px;border:0;border-radius:8px;padding:6px 12px;background:var(--dsw-alias-brand-primary,#4d6bfe);color:#fff;font:inherit;font-weight:600;cursor:pointer}',
1127
+ '.dt-icon{position:relative;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;line-height:0;white-space:nowrap;overflow:visible}',
1128
+ '.dt-badge{position:absolute;top:-3px;right:-5px;display:block;min-width:13px;height:13px;padding:0 3px;border-radius:7px;background:var(--dsw-alias-brand-primary,#4d6bfe);color:#fff;font-size:9px;font-weight:700;line-height:13px;font-variant-numeric:tabular-nums;white-space:nowrap;text-align:center;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-base,#fff))}',
1129
+ '.dt-tab-input{width:130px;border:1px solid var(--dsw-alias-brand-primary,#4d6bfe);border-radius:5px;background:var(--dsw-alias-bg-base,#fff);color:inherit;font:inherit;padding:1px 4px;outline:none}',
1130
+ '.dt-dot{flex:0 0 auto;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-label-secondary,#9ca3af)}',
1131
+ '.dt-dot.on{background:var(--dsw-alias-state-success-primary,#16a34a)}',
1132
+ ].join('\n');
1133
+
1134
+ /** Parse a computed CSS colour into [r, g, b], for the luminance probe. */
1135
+ function parseColor(raw) {
1136
+ if (!raw) return null;
1137
+ const text = raw.trim();
1138
+ if (text[0] === '#') {
1139
+ if (text.length === 4) {
1140
+ return [parseInt(text[1] + text[1], 16), parseInt(text[2] + text[2], 16), parseInt(text[3] + text[3], 16)];
1141
+ }
1142
+ if (text.length >= 7) {
1143
+ return [parseInt(text.slice(1, 3), 16), parseInt(text.slice(3, 5), 16), parseInt(text.slice(5, 7), 16)];
1144
+ }
1145
+ return null;
1146
+ }
1147
+ const match = /rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i.exec(text);
1148
+ if (!match) return null;
1149
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
1150
+ }
1151
+
1152
+ /** Light or dark: the theme service is authoritative, the painted tokens are the fallback. */
1153
+ function detectScheme(ctx) {
1154
+ try {
1155
+ const theme = ctx && ctx.get ? ctx.get('theme') : null;
1156
+ const snapshot = theme && typeof theme.getTheme === 'function' ? theme.getTheme() : null;
1157
+ const scheme = snapshot && snapshot.active ? snapshot.active.colorScheme : null;
1158
+ if (scheme === 'light' || scheme === 'dark') return scheme;
1159
+ } catch (err) {
1160
+ /* fall through to the painted tokens */
1161
+ }
1162
+ try {
1163
+ const probe = document.querySelector('.dt-probe') || document.body || document.documentElement;
1164
+ const rgb = parseColor(window.getComputedStyle(probe).getPropertyValue('--dsw-alias-bg-base'));
1165
+ if (rgb) {
1166
+ const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
1167
+ return luminance > 0.55 ? 'light' : 'dark';
1168
+ }
1169
+ } catch (err) {
1170
+ /* keep the current scheme */
1171
+ }
1172
+ return runtime.scheme;
1173
+ }
1174
+
1175
+ function installTheme(ctx) {
1176
+ const sync = () => {
1177
+ const next = detectScheme(ctx);
1178
+ if (next === runtime.scheme) return;
1179
+ runtime.scheme = next;
1180
+ styleCache.clear();
1181
+ store.patch({ scheme: next });
1182
+ };
1183
+ runtime.syncScheme = sync;
1184
+ sync();
1185
+ try {
1186
+ ctx.effect(() => ctx.on('theme/change', sync));
1187
+ } catch (err) {
1188
+ /* theme events unavailable: the palette simply stays as first detected */
1189
+ }
1190
+ }
1191
+
1192
+ /**
1193
+ * Insert the package stylesheet once, for the whole plugin run. It must not
1194
+ * wait for a component to mount: the sidebar panel icon renders before (and
1195
+ * without) the terminal panel, and an unstyled count badge would wrap.
1196
+ */
1197
+ function installStyles(ctx) {
1198
+ const disposers = [];
1199
+ try {
1200
+ if (typeof styles !== 'undefined' && styles && typeof styles.insert === 'function') {
1201
+ disposers.push(styles.insert(CSS));
1202
+ }
1203
+ } catch (err) {
1204
+ /* fall back to a plain style element below */
1205
+ }
1206
+ if (!disposers.length) {
1207
+ try {
1208
+ const node = document.createElement('style');
1209
+ node.setAttribute('data-dsh-plugin', NS);
1210
+ node.textContent = CSS;
1211
+ document.head.appendChild(node);
1212
+ disposers.push(() => {
1213
+ if (node.parentNode) node.parentNode.removeChild(node);
1214
+ });
1215
+ } catch (err) {
1216
+ return;
1217
+ }
1218
+ }
1219
+ ctx.effect(() => () => {
1220
+ for (const dispose of disposers) {
1221
+ try {
1222
+ dispose();
1223
+ } catch (err) {
1224
+ /* already removed */
1225
+ }
1226
+ }
1227
+ });
1228
+ }
1229
+
1230
+ // -------------------------------------------------------------- install
1231
+
1232
+ function installLocale(ctx) {
1233
+ const locale = ctx.get('locale');
1234
+ if (!locale || typeof locale.register !== 'function' || typeof locale.getLocale !== 'function') return;
1235
+ runtime.locale = locale;
1236
+ const dictFor = (id) => (/^zh/i.test(id) ? DICTS.zh : DICTS.en);
1237
+ const done = new Set();
1238
+ const sync = () => {
1239
+ let snapshot;
1240
+ try {
1241
+ snapshot = locale.getLocale();
1242
+ } catch (err) {
1243
+ return;
1244
+ }
1245
+ const defs = new Map(((snapshot && snapshot.locales) || []).map((entry) => [entry.id, entry]));
1246
+ const seen = new Set();
1247
+ let id = snapshot && snapshot.active;
1248
+ while (id && !seen.has(id)) {
1249
+ seen.add(id);
1250
+ if (!done.has(id)) {
1251
+ try {
1252
+ locale.register(NS, id, dictFor(id));
1253
+ } catch (err) {
1254
+ /* already registered */
1255
+ }
1256
+ done.add(id);
1257
+ }
1258
+ const definition = defs.get(id);
1259
+ id = definition && definition.fallback ? definition.fallback : undefined;
1260
+ }
1261
+ };
1262
+ sync();
1263
+ if (typeof locale.subscribe === 'function') ctx.effect(() => locale.subscribe(sync));
1264
+ if (typeof locale.bind === 'function') {
1265
+ try {
1266
+ const bound = locale.bind(NS);
1267
+ if (typeof bound === 'function') {
1268
+ runtime.t = (key) => {
1269
+ try {
1270
+ const text = bound(key);
1271
+ return text === undefined || text === null ? key : String(text);
1272
+ } catch (err) {
1273
+ return key;
1274
+ }
1275
+ };
1276
+ }
1277
+ } catch (err) {
1278
+ /* keep the built-in dictionary */
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+ return {
1284
+ inject: ['slots'],
1285
+ apply(ctx) {
1286
+ runtime.ctx = ctx;
1287
+ installLocale(ctx);
1288
+ installStyles(ctx);
1289
+ installTheme(ctx);
1290
+ ctx.effect(() => {
1291
+ refresh();
1292
+ const timer = window.setInterval(refresh, POLL_MS);
1293
+ return () => window.clearInterval(timer);
1294
+ });
1295
+ ctx.slots.inject('main', () =>
1296
+ ctx.slots.register({ name: 'main', key: PANEL_KEY }, TerminalPanel),
1297
+ );
1298
+ ctx.slots.inject('sidebar.panellist', () =>
1299
+ ctx.slots.register(
1300
+ {
1301
+ name: 'sidebar.panellist',
1302
+ id: PANEL_KEY,
1303
+ order: 22,
1304
+ label: () => {
1305
+ const running = store.snapshot.items.filter((item) => item.status === 'running').length;
1306
+ return running ? runtime.t('title') + ' · ' + running : runtime.t('title');
1307
+ },
1308
+ },
1309
+ PanelIcon,
1310
+ ),
1311
+ );
1312
+ },
1313
+ };
1314
+ },
1315
+ });