dsh-single-terminal 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -43,18 +43,25 @@ see [preview.md](preview.md).
43
43
 
44
44
  ## Features
45
45
 
46
- - **Header entry** (`conversation.session.header.utilities`): a panel-bottom
47
- icon button in the session header's utilities row toggles
48
- the drawer. Hovering shows a bubble tooltip (the host `Tooltip` component)
49
- with the shortcut; `Alt+C` toggles from anywhere except while typing inside
50
- the terminal (there `Alt+C` is passed to the shell as `ESC c`).
46
+ - **Drawer-handle entry** (rendered inside the drawer in `shell.overlay`): a
47
+ `>_` prompt tab stuck to the drawer's top-right outer edge — the terminal's
48
+ entry button. It rides the drawer's own slide animation: expanded it sticks
49
+ to the terminal's top-right corner like a drawer handle; minimized it settles
50
+ at the page's bottom-right corner. Clicking toggles the drawer; `Alt+C`
51
+ toggles from anywhere except while typing inside the terminal (there `Alt+C`
52
+ is passed to the shell as `ESC c`).
53
+ - **Invisible session bridge** (`conversation.session.header.utilities`): a
54
+ `null`-rendering component that only syncs the current session's workspace
55
+ root into the controller (so new terminals start there); the header shows no
56
+ visible UI for this plugin.
51
57
  - **Drawer** (`shell.overlay`): a frame-level bottom drawer with
52
58
  - a tab strip — one PTY session per tab, independent shells, close button
53
59
  per tab (terminates the whole process tree; the pid is verified gone),
54
- - a `+` button (new tab with the default shell) and a `▸` menu
55
- (all shells found on this machine; unavailable ones are not listed),
60
+ - one `+` button whose dropdown lists all shells found on this machine
61
+ (unavailable ones are not listed), the default shell pinned first and
62
+ marked,
56
63
  - a mode switch (*Docked* / *Overlay*), a connection status dot and a
57
- collapse button,
64
+ minimize (`−`) button,
58
65
  - a drag handle on the top edge (pointer-capture drag, min 140px).
59
66
  - **Docked mode** pushes the app frame up with `padding-bottom` on the frame
60
67
  root element (no host hook exists for bottom docks); when the anchor cannot
@@ -83,7 +90,8 @@ profile `cordis.patch.yml`:
83
90
  args: []
84
91
  ```
85
92
 
86
- - `defaultShell` — shell used by the `+` button; when unavailable it falls
93
+ - `defaultShell` — shell preselected in the `+` dropdown (pinned first and
94
+ marked "Default"); when unavailable it falls
87
95
  back (`powershell` on Windows, `$SHELL`/`bash` on POSIX).
88
96
  - `defaultCwd` — start directory when there is no workspace context: `home`
89
97
  (default) starts in the user home; `workspace` is reserved (currently
@@ -157,7 +165,7 @@ pnpm run verify # simulate the host seed table to check lib/client.js loa
157
165
  ```
158
166
  ├── src/ # Source
159
167
  │ ├── host/ # Host half: index.ts (entry, ws route + config), hub.ts (session hub + frame protocol), shells.ts (registry + probing), types.ts
160
- │ └── client/ # Browser half: plugin.tsx (slots), drawer.tsx, term.tsx, controller.ts, ws.ts, styles.ts, theme.ts, toggle.tsx, i18n.ts ...
168
+ │ └── client/ # Browser half: plugin.tsx (slots), drawer.tsx (drawer + entry handle), term.tsx, controller.ts, ws.ts, styles.ts, theme.ts, scope.tsx (session bridge), i18n.ts ...
161
169
  ├── lib/ # Build artifacts (committed: git installs need no build)
162
170
  │ ├── index.js # Host half (tsdown, ESM)
163
171
  │ ├── client.js # Browser half (tsdown → __ModuleLoader__ factory, xterm inlined)
@@ -211,4 +219,5 @@ pnpm run verify # simulate the host seed table to check lib/client.js loa
211
219
  a translucent terminal background under the frosted blur in every mode —
212
220
  the WebGL addon was removed rather than swapped at runtime.
213
221
  - **The official `deepseek-harness` project is not modified**; all UI sits in
