@lijian-ui/dsh-file-manager 0.1.21 → 0.2.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.
package/lib/client.js CHANGED
@@ -1083,6 +1083,274 @@ window.__ModuleLoader__.load({
1083
1083
  return `/filemgr/raw?root=${encodeURIComponent(root)}&path=${encodeURIComponent(path)}&v=${nonce}`;
1084
1084
  }
1085
1085
  //#endregion
1086
+ //#region src/client/locales.ts
1087
+ /**
1088
+ * Locale strings for the panel surfaces (zh/en). The client registers the
1089
+ * dictionary through the locale service like the sibling plugins; copy is
1090
+ * deliberately short and technical.
1091
+ * @module dsh-filemgr/client/locales
1092
+ */
1093
+ const zh = {
1094
+ "explorer.tabs.files": "文件",
1095
+ "explorer.tabs.changes": "变更",
1096
+ "explorer.search.placeholder": "按文件名搜索",
1097
+ "explorer.search.searching": "搜索中…",
1098
+ "explorer.search.empty": "没有匹配的文件",
1099
+ "explorer.search.error": "搜索失败",
1100
+ "explorer.search.truncated": "结果过多,仅显示前 {count} 条",
1101
+ "explorer.tree.empty": "项目为空",
1102
+ "explorer.collapse": "收起面板",
1103
+ "explorer.expand": "展开面板",
1104
+ "explorer.maximize": "最大化文件面板",
1105
+ "explorer.restore": "还原面板",
1106
+ "explorer.openPreview": "打开预览",
1107
+ "explorer.drag.dropHint": "松手插入文件路径",
1108
+ "scm.repositories": "存储库",
1109
+ "scm.changes": "变更",
1110
+ "scm.staged": "已暂存",
1111
+ "scm.unstaged": "变更",
1112
+ "scm.untracked": "未跟踪",
1113
+ "scm.conflicted": "冲突",
1114
+ "scm.stage": "暂存",
1115
+ "scm.unstage": "取消暂存",
1116
+ "scm.discard": "放弃更改",
1117
+ "scm.stageAll": "全部暂存",
1118
+ "scm.discardAll": "全部放弃",
1119
+ "scm.empty": "没有更改",
1120
+ "scm.notRepo": "当前目录不是 git 仓库",
1121
+ "scm.gitMissing": "未检测到 git,请先安装 git 后重试",
1122
+ "scm.loading": "读取状态中…",
1123
+ "scm.failed": "操作失败",
1124
+ "scm.viewList": "列表视图",
1125
+ "scm.viewTree": "树视图",
1126
+ "scm.discardConfirmTracked": "放弃对 {count} 个文件的更改?此操作不可恢复。",
1127
+ "scm.discardConfirmUntracked": "删除 {count} 个未跟踪文件?此操作不可恢复。",
1128
+ "preview.noTabs": "没有打开的预览",
1129
+ "preview.newUrlTab": "新建 URL 预览",
1130
+ "preview.collapsePanel": "收起预览面板",
1131
+ "preview.maximize": "最大化预览面板",
1132
+ "preview.restore": "还原面板",
1133
+ "preview.source": "源码",
1134
+ "preview.preview": "预览",
1135
+ "preview.editor": "编辑器",
1136
+ "preview.split": "分屏",
1137
+ "preview.refresh": "刷新",
1138
+ "preview.refresh.updated": "文件已在磁盘更新",
1139
+ "preview.save": "保存",
1140
+ "preview.download": "下载",
1141
+ "preview.copyCode": "复制代码",
1142
+ "preview.copyCodeDone": "已复制",
1143
+ "preview.openExternal": "在系统应用中打开",
1144
+ "preview.dirty": "未保存的更改",
1145
+ "preview.closeLeft": "关闭左侧",
1146
+ "preview.closeRight": "关闭右侧",
1147
+ "preview.closeOthers": "关闭其他",
1148
+ "preview.closeAll": "关闭全部",
1149
+ "preview.closeConfirmTitle": "关闭未保存的标签页",
1150
+ "preview.closeConfirmBody": "{count} 个标签页有未保存的更改,关闭将丢失这些更改。",
1151
+ "preview.saved": "已保存",
1152
+ "preview.saveConflict": "文件已在磁盘上被修改,保存冲突:请刷新后重试",
1153
+ "preview.errorOversized": "文件过大,仅加载前 80,000 字符",
1154
+ "preview.unsupported": "此格式暂不支持预览",
1155
+ "preview.downloadHint": "可在系统应用中打开或下载查看",
1156
+ "preview.url.placeholder": "输入网址,回车打开",
1157
+ "preview.url.hint": "按 Esc 还原",
1158
+ "common.cancel": "取消",
1159
+ "common.confirm": "确定",
1160
+ "common.close": "关闭",
1161
+ "common.delete": "删除",
1162
+ "common.copyPath": "复制路径",
1163
+ "common.copied": "已复制",
1164
+ "explorer.menu.copyPath": "复制路径",
1165
+ "explorer.menu.copyName": "复制名称",
1166
+ "explorer.menu.reveal": "在文件管理器中显示",
1167
+ "explorer.menu.openWithDefault": "用默认应用打开",
1168
+ "explorer.menu.rename": "重命名",
1169
+ "explorer.menu.newFile": "新建文件",
1170
+ "explorer.menu.newFolder": "新建文件夹",
1171
+ "explorer.menu.delete": "删除",
1172
+ "explorer.rename.title": "重命名",
1173
+ "explorer.newFile.title": "新建文件",
1174
+ "explorer.newFolder.title": "新建文件夹",
1175
+ "explorer.deleteConfirmTitle": "删除确认",
1176
+ "explorer.deleteConfirmBody": "确定要删除「{name}」吗?此操作不可恢复。",
1177
+ "explorer.opFailed": "操作失败",
1178
+ "explorer.refToInput": "引用到输入框",
1179
+ "settings.title": "右侧面板",
1180
+ "settings.description": "Explorer / Preview 与 SCM 变更面板的总开关。",
1181
+ "settings.enabled": "启用右侧面板",
1182
+ "settings.enabledHint": "关闭后不再挂载右侧文件树与预览列、不显示浮动展开按钮、不注册 /filemgr 路由,也不做工作区监视与 git 轮询;聚合包其余插件不受影响。",
1183
+ "settings.overridden": "已覆盖",
1184
+ "settings.reset": "恢复默认",
1185
+ "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。",
1186
+ "settings.readOnly": "当前部署的设置只读。",
1187
+ "settings.inherit": "继承",
1188
+ "settings.on": "开",
1189
+ "settings.off": "关",
1190
+ "settings.expand": "展开设置",
1191
+ "settings.collapse": "收起设置",
1192
+ "settings.save": "保存",
1193
+ "settings.saving": "保存中…",
1194
+ "settings.discard": "放弃",
1195
+ "settings.unsaved": "未保存",
1196
+ "settings.saveFailed": "部署未接受这些值,已保留供你修改。",
1197
+ "settings.invalidNumber": "请输入数字,留空则使用默认值。",
1198
+ "dock.filePanel.label": "文件面板",
1199
+ "preview.addToChat": "添加到对话",
1200
+ "picker.title": "引用项目文件",
1201
+ "picker.searchPlaceholder": "搜索工作区文件…",
1202
+ "picker.loading": "加载中…",
1203
+ "picker.emptyDir": "(空目录)",
1204
+ "picker.emptySelection": "选择文件或目录(可多选)",
1205
+ "picker.insert": "插入",
1206
+ "picker.removeAria": "移除",
1207
+ "picker.clearAria": "清空",
1208
+ "picker.closeAria": "关闭"
1209
+ };
1210
+ const en = {
1211
+ "explorer.tabs.files": "Files",
1212
+ "explorer.tabs.changes": "Changes",
1213
+ "explorer.search.placeholder": "Search file names",
1214
+ "explorer.search.searching": "Searching…",
1215
+ "explorer.search.empty": "No matching files",
1216
+ "explorer.search.error": "Search failed",
1217
+ "explorer.search.truncated": "Too many results, showing first {count}",
1218
+ "explorer.tree.empty": "The project is empty",
1219
+ "explorer.collapse": "Collapse panel",
1220
+ "explorer.expand": "Expand panel",
1221
+ "explorer.maximize": "Maximize files panel",
1222
+ "explorer.restore": "Restore panel",
1223
+ "explorer.openPreview": "Open preview",
1224
+ "explorer.drag.dropHint": "Release to insert the file path",
1225
+ "scm.repositories": "Repositories",
1226
+ "scm.changes": "Changes",
1227
+ "scm.staged": "Staged",
1228
+ "scm.unstaged": "Changes",
1229
+ "scm.untracked": "Untracked",
1230
+ "scm.conflicted": "Conflict",
1231
+ "scm.stage": "Stage",
1232
+ "scm.unstage": "Unstage",
1233
+ "scm.discard": "Discard",
1234
+ "scm.stageAll": "Stage all",
1235
+ "scm.discardAll": "Discard all",
1236
+ "scm.empty": "No changes",
1237
+ "scm.notRepo": "Not a git repository",
1238
+ "scm.gitMissing": "Git is not installed. Install git and reload to use the changes panel",
1239
+ "scm.loading": "Loading status…",
1240
+ "scm.failed": "Operation failed",
1241
+ "scm.viewList": "List view",
1242
+ "scm.viewTree": "Tree view",
1243
+ "scm.discardConfirmTracked": "Discard changes in {count} files? This cannot be undone.",
1244
+ "scm.discardConfirmUntracked": "Delete {count} untracked files? This cannot be undone.",
1245
+ "preview.noTabs": "No open previews",
1246
+ "preview.newUrlTab": "New URL preview",
1247
+ "preview.collapsePanel": "Collapse preview panel",
1248
+ "preview.maximize": "Maximize preview panel",
1249
+ "preview.restore": "Restore panel",
1250
+ "preview.source": "Source",
1251
+ "preview.preview": "Preview",
1252
+ "preview.editor": "Editor",
1253
+ "preview.split": "Split",
1254
+ "preview.refresh": "Refresh",
1255
+ "preview.refresh.updated": "File updated on disk",
1256
+ "preview.save": "Save",
1257
+ "preview.download": "Download",
1258
+ "preview.copyCode": "Copy code",
1259
+ "preview.copyCodeDone": "Copied",
1260
+ "preview.openExternal": "Open in system app",
1261
+ "preview.dirty": "Unsaved changes",
1262
+ "preview.closeLeft": "Close left",
1263
+ "preview.closeRight": "Close right",
1264
+ "preview.closeOthers": "Close others",
1265
+ "preview.closeAll": "Close all",
1266
+ "preview.closeConfirmTitle": "Close unsaved tabs",
1267
+ "preview.closeConfirmBody": "{count} tabs have unsaved changes. Closing will lose them.",
1268
+ "preview.saved": "Saved",
1269
+ "preview.saveConflict": "File changed on disk. Save conflict: refresh and retry",
1270
+ "preview.errorOversized": "File too large, only the first 80,000 characters loaded",
1271
+ "preview.unsupported": "Preview not supported for this format",
1272
+ "preview.downloadHint": "Open in a system app or download to view",
1273
+ "preview.url.placeholder": "Enter a URL and press Enter",
1274
+ "preview.url.hint": "Press Esc to revert",
1275
+ "common.cancel": "Cancel",
1276
+ "common.confirm": "OK",
1277
+ "common.close": "Close",
1278
+ "common.delete": "Delete",
1279
+ "common.copyPath": "Copy path",
1280
+ "common.copied": "Copied",
1281
+ "explorer.menu.copyPath": "Copy path",
1282
+ "explorer.menu.copyName": "Copy name",
1283
+ "explorer.menu.reveal": "Reveal in file manager",
1284
+ "explorer.menu.openWithDefault": "Open with default app",
1285
+ "explorer.menu.rename": "Rename",
1286
+ "explorer.menu.newFile": "New file",
1287
+ "explorer.menu.newFolder": "New folder",
1288
+ "explorer.menu.delete": "Delete",
1289
+ "explorer.rename.title": "Rename",
1290
+ "explorer.newFile.title": "New file",
1291
+ "explorer.newFolder.title": "New folder",
1292
+ "explorer.deleteConfirmTitle": "Confirm delete",
1293
+ "explorer.deleteConfirmBody": "Delete \"{name}\"? This cannot be undone.",
1294
+ "explorer.opFailed": "Operation failed",
1295
+ "explorer.refToInput": "Reference to input",
1296
+ "settings.title": "Right panel",
1297
+ "settings.description": "Master switch for the Explorer / Preview and SCM changes panels.",
1298
+ "settings.enabled": "Enable the right panel",
1299
+ "settings.enabledHint": "When off, the Explorer and Preview columns are not mounted, the floating expand button disappears, the /filemgr routes are not registered, and workspace watching and git polling stop. The rest of the family bundle is unaffected.",
1300
+ "settings.overridden": "Overridden",
1301
+ "settings.reset": "Reset",
1302
+ "settings.notExposed": "This DSH version does not expose this plugin's settings namespace; the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to the WEB_SETTINGS_NAMESPACES allowlist of dsh-host-apiproxy and restart.",
1303
+ "settings.readOnly": "Settings are read-only in this deployment.",
1304
+ "settings.inherit": "Inherit",
1305
+ "settings.on": "On",
1306
+ "settings.off": "Off",
1307
+ "settings.expand": "Expand settings",
1308
+ "settings.collapse": "Collapse settings",
1309
+ "settings.save": "Save",
1310
+ "settings.saving": "Saving…",
1311
+ "settings.discard": "Discard",
1312
+ "settings.unsaved": "Unsaved",
1313
+ "settings.saveFailed": "The deployment rejected these values; your edits are kept.",
1314
+ "settings.invalidNumber": "Enter a number, or leave it empty to use the default.",
1315
+ "dock.filePanel.label": "File Panel",
1316
+ "preview.addToChat": "Add to Chat",
1317
+ "picker.title": "Reference Project Files",
1318
+ "picker.searchPlaceholder": "Search workspace files…",
1319
+ "picker.loading": "Loading…",
1320
+ "picker.emptyDir": "(empty directory)",
1321
+ "picker.emptySelection": "Select files or directories (multi-select)",
1322
+ "picker.insert": "Insert",
1323
+ "picker.removeAria": "Remove",
1324
+ "picker.clearAria": "Clear",
1325
+ "picker.closeAria": "Close"
1326
+ };
1327
+ /** The dictionary namespace this plugin owns. */
1328
+ const NS = "filemgr";
1329
+ /** Format one copy string with {name} placeholders. */
1330
+ function format(template, params) {
1331
+ return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
1332
+ }
1333
+ /** Simple dictionary access (zh/en by a global flag the client sets). */
1334
+ const dictionaries = {
1335
+ zh,
1336
+ en
1337
+ };
1338
+ let currentLanguage = "zh";
1339
+ /** Set the active language (the client mirrors the locale service). */
1340
+ function setLanguage(language) {
1341
+ currentLanguage = language === "en" ? "en" : "zh";
1342
+ }
1343
+ let _boundT = null;
1344
+ function bindT(t) {
1345
+ _boundT = t;
1346
+ }
1347
+ /** Translate one key with optional params. */
1348
+ function t(key, params) {
1349
+ if (_boundT !== null) return _boundT(key, params);
1350
+ const template = (dictionaries[currentLanguage] ?? zh)[key] ?? zh[key];
1351
+ return params === void 0 ? template : format(template, params);
1352
+ }
1353
+ //#endregion
1086
1354
  //#region src/client/persist.ts
1087
1355
  /**
1088
1356
  * Persistence helpers for panel preferences: range-validated reads (invalid
@@ -2179,7 +2447,7 @@ window.__ModuleLoader__.load({
2179
2447
  if (!result.ok) return {
2180
2448
  ...item,
2181
2449
  loading: false,
2182
- error: result.error.code === "write-conflict" ? "文件已在磁盘上被修改,保存冲突:请刷新后重试" : result.error.message
2450
+ error: result.error.code === "write-conflict" ? t("preview.saveConflict") : result.error.message
2183
2451
  };
2184
2452
  if (item.content !== sentContent) return {
2185
2453
  ...item,
@@ -2348,28 +2616,6 @@ window.__ModuleLoader__.load({
2348
2616
  function maximizedOverlay(availableWidth) {
2349
2617
  return availableWidth > 0 && availableWidth < 640;
2350
2618
  }
2351
- /** The WCO titlebar height when visible; 0 in a plain browser tab. */
2352
- function titlebarAreaHeight() {
2353
- const wco = navigator.windowControlsOverlay;
2354
- if (wco === void 0 || !wco.visible || wco.getTitlebarAreaRect === void 0) return 0;
2355
- try {
2356
- const height = wco.getTitlebarAreaRect()?.height ?? 0;
2357
- return height > 0 ? Math.round(height) : 0;
2358
- } catch {
2359
- return 0;
2360
- }
2361
- }
2362
- /** Clamp a requested top px into the usable vertical range. */
2363
- function clampFloatingTop(top, viewportHeight, buttonHeight, titlebar) {
2364
- const min = titlebar + 6;
2365
- const max = Math.max(min, viewportHeight - buttonHeight - 6);
2366
- if (!Number.isFinite(top)) return min;
2367
- return Math.min(max, Math.max(min, top));
2368
- }
2369
- /** The default top: aligned with the collapse chevron at the top-right. */
2370
- function topAlignedFloatingTop(viewportHeight, buttonHeight, titlebar) {
2371
- return clampFloatingTop(titlebar + 6, viewportHeight, buttonHeight, titlebar);
2372
- }
2373
2619
  //#endregion
2374
2620
  //#region src/client/layout.ts
2375
2621
  /**
@@ -2457,7 +2703,6 @@ window.__ModuleLoader__.load({
2457
2703
  explorerCol = null;
2458
2704
  explorerHandle = null;
2459
2705
  previewHandle = null;
2460
- floatingButton = null;
2461
2706
  styleObserver = null;
2462
2707
  sizeObserver = null;
2463
2708
  waitObserver = null;
@@ -2519,23 +2764,6 @@ window.__ModuleLoader__.load({
2519
2764
  this.previewHandle = this.createHandle("filemgr-preview-handle", 20, true, "preview");
2520
2765
  frame.appendChild(this.explorerHandle);
2521
2766
  frame.appendChild(this.previewHandle);
2522
- this.floatingButton = document.createElement("button");
2523
- this.floatingButton.type = "button";
2524
- this.floatingButton.className = "filemgr-floating-expand";
2525
- this.floatingButton.setAttribute("aria-label", "Expand explorer");
2526
- this.floatingButton.innerHTML = "<svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M6 3l5 5-5 5\"/></svg>";
2527
- this.floatingButton.addEventListener("click", () => {
2528
- this.toggleExplorer();
2529
- });
2530
- document.body.appendChild(this.floatingButton);
2531
- const overlay = navigator.windowControlsOverlay;
2532
- if (overlay !== void 0) {
2533
- const onGeometryChange = () => {
2534
- this.positionFloatingButton();
2535
- };
2536
- overlay.addEventListener("geometrychange", onGeometryChange);
2537
- this.disposers.push(() => overlay.removeEventListener("geometrychange", onGeometryChange));
2538
- }
2539
2767
  const onKeyDown = (event) => {
2540
2768
  if (event.key !== "Escape") return;
2541
2769
  const target = event.target instanceof Element ? event.target : null;
@@ -2552,483 +2780,231 @@ window.__ModuleLoader__.load({
2552
2780
  if (el === null) return;
2553
2781
  const inline = el.style.gridTemplateColumns;
2554
2782
  if (inline === "") return;
2555
- const tracks = parseGridTracks(inline);
2556
- if (tracks.length >= 2 && tracks.length <= 3) {
2557
- this.shellTracks = tracks;
2558
- this.applyGrid();
2559
- return;
2560
- }
2561
- if (tracks.length === 5 && this.shellTracks.length === 3) return;
2562
- };
2563
- this.styleObserver = new MutationObserver(syncGrid);
2564
- this.styleObserver.observe(frame, {
2565
- attributes: true,
2566
- attributeFilter: ["style"]
2567
- });
2568
- const measure = () => {
2569
- if (this.frame === null) return;
2570
- this.frameWidth = this.frame.getBoundingClientRect().width;
2571
- const sidebar = this.shellTracks.length >= 1 ? trackPx(this.shellTracks[0]) : 0;
2572
- const details = this.shellTracks.length >= 3 ? trackPx(this.shellTracks[2]) : 0;
2573
- const available = Math.max(0, this.frameWidth - sidebar - details);
2574
- const state = this.layout.getSnapshot();
2575
- if (Math.abs(state.availableWidth - available) > .5) this.layout.update((prev) => ({
2576
- ...prev,
2577
- availableWidth: available
2578
- }));
2579
- this.layout.shrinkToFit(this.layout.getSnapshot());
2580
- };
2581
- this.sizeObserver = new ResizeObserver(() => {
2582
- measure();
2583
- this.applyGrid();
2584
- });
2585
- this.sizeObserver.observe(frame);
2586
- this.disposers.push(this.layout.subscribe(() => this.applyGrid()));
2587
- const initial = frame.style.gridTemplateColumns;
2588
- if (initial !== "") {
2589
- const tracks = parseGridTracks(initial);
2590
- if (tracks.length >= 2 && tracks.length <= 3) this.shellTracks = tracks;
2591
- else if (tracks.length === 5 && trackPx(tracks[0]) > 0) this.shellTracks = tracks.slice(0, 3);
2592
- }
2593
- measure();
2594
- this.applyGrid();
2595
- }
2596
- /** Create one drag handle element with its pointer wiring. */
2597
- createHandle(className, hitWidth, reverse, kind) {
2598
- const el = document.createElement("div");
2599
- el.className = className;
2600
- el.style.position = "absolute";
2601
- el.style.top = "0";
2602
- el.style.bottom = "0";
2603
- el.style.zIndex = "30";
2604
- el.style.cursor = "col-resize";
2605
- el.style.width = `${hitWidth}px`;
2606
- if (reverse) el.style.marginLeft = `-${hitWidth}px`;
2607
- el.addEventListener("pointerdown", (event) => {
2608
- const isExplorer = kind === "explorer";
2609
- handlePointerDragStart(event, el, {
2610
- reverse,
2611
- getStartWidth: () => {
2612
- const state = this.layout.getSnapshot();
2613
- return isExplorer ? state.explorerWidth : state.previewWidth;
2614
- },
2615
- compute: (startWidth, deltaX) => dragTargetWidth(isExplorer ? "explorer" : "preview", startWidth, deltaX, this.layout.getSnapshot()),
2616
- onFrame: (width) => {
2617
- this.layout.update((prev) => isExplorer ? {
2618
- ...prev,
2619
- explorerWidth: width
2620
- } : {
2621
- ...prev,
2622
- previewWidth: width
2623
- });
2624
- },
2625
- onEnd: (width) => {
2626
- writeStoredNumber(isExplorer ? KEY_EXPLORER_WIDTH : KEY_PREVIEW_WIDTH, width);
2627
- }
2628
- });
2629
- });
2630
- el.addEventListener("dblclick", () => {
2631
- this.instant(() => {
2632
- const width = kind === "explorer" ? 260 : 480;
2633
- this.layout.update((prev) => kind === "explorer" ? {
2634
- ...prev,
2635
- explorerWidth: width
2636
- } : {
2637
- ...prev,
2638
- previewWidth: width
2639
- });
2640
- writeStoredNumber(kind === "explorer" ? KEY_EXPLORER_WIDTH : KEY_PREVIEW_WIDTH, width);
2641
- this.applyGrid();
2642
- });
2643
- });
2644
- return el;
2645
- }
2646
- /** Toggle explorer collapse (width 0, kept mounted; no transition). */
2647
- toggleExplorer() {
2648
- const state = this.layout.getSnapshot();
2649
- const next = !state.explorerCollapsed;
2650
- this.instant(() => {
2651
- this.layout.update((prev) => ({
2652
- ...prev,
2653
- explorerCollapsed: next
2654
- }));
2655
- try {
2656
- localStorage.setItem(`project-panel-collapse:${state.root}`, next ? "collapsed" : "expanded");
2657
- } catch {}
2658
- this.applyGrid();
2659
- });
2660
- }
2661
- /** Toggle the preview region (open = tabs exist; close keeps tabs). */
2662
- setPreviewOpen(open) {
2663
- this.instant(() => {
2664
- this.layout.update((prev) => ({
2665
- ...prev,
2666
- previewOpen: open
2667
- }));
2668
- this.applyGrid();
2669
- });
2670
- }
2671
- /**
2672
- * Locate the shell conversation header: its bottom border is the
2673
- * horizontal divider under the "Session log" row the button should
2674
- * sit below. Resolved per call (the shell may mount it late); null when
2675
- * the shell has no header (standalone installs, desktop variants).
2676
- */
2677
- findHeaderBottom() {
2678
- const frame = this.frame;
2679
- if (frame === null) return null;
2680
- const header = frame.querySelector("[data-pane=\"conversation\"] header, [class*=\"centerCol\"] header");
2681
- if (header === null) return null;
2682
- const bottom = header.getBoundingClientRect().bottom;
2683
- return Number.isFinite(bottom) ? bottom : null;
2684
- }
2685
- /** Position the floating button: docked at the top-right corner, just
2686
- * below the shell header's bottom divider (fallback: the chevron row). */
2687
- positionFloatingButton() {
2688
- const el = this.floatingButton;
2689
- if (el === null) return;
2690
- const height = window.innerHeight;
2691
- const titlebar = titlebarAreaHeight();
2692
- const headerBottom = this.findHeaderBottom();
2693
- const top = headerBottom !== null ? clampFloatingTop(headerBottom + 8, height, 24, titlebar) : topAlignedFloatingTop(height, 24, titlebar);
2694
- el.style.top = `${Math.round(top)}px`;
2695
- el.style.transform = "none";
2696
- }
2697
- /** Apply one store update with transitions disabled for exactly one frame. */
2698
- instant(fn) {
2699
- const frame = this.frame;
2700
- if (frame === null) {
2701
- fn();
2702
- return;
2703
- }
2704
- frame.setAttribute("data-filemgr-instant", "");
2705
- if (this.instantTimer !== void 0) clearTimeout(this.instantTimer);
2706
- this.instantTimer = setTimeout(() => {
2707
- this.instantTimer = void 0;
2708
- frame.removeAttribute("data-filemgr-instant");
2709
- }, 0);
2710
- fn();
2711
- }
2712
- /** Re-write the frame grid and reposition handles + floating button. */
2713
- applyGrid() {
2714
- const frame = this.frame;
2715
- if (frame === null) return;
2716
- if (this.shellTracks.length !== 3) return;
2717
- const state = this.layout.getSnapshot();
2718
- const width = this.frameWidth > 0 ? this.frameWidth : frame.getBoundingClientRect().width;
2719
- if (state.maximized !== null) {
2720
- this.applyMaximized(frame, state.maximized, width);
2721
- return;
2722
- }
2723
- this.clearMaximizedChrome();
2724
- const explorer = this.layout.explorerWidthPx(state);
2725
- const preview = this.layout.previewWidthPx(state);
2726
- frame.style.gridTemplateColumns = `${this.shellTracks[0]} minmax(0, 1fr) ${this.shellTracks[2]} ${Math.round(preview)}px ${Math.round(explorer)}px`;
2727
- if (this.explorerCol !== null) this.explorerCol.style.visibility = explorer > 0 ? "visible" : "hidden";
2728
- if (this.previewCol !== null) this.previewCol.style.visibility = preview > 0 ? "visible" : "hidden";
2729
- if (this.explorerHandle !== null) {
2730
- const left = Math.round(width - explorer);
2731
- this.explorerHandle.style.left = `${left}px`;
2732
- this.explorerHandle.style.marginLeft = `${-12 / 2}px`;
2733
- this.explorerHandle.style.display = explorer > 0 && state.root !== "" ? "block" : "none";
2734
- }
2735
- if (this.previewHandle !== null) {
2736
- const left = Math.round(width - explorer - preview);
2737
- this.previewHandle.style.left = `${left}px`;
2738
- this.previewHandle.style.display = preview > 0 && state.root !== "" ? "block" : "none";
2739
- }
2740
- const detailsTrack = trackPx(this.shellTracks[2]);
2741
- if (this.detailsHandle === null || !this.detailsHandle.isConnected) this.detailsHandle = frame.querySelector("[data-side=\"details\"]");
2742
- if (this.detailsHandle !== null) this.detailsHandle.style.left = `${Math.round(width - detailsTrack - preview - explorer)}px`;
2743
- if (this.floatingButton !== null) {
2744
- const show = state.root !== "" && state.explorerCollapsed;
2745
- this.floatingButton.style.display = show ? "flex" : "none";
2746
- this.positionFloatingButton();
2747
- }
2748
- }
2749
- /**
2750
- * Maximize layout: the target column takes over the whole frame row (the
2751
- * other tracks collapse to 0px). On narrow viewports the takeover grid is
2752
- * skipped and the column renders as a fixed full-screen overlay instead
2753
- * (issue #315). Everything stays mounted — only geometry changes.
2754
- */
2755
- applyMaximized(frame, target, width) {
2756
- const overlay = maximizedOverlay(this.layout.getSnapshot().availableWidth);
2757
- if (!overlay) frame.style.gridTemplateColumns = maximizedGridTracks(target, width);
2758
- if (this.explorerCol !== null) {
2759
- this.explorerCol.style.visibility = target === "explorer" ? "visible" : "hidden";
2760
- this.explorerCol.classList.toggle("filemgr-maximized", target === "explorer" && overlay);
2761
- }
2762
- if (this.previewCol !== null) {
2763
- this.previewCol.style.visibility = target === "preview" ? "visible" : "hidden";
2764
- this.previewCol.classList.toggle("filemgr-maximized", target === "preview" && overlay);
2765
- }
2766
- if (this.explorerHandle !== null) this.explorerHandle.style.display = "none";
2767
- if (this.previewHandle !== null) this.previewHandle.style.display = "none";
2768
- if (this.floatingButton !== null) this.floatingButton.style.display = "none";
2769
- }
2770
- /** Remove the narrow-screen overlay class from both columns. */
2771
- clearMaximizedChrome() {
2772
- this.explorerCol?.classList.remove("filemgr-maximized");
2773
- this.previewCol?.classList.remove("filemgr-maximized");
2774
- }
2775
- /** Detach everything (plugin unload). */
2776
- dispose() {
2777
- this.waitObserver?.disconnect();
2778
- this.styleObserver?.disconnect();
2779
- this.sizeObserver?.disconnect();
2780
- for (const dispose of this.disposers) dispose();
2781
- this.previewCol?.remove();
2782
- this.explorerCol?.remove();
2783
- this.explorerHandle?.remove();
2784
- this.previewHandle?.remove();
2785
- this.floatingButton?.remove();
2786
- if (this.instantTimer !== void 0) clearTimeout(this.instantTimer);
2787
- if (frameElement === this.frame) frameElement = null;
2788
- this.frame = null;
2789
- }
2790
- };
2791
- //#endregion
2792
- //#region src/client/locales.ts
2793
- /**
2794
- * Locale strings for the panel surfaces (zh/en). The client registers the
2795
- * dictionary through the locale service like the sibling plugins; copy is
2796
- * deliberately short and technical.
2797
- * @module dsh-filemgr/client/locales
2798
- */
2799
- const zh = {
2800
- "explorer.tabs.files": "文件",
2801
- "explorer.tabs.changes": "变更",
2802
- "explorer.search.placeholder": "按文件名搜索",
2803
- "explorer.search.searching": "搜索中…",
2804
- "explorer.search.empty": "没有匹配的文件",
2805
- "explorer.search.error": "搜索失败",
2806
- "explorer.search.truncated": "结果过多,仅显示前 {count} 条",
2807
- "explorer.tree.empty": "项目为空",
2808
- "explorer.collapse": "收起面板",
2809
- "explorer.expand": "展开面板",
2810
- "explorer.maximize": "最大化文件面板",
2811
- "explorer.restore": "还原面板",
2812
- "explorer.openPreview": "打开预览",
2813
- "explorer.drag.dropHint": "松手插入文件路径",
2814
- "scm.repositories": "存储库",
2815
- "scm.changes": "变更",
2816
- "scm.staged": "已暂存",
2817
- "scm.unstaged": "变更",
2818
- "scm.untracked": "未跟踪",
2819
- "scm.conflicted": "冲突",
2820
- "scm.stage": "暂存",
2821
- "scm.unstage": "取消暂存",
2822
- "scm.discard": "放弃更改",
2823
- "scm.stageAll": "全部暂存",
2824
- "scm.discardAll": "全部放弃",
2825
- "scm.empty": "没有更改",
2826
- "scm.notRepo": "当前目录不是 git 仓库",
2827
- "scm.gitMissing": "未检测到 git,请先安装 git 后重试",
2828
- "scm.loading": "读取状态中…",
2829
- "scm.failed": "操作失败",
2830
- "scm.viewList": "列表视图",
2831
- "scm.viewTree": "树视图",
2832
- "scm.discardConfirmTracked": "放弃对 {count} 个文件的更改?此操作不可恢复。",
2833
- "scm.discardConfirmUntracked": "删除 {count} 个未跟踪文件?此操作不可恢复。",
2834
- "preview.noTabs": "没有打开的预览",
2835
- "preview.newUrlTab": "新建 URL 预览",
2836
- "preview.collapsePanel": "收起预览面板",
2837
- "preview.maximize": "最大化预览面板",
2838
- "preview.restore": "还原面板",
2839
- "preview.source": "源码",
2840
- "preview.preview": "预览",
2841
- "preview.editor": "编辑器",
2842
- "preview.split": "分屏",
2843
- "preview.refresh": "刷新",
2844
- "preview.refresh.updated": "文件已在磁盘更新",
2845
- "preview.save": "保存",
2846
- "preview.download": "下载",
2847
- "preview.copyCode": "复制代码",
2848
- "preview.copyCodeDone": "已复制",
2849
- "preview.openExternal": "在系统应用中打开",
2850
- "preview.dirty": "未保存的更改",
2851
- "preview.closeLeft": "关闭左侧",
2852
- "preview.closeRight": "关闭右侧",
2853
- "preview.closeOthers": "关闭其他",
2854
- "preview.closeAll": "关闭全部",
2855
- "preview.closeConfirmTitle": "关闭未保存的标签页",
2856
- "preview.closeConfirmBody": "{count} 个标签页有未保存的更改,关闭将丢失这些更改。",
2857
- "preview.saved": "已保存",
2858
- "preview.saveConflict": "文件已在磁盘上被修改,保存冲突:请刷新后重试",
2859
- "preview.errorOversized": "文件过大,仅加载前 80,000 字符",
2860
- "preview.unsupported": "此格式暂不支持预览",
2861
- "preview.downloadHint": "可在系统应用中打开或下载查看",
2862
- "preview.url.placeholder": "输入网址,回车打开",
2863
- "preview.url.hint": "按 Esc 还原",
2864
- "common.cancel": "取消",
2865
- "common.confirm": "确定",
2866
- "common.close": "关闭",
2867
- "common.delete": "删除",
2868
- "common.copyPath": "复制路径",
2869
- "common.copied": "已复制",
2870
- "explorer.menu.copyPath": "复制路径",
2871
- "explorer.menu.copyName": "复制名称",
2872
- "explorer.menu.reveal": "在文件管理器中显示",
2873
- "explorer.menu.openWithDefault": "用默认应用打开",
2874
- "explorer.menu.rename": "重命名",
2875
- "explorer.menu.newFile": "新建文件",
2876
- "explorer.menu.newFolder": "新建文件夹",
2877
- "explorer.menu.delete": "删除",
2878
- "explorer.rename.title": "重命名",
2879
- "explorer.newFile.title": "新建文件",
2880
- "explorer.newFolder.title": "新建文件夹",
2881
- "explorer.deleteConfirmTitle": "删除确认",
2882
- "explorer.deleteConfirmBody": "确定要删除「{name}」吗?此操作不可恢复。",
2883
- "explorer.opFailed": "操作失败",
2884
- "explorer.refToInput": "引用到输入框",
2885
- "settings.title": "右侧面板",
2886
- "settings.description": "Explorer / Preview 与 SCM 变更面板的总开关。",
2887
- "settings.enabled": "启用右侧面板",
2888
- "settings.enabledHint": "关闭后不再挂载右侧文件树与预览列、不显示浮动展开按钮、不注册 /filemgr 路由,也不做工作区监视与 git 轮询;聚合包其余插件不受影响。",
2889
- "settings.overridden": "已覆盖",
2890
- "settings.reset": "恢复默认",
2891
- "settings.notExposed": "当前 DSH 版本未向设置页暴露本插件的配置命名空间,表单不可用。可编辑 ~/.dsh/settings.yaml 直接配置,或为 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES 白名单补充本命名空间后重启。",
2892
- "settings.readOnly": "当前部署的设置只读。",
2893
- "settings.inherit": "继承",
2894
- "settings.on": "开",
2895
- "settings.off": "关",
2896
- "settings.expand": "展开设置",
2897
- "settings.collapse": "收起设置",
2898
- "settings.save": "保存",
2899
- "settings.saving": "保存中…",
2900
- "settings.discard": "放弃",
2901
- "settings.unsaved": "未保存",
2902
- "settings.saveFailed": "部署未接受这些值,已保留供你修改。",
2903
- "settings.invalidNumber": "请输入数字,留空则使用默认值。"
2904
- };
2905
- const en = {
2906
- "explorer.tabs.files": "Files",
2907
- "explorer.tabs.changes": "Changes",
2908
- "explorer.search.placeholder": "Search file names",
2909
- "explorer.search.searching": "Searching…",
2910
- "explorer.search.empty": "No matching files",
2911
- "explorer.search.error": "Search failed",
2912
- "explorer.search.truncated": "Too many results, showing first {count}",
2913
- "explorer.tree.empty": "The project is empty",
2914
- "explorer.collapse": "Collapse panel",
2915
- "explorer.expand": "Expand panel",
2916
- "explorer.maximize": "Maximize files panel",
2917
- "explorer.restore": "Restore panel",
2918
- "explorer.openPreview": "Open preview",
2919
- "explorer.drag.dropHint": "Release to insert the file path",
2920
- "scm.repositories": "Repositories",
2921
- "scm.changes": "Changes",
2922
- "scm.staged": "Staged",
2923
- "scm.unstaged": "Changes",
2924
- "scm.untracked": "Untracked",
2925
- "scm.conflicted": "Conflict",
2926
- "scm.stage": "Stage",
2927
- "scm.unstage": "Unstage",
2928
- "scm.discard": "Discard",
2929
- "scm.stageAll": "Stage all",
2930
- "scm.discardAll": "Discard all",
2931
- "scm.empty": "No changes",
2932
- "scm.notRepo": "Not a git repository",
2933
- "scm.gitMissing": "Git is not installed. Install git and reload to use the changes panel",
2934
- "scm.loading": "Loading status…",
2935
- "scm.failed": "Operation failed",
2936
- "scm.viewList": "List view",
2937
- "scm.viewTree": "Tree view",
2938
- "scm.discardConfirmTracked": "Discard changes in {count} files? This cannot be undone.",
2939
- "scm.discardConfirmUntracked": "Delete {count} untracked files? This cannot be undone.",
2940
- "preview.noTabs": "No open previews",
2941
- "preview.newUrlTab": "New URL preview",
2942
- "preview.collapsePanel": "Collapse preview panel",
2943
- "preview.maximize": "Maximize preview panel",
2944
- "preview.restore": "Restore panel",
2945
- "preview.source": "Source",
2946
- "preview.preview": "Preview",
2947
- "preview.editor": "Editor",
2948
- "preview.split": "Split",
2949
- "preview.refresh": "Refresh",
2950
- "preview.refresh.updated": "File updated on disk",
2951
- "preview.save": "Save",
2952
- "preview.download": "Download",
2953
- "preview.copyCode": "Copy code",
2954
- "preview.copyCodeDone": "Copied",
2955
- "preview.openExternal": "Open in system app",
2956
- "preview.dirty": "Unsaved changes",
2957
- "preview.closeLeft": "Close left",
2958
- "preview.closeRight": "Close right",
2959
- "preview.closeOthers": "Close others",
2960
- "preview.closeAll": "Close all",
2961
- "preview.closeConfirmTitle": "Close unsaved tabs",
2962
- "preview.closeConfirmBody": "{count} tabs have unsaved changes. Closing will lose them.",
2963
- "preview.saved": "Saved",
2964
- "preview.saveConflict": "File changed on disk. Save conflict: refresh and retry",
2965
- "preview.errorOversized": "File too large, only the first 80,000 characters loaded",
2966
- "preview.unsupported": "Preview not supported for this format",
2967
- "preview.downloadHint": "Open in a system app or download to view",
2968
- "preview.url.placeholder": "Enter a URL and press Enter",
2969
- "preview.url.hint": "Press Esc to revert",
2970
- "common.cancel": "Cancel",
2971
- "common.confirm": "OK",
2972
- "common.close": "Close",
2973
- "common.delete": "Delete",
2974
- "common.copyPath": "Copy path",
2975
- "common.copied": "Copied",
2976
- "explorer.menu.copyPath": "Copy path",
2977
- "explorer.menu.copyName": "Copy name",
2978
- "explorer.menu.reveal": "Reveal in file manager",
2979
- "explorer.menu.openWithDefault": "Open with default app",
2980
- "explorer.menu.rename": "Rename",
2981
- "explorer.menu.newFile": "New file",
2982
- "explorer.menu.newFolder": "New folder",
2983
- "explorer.menu.delete": "Delete",
2984
- "explorer.rename.title": "Rename",
2985
- "explorer.newFile.title": "New file",
2986
- "explorer.newFolder.title": "New folder",
2987
- "explorer.deleteConfirmTitle": "Confirm delete",
2988
- "explorer.deleteConfirmBody": "Delete \"{name}\"? This cannot be undone.",
2989
- "explorer.opFailed": "Operation failed",
2990
- "explorer.refToInput": "Reference to input",
2991
- "settings.title": "Right panel",
2992
- "settings.description": "Master switch for the Explorer / Preview and SCM changes panels.",
2993
- "settings.enabled": "Enable the right panel",
2994
- "settings.enabledHint": "When off, the Explorer and Preview columns are not mounted, the floating expand button disappears, the /filemgr routes are not registered, and workspace watching and git polling stop. The rest of the family bundle is unaffected.",
2995
- "settings.overridden": "Overridden",
2996
- "settings.reset": "Reset",
2997
- "settings.notExposed": "This DSH version does not expose this plugin's settings namespace; the form is unavailable. Edit ~/.dsh/settings.yaml directly, or add the namespace to the WEB_SETTINGS_NAMESPACES allowlist of dsh-host-apiproxy and restart.",
2998
- "settings.readOnly": "Settings are read-only in this deployment.",
2999
- "settings.inherit": "Inherit",
3000
- "settings.on": "On",
3001
- "settings.off": "Off",
3002
- "settings.expand": "Expand settings",
3003
- "settings.collapse": "Collapse settings",
3004
- "settings.save": "Save",
3005
- "settings.saving": "Saving…",
3006
- "settings.discard": "Discard",
3007
- "settings.unsaved": "Unsaved",
3008
- "settings.saveFailed": "The deployment rejected these values; your edits are kept.",
3009
- "settings.invalidNumber": "Enter a number, or leave it empty to use the default."
3010
- };
3011
- /** The dictionary namespace this plugin owns. */
3012
- const NS = "filemgr";
3013
- /** Format one copy string with {name} placeholders. */
3014
- function format(template, params) {
3015
- return template.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
3016
- }
3017
- /** Simple dictionary access (zh/en by a global flag the client sets). */
3018
- const dictionaries = {
3019
- zh,
3020
- en
2783
+ const tracks = parseGridTracks(inline);
2784
+ if (tracks.length >= 2 && tracks.length <= 3) {
2785
+ this.shellTracks = tracks;
2786
+ this.applyGrid();
2787
+ return;
2788
+ }
2789
+ if (tracks.length === 5 && this.shellTracks.length === 3) return;
2790
+ if (tracks.length === 6 && tracks.includes("[dsh-term]") && this.shellTracks.length === 3) {
2791
+ this.applyGrid();
2792
+ return;
2793
+ }
2794
+ };
2795
+ this.styleObserver = new MutationObserver(syncGrid);
2796
+ this.styleObserver.observe(frame, {
2797
+ attributes: true,
2798
+ attributeFilter: ["style"]
2799
+ });
2800
+ const measure = () => {
2801
+ if (this.frame === null) return;
2802
+ this.frameWidth = this.frame.getBoundingClientRect().width;
2803
+ const sidebar = this.shellTracks.length >= 1 ? trackPx(this.shellTracks[0]) : 0;
2804
+ const details = this.shellTracks.length >= 3 ? trackPx(this.shellTracks[2]) : 0;
2805
+ const available = Math.max(0, this.frameWidth - sidebar - details);
2806
+ const state = this.layout.getSnapshot();
2807
+ if (Math.abs(state.availableWidth - available) > .5) this.layout.update((prev) => ({
2808
+ ...prev,
2809
+ availableWidth: available
2810
+ }));
2811
+ this.layout.shrinkToFit(this.layout.getSnapshot());
2812
+ };
2813
+ this.sizeObserver = new ResizeObserver(() => {
2814
+ measure();
2815
+ this.applyGrid();
2816
+ });
2817
+ this.sizeObserver.observe(frame);
2818
+ this.disposers.push(this.layout.subscribe(() => this.applyGrid()));
2819
+ const initial = frame.style.gridTemplateColumns;
2820
+ if (initial !== "") {
2821
+ const tracks = parseGridTracks(initial);
2822
+ if (tracks.length >= 2 && tracks.length <= 3) this.shellTracks = tracks;
2823
+ else if (tracks.length === 5 && trackPx(tracks[0]) > 0) this.shellTracks = tracks.slice(0, 3);
2824
+ }
2825
+ measure();
2826
+ this.applyGrid();
2827
+ }
2828
+ /** Create one drag handle element with its pointer wiring. */
2829
+ createHandle(className, hitWidth, reverse, kind) {
2830
+ const el = document.createElement("div");
2831
+ el.className = className;
2832
+ el.style.position = "absolute";
2833
+ el.style.top = "0";
2834
+ el.style.bottom = "0";
2835
+ el.style.zIndex = "30";
2836
+ el.style.cursor = "col-resize";
2837
+ el.style.width = `${hitWidth}px`;
2838
+ if (reverse) el.style.marginLeft = `-${hitWidth}px`;
2839
+ el.addEventListener("pointerdown", (event) => {
2840
+ const isExplorer = kind === "explorer";
2841
+ handlePointerDragStart(event, el, {
2842
+ reverse,
2843
+ getStartWidth: () => {
2844
+ const state = this.layout.getSnapshot();
2845
+ return isExplorer ? state.explorerWidth : state.previewWidth;
2846
+ },
2847
+ compute: (startWidth, deltaX) => dragTargetWidth(isExplorer ? "explorer" : "preview", startWidth, deltaX, this.layout.getSnapshot()),
2848
+ onFrame: (width) => {
2849
+ this.layout.update((prev) => isExplorer ? {
2850
+ ...prev,
2851
+ explorerWidth: width
2852
+ } : {
2853
+ ...prev,
2854
+ previewWidth: width
2855
+ });
2856
+ },
2857
+ onEnd: (width) => {
2858
+ writeStoredNumber(isExplorer ? KEY_EXPLORER_WIDTH : KEY_PREVIEW_WIDTH, width);
2859
+ }
2860
+ });
2861
+ });
2862
+ el.addEventListener("dblclick", () => {
2863
+ this.instant(() => {
2864
+ const width = kind === "explorer" ? 260 : 480;
2865
+ this.layout.update((prev) => kind === "explorer" ? {
2866
+ ...prev,
2867
+ explorerWidth: width
2868
+ } : {
2869
+ ...prev,
2870
+ previewWidth: width
2871
+ });
2872
+ writeStoredNumber(kind === "explorer" ? KEY_EXPLORER_WIDTH : KEY_PREVIEW_WIDTH, width);
2873
+ this.applyGrid();
2874
+ });
2875
+ });
2876
+ return el;
2877
+ }
2878
+ /** Toggle explorer collapse (width 0, kept mounted; no transition). */
2879
+ toggleExplorer() {
2880
+ const state = this.layout.getSnapshot();
2881
+ const next = !state.explorerCollapsed;
2882
+ this.instant(() => {
2883
+ this.layout.update((prev) => ({
2884
+ ...prev,
2885
+ explorerCollapsed: next
2886
+ }));
2887
+ try {
2888
+ localStorage.setItem(`project-panel-collapse:${state.root}`, next ? "collapsed" : "expanded");
2889
+ } catch {}
2890
+ this.applyGrid();
2891
+ });
2892
+ }
2893
+ /** Toggle the preview region (open = tabs exist; close keeps tabs). */
2894
+ setPreviewOpen(open) {
2895
+ this.instant(() => {
2896
+ this.layout.update((prev) => ({
2897
+ ...prev,
2898
+ previewOpen: open
2899
+ }));
2900
+ this.applyGrid();
2901
+ });
2902
+ }
2903
+ /**
2904
+ * Locate the shell conversation header: its bottom border is the
2905
+ * horizontal divider under the "Session log" row the button should
2906
+ * sit below. Resolved per call (the shell may mount it late); null when
2907
+ * the shell has no header (standalone installs, desktop variants).
2908
+ */
2909
+ findHeaderBottom() {
2910
+ const frame = this.frame;
2911
+ if (frame === null) return null;
2912
+ const header = frame.querySelector("[data-pane=\"conversation\"] header, [class*=\"centerCol\"] header");
2913
+ if (header === null) return null;
2914
+ const bottom = header.getBoundingClientRect().bottom;
2915
+ return Number.isFinite(bottom) ? bottom : null;
2916
+ }
2917
+ /** Apply one store update with transitions disabled for exactly one frame. */
2918
+ instant(fn) {
2919
+ const frame = this.frame;
2920
+ if (frame === null) {
2921
+ fn();
2922
+ return;
2923
+ }
2924
+ frame.setAttribute("data-filemgr-instant", "");
2925
+ if (this.instantTimer !== void 0) clearTimeout(this.instantTimer);
2926
+ this.instantTimer = setTimeout(() => {
2927
+ this.instantTimer = void 0;
2928
+ frame.removeAttribute("data-filemgr-instant");
2929
+ }, 0);
2930
+ fn();
2931
+ }
2932
+ /** Re-write the frame grid and reposition handles + floating button. */
2933
+ applyGrid() {
2934
+ const frame = this.frame;
2935
+ if (frame === null) return;
2936
+ if (this.shellTracks.length !== 3) return;
2937
+ const state = this.layout.getSnapshot();
2938
+ const width = this.frameWidth > 0 ? this.frameWidth : frame.getBoundingClientRect().width;
2939
+ if (state.maximized !== null) {
2940
+ this.applyMaximized(frame, state.maximized, width);
2941
+ return;
2942
+ }
2943
+ this.clearMaximizedChrome();
2944
+ const explorer = this.layout.explorerWidthPx(state);
2945
+ const preview = this.layout.previewWidthPx(state);
2946
+ const dshTermCol = parseGridTracks(frame.style.gridTemplateColumns).includes("[dsh-term]") ? frame.querySelector("[data-dsh-term-col]") : null;
2947
+ const dshTermOpen = dshTermCol !== null && dshTermCol.style.display !== "none" && dshTermCol.getBoundingClientRect().width > 0;
2948
+ const dshTermWidth = dshTermOpen ? Math.round(dshTermCol.getBoundingClientRect().width) : 0;
2949
+ const baseGrid = `${this.shellTracks[0]} minmax(0, 1fr) ${this.shellTracks[2]} ${Math.round(preview)}px ${Math.round(explorer)}px`;
2950
+ frame.style.gridTemplateColumns = dshTermOpen ? `${baseGrid} [dsh-term] ${dshTermWidth}px` : baseGrid;
2951
+ if (this.explorerCol !== null) this.explorerCol.style.visibility = explorer > 0 ? "visible" : "hidden";
2952
+ if (this.previewCol !== null) this.previewCol.style.visibility = preview > 0 ? "visible" : "hidden";
2953
+ if (this.explorerHandle !== null) {
2954
+ const left = Math.round(width - explorer - dshTermWidth);
2955
+ this.explorerHandle.style.left = `${left}px`;
2956
+ this.explorerHandle.style.marginLeft = `${-12 / 2}px`;
2957
+ this.explorerHandle.style.display = explorer > 0 && state.root !== "" ? "block" : "none";
2958
+ }
2959
+ if (this.previewHandle !== null) {
2960
+ const left = Math.round(width - explorer - preview - dshTermWidth);
2961
+ this.previewHandle.style.left = `${left}px`;
2962
+ this.previewHandle.style.display = preview > 0 && state.root !== "" ? "block" : "none";
2963
+ }
2964
+ const detailsTrack = trackPx(this.shellTracks[2]);
2965
+ if (this.detailsHandle === null || !this.detailsHandle.isConnected) this.detailsHandle = frame.querySelector("[data-side=\"details\"]");
2966
+ if (this.detailsHandle !== null) this.detailsHandle.style.left = `${Math.round(width - detailsTrack - preview - explorer - dshTermWidth)}px`;
2967
+ }
2968
+ /**
2969
+ * Maximize layout: the target column takes over the whole frame row (the
2970
+ * other tracks collapse to 0px). On narrow viewports the takeover grid is
2971
+ * skipped and the column renders as a fixed full-screen overlay instead
2972
+ * (issue #315). Everything stays mounted — only geometry changes.
2973
+ */
2974
+ applyMaximized(frame, target, width) {
2975
+ const overlay = maximizedOverlay(this.layout.getSnapshot().availableWidth);
2976
+ if (!overlay) frame.style.gridTemplateColumns = maximizedGridTracks(target, width);
2977
+ if (this.explorerCol !== null) {
2978
+ this.explorerCol.style.visibility = target === "explorer" ? "visible" : "hidden";
2979
+ this.explorerCol.classList.toggle("filemgr-maximized", target === "explorer" && overlay);
2980
+ }
2981
+ if (this.previewCol !== null) {
2982
+ this.previewCol.style.visibility = target === "preview" ? "visible" : "hidden";
2983
+ this.previewCol.classList.toggle("filemgr-maximized", target === "preview" && overlay);
2984
+ }
2985
+ if (this.explorerHandle !== null) this.explorerHandle.style.display = "none";
2986
+ if (this.previewHandle !== null) this.previewHandle.style.display = "none";
2987
+ }
2988
+ /** Remove the narrow-screen overlay class from both columns. */
2989
+ clearMaximizedChrome() {
2990
+ this.explorerCol?.classList.remove("filemgr-maximized");
2991
+ this.previewCol?.classList.remove("filemgr-maximized");
2992
+ }
2993
+ /** Detach everything (plugin unload). */
2994
+ dispose() {
2995
+ this.waitObserver?.disconnect();
2996
+ this.styleObserver?.disconnect();
2997
+ this.sizeObserver?.disconnect();
2998
+ for (const dispose of this.disposers) dispose();
2999
+ this.previewCol?.remove();
3000
+ this.explorerCol?.remove();
3001
+ this.explorerHandle?.remove();
3002
+ this.previewHandle?.remove();
3003
+ if (this.instantTimer !== void 0) clearTimeout(this.instantTimer);
3004
+ if (frameElement === this.frame) frameElement = null;
3005
+ this.frame = null;
3006
+ }
3021
3007
  };
