termdock 1.4.55 → 1.4.57

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.
Files changed (70) hide show
  1. package/dist/client/assets/ModelPreview-Cc96uOV4.js +4108 -0
  2. package/dist/client/assets/{abnfDiagram-VRR7QNED-v9KOyvUA.js → abnfDiagram-VRR7QNED-DZYgxd_m.js} +1 -1
  3. package/dist/client/assets/{arc-Dokn57ft.js → arc-CUTAoWR1.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-B2_2J4-w.js → architectureDiagram-ZJ3FMSHR-CaRSesi9.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-677ZJIJ3-DFl0I-w9.js → blockDiagram-677ZJIJ3-D5XDdY80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-LMCZKHZV-Bo1Hy5ly.js → c4Diagram-LMCZKHZV-Bv99p2gv.js} +1 -1
  7. package/dist/client/assets/channel-BgCPMo_A.js +1 -0
  8. package/dist/client/assets/{chunk-2Q5K7J3B-DVTvlK-j.js → chunk-2Q5K7J3B-VVyNmkmL.js} +1 -1
  9. package/dist/client/assets/{chunk-32BRIVSS-NdC1yaOj.js → chunk-32BRIVSS-EAEC1KDa.js} +1 -1
  10. package/dist/client/assets/{chunk-5VM5RSS4-ZRKNZVGo.js → chunk-5VM5RSS4-DdF3_HPI.js} +1 -1
  11. package/dist/client/assets/{chunk-EX3LRPZG-CzhNQmEu.js → chunk-EX3LRPZG-CAE4jduk.js} +1 -1
  12. package/dist/client/assets/{chunk-JWPE2WC7-BL76uLj3.js → chunk-JWPE2WC7-BO8M7yBD.js} +1 -1
  13. package/dist/client/assets/{chunk-MOJQB5TN-ZNx7ZEmO.js → chunk-MOJQB5TN-Bx9n-q1Y.js} +1 -1
  14. package/dist/client/assets/{chunk-RYQCIY6F-xxcXIEwu.js → chunk-RYQCIY6F-DUMAVoqS.js} +1 -1
  15. package/dist/client/assets/{chunk-V7JOEXUC-Bg_lp0y3.js → chunk-V7JOEXUC-Df8pttJR.js} +1 -1
  16. package/dist/client/assets/{chunk-VR4S4FIN-C93CSXty.js → chunk-VR4S4FIN-B6B0MzBC.js} +1 -1
  17. package/dist/client/assets/{chunk-XXDRQBXY-DHn8Vn3c.js → chunk-XXDRQBXY-BMpgnm2D.js} +1 -1
  18. package/dist/client/assets/classDiagram-OUVF2IWQ-DXBrcRht.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-EOCWNBFH-DXBrcRht.js +1 -0
  20. package/dist/client/assets/{cose-bilkent-JH36ORCC-Baif6bSx.js → cose-bilkent-JH36ORCC-CLynu2qg.js} +1 -1
  21. package/dist/client/assets/{cynefin-VYW2F7L2-DryF4Xhm.js → cynefin-VYW2F7L2-BLva0hv1.js} +1 -1
  22. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-BdlUIn8W.js → cynefinDiagram-TSTJHNR4-Cu0GsUas.js} +1 -1
  23. package/dist/client/assets/{dagre-VKFMJZFB-C-D019df.js → dagre-VKFMJZFB-D4NGJ5mV.js} +1 -1
  24. package/dist/client/assets/{diagram-FQU43EPY-DgdPi3di.js → diagram-FQU43EPY-QcK_o6qr.js} +1 -1
  25. package/dist/client/assets/{diagram-G47NLZAW-CZ-lA51n.js → diagram-G47NLZAW-Dyg4THNX.js} +1 -1
  26. package/dist/client/assets/{diagram-NH7WQ7WH-A698GPm8.js → diagram-NH7WQ7WH-0uQxu1x8.js} +1 -1
  27. package/dist/client/assets/{diagram-OA4YK3LP-DR695wJg.js → diagram-OA4YK3LP-GiXoX0mz.js} +1 -1
  28. package/dist/client/assets/{diagram-WEI45ONY-jNiC3Mji.js → diagram-WEI45ONY-CUgxdG_h.js} +1 -1
  29. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-DV-zX_Gy.js → ebnfDiagram-CCIWWBDH-CXMOFJem.js} +1 -1
  30. package/dist/client/assets/{erDiagram-Q63AITRT-q8tX38xm.js → erDiagram-Q63AITRT-BuwqDEKI.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-23GEKE2U-ChsZYCRf.js → flowDiagram-23GEKE2U-4_4ENI5h.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-NO4QXBWP-BqhsmAM7.js → ganttDiagram-NO4QXBWP-CQrNj7GA.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-BnUGLA-M.js → gitGraphDiagram-IHSO6WYX-DwLebu8y.js} +1 -1
  34. package/dist/client/assets/index-B-ASvk_D.css +1 -0
  35. package/dist/client/assets/index-C_4nBgHt.js +438 -0
  36. package/dist/client/assets/{infoDiagram-FWYZ7A6U-DB2TpJYj.js → infoDiagram-FWYZ7A6U-CXxo6ay4.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-Bt3UPJD3.js → ishikawaDiagram-FXEZZL3T-Dm6F00o3.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-5HDEW3XC-DiW3ZF-j.js → journeyDiagram-5HDEW3XC-D0IivWuB.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-HUTT4EX6-DIqWSzdn.js → kanban-definition-HUTT4EX6-DSUdfCUg.js} +1 -1
  40. package/dist/client/assets/{linear-DseQ2yoz.js → linear-80DX1B10.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-p5MzsEWg.js → mermaid.core-DwdnFhTj.js} +4 -4
  42. package/dist/client/assets/{mindmap-definition-LN4V7U3C-Bcr8vE-x.js → mindmap-definition-LN4V7U3C-DBRmYihp.js} +1 -1
  43. package/dist/client/assets/{pegDiagram-2B236MQR-BIgxPBoQ.js → pegDiagram-2B236MQR-Cn6RnMqR.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-ENE6RG2P-CY4Lg0At.js → pieDiagram-ENE6RG2P-DyXQNB8P.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CIBEQHQ4.js → quadrantDiagram-ABIIQ3AL-IdtNQ0W_.js} +1 -1
  46. package/dist/client/assets/{railroadDiagram-RFXS5EU6-Caq1SbrK.js → railroadDiagram-RFXS5EU6-DcQPaOmc.js} +1 -1
  47. package/dist/client/assets/{requirementDiagram-TGXJPOKE-B6ZlT1wV.js → requirementDiagram-TGXJPOKE-DSFn8fnf.js} +1 -1
  48. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-B1f4JOr-.js → sankeyDiagram-HTMAVEWB-CxU-S3Dj.js} +1 -1
  49. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-CB12eCRf.js → sequenceDiagram-DBY2YBRQ-K6Q-rsSo.js} +1 -1
  50. package/dist/client/assets/{sizeCapture-X5ZJPWSS-BTKjMjPw.js → sizeCapture-X5ZJPWSS-D4Bt9ZVt.js} +1 -1
  51. package/dist/client/assets/{stateDiagram-2N3HPSRC-BwoS0_yj.js → stateDiagram-2N3HPSRC-CINQ6i8C.js} +1 -1
  52. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-DM_9UWsD.js +1 -0
  53. package/dist/client/assets/{swimlanes-5IMT3BWC-CyY7nmOK.js → swimlanes-5IMT3BWC-CIF_FKrE.js} +2 -2
  54. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CI76vK0H.js +8 -0
  55. package/dist/client/assets/{timeline-definition-FHXFAJF6-DuJhrZL3.js → timeline-definition-FHXFAJF6-zvFZjTxi.js} +1 -1
  56. package/dist/client/assets/{vennDiagram-L72KCM5P-QY6zAcOu.js → vennDiagram-L72KCM5P-X7O88mEN.js} +1 -1
  57. package/dist/client/assets/{wardleyDiagram-EHGQE667-tCRSKFnO.js → wardleyDiagram-EHGQE667-CMgPIEJC.js} +1 -1
  58. package/dist/client/assets/{xychartDiagram-FW5EYKEG-hrmaVtk8.js → xychartDiagram-FW5EYKEG-C6DhA7s3.js} +1 -1
  59. package/dist/client/index.html +2 -2
  60. package/dist/client/sw.js +1 -1
  61. package/dist/server/routes/filesystem.js +402 -35
  62. package/package.json +3 -1
  63. package/runtime-manifest.json +3 -3
  64. package/dist/client/assets/channel-CMy1yWmT.js +0 -1
  65. package/dist/client/assets/classDiagram-OUVF2IWQ-CWL2Kj7x.js +0 -1
  66. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CWL2Kj7x.js +0 -1
  67. package/dist/client/assets/index-7oItXRxL.js +0 -436
  68. package/dist/client/assets/index-BxDTdORQ.css +0 -1
  69. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-CPTGG4BJ.js +0 -1
  70. package/dist/client/assets/swimlanesDiagram-G3AALYLV-BsEBNolb.js +0 -8