214
- existing slots (`shell.overlay`, `conversation.session.header.utilities`).
222
+ existing slots (`shell.overlay` for the drawer and its entry handle,
223
+ `conversation.session.header.utilities` for the invisible session bridge).
package/README.zh.md CHANGED
@@ -32,17 +32,20 @@ Web 应用底部挂一条交互式 PTY 终端(xterm.js)——可以正常敲
32
32
 
33
33
  ## 功能
34
34
 
35
- - **会话头部入口**(`conversation.session.header.utilities`):会话头部
36
- utilities 区的「底部面板」(panel-bottom) 图标按钮,点击开合抽屉;悬停显示
37
- 宿主 `Tooltip`
38
- 气泡(含快捷键提示)。`Alt+C` 全局开合抽屉;焦点在终端输入区内时放行
39
- (`Alt+C` 作为 `ESC c` 发给 shell)。
35
+ - **抽屉把手入口**(渲染在 `shell.overlay` 的抽屉内部):`>_` 提示符小标签
36
+ 吸附在抽屉右上角外沿——即终端入口按钮。它随抽屉自身的滑动动画移动:
37
+ 展开时像抽屉把手一样吸附在终端右上角;最小化后落在页面右下角。点击开合
38
+ 抽屉;`Alt+C` 全局开合,焦点在终端输入区内时放行(`Alt+C` 作为 `ESC c`
39
+ 发给 shell)。
40
+ - **隐形会话桥**(`conversation.session.header.utilities`):渲染 null 的
41
+ 组件,只负责把当前会话的工作区根目录同步给 controller(新建终端落在该
42
+ 目录);会话头部不显示本插件任何可见 UI。
40
43
  - **抽屉**(`shell.overlay`):frame 级底部抽屉,包含
41
44
  - 标签条 —— 每标签一条独立 PTY 会话,可独立选择 shell;每标签有关闭按钮
42
45
  (终止整棵进程树,已实测 pid 消失);
43
- - `+` 按钮(默认 shell 新建标签)与 `▸` 菜单(列出本机探测到的全部 shell
44
- 不可用的不显示);
45
- - 模式切换(占高度 / 浮层)、连接状态点、收起按钮;
46
+ - 单一 `+` 按钮 —— 下拉列出本机探测到的全部 shell(不可用的不显示),
47
+ 默认 shell 置顶并标记;
48
+ - 模式切换(占高度 / 浮层)、连接状态点、最小化(`−`)按钮;
46
49
  - 顶边拖拽条(pointer capture 拖拽,最小 140px)。
47
50
  - **占高度模式** 通过对框架根元素设 `padding-bottom` 把内容顶起(宿主没有底部
48
51
  停靠钩子);找不到锚点元素时静默降级为浮层。
@@ -70,8 +73,8 @@ Schemastery `Config`(宿主 Plugins 设置页自动渲染),和 / 或 profi
70
73
  args: []
71
74
  ```
72
75
 
73
- - `defaultShell` —— `+` 按钮使用的 shell;不可用时自动回退(Windows 回退
74
- `powershell`,POSIX 回退 `$SHELL`/`bash`)。
76
+ - `defaultShell` —— `+` 下拉菜单中预选的 shell(置顶并标记「默认」);不可用
77
+ 时自动回退(Windows 回退 `powershell`,POSIX 回退 `$SHELL`/`bash`)。
75
78
  - `defaultCwd` —— 无工作区上下文时的启动目录:`home`(默认)从用户主目录启动;
76
79
  `workspace` 预留(当前等同 home);绝对路径必须存在。当前会话归属工作区时
77
80
  优先使用工作区根目录(见上)。
@@ -137,7 +140,7 @@ pnpm run verify # 模拟宿主 seed 表检查 lib/client.js 可加载
137
140
  ```
138
141
  ├── src/ # 源码
139
142
  │ ├── host/ # 宿主半边:index.ts(入口,ws 路由 + 配置)、hub.ts(会话 Hub + 帧协议)、shells.ts(注册表 + 探测)、types.ts