3022
- let currentLanguage = "zh";
3023
- /** Set the active language (the client mirrors the locale service). */
3024
- function setLanguage(language) {
3025
- currentLanguage = language === "en" ? "en" : "zh";
3026
- }
3027
- /** Translate one key with optional params. */
3028
- function t(key, params) {
3029
- const template = (dictionaries[currentLanguage] ?? zh)[key] ?? zh[key];
3030
- return params === void 0 ? template : format(template, params);
3031
- }
3032
3008
  //#endregion
3033
3009
  //#region src/client/hooks/useStore.ts
3034
3010
  /**
@@ -4104,7 +4080,7 @@ window.__ModuleLoader__.load({
4104
4080
  };
4105
4081
  //#endregion
4106
4082
  //#region \0dsh-css:src/client/styles/tokens.module.css.mjs
4107
- const css$4 = ":root{--aion-bg-base:#fff;--aion-bg-1:#f9fafb;--aion-bg-2:#f2f3f5;--aion-bg-3:#e5e6eb;--aion-bg-hover:#f3f4f6;--aion-bg-active:#e5e6eb;--aion-text-primary:#000;--aion-text-secondary:#454d5f;--aion-text-tertiary:#86909c;--aion-text-disabled:#c9cdd4;--aion-primary:#165dff;--aion-success:#00b42a;--aion-warning:#ff7d00;--aion-danger:#f53f3f;--aion-brand:#7583b2;--aion-aou-1:#eff0f6;--aion-aou-2:#e5e7f0;--aion-aou-3:#d1d5e5;--aion-aou-4:#b5bcd6;--aion-aou-5:#97a0c5;--aion-aou-6:#7583b2;--aion-fill-2:#f2f3f5;--aion-fill-3:#e5e6eb;--aion-border-base:#e5e6eb;--aion-overlay-shadow:0 8px 24px #0f172a1f;--aion-font-sans:-apple-system, \"system-ui\", \"Segoe UI\", Roboto, \"Helvetica Neue\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;--aion-font-mono:ui-monospace, \"SF Mono\", SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace}body[data-ds-dark-theme]{--aion-bg-base:#0e0e0e;--aion-bg-1:#1a1a1a;--aion-bg-2:#262626;--aion-bg-3:#333;--aion-bg-hover:#1f1f1f;--aion-bg-active:#2d2d2d;--aion-text-primary:#fff;--aion-text-secondary:#ced3da;--aion-text-tertiary:#737373;--aion-text-disabled:#737373;--aion-primary:#4d9fff;--aion-success:#23c343;--aion-warning:#ff9a2e;--aion-danger:#f76560;--aion-brand:#a1aacb;--aion-aou-1:#2a2a2a;--aion-aou-2:#3d4150;--aion-aou-3:#525a77;--aion-aou-4:#6a749b;--aion-aou-5:#838fba;--aion-aou-6:#a1aacb;--aion-fill-2:#ffffff14;--aion-fill-3:#ffffff1f;--aion-border-base:#333;--aion-overlay-shadow:0 12px 32px #00000073;color-scheme:dark}.filemgr-root{font-family:var(--aion-font-sans);color:var(--aion-text-primary);background-color:var(--aion-bg-1);font-size:13px}.filemgr-root *,.filemgr-root :before,.filemgr-root :after{box-sizing:border-box}.filemgr-root ::-webkit-scrollbar{width:8px;height:8px}.filemgr-root ::-webkit-scrollbar-thumb{background:var(--aion-bg-3);border-radius:4px}.filemgr-root ::-webkit-scrollbar-thumb:hover{background:var(--aion-bg-4,#c9cdd4)}.filemgr-root ::-webkit-scrollbar-track{background:0 0}@media (prefers-reduced-motion:reduce){.filemgr-root *,.filemgr-root :before,.filemgr-root :after{transition:none!important;animation:none!important}}[data-dsh-frame][data-filemgr-instant]{transition:none!important}.filemgr-preview-col,.filemgr-explorer-col{background-color:var(--aion-bg-1);z-index:30;flex-direction:column;min-width:0;display:flex;overflow:hidden}.filemgr-preview-col{border-left:1px solid var(--aion-bg-3)}.filemgr-preview-col.filemgr-maximized,.filemgr-explorer-col.filemgr-maximized{z-index:60;inset:0;position:fixed!important}.filemgr-explorer-col{border-left:1px solid var(--aion-bg-3)}.filemgr-preview-col.filemgr-preview-enter{animation:.25s cubic-bezier(.4,0,.2,1) aPVr2q_filemgr-preview-enter}@keyframes aPVr2q_filemgr-preview-enter{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.filemgr-explorer-handle,.filemgr-preview-handle{touch-action:none}.filemgr-explorer-handle:after,.filemgr-preview-handle:after{content:\"\";background-color:var(--aion-bg-3);opacity:.9;pointer-events:none;border-radius:9999px;width:2px;transition:width .15s cubic-bezier(.4,0,.2,1),background-color .15s cubic-bezier(.4,0,.2,1);position:absolute;top:0;bottom:0;left:0}.filemgr-explorer-handle:hover:after,.filemgr-explorer-handle:active:after,.filemgr-preview-handle:hover:after,.filemgr-preview-handle:active:after{background-color:var(--aion-brand);width:6px}.filemgr-preview-handle:after{opacity:.3;left:auto;right:0}.filemgr-preview-handle:hover:after,.filemgr-preview-handle:active:after{opacity:1}.filemgr-floating-expand{top:calc(6px + env(aPVr2q_titlebar-area-height,0px));background-color:var(--aion-bg-2);width:24px;height:24px;color:var(--aion-text-secondary);cursor:pointer;z-index:100;border:none;border-radius:4px;justify-content:center;align-items:center;transition:background-color .15s cubic-bezier(.4,0,.2,1);display:none;position:fixed;right:8px;box-shadow:0 2px 8px #0000001f}.filemgr-floating-expand:hover{background-color:var(--aion-bg-3);color:var(--aion-text-primary)}.filemgr-floating-expand:active{background-color:var(--aion-bg-active)}.filemgr-floating-expand:focus-visible{outline:2px solid var(--aion-primary);outline-offset:2px}.filemgr-collapse-chevron{top:calc(6px + env(aPVr2q_titlebar-area-height,0px));z-index:30;width:24px;height:24px;color:var(--aion-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;transition:background-color .15s cubic-bezier(.4,0,.2,1);display:flex;position:absolute;right:8px}.filemgr-collapse-chevron:hover{background-color:var(--aion-bg-3);color:var(--aion-text-primary)}.filemgr-collapse-chevron:active{background-color:var(--aion-bg-active)}.filemgr-collapse-chevron:focus-visible{outline:2px solid var(--aion-primary);outline-offset:2px}.filemgr-overlay{z-index:1000;background:#00000059;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.filemgr-dialog{background:var(--aion-bg-base);width:400px;max-width:calc(100vw - 48px);box-shadow:var(--aion-overlay-shadow);font-family:var(--aion-font-sans);border-radius:16px;overflow:hidden}.filemgr-dialog-title{color:var(--aion-text-primary);padding:16px 20px 8px;font-size:14px;font-weight:600}.filemgr-dialog-body{color:var(--aion-text-secondary);padding:0 20px;font-size:13px;line-height:1.6}.filemgr-dialog-actions{justify-content:flex-end;gap:8px;padding:16px 20px 20px;display:flex}.filemgr-btn{border:1px solid var(--aion-bg-3);background:var(--aion-bg-base);height:28px;color:var(--aion-text-primary);cursor:pointer;border-radius:4px;padding:0 14px;font-size:13px;transition:background-color .15s cubic-bezier(.4,0,.2,1)}.filemgr-btn:hover{background:var(--aion-bg-hover)}.filemgr-btn:active{background:var(--aion-bg-active)}.filemgr-btn:focus-visible{outline:2px solid var(--aion-primary);outline-offset:2px}.filemgr-btn-primary{background:var(--aion-primary);border-color:var(--aion-primary);color:#fff}.filemgr-btn-primary:hover{background:var(--aion-primary);opacity:.9}.filemgr-btn-primary:active{opacity:.75;filter:saturate(.9)}.filemgr-btn-danger{background:var(--aion-danger);border-color:var(--aion-danger);color:#fff}.filemgr-btn-danger:hover{opacity:.9}.filemgr-btn-danger:active{opacity:.75}.filemgr-menu{z-index:1100;background:var(--aion-bg-base);min-width:160px;box-shadow:var(--aion-overlay-shadow);border:1px solid var(--aion-bg-3);font-family:var(--aion-font-sans);border-radius:8px;padding:4px;position:fixed}.filemgr-menu-item{height:28px;color:var(--aion-text-primary);cursor:pointer;white-space:nowrap;border-radius:4px;align-items:center;padding:0 10px;font-size:13px;display:flex}.filemgr-menu-item:hover{background:var(--aion-fill-2)}.filemgr-menu-item:focus-visible{outline:2px solid var(--aion-primary);outline-offset:-2px}.filemgr-menu-item-disabled{color:var(--aion-text-disabled);cursor:default}.filemgr-menu-item-disabled:hover{background:0 0}.filemgr-menu-sep{background:var(--aion-bg-3);height:1px;margin:4px 8px}.filemgr-input{border:1px solid var(--aion-bg-3);background:var(--aion-bg-base);width:100%;height:32px;color:var(--aion-text-primary);font-size:13px;font-family:var(--aion-font-sans);box-sizing:border-box;border-radius:6px;outline:none;padding:0 10px}.filemgr-input:focus{border-color:var(--aion-primary)}.filemgr-toast{z-index:1200;background:var(--aion-bg-base);max-width:70vw;color:var(--aion-text-primary);font-size:13px;font-family:var(--aion-font-sans);box-shadow:var(--aion-overlay-shadow);border:1px solid var(--aion-bg-3);border-radius:6px;padding:8px 14px;animation:.2s cubic-bezier(.4,0,.2,1) aPVr2q_filemgr-toast-in;position:fixed;bottom:32px;left:50%;transform:translate(-50%)}@keyframes aPVr2q_filemgr-toast-in{0%{opacity:0;transform:translate(-50%)translateY(8px)}to{opacity:1;transform:translate(-50%)translateY(0)}}";
4083
+ const css$4 = ":root{--aion-bg-base:#fff;--aion-bg-1:#f9fafb;--aion-bg-2:#f2f3f5;--aion-bg-3:#e5e6eb;--aion-bg-hover:#f3f4f6;--aion-bg-active:#e5e6eb;--aion-text-primary:#000;--aion-text-secondary:#454d5f;--aion-text-tertiary:#86909c;--aion-text-disabled:#c9cdd4;--aion-primary:#165dff;--aion-success:#00b42a;--aion-warning:#ff7d00;--aion-danger:#f53f3f;--aion-brand:#7583b2;--aion-aou-1:#eff0f6;--aion-aou-2:#e5e7f0;--aion-aou-3:#d1d5e5;--aion-aou-4:#b5bcd6;--aion-aou-5:#97a0c5;--aion-aou-6:#7583b2;--aion-fill-2:#f2f3f5;--aion-fill-3:#e5e6eb;--aion-border-base:#e5e6eb;--aion-overlay-shadow:0 8px 24px #0f172a1f;--aion-font-sans:-apple-system, \"system-ui\", \"Segoe UI\", Roboto, \"Helvetica Neue\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;--aion-font-mono:ui-monospace, \"SF Mono\", SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace}body[data-ds-dark-theme]{--aion-bg-base:#0e0e0e;--aion-bg-1:#1a1a1a;--aion-bg-2:#262626;--aion-bg-3:#333;--aion-bg-hover:#1f1f1f;--aion-bg-active:#2d2d2d;--aion-text-primary:#fff;--aion-text-secondary:#ced3da;--aion-text-tertiary:#737373;--aion-text-disabled:#737373;--aion-primary:#4d9fff;--aion-success:#23c343;--aion-warning:#ff9a2e;--aion-danger:#f76560;--aion-brand:#a1aacb;--aion-aou-1:#2a2a2a;--aion-aou-2:#3d4150;--aion-aou-3:#525a77;--aion-aou-4:#6a749b;--aion-aou-5:#838fba;--aion-aou-6:#a1aacb;--aion-fill-2:#ffffff14;--aion-fill-3:#ffffff1f;--aion-border-base:#333;--aion-overlay-shadow:0 12px 32px #00000073;color-scheme:dark}.filemgr-root{font-family:var(--aion-font-sans);color:var(--aion-text-primary);background-color:var(--aion-bg-1);font-size:13px}.filemgr-root *,.filemgr-root :before,.filemgr-root :after{box-sizing:border-box}.filemgr-root ::-webkit-scrollbar{width:8px;height:8px}.filemgr-root ::-webkit-scrollbar-thumb{background:var(--aion-bg-3);border-radius:4px}.filemgr-root ::-webkit-scrollbar-thumb:hover{background:var(--aion-bg-4,#c9cdd4)}.filemgr-root ::-webkit-scrollbar-track{background:0 0}@media (prefers-reduced-motion:reduce){.filemgr-root *,.filemgr-root :before,.filemgr-root :after{transition:none!important;animation:none!important}}[data-dsh-frame][data-filemgr-instant]{transition:none!important}.filemgr-preview-col,.filemgr-explorer-col{background-color:var(--aion-bg-1);z-index:30;flex-direction:column;min-width:0;display:flex;overflow:hidden}.filemgr-preview-col{border-left:1px solid var(--aion-bg-3)}.filemgr-preview-col.filemgr-maximized,.filemgr-explorer-col.filemgr-maximized{z-index:60;inset:0;position:fixed!important}.filemgr-explorer-col{border-left:1px solid var(--aion-bg-3)}.filemgr-preview-col.filemgr-preview-enter{animation:.25s cubic-bezier(.4,0,.2,1) aPVr2q_filemgr-preview-enter}@keyframes aPVr2q_filemgr-preview-enter{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.filemgr-explorer-handle,.filemgr-preview-handle{touch-action:none;cursor:col-resize}.filemgr-collapse-chevron{top:calc(6px + env(aPVr2q_titlebar-area-height,0px));z-index:30;width:24px;height:24px;color:var(--aion-text-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;transition:background-color .15s cubic-bezier(.4,0,.2,1);display:flex;position:absolute;right:8px}.filemgr-collapse-chevron:hover{background-color:var(--aion-bg-3);color:var(--aion-text-primary)}.filemgr-collapse-chevron:active{background-color:var(--aion-bg-active)}.filemgr-collapse-chevron:focus-visible{outline:2px solid var(--aion-primary);outline-offset:2px}.filemgr-overlay{z-index:1000;background:#00000059;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.filemgr-dialog{background:var(--aion-bg-base);width:400px;max-width:calc(100vw - 48px);box-shadow:var(--aion-overlay-shadow);font-family:var(--aion-font-sans);border-radius:16px;overflow:hidden}.filemgr-dialog-title{color:var(--aion-text-primary);padding:16px 20px 8px;font-size:14px;font-weight:600}.filemgr-dialog-body{color:var(--aion-text-secondary);padding:0 20px;font-size:13px;line-height:1.6}.filemgr-dialog-actions{justify-content:flex-end;gap:8px;padding:16px 20px 20px;display:flex}.filemgr-btn{border:1px solid var(--aion-bg-3);background:var(--aion-bg-base);height:28px;color:var(--aion-text-primary);cursor:pointer;border-radius:4px;padding:0 14px;font-size:13px;transition:background-color .15s cubic-bezier(.4,0,.2,1)}.filemgr-btn:hover{background:var(--aion-bg-hover)}.filemgr-btn:active{background:var(--aion-bg-active)}.filemgr-btn:focus-visible{outline:2px solid var(--aion-primary);outline-offset:2px}.filemgr-btn-primary{background:var(--aion-primary);border-color:var(--aion-primary);color:#fff}.filemgr-btn-primary:hover{background:var(--aion-primary);opacity:.9}.filemgr-btn-primary:active{opacity:.75;filter:saturate(.9)}.filemgr-btn-danger{background:var(--aion-danger);border-color:var(--aion-danger);color:#fff}.filemgr-btn-danger:hover{opacity:.9}.filemgr-btn-danger:active{opacity:.75}.filemgr-menu{z-index:1100;background:var(--aion-bg-base);min-width:160px;box-shadow:var(--aion-overlay-shadow);border:1px solid var(--aion-bg-3);font-family:var(--aion-font-sans);border-radius:8px;padding:4px;position:fixed}.filemgr-menu-item{height:28px;color:var(--aion-text-primary);cursor:pointer;white-space:nowrap;border-radius:4px;align-items:center;padding:0 10px;font-size:13px;display:flex}.filemgr-menu-item:hover{background:var(--aion-fill-2)}.filemgr-menu-item:focus-visible{outline:2px solid var(--aion-primary);outline-offset:-2px}.filemgr-menu-item-disabled{color:var(--aion-text-disabled);cursor:default}.filemgr-menu-item-disabled:hover{background:0 0}.filemgr-menu-sep{background:var(--aion-bg-3);height:1px;margin:4px 8px}.filemgr-input{border:1px solid var(--aion-bg-3);background:var(--aion-bg-base);width:100%;height:32px;color:var(--aion-text-primary);font-size:13px;font-family:var(--aion-font-sans);box-sizing:border-box;border-radius:6px;outline:none;padding:0 10px}.filemgr-input:focus{border-color:var(--aion-primary)}.filemgr-toast{z-index:1200;background:var(--aion-bg-base);max-width:70vw;color:var(--aion-text-primary);font-size:13px;font-family:var(--aion-font-sans);box-shadow:var(--aion-overlay-shadow);border:1px solid var(--aion-bg-3);border-radius:6px;padding:8px 14px;animation:.2s cubic-bezier(.4,0,.2,1) aPVr2q_filemgr-toast-in;position:fixed;bottom:32px;left:50%;transform:translate(-50%)}@keyframes aPVr2q_filemgr-toast-in{0%{opacity:0;transform:translate(-50%)translateY(8px)}to{opacity:1;transform:translate(-50%)translateY(0)}}";
4108
4084
  const tagId$4 = "@lijian-ui/dsh-file-manager/tokens.module.css";
4109
4085
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
4110
4086
  const tag = document.createElement("style");
@@ -6195,7 +6171,7 @@ window.__ModuleLoader__.load({
6195
6171
  onClick: handleAddToChat,
6196
6172
  role: "button",
6197
6173
  tabIndex: -1,
6198
- children: "添加到对话"
6174
+ children: t("preview.addToChat")
6199
6175
  }),
6200
6176
  closingIds !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
6201
6177
  title: t("preview.closeConfirmTitle"),
@@ -6220,6 +6196,15 @@ window.__ModuleLoader__.load({
6220
6196
  * failure degrades the panels, never the GUI boot.
6221
6197
  * @module dsh-filemgr/client/mount
6222
6198
  */