@@ -13,6 +13,8 @@ import { inspectBinaryFile } from '../utils/binaryFile.js';
13
13
  const router = Router();
14
14
  const MAX_FILE_SIZE = 1024 * 1024; // 1MB
15
15
  const MAX_IMAGE_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
16
+ const MAX_HTML_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB
17
+ const MAX_VIDEO_PREVIEW_SIZE = 4 * 1024 * 1024 * 1024; // 4GB
16
18
  const MAX_DOWNLOAD_SIZE = 200 * 1024 * 1024; // 200MB
17
19
  const MAX_UPLOAD_SIZE = 100 * 1024 * 1024; // 100MB per file
18
20
  const MAX_UPLOAD_FILES = 50; // max 50 files per upload
@@ -44,6 +46,128 @@ const RESTORE_CONFIRM_PHRASES = new Set(['丢弃改动', 'discard changes']);
44
46
  const FS_IO_LOG_NAME = 'fs-io.log';
45
47
  const activeDiffSlots = new Map();
46
48
  const activeIoSlots = new Map();
49
+ const sharedWatchRegistry = new Map();
50
+ const SHARED_WATCH_EVICT_DELAY_MS = 60_000;
51
+ function sharedWatchCallback(rootPath, entry) {
52
+ return (error, events) => {
53
+ if (entry.failure)
54
+ return;
55
+ if (error) {
56
+ entry.failure = error.message || 'watch-error';
57
+ const reason = entry.failure;
58
+ for (const client of entry.clients) {
59
+ if (!client.closed)
60
+ client.enqueue({ type: 'rescan-required', path: rootPath, reason });
61
+ }
62
+ return;
63
+ }
64
+ for (const event of events) {
65
+ const changedPath = path.resolve(event.path);
66
+ if (!isPathInside(rootPath, changedPath) || isIgnoredWatchPath(rootPath, changedPath))
67
+ continue;
68
+ if (event.type === 'delete') {
69
+ for (const client of entry.clients) {
70
+ if (!client.closed)
71
+ client.enqueue({ type: 'deleted', path: changedPath });
72
+ }
73
+ continue;
74
+ }
75
+ fs.promises.lstat(changedPath)
76
+ .then((stat) => {
77
+ const payload = {
78
+ type: event.type === 'create' ? 'created' : 'updated',
79
+ path: changedPath,
80
+ entry: toFileEntry(changedPath, stat),
81
+ };
82
+ for (const client of entry.clients) {
83
+ if (!client.closed)
84
+ client.enqueue(payload);
85
+ }
86
+ })
87
+ .catch(() => {
88
+ for (const client of entry.clients) {
89
+ if (!client.closed)
90
+ client.enqueue({ type: 'deleted', path: changedPath });
91
+ }
92
+ });
93
+ }
94
+ };
95
+ }
96
+ async function acquireSharedWatch(rootPath, client) {
97
+ let entry = sharedWatchRegistry.get(rootPath);
98
+ if (!entry) {
99
+ entry = {
100
+ refs: 0,
101
+ subscription: null,
102
+ failure: null,
103
+ clients: new Set(),
104
+ inFlight: null,
105
+ evictTimer: null,
106
+ };
107
+ sharedWatchRegistry.set(rootPath, entry);
108
+ }
109
+ if (entry.evictTimer) {
110
+ clearTimeout(entry.evictTimer);
111
+ entry.evictTimer = null;
112
+ }
113
+ entry.refs += 1;
114
+ entry.clients.add(client);
115
+ if (entry.failure)
116
+ return null;
117
+ if (entry.subscription)
118
+ return rootPath;
119
+ if (!entry.inFlight) {
120
+ entry.inFlight = watcher.subscribe(rootPath, sharedWatchCallback(rootPath, entry), {
121
+ ignore: Array.from(WATCH_IGNORED_NAMES).map((name) => `**/${name}/**`),
122
+ })
123
+ .then((subscription) => {
124
+ const current = sharedWatchRegistry.get(rootPath);
125
+ if (!current || current.refs === 0) {
126
+ // Nobody is watching anymore (client disconnected mid-scan): free
127
+ // the freshly built subscription instead of leaking its DirTree.
128
+ void subscription.unsubscribe().catch(() => undefined);
129
+ return null;
130
+ }
131
+ current.subscription = subscription;
132
+ return subscription;
133
+ })
134
+ .catch((error) => {
135
+ const current = sharedWatchRegistry.get(rootPath);
136
+ if (current)
137
+ current.failure = error instanceof Error ? error.message : String(error);
138
+ return null;
139
+ })
140
+ .finally(() => {
141
+ const current = sharedWatchRegistry.get(rootPath);
142
+ if (current)
143
+ current.inFlight = null;
144
+ });
145
+ }
146
+ await entry.inFlight;
147
+ const current = sharedWatchRegistry.get(rootPath);
148
+ return current && !current.failure ? rootPath : null;
149
+ }
150
+ function releaseSharedWatch(rootPath, client) {
151
+ client.closed = true;
152
+ const entry = sharedWatchRegistry.get(rootPath);
153
+ if (!entry)
154
+ return;
155
+ entry.clients.delete(client);
156
+ entry.refs -= 1;
157
+ if (entry.refs > 0)
158
+ return;
159
+ if (entry.evictTimer)
160
+ clearTimeout(entry.evictTimer);
161
+ entry.evictTimer = setTimeout(() => {
162
+ const current = sharedWatchRegistry.get(rootPath);
163
+ if (!current || current.refs > 0)
164
+ return;
165
+ sharedWatchRegistry.delete(rootPath);
166
+ void current.subscription?.unsubscribe().catch(() => undefined);
167
+ current.subscription = null;
168
+ }, SHARED_WATCH_EVICT_DELAY_MS);
169
+ entry.evictTimer.unref?.();
170
+ }
47
171
  const untrackedJobs = new Map();
