@lijian-ui/dsh-term 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/client.js +468 -87
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +454 -29
  4. package/lib/tsconfig.client.tsbuildinfo +1 -1
  5. package/lib/tsconfig.host.tsbuildinfo +1 -1
  6. package/lib/types/client/client-i18n.d.ts +29 -0
  7. package/lib/types/client/client-i18n.d.ts.map +1 -0
  8. package/lib/types/client/i18n-seat.d.ts +15 -0
  9. package/lib/types/client/i18n-seat.d.ts.map +1 -0
  10. package/lib/types/client/index.d.ts +7 -0
  11. package/lib/types/client/index.d.ts.map +1 -1
  12. package/lib/types/client/term/AnimatedDock.d.ts.map +1 -1
  13. package/lib/types/client/term/TerminalPanel.d.ts +8 -3
  14. package/lib/types/client/term/TerminalPanel.d.ts.map +1 -1
  15. package/lib/types/core/types.d.ts +1 -0
  16. package/lib/types/core/types.d.ts.map +1 -1
  17. package/lib/types/gateway/i18n.d.ts +17 -0
  18. package/lib/types/gateway/i18n.d.ts.map +1 -0
  19. package/lib/types/host/routes.d.ts +3 -1
  20. package/lib/types/host/routes.d.ts.map +1 -1
  21. package/lib/types/index.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/client/client-i18n.ts +68 -0
  24. package/src/client/i18n-seat.ts +27 -0
  25. package/src/client/index.ts +22 -3
  26. package/src/client/term/AnimatedDock.tsx +3 -1
  27. package/src/client/term/TerminalPanel.tsx +327 -44
  28. package/src/client/term/api.ts +17 -2
  29. package/src/client/term/chat-helper.ts +28 -0
  30. package/src/client/term/term.module.css +137 -0
  31. package/src/core/types.ts +22 -4
  32. package/src/gateway/i18n.ts +67 -0
  33. package/src/host/pty-service.ts +160 -17
  34. package/src/host/routes.ts +44 -4
  35. package/src/index.ts +31 -1