6199
+ /**
6200
+ * Wrapper that subscribes to locale revision so createRoot-mounted panels
6201
+ * re-render on language switch (slot outlets do this automatically via
6202
+ * useLocaleRevision; createRoot trees must opt in explicitly).
6203
+ */
6204
+ function LocaleAwareRoot({ children, locale }) {
6205
+ (0, react.useSyncExternalStore)((cb) => locale.subscribe(cb), () => locale.getSnapshot().revision);
6206
+ return children;
6207
+ }
6223
6208
  const EXPLORER_COL_SELECTOR = "[data-filemgr-explorer-col]";
6224
6209
  const PREVIEW_COL_SELECTOR = "[data-filemgr-preview-col]";
6225
6210
  /** Wait for one selector (the shell/frame mounts after boot settlement). */
@@ -6258,23 +6243,29 @@ window.__ModuleLoader__.load({
6258
6243
  * a preview selection (path + start/end line).
6259
6244
  * @returns a disposer unmounting both trees.
6260
6245
  */
6261
- function mountPanels(stores, onToggleExplorer, onReference, onAddFileReference) {
6246
+ function mountPanels(stores, onToggleExplorer, onReference, onAddFileReference, locale) {
6262
6247
  let explorerRoot;
6263
6248
  let previewRoot;
6264
6249
  const disposers = [];
6265
6250
  disposers.push(waitForElement(EXPLORER_COL_SELECTOR, (el) => {
6266
6251
  explorerRoot = (0, react_dom_client.createRoot)(el);
6267
- explorerRoot.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExplorerPanel, {
6268
- stores,
6269
- onToggleCollapse: onToggleExplorer,
6270
- onReference
6252
+ explorerRoot.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LocaleAwareRoot, {
6253
+ locale,
6254
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExplorerPanel, {
6255
+ stores,
6256
+ onToggleCollapse: onToggleExplorer,
6257
+ onReference
6258
+ })
6271
6259
  }));
6272
6260
  }));
6273
6261
  disposers.push(waitForElement(PREVIEW_COL_SELECTOR, (el) => {
6274
6262
  previewRoot = (0, react_dom_client.createRoot)(el);
6275
- previewRoot.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreviewPanel, {
6276
- stores,
6277
- onAddFileReference
6263
+ previewRoot.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LocaleAwareRoot, {
6264
+ locale,
6265
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PreviewPanel, {
6266
+ stores,
6267
+ onAddFileReference
6268
+ })
6278
6269
  }));
6279
6270
  }));