140
- │ └── client/ # 浏览器半边:plugin.tsx(slots)、drawer.tsxterm.tsx、controller.ts、ws.ts、styles.ts、theme.ts、toggle.tsxi18n.ts ...
143
+ │ └── client/ # 浏览器半边:plugin.tsx(slots)、drawer.tsx(抽屉 + 入口把手)、term.tsx、controller.ts、ws.ts、styles.ts、theme.ts、scope.tsx(会话桥)、i18n.ts ...
141
144
  ├── lib/ # 构建产物(入库:git 安装无需本地构建)
142
145
  │ ├── index.js # 宿主半边(tsdown,ESM)
143
146
  │ ├── client.js # 浏览器半边(tsdown → __ModuleLoader__ 工厂,xterm 已内联)
@@ -183,4 +186,5 @@ pnpm run verify # 模拟宿主 seed 表检查 lib/client.js 可加载
183
186
  WebGL canvas 不支持 alpha)。两种模式共用磨砂样式,因此统一保持 DOM 渲染器 +
184
187
  半透明终端背景置于磨砂模糊之上——不做运行时渲染器切换,WebGL 插件已移除。
185
188
  - **不修改官方 `deepseek-harness` 项目**;全部 UI 落在既有插槽
186
- (`shell.overlay`、`conversation.session.header.utilities`)。
189
+ (抽屉与其入口把手在 `shell.overlay`,隐形会话桥在
190
+ `conversation.session.header.utilities`)。
package/lib/client.js CHANGED
@@ -6,7 +6,6 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
6
6
  //#endregion
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
9
  //#region src/client/storage.ts
11
10
  /**
12
11
  * dsh-single-terminal —— 浏览器侧 UI 状态持久化(localStorage,JSON 容错)。
@@ -376,8 +375,9 @@ const DICT = {
376
375
  "terminal.title": "终端",
377
376
  "terminal.toggleTip": "打开终端面板 Alt+C",
378
377
  "terminal.new": "新建终端",
378
+ "terminal.defaultShell": "默认",
379
379
  "terminal.closeTab": "关闭终端",
380
- "terminal.closeDrawer": "收起抽屉",
380
+ "terminal.minimize": "最小化",
381
381
  "terminal.mode.dock": "占高度",
382
382
  "terminal.mode.overlay": "浮层",
383
383
  "terminal.mode.toggle": "切换抽屉模式",
@@ -386,7 +386,7 @@ const DICT = {
386
386
  "terminal.status.reconnecting": "重连中…",
387
387
  "terminal.status.disconnected": "已断开",
388
388
  "terminal.exited": "已退出",
389
- "terminal.empty": "暂无终端会话,点击右上角 + 新建",
389
+ "terminal.empty": "暂无终端会话,点击 新建",
390
390
  "terminal.error.limit": "终端数量已达上限",
391
391
  "terminal.error.spawn": "终端启动失败"
392
392
  },
@@ -394,8 +394,9 @@ const DICT = {
394
394
  "terminal.title": "Terminal",
395
395
  "terminal.toggleTip": "Open terminal panel Alt+C",
396
396
  "terminal.new": "New terminal",
397
+ "terminal.defaultShell": "Default",
397
398
  "terminal.closeTab": "Close terminal",
398
- "terminal.closeDrawer": "Collapse drawer",
399
+ "terminal.minimize": "Minimize",
399
400
  "terminal.mode.dock": "Docked",
400
401
  "terminal.mode.overlay": "Overlay",
401
402
  "terminal.mode.toggle": "Toggle drawer mode",
@@ -7850,9 +7851,27 @@ function TerminalView(props) {
7850
7851
  * 模式只决定停靠行为,样式两种模式统一(磨砂半透明,见 styles.ts);
7851
7852
  * - 顶部拖拽条调高度(pointer capture + 帧节流),localStorage 持久化 mode/height;
7852
7853
  * - 组件常挂载(关闭时 transform 滑出 + visibility 隐藏,布局不变),保证
7853
- * xterm 缓冲与会话不因抽屉开合丢失;开合动画见 styles.ts
7854
+ * xterm 缓冲与会话不因抽屉开合丢失;开合动画见 styles.ts
7855
+ * - 终端入口按钮 = 抽屉把手(.dst-entry,见 styles.ts):挂在抽屉内、贴在抽屉
7856
+ * 右上角外沿,随抽屉一同滑动——展开时吸附在终端右上角,收起后停在页面
7857
+ * 右下角(把手覆写 visibility,收起后仍可见可点)。
7854
7858
  */