package/lib/client.js CHANGED
@@ -10,6 +10,56 @@ window.__ModuleLoader__.load({
10
10
  let react = require("react");
11
11
  let react_dom_client = require("react-dom/client");
12
12
  let react_jsx_runtime = require("react/jsx-runtime");
13
+ //#region src/client/client-i18n.ts
14
+ /**
15
+ * 客户端 i18n 字典:dsh-term 面板的中英文翻译。
16
+ *
17
+ * 通过 ctx.locale.register('dsh-term', dicts) 注册,
18
+ * 组件中用 const t = ctx.locale.bind('dsh-term') 获取翻译函数。
19
+ *
20
+ * @module dsh-term/client-i18n
21
+ */
22
+ const zh = {
23
+ "ui.panel.title": "终端",
24
+ "ui.panel.addTabTitle": "新建终端",
25
+ "ui.panel.collapseTitle": "收起",
26
+ "ui.panel.emptyHint": "点击 + 新建终端",
27
+ "ui.tab.closeAria": "关闭 {0}",
28
+ "msg.sessionExited": "[dsh-term] 进程已退出(code {0})",
29
+ "msg.spawnFailed": "[dsh-term] 启动失败: {0}",
30
+ "ui.panel.shellTitle": "Shell",
31
+ "ui.shell.bash": "Bash",
32
+ "ui.shell.zsh": "Zsh",
33
+ "ui.shell.powershell": "PowerShell",
34
+ "ui.shell.cmd": "命令提示符",
35
+ "ui.shell.gitbash": "Git Bash",
36
+ "ui.panel.reopenTitle": "恢复终端",
37
+ "ui.panel.backgroundSessions": "后台终端",
38
+ "ui.panel.noBackground": "无后台终端",
39
+ "ui.panel.addToChat": "添加到对话",
40
+ "ui.dock.label": "终端"
41
+ };
42
+ const en = {
43
+ "ui.panel.title": "Terminal",
44
+ "ui.panel.addTabTitle": "New Terminal",
45
+ "ui.panel.collapseTitle": "Collapse",
46
+ "ui.panel.emptyHint": "Click + to create a terminal",
47
+ "ui.tab.closeAria": "Close {0}",
48
+ "msg.sessionExited": "[dsh-term] Process exited (code {0})",
49
+ "msg.spawnFailed": "[dsh-term] Spawn failed: {0}",
50
+ "ui.panel.shellTitle": "Shell",
51
+ "ui.shell.bash": "Bash",
52
+ "ui.shell.zsh": "Zsh",
53
+ "ui.shell.powershell": "PowerShell",
54
+ "ui.shell.cmd": "Command Prompt",
55
+ "ui.shell.gitbash": "Git Bash",
56
+ "ui.panel.reopenTitle": "Reopen terminal",
57
+ "ui.panel.backgroundSessions": "Background terminals",
58
+ "ui.panel.noBackground": "No background terminals",
59
+ "ui.panel.addToChat": "Add to chat",
60
+ "ui.dock.label": "Terminal"
61
+ };
62
+ //#endregion
13
63
  //#region src/client/term/api.ts
14
64
  async function call(path, body) {
15
65
  const envelope = await (await fetch(path, {
@@ -41,10 +91,22 @@ window.__ModuleLoader__.load({
41
91
  rows
42
92
  });
43
93
  }
44
- /** Close one session. */
94
+ /** Close one session (kill the PTY). */
45
95
  close(id) {
46
96
  return call("/dsh-term/close", { id });
47
97
  }
98
+ /** Detach a session (keep the PTY alive, mark as detached). */
99
+ detach(id) {
100
+ return call("/dsh-term/detach", { id });
101
+ }
102
+ /** Reattach to a detached session; returns the wire info. */
103
+ reattach(id) {
104
+ return call("/dsh-term/reattach", { id });
105
+ }
106
+ /** List available shells on the host. */
107
+ shells() {
108
+ return call("/dsh-term/shells");
109
+ }
48
110
  /** Current session listing (used on reconnect). */
49
111
  list() {
50
112
  return call("/dsh-term/list");
@@ -7176,10 +7238,29 @@ window.__ModuleLoader__.load({
7176
7238
  })()));
7177
7239
  }));
7178
7240
  //#endregion
7179
- //#region src/client/term/xterm-styles.ts
7241
+ //#region src/client/term/chat-helper.ts
7180
7242
  var import_xterm = require_xterm();
7181
7243
  var import_addon_fit = require_addon_fit();
7182
7244
  /**
7245
+ * Append `text` to the current session's composer draft.
7246
+ * Returns false when there is no active session or the conversation service
7247
+ * is unavailable.
7248
+ */
7249
+ function appendToConversationDraft(ctx, text) {
7250
+ const sessionId = ctx.sessions.list.getSnapshot().current;
7251
+ if (sessionId === void 0) return false;
7252
+ const actx = ctx.sessions.scope(sessionId);
7253
+ if (actx === void 0) return false;
7254
+ const conversation = ctx.conversation;
7255
+ if (conversation === void 0) return false;
7256
+ const input = conversation.input.for(actx);
7257
+ const draft = input.state.getSnapshot().draft;
7258
+ input.setDraft(draft.trim() === "" ? text : `${draft}\n${text}`);
7259
+ return true;
7260
+ }
7261
+ //#endregion
7262
+ //#region src/client/term/xterm-styles.ts
7263
+ /**
7183
7264
  * Inlined xterm.js 5.3.0 core styles (MIT) — injected as a <style> tag at
7184
7265
  * runtime because the client build compiles only *.module.css and the
7185
7266
  * shell's css-modules pipeline rejects a raw :global block with xterm's
@@ -7362,7 +7443,7 @@ window.__ModuleLoader__.load({
7362
7443
  `;
7363
7444
  //#endregion
7364
7445
  //#region \0dsh-css:src/client/term/term.module.css.mjs
7365
- const css = ".WK4grq_col{border-left:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-1,#f9fafb);width:100%;height:100%;font-family:var(--aion-font-sans,-apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);flex-direction:column;display:flex;overflow:hidden}.WK4grq_title{color:var(--aion-text-primary,#1f2329);user-select:none;flex:none;padding:0 8px;font-size:12px;font-weight:600}.WK4grq_tabDivider{background:var(--aion-border-base,#e5e6eb);flex:none;width:1px;height:16px;margin:0 4px}.WK4grq_toolbar{border-bottom:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-base,#fff);flex:none;align-items:center;gap:4px;height:32px;padding:0 6px;display:flex}.WK4grq_tab{height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;background:0 0;border:none;border-radius:5px;align-items:center;gap:6px;max-width:200px;padding:0 8px;font-size:12px;display:inline-flex}.WK4grq_tabActive{background:var(--aion-bg-2,#f2f3f5);color:var(--aion-text-primary,#000)}.WK4grq_tabClose{width:14px;height:14px;color:var(--aion-text-tertiary,#86909c);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;font-size:11px;line-height:1;display:inline-flex}.WK4grq_tabClose:hover{background:var(--aion-bg-3,#e5e6eb);color:var(--aion-text-primary,#000)}.WK4grq_addTab{width:22px;height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;font-size:14px;display:inline-flex}.WK4grq_addTab:hover{background:var(--aion-bg-2,#f2f3f5)}.WK4grq_spacer{flex:1}.WK4grq_collapse{width:22px;height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;font-size:13px;display:inline-flex}.WK4grq_collapse:hover{background:var(--aion-bg-2,#f2f3f5)}.WK4grq_stage{background:var(--aion-bg-base,#fff);flex:1;min-height:0;position:relative}.WK4grq_termWrap{padding:4px;display:none;position:absolute;inset:0}.WK4grq_termWrapActive{display:block}.WK4grq_emptyHint{color:var(--aion-text-tertiary,#86909c);justify-content:center;align-items:center;font-size:13px;display:flex;position:absolute;inset:0}";
7446
+ const css = ".WK4grq_col{border-left:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-1,#f9fafb);width:100%;height:100%;font-family:var(--aion-font-sans,-apple-system, \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif);flex-direction:column;display:flex;overflow:hidden}.WK4grq_title{color:var(--aion-text-primary,#1f2329);user-select:none;flex:none;padding:0 8px;font-size:12px;font-weight:600}.WK4grq_tabDivider{background:var(--aion-border-base,#e5e6eb);flex:none;width:1px;height:16px;margin:0 4px}.WK4grq_toolbar{border-bottom:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-base,#fff);flex:none;align-items:center;gap:4px;height:32px;padding:0 6px;display:flex}.WK4grq_tab{height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;background:0 0;border:none;border-radius:5px;align-items:center;gap:6px;max-width:200px;padding:0 8px;font-size:12px;display:inline-flex}.WK4grq_tabActive{background:var(--aion-bg-2,#f2f3f5);color:var(--aion-text-primary,#000)}.WK4grq_tabClose{width:14px;height:14px;color:var(--aion-text-tertiary,#86909c);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;font-size:11px;line-height:1;display:inline-flex}.WK4grq_tabClose:hover{background:var(--aion-bg-3,#e5e6eb);color:var(--aion-text-primary,#000)}.WK4grq_addTab{width:22px;height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;font-size:14px;display:inline-flex}.WK4grq_addTab:hover{background:var(--aion-bg-2,#f2f3f5)}.WK4grq_spacer{flex:1}.WK4grq_collapse{width:22px;height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;font-size:13px;display:inline-flex}.WK4grq_collapse:hover{background:var(--aion-bg-2,#f2f3f5)}.WK4grq_stage{background:var(--aion-bg-base,#fff);flex:1;min-height:0;position:relative}.WK4grq_termWrap{padding:4px;display:none;position:absolute;inset:0}.WK4grq_termWrapActive{display:block}.WK4grq_emptyHint{color:var(--aion-text-tertiary,#86909c);justify-content:center;align-items:center;font-size:13px;display:flex;position:absolute;inset:0}.WK4grq_shellSelect{border:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-base,#fff);height:22px;color:var(--aion-text-primary,#1f2329);cursor:pointer;border-radius:5px;outline:none;flex:none;padding:0 6px;font-size:12px}.WK4grq_shellSelect:hover{border-color:var(--aion-border-strong,#c9cdd4)}.WK4grq_shellSelect:focus{border-color:var(--aion-primary,#1677ff)}.WK4grq_reopenBtn{background:var(--aion-bg-2,#f2f3f5);height:22px;color:var(--aion-text-secondary,#454d5f);cursor:pointer;border:none;border-radius:5px;justify-content:center;align-items:center;gap:2px;padding:0 6px;font-size:12px;display:inline-flex}.WK4grq_reopenBtn:hover{background:var(--aion-bg-3,#e5e6eb);color:var(--aion-text-primary,#000)}.WK4grq_reopenDropdown{z-index:50;border:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-base,#fff);border-radius:8px;min-width:180px;max-width:280px;padding:4px 0;position:absolute;top:32px;left:6px;box-shadow:0 4px 12px #00000014}.WK4grq_reopenHeader{color:var(--aion-text-tertiary,#86909c);user-select:none;padding:4px 10px;font-size:11px;font-weight:600}.WK4grq_reopenItem{align-items:center;gap:4px;padding:0 6px 0 4px;display:flex}.WK4grq_reopenItemBtn{height:28px;color:var(--aion-text-primary,#1f2329);cursor:pointer;text-align:left;text-overflow:ellipsis;white-space:nowrap;background:0 0;border:none;border-radius:5px;flex:1;align-items:center;padding:0 8px;font-size:12px;display:flex;overflow:hidden}.WK4grq_reopenItemBtn:hover{background:var(--aion-bg-2,#f2f3f5)}.WK4grq_reopenItemKill{width:20px;height:20px;color:var(--aion-text-tertiary,#86909c);cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;font-size:13px;display:inline-flex}.WK4grq_reopenItemKill:hover{background:var(--aion-bg-3,#e5e6eb);color:var(--aion-text-primary,#000)}.WK4grq_addToChatBtn{z-index:60;border:1px solid var(--aion-border-base,#e5e6eb);background:var(--aion-bg-base,#fff);height:28px;color:var(--aion-text-primary,#1f2329);cursor:pointer;white-space:nowrap;border-radius:14px;align-items:center;gap:4px;padding:0 10px;font-size:12px;font-weight:500;display:inline-flex;position:absolute;box-shadow:0 2px 8px #0000001a}.WK4grq_addToChatBtn:hover{background:var(--aion-bg-2,#f2f3f5);border-color:var(--aion-primary,#1677ff);color:var(--aion-primary,#1677ff)}";
7366
7447
  const tagId = "@lijian-ui/dsh-term/term.module.css";
7367
7448
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
7368
7449
  const tag = document.createElement("style");
@@ -7373,9 +7454,17 @@ window.__ModuleLoader__.load({
7373
7454
  }
7374
7455
  var term_module_css_default = {
7375
7456
  "addTab": "WK4grq_addTab",
7457
+ "addToChatBtn": "WK4grq_addToChatBtn",
7376
7458
  "col": "WK4grq_col",
7377
7459
  "collapse": "WK4grq_collapse",
7378
7460
  "emptyHint": "WK4grq_emptyHint",
7461
+ "reopenBtn": "WK4grq_reopenBtn",
7462
+ "reopenDropdown": "WK4grq_reopenDropdown",
7463
+ "reopenHeader": "WK4grq_reopenHeader",
7464
+ "reopenItem": "WK4grq_reopenItem",
7465
+ "reopenItemBtn": "WK4grq_reopenItemBtn",
7466
+ "reopenItemKill": "WK4grq_reopenItemKill",
7467
+ "shellSelect": "WK4grq_shellSelect",
7379
7468
  "spacer": "WK4grq_spacer",
7380
7469
  "stage": "WK4grq_stage",
7381
7470
  "tab": "WK4grq_tab",
@@ -7399,10 +7488,57 @@ window.__ModuleLoader__.load({
7399
7488
  * the web shell's frame, beside the file-manager panels (preview/explorer)
7400
7489
  * when present. The panel itself only owns its inner content (header + stage).
7401
7490
  *
7402
- * A-version scope: pure user terminal. No agent integration, no SSH targets
7403
- * yetthose are the B-version.
7491
+ * Features: shell selector with availability detection, terminal reuse
7492
+ * (close detaches PTY keeps running; reopen re-attaches), 16-color ANSI
7493
+ * palette, safe cwd fallback, selection → add to conversation.
7404
7494
  * @module dsh-term/client/term/TerminalPanel
7405
7495
  */
7496
+ /** All shell kinds in display order (used when detection fails). */
7497
+ const FALLBACK_SHELLS = [
7498
+ "zsh",
7499
+ "bash",
7500
+ "gitbash",
7501
+ "powershell",
7502
+ "cmd"
7503
+ ];
7504
+ /** 16-color ANSI palette for dark backgrounds (One Dark inspired). */
7505
+ const DARK_PALETTE = {
7506
+ black: "#282c34",
7507
+ red: "#e06c75",
7508
+ green: "#98c379",
7509
+ yellow: "#e5c07b",
7510
+ blue: "#61afef",
7511
+ magenta: "#c678dd",
7512
+ cyan: "#56b6c2",
7513
+ white: "#abb2bf",
7514
+ brightBlack: "#5c6370",
7515
+ brightRed: "#ef6e7e",
7516
+ brightGreen: "#a3d978",
7517
+ brightYellow: "#f0c674",
7518
+ brightBlue: "#7ab8f5",
7519
+ brightMagenta: "#d68adf",
7520
+ brightCyan: "#6fd0dc",
7521
+ brightWhite: "#d7dae0"
7522
+ };
7523
+ /** 16-color ANSI palette for light backgrounds (Nord inspired). */
7524
+ const LIGHT_PALETTE = {
7525
+ black: "#3b4252",
7526
+ red: "#bf616a",
7527
+ green: "#a3be8c",
7528
+ yellow: "#ebcb8b",
7529
+ blue: "#81a1c1",
7530
+ magenta: "#b48ead",
7531
+ cyan: "#8fbcbb",
7532
+ white: "#4c566a",
7533
+ brightBlack: "#4c566a",
7534
+ brightRed: "#bf616a",
7535
+ brightGreen: "#a3be8c",
7536
+ brightYellow: "#ebcb8b",
7537
+ brightBlue: "#81a1c1",
7538
+ brightMagenta: "#b48ead",
7539
+ brightCyan: "#8fbcbb",
7540
+ brightWhite: "#2e3440"
7541
+ };
7406
7542
  /** Inject the xterm core styles once (idempotent). */
7407
7543
  const XTERM_STYLE_ID = "dsh-term-xterm-style";
7408
7544
  function adoptXtermStyles() {
@@ -7418,29 +7554,15 @@ window.__ModuleLoader__.load({
7418
7554
  background: "#0e0e0e",
7419
7555
  foreground: "#ced3da",
7420
7556
  cursor: "#ced3da",
7421
- selectionBackground: "rgba(255,255,255,0.2)"
7557
+ selectionBackground: "rgba(255,255,255,0.2)",
7558
+ ...DARK_PALETTE
7422
7559
  };
7423
7560
  return {
7424
7561
  background: "#ffffff",
7425
7562
  foreground: "#1f2329",
7426
7563
  cursor: "#1f2329",
7427
7564
  selectionBackground: "rgba(22,93,255,0.25)",
7428
- black: "#1f2329",
7429
- red: "#d4393b",
7430
- green: "#167c2e",
7431
- yellow: "#b58900",
7432
- blue: "#0a4d8c",
7433
- magenta: "#a020a0",
7434
- cyan: "#0379a6",
7435
- white: "#5a5f66",
7436
- brightBlack: "#6a6f76",
7437
- brightRed: "#e5585a",
7438
- brightGreen: "#1a9c3a",
7439
- brightYellow: "#d6a200",
7440
- brightBlue: "#1a6fcf",
7441
- brightMagenta: "#c040c0",
7442
- brightCyan: "#1aa0d6",
7443
- brightWhite: "#2f353b"
7565
+ ...LIGHT_PALETTE
7444
7566
  };
7445
7567
  }
7446
7568
  /** Current workspace cwd from the session list ('' when none). */
@@ -7450,16 +7572,46 @@ window.__ModuleLoader__.load({
7450
7572
  const cwd = sessionId === void 0 ? void 0 : snapshot.byId[sessionId]?.cwd;
7451
7573
  return typeof cwd === "string" && cwd !== "" ? cwd : "";
7452
7574
  }
7453
- /** The docked panel: header (title / tabs / new / collapse) + xterm stage. */
7454
- function TerminalPanel({ ctx, api, onClose }) {
7575
+ /** The docked panel: header (title / shell / tabs / new / reopen / collapse) + xterm stage. */
7576
+ function TerminalPanel({ ctx, api, onClose, t }) {
7577
+ (0, react.useSyncExternalStore)((cb) => ctx.locale.subscribe(cb), () => ctx.locale.getSnapshot().revision);
7455
7578
  const [tabs, setTabs] = (0, react.useState)([]);
7456
7579
  const [active, setActive] = (0, react.useState)(null);
7580
+ const [shells, setShells] = (0, react.useState)(null);
7581
+ const [selectedShell, setSelectedShell] = (0, react.useState)("bash");
7582
+ const [detachedSessions, setDetachedSessions] = (0, react.useState)([]);
7583
+ const [showReopen, setShowReopen] = (0, react.useState)(false);
7584
+ const [reopenLeft, setReopenLeft] = (0, react.useState)(6);
7585
+ const [floatBtn, setFloatBtn] = (0, react.useState)(null);
7457
7586
  const stageRef = (0, react.useRef)(null);
7587
+ const panelRef = (0, react.useRef)(null);
7458
7588
  const tabsRef = (0, react.useRef)(tabs);
7459
7589
  tabsRef.current = tabs;
7590
+ const floatBtnRef = (0, react.useRef)(null);
7460
7591
  (0, react.useEffect)(() => {
7461
7592
  adoptXtermStyles();
7462
7593
  }, []);
7594
+ (0, react.useEffect)(() => {
7595
+ let cancelled = false;
7596
+ api.shells().then((result) => {
7597
+ if (cancelled) return;
7598
+ const list = result.shells;
7599
+ setShells(list);
7600
+ if (list.length > 0) {
7601
+ const preferred = navigator.userAgent.includes("Windows") ? "powershell" : "zsh";
7602
+ const initial = list.some((s) => s.id === preferred) ? preferred : list[0].id;
7603
+ setSelectedShell(initial);
7604
+ }
7605
+ }).catch(() => {
7606
+ if (!cancelled) setShells(FALLBACK_SHELLS.map((id) => ({
7607
+ id,
7608
+ labelKey: `ui.shell.${id}`
7609
+ })));
7610
+ });
7611
+ return () => {
7612
+ cancelled = true;
7613
+ };
7614
+ }, [api]);
7463
7615
  (0, react.useEffect)(() => {
7464
7616
  const applyTheme = () => {
7465
7617
  const theme = themeOf();
@@ -7476,17 +7628,24 @@ window.__ModuleLoader__.load({
7476
7628
  (0, react.useEffect)(() => {
7477
7629
  return api.subscribe((event) => {
7478
7630
  if (event.kind === "output") {
7479
- tabsRef.current.find((t) => t.sessionId === event.id)?.term.write(event.data);
7631
+ tabsRef.current.find((tb) => tb.sessionId === event.id)?.term.write(event.data);
7480
7632
  return;
7481
7633
  }
7482
7634
  if (event.kind === "exit") {
7483
- const tab = tabsRef.current.find((t) => t.sessionId === event.id);
7484
- if (tab !== void 0) tab.term.write(`\r\n[dsh-term] 进程已退出(code ${event.exitCode})\r\n`);
7635
+ const tab = tabsRef.current.find((tb) => tb.sessionId === event.id);
7636
+ if (tab !== void 0) tab.term.write(`\r\n${event.message ?? `\r\n[dsh-term] Process exited (code ${event.exitCode})`}\r\n`);
7637
+ setDetachedSessions((prev) => prev.filter((s) => s.id !== event.id));
7638
+ return;
7639
+ }
7640
+ if (event.kind === "detached") return;
7641
+ if (event.kind === "reattached") {
7642
+ setDetachedSessions((prev) => prev.filter((s) => s.id !== event.session.id));
7643
+ return;
7485
7644
  }
7486
7645
  });
7487
7646
  }, [api]);
7488
7647
  /** Create a tab: xterm instance + host spawn, then attach output routing. */
7489
- const openTab = (0, react.useCallback)(async () => {
7648
+ const openTab = (0, react.useCallback)(async (shell) => {
7490
7649
  if (stageRef.current === null) return;
7491
7650
  const wrap = document.createElement("div");
7492
7651
  wrap.className = term_module_css_default.termWrap;
@@ -7505,18 +7664,19 @@ window.__ModuleLoader__.load({
7505
7664
  fit.fit();
7506
7665
  } catch {}
7507
7666
  term.onData((data) => {
7508
- const tab = tabsRef.current.find((t) => t.term === term);
7667
+ const tab = tabsRef.current.find((tb) => tb.term === term);
7509
7668
  if (tab !== void 0) api.write(tab.sessionId, data);
7510
7669
  });
7511
7670
  let session;
7512
7671
  try {
7513
7672
  session = await api.spawn({
7673
+ shell: shell ?? selectedShell,
7514
7674
  cwd: currentCwd(ctx) || void 0,
7515
7675
  cols: term.cols,
7516
7676
  rows: term.rows
7517
7677
  });
7518
7678
  } catch (error) {
7519
- term.write(`\r\n[dsh-term] 启动失败: ${String(error)}\r\n`);
7679
+ term.write(`\r\n${t("msg.spawnFailed", { 0: String(error) })}\r\n`);
7520
7680
  return;
7521
7681
  }
7522
7682
  const tab = {
@@ -7530,15 +7690,62 @@ window.__ModuleLoader__.load({
7530
7690
  setActive(session.id);
7531
7691
  api.resize(session.id, term.cols, term.rows);
7532
7692
  term.focus();
7533
- }, [api, ctx]);
7693
+ }, [
7694
+ api,
7695
+ ctx,
7696
+ selectedShell,
7697
+ t
7698
+ ]);
7699
+ /** Reattach to a detached session: create a fresh xterm and wire it up. */
7700
+ const reopenTab = (0, react.useCallback)(async (sessionId) => {
7701
+ if (stageRef.current === null) return;
7702
+ let session;
7703
+ try {
7704
+ session = await api.reattach(sessionId);
7705
+ } catch {
7706
+ return;
7707
+ }
7708
+ const wrap = document.createElement("div");
7709
+ wrap.className = term_module_css_default.termWrap;
7710
+ stageRef.current.appendChild(wrap);
7711
+ const term = new import_xterm.Terminal({
7712
+ fontSize: 13,
7713
+ fontFamily: "ui-monospace, \"SF Mono\", Menlo, Consolas, monospace",
7714
+ cursorBlink: true,
7715
+ theme: themeOf(),
7716
+ scrollback: 5e3
7717
+ });
7718
+ const fit = new import_addon_fit.FitAddon();
7719
+ term.loadAddon(fit);
7720
+ term.open(wrap);
7721
+ try {
7722
+ fit.fit();
7723
+ } catch {}
7724
+ term.onData((data) => {
7725
+ const tab = tabsRef.current.find((tb) => tb.term === term);
7726
+ if (tab !== void 0) api.write(tab.sessionId, data);
7727
+ });
7728
+ const tab = {
7729
+ sessionId: session.id,
7730
+ title: session.title,
7731
+ term,
7732
+ fit,
7733
+ wrap
7734
+ };
7735
+ setTabs((prev) => [...prev, tab]);
7736
+ setActive(session.id);
7737
+ api.resize(session.id, term.cols, term.rows);
7738
+ setShowReopen(false);
7739
+ term.focus();
7740
+ }, [api]);
7534
7741
  (0, react.useEffect)(() => {
7535
- if (tabs.length === 0) openTab();
7536
- }, []);
7742
+ if (tabs.length === 0 && shells !== null) openTab();
7743
+ }, [shells]);
7537
7744
  (0, react.useEffect)(() => {
7538
7745
  const stage = stageRef.current;
7539
7746
  if (stage === null) return;
7540
7747
  const refit = () => {
7541
- const tab = tabsRef.current.find((t) => t.sessionId === active);
7748
+ const tab = tabsRef.current.find((tb) => tb.sessionId === active);
7542
7749
  if (tab === void 0) return;
7543
7750
  try {
7544
7751
  tab.fit.fit();
@@ -7552,7 +7759,7 @@ window.__ModuleLoader__.load({
7552
7759
  (0, react.useEffect)(() => {
7553
7760
  for (const tab of tabsRef.current) tab.wrap.classList.toggle(term_module_css_default.termWrapActive, tab.sessionId === active);
7554
7761
  if (active !== null) {
7555
- const tab = tabsRef.current.find((t) => t.sessionId === active);
7762
+ const tab = tabsRef.current.find((tb) => tb.sessionId === active);
7556
7763
  if (tab !== void 0) {
7557
7764
  try {
7558
7765
  tab.fit.fit();
@@ -7566,16 +7773,38 @@ window.__ModuleLoader__.load({
7566
7773
  api,
7567
7774
  tabs
7568
7775
  ]);
7776
+ /** Close a tab: detach the PTY (keep it alive) and dispose the xterm locally. */
7569
7777
  const closeTab = (sessionId) => {
7570
- const tab = tabsRef.current.find((t) => t.sessionId === sessionId);
7778
+ const tab = tabsRef.current.find((tb) => tb.sessionId === sessionId);
7571
7779
  if (tab !== void 0) {
7572
- api.close(sessionId);
7780
+ api.detach(sessionId);
7573
7781
  tab.term.dispose();
7574
7782
  tab.wrap.remove();
7575
7783
  }
7576
- const next = tabsRef.current.filter((t) => t.sessionId !== sessionId);
7784
+ const next = tabsRef.current.filter((tb) => tb.sessionId !== sessionId);
7577
7785
  setTabs(next);
7578
7786
  if (active === sessionId) setActive(next[0]?.sessionId ?? null);
7787
+ setDetachedSessions((prev) => {
7788
+ if (prev.some((s) => s.id === sessionId)) return prev;
7789
+ const tab2 = tabsRef.current.find((tb) => tb.sessionId === sessionId);
7790
+ if (tab2 === void 0) return prev;
7791
+ return [...prev, {
7792
+ id: sessionId,
7793
+ title: tab2.title,
7794
+ cwd: "",
7795
+ cols: 80,
7796
+ rows: 24,
7797
+ alive: true,
7798
+ exitCode: null,
7799
+ shell: "bash",
7800
+ detached: true
7801
+ }];
7802
+ });
7803
+ };
7804
+ /** Kill a detached session permanently (remove from background list). */
7805
+ const killDetached = (sessionId) => {
7806
+ api.close(sessionId);
7807
+ setDetachedSessions((prev) => prev.filter((s) => s.id !== sessionId));
7579
7808
  };
7580
7809
  (0, react.useEffect)(() => {
7581
7810
  const current = tabsRef.current;
@@ -7587,57 +7816,185 @@ window.__ModuleLoader__.load({
7587
7816
  }
7588
7817
  };
7589
7818
  }, []);
7819
+ (0, react.useEffect)(() => {
7820
+ const stage = stageRef.current;
7821
+ const panel = panelRef.current;
7822
+ if (stage === null || panel === null) return;
7823
+ const onMouseUp = (e) => {
7824
+ if (floatBtnRef.current !== null && floatBtnRef.current.contains(e.target)) return;
7825
+ requestAnimationFrame(() => {
7826
+ const sel = tabsRef.current.find((tb) => tb.sessionId === active)?.term.getSelection() ?? "";
7827
+ if (!sel.trim()) {
7828
+ setFloatBtn(null);
7829
+ return;
7830
+ }
7831
+ const panelRect = panel.getBoundingClientRect();
7832
+ const top = Math.max(e.clientY - panelRect.top, 44);
7833
+ const left = Math.min(Math.max(e.clientX - panelRect.left, 70), panelRect.width - 70);
7834
+ setFloatBtn({
7835
+ top,
7836
+ left,
7837
+ content: sel
7838
+ });
7839
+ });
7840
+ };
7841
+ const onDocMouseDown = (e) => {
7842
+ if (floatBtnRef.current !== null && floatBtnRef.current.contains(e.target)) return;
7843
+ setFloatBtn(null);
7844
+ };
7845
+ stage.addEventListener("mouseup", onMouseUp);
7846
+ document.addEventListener("mousedown", onDocMouseDown);
7847
+ return () => {
7848
+ stage.removeEventListener("mouseup", onMouseUp);
7849
+ document.removeEventListener("mousedown", onDocMouseDown);
7850
+ };
7851
+ }, [active]);
7852
+ (0, react.useEffect)(() => {
7853
+ const tab = tabs.find((tb) => tb.sessionId === active);
7854
+ if (tab === void 0) return;
7855
+ const d = tab.term.onSelectionChange(() => {
7856
+ if (!tab.term.hasSelection()) setFloatBtn(null);
7857
+ });
7858
+ return () => d.dispose();
7859
+ }, [active, tabs]);
7860
+ (0, react.useEffect)(() => {
7861
+ if (!showReopen) return;
7862
+ const onDown = (e) => {
7863
+ const target = e.target;
7864
+ if (target.closest(`[data-reopen-dropdown]`) !== null) return;
7865
+ if (target.closest(`[data-reopen-btn]`) !== null) return;
7866
+ setShowReopen(false);
7867
+ };
7868
+ document.addEventListener("mousedown", onDown);
7869
+ return () => document.removeEventListener("mousedown", onDown);
7870
+ }, [showReopen]);
7871
+ const shellOptions = shells ?? FALLBACK_SHELLS.map((id) => ({
7872
+ id,
7873
+ labelKey: `ui.shell.${id}`
7874
+ }));
7590
7875
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7591
7876
  className: term_module_css_default.col,
7592
7877
  "data-dsh-term": "",
7593
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7594
- className: term_module_css_default.toolbar,
7595
- children: [
7596
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7597
- className: term_module_css_default.title,
7598
- children: "终端"
7599
- }),
7600
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: term_module_css_default.tabDivider }),
7601
- tabs.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7602
- type: "button",
7603
- className: `${term_module_css_default.tab}${tab.sessionId === active ? ` ${term_module_css_default.tabActive}` : ""}`,
7604
- onClick: () => setActive(tab.sessionId),
7605
- title: tab.title,
7606
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tab.title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7607
- role: "button",
7608
- "aria-label": `关闭 ${tab.title}`,
7609
- className: term_module_css_default.tabClose,
7610
- onClick: (event) => {
7611
- event.stopPropagation();
7612
- closeTab(tab.sessionId);
7878
+ ref: panelRef,
7879
+ children: [
7880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7881
+ className: term_module_css_default.toolbar,
7882
+ children: [
7883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7884
+ className: term_module_css_default.title,
7885
+ children: t("ui.panel.title")
7886
+ }),
7887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: term_module_css_default.tabDivider }),
7888
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
7889
+ className: term_module_css_default.shellSelect,
7890
+ value: selectedShell,
7891
+ onChange: (e) => {
7892
+ const next = e.target.value;
7893
+ if (next === selectedShell) return;
7894
+ setSelectedShell(next);
7895
+ if (active !== null) closeTab(active);
7896
+ openTab(next);
7613
7897
  },
7614
- children: "×"
7615
- })]
7616
- }, tab.sessionId)),
7617
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7618
- type: "button",
7619
- className: term_module_css_default.addTab,
7620
- title: "新建终端",
7621
- onClick: () => void openTab(),
7622
- children: "+"
7623
- }),
7624
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: term_module_css_default.spacer }),
7625
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7626
- type: "button",
7627
- className: term_module_css_default.collapse,
7628
- title: "收起",
7629
- onClick: onClose,
7630
- children: "—"
7898
+ title: t("ui.panel.shellTitle"),
7899
+ disabled: shells === null,
7900
+ children: shellOptions.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
7901
+ value: s.id,
7902
+ children: t(s.labelKey)
7903
+ }, s.id))
7904
+ }),
7905
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: term_module_css_default.tabDivider }),
7906
+ tabs.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7907
+ type: "button",
7908
+ className: `${term_module_css_default.tab}${tab.sessionId === active ? ` ${term_module_css_default.tabActive}` : ""}`,
7909
+ onClick: () => setActive(tab.sessionId),
7910
+ title: tab.title,
7911
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tab.title }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7912
+ role: "button",
7913
+ "aria-label": t("ui.tab.closeAria", { 0: tab.title }),
7914
+ className: term_module_css_default.tabClose,
7915
+ onClick: (event) => {
7916
+ event.stopPropagation();
7917
+ closeTab(tab.sessionId);
7918
+ },
7919
+ children: "×"
7920
+ })]
7921
+ }, tab.sessionId)),
7922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7923
+ type: "button",
7924
+ className: term_module_css_default.addTab,
7925
+ title: t("ui.panel.addTabTitle"),
7926
+ onClick: () => void openTab(),
7927
+ children: "+"
7928
+ }),
7929
+ detachedSessions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7930
+ type: "button",
7931
+ "data-reopen-btn": "",
7932
+ className: term_module_css_default.reopenBtn,
7933
+ title: t("ui.panel.reopenTitle"),
7934
+ onClick: (e) => {
7935
+ setReopenLeft(e.currentTarget.offsetLeft);
7936
+ setShowReopen((v) => !v);
7937
+ },
7938
+ children: ["↻", detachedSessions.length]
7939
+ }),
7940
+ showReopen && detachedSessions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7941
+ "data-reopen-dropdown": "",
7942
+ className: term_module_css_default.reopenDropdown,
7943
+ style: { left: reopenLeft },
7944
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7945
+ className: term_module_css_default.reopenHeader,
7946
+ children: t("ui.panel.backgroundSessions")
7947
+ }), detachedSessions.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7948
+ className: term_module_css_default.reopenItem,
7949
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7950
+ type: "button",
7951
+ className: term_module_css_default.reopenItemBtn,
7952
+ onClick: () => void reopenTab(s.id),
7953
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: s.title })
7954
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7955
+ type: "button",
7956
+ className: term_module_css_default.reopenItemKill,
7957
+ title: "×",
7958
+ onClick: () => killDetached(s.id),
7959
+ children: "×"
7960
+ })]
7961
+ }, s.id))]
7962
+ }),
7963
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: term_module_css_default.spacer }),
7964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7965
+ type: "button",
7966
+ className: term_module_css_default.collapse,
7967
+ title: t("ui.panel.collapseTitle"),
7968
+ onClick: onClose,
7969
+ children: "—"
7970
+ })
7971
+ ]
7972
+ }),
7973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7974
+ className: term_module_css_default.stage,
7975
+ ref: stageRef,
7976
+ children: tabs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7977
+ className: term_module_css_default.emptyHint,
7978
+ children: t("ui.panel.emptyHint")
7631
7979
  })