6280
6271
  return () => {
@@ -6852,11 +6843,11 @@ window.__ModuleLoader__.load({
6852
6843
  }), isDir && isOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6853
6844
  className: picker_module_css_default.note,
6854
6845
  style: { paddingLeft: 8 + (depth + 1) * 16 },
6855
- children: "加载中…"
6846
+ children: t("picker.loading")
6856
6847
  }) : children.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6857
6848
  className: picker_module_css_default.note,
6858
6849
  style: { paddingLeft: 8 + (depth + 1) * 16 },
6859
- children: "(空目录)"
6850
+ children: t("picker.emptyDir")
6860
6851
  }) : children.map((child) => renderNode(child, depth + 1)) })] }, entry.path);
6861
6852
  };
6862
6853
  return renderNode;
@@ -6891,12 +6882,12 @@ window.__ModuleLoader__.load({
6891
6882
  className: picker_module_css_default.titleRow,
6892
6883
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6893
6884
  className: picker_module_css_default.title,
6894
- children: "引用项目文件"
6885
+ children: t("picker.title")
6895
6886
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6896
6887
  type: "button",
6897
6888
  className: picker_module_css_default.closeBtn,
6898
6889
  onClick: onClose,
6899
- "aria-label": "关闭",
6890
+ "aria-label": t("picker.closeAria"),
6900
6891
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, { size: 16 })
6901
6892
  })]