7855
7859
  const HEIGHT_MIN = 140;
7860
+ /** 终端提示符图标(>_):填充路径,与宿主图标同风格(currentColor、直角)。 */
7861
+ const TERMINAL_PROMPT_PATH = "M2.2 3 L8.2 8 L2.2 13 L2.2 10.9 L5.7 8 L2.2 5.1 Z M9 11.6 H13.8 V13.4 H9 Z";
7862
+ function TerminalIcon(props) {
7863
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
7864
+ width: props.size ?? 16,
7865
+ height: props.size ?? 16,
7866
+ viewBox: "0 0 16 16",
7867
+ fill: "none",
7868
+ "aria-hidden": "true",
7869
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
7870
+ d: TERMINAL_PROMPT_PATH,
7871
+ fill: "currentColor"
7872
+ })
7873
+ });
7874
+ }
7856
7875
  function findFrameElement() {
7857
7876
  const frame = document.querySelector("[data-shell-overlay]")?.parentElement;
7858
7877
  return frame instanceof HTMLElement ? frame : null;
@@ -7884,12 +7903,17 @@ function TerminalDrawer() {
7884
7903
  const onPointerDown = (event) => {
7885
7904
  if (menuRef.current !== null && !menuRef.current.contains(event.target)) setMenuOpen(false);
7886
7905
  };
7906
+ const onKeyDown = (event) => {
7907
+ if (event.key === "Escape") setMenuOpen(false);
7908
+ };
7887
7909
  document.addEventListener("pointerdown", onPointerDown);
7910
+ document.addEventListener("keydown", onKeyDown);
7888
7911
  return () => {
7889
7912
  document.removeEventListener("pointerdown", onPointerDown);
7913
+ document.removeEventListener("keydown", onKeyDown);
7890
7914
  };
7891
7915
  }, [menuOpen]);
7892
- const availableShells = state.shells.filter((shell) => shell.available);
7916
+ const menuShells = [...state.shells.filter((shell) => shell.available)].sort((a, b) => (a.id === state.defaultShell ? -1 : 0) - (b.id === state.defaultShell ? -1 : 0));
7893
7917
  const startDrag = (event) => {
7894
7918
  event.preventDefault();
7895
7919
  const handle = event.currentTarget;
@@ -7916,6 +7940,16 @@ function TerminalDrawer() {
7916
7940
  className: `dst-drawer${state.open ? " open" : ""}`,
7917
7941
  style: { "--dst-height": `${state.height}px` },
7918
7942
  children: [
7943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7944
+ type: "button",
7945
+ className: "dst-entry",
7946
+ "aria-label": state.open ? t("terminal.minimize") : t("terminal.toggleTip"),
7947
+ title: state.open ? t("terminal.minimize") : t("terminal.toggleTip"),
7948
+ onClick: () => {
7949
+ terminal.toggle();
7950
+ },
7951
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalIcon, { size: 16 })
7952
+ }),
7919
7953
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7920
7954
  className: "dst-drag",
7921
7955
  onPointerDown: startDrag,
