dominds 1.2.7 → 1.3.1

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 (104) hide show
  1. package/dist/docs/context-health.md +17 -5
  2. package/dist/llm/defaults.yaml +9 -0
  3. package/dist/minds/system-prompt-parts.js +4 -4
  4. package/dist/server/api-routes.js +577 -80
  5. package/dist/shared/i18n/driver-messages.js +14 -12
  6. package/dist/static/assets/{_basePickBy-BMCtwrV7.js → _basePickBy-CmziIRM9.js} +3 -3
  7. package/dist/static/assets/{_basePickBy-BMCtwrV7.js.map → _basePickBy-CmziIRM9.js.map} +1 -1
  8. package/dist/static/assets/{_baseUniq-BuyCgJiA.js → _baseUniq-CgDZxcD0.js} +2 -2
  9. package/dist/static/assets/{_baseUniq-BuyCgJiA.js.map → _baseUniq-CgDZxcD0.js.map} +1 -1
  10. package/dist/static/assets/{arc-BDuN8lwA.js → arc-Df9rjNNk.js} +2 -2
  11. package/dist/static/assets/{arc-BDuN8lwA.js.map → arc-Df9rjNNk.js.map} +1 -1
  12. package/dist/static/assets/{architectureDiagram-VXUJARFQ-C-ekqGAD.js → architectureDiagram-VXUJARFQ-Bif8topC.js} +7 -7
  13. package/dist/static/assets/{architectureDiagram-VXUJARFQ-C-ekqGAD.js.map → architectureDiagram-VXUJARFQ-Bif8topC.js.map} +1 -1
  14. package/dist/static/assets/{blockDiagram-VD42YOAC-CgQiNuuQ.js → blockDiagram-VD42YOAC-D9Egoflr.js} +7 -7
  15. package/dist/static/assets/{blockDiagram-VD42YOAC-CgQiNuuQ.js.map → blockDiagram-VD42YOAC-D9Egoflr.js.map} +1 -1
  16. package/dist/static/assets/{c4Diagram-YG6GDRKO-DONC39q-.js → c4Diagram-YG6GDRKO-DBf1NeBf.js} +3 -3
  17. package/dist/static/assets/{c4Diagram-YG6GDRKO-DONC39q-.js.map → c4Diagram-YG6GDRKO-DBf1NeBf.js.map} +1 -1
  18. package/dist/static/assets/{channel-CJTFwXIG.js → channel-Dc34yAJl.js} +2 -2
  19. package/dist/static/assets/{channel-CJTFwXIG.js.map → channel-Dc34yAJl.js.map} +1 -1
  20. package/dist/static/assets/{chunk-4BX2VUAB-NaIy4uLJ.js → chunk-4BX2VUAB-B65G1dJI.js} +2 -2
  21. package/dist/static/assets/{chunk-4BX2VUAB-NaIy4uLJ.js.map → chunk-4BX2VUAB-B65G1dJI.js.map} +1 -1
  22. package/dist/static/assets/{chunk-55IACEB6-JUKI_Ayx.js → chunk-55IACEB6-CSDEOGl2.js} +2 -2
  23. package/dist/static/assets/{chunk-55IACEB6-JUKI_Ayx.js.map → chunk-55IACEB6-CSDEOGl2.js.map} +1 -1
  24. package/dist/static/assets/{chunk-B4BG7PRW-dIswFJDn.js → chunk-B4BG7PRW-Cb6W3QWJ.js} +5 -5
  25. package/dist/static/assets/{chunk-B4BG7PRW-dIswFJDn.js.map → chunk-B4BG7PRW-Cb6W3QWJ.js.map} +1 -1
  26. package/dist/static/assets/{chunk-DI55MBZ5-DU2b_N30.js → chunk-DI55MBZ5-ZAJWeVWH.js} +4 -4
  27. package/dist/static/assets/{chunk-DI55MBZ5-DU2b_N30.js.map → chunk-DI55MBZ5-ZAJWeVWH.js.map} +1 -1
  28. package/dist/static/assets/{chunk-FMBD7UC4-BgExcScw.js → chunk-FMBD7UC4-DiwRlImb.js} +2 -2
  29. package/dist/static/assets/{chunk-FMBD7UC4-BgExcScw.js.map → chunk-FMBD7UC4-DiwRlImb.js.map} +1 -1
  30. package/dist/static/assets/{chunk-QN33PNHL-bitxyqh7.js → chunk-QN33PNHL-wilj7fb5.js} +2 -2
  31. package/dist/static/assets/{chunk-QN33PNHL-bitxyqh7.js.map → chunk-QN33PNHL-wilj7fb5.js.map} +1 -1
  32. package/dist/static/assets/{chunk-QZHKN3VN-Cor8u7DT.js → chunk-QZHKN3VN-DGmviJfR.js} +2 -2
  33. package/dist/static/assets/{chunk-QZHKN3VN-Cor8u7DT.js.map → chunk-QZHKN3VN-DGmviJfR.js.map} +1 -1
  34. package/dist/static/assets/{chunk-TZMSLE5B-Aceoxav_.js → chunk-TZMSLE5B-Nm5wTXa_.js} +2 -2
  35. package/dist/static/assets/{chunk-TZMSLE5B-Aceoxav_.js.map → chunk-TZMSLE5B-Nm5wTXa_.js.map} +1 -1
  36. package/dist/static/assets/{classDiagram-2ON5EDUG-D1Q6a8Hg.js → classDiagram-2ON5EDUG-BvUbXD6H.js} +6 -6
  37. package/dist/static/assets/{classDiagram-2ON5EDUG-D1Q6a8Hg.js.map → classDiagram-2ON5EDUG-BvUbXD6H.js.map} +1 -1
  38. package/dist/static/assets/{classDiagram-v2-WZHVMYZB-D1Q6a8Hg.js → classDiagram-v2-WZHVMYZB-BvUbXD6H.js} +6 -6
  39. package/dist/static/assets/{classDiagram-v2-WZHVMYZB-D1Q6a8Hg.js.map → classDiagram-v2-WZHVMYZB-BvUbXD6H.js.map} +1 -1
  40. package/dist/static/assets/{clone-MlWbv1V0.js → clone-0VLS7GaA.js} +2 -2
  41. package/dist/static/assets/{clone-MlWbv1V0.js.map → clone-0VLS7GaA.js.map} +1 -1
  42. package/dist/static/assets/{cose-bilkent-S5V4N54A-DWPCXSrn.js → cose-bilkent-S5V4N54A-anaPs-75.js} +2 -2
  43. package/dist/static/assets/{cose-bilkent-S5V4N54A-DWPCXSrn.js.map → cose-bilkent-S5V4N54A-anaPs-75.js.map} +1 -1
  44. package/dist/static/assets/{dagre-6UL2VRFP-C8ptQ9V3.js → dagre-6UL2VRFP-YwdsZ11r.js} +7 -7
  45. package/dist/static/assets/{dagre-6UL2VRFP-C8ptQ9V3.js.map → dagre-6UL2VRFP-YwdsZ11r.js.map} +1 -1
  46. package/dist/static/assets/{diagram-PSM6KHXK-Bgf1FqkE.js → diagram-PSM6KHXK-5KQCf3h2.js} +8 -8
  47. package/dist/static/assets/{diagram-PSM6KHXK-Bgf1FqkE.js.map → diagram-PSM6KHXK-5KQCf3h2.js.map} +1 -1
  48. package/dist/static/assets/{diagram-QEK2KX5R-BZ5xzofU.js → diagram-QEK2KX5R-Mf24XxZL.js} +7 -7
  49. package/dist/static/assets/{diagram-QEK2KX5R-BZ5xzofU.js.map → diagram-QEK2KX5R-Mf24XxZL.js.map} +1 -1
  50. package/dist/static/assets/{diagram-S2PKOQOG-Dwp47T9I.js → diagram-S2PKOQOG-DyQjD4D5.js} +7 -7
  51. package/dist/static/assets/{diagram-S2PKOQOG-Dwp47T9I.js.map → diagram-S2PKOQOG-DyQjD4D5.js.map} +1 -1
  52. package/dist/static/assets/{erDiagram-Q2GNP2WA-Cx4weIHl.js → erDiagram-Q2GNP2WA-CEzTKw1u.js} +5 -5
  53. package/dist/static/assets/{erDiagram-Q2GNP2WA-Cx4weIHl.js.map → erDiagram-Q2GNP2WA-CEzTKw1u.js.map} +1 -1
  54. package/dist/static/assets/{flowDiagram-NV44I4VS-vNUuIeRk.js → flowDiagram-NV44I4VS-DT821XSz.js} +6 -6
  55. package/dist/static/assets/{flowDiagram-NV44I4VS-vNUuIeRk.js.map → flowDiagram-NV44I4VS-DT821XSz.js.map} +1 -1
  56. package/dist/static/assets/{ganttDiagram-JELNMOA3-BEfozJAr.js → ganttDiagram-JELNMOA3-DlmeVsGg.js} +3 -3
  57. package/dist/static/assets/{ganttDiagram-JELNMOA3-BEfozJAr.js.map → ganttDiagram-JELNMOA3-DlmeVsGg.js.map} +1 -1
  58. package/dist/static/assets/{gitGraphDiagram-V2S2FVAM-eHxwc3d9.js → gitGraphDiagram-V2S2FVAM-yAfyBG_d.js} +8 -8
  59. package/dist/static/assets/{gitGraphDiagram-V2S2FVAM-eHxwc3d9.js.map → gitGraphDiagram-V2S2FVAM-yAfyBG_d.js.map} +1 -1
  60. package/dist/static/assets/{graph-C6a6uAok.js → graph-BYv8vyWe.js} +3 -3
  61. package/dist/static/assets/{graph-C6a6uAok.js.map → graph-BYv8vyWe.js.map} +1 -1
  62. package/dist/static/assets/{index-D3TQbAKh.js → index-DMbwqOm6.js} +140 -61
  63. package/dist/static/assets/{index-D3TQbAKh.js.map → index-DMbwqOm6.js.map} +1 -1
  64. package/dist/static/assets/{infoDiagram-HS3SLOUP-CX0NiId3.js → infoDiagram-HS3SLOUP-DaadramQ.js} +6 -6
  65. package/dist/static/assets/{infoDiagram-HS3SLOUP-CX0NiId3.js.map → infoDiagram-HS3SLOUP-DaadramQ.js.map} +1 -1
  66. package/dist/static/assets/{journeyDiagram-XKPGCS4Q-C1IepPZ-.js → journeyDiagram-XKPGCS4Q-ftN8hxu3.js} +5 -5
  67. package/dist/static/assets/{journeyDiagram-XKPGCS4Q-C1IepPZ-.js.map → journeyDiagram-XKPGCS4Q-ftN8hxu3.js.map} +1 -1
  68. package/dist/static/assets/{kanban-definition-3W4ZIXB7-uMNX4Z1W.js → kanban-definition-3W4ZIXB7-BIXETQ-C.js} +3 -3
  69. package/dist/static/assets/{kanban-definition-3W4ZIXB7-uMNX4Z1W.js.map → kanban-definition-3W4ZIXB7-BIXETQ-C.js.map} +1 -1
  70. package/dist/static/assets/{layout-CpE3kk5z.js → layout-OTbrj0Ye.js} +5 -5
  71. package/dist/static/assets/{layout-CpE3kk5z.js.map → layout-OTbrj0Ye.js.map} +1 -1
  72. package/dist/static/assets/{linear-DV8laXr9.js → linear-CVfOC669.js} +2 -2
  73. package/dist/static/assets/{linear-DV8laXr9.js.map → linear-CVfOC669.js.map} +1 -1
  74. package/dist/static/assets/{mindmap-definition-VGOIOE7T-CKjgVM9S.js → mindmap-definition-VGOIOE7T-D2zv5uI9.js} +4 -4
  75. package/dist/static/assets/{mindmap-definition-VGOIOE7T-CKjgVM9S.js.map → mindmap-definition-VGOIOE7T-D2zv5uI9.js.map} +1 -1
  76. package/dist/static/assets/{pieDiagram-ADFJNKIX-BBonlNyT.js → pieDiagram-ADFJNKIX-DaUXTsv7.js} +8 -8
  77. package/dist/static/assets/{pieDiagram-ADFJNKIX-BBonlNyT.js.map → pieDiagram-ADFJNKIX-DaUXTsv7.js.map} +1 -1
  78. package/dist/static/assets/{quadrantDiagram-AYHSOK5B-BTI8HbBu.js → quadrantDiagram-AYHSOK5B-B7O2wPX9.js} +3 -3
  79. package/dist/static/assets/{quadrantDiagram-AYHSOK5B-BTI8HbBu.js.map → quadrantDiagram-AYHSOK5B-B7O2wPX9.js.map} +1 -1
  80. package/dist/static/assets/{requirementDiagram-UZGBJVZJ-ZtSr9Q5R.js → requirementDiagram-UZGBJVZJ-DpauVPY1.js} +4 -4
  81. package/dist/static/assets/{requirementDiagram-UZGBJVZJ-ZtSr9Q5R.js.map → requirementDiagram-UZGBJVZJ-DpauVPY1.js.map} +1 -1
  82. package/dist/static/assets/{sankeyDiagram-TZEHDZUN-DibLVGzg.js → sankeyDiagram-TZEHDZUN-B3Hbfvad.js} +2 -2
  83. package/dist/static/assets/{sankeyDiagram-TZEHDZUN-DibLVGzg.js.map → sankeyDiagram-TZEHDZUN-B3Hbfvad.js.map} +1 -1
  84. package/dist/static/assets/{sequenceDiagram-WL72ISMW-qXatfzVt.js → sequenceDiagram-WL72ISMW-KdbWByWT.js} +4 -4
  85. package/dist/static/assets/{sequenceDiagram-WL72ISMW-qXatfzVt.js.map → sequenceDiagram-WL72ISMW-KdbWByWT.js.map} +1 -1
  86. package/dist/static/assets/{stateDiagram-FKZM4ZOC-7fgxCQHo.js → stateDiagram-FKZM4ZOC-yDOCVezC.js} +9 -9
  87. package/dist/static/assets/{stateDiagram-FKZM4ZOC-7fgxCQHo.js.map → stateDiagram-FKZM4ZOC-yDOCVezC.js.map} +1 -1
  88. package/dist/static/assets/{stateDiagram-v2-4FDKWEC3-DcWlOAnF.js → stateDiagram-v2-4FDKWEC3-CpCJhvQO.js} +5 -5
  89. package/dist/static/assets/{stateDiagram-v2-4FDKWEC3-DcWlOAnF.js.map → stateDiagram-v2-4FDKWEC3-CpCJhvQO.js.map} +1 -1
  90. package/dist/static/assets/{timeline-definition-IT6M3QCI-iX2MRdpY.js → timeline-definition-IT6M3QCI-CHxuEjhV.js} +3 -3
  91. package/dist/static/assets/{timeline-definition-IT6M3QCI-iX2MRdpY.js.map → timeline-definition-IT6M3QCI-CHxuEjhV.js.map} +1 -1
  92. package/dist/static/assets/{treemap-GDKQZRPO-AVRnyXu1.js → treemap-GDKQZRPO-Bsqu3wIy.js} +5 -5
  93. package/dist/static/assets/{treemap-GDKQZRPO-AVRnyXu1.js.map → treemap-GDKQZRPO-Bsqu3wIy.js.map} +1 -1
  94. package/dist/static/assets/{xychartDiagram-PRI3JC2R-DVYEo5aJ.js → xychartDiagram-PRI3JC2R-RZy33lFF.js} +3 -3
  95. package/dist/static/assets/{xychartDiagram-PRI3JC2R-DVYEo5aJ.js.map → xychartDiagram-PRI3JC2R-RZy33lFF.js.map} +1 -1
  96. package/dist/static/index.html +1 -1
  97. package/dist/tools/ctrl.js +3 -3
  98. package/dist/tools/prompts/control/en/index.md +5 -4
  99. package/dist/tools/prompts/control/en/principles.md +11 -7
  100. package/dist/tools/prompts/control/en/tools.md +19 -0
  101. package/dist/tools/prompts/control/zh/index.md +5 -4
  102. package/dist/tools/prompts/control/zh/principles.md +11 -7
  103. package/dist/tools/prompts/control/zh/tools.md +19 -0
  104. package/package.json +3 -3