6902
6893
  }),
@@ -6909,7 +6900,7 @@ window.__ModuleLoader__.load({
6909
6900
  }),
6910
6901
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6911
6902
  className: picker_module_css_default.searchInput,
6912
- placeholder: "搜索工作区文件…",
6903
+ placeholder: t("picker.searchPlaceholder"),
6913
6904
  value: search,
6914
6905
  onChange: (e) => setSearch(e.target.value),
6915
6906
  autoFocus: true
@@ -6918,7 +6909,7 @@ window.__ModuleLoader__.load({
6918
6909
  type: "button",
6919
6910
  className: picker_module_css_default.searchClear,
6920
6911
  onClick: () => setSearch(""),
6921
- "aria-label": "清空",
6912
+ "aria-label": t("picker.clearAria"),
6922
6913
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, { size: 13 })
6923
6914
  })
6924
6915
  ]
@@ -6934,10 +6925,10 @@ window.__ModuleLoader__.load({
6934
6925
  className: picker_module_css_default.body,
6935
6926
  children: searchResults !== null ? searching ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6936
6927
  className: picker_module_css_default.note,
6937
- children: "搜索中…"
6928
+ children: t("explorer.search.searching")
6938
6929
  }) : searchResults.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6939
6930
  className: picker_module_css_default.note,
