dsh-code-ui 1.3.0 → 1.3.2

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
@@ -2,13 +2,31 @@
2
2
 
3
3
  在 DeepSeek Harness(DSH)里的 **Cursor 风格 AI 代码编辑器**插件:文件树、多标签页编辑、语法高亮、引用/翻译给 AI。
4
4
 
5
- 它把 Cursor 那种「边看代码、边跟 AI 对话」的体验搬进 DSH:一个带活动栏、文件树、多标签页编辑器的代码工作区。**v1.2.0 起支持两种布局**:右侧停靠(code UI 固定占屏幕右侧、与聊天并排,默认)或悬浮窗口(可拖拽浮动窗口,内嵌 AI 输入框),插件卡片里可随时切换。
5
+ 它把 Cursor 那种「边看代码、边跟 AI 对话」的体验搬进 DSH:一个带活动栏、文件树、多标签页编辑器的代码工作区。**v1.2.0 起支持两种布局**:右侧停靠(code UI 固定占屏幕右侧、与聊天并排,默认布局)或悬浮窗口(可拖拽浮动窗口,内嵌 AI 输入框),插件卡片里可随时切换。
6
+
7
+ ## 界面一览
8
+
9
+ **右侧停靠布局(默认布局)**——对话与 code UI 左右并排,边聊边看代码:
10
+
11
+ ![右侧停靠布局:对话与 code UI 左右并排](https://github.com/soGoodayo/dsh-code-ui/raw/main/image/%E4%B8%8A%E4%B8%8B%E6%96%87%E4%B8%8EcodeUI%20%E5%B7%A6%E5%8F%B3%E5%B8%83%E5%B1%80.png)
12
+
13
+ **悬浮窗口布局**——可拖拽、可缩放的浮动编辑器窗口,内嵌 AI 输入框:
14
+
15
+ ![悬浮窗口布局](https://github.com/soGoodayo/dsh-code-ui/raw/main/image/%E6%82%AC%E6%B5%AE%E5%B8%83%E5%B1%80.png)
16
+
17
+ **两种切换按钮(插件卡片「切换按钮」可选)**——点击在 code UI 与 dsh UI(DSH 原生界面)之间切换,与布局任意组合:
18
+
19
+ | 会话头按钮 | 悬浮模式条(默认,可拖动) |
20
+ | :---: | :---: |
21
+ | ![会话头 ⌨ code UI 按钮](https://github.com/soGoodayo/dsh-code-ui/raw/main/image/%E4%BC%9A%E8%AF%9D%E5%A4%B4%E6%8C%89%E9%92%AE.png) | ![悬浮模式条](https://github.com/soGoodayo/dsh-code-ui/raw/main/image/%E6%82%AC%E6%B5%AE%E6%8C%89%E9%92%AE.png) |
22
+
23
+ > 📍 **所有配置都在:设置 → 插件 → 插件列表 → dsh-code-ui 的配置卡片**——编辑器界面(右侧停靠 / 悬浮窗口 / 隐藏)、编辑器主题、切换按钮、启用开关、检查更新,切换即时生效并持久化保存。
6
24
 
7
25
  ## 功能
8
26
 
9
- - **双布局可选**:右侧停靠(默认,与聊天并排) / 悬浮窗口(可拖拽缩放),插件卡片切换、即时生效
10
- - **切换按钮可选**:会话头 `⌨ code UI` 按钮(默认) / 悬浮模式条,与布局任意组合
11
- - **界面模式可持久化**:code UI / dsh UI(隐藏编辑器回归原生界面),所有入口切换均同步记忆
27
+ - **双布局可选**:右侧停靠(默认布局,与聊天并排) / 悬浮窗口(可拖拽缩放),插件卡片切换、即时生效
28
+ - **切换按钮可选**:悬浮模式条(默认,可拖动) / 会话头 `⌨ code UI` 按钮,与布局任意组合
29
+ - **界面模式可持久化**:code UI / dsh UI(**默认隐藏**,不自动占屏,入口按钮随时唤回),所有入口切换均同步记忆
12
30
  - **Cursor 风格工作区**:活动栏 + 文件树侧边栏 + 多标签页编辑器 + 状态栏
13
31
  - **侧栏收起与调宽**:« 按钮 / 📁 三态 / 拖到最窄自动收起,» 把手展开,右缘拖拽调宽
14
32
  - **停靠面板拖宽**:左缘手柄实时调整面板宽度,DSH 界面自动跟随
@@ -24,7 +42,7 @@
24
42
  - **内嵌 AI 输入框**(悬浮布局):直接输入、`Ctrl+Enter` 发送;发送时不粘贴引用正文,只附一行引用指针,内容由 AI 调用 `get_referenced_snippets` 工具读取
25
43
  - **主题四态**:跟随 DSH(默认)/ 跟随系统 / 浅色 / 深色,插件卡片切换
26
44
  - **以此文件夹为根**:文件浏览器里可选任意目录作为工作区根
27
- - **检查更新 / 一键更新**:设置 → 插件 → 插件配置 中的 dsh-code-ui 卡片,支持在线检查 npm 新版本并一键更新;未发布/开发期可填本地 `.tgz` 路径安装(pnpm 流式进度展示,失败自动归类给出建议,完成后提示重启 DSH 生效)
45
+ - **检查更新 / 一键更新**:设置 → 插件 → 插件列表 → dsh-code-ui 的配置卡片,支持在线检查 npm 新版本并一键更新;未发布/开发期可填本地 `.tgz` 路径安装(pnpm 流式进度展示,失败自动归类给出建议,完成后提示重启 DSH 生效)
28
46
 
29
47
  ## 安装
30
48
 
@@ -34,7 +52,7 @@
34
52
  dsh plugin --profile web add dsh-code-ui
35
53
  ```
36
54
 
37
- 安装完成后重启 DSH,会话标题栏出现 `⌨ code UI` 按钮,点击即可打开右侧停靠的编辑器(默认);也可在插件卡片改为悬浮窗口布局与悬浮模式条切换。
55
+ 安装完成后重启 DSH,编辑器**默认隐藏**(dsh UI,不自动占屏);页面顶部出现可拖动的**悬浮模式条**(默认切换按钮),点 `code UI` 即可打开编辑器——默认布局为右侧停靠、默认根目录为**当前会话的工作区**,想换会话头 ⌨ 按钮或悬浮窗口,到 **设置 → 插件 → 插件列表 → dsh-code-ui** 的配置卡片里切换。
38
56
 
39
57
  或手动安装:
40
58
 
@@ -84,22 +102,15 @@ dsh-code-ui/
84
102
  - `lib/client.js`:改编辑器 UI 布局、配色(`styles` 模板串里的 CSS)、或新增右键菜单项
85
103
  - 引用/备注数据存模块内存(运行期有效,重启清空)
86
104
 
87
- ## 发布
88
-
89
- ```bash
90
- npm publish --dry-run
91
- npm publish
92
- ```
93
-
94
- 发布前确认 `name` 未被占用,必要时改为 scope 如 `@yourname/dsh-code-ui`。
95
-
96
105
  ## 更新日志
97
106
 
98
107
  > 约定:每次版本升级在本节顶部追加一行(时间 | 版本 | 修复/新增内容)。
99
108
 
100
109
  | 时间 | 版本 | 修复 / 新增 |
101
110
  | --- | --- | --- |
102
- | 2026-09-02 | v1.3.0 | 里程碑:文件树右键操作(真实磁盘)。右键文件:打开 / 引用给 AI(整个文件)/ 复制路径 / 在资源管理器中显示 / 重命名 / 删除;右键文件夹:新建文件 / 新建文件夹 / 以此文件夹为根 / 刷新 / 复制路径 / 定位 / 重命名 / 删除(递归强确认);空白区右键:根目录新建 / 刷新 / 更换根;新建重命名输入弹窗(Enter 确认)、危险操作确认弹窗、工作区根禁删;重命名自动同步标签页/缓冲/watcher,删除自动清理该路径下全部标签。引用体系升级:整文件引用只传绝对路径由 AI 自行读取;停靠布局下 DSH 原生输入条发送也自动附加引用指针(挂钩 submit,两布局统一)。打开文件弹窗:输入路径回车直接认定(文件夹=设为根 / 文件=打开)。右键菜单视口边界自适应、点击菜单项后自动关闭;⟳ 刷新不再折叠整棵树 |
111
+ | 2026-09-02 | v1.3.2 | 调整:**切换按钮默认改为悬浮模式条**(原会话头 ⌨ 按钮,已保存选择的用户不受影响);**文件树默认根跟随当前会话的工作区目录**——打开编辑器即定位到当前对话的项目根,会话切换自动换根(有未保存文件时暂不跟随并提示,保存后点 ⟳ 即切换);手动「以此文件夹为根」后不再自动跟随 |
112
+ | 2026-09-02 | v1.3.1 | 调整:安装后编辑器**默认隐藏**(dsh UI,新装不自动占屏,⌨ / 模式条随时唤回),布局默认仍为右侧停靠;配置卡片「编辑器界面」三态说明与 README 措辞同步。新增:一键更新 / 本地包更新成功后按钮文案临时变为「需重启 DSH 生效」(置灰防误点);README 增加「界面一览」(双布局与两种切换按钮截图)并明确配置入口路径(设置 → 插件 → 插件列表 → dsh-code-ui) |
113
+ | 2026-09-02 | v1.3.0 | 新增:文件树右键操作(真实磁盘)。右键文件:打开 / 引用给 AI(整个文件)/ 复制路径 / 在资源管理器中显示 / 重命名 / 删除;右键文件夹:新建文件 / 新建文件夹 / 以此文件夹为根 / 刷新 / 复制路径 / 定位 / 重命名 / 删除(递归强确认);空白区右键:根目录新建 / 刷新 / 更换根;新建重命名输入弹窗(Enter 确认)、危险操作确认弹窗、工作区根禁删;重命名自动同步标签页/缓冲/watcher,删除自动清理该路径下全部标签。引用体系升级:整文件引用只传绝对路径由 AI 自行读取;停靠布局下 DSH 原生输入条发送也自动附加引用指针(挂钩 submit,两布局统一)。配置卡片:「布局模式 × 界面模式」合并为三态「编辑器界面」(右侧停靠/悬浮窗口/隐藏 dsh UI,隐藏时记住布局)。打开文件弹窗:输入路径回车直接认定(文件夹=设为根 / 文件=打开)。提示条移至顶部居中、可拖拽自定位置(⠿ 把手)。右键菜单视口边界自适应、点击菜单项后自动关闭;⟳ 刷新不再折叠整棵树 |
103
114
  | 2026-09-02 | v1.2.0 | 里程碑:双布局与全面可配置。新增右侧停靠布局(code UI 固定占右侧、DSH 界面推到左侧并排,Cursor 式;左缘拖拽实时调宽,壳层帧用 [data-slot=root] 语义属性定位 + 内联 margin-right 推挤);插件卡片新增「布局模式」(右侧停靠=默认 / 悬浮窗口)、「界面模式」(code UI / dsh UI,状态持久化、所有入口同步记忆)、「切换按钮」(会话头 ⌨ 按钮=默认 / 悬浮模式条,与布局任意组合),全部即时生效 + 持久化;文件树侧栏可收起(« / 📁 三态 / 拖窄收起)+ » 展开把手 + 右缘拖宽;侧栏标题不折行;停靠面板去投影;界面文案更名(Code UI→code UI、原始风格-ui→dsh UI);悬浮布局也获得侧栏收起/调宽能力 |
104
115
  | 2026-09-02 | v1.2.0-beta.1 | 里程碑:右侧停靠模式(feature/native-layout 分支)--会话头「⌨ Code UI」开关按钮,Code UI 固定占右侧、DSH 界面推到左侧并排(Cursor 式布局);左缘拖拽实时调宽;文件树侧栏可收起(« 按钮 / 📁 三态 / 拖到最窄松手收起)+ » 展开把手 + 右缘拖拽调宽;侧栏标题不折行;移除停靠面板投影。推挤采用 [data-slot=root] 语义属性 + 内联 margin-right,不依赖哈希类名 |
105
116
  | 2026-09-01 | v1.1.10 | 调整:底部状态栏不再用反色按钮样式,改用输入框同款背景(bg-overlay)+ 次要文字色 + 顶部分隔线,深浅色模式下均与编辑器融为一体 |
package/lib/client.js CHANGED
@@ -142,7 +142,9 @@ window.__ModuleLoader__.load({
142
142
  .cdex-ref-code{background:var(--dsw-alias-bg-base,#1e1e1e);border-radius:4px;padding:6px;font-family:Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-primary,#d4d4d4);max-height:140px;overflow:auto;}
143
143
  .cdex-section{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin:12px 0 6px;}
144
144
  .cdex-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-top:12px;line-height:1.6;}
145
- .cdex-toast{position:fixed;z-index:10003;bottom:36px;left:50%;transform:translateX(-50%);background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);padding:9px 16px;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.4);max-width:80vw;}
145
+ .cdex-toast{position:fixed;z-index:10003;top:36px;left:50%;transform:translateX(-50%);background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);padding:9px 16px;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.4);max-width:80vw;cursor:grab;user-select:none;touch-action:none;}
146
+ .cdex-toast.placed{transform:none;cursor:grabbing;}
147
+ .cdex-toast-grip{margin-right:8px;opacity:.55;font-size:12px;letter-spacing:-1px;}
146
148
  .cdex-resize{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:10;}
147
149
  .cdex-resize::after{content:'';position:absolute;right:4px;bottom:4px;width:8px;height:8px;border-right:2px solid var(--dsw-alias-label-secondary,#7f849c);border-bottom:2px solid var(--dsw-alias-label-secondary,#7f849c);}
148
150
  .cdex-aiinput-wrap{position:relative;padding:10px 14px 16px;background:transparent;border-top:1px solid var(--dsw-alias-border-l1,#3e3e42);}
@@ -261,6 +263,11 @@ window.__ModuleLoader__.load({
261
263
  confirmBox: null,
262
264
  noteEditor: null,
263
265
  toast: null,
266
+ toastKeep: false,
267
+ toastPos: null,
268
+ sessionId: null, // 当前会话 id(InputBridge 从 slot kit 捕获):文件树默认根跟随当前会话
269
+ rootIsAuto: true, // 根是否为自动跟随(用户「以此文件夹为根」后置 false,不再跟随)
270
+ rootSessionId: null, // 当前自动根来自哪个会话(落后于 sessionId 时触发重新解析)
264
271
  error: null,
265
272
  }
266
273
  const listeners = new Set()
@@ -277,10 +284,55 @@ window.__ModuleLoader__.load({
277
284
 
278
285
  function showToast(msg) {
279
286
  store.toast = { msg }
287
+ store.toastKeep = false
280
288
  emit()
289
+ scheduleToastHide(2800)
290
+ }
291
+ // 提示自动消失;正在被拖拽(toastKeep)时顺延,松手后短暂展示再消失
292
+ function scheduleToastHide(ms) {
281
293
  if (timer && typeof timer.timeout === 'function') {
282
- timer.timeout(() => { store.toast = null; emit() }, 2800)
294
+ timer.timeout(() => {
295
+ if (store.toastKeep) { scheduleToastHide(600); return }
296
+ store.toast = null; emit()
297
+ }, ms)
298
+ }
299
+ }
300
+ // 提示条拖拽:按住移动自定义位置(默认顶部居中,拖过之后记住位置,防止挡内容)
301
+ function onToastPointerDown(e) {
302
+ e.preventDefault()
303
+ const el = e.currentTarget
304
+ const rect = el.getBoundingClientRect()
305
+ const base = store.toastPos || { x: rect.left, y: rect.top }
306
+ const px = e.clientX, py = e.clientY
307
+ store.toastKeep = true
308
+ try { el.setPointerCapture(e.pointerId) } catch (err) {}
309
+ function move(ev) {
310
+ const vw = (typeof window !== 'undefined' && window.innerWidth) || 1200
311
+ const vh = (typeof window !== 'undefined' && window.innerHeight) || 800
312
+ store.toastPos = {
313
+ x: Math.max(4, Math.min(base.x + (ev.clientX - px), vw - rect.width - 4)),
314
+ y: Math.max(4, Math.min(base.y + (ev.clientY - py), vh - rect.height - 4)),
315
+ }
316
+ emit()
317
+ }
318
+ function up() {
319
+ store.toastKeep = false
320
+ el.removeEventListener('pointermove', move)
321
+ el.removeEventListener('pointerup', up)
322
+ el.removeEventListener('pointercancel', up)
283
323
  }
324
+ el.addEventListener('pointermove', move)
325
+ el.addEventListener('pointerup', up)
326
+ el.addEventListener('pointercancel', up)
327
+ }
328
+ function Toast() {
329
+ const st = useStore()
330
+ if (!st.toast) return null
331
+ const style = st.toastPos ? { left: st.toastPos.x, top: st.toastPos.y } : null
332
+ return h('div', { className: 'cdex-toast' + (st.toastPos ? ' placed' : ''), style: style, title: '按住可拖动位置(不再挡内容)', onPointerDown: onToastPointerDown },
333
+ h('span', { className: 'cdex-toast-grip' }, '⠿'),
334
+ st.toast.msg,
335
+ )
284
336
  }
285
337
 
286
338
  /* ============ 编辑器主题四态:dsh(默认,跟随 DSH 宿主外观) / system(跟随 OS) / light / dark ============ */
@@ -564,7 +616,7 @@ window.__ModuleLoader__.load({
564
616
  emit()
565
617
  const start = store.browsePath || store.rootPath || null
566
618
  if (start) { browseDir(start); return }
567
- callHost('get-root').then((r) => {
619
+ callHost('get-root', store.sessionId ? { sessionId: store.sessionId } : {}).then((r) => {
568
620
  if (r && r.root) { browseDir(r.root) }
569
621
  else { store.error = '未定位到工作区根,请在输入框粘贴目录或文件路径后回车'; emit() }
570
622
  })
@@ -578,7 +630,13 @@ window.__ModuleLoader__.load({
578
630
  if (!path) return
579
631
  const res = await callHost('set-root', { path })
580
632
  if (res && res.error) { showToast('设置失败:' + res.error); return }
581
- const newRoot = (res && res.root) ? res.root : path
633
+ store.rootIsAuto = false // 用户显式选择:此后不再跟随会话切换
634
+ store.rootSessionId = null
635
+ await applyRoot((res && res.root) ? res.root : path)
636
+ showToast('工作区根目录:' + store.rootPath)
637
+ }
638
+ // 换根并整体重置文件树/标签页/缓冲(自动跟随与手动设置共用)
639
+ async function applyRoot(newRoot) {
582
640
  store.rootPath = newRoot
583
641
  store.nodes = {}
584
642
  store.tabs = []
@@ -593,19 +651,18 @@ window.__ModuleLoader__.load({
593
651
  store.filePickerOpen = false
594
652
  emit()
595
653
  await toggleExpand(newRoot)
596
- showToast('工作区根目录:' + newRoot)
597
654
  }
598
655
  // —— 文件树 ——
599
656
  async function refresh() {
600
657
  store.error = null
601
- if (!store.rootPath) {
602
- const r = await callHost('get-root')
603
- store.rootPath = (r && r.root) || null
604
- if (!store.rootPath) { store.error = '未找到工作区根目录'; emit(); return }
605
- store.nodes = {}
606
- store.nodes[store.rootPath] = { name: baseName(store.rootPath), path: store.rootPath, isDir: true, children: [], expanded: true, loaded: false }
607
- emit()
608
- await toggleExpand(store.rootPath)
658
+ const sid = store.sessionId
659
+ // 自动根模式:首次无根、或会话已切换(rootSessionId 落后)都重新解析当前会话的工作区
660
+ if (!store.rootPath || (store.rootIsAuto && sid && sid !== store.rootSessionId)) {
661
+ const r = await callHost('get-root', sid ? { sessionId: sid } : {})
662
+ const root = (r && r.root) || null
663
+ if (!root) { store.error = '未找到工作区根目录'; emit(); return }
664
+ await applyRoot(root)
665
+ store.rootSessionId = sid
609
666
  } else {
610
667
  // 修复:刷新不再折叠整棵树,只重载根目录(子目录展开状态保留在 store.nodes)
611
668
  await reloadDir(store.rootPath)
@@ -986,6 +1043,15 @@ window.__ModuleLoader__.load({
986
1043
  store.inputDraft = inputState.draft
987
1044
  store.inputPhase = inputState.phase
988
1045
  }
1046
+ // 捕获当前会话 id:文件树默认根跟随当前会话的工作区目录
1047
+ if (props.sessionId && props.sessionId !== store.sessionId) {
1048
+ store.sessionId = props.sessionId
1049
+ if (store.rootIsAuto) {
1050
+ const hasDirty = Object.keys(store.dirty || {}).some((k) => store.dirty[k])
1051
+ if (hasDirty) showToast('会话已切换:有未保存文件,工作区暂不跟随(保存后点 ⟳ 刷新即可)')
1052
+ else refresh()
1053
+ }
1054
+ }
989
1055
  emit()
990
1056
  })
991
1057
  // 挂钩 DSH 原生输入条的发送:存在引用时把指针附进草稿再发(整文件=绝对路径)。
@@ -1472,7 +1538,7 @@ window.__ModuleLoader__.load({
1472
1538
  st.noteEditor ? h(NoteEditor) : null,
1473
1539
  st.nameDialog ? h(NameDialog) : null,
1474
1540
  st.confirmBox ? h(ConfirmBox) : null,
1475
- st.toast ? h('div', { className: 'cdex-toast' }, st.toast.msg) : null,
1541
+ st.toast ? h(Toast) : null,
1476
1542
  )
1477
1543
  }
1478
1544
 
@@ -1586,7 +1652,7 @@ window.__ModuleLoader__.load({
1586
1652
  st.noteEditor ? h(NoteEditor) : null,
1587
1653
  st.nameDialog ? h(NameDialog) : null,
1588
1654
  st.confirmBox ? h(ConfirmBox) : null,
1589
- st.toast ? h('div', { className: 'cdex-toast' }, st.toast.msg) : null,
1655
+ st.toast ? h(Toast) : null,
1590
1656
  )
1591
1657
  }
1592
1658
 
@@ -1595,7 +1661,7 @@ window.__ModuleLoader__.load({
1595
1661
  // get-self 失败 / 字段缺失(旧 host)时绝不隐藏编辑器,只有明确 enabled===false 才不挂载。
1596
1662
  let unmountEditorFns = null
1597
1663
  let layoutMode = 'dock' // dock(右侧停靠,默认) / float(悬浮窗口):编辑器以哪种布局呈现
1598
- let toggleStyle = 'header' // header(会话头 ⌨ 按钮,默认) / bar(悬浮模式条):用哪种按钮切换界面
1664
+ let toggleStyle = 'bar' // bar(悬浮模式条,默认) / header(会话头 ⌨ 按钮):用哪种按钮切换界面
1599
1665
  function mountEditor() {
1600
1666
  if (unmountEditorFns) return
1601
1667
  const disposers = []
@@ -1669,13 +1735,14 @@ window.__ModuleLoader__.load({
1669
1735
  if (v) mountEditor()
1670
1736
  else unmountEditor()
1671
1737
  }
1672
- // 启动时先查开关/主题/布局/界面模式/切换按钮再挂载(RPC 失败 = 全默认:启用+跟随DSH+停靠+显示+会话头按钮)
1738
+ // 启动时先查开关/主题/布局/界面模式/切换按钮再挂载(新装默认:启用+跟随DSH+停靠+隐藏+悬浮模式条;
1739
+ // RPC 失败回退到「可见+会话头按钮」保证可发现性)
1673
1740
  callHost('get-self').then((r) => {
1674
1741
  if (r && !r.error) {
1675
1742
  setThemeMode(r.theme)
1676
1743
  layoutMode = (r.layout === 'float') ? 'float' : 'dock'
1677
- toggleStyle = (r.toggle === 'bar') ? 'bar' : 'header'
1678
- applyUiMode(r.uiMode === 'native' ? 'native' : 'codeui')
1744
+ toggleStyle = (r.toggle === 'header') ? 'header' : 'bar'
1745
+ applyUiMode(r.uiMode === 'codeui' ? 'codeui' : 'native')
1679
1746
  applyEnabled(!(r.enabled === false))
1680
1747
  } else {
1681
1748
  setThemeMode('dsh')
@@ -1695,8 +1762,7 @@ window.__ModuleLoader__.load({
1695
1762
  const [localPath, setLocalPath] = React.useState('')
1696
1763
  const [enabling, setEnabling] = React.useState(false)
1697
1764
  const [theming, setTheming] = React.useState(false)
1698
- const [layouting, setLayouting] = React.useState(false)
1699
- const [uiModeing, setUiModeing] = React.useState(false)
1765
+ const [uishing, setUishing] = React.useState(false)
1700
1766
  const [toggling, setToggling] = React.useState(false)
1701
1767
  const lastPayloadRef = React.useRef(null)
1702
1768
  const pollDisposeRef = React.useRef(null)
@@ -1717,36 +1783,46 @@ window.__ModuleLoader__.load({
1717
1783
  })
1718
1784
  }
1719
1785
 
1720
- // 布局模式:写 host settings 持久化;成功后立即卸载旧挂载、按新模式重挂
1721
- function setLayout(m) {
1722
- if (!self || layouting) return
1723
- setLayouting(true)
1724
- callHost('set-layout', { layout: m }).then((r) => {
1725
- setLayouting(false)
1726
- if (r && !r.error) {
1727
- setSelf(Object.assign({}, self, { layout: m }))
1728
- applyLayout(m)
1729
- showToast('布局已切换:' + (m === 'dock' ? '右侧停靠' : '悬浮窗口'))
1730
- } else {
1731
- showToast('切换失败:' + ((r && r.error) || '未知错误'))
1732
- }
1733
- })
1734
- }
1735
-
1736
- // 界面模式:写 host settings 持久化;成功后立即显示/隐藏编辑器(按当前布局)
1737
- function setUiMode(m) {
1738
- if (!self || uiModeing) return
1739
- setUiModeing(true)
1740
- callHost('set-uimode', { uiMode: m }).then((r) => {
1741
- setUiModeing(false)
1742
- if (r && !r.error) {
1743
- setSelf(Object.assign({}, self, { uiMode: m }))
1744
- applyUiMode(m)
1745
- showToast('界面已切换:' + (m === 'codeui' ? 'code UI' : 'dsh UI'))
1746
- } else {
1747
- showToast('切换失败:' + ((r && r.error) || '未知错误'))
1748
- }
1749
- })
1786
+ // 编辑器界面(三态,合并原「布局模式 × 界面模式」):
1787
+ // dock=右侧停靠 / float=悬浮窗口 / hidden=隐藏(dsh UI,内部记住当前布局,切回原样恢复)
1788
+ function setUiShape(v) {
1789
+ if (!self || uishing) return
1790
+ const curLayout = (self.layout === 'float') ? 'float' : 'dock'
1791
+ const curHidden = self.uiMode === 'native'
1792
+ if (v === 'hidden') {
1793
+ if (curHidden) return
1794
+ setUishing(true)
1795
+ callHost('set-uimode', { uiMode: 'native' }).then((r) => {
1796
+ setUishing(false)
1797
+ if (r && !r.error) {
1798
+ setSelf(Object.assign({}, self, { uiMode: 'native' }))
1799
+ applyUiMode('native')
1800
+ showToast('编辑器已隐藏(dsh UI)')
1801
+ } else { showToast('切换失败:' + ((r && r.error) || '未知错误')) }
1802
+ })
1803
+ return
1804
+ }
1805
+ const needLayout = curLayout !== v
1806
+ const needShow = curHidden
1807
+ if (!needLayout && !needShow) return
1808
+ setUishing(true)
1809
+ const next = Object.assign({}, self, { layout: v, uiMode: 'codeui' })
1810
+ const step = () => {
1811
+ callHost('set-uimode', { uiMode: 'codeui' }).then((r2) => {
1812
+ setUishing(false)
1813
+ if (r2 && !r2.error) {
1814
+ setSelf(next)
1815
+ applyUiMode('codeui')
1816
+ showToast('编辑器界面:' + (v === 'dock' ? '右侧停靠' : '悬浮窗口'))
1817
+ } else { showToast('切换失败:' + ((r2 && r2.error) || '未知错误')) }
1818
+ })
1819
+ }
1820
+ if (needLayout) {
1821
+ callHost('set-layout', { layout: v }).then((r1) => {
1822
+ if (r1 && !r1.error) { applyLayout(v); step() }
1823
+ else { setUishing(false); showToast('切换失败:' + ((r1 && r1.error) || '未知错误')) }
1824
+ })
1825
+ } else { step() }
1750
1826
  }
1751
1827
 
1752
1828
  // 切换按钮样式:写 host settings 持久化;成功后立即换一种切换按钮
@@ -1828,6 +1904,8 @@ window.__ModuleLoader__.load({
1828
1904
 
1829
1905
  const cur = (self && self.version) || (chk && chk.current) || '?'
1830
1906
  const started = !!(run && !run.done)
1907
+ // 更新已成功、等待重启:更新类按钮文案临时改为「需重启 DSH 生效」,用户一眼可见
1908
+ const updated = !!(run && run.done && run.ok)
1831
1909
  let checkLine = null
1832
1910
  if (chk && chk.checking) checkLine = h('p', { className: 'cdx-pc-note' }, '检查中…')
1833
1911
  else if (chk && chk.error) checkLine = h('p', { className: 'cdx-pc-err' }, chk.error)
@@ -1883,37 +1961,25 @@ window.__ModuleLoader__.load({
1883
1961
  ),
1884
1962
  h('p', { className: 'cdx-pc-note' }, '「跟随 DSH」随 DSH 整体外观(默认);「跟随系统」随操作系统浅色/深色实时切换;浅色/深色为强制指定。切换立即生效,仅作用于编辑器界面,选择持久化保存。'),
1885
1963
  ),
1886
- // 字段:布局模式
1964
+ // 字段:编辑器界面(三态,合并原「布局模式 × 界面模式」)
1887
1965
  h('div', { className: 'cdx-pc-field' },
1888
1966
  h('div', { className: 'cdx-pc-head' },
1889
- h('span', { className: 'cdx-pc-label' }, '布局模式'),
1967
+ h('span', { className: 'cdx-pc-label' }, '编辑器界面'),
1890
1968
  h('span', { className: 'cdx-pc-head' },
1891
- ['dock', 'float'].map((m) => h('button', {
1892
- key: m,
1893
- type: 'button',
1894
- className: 'cdx-pc-btn ' + (((self && self.layout) || 'dock') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1895
- disabled: !self || layouting,
1896
- onClick: () => setLayout(m),
1897
- }, m === 'dock' ? '右侧停靠' : '悬浮窗口')),
1898
- ),
1899
- ),
1900
- h('p', { className: 'cdx-pc-note' }, '「右侧停靠」code UI 固定占屏幕右侧、与聊天并排,会话头 ⌨ 按钮开关(默认);「悬浮窗口」为可拖拽的浮动编辑器窗口,盖住页面、含自带输入框。切换即时生效,选择持久化保存。'),
1901
- ),
1902
- // 字段:界面模式
1903
- h('div', { className: 'cdx-pc-field' },
1904
- h('div', { className: 'cdx-pc-head' },
1905
- h('span', { className: 'cdx-pc-label' }, '界面模式'),
1906
- h('span', { className: 'cdx-pc-head' },
1907
- ['codeui', 'native'].map((m) => h('button', {
1908
- key: m,
1909
- type: 'button',
1910
- className: 'cdx-pc-btn ' + (((self && self.uiMode) || 'codeui') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1911
- disabled: !self || uiModeing,
1912
- onClick: () => setUiMode(m),
1913
- }, m === 'codeui' ? 'code UI' : 'dsh UI')),
1969
+ (function () {
1970
+ const curLayout = ((self && self.layout) === 'float') ? 'float' : 'dock'
1971
+ const cur = (self && self.uiMode) === 'native' ? 'hidden' : curLayout
1972
+ return ['dock', 'float', 'hidden'].map((m) => h('button', {
1973
+ key: m,
1974
+ type: 'button',
1975
+ className: 'cdx-pc-btn ' + (cur === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1976
+ disabled: !self || uishing,
1977
+ onClick: () => setUiShape(m),
1978
+ }, m === 'dock' ? '右侧停靠' : m === 'float' ? '悬浮窗口' : '隐藏(dsh UI)'))
1979
+ })(),
1914
1980
  ),
1915
1981
  ),
1916
- h('p', { className: 'cdx-pc-note' }, '「code UI」显示编辑器界面(默认);「dsh UI」隐藏编辑器、回归 DSH 原生界面(入口按钮保留,随时切回)。悬浮模式条 / ⌨ / ✕ / ↩ 的每次切换都会同步到此处,选择持久化保存。'),
1982
+ h('p', { className: 'cdx-pc-note' }, '「隐藏(dsh UI)」为默认:新装不自动占屏,⌨ / 模式条 / ✕ 随时唤回,隐藏期间记住当前布局、切回原样恢复;「右侧停靠」code UI 固定占屏幕右侧、与聊天并排(默认布局);「悬浮窗口」为可拖拽的浮动编辑器窗口、含自带输入框。切换即时生效,持久化保存。'),
1917
1983
  ),
1918
1984
  // 字段:切换按钮
1919
1985
  h('div', { className: 'cdx-pc-field' },
@@ -1923,13 +1989,13 @@ window.__ModuleLoader__.load({
1923
1989
  ['header', 'bar'].map((m) => h('button', {
1924
1990
  key: m,
1925
1991
  type: 'button',
1926
- className: 'cdx-pc-btn ' + (((self && self.toggle) || 'header') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1992
+ className: 'cdx-pc-btn ' + (((self && self.toggle) || 'bar') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1927
1993
  disabled: !self || toggling,
1928
1994
  onClick: () => setToggle(m),
1929
1995
  }, m === 'header' ? '会话头按钮' : '悬浮模式条')),
1930
1996
  ),
1931
1997
  ),
1932
- h('p', { className: 'cdx-pc-note' }, '选择用哪种按钮在 code UI 与dsh UI 之间切换:「会话头按钮」在会话标题栏显示 ⌨ code UI(默认);「悬浮模式条」为屏幕上方可拖动的横条(code UI / dsh UI 两键)。与布局模式无关,可任意组合,选择持久化保存。'),
1998
+ h('p', { className: 'cdx-pc-note' }, '选择用哪种按钮在 code UI 与 dsh UI 之间切换:「悬浮模式条」为屏幕上方可拖动的横条(code UI / dsh UI 两键,默认);「会话头按钮」在会话标题栏显示 ⌨ code UI。与布局模式无关,可任意组合,选择持久化保存。'),
1933
1999
  ),
1934
2000
  // 字段一:在线检查更新
1935
2001
  h('div', { className: 'cdx-pc-field' },
@@ -1943,14 +2009,14 @@ window.__ModuleLoader__.load({
1943
2009
  chk && chk.updateAvailable ? h('div', { className: 'cdx-pc-field' },
1944
2010
  h('div', { className: 'cdx-pc-head' },
1945
2011
  h('span', { className: 'cdx-pc-label' }, h('span', { className: 'cdx-pc-new' }, '🆕 有新版本:v' + chk.current + ' -> v' + chk.latest)),
1946
- h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', disabled: started, onClick: () => startUpdate({ source: 'registry', version: chk.latest }) }, '一键更新到 v' + chk.latest),
2012
+ h('button', { type: 'button', className: 'cdx-pc-btn ' + (updated ? 'cdx-pc-btn-outline' : 'cdx-pc-btn-primary'), disabled: started || updated, onClick: () => startUpdate({ source: 'registry', version: chk.latest }) }, updated ? '需重启 DSH 生效' : '一键更新到 v' + chk.latest),
1947
2013
  ),
1948
2014
  ) : null,
1949
2015
  // 字段三:本地包更新
1950
2016
  h('div', { className: 'cdx-pc-field' },
1951
2017
  h('div', { className: 'cdx-pc-head' },
1952
2018
  h('span', { className: 'cdx-pc-label' }, '本地包更新(开发期 / 未发布时)'),
1953
- h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: started || !localPath.trim(), onClick: () => startUpdate({ source: 'local', path: localPath.trim() }) }, '安装本地包'),
2019
+ h('button', { type: 'button', className: 'cdx-pc-btn ' + (updated ? 'cdx-pc-btn-outline' : 'cdx-pc-btn-outline'), disabled: started || updated || !localPath.trim(), onClick: () => startUpdate({ source: 'local', path: localPath.trim() }) }, updated ? '需重启 DSH 生效' : '安装本地包'),
1954
2020
  ),
1955
2021
  h('div', { className: 'cdx-pc-head' },
1956
2022
  h('input', {
package/lib/index.js CHANGED
@@ -100,13 +100,13 @@ const CodeUiSettingsSchema = z.object({
100
100
  theme: z.string().default("dsh"),
101
101
  // 布局模式:dock(右侧停靠,默认) / float(悬浮窗口)
102
102
  layout: z.string().default("dock"),
103
- // 界面模式:codeui(显示编辑器,默认) / native(原始风格-ui,仅留入口按钮)
104
- uiMode: z.string().default("codeui"),
105
- // 切换按钮样式:header(会话头 ⌨ 按钮,默认) / bar(悬浮模式条)
106
- toggle: z.string().default("header"),
103
+ // 界面模式:native(隐藏编辑器,默认——新装不自动占屏,由 ⌨ 按钮唤出) / codeui(显示编辑器)
104
+ uiMode: z.string().default("native"),
105
+ // 切换按钮样式:bar(悬浮模式条,默认) / header(会话头 ⌨ 按钮)
106
+ toggle: z.string().default("bar"),
107
107
  });
108
108
  // 解析后的当前值:settings 服务未挂载/未写入时保持默认
109
- const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "codeui", toggle: "header" };
109
+ const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "native", toggle: "bar" };
110
110
  // settings 服务引用(「启用/禁用」开关写入用;由 installSelfSettings 捕获)
111
111
  let settingsServiceRef = null;
112
112
  function installSelfSettings(ctx) {
@@ -349,13 +349,9 @@ function unwatchAllFiles() {
349
349
 
350
350
  function rpcHandler(ctx) {
351
351
  const fs = ctx.get("fs");
352
- let currentRootRef = detectWorkspaceRoot(ctx);
352
+ let currentRootRef = detectWorkspaceRoot(ctx); // 最近一次解析的根:list-dir 的兜底
353
+ let manualRootRef = null; // 用户「以此文件夹为根」的显式选择:一旦设置不再跟随会话
353
354
 
354
- // 每次 get-root 动态回读会话工作区根,避免一次缓存导致拿不到当前项目目录
355
- function baseRoot() {
356
- if (currentRootRef) return currentRootRef;
357
- return detectWorkspaceRoot(ctx);
358
- }
359
355
  function detectWorkspaceRoot(_ctx) {
360
356
  // 1) 当前会话的真实工作区根(项目根):优先取正在发起调用的 agent 的 session cwd。
361
357
  // sandboxPolicy.workspaceRoot 只是「部署回退根」,常是 DSH 进程 cwd 而非项目目录。
@@ -388,13 +384,29 @@ function rpcHandler(ctx) {
388
384
  const args = payload && typeof payload === "object" ? payload : {};
389
385
 
390
386
  if (endpoint === "get-root") {
391
- const root = baseRoot();
387
+ // 优先级:手动设置的根 > 浏览器上报的当前会话 cwd(agent/session id 共享,agents.get 可查)
388
+ // > initiator 会话 cwd(agent 驱动中)> 部署回退根 > 进程 cwd
389
+ let root = manualRootRef;
390
+ if (!root && args.sessionId) {
391
+ const agentsSvc = ctx.get("agents");
392
+ if (agentsSvc) {
393
+ try {
394
+ const a = agentsSvc.get(String(args.sessionId));
395
+ if (a && a.session && a.session.header && a.session.header.cwd) {
396
+ root = a.session.header.cwd;
397
+ }
398
+ } catch (_) {}
399
+ }
400
+ }
401
+ if (!root) root = detectWorkspaceRoot(ctx);
402
+ if (root) currentRootRef = root;
392
403
  return ok({ root });
393
404
  }
394
405
 
395
406
  if (endpoint === "set-root") {
396
407
  if (!args.path) return ok({ error: "no-path" });
397
- currentRootRef = String(args.path);
408
+ manualRootRef = String(args.path);
409
+ currentRootRef = manualRootRef;
398
410
  return ok({ root: currentRootRef });
399
411
  }
400
412
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-ui",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Cursor 风格的 AI 代码编辑器工作台插件(DSH):文件树、多标签页编辑、语法高亮、引用/翻译给 AI、右侧停靠/悬浮窗口双布局",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",