48
172
  const nestedGitRootsCache = new Map();
49
173
  const gitActionJobs = new Map();
@@ -287,6 +411,88 @@ const IMAGE_MIME_BY_EXT = {
287
411
  function getImageMimeType(filePath) {
288
412
  return IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
289
413
  }
414
+ // Video files play in the right sidebar through the /video route, which
415
+ // streams with HTTP Range support so the browser <video> element can seek
416
+ // without loading the whole file into memory. Keep the map to formats the
417
+ // browser can actually decode (mkv/avi intentionally excluded).
418
+ const VIDEO_MIME_BY_EXT = {
419
+ '.mp4': 'video/mp4',
420
+ '.m4v': 'video/x-m4v',
421
+ '.webm': 'video/webm',
422
+ '.mov': 'video/quicktime',
423
+ '.ogv': 'video/ogg',
424
+ '.mpeg': 'video/mpeg',
425
+ '.mpg': 'video/mpeg',
426
+ };
427
+ function getVideoMimeType(filePath) {
428
+ return VIDEO_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
429
+ }
430
+ // Single-range `Range: bytes=start-end` / `bytes=-N` parsing. Malformed or
431
+ // multi-range headers fall back to a full 200 response, which browsers accept
432
+ // (they just cannot seek until the file is buffered).
433
+ function parseVideoRange(rangeHeader, total) {
434
+ if (!rangeHeader || total <= 0)
435
+ return { kind: 'full' };
436
+ const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
437
+ if (!match)
438
+ return { kind: 'full' };
439
+ const rawStart = match[1];
440
+ const rawEnd = match[2];
441
+ if (rawStart === '' && rawEnd === '')
442
+ return { kind: 'full' };
443
+ if (rawStart === '') {
444
+ const suffixLength = Number(rawEnd);
445
+ if (!Number.isInteger(suffixLength) || suffixLength <= 0)
446
+ return { kind: 'unsatisfiable' };
447
+ return { kind: 'range', start: Math.max(0, total - suffixLength), end: total - 1 };
448
+ }
449
+ const start = Number(rawStart);
450
+ if (!Number.isInteger(start) || start < 0 || start >= total)
451
+ return { kind: 'unsatisfiable' };
452
+ const end = rawEnd === '' ? total - 1 : Math.min(Number(rawEnd), total - 1);
453
+ if (!Number.isInteger(end) || end < start)
454
+ return { kind: 'unsatisfiable' };
455
+ return { kind: 'range', start, end };
456
+ }
457
+ // MIME types for the right-sidebar HTML preview route. The route mirrors the
458
+ // absolute filesystem path in the URL, so every relative resource referenced
459
+ // by a previewed document (css/js/fonts/images/...) is served by the same
460
+ // route with a type the browser can render.
461
+ const HTML_PREVIEW_MIME_BY_EXT = {
462
+ '.html': 'text/html; charset=utf-8',
463
+ '.htm': 'text/html; charset=utf-8',
464
+ '.css': 'text/css; charset=utf-8',
465
+ '.js': 'text/javascript; charset=utf-8',
466
+ '.mjs': 'text/javascript; charset=utf-8',
467
+ '.json': 'application/json; charset=utf-8',
468
+ '.map': 'application/json; charset=utf-8',
469
+ '.txt': 'text/plain; charset=utf-8',
470
+ '.md': 'text/markdown; charset=utf-8',
471
+ '.xml': 'application/xml; charset=utf-8',
472
+ '.svg': 'image/svg+xml',
473
+ '.png': 'image/png',
474
+ '.jpg': 'image/jpeg',
475
+ '.jpeg': 'image/jpeg',
476
+ '.gif': 'image/gif',
477
+ '.webp': 'image/webp',
478
+ '.avif': 'image/avif',
479
+ '.ico': 'image/x-icon',
480
+ '.bmp': 'image/bmp',
481
+ '.woff': 'font/woff',
482
+ '.woff2': 'font/woff2',
483
+ '.ttf': 'font/ttf',
484
+ '.otf': 'font/otf',
485
+ '.eot': 'application/vnd.ms-fontobject',
486
+ '.wasm': 'application/wasm',
487
+ '.mp4': 'video/mp4',
488
+ '.webm': 'video/webm',
489
+ '.ogg': 'audio/ogg',
490
+ '.mp3': 'audio/mpeg',
491
+ '.wav': 'audio/wav',
492
+ };
493
+ function getHtmlPreviewMimeType(filePath) {
494
+ return HTML_PREVIEW_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
495
+ }
290
496
  function toContentDispositionFilename(name) {
291
497
  return name.replace(/["\\\r\n]/g, '_').replace(/[^\x20-\x7E]/g, '_');
292
498
  }
@@ -2187,7 +2393,7 @@ router.get('/watch', async (req, res) => {
2187
2393
  let closed = false;
2188
2394
  let pending = [];
2189
2395
  let flushTimer = null;
2190
- const subscriptions = [];
2396
+ const attachedRoots = new Map();
2191
2397
  const writeEvent = (type, payload) => {
2192
2398
  if (closed || res.destroyed)
2193
2399
  return;
@@ -2216,46 +2422,26 @@ router.get('/watch', async (req, res) => {
2216
2422
  }
2217
2423
  };
2218
2424
  writeEvent('ready', { roots });
2219
- for (const rootPath of roots) {
2220
- try {
2221
- const subscription = await watcher.subscribe(rootPath, (error, events) => {
2222
- if (closed)
2223
- return;
2224
- if (error) {
2225
- enqueue({ type: 'rescan-required', path: rootPath, reason: error.message || 'watch-error' });
2226
- return;
2227
- }
2228
- for (const event of events) {
2229
- const changedPath = path.resolve(event.path);
2230
- if (!isPathInside(rootPath, changedPath) || isIgnoredWatchPath(rootPath, changedPath))
2231
- continue;
2232
- if (event.type === 'delete') {
2233
- enqueue({ type: 'deleted', path: changedPath });
2234
- continue;
2235
- }
2236
- fs.promises.lstat(changedPath)
2237
- .then((stat) => {
2238
- enqueue({ type: event.type === 'create' ? 'created' : 'updated', path: changedPath, entry: toFileEntry(changedPath, stat) });
2239
- })
2240
- .catch(() => {
2241
- enqueue({ type: 'deleted', path: changedPath });
2242
- });
2243
- }
2244
- }, {
2245
- ignore: Array.from(WATCH_IGNORED_NAMES).map((name) => `**/${name}/**`),
2246
- });
2247
- subscriptions.push(subscription);
2248
- }
2249
- catch (error) {
2250
- enqueue({ type: 'rescan-required', path: rootPath, reason: error instanceof Error ? error.message : 'watch-unavailable' });
2425
+ const attachRoot = async (rootPath) => {
2426
+ const client = { closed: false, enqueue };
2427
+ attachedRoots.set(rootPath, client);
2428
+ const ok = await acquireSharedWatch(rootPath, client);
2429
+ if (closed)
2430
+ return;
2431
+ if (!ok) {
2432
+ const reason = sharedWatchRegistry.get(rootPath)?.failure ?? 'watch-unavailable';
2433
+ enqueue({ type: 'rescan-required', path: rootPath, reason });
2251
2434
  }
2435
+ };
2436
+ for (const rootPath of roots) {
2437
+ await attachRoot(rootPath);
2252
2438
  }
2253
2439
  req.on('close', () => {
2254
2440
  closed = true;
2255
2441
  if (flushTimer)
2256
2442
  clearTimeout(flushTimer);
2257
- for (const subscription of subscriptions) {
2258
- void subscription.unsubscribe().catch(() => undefined);
2443
+ for (const [rootPath, client] of attachedRoots) {
2444
+ releaseSharedWatch(rootPath, client);
2259
2445
  }
2260
2446
  });
2261
2447
  });
@@ -2642,6 +2828,187 @@ router.get('/blob', async (req, res) => {
2642
2828
  res.status(status).json({ ...payload, code });
2643
2829
  }
2644
2830
  });
2831
+ // Serve video files for the right-sidebar preview. The route streams with
2832
+ // HTTP Range support, so <video> playback starts quickly and seeking works
2833
+ // without buffering the whole file in the browser or server memory.
2834
+ router.get('/video', async (req, res) => {
2835
+ const requestId = ++fsIoRequestSeq;
2836
+ const startedAt = Date.now();
2837
+ const requestedPath = req.query.path;
2838
+ const action = getRequestAction(req, 'view_file');
2839
+ const requestSlotId = typeof req.query.requestSlotId === 'string' ? req.query.requestSlotId : undefined;
2840
+ const controller = new AbortController();
2841
+ const abortRequest = () => {
2842
+ if (!res.writableEnded)
2843
+ controller.abort(new SupersededRequestError('fs.video'));
2844
+ };
2845
+ req.on('aborted', abortRequest);
2846
+ res.on('close', abortRequest);
2847
+ registerIoSlot({ requestId, op: 'fs.video', action, slotId: requestSlotId, controller, path: requestedPath });
2848
+ let logged = false;
2849
+ const logOnce = (status, entry = {}) => {
2850
+ if (logged)
2851
+ return;
2852
+ logged = true;
2853
+ releaseIoSlot(requestSlotId, requestId);
2854
+ logFsIo({
2855
+ action,
2856
+ op: 'fs.video',
2857
+ id: requestId,
2858
+ startedAt,
2859
+ status,
2860
+ path: requestedPath,
2861
+ ...entry,
2862
+ extra: { ...(entry.extra ?? {}), requestSlotId },
2863
+ });
2864
+ };
2865
+ try {
2866
+ if (!requestedPath) {
2867
+ res.status(400).json({ error: 'Missing path parameter' });
2868
+ return;
2869
+ }
2870
+ const { resolvedPath, stat, mimeType } = await withTimeout((async () => {
2871
+ const resolvedPath = await pathValidator.validatePathAsync(requestedPath);
2872
+ throwIfAborted(controller.signal, 'fs.video');
2873
+ const stat = await fs.promises.stat(resolvedPath);
2874
+ throwIfAborted(controller.signal, 'fs.video');
2875
+ if (!stat.isFile()) {
2876
+ throw new Error('Path is not a file');
2877
+ }
2878
+ const mimeType = getVideoMimeType(resolvedPath);
2879
+ if (!mimeType) {
2880
+ const error = new Error('Unsupported video type');
2881
+ error.status = 415;
2882
+ throw error;
2883
+ }
2884
+ if (stat.size > MAX_VIDEO_PREVIEW_SIZE) {
2885
+ const error = new Error('Video is too large to preview');
2886
+ error.code = 'VIDEO_TOO_LARGE';
2887
+ error.status = 413;
2888
+ throw error;
2889
+ }
2890
+ return { resolvedPath, stat, mimeType };
2891
+ })(), FS_ROUTE_TIMEOUT_MS, 'Video preview took too long. The file may be on slow storage or currently blocked by another process.', 'FS_VIDEO_TIMEOUT');
2892
+ const total = stat.size;
2893
+ const range = parseVideoRange(typeof req.headers.range === 'string' ? req.headers.range : undefined, total);
2894
+ res.setHeader('Accept-Ranges', 'bytes');
2895
+ res.setHeader('Content-Type', mimeType);
2896
+ res.setHeader('Last-Modified', stat.mtime.toUTCString());
2897
+ res.setHeader('Cache-Control', 'no-store');
2898
+ res.setHeader('X-Content-Type-Options', 'nosniff');
2899
+ res.setHeader('Content-Disposition', buildContentDisposition('inline', path.basename(resolvedPath)));
2900
+ if (req.method === 'HEAD') {
2901
+ res.setHeader('Content-Length', total.toString());
2902
+ if (range.kind === 'range')
2903
+ res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${total}`);
2904
+ res.end();
2905
+ logOnce('ok', { path: resolvedPath, bytes: total, extra: { mimeType } });
2906
+ return;
2907
+ }
2908
+ if (range.kind === 'unsatisfiable') {
2909
+ res.status(416);
2910
+ res.setHeader('Content-Range', `bytes */${total}`);
2911
+ res.end();
2912
+ logOnce('ok', { path: resolvedPath, bytes: 0, extra: { mimeType, status: 416 } });
2913
+ return;
2914
+ }
2915
+ if (range.kind === 'range') {
2916
+ res.status(206);
2917
+ res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${total}`);
2918
+ res.setHeader('Content-Length', String(range.end - range.start + 1));
2919
+ }
2920
+ else {
2921
+ res.setHeader('Content-Length', total.toString());
2922
+ }
2923
+ const stream = fs.createReadStream(resolvedPath, range.kind === 'range' ? { start: range.start, end: range.end } : undefined);
2924
+ controller.signal.addEventListener('abort', () => stream.destroy(controller.signal.reason instanceof Error ? controller.signal.reason : undefined), { once: true });
2925
+ const bytesServed = range.kind === 'range' ? range.end - range.start + 1 : total;
2926
+ res.on('finish', () => logOnce('ok', { path: resolvedPath, bytes: bytesServed, extra: { mimeType } }));
2927
+ res.on('close', () => {
2928
+ if (!res.writableEnded)
2929
+ logOnce('error', { path: resolvedPath, bytes: bytesServed, code: 'CLIENT_CLOSED', error: 'Client closed video preview request' });
2930
+ });
2931
+ stream.on('error', (error) => {
2932
+ logOnce('error', { path: resolvedPath, code: 'FS_VIDEO_STREAM_ERROR', error: error instanceof Error ? error.message : 'Failed to read video' });
2933
+ if (!res.headersSent) {
2934
+ const message = error instanceof Error ? error.message : 'Failed to read video';
2935
+ res.status(500).json({ error: message });
2936
+ return;
2937
+ }
2938
+ res.destroy(error instanceof Error ? error : undefined);
2939
+ });
2940
+ stream.pipe(res);
2941
+ }
2942
+ catch (error) {
2943
+ const payload = getErrorPayload(error);
2944
+ const status = error instanceof OperationTimeoutError
2945
+ ? 504
2946
+ : typeof error?.status === 'number'
2947
+ ? error.status
2948
+ : 403;
2949
+ const code = payload.code ?? (typeof error?.code === 'string' ? error.code : undefined);
2950
+ logOnce('error', { code, error: payload.error });
2951
+ res.status(status).json({ ...payload, code });
2952
+ }
2953
+ });
2954
+ // Serve workspace files for the right-sidebar HTML preview. The URL path
2955
+ // mirrors the absolute filesystem path (e.g. .../preview/home/user/proj/
2956
+ // index.html), so relative css/js/image references inside a document resolve
2957
+ // to the file's own directory without any content rewriting — the document
2958
+ // URL itself acts as the <base>. The frontend renders this in a sandboxed
2959
+ // iframe (no allow-same-origin), so previewed scripts never gain the termdock
2960
+ // origin.
2961
+ router.get('/preview/*path', async (req, res) => {
2962
+ try {
2963
+ const rawSegments = req.params.path;
2964
+ const segments = Array.isArray(rawSegments) ? rawSegments : rawSegments ? [rawSegments] : [];
2965
+ const requestedPath = `/${segments.join('/')}`;
2966
+ if (!requestedPath || requestedPath === '/') {
2967
+ res.status(400).json({ error: 'Missing path parameter' });
2968
+ return;
2969
+ }
2970
+ const resolvedPath = await pathValidator.validatePathAsync(requestedPath);
2971
+ const stat = await fs.promises.stat(resolvedPath);
2972
+ if (!stat.isFile()) {
2973
+ res.status(400).json({ error: 'Path is not a file' });
2974
+ return;
2975
+ }
2976
+ const mimeType = getHtmlPreviewMimeType(resolvedPath);
2977
+ if (!mimeType) {
2978
+ res.status(415).json({ error: 'Unsupported file type for preview' });
2979
+ return;
2980
+ }
2981
+ if (stat.size > MAX_HTML_PREVIEW_SIZE) {
2982
+ res.status(413).json({
2983
+ error: 'File is too large to preview',
2984
+ code: 'FILE_TOO_LARGE',
2985
+ size: stat.size,
2986
+ maxSize: MAX_HTML_PREVIEW_SIZE,
2987
+ });
2988
+ return;
2989
+ }
2990
+ res.setHeader('Content-Type', mimeType);
2991
+ res.setHeader('Content-Length', stat.size.toString());
2992
+ res.setHeader('Last-Modified', stat.mtime.toUTCString());
2993
+ res.setHeader('Cache-Control', 'no-store');
2994
+ res.setHeader('X-Content-Type-Options', 'nosniff');
2995
+ res.setHeader('Content-Disposition', buildContentDisposition('inline', path.basename(resolvedPath)));
2996
+ const stream = fs.createReadStream(resolvedPath);
2997
+ stream.on('error', (error) => {
2998
+ if (!res.headersSent) {
2999
+ const message = error instanceof Error ? error.message : 'Failed to serve file';
3000
+ res.status(500).json({ error: message });
3001
+ return;
3002
+ }
3003
+ res.destroy(error instanceof Error ? error : undefined);
3004
+ });
3005
+ stream.pipe(res);
3006
+ }
3007
+ catch (error) {
3008
+ const message = error instanceof Error ? error.message : 'Unknown error';
3009
+ res.status(403).json({ error: message });
3010
+ }
3011
+ });
2645
3012
  // Download any file as an attachment. Works in both PWA and normal browser
2646
3013
  // contexts — the frontend fetches this as a blob and either pipes it through
2647
3014
  // the File System Access API (showSaveFilePicker, desktop PWA/Chromium) or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termdock",
3
- "version": "1.4.55",
3
+ "version": "1.4.57",
4
4
  "description": "A complete web-based terminal application with modern UI",
5
5
  "type": "module",
6
6
  "main": ".desktop-dist/main.js",
@@ -83,6 +83,7 @@
83
83
  "refractor": "^2.10.1",
84
84
  "swiper": "^12.1.3",
85
85
  "tailwind-merge": "^2.2.0",
86
+ "three": "^0.185.1",
86
87
  "uuid": "^13.0.0",
87
88
  "web-push": "^3.6.7",
88
89
  "ws": "^8.21.0",
@@ -99,6 +100,7 @@
99
100
  "@types/node": "^20.11.0",
100
101
  "@types/react": "^18.2.0",
101
102
  "@types/react-dom": "^18.2.0",
103
+ "@types/three": "^0.185.3",
102
104
  "@types/uuid": "^10.0.0",
103
105
  "@types/web-push": "^3.6.4",
104
106
  "@types/ws": "^8.18.1",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "termdock",
4
- "version": "1.4.55",
4
+ "version": "1.4.57",
5
5
  "runtimeProtocolVersion": 1,
6
6
  "minimumDesktopVersion": "1.4.46",
7
7
  "nodeMajor": 22,
8
- "dependencyHash": "sha256-aUokhQIdbogiFjsYarTRvcQ40Rva3j5CIot/0H1Lyl4=",
9
- "serverBundleHash": "sha256-+mlh3Ar6V1IOLEJ5l39G2XsUQ+TOgtvmKInjzinHPsA=",
8
+ "dependencyHash": "sha256-HxC6p3b4kFesefw+vmNYJtPtdIKJaZR1jFNb7DRJqjU=",
9
+ "serverBundleHash": "sha256-J9kh6qO+A3xC2pDnOVTjabHIwJicckHS3zzeXwAqheM=",
10
10
  "entrypoint": "dist/server/cli.js",
11
11
  "clientEntrypoint": "dist/client/index.html"
12
12
  }
@@ -1 +0,0 @@
1
- import{U as a,C as n}from"./mermaid.core-p5MzsEWg.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-Bg_lp0y3.js";import{b as i}from"./mermaid.core-p5MzsEWg.js";import"./chunk-5VM5RSS4-ZRKNZVGo.js";import"./chunk-XXDRQBXY-DHn8Vn3c.js";import"./chunk-VR4S4FIN-C93CSXty.js";import"./chunk-32BRIVSS-NdC1yaOj.js";import"./purify.es-CC4Brkr_.js";import"./index-7oItXRxL.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};
@@ -1 +0,0 @@
1
- import{s as a,a as s,c as t,C as e}from"./chunk-V7JOEXUC-Bg_lp0y3.js";import{b as i}from"./mermaid.core-p5MzsEWg.js";import"./chunk-5VM5RSS4-ZRKNZVGo.js";import"./chunk-XXDRQBXY-DHn8Vn3c.js";import"./chunk-VR4S4FIN-C93CSXty.js";import"./chunk-32BRIVSS-NdC1yaOj.js";import"./purify.es-CC4Brkr_.js";import"./index-7oItXRxL.js";import"./react-XDCjal2T.js";import"./dnd-BCFAPC0L.js";import"./terminal-C7X98Amf.js";var g={parser:t,get db(){return new e},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{g as diagram};