6940
- children: "没有匹配的文件"
6931
+ children: t("explorer.search.empty")
6941
6932
  }) : searchResults.map((hit) => {
6942
6933
  const isSel = selected.has(hit.path);
6943
6934
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6969,10 +6960,10 @@ window.__ModuleLoader__.load({
6969
6960
  }, hit.path);
6970
6961
  }) : rootLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6971
6962
  className: picker_module_css_default.note,
6972
- children: "加载中…"
6963
+ children: t("picker.loading")
6973
6964
  }) : rootEntries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6974
6965
  className: picker_module_css_default.note,
6975
- children: "(空目录)"
6966
+ children: t("picker.emptyDir")
6976
6967
  }) : rootEntries.map((entry) => tree(entry, 0))
6977
6968
  }),
6978
6969
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6981,7 +6972,7 @@ window.__ModuleLoader__.load({
6981
6972
  className: picker_module_css_default.chips,
6982
6973
  children: selectedList.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6983
6974
  className: picker_module_css_default.chipsEmpty,
6984
- children: "选择文件或目录(可多选)"
6975
+ children: t("picker.emptySelection")
6985
6976
  }) : selectedList.map((pick) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6986
6977
  className: picker_module_css_default.chip,
6987
6978
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
@@ -6992,7 +6983,7 @@ window.__ModuleLoader__.load({
6992
6983
  type: "button",
6993
6984
  className: picker_module_css_default.chipX,
6994
6985
  onClick: () => toggleSelect(pick.path, pick.isDir),
6995
- "aria-label": "移除",
6986
+ "aria-label": t("picker.removeAria"),
6996
6987
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, { size: 11 })
6997
6988
  })]