@@ -330,7 +330,7 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
330
330
  <head>
331
331
  <meta charset="utf-8" />
332
332
  <meta name="viewport" content="width=device-width, initial-scale=1" />
333
- <title>Dominds File Preview</title>
333
+ <title>Dominds Workspace Preview</title>
334
334
  <link
335
335
  rel="stylesheet"
336
336
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css"
@@ -343,7 +343,9 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
343
343
  background: #0f172a;
344
344
  color: #e2e8f0;
345
345
  }
346
- .wrap { max-width: 1200px; margin: 0 auto; padding: 16px; }
346
+ a { color: #93c5fd; text-decoration: none; }
347
+ a:hover { text-decoration: underline; }
348
+ .wrap { max-width: 1280px; margin: 0 auto; padding: 16px; }
347
349
  .panel {
348
350
  border: 1px solid #334155;
349
351
  border-radius: 10px;
@@ -379,78 +381,238 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
379
381
  background: #1f1010;
380
382
  color: #fecaca;
381
383
  }
382
- pre {
383
- margin: 0;
384
+ .code-wrap, .dir-wrap {
384
385
  border-radius: 8px;
385
386
  border: 1px solid #334155;
386
387
  background: #0b1220;
387
388
  overflow: auto;
388
389
  max-height: calc(100vh - 190px);
389
390
  }
390
- code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
391
+ .code-view {
392
+ min-width: max-content;
393
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
394
+ font-size: 12px;
395
+ line-height: 1.6;
396
+ }
397
+ .code-line {
398
+ display: grid;
399
+ grid-template-columns: auto 1fr;
400
+ }
401
+ .code-line:hover {
402
+ background: rgba(148, 163, 184, 0.08);
403
+ }
404
+ .code-line.target-line {
405
+ background: rgba(245, 158, 11, 0.14);
406
+ }
407
+ .line-no {
408
+ position: sticky;
409
+ left: 0;
410
+ padding: 0 12px;
411
+ min-width: 56px;
412
+ box-sizing: border-box;
413
+ text-align: right;
414
+ user-select: none;
415
+ -webkit-user-select: none;
416
+ -moz-user-select: none;
417
+ color: #64748b;
418
+ background: #0f172a;
419
+ border-right: 1px solid #1e293b;
420
+ }
421
+ .code-line.target-line .line-no {
422
+ color: #fbbf24;
423
+ background: #1c1917;
424
+ }
425
+ .line-content {
426
+ display: block;
427
+ margin: 0;
428
+ padding: 0 12px;
429
+ white-space: pre;
430
+ tab-size: 2;
431
+ }
432
+ .line-content:empty::after {
433
+ content: ' ';
434
+ }
435
+ .target-col {
436
+ background: #f59e0b;
437
+ color: #111827;
438
+ border-radius: 3px;
439
+ box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.45);
440
+ }
441
+ .target-col-caret {
442
+ display: inline-block;
443
+ width: 2px;
444
+ min-height: 1.2em;
445
+ vertical-align: text-bottom;
446
+ background: #f59e0b;
447
+ box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.35);
448
+ }
449
+ .dir-list {
450
+ width: 100%;
451
+ border-collapse: collapse;
452
+ font-size: 13px;
453
+ }
454
+ .dir-list thead th {
455
+ position: sticky;
456
+ top: 0;
457
+ z-index: 1;
458
+ background: #111827;
459
+ color: #94a3b8;
460
+ font-weight: 600;
461
+ text-align: left;
462
+ border-bottom: 1px solid #334155;
463
+ }
464
+ .dir-list th, .dir-list td {
465
+ padding: 8px 10px;
466
+ border-bottom: 1px solid #1e293b;
467
+ vertical-align: top;
468
+ }
469
+ .dir-list tbody tr:hover {
470
+ background: rgba(148, 163, 184, 0.08);
471
+ }
472
+ .entry-name {
473
+ display: inline-flex;
474
+ align-items: center;
475
+ gap: 8px;
476
+ min-width: 0;
477
+ }
478
+ .entry-icon {
479
+ width: 1.2em;
480
+ text-align: center;
481
+ color: #cbd5e1;
482
+ }
483
+ .entry-label {
484
+ word-break: break-word;
485
+ }
486
+ .entry-type, .entry-size {
487
+ white-space: nowrap;
488
+ color: #94a3b8;
489
+ }
490
+ .entry-note {
491
+ color: #94a3b8;
492
+ font-size: 12px;
493
+ word-break: break-word;
494
+ }
495
+ .entry-note.warn {
496
+ color: #fca5a5;
497
+ }
391
498
  </style>