7632
- ]
7633
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7634
- className: term_module_css_default.stage,
7635
- ref: stageRef,
7636
- children: tabs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7637
- className: term_module_css_default.emptyHint,
7638
- children: "点击 + 新建终端"
7980
+ }),
7981
+ floatBtn !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7982
+ ref: floatBtnRef,
7983
+ className: term_module_css_default.addToChatBtn,
7984
+ style: {
7985
+ top: floatBtn.top,
7986
+ left: floatBtn.left
7987
+ },
7988
+ onMouseDown: (e) => e.preventDefault(),
7989
+ onClick: () => {
7990
+ appendToConversationDraft(ctx, floatBtn.content.replace(/\s+$/, ""));
7991
+ setFloatBtn(null);
7992
+ tabsRef.current.find((tb) => tb.sessionId === active)?.term.clearSelection();
7993
+ },
7994
+ title: t("ui.panel.addToChat"),
7995
+ children: t("ui.panel.addToChat")
7639
7996
  })
7640
- })]
7997
+ ]
7641
7998
  });
7642
7999
  }
7643
8000
  //#endregion
@@ -7667,6 +8024,16 @@ window.__ModuleLoader__.load({
7667
8024
  });
7668
8025
  }
7669
8026
  //#endregion
8027
+ //#region src/client/i18n-seat.ts
8028
+ let _t = null;
8029
+ function bindI18n(t) {
8030
+ _t = t;
8031
+ }
8032
+ function getT() {
8033
+ if (_t === null) return ((key) => key);
8034
+ return _t;
8035
+ }
8036
+ //#endregion
7670
8037
  //#region src/client/term/AnimatedDock.tsx