6998
6989
  }, pick.path))
@@ -7002,13 +6993,13 @@ window.__ModuleLoader__.load({
7002
6993
  type: "button",
7003
6994
  className: picker_module_css_default.cancelBtn,
7004
6995
  onClick: onClose,
7005
- children: "取消"
6996
+ children: t("common.cancel")
7006
6997
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7007
6998
  type: "button",
7008
6999
  className: picker_module_css_default.insertBtn,
7009
7000
  disabled: selectedList.length === 0,
7010
7001
  onClick: () => onInsert(selectedList),
7011
- children: ["插入", selectedList.length > 0 ? ` (${selectedList.length})` : ""]
7002
+ children: [t("picker.insert"), selectedList.length > 0 ? ` (${selectedList.length})` : ""]
7012
7003
  })]
7013
7004
  })]
7014
7005
  })
@@ -7339,6 +7330,75 @@ window.__ModuleLoader__.load({
7339
7330
  document.head.appendChild(tag);
7340
7331
  }
7341
7332
  //#endregion
7333
+ //#region src/client/DockItem.tsx
7334
+ const STYLE_ID = "dsh-dock-item-style";
7335
+ function ensureStyle() {
7336
+ if (typeof document === "undefined") return;
7337
+ if (document.getElementById(STYLE_ID) !== null) return;
7338
+ const tag = document.createElement("style");
7339
+ tag.id = STYLE_ID;
7340
+ tag.textContent = ".dsh-dock-item{width:32px;height:32px;border:none;border-radius:9999px;background:#000;color:#fff;cursor:pointer;position:relative;transition:transform .15s ease,background .18s ease;display:inline-flex;align-items:center;justify-content:center}.dsh-dock-item:hover{transform:scale(1.15);background:#1a1a1a}.dsh-dock-item-active{background:var(--aion-primary,#6ea0ff)}.dsh-dock-tooltip{position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);padding:2px 8px;font-size:12px;line-height:1.4;white-space:nowrap;color:#fff;background:rgba(20,20,30,.9);border:1px solid rgba(255,255,255,.12);border-radius:6px;opacity:0;pointer-events:none;transition:opacity .15s}.dsh-dock-item:hover .dsh-dock-tooltip{opacity:1}";
7341
+ document.head.appendChild(tag);
7342
+ }
7343
+ ensureStyle();
7344
+ function DockItem(props) {
7345
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7346
+ type: "button",
7347
+ className: `dsh-dock-item${props.active ? " dsh-dock-item-active" : ""}`,
7348
+ onClick: props.onClick,
7349
+ "aria-label": props.label,
7350
+ "aria-pressed": props.active,
7351
+ title: props.label,
7352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7353
+ className: "dsh-dock-tooltip",
7354
+ children: props.label
7355
+ }), props.children]
7356
+ });
7357
+ }
7358
+ //#endregion
7359
+ //#region src/client/FilePanelDockItem.tsx
7360
+ /**
7361
+ * FilePanelDockItem — the file-panel dock button rendered into the conversation
7362
+ * session header (the `conversation.session.header.utilities` slot).
7363
+ *
7364
+ * This plugin owns its own dock button independently: dsh-term renders its own
7365
+ * terminal button, dsh-file-manager renders this file-panel button. The two
7366
+ * are fully decoupled — install either alone and you get just its button;
7367
+ * install both and both buttons appear side by side with shared magnification.
7368
+ *
7369
+ * Clicking broadcasts `dsh-dock:toggle-filepanel` (handled in dsh-file-manager
7370
+ * index); the open state is mirrored back through `dsh-dock:filepanel-state`.
7371
+ * @module dsh-file-manager/client/FilePanelDockItem
7372
+ */
7373
+ const EV = {
7374
+ toggleFile: "dsh-dock:toggle-filepanel",
7375
+ fileState: "dsh-dock:filepanel-state"
7376
+ };
7377
+ function FilePanelDockItem() {
7378
+ const [active, setActive] = (0, react.useState)(false);
7379
+ (0, react.useEffect)(() => {
7380
+ const onState = (e) => setActive(Boolean(e.detail));
7381
+ window.addEventListener(EV.fileState, onState);
7382
+ return () => window.removeEventListener(EV.fileState, onState);
7383
+ }, []);
7384
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DockItem, {
7385
+ active,
7386
+ label: t("dock.filePanel.label"),
7387
+ onClick: () => window.dispatchEvent(new CustomEvent(EV.toggleFile)),
7388
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
7389
+ width: "16",
7390
+ height: "16",
7391
+ viewBox: "0 0 24 24",
7392
+ fill: "none",
7393
+ stroke: "currentColor",
7394
+ strokeWidth: "2",
7395
+ strokeLinecap: "round",
7396
+ strokeLinejoin: "round",
7397
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 14 1.45-2.9a2 2 0 0 1 1.79-1.1h11.76a2 2 0 0 1 1.94 1.5l1.55 6a2 2 0 0 1-1.94 2.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.5a2 2 0 0 1 1.59.79l.91 1.21a2 2 0 0 0 1.59.79H18a2 2 0 0 1 2 2v2" })
7398
+ })
7399
+ });
7400
+ }
7401
+ //#endregion
7342
7402
  //#region src/client/index.ts