@@ -7967,51 +8001,48 @@ function TerminalDrawer() {
7967
8001
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7968
8002
  style: { position: "relative" },
7969
8003
  ref: menuRef,
7970
- children: [
7971
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7972
- className: "dst-iconbtn",
7973
- onClick: () => {
7974
- terminal.newTerminal(state.defaultShell);
8004
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8005
+ className: "dst-iconbtn",
8006
+ "aria-haspopup": "menu",
8007
+ "aria-expanded": menuOpen,
8008
+ onClick: () => {
8009
+ setMenuOpen((open) => !open);
8010
+ },
8011
+ title: t("terminal.new"),
8012
+ children: "+"
8013
+ }), menuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8014
+ className: "dst-menu",
8015
+ role: "menu",
8016
+ children: [menuShells.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8017
+ className: "dst-menuitem",
8018
+ style: {
8019
+ cursor: "default",
8020
+ opacity: .6
7975
8021
  },
7976
- title: `${t("terminal.new")}(${availableShells.find((shell) => shell.id === state.defaultShell)?.name ?? state.defaultShell})`,
7977
- children: ""
7978
- }),
7979
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7980
- className: "dst-iconbtn",
7981
- style: { width: 18 },
8022
+ children: ""
8023
+ }), menuShells.map((shell) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8024
+ type: "button",
8025
+ role: "menuitem",
8026
+ className: "dst-menuitem",
7982
8027
  onClick: () => {
7983
- setMenuOpen((open) => !open);
8028
+ setMenuOpen(false);
8029
+ terminal.newTerminal(shell.id);
7984
8030
  },
7985
- title: t("terminal.new"),
7986
- children: ""
7987
- }),
7988
- menuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7989
- className: "dst-menu",
7990
- children: [availableShells.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7991
- className: "dst-menuitem",
7992
- style: {
7993
- cursor: "default",
7994
- opacity: .6
7995
- },
7996
- children: "—"
7997
- }), availableShells.map((shell) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7998
- className: "dst-menuitem",
7999
- onClick: () => {
8000
- setMenuOpen(false);
8001
- terminal.newTerminal(shell.id);
8002
- },
8003
- children: shell.name
8004
- }, shell.id))]
8005
- })
8006
- ]
8031
+ children: [shell.name, shell.id === state.defaultShell && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8032
+ className: "dst-menudfl",
8033
+ children: t("terminal.defaultShell")
8034
+ })]
8035
+ }, shell.id))]
8036
+ })]
8007
8037
  }),
8008
8038
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8009
8039
  className: "dst-iconbtn",
8010
8040
  onClick: () => {
8011
8041
  terminal.setOpen(false);
8012
8042
  },
8013
- title: t("terminal.closeDrawer"),
8014
- children: ""
8043
+ title: t("terminal.minimize"),
8044
+ "aria-label": t("terminal.minimize"),
8045
+ children: "−"
8015
8046
  })
8016
8047
  ]
8017
8048
  })
@@ -8040,6 +8071,33 @@ function TerminalDrawer() {
8040
8071
  });
8041
8072
  }
8042
8073
  //#endregion
8074
+ //#region src/client/scope.tsx
8075
+ /**
8076
+ * dsh-single-terminal —— 会话作用域桥(conversation.session.header.utilities 插槽)。
8077
+ *
8078
+ * 终端入口按钮已移入抽屉(drawer.tsx 的 .dst-entry 把手:吸附抽屉右上角,
8079
+ * 收起后落在页面右下角),会话头部不再渲染可见 UI。这里保留一个渲染 null
8080
+ * 的不可见锚点组件:借助会话作用域标准 props(sessionId + 全局
8081
+ * useWorkspaces)解析当前会话归属的工作区根目录并同步给 controller,让新建
8082
+ * 终端(含自动新建)落在工作区路径。渲染 null 后 utilities 容器命中宿主
8083
+ * `.headerUtilities:empty { display: none }` 规则,头部不留空位。
8084
+ */
8085
+ const noWorkspaces = () => null;
8086
+ function SessionWorkspaceBridge(props) {
8087
+ const { sessionId, useWorkspaces = noWorkspaces } = props;
8088
+ const workspacePath = useWorkspaces((snapshot) => {
8089
+ if (sessionId === void 0) return null;
8090
+ return snapshot.items.find((row) => row.sessionIds.includes(sessionId))?.path ?? null;
8091
+ });
8092
+ (0, react.useEffect)(() => {
8093
+ terminal.setWorkspaceCwd(workspacePath);
8094
+ return () => {
8095
+ terminal.setWorkspaceCwd(null);
8096
+ };
8097
+ }, [workspacePath]);
8098
+ return null;
8099
+ }
8100
+ //#endregion
8043
8101
  //#region src/client/xterm-css.ts
8044
8102
  /**
8045
8103
  * xterm.css(@xterm/xterm 5.x 自带样式)· 生成期内嵌。
@@ -8276,6 +8334,7 @@ const XTERM_CSS = `/**
8276
8334
  * alpha canvas,WebGL 已移除)。开合动画走 transform 滑出 +
8277
8335
  * visibility(布局常驻,xterm 不因开合重排),曲线用宿主
8278
8336
  * --ds-ease-in-out / --ds-transition-duration-slow,并尊重系统减动效设置。
8337
+ * 终端入口按钮为吸附抽屉右上角的把手(.dst-entry),随抽屉滑动。
8279
8338
  */