7671
8038
  /**
7672
8039
  * AnimatedDock — the terminal dock button rendered into the conversation
@@ -7688,6 +8055,7 @@ window.__ModuleLoader__.load({
7688
8055
  terminalState: "dsh-dock:terminal-state"
7689
8056
  };
7690
8057
  function AnimatedDock() {
8058
+ const t = getT();
7691
8059
  const [active, setActive] = (0, react.useState)(false);
7692
8060
  (0, react.useEffect)(() => {
7693
8061
  const onState = (e) => setActive(Boolean(e.detail));
@@ -7696,7 +8064,7 @@ window.__ModuleLoader__.load({
7696
8064
  }, []);
7697
8065
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DockItem, {
7698
8066
  active,
7699
- label: "终端",
8067
+ label: t("ui.dock.label"),
7700
8068
  onClick: () => window.dispatchEvent(new CustomEvent(EV$1.toggleTerminal)),
7701
8069
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
7702
8070
  width: "16",
@@ -7738,8 +8106,13 @@ window.__ModuleLoader__.load({
7738
8106
  * whole boot when a plugin apply throws.
7739
8107
  * @module dsh-term/client
7740
8108
  */
7741
- /** Required services: sessions (for the workspace cwd). */
7742
- const inject = ["sessions"];
8109
+ /** Required services: sessions (for the workspace cwd), conversation (for add-to-chat). */
8110
+ const inject = [
8111
+ "sessions",
8112
+ "locale",
8113
+ "slots",
8114
+ "conversation"
8115
+ ];
7743
8116
  /** Cross-plugin event names shared with the AnimatedDock header group. */