7343
7403
  /**
7344
7404
  * FileManager right-panel system — browser half: mounts the explorer and preview
@@ -7365,7 +7425,16 @@ window.__ModuleLoader__.load({
7365
7425
  const panelApi = new PanelApi();
7366
7426
  /** Apply the browser half. */
7367
7427
  function apply(ctx) {
7428
+ ctx.inject(["slots"], (scope) => {
7429
+ scope.slots.inject("conversation.session.header.utilities", () => scope.slots.register({
7430
+ name: "conversation.session.header.utilities",
7431
+ id: "filemgr-dock-item",
7432
+ order: -2,
7433
+ inject: () => ({})
7434
+ }, FilePanelDockItem));
7435
+ });
7368
7436
  ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-filemgr: dictionaries");
7437
+ bindT(ctx.locale.bind(NS));
7369
7438
  ctx.effect(() => registerFileReferenceSource(ctx), "dsh-filemgr: file reference source");
7370
7439
  ctx.inject([
7371
7440
  "slots",
@@ -7561,7 +7630,7 @@ window.__ModuleLoader__.load({
7561
7630
  syncLanguage();
7562
7631
  try {
7563
7632
  layout.mount();
7564
- mountPanels(stores, () => layout.toggleExplorer(), onReference, onAddFileReference);
7633
+ mountPanels(stores, () => layout.toggleExplorer(), onReference, onAddFileReference, ctx.locale);
7565
7634
  } catch (error) {
7566
7635
  console.error("[dsh-filemgr] mount failed:", error);
7567
7636
  }