8280
8339
  const DRAWER_CSS = `
8281
8340
  .dst-drawer {
@@ -8364,7 +8423,7 @@ const DRAWER_CSS = `
8364
8423
  }
8365
8424
  .dst-modebtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
8366
8425
  .dst-menu {
8367
- position: absolute; right: 8px; bottom: 44px; min-width: 170px;
8426
+ position: absolute; right: 0; bottom: calc(100% + 6px); min-width: 170px;
8368
8427
  background: var(--dsw-specific-menu); border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px;
8369
8428
  padding: 4px; z-index: 10; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
8370
8429
  }
@@ -8375,6 +8434,8 @@ const DRAWER_CSS = `
8375
8434
  cursor: pointer; text-align: left;
8376
8435
  }
8377
8436
  .dst-menuitem:hover { background: var(--dsw-alias-interactive-bg-hover); }
8437
+ /* 默认 shell 标记:菜单项右端的浅色小字。 */
8438
+ .dst-menudfl { margin-left: auto; padding-left: 16px; opacity: 0.55; font-size: 11px; }
8378
8439
  /* 终端区不带背景:直接透出抽屉容器的磨砂底色,与头部一致。 */
8379
8440
  .dst-body { flex: 1; min-height: 0; position: relative; padding: 2px 6px 6px; }
8380
8441
  /* 半透明底(合成层)使浏览器放弃亚像素抗锯齿、字形偏细;用 currentColor 细描边
@@ -8393,13 +8454,36 @@ const DRAWER_CSS = `
8393
8454
  color: var(--dsw-alias-state-error-primary); font-size: 12px;
8394
8455
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
8395
8456
  }
8396
- .dst-headbtn {
8397
- display: inline-flex; align-items: center; justify-content: center;
8398
- width: 26px; height: 26px; padding: 0;
8399
- border: none; border-radius: 6px; background: transparent;
8400
- color: inherit; cursor: pointer;
8457
+ /* 终端入口按钮 = 抽屉把手:贴在抽屉右上角外沿(bottom: 100%),随抽屉的
8458
+ transform 一同滑动——展开时吸附在终端右上角,收起后停在 frame 右下角。
8459
+ 抽屉收起态整树 visibility: hidden,这里显式覆写 visible,保证收起后把手
8460
+ 仍可见可点(visibility 的子树覆写语义)。背景与抽屉同为磨砂半透明,
8461
+ 收起时浮在页面内容之上;底部无边框,与抽屉顶边框线自然闭合。 */
8462
+ .dst-entry {
8463
+ position: absolute;
8464
+ right: 12px;
8465
+ bottom: 100%;
8466
+ display: inline-flex;
8467
+ align-items: center;
8468
+ justify-content: center;
8469
+ width: 40px;
8470
+ height: 26px;
8471
+ padding: 0;
8472
+ border: 1px solid var(--dsw-alias-border-l2);
8473
+ border-bottom: none;
8474
+ border-radius: 8px 8px 0 0;
8475
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-1) 75%, transparent);
8476
+ -webkit-backdrop-filter: blur(22px) saturate(1.4);
8477
+ backdrop-filter: blur(22px) saturate(1.4);
8478
+ color: var(--dsw-alias-label-secondary);
8479
+ cursor: pointer;
8480
+ visibility: visible;
8481
+ z-index: 2;
8482
+ }
8483
+ .dst-entry:hover {
8484
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-1) 92%, transparent);
8485
+ color: var(--dsw-alias-label-primary);
8401
8486
  }