7744
8117
  const EV = {
7745
8118
  toggleTerminal: "dsh-dock:toggle-terminal",
@@ -7844,6 +8217,13 @@ window.__ModuleLoader__.load({
7844
8217
  }
7845
8218
  /** Apply the browser half. */
7846
8219
  function apply(ctx) {
8220
+ const I18N_NS = "dsh-term";
8221
+ ctx.effect(() => ctx.locale.register(I18N_NS, {
8222
+ zh,
8223
+ en
8224
+ }), "dsh-term: client dictionaries");
8225
+ const t = ctx.locale.bind(I18N_NS);
8226
+ bindI18n(t);
7847
8227
  ctx.inject(["slots"], (scope) => {
7848
8228
  scope.slots.inject("conversation.session.header.utilities", () => scope.slots.register({
7849
8229
  name: "conversation.session.header.utilities",
@@ -7983,7 +8363,8 @@ window.__ModuleLoader__.load({
7983
8363
  root.render((0, react.createElement)(TerminalPanel, {
7984
8364
  ctx,
7985
8365
  api,
7986
- onClose: () => setVisible(false)
8366
+ onClose: () => setVisible(false),
8367
+ t
7987
8368
  }));
7988
8369
  disposeFrame = () => {
7989
8370
  keepLast.disconnect();