392
499
  </head>
393
500
  <body>
394
501
  <div class="wrap">
395
502
  <div class="panel">
396
503
  <div class="head">
397
- <div id="file-path" class="path">Loading...</div>
398
- <div id="file-meta" class="meta"></div>
504
+ <div id="preview-path" class="path">Loading...</div>
505
+ <div id="preview-meta" class="meta"></div>
399
506
  </div>
400
507
  <div class="body">
401
- <div id="status" class="status">Loading file...</div>
402
- <pre id="code-wrap" style="display:none;"><code id="code"></code></pre>
508
+ <div id="status" class="status">Loading workspace entry...</div>
509
+ <div id="code-wrap" class="code-wrap" style="display:none;">
510
+ <div id="code-view" class="code-view"></div>
511
+ </div>
512
+ <div id="dir-wrap" class="dir-wrap" style="display:none;">
513
+ <table class="dir-list">
514
+ <thead>
515
+ <tr>
516
+ <th>Name</th>
517
+ <th>Type</th>
518
+ <th>Size</th>
519
+ <th>Notes</th>
520
+ </tr>
521
+ </thead>
522
+ <tbody id="dir-body"></tbody>
523
+ </table>
524
+ </div>
403
525
  </div>
404
526
  </div>
405
527
  </div>