8402
- .dst-headbtn:hover, .dst-headbtn.active { background: var(--dsw-alias-interactive-bg-active); }
8403
8487
  `;
8404
8488
  let injected = false;
8405
8489
  function injectStyles() {
@@ -8411,69 +8495,14 @@ function injectStyles() {
8411
8495
  document.head.appendChild(style);
8412
8496
  }
8413
8497
  //#endregion
8414
- //#region src/client/toggle.tsx
8415
- /**
8416
- * dsh-single-terminal —— 会话头部开关按钮(conversation.session.header.utilities 插槽)。
8417
- *
8418
- * 图标为 panel-bottom(面板底部条填充)样式:横长矩形(16×12,高 < 宽)、
8419
- * 直角无圆角,与宿主图标同风格(currentColor 填充);hover 气泡用宿主
8420
- * Tooltip(label 传 thunk,气泡显示时求值,语言切换即生效)。
8421
- *
8422
- * 会话作用域标准 props 带 sessionId 与全局 useWorkspaces:据此解析当前会话
8423
- * 归属的工作区根目录并同步给 controller,让新建终端自动落在工作区路径。
8424
- */
8425
- const noWorkspaces = () => null;
8426
- /** 外框 16×12(y 2→14)+ 上部镂空窗(壁 1.4),底部条 4.2 填充;全部直角。 */
8427
- const PANEL_BOTTOM_PATH = "M0 2 H16 V14 H0 Z M1.4 3.4 H14.6 V9.8 H1.4 Z";
8428
- function PanelBottomIcon(props) {
8429
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
8430
- width: props.size ?? 16,
8431
- height: props.size ?? 16,
8432
- viewBox: "0 0 16 16",
8433
- fill: "none",
8434
- "aria-hidden": "true",
8435
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
8436
- d: PANEL_BOTTOM_PATH,
8437
- fillRule: "evenodd",
8438
- clipRule: "evenodd",
8439
- fill: "currentColor"
8440
- })
8441
- });
8442
- }
8443
- function TerminalToggle(props) {
8444
- const state = useDrawerState();
8445
- const { sessionId, useWorkspaces = noWorkspaces } = props;
8446
- const workspacePath = useWorkspaces((snapshot) => {
8447
- if (sessionId === void 0) return null;
8448
- return snapshot.items.find((row) => row.sessionIds.includes(sessionId))?.path ?? null;
8449
- });
8450
- (0, react.useEffect)(() => {
8451
- terminal.setWorkspaceCwd(workspacePath);
8452
- return () => {
8453
- terminal.setWorkspaceCwd(null);
8454
- };
8455
- }, [workspacePath]);
8456
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
8457
- label: () => t("terminal.toggleTip"),
8458
- side: "bottom",
8459
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8460
- type: "button",
8461
- className: `dst-headbtn${state.open ? " active" : ""}`,
8462
- "aria-label": t("terminal.toggleTip"),
8463
- onClick: () => {
8464
- terminal.toggle();
8465
- },
8466
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelBottomIcon, { size: 16 })
8467
- })
8468
- });
8469
- }
8470
- //#endregion
8471
8498
  //#region src/client/plugin.tsx
8472
8499
  /**
8473
8500
  * dsh-single-terminal —— 浏览器半边插件主体(slots 注册)。
8474
8501
  *
8475
- * - shell.overlay:底部终端抽屉;
8476
- * - conversation.session.header.utilities:会话头部开关按钮(宿主 Tooltip 气泡);
8502
+ * - shell.overlay:底部终端抽屉(抽屉右上角外沿吸附终端入口把手,收起后
8503
+ * 把手落在页面右下角);
8504
+ * - conversation.session.header.utilities:不可见的会话作用域桥(渲染 null,
8505
+ * 只同步当前会话的工作区 cwd,头部无可见 UI);
8477
8506
  * - Alt+C 快捷键开关抽屉(ctx.effect 挂窗级监听,卸载自动清理;焦点在终端
8478
8507
  * 输入区时放行,让 shell 收到 ESC c 而不误触);
8479
8508
  * - 语言跟随宿主 locale 服务(软依赖,缺失时回退 en)。
@@ -8505,9 +8534,9 @@ function createPlugin() {
8505
8534
  }, TerminalDrawer));
8506
8535
  slots.inject("conversation.session.header.utilities", () => slots.register({
8507
8536
  name: "conversation.session.header.utilities",
8508
- id: "dsh-single-terminal-toggle",
8537
+ id: "dsh-single-terminal-scope",
8509
8538
  order: 100
8510
- }, TerminalToggle));
8539
+ }, SessionWorkspaceBridge));
8511
8540
  ctx.effect(() => {
8512
8541
  const onKeyDown = (event) => {
8513
8542
  if (!event.altKey || event.ctrlKey || event.metaKey || event.code !== "KeyC") return;