406
528
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
407
529
  <script>
408
530
  (function () {
409
- var filePathEl = document.getElementById('file-path');
410
- var fileMetaEl = document.getElementById('file-meta');
531
+ var pathEl = document.getElementById('preview-path');
532
+ var metaEl = document.getElementById('preview-meta');
411
533
  var statusEl = document.getElementById('status');
412
534
  var codeWrapEl = document.getElementById('code-wrap');
413
- var codeEl = document.getElementById('code');
414
- if (!filePathEl || !fileMetaEl || !statusEl || !codeWrapEl || !codeEl) return;
535
+ var codeViewEl = document.getElementById('code-view');
536
+ var dirWrapEl = document.getElementById('dir-wrap');
537
+ var dirBodyEl = document.getElementById('dir-body');
538
+ if (
539
+ !pathEl ||
540
+ !metaEl ||
541
+ !statusEl ||
542
+ !codeWrapEl ||
543
+ !codeViewEl ||
544
+ !dirWrapEl ||
545
+ !dirBodyEl
546
+ ) return;
415
547
 
416
548
  function setError(message) {
417
549
  statusEl.classList.add('err');
418
550
  statusEl.textContent = message;
551
+ statusEl.style.display = 'block';
419
552
  codeWrapEl.style.display = 'none';
553
+ dirWrapEl.style.display = 'none';
420
554
  }
421
555
 
422
- function normalizeRelativePath(input) {
556
+ function resetViews() {
557
+ statusEl.classList.remove('err');
558
+ codeWrapEl.style.display = 'none';
559
+ dirWrapEl.style.display = 'none';
560
+ codeViewEl.innerHTML = '';
561
+ dirBodyEl.innerHTML = '';
562
+ }
563
+
564
+ function showReady() {
565
+ statusEl.style.display = 'none';
566
+ }
567
+
568
+ function escapeHtml(value) {
569
+ return String(value)
570
+ .replace(/&/g, '&amp;')
571
+ .replace(/</g, '&lt;')
572
+ .replace(/>/g, '&gt;')
573
+ .replace(/"/g, '&quot;')
574
+ .replace(/'/g, '&#39;');
575
+ }
576
+
577
+ function normalizeRelativePath(input, allowRoot) {
423
578
  if (typeof input !== 'string') return null;
424
- if (input.length < 1) return null;
425
579
  if (input.indexOf('\\\\') >= 0) return null;
426
580
  if (/[\\u0000]/.test(input)) return null;
427
581
  if (input.charAt(0) === '/') return null;
428
- var parts = input.split('/').filter(function (s) { return s.length > 0; });
429
- if (parts.length < 1) return null;
582
+ var parts = input.split('/');
583
+ var normalized = [];
430
584
  for (var i = 0; i < parts.length; i += 1) {
431
585
  var seg = parts[i];
432
- if (seg === '.' || seg === '..') return null;
586
+ if (!seg || seg === '.') continue;
587
+ if (seg === '..') {
588
+ if (normalized.length < 1) return null;
589
+ normalized.pop();
590
+ continue;
591
+ }
592
+ normalized.push(seg);
433
593
  }
434
- return parts.join('/');
594
+ if (normalized.length < 1) return allowRoot ? '' : null;
595
+ return normalized.join('/');
435
596
  }
436
597
 
437
598
  function parseRelativePathFromLocation() {
438
599
  var pathname = window.location.pathname || '';
600
+ if (pathname === '/f' || pathname === '/f/') return '';
439
601
  if (!pathname.startsWith('/f/')) return null;
440
602
  var tail = pathname.slice('/f/'.length);
441
- if (tail.length < 1) return null;
603
+ if (tail.length < 1) return '';
442
604
  var rawParts = tail.split('/');
443
- if (rawParts.some(function (s) { return s.length < 1; })) return null;
444
605
  var decodedParts = [];
445
606
  for (var i = 0; i < rawParts.length; i += 1) {
607
+ var rawPart = rawParts[i];
608
+ if (rawPart.length < 1) continue;
446
609
  var decoded;
447
- try { decoded = decodeURIComponent(rawParts[i]); } catch { return null; }
448
- if (decoded.length < 1) return null;
610
+ try { decoded = decodeURIComponent(rawPart); } catch { return null; }
611
+ if (decoded.length < 1) continue;
449
612
  if (decoded.indexOf('/') >= 0 || decoded.indexOf('\\\\') >= 0 || /[\\u0000]/.test(decoded)) return null;
450
- if (decoded === '.' || decoded === '..') return null;
451
613
  decodedParts.push(decoded);
452
614
  }
453
- return normalizeRelativePath(decodedParts.join('/'));
615
+ return normalizeRelativePath(decodedParts.join('/'), true);
454
616
  }
455
617
 
456
618
  function parsePositiveInt(raw) {
@@ -474,15 +636,206 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
474
636
  }
475
637
 
476
638
  function formatBytes(size) {
477
- if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) return null;
639
+ if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) return '';
478
640
  if (size < 1024) return String(size) + ' B';
479
641
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KiB';
480
642
  return (size / (1024 * 1024)).toFixed(1) + ' MiB';
481
643
  }
482
644
 
483
- var fileRelPath = parseRelativePathFromLocation();
484
- if (!fileRelPath) {
485
- filePathEl.textContent = 'Invalid file path';
645
+ function buildPreviewHref(relativePath) {
646
+ if (typeof relativePath !== 'string') return '/f';
647
+ var normalized = normalizeRelativePath(relativePath, true);
648
+ if (normalized === null || normalized.length < 1) return '/f';
649
+ return '/f/' + normalized.split('/').map(function (segment) {
650
+ return encodeURIComponent(segment);
651
+ }).join('/');
652
+ }
653
+
654
+ function parentPathOf(relativePath) {
655
+ if (relativePath.length < 1) return null;
656
+ var parts = relativePath.split('/');
657
+ parts.pop();
658
+ return parts.join('/');
659
+ }
660
+
661
+ function renderHighlightedSegment(raw, lang) {
662
+ if (typeof raw !== 'string' || raw.length < 1) return '';
663
+ if (
664
+ window.hljs &&
665
+ typeof window.hljs.highlight === 'function' &&
666
+ typeof window.hljs.getLanguage === 'function' &&
667
+ lang !== 'plaintext' &&
668
+ window.hljs.getLanguage(lang)
669
+ ) {
670
+ return window.hljs.highlight(raw, { language: lang, ignoreIllegals: true }).value;
671
+ }
672
+ return escapeHtml(raw);
673
+ }
674
+
675
+ function buildLineHtml(rawLine, lang, isTargetLine, targetColumn) {
676
+ if (!isTargetLine || targetColumn === null) {
677
+ return rawLine.length > 0 ? renderHighlightedSegment(rawLine, lang) : '';
678
+ }
679
+ var clampedColumn = targetColumn;
680
+ if (clampedColumn < 1) clampedColumn = 1;
681
+ if (clampedColumn > rawLine.length + 1) clampedColumn = rawLine.length + 1;
682
+ var splitIndex = clampedColumn - 1;
683
+ var before = rawLine.slice(0, splitIndex);
684
+ var after = rawLine.slice(splitIndex + 1);
685
+ if (splitIndex >= rawLine.length) {
686
+ return renderHighlightedSegment(rawLine, lang) + '<span class="target-col-caret" aria-hidden="true"></span>';
687
+ }
688
+ var targetChar = rawLine.charAt(splitIndex);
689
+ return (
690
+ renderHighlightedSegment(before, lang) +
691
+ '<span class="target-col">' + renderHighlightedSegment(targetChar, lang) + '</span>' +
692
+ renderHighlightedSegment(after, lang)
693
+ );
694
+ }
695
+
696
+ function renderFile(raw, lang, targetLine, targetColumn) {
697
+ codeViewEl.innerHTML = '';
698
+ var lines = raw.split('\\n');
699
+ var fragment = document.createDocumentFragment();
700
+ var selectedLineEl = null;
701
+ for (var i = 0; i < lines.length; i += 1) {
702
+ var lineNumber = i + 1;
703
+ var rawLine = lines[i];
704
+ var rowEl = document.createElement('div');
705
+ rowEl.className = 'code-line' + (lineNumber === targetLine ? ' target-line' : '');
706
+ rowEl.setAttribute('data-line', String(lineNumber));
707
+
708
+ var lineNoEl = document.createElement('span');
709
+ lineNoEl.className = 'line-no';
710
+ lineNoEl.textContent = String(lineNumber);
711
+ lineNoEl.setAttribute('aria-hidden', 'true');
712
+
713
+ var lineContentEl = document.createElement('span');
714
+ lineContentEl.className = 'line-content hljs';
715
+ lineContentEl.innerHTML = buildLineHtml(rawLine, lang, lineNumber === targetLine, targetColumn);
716
+
717
+ rowEl.appendChild(lineNoEl);
718
+ rowEl.appendChild(lineContentEl);
719
+ fragment.appendChild(rowEl);
720
+ if (lineNumber === targetLine) {
721
+ selectedLineEl = rowEl;
722
+ }
723
+ }
724
+ codeViewEl.appendChild(fragment);
725
+ codeWrapEl.style.display = 'block';
726
+ if (selectedLineEl && typeof selectedLineEl.scrollIntoView === 'function') {
727
+ selectedLineEl.scrollIntoView({ block: 'center' });
728
+ }
729
+ }
730
+
731
+ function createEntryLink(relativePath, label, icon) {
732
+ var link = document.createElement('a');
733
+ link.href = buildPreviewHref(relativePath);
734
+ link.className = 'entry-name';
735
+
736
+ var iconEl = document.createElement('span');
737
+ iconEl.className = 'entry-icon';
738
+ iconEl.textContent = icon;
739
+
740
+ var labelEl = document.createElement('span');
741
+ labelEl.className = 'entry-label';
742
+ labelEl.textContent = label;
743
+
744
+ link.appendChild(iconEl);
745
+ link.appendChild(labelEl);
746
+ return link;
747
+ }
748
+
749
+ function describeEntryNote(entry) {
750
+ if (!entry || entry.isSymlink !== true) return '';
751
+ if (typeof entry.symlinkTarget !== 'string' || entry.symlinkTarget.length < 1) return 'symlink';
752
+ return 'symlink → ' + entry.symlinkTarget;
753
+ }
754
+
755
+ function createDirRow(entry) {
756
+ var row = document.createElement('tr');
757
+
758
+ var nameCell = document.createElement('td');
759
+ var label = entry.name + (entry.kind === 'directory' ? '/' : '');
760
+ var icon = entry.kind === 'directory' ? '📁' : entry.kind === 'file' ? '📄' : '⛔';
761
+ if (entry.kind === 'directory' || entry.kind === 'file') {
762
+ nameCell.appendChild(createEntryLink(entry.path, label, icon));
763
+ } else {
764
+ var nameWrap = document.createElement('span');
765
+ nameWrap.className = 'entry-name';
766
+ var iconEl = document.createElement('span');
767
+ iconEl.className = 'entry-icon';
768
+ iconEl.textContent = icon;
769
+ var labelEl = document.createElement('span');
770
+ labelEl.className = 'entry-label';
771
+ labelEl.textContent = label;
772
+ nameWrap.appendChild(iconEl);
773
+ nameWrap.appendChild(labelEl);
774
+ nameCell.appendChild(nameWrap);
775
+ }
776
+
777
+ var typeCell = document.createElement('td');
778
+ typeCell.className = 'entry-type';
779
+ typeCell.textContent = entry.kind;
780
+
781
+ var sizeCell = document.createElement('td');
782
+ sizeCell.className = 'entry-size';
783
+ sizeCell.textContent = typeof entry.size === 'number' ? formatBytes(entry.size) : '';
784
+
785
+ var noteCell = document.createElement('td');
786
+ var note = describeEntryNote(entry);
787
+ if (typeof entry.resolvedKind === 'string' && entry.resolvedKind !== entry.kind) {
788
+ note = note
789
+ ? note + ' | ' + entry.resolvedKind
790
+ : entry.resolvedKind;
791
+ }
792
+ if (note) {
793
+ var noteEl = document.createElement('div');
794
+ noteEl.className = 'entry-note' + (entry.kind === 'other' ? ' warn' : '');
795
+ noteEl.textContent = note;
796
+ noteCell.appendChild(noteEl);
797
+ }
798
+
799
+ row.appendChild(nameCell);
800
+ row.appendChild(typeCell);
801
+ row.appendChild(sizeCell);
802
+ row.appendChild(noteCell);
803
+ return row;
804
+ }
805
+
806
+ function renderDirectory(currentPath, entries) {
807
+ dirBodyEl.innerHTML = '';
808
+ var fragment = document.createDocumentFragment();
809
+
810
+ var parentPath = parentPathOf(currentPath);
811
+ if (parentPath !== null) {
812
+ var parentRow = document.createElement('tr');
813
+ var nameCell = document.createElement('td');
814
+ nameCell.appendChild(createEntryLink(parentPath, '../', '↩'));
815
+ var typeCell = document.createElement('td');
816
+ typeCell.className = 'entry-type';
817
+ typeCell.textContent = 'directory';
818
+ var sizeCell = document.createElement('td');
819
+ sizeCell.className = 'entry-size';
820
+ var noteCell = document.createElement('td');
821
+ parentRow.appendChild(nameCell);
822
+ parentRow.appendChild(typeCell);
823
+ parentRow.appendChild(sizeCell);
824
+ parentRow.appendChild(noteCell);
825
+ fragment.appendChild(parentRow);
826
+ }
827
+
828
+ for (var i = 0; i < entries.length; i += 1) {
829
+ fragment.appendChild(createDirRow(entries[i]));
830
+ }
831
+
832
+ dirBodyEl.appendChild(fragment);
833
+ dirWrapEl.style.display = 'block';
834
+ }
835
+
836
+ var previewPath = parseRelativePathFromLocation();
837
+ if (previewPath === null) {
838
+ pathEl.textContent = 'Invalid preview path';
486
839
  setError('Invalid preview path. Expected /f/<rtws-relative-path> and no ..');
487
840
  return;
488
841
  }
@@ -497,13 +850,11 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
497
850
  (typeof authFromUrl === 'string' && authFromUrl.trim() !== '' ? authFromUrl.trim() : null) ||
498
851
  (typeof authFromStorage === 'string' && authFromStorage.trim() !== '' ? authFromStorage.trim() : null);
499
852
 
500
- filePathEl.textContent = fileRelPath;
501
- var meta = [];
853
+ pathEl.textContent = previewPath.length > 0 ? previewPath : '.';
502
854
  if (line !== null) {
503
- meta.push('Line ' + String(line) + (column !== null ? ':' + String(column) : ''));
504
- }
505
- if (meta.length > 0) {
506
- fileMetaEl.textContent = meta.join(' | ');
855
+ metaEl.textContent = 'Line ' + String(line) + (column !== null ? ':' + String(column) : '');
856
+ } else {
857
+ metaEl.textContent = '';
507
858
  }
508
859
 
509
860
  var headers = { Accept: 'application/json' };
@@ -511,7 +862,8 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
511
862
  headers['Authorization'] = 'Bearer ' + token;
512
863
  }
513
864
 
514
- fetch('/api/workspace/file?file=' + encodeURIComponent(fileRelPath), {
865
+ resetViews();
866
+ fetch('/api/workspace/entry?path=' + encodeURIComponent(previewPath), {
515
867
  method: 'GET',
516
868
  headers: headers,
517
869
  cache: 'no-store'
@@ -522,7 +874,7 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
522
874
  });
523
875
  })
524
876
  .then(function (result) {
525
- if (!result.ok || !result.payload || result.payload.success !== true || typeof result.payload.raw !== 'string') {
877
+ if (!result.ok || !result.payload || result.payload.success !== true || typeof result.payload.kind !== 'string') {
526
878
  var msg = result.payload && typeof result.payload.error === 'string' && result.payload.error !== ''
527
879
  ? result.payload.error
528
880
  : ('Request failed: HTTP ' + String(result.status));
@@ -530,24 +882,37 @@ async function handleWorkspaceFilePreviewPage(req, res, pathname) {
530
882
  return;
531
883
  }
532
884
 
533
- statusEl.style.display = 'none';
534
- codeWrapEl.style.display = 'block';
535
- var lang = detectLang(fileRelPath);
536
- var sizeText = formatBytes(result.payload.size);
537
- var metaItems = [];
538
- if (line !== null) metaItems.push('Line ' + String(line) + (column !== null ? ':' + String(column) : ''));
539
- metaItems.push(lang);
540
- if (sizeText !== null) metaItems.push(sizeText);
541
- fileMetaEl.textContent = metaItems.join(' | ');
885
+ showReady();
886
+ pathEl.textContent =
887
+ typeof result.payload.path === 'string' && result.payload.path.length > 0
888
+ ? result.payload.path
889
+ : '.';
542
890
 
543
- codeEl.className = 'language-' + lang;
544
- codeEl.textContent = result.payload.raw;
545
- if (window.hljs && typeof window.hljs.highlightElement === 'function') {
546
- window.hljs.highlightElement(codeEl);
891
+ if (result.payload.kind === 'file' && typeof result.payload.raw === 'string') {
892
+ var lang = detectLang(typeof result.payload.path === 'string' ? result.payload.path : previewPath);
893
+ var sizeText = formatBytes(result.payload.size);
894
+ var metaItems = [];
895
+ if (line !== null) metaItems.push('Line ' + String(line) + (column !== null ? ':' + String(column) : ''));
896
+ metaItems.push(lang);
897
+ if (sizeText) metaItems.push(sizeText);
898
+ metaEl.textContent = metaItems.join(' | ');
899
+ renderFile(result.payload.raw, lang, line, column);
900
+ return;
901
+ }
902
+
903
+ if (result.payload.kind === 'directory' && Array.isArray(result.payload.entries)) {
904
+ metaEl.textContent = 'directory | ' + String(result.payload.entries.length) + ' entries';
905
+ renderDirectory(
906
+ typeof result.payload.path === 'string' ? result.payload.path : previewPath,
907
+ result.payload.entries,
908
+ );
909
+ return;
547
910
  }
911
+
912
+ setError('Invalid preview payload');
548
913
  })
549
914
  .catch(function (err) {
550
- var msg = err && typeof err.message === 'string' ? err.message : 'Failed to load file';
915
+ var msg = err && typeof err.message === 'string' ? err.message : 'Failed to load workspace entry';
551
916
  setError(msg);
552
917
  });
553
918
  })();
@@ -781,9 +1146,9 @@ async function handleApiRoute(req, res, pathname, context) {
781
1146
  if (pathname === '/api/docs/read' && req.method === 'GET') {
782
1147
  return await handleReadDocsMarkdown(req, res);
783
1148
  }
784
- // Read workspace file content for markdown file-link preview.
785
- if (pathname === '/api/workspace/file' && req.method === 'GET') {
786
- return await handleReadWorkspaceFile(req, res);
1149
+ // Read workspace file or directory content for markdown preview links.
1150
+ if (pathname === '/api/workspace/entry' && req.method === 'GET') {
1151
+ return await handleReadWorkspaceEntry(req, res);
787
1152
  }
788
1153
  if (pathname === '/api/snippets/builtin' && req.method === 'GET') {
789
1154
  const payload = await (0, snippets_routes_1.handleGetBuiltinSnippets)();
@@ -1035,10 +1400,8 @@ function isWithinResolvedRoot(targetAbsPath, rootAbsPath) {
1035
1400
  return true;
1036
1401
  return targetAbsPath.startsWith(ensurePathSuffixSeparator(rootAbsPath));
1037
1402
  }
1038
- function normalizeRtwsRelativeFilePath(input) {
1403
+ function normalizeRtwsRelativePath(input, options) {
1039
1404
  const trimmed = input.trim();
1040
- if (trimmed.length < 1)
1041
- return null;
1042
1405
  if (trimmed.includes('\0'))
1043
1406
  return null;
1044
1407
  if (trimmed.includes('\\'))
@@ -1046,8 +1409,11 @@ function normalizeRtwsRelativeFilePath(input) {
1046
1409
  if (path.posix.isAbsolute(trimmed))
1047
1410
  return null;
1048
1411
  const normalized = path.posix.normalize(trimmed);
1049
- if (normalized.length < 1 || normalized === '.' || normalized === '..')
1412
+ if (normalized === '..')
1050
1413
  return null;
1414
+ if (normalized === '.' || normalized.length < 1) {
1415
+ return options.allowRoot ? '' : null;
1416
+ }
1051
1417
  if (normalized.startsWith('../'))
1052
1418
  return null;
1053
1419
  if (normalized.includes('/../'))
@@ -1060,50 +1426,181 @@ function normalizeRtwsRelativeFilePath(input) {
1060
1426
  }
1061
1427
  return normalized;
1062
1428
  }
1063
- async function handleReadWorkspaceFile(req, res) {
1064
- const urlObj = new URL(req.url ?? '', 'http://127.0.0.1');
1065
- const fileRaw = urlObj.searchParams.get('file');
1066
- const fileRelPath = typeof fileRaw === 'string' ? normalizeRtwsRelativeFilePath(fileRaw) : null;
1067
- if (fileRelPath === null) {
1068
- respondJson(res, 400, { success: false, error: 'Invalid file path' });
1069
- return true;
1429
+ async function getWorkspaceRootRealAbs() {
1430
+ const workspaceRootAbs = path.resolve(process.cwd());
1431
+ try {
1432
+ return await promises_1.default.realpath(workspaceRootAbs);
1433
+ }
1434
+ catch {
1435
+ return workspaceRootAbs;
1070
1436
  }
1437
+ }
1438
+ function classifyWorkspaceEntryKind(followStat) {
1439
+ if (followStat.isDirectory())
1440
+ return 'directory';
1441
+ if (followStat.isFile())
1442
+ return 'file';
1443
+ return 'other';
1444
+ }
1445
+ async function resolveWorkspacePreviewPath(pathRel) {
1071
1446
  const workspaceRootAbs = path.resolve(process.cwd());
1072
- const candidateAbsPath = path.resolve(workspaceRootAbs, fileRelPath);
1447
+ const workspaceRootRealAbs = await getWorkspaceRootRealAbs();
1448
+ const candidateAbsPath = pathRel.length < 1 ? workspaceRootAbs : path.resolve(workspaceRootAbs, pathRel);
1073
1449
  if (!isWithinResolvedRoot(candidateAbsPath, workspaceRootAbs)) {
1074
- respondJson(res, 403, {
1075
- success: false,
1076
- error: 'File path is outside rtws',
1077
- path: fileRelPath,
1078
- });
1450
+ const error = new Error(`Workspace preview path escaped rtws: ${pathRel}`);
1451
+ error.code = 'OUTSIDE_RTWS';
1452
+ throw error;
1453
+ }
1454
+ const resolvedAbsPath = await promises_1.default.realpath(candidateAbsPath);
1455
+ if (!isWithinResolvedRoot(resolvedAbsPath, workspaceRootRealAbs)) {
1456
+ const error = new Error(`Workspace preview path resolved outside rtws: ${pathRel}`);
1457
+ error.code = 'OUTSIDE_RTWS';
1458
+ throw error;
1459
+ }
1460
+ return {
1461
+ workspaceRootAbs,
1462
+ workspaceRootRealAbs,
1463
+ candidateAbsPath,
1464
+ resolvedAbsPath,
1465
+ };
1466
+ }
1467
+ async function listWorkspaceDirectoryEntries(params) {
1468
+ const entryNames = await promises_1.default.readdir(params.dirAbsPath);
1469
+ const entries = await Promise.all(entryNames.map(async (name) => {
1470
+ const childRelPath = params.pathRel.length < 1 ? name : `${params.pathRel}/${name}`;
1471
+ const childAbsPath = path.resolve(process.cwd(), childRelPath);
1472
+ const lstat = await promises_1.default.lstat(childAbsPath);
1473
+ const isSymlink = lstat.isSymbolicLink();
1474
+ let symlinkTarget;
1475
+ if (isSymlink) {
1476
+ try {
1477
+ symlinkTarget = await promises_1.default.readlink(childAbsPath);
1478
+ }
1479
+ catch {
1480
+ symlinkTarget = undefined;
1481
+ }
1482
+ }
1483
+ let resolvedKind;
1484
+ let kind;
1485
+ let size;
1486
+ try {
1487
+ const resolvedChildAbsPath = await promises_1.default.realpath(childAbsPath);
1488
+ if (!isWithinResolvedRoot(resolvedChildAbsPath, params.workspaceRootRealAbs)) {
1489
+ resolvedKind = 'outside_rtws';
1490
+ kind = 'other';
1491
+ }
1492
+ else {
1493
+ const followStat = await promises_1.default.stat(childAbsPath);
1494
+ resolvedKind = classifyWorkspaceEntryKind(followStat);
1495
+ kind = resolvedKind;
1496
+ size = followStat.isFile() ? followStat.size : undefined;
1497
+ }
1498
+ }
1499
+ catch (error) {
1500
+ if (getErrorCode(error) === 'ENOENT') {
1501
+ resolvedKind = 'broken';
1502
+ kind = 'other';
1503
+ }
1504
+ else {
1505
+ throw error;
1506
+ }
1507
+ }
1508
+ return {
1509
+ name,
1510
+ path: childRelPath,
1511
+ kind,
1512
+ resolvedKind,
1513
+ size,
1514
+ isSymlink,
1515
+ symlinkTarget,
1516
+ };
1517
+ }));
1518
+ entries.sort((a, b) => {
1519
+ const rank = (kind) => {
1520
+ switch (kind) {
1521
+ case 'directory':
1522
+ return 0;
1523
+ case 'file':
1524
+ return 1;
1525
+ case 'other':
1526
+ return 2;
1527
+ }
1528
+ };
1529
+ const rankDiff = rank(a.kind) - rank(b.kind);
1530
+ if (rankDiff !== 0)
1531
+ return rankDiff;
1532
+ return a.name.localeCompare(b.name);
1533
+ });
1534
+ return entries;
1535
+ }
1536
+ async function handleReadWorkspaceEntry(req, res) {
1537
+ const urlObj = new URL(req.url ?? '', 'http://127.0.0.1');
1538
+ const pathRaw = urlObj.searchParams.get('path');
1539
+ const pathRel = typeof pathRaw === 'string' ? normalizeRtwsRelativePath(pathRaw, { allowRoot: true }) : '';
1540
+ if (pathRel === null) {
1541
+ respondJson(res, 400, { success: false, error: 'Invalid workspace path' });
1079
1542
  return true;
1080
1543
  }
1081
1544
  try {
1082
- const stat = await promises_1.default.stat(candidateAbsPath);
1545
+ const resolved = await resolveWorkspacePreviewPath(pathRel);
1546
+ const stat = await promises_1.default.stat(resolved.candidateAbsPath);
1547
+ if (stat.isDirectory()) {
1548
+ const entries = await listWorkspaceDirectoryEntries({
1549
+ pathRel,
1550
+ dirAbsPath: resolved.resolvedAbsPath,
1551
+ workspaceRootRealAbs: resolved.workspaceRootRealAbs,
1552
+ });
1553
+ respondJson(res, 200, {
1554
+ success: true,
1555
+ kind: 'directory',
1556
+ path: pathRel,
1557
+ entries,
1558
+ });
1559
+ return true;
1560
+ }
1083
1561
  if (!stat.isFile()) {
1084
- respondJson(res, 400, { success: false, error: 'Path must be a file', path: fileRelPath });
1562
+ respondJson(res, 400, {
1563
+ success: false,
1564
+ error: 'Path must resolve to a file or directory',
1565
+ path: pathRel,
1566
+ });
1085
1567
  return true;
1086
1568
  }
1087
1569
  if (stat.size > WORKSPACE_FILE_PREVIEW_MAX_BYTES) {
1088
1570
  respondJson(res, 413, {
1089
1571
  success: false,
1090
1572
  error: `File too large for preview (max ${WORKSPACE_FILE_PREVIEW_MAX_BYTES} bytes)`,
1091
- path: fileRelPath,
1573
+ path: pathRel,
1092
1574
  size: stat.size,
1093
1575
  });
1094
1576
  return true;
1095
1577
  }
1096
- const raw = await promises_1.default.readFile(candidateAbsPath, 'utf-8');
1097
- respondJson(res, 200, { success: true, path: fileRelPath, raw, size: stat.size });
1578
+ const raw = await promises_1.default.readFile(resolved.candidateAbsPath, 'utf-8');
1579
+ respondJson(res, 200, {
1580
+ success: true,
1581
+ kind: 'file',
1582
+ path: pathRel,
1583
+ raw,
1584
+ size: stat.size,
1585
+ });
1098
1586
  return true;
1099
1587
  }
1100
1588
  catch (error) {
1101
- if (getErrorCode(error) === 'ENOENT') {
1102
- respondJson(res, 404, { success: false, error: 'File not found', path: fileRelPath });
1589
+ const code = getErrorCode(error);
1590
+ if (code === 'ENOENT') {
1591
+ respondJson(res, 404, { success: false, error: 'Path not found', path: pathRel });
1592
+ return true;
1593
+ }
1594
+ if (code === 'OUTSIDE_RTWS') {
1595
+ respondJson(res, 403, {
1596
+ success: false,
1597
+ error: 'Path resolves outside rtws',
1598
+ path: pathRel,
1599
+ });
1103
1600
  return true;
1104
1601
  }
1105
- log.error('Failed to read workspace file', error, { path: fileRelPath });
1106
- respondJson(res, 500, { success: false, error: 'Failed to read workspace file' });
1602
+ log.error('Failed to read workspace entry', error, { path: pathRel });
1603
+ respondJson(res, 500, { success: false, error: 'Failed to read workspace entry' });
1107
1604
  return true;
1108
1605
  }
1109
1606
  }