dsh-code-ui 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -14
- package/lib/client.js +370 -91
- package/lib/index.js +80 -8
- package/package.json +1 -1
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
|
|
5
|
+
它把 Cursor 那种「边看代码、边跟 AI 对话」的体验搬进 DSH:一个带活动栏、文件树、多标签页编辑器的代码工作区。**v1.2.0 起支持两种布局**:右侧停靠(code UI 固定占屏幕右侧、与聊天并排,默认布局)或悬浮窗口(可拖拽浮动窗口,内嵌 AI 输入框),插件卡片里可随时切换。
|
|
6
|
+
|
|
7
|
+
## 界面一览
|
|
8
|
+
|
|
9
|
+
**右侧停靠布局(默认布局)**——对话与 code UI 左右并排,边聊边看代码:
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
**悬浮窗口布局**——可拖拽、可缩放的浮动编辑器窗口,内嵌 AI 输入框:
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
**两种切换按钮(插件卡片「切换按钮」可选)**——点击在 code UI 与 dsh UI(DSH 原生界面)之间切换,与布局任意组合:
|
|
18
|
+
|
|
19
|
+
| 会话头按钮(默认) | 悬浮模式条(可拖动) |
|
|
20
|
+
| :---: | :---: |
|
|
21
|
+
|  |  |
|
|
22
|
+
|
|
23
|
+
> 📍 **所有配置都在:设置 → 插件 → 插件列表 → dsh-code-ui 的配置卡片**——编辑器界面(右侧停靠 / 悬浮窗口 / 隐藏)、编辑器主题、切换按钮、启用开关、检查更新,切换即时生效并持久化保存。
|
|
6
24
|
|
|
7
25
|
## 功能
|
|
8
26
|
|
|
9
|
-
-
|
|
27
|
+
- **双布局可选**:右侧停靠(默认布局,与聊天并排) / 悬浮窗口(可拖拽缩放),插件卡片切换、即时生效
|
|
10
28
|
- **切换按钮可选**:会话头 `⌨ code UI` 按钮(默认) / 悬浮模式条,与布局任意组合
|
|
11
|
-
- **界面模式可持久化**:code UI / dsh 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
|
-
- **检查更新 / 一键更新**:设置 → 插件 →
|
|
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
|
|
55
|
+
安装完成后重启 DSH,编辑器**默认隐藏**(dsh UI,不自动占屏);会话标题栏出现 `⌨ code UI` 按钮,点击即可打开编辑器——默认布局为右侧停靠,想换悬浮窗口或悬浮模式条,到 **设置 → 插件 → 插件列表 → dsh-code-ui** 的配置卡片里切换。
|
|
38
56
|
|
|
39
57
|
或手动安装:
|
|
40
58
|
|
|
@@ -84,21 +102,14 @@ 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
|
| --- | --- | --- |
|
|
111
|
+
| 2026-09-02 | v1.3.1 | 调整:安装后编辑器**默认隐藏**(dsh UI,新装不自动占屏,⌨ / 模式条随时唤回),布局默认仍为右侧停靠;配置卡片「编辑器界面」三态说明与 README 措辞同步。新增:一键更新 / 本地包更新成功后按钮文案临时变为「需重启 DSH 生效」(置灰防误点);README 增加「界面一览」(双布局与两种切换按钮截图)并明确配置入口路径(设置 → 插件 → 插件列表 → dsh-code-ui) |
|
|
112
|
+
| 2026-09-02 | v1.3.0 | 新增:文件树右键操作(真实磁盘)。右键文件:打开 / 引用给 AI(整个文件)/ 复制路径 / 在资源管理器中显示 / 重命名 / 删除;右键文件夹:新建文件 / 新建文件夹 / 以此文件夹为根 / 刷新 / 复制路径 / 定位 / 重命名 / 删除(递归强确认);空白区右键:根目录新建 / 刷新 / 更换根;新建重命名输入弹窗(Enter 确认)、危险操作确认弹窗、工作区根禁删;重命名自动同步标签页/缓冲/watcher,删除自动清理该路径下全部标签。引用体系升级:整文件引用只传绝对路径由 AI 自行读取;停靠布局下 DSH 原生输入条发送也自动附加引用指针(挂钩 submit,两布局统一)。配置卡片:「布局模式 × 界面模式」合并为三态「编辑器界面」(右侧停靠/悬浮窗口/隐藏 dsh UI,隐藏时记住布局)。打开文件弹窗:输入路径回车直接认定(文件夹=设为根 / 文件=打开)。提示条移至顶部居中、可拖拽自定位置(⠿ 把手)。右键菜单视口边界自适应、点击菜单项后自动关闭;⟳ 刷新不再折叠整棵树 |
|
|
102
113
|
| 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);悬浮布局也获得侧栏收起/调宽能力 |
|
|
103
114
|
| 2026-09-02 | v1.2.0-beta.1 | 里程碑:右侧停靠模式(feature/native-layout 分支)--会话头「⌨ Code UI」开关按钮,Code UI 固定占右侧、DSH 界面推到左侧并排(Cursor 式布局);左缘拖拽实时调宽;文件树侧栏可收起(« 按钮 / 📁 三态 / 拖到最窄松手收起)+ » 展开把手 + 右缘拖拽调宽;侧栏标题不折行;移除停靠面板投影。推挤采用 [data-slot=root] 语义属性 + 内联 margin-right,不依赖哈希类名 |
|
|
104
115
|
| 2026-09-01 | v1.1.10 | 调整:底部状态栏不再用反色按钮样式,改用输入框同款背景(bg-overlay)+ 次要文字色 + 顶部分隔线,深浅色模式下均与编辑器融为一体 |
|
package/lib/client.js
CHANGED
|
@@ -123,13 +123,18 @@ window.__ModuleLoader__.load({
|
|
|
123
123
|
.cdex-btn-ghost{border:1px solid var(--dsw-alias-border-l1,#3e3e42);background:var(--dsw-alias-bg-layer-2,#3c3c3c);color:var(--dsw-alias-label-primary,#cccccc);border-radius:4px;padding:3px 8px;cursor:pointer;font-size:12px;}
|
|
124
124
|
.cdex-btn-ghost:hover{background:var(--dsw-alias-bg-layer-1,#4a4a4a);}
|
|
125
125
|
.cdex-menu-backdrop{position:fixed;inset:0;z-index:10001;pointer-events:auto;}
|
|
126
|
-
.cdex-menu{position:fixed;z-index:10002;background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l2,#454545);border-radius:6px;padding:4px;min-width:180px;box-shadow:0 8px 24px rgba(0,0,0,.5);}
|
|
126
|
+
.cdex-menu{position:fixed;z-index:10002;background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l2,#454545);border-radius:6px;padding:4px;min-width:180px;box-shadow:0 8px 24px rgba(0,0,0,.5);max-height:calc(100vh - 24px);overflow-y:auto;}
|
|
127
127
|
.cdex-menu-item{display:flex;gap:8px;align-items:center;padding:7px 10px;border-radius:4px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary,#d4d4d4);}
|
|
128
128
|
.cdex-menu-item:hover{background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);}
|
|
129
129
|
.cdex-modal{position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;z-index:10002;pointer-events:auto;}
|
|
130
130
|
.cdex-modal-box{background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l2,#454545);border-radius:8px;padding:16px;width:560px;max-width:92vw;}
|
|
131
131
|
.cdex-modal-title{font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-primary,#e8e8e8);word-break:break-all;}
|
|
132
132
|
.cdex-textarea2{width:100%;min-height:150px;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);border:1px solid var(--dsw-alias-border-l1,#3e3e42);border-radius:4px;padding:8px;font-family:inherit;font-size:13px;resize:vertical;box-sizing:border-box;}
|
|
133
|
+
.cdex-input{width:100%;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);border:1px solid var(--dsw-alias-border-l1,#3e3e42);border-radius:4px;padding:8px 10px;font-family:inherit;font-size:13px;box-sizing:border-box;outline:none;}
|
|
134
|
+
.cdex-input:focus{border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
|
|
135
|
+
.cdex-confirm-msg{color:var(--dsw-alias-label-primary,#d4d4d4);font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word;}
|
|
136
|
+
.cdex-btn-danger{background:var(--dsw-alias-state-error-primary,#f14c4c);color:#ffffff;border-color:transparent;}
|
|
137
|
+
.cdex-btn-danger:hover{opacity:.9;}
|
|
133
138
|
.cdex-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px;}
|
|
134
139
|
.cdex-ref{background:var(--dsw-alias-bg-layer-1,#2d2d2d);border:1px solid var(--dsw-alias-border-l1,#3e3e42);border-radius:6px;padding:8px;margin-bottom:8px;}
|
|
135
140
|
.cdex-ref-head{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:12px;}
|
|
@@ -137,7 +142,9 @@ window.__ModuleLoader__.load({
|
|
|
137
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;}
|
|
138
143
|
.cdex-section{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin:12px 0 6px;}
|
|
139
144
|
.cdex-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-top:12px;line-height:1.6;}
|
|
140
|
-
.cdex-toast{position:fixed;z-index:10003;
|
|
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;}
|
|
141
148
|
.cdex-resize{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:10;}
|
|
142
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);}
|
|
143
150
|
.cdex-aiinput-wrap{position:relative;padding:10px 14px 16px;background:transparent;border-top:1px solid var(--dsw-alias-border-l1,#3e3e42);}
|
|
@@ -252,8 +259,12 @@ window.__ModuleLoader__.load({
|
|
|
252
259
|
browsePath: null,
|
|
253
260
|
browseEntries: [],
|
|
254
261
|
ctxMenu: null,
|
|
262
|
+
nameDialog: null,
|
|
263
|
+
confirmBox: null,
|
|
255
264
|
noteEditor: null,
|
|
256
265
|
toast: null,
|
|
266
|
+
toastKeep: false,
|
|
267
|
+
toastPos: null,
|
|
257
268
|
error: null,
|
|
258
269
|
}
|
|
259
270
|
const listeners = new Set()
|
|
@@ -270,10 +281,55 @@ window.__ModuleLoader__.load({
|
|
|
270
281
|
|
|
271
282
|
function showToast(msg) {
|
|
272
283
|
store.toast = { msg }
|
|
284
|
+
store.toastKeep = false
|
|
273
285
|
emit()
|
|
286
|
+
scheduleToastHide(2800)
|
|
287
|
+
}
|
|
288
|
+
// 提示自动消失;正在被拖拽(toastKeep)时顺延,松手后短暂展示再消失
|
|
289
|
+
function scheduleToastHide(ms) {
|
|
274
290
|
if (timer && typeof timer.timeout === 'function') {
|
|
275
|
-
timer.timeout(() => {
|
|
291
|
+
timer.timeout(() => {
|
|
292
|
+
if (store.toastKeep) { scheduleToastHide(600); return }
|
|
293
|
+
store.toast = null; emit()
|
|
294
|
+
}, ms)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 提示条拖拽:按住移动自定义位置(默认顶部居中,拖过之后记住位置,防止挡内容)
|
|
298
|
+
function onToastPointerDown(e) {
|
|
299
|
+
e.preventDefault()
|
|
300
|
+
const el = e.currentTarget
|
|
301
|
+
const rect = el.getBoundingClientRect()
|
|
302
|
+
const base = store.toastPos || { x: rect.left, y: rect.top }
|
|
303
|
+
const px = e.clientX, py = e.clientY
|
|
304
|
+
store.toastKeep = true
|
|
305
|
+
try { el.setPointerCapture(e.pointerId) } catch (err) {}
|
|
306
|
+
function move(ev) {
|
|
307
|
+
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1200
|
|
308
|
+
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800
|
|
309
|
+
store.toastPos = {
|
|
310
|
+
x: Math.max(4, Math.min(base.x + (ev.clientX - px), vw - rect.width - 4)),
|
|
311
|
+
y: Math.max(4, Math.min(base.y + (ev.clientY - py), vh - rect.height - 4)),
|
|
312
|
+
}
|
|
313
|
+
emit()
|
|
314
|
+
}
|
|
315
|
+
function up() {
|
|
316
|
+
store.toastKeep = false
|
|
317
|
+
el.removeEventListener('pointermove', move)
|
|
318
|
+
el.removeEventListener('pointerup', up)
|
|
319
|
+
el.removeEventListener('pointercancel', up)
|
|
276
320
|
}
|
|
321
|
+
el.addEventListener('pointermove', move)
|
|
322
|
+
el.addEventListener('pointerup', up)
|
|
323
|
+
el.addEventListener('pointercancel', up)
|
|
324
|
+
}
|
|
325
|
+
function Toast() {
|
|
326
|
+
const st = useStore()
|
|
327
|
+
if (!st.toast) return null
|
|
328
|
+
const style = st.toastPos ? { left: st.toastPos.x, top: st.toastPos.y } : null
|
|
329
|
+
return h('div', { className: 'cdex-toast' + (st.toastPos ? ' placed' : ''), style: style, title: '按住可拖动位置(不再挡内容)', onPointerDown: onToastPointerDown },
|
|
330
|
+
h('span', { className: 'cdex-toast-grip' }, '⠿'),
|
|
331
|
+
st.toast.msg,
|
|
332
|
+
)
|
|
277
333
|
}
|
|
278
334
|
|
|
279
335
|
/* ============ 编辑器主题四态:dsh(默认,跟随 DSH 宿主外观) / system(跟随 OS) / light / dark ============ */
|
|
@@ -337,6 +393,7 @@ window.__ModuleLoader__.load({
|
|
|
337
393
|
if (r.lineStart && r.lineEnd) {
|
|
338
394
|
return name + '(' + r.lineStart + '-' + r.lineEnd + ')'
|
|
339
395
|
}
|
|
396
|
+
if (!r.lineStart && r.kind === 'cite') return name + '(整文件)'
|
|
340
397
|
return name
|
|
341
398
|
}
|
|
342
399
|
function parentPath(p) {
|
|
@@ -599,9 +656,8 @@ window.__ModuleLoader__.load({
|
|
|
599
656
|
emit()
|
|
600
657
|
await toggleExpand(store.rootPath)
|
|
601
658
|
} else {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
await toggleExpand(store.rootPath)
|
|
659
|
+
// 修复:刷新不再折叠整棵树,只重载根目录(子目录展开状态保留在 store.nodes)
|
|
660
|
+
await reloadDir(store.rootPath)
|
|
605
661
|
}
|
|
606
662
|
}
|
|
607
663
|
async function toggleExpand(path) {
|
|
@@ -719,7 +775,14 @@ window.__ModuleLoader__.load({
|
|
|
719
775
|
const text = full.slice(start, end)
|
|
720
776
|
const lineStart = full.slice(0, start).split('\n').length
|
|
721
777
|
const lineEnd = full.slice(0, end).split('\n').length
|
|
722
|
-
|
|
778
|
+
const sel = { text, lineStart, lineEnd, path: store.activePath, language: langOf(store.activePath) }
|
|
779
|
+
store.ctxMenu = { x: e.clientX, y: e.clientY, items: [
|
|
780
|
+
{ icon: '📋', label: '复制', act: () => { copyText(sel.text); showToast('已复制'); closeMenu() } },
|
|
781
|
+
{ icon: '📥', label: '粘贴到备注', act: () => { pasteToNote(sel); closeMenu() } },
|
|
782
|
+
{ icon: '📝', label: '添加备注', act: () => { openNoteEditor(sel); closeMenu() } },
|
|
783
|
+
{ icon: '🌐', label: '翻译', act: () => { doTranslate(sel); closeMenu() } },
|
|
784
|
+
{ icon: '🔗', label: '引用(给 AI)', act: () => { doCite(sel); closeMenu() } },
|
|
785
|
+
] }
|
|
723
786
|
emit()
|
|
724
787
|
}
|
|
725
788
|
function closeMenu() { store.ctxMenu = null; emit() }
|
|
@@ -737,6 +800,113 @@ window.__ModuleLoader__.load({
|
|
|
737
800
|
store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: '' }
|
|
738
801
|
emit()
|
|
739
802
|
}
|
|
803
|
+
// —— 文件树右键:磁盘操作(真实文件系统,经 host RPC) ——
|
|
804
|
+
function joinPath(dir, name) {
|
|
805
|
+
const sep = String(dir).includes('\\') ? '\\' : '/'
|
|
806
|
+
return String(dir).replace(/[\\/]+$/, '') + sep + name
|
|
807
|
+
}
|
|
808
|
+
// 重载单个目录节点(保持展开状态,不折叠整个树)
|
|
809
|
+
async function reloadDir(path) {
|
|
810
|
+
const n = store.nodes[path]
|
|
811
|
+
if (!n || !n.isDir) return
|
|
812
|
+
n.loaded = false
|
|
813
|
+
n.children = []
|
|
814
|
+
n.expanded = true
|
|
815
|
+
n.loading = true; emit()
|
|
816
|
+
const res = await callHost('list-dir', { path })
|
|
817
|
+
n.loading = false; n.loaded = true
|
|
818
|
+
if (res && res.error) { store.error = res.error; emit(); return }
|
|
819
|
+
const entries = (res && res.entries) || []
|
|
820
|
+
n.children = entries.map((e) => { store.nodes[e.path] = e; return e.path })
|
|
821
|
+
emit()
|
|
822
|
+
}
|
|
823
|
+
// 整文件引用:只给 AI 绝对路径,AI 用自己的读取工具打开
|
|
824
|
+
async function citeWholeFile(path) {
|
|
825
|
+
const ref = await callHost('add-reference', { path: path, language: langOf(path), lineStart: null, lineEnd: null, text: '', kind: 'cite' })
|
|
826
|
+
if (ref && ref.reference) { store.references.push(ref.reference); emit() }
|
|
827
|
+
showToast('已引用整个文件:' + baseName(path) + '(AI 将自行读取)')
|
|
828
|
+
}
|
|
829
|
+
// 重命名后同步标签页/缓冲/激活路径/watcher
|
|
830
|
+
function renameTabSync(fromPath, toPath) {
|
|
831
|
+
const affected = store.tabs.filter((t) => t.path === fromPath)
|
|
832
|
+
affected.forEach((t) => { t.path = toPath; t.name = baseName(toPath) })
|
|
833
|
+
if (toPath in store.buffers === false && fromPath in store.buffers) {
|
|
834
|
+
store.buffers[toPath] = store.buffers[fromPath]
|
|
835
|
+
store.savedContent[toPath] = store.savedContent[fromPath]
|
|
836
|
+
store.dirty[toPath] = store.dirty[fromPath]
|
|
837
|
+
store.externalChanged[toPath] = false
|
|
838
|
+
}
|
|
839
|
+
delete store.buffers[fromPath]; delete store.savedContent[fromPath]; delete store.dirty[fromPath]; delete store.externalChanged[fromPath]
|
|
840
|
+
if (store.activePath === fromPath) store.activePath = toPath
|
|
841
|
+
callHost('unwatch-file', { path: fromPath }).then(() => {})
|
|
842
|
+
if (affected.length > 0) callHost('watch-file', { path: toPath }).then(() => {})
|
|
843
|
+
}
|
|
844
|
+
// 删除后清理:关掉该路径(及其目录下)所有打开的标签
|
|
845
|
+
function closeTabsUnder(path) {
|
|
846
|
+
const sep = String(path).includes('\\') ? '\\' : '/'
|
|
847
|
+
const prefix = String(path).replace(/[\\/]+$/, '') + sep
|
|
848
|
+
store.tabs.filter((t) => t.path === path || t.path.indexOf(prefix) === 0)
|
|
849
|
+
.map((t) => t.path).forEach((p) => closeTab(p))
|
|
850
|
+
}
|
|
851
|
+
async function doRename(path, isDir) {
|
|
852
|
+
const parent = parentPath(path)
|
|
853
|
+
store.nameDialog = { mode: 'rename', path: path, dir: parent, isDir: !!isDir, curName: baseName(path) }
|
|
854
|
+
emit()
|
|
855
|
+
}
|
|
856
|
+
async function doDelete(path, isDir) {
|
|
857
|
+
store.confirmBox = {
|
|
858
|
+
msg: isDir
|
|
859
|
+
? '确定删除文件夹「' + baseName(path) + '」吗?\n其中所有内容(含子目录)都会被永久删除。'
|
|
860
|
+
: '确定删除文件「' + baseName(path) + '」吗?',
|
|
861
|
+
danger: true,
|
|
862
|
+
onOk: async () => {
|
|
863
|
+
const res = await callHost('fs-delete', { path: path, recursive: !!isDir })
|
|
864
|
+
if (res && res.error) { showToast('删除失败:' + res.error); return }
|
|
865
|
+
closeTabsUnder(path)
|
|
866
|
+
const parent = parentPath(path)
|
|
867
|
+
if (parent && store.nodes[parent]) await reloadDir(parent)
|
|
868
|
+
else await refresh()
|
|
869
|
+
showToast('已删除:' + baseName(path))
|
|
870
|
+
},
|
|
871
|
+
}
|
|
872
|
+
emit()
|
|
873
|
+
}
|
|
874
|
+
// 文件树节点右键菜单
|
|
875
|
+
function openNodeMenu(e, node) {
|
|
876
|
+
e.preventDefault(); e.stopPropagation()
|
|
877
|
+
const items = []
|
|
878
|
+
if (node.isDir) {
|
|
879
|
+
items.push({ icon: '📄', label: '新建文件', act: () => { store.nameDialog = { mode: 'newfile', dir: node.path }; emit() } })
|
|
880
|
+
items.push({ icon: '🗂', label: '新建文件夹', act: () => { store.nameDialog = { mode: 'newdir', dir: node.path }; emit() } })
|
|
881
|
+
items.push({ icon: '🏠', label: '以此文件夹为根', act: () => setWorkspaceRoot(node.path) })
|
|
882
|
+
items.push({ icon: '⟳', label: '刷新', act: () => reloadDir(node.path) })
|
|
883
|
+
} else {
|
|
884
|
+
items.push({ icon: '📄', label: '打开', act: () => openFile(node.path) })
|
|
885
|
+
items.push({ icon: '🔗', label: '引用给 AI(整个文件)', act: () => citeWholeFile(node.path) })
|
|
886
|
+
}
|
|
887
|
+
items.push({ icon: '📋', label: '复制路径', act: () => { copyText(node.path); showToast('已复制路径:' + node.path) } })
|
|
888
|
+
items.push({ icon: '📂', label: '在资源管理器中显示', act: () => callHost('fs-reveal', { path: node.path }).then((r) => { if (r && r.error) showToast('失败:' + r.error) }) })
|
|
889
|
+
items.push({ icon: '✏️', label: '重命名', act: () => doRename(node.path, node.isDir) })
|
|
890
|
+
const isRoot = node.path === store.rootPath
|
|
891
|
+
items.push({ icon: '🗑', label: isRoot ? '删除(工作区根)' : (node.isDir ? '删除文件夹…' : '删除'), act: () => {
|
|
892
|
+
if (isRoot) { showToast('工作区根目录不能在树里删除;请先更换根目录'); return }
|
|
893
|
+
doDelete(node.path, node.isDir)
|
|
894
|
+
} })
|
|
895
|
+
store.ctxMenu = { x: e.clientX, y: e.clientY, items: items }
|
|
896
|
+
emit()
|
|
897
|
+
}
|
|
898
|
+
// 文件树空白区右键
|
|
899
|
+
function openTreeBlankMenu(e) {
|
|
900
|
+
e.preventDefault()
|
|
901
|
+
if (!store.rootPath) return
|
|
902
|
+
store.ctxMenu = { x: e.clientX, y: e.clientY, items: [
|
|
903
|
+
{ icon: '📄', label: '新建文件(根目录)', act: () => { store.nameDialog = { mode: 'newfile', dir: store.rootPath }; emit() } },
|
|
904
|
+
{ icon: '🗂', label: '新建文件夹(根目录)', act: () => { store.nameDialog = { mode: 'newdir', dir: store.rootPath }; emit() } },
|
|
905
|
+
{ icon: '⟳', label: '刷新', act: () => refresh() },
|
|
906
|
+
{ icon: '🗃', label: '更换工作区根', act: () => openFilePicker() },
|
|
907
|
+
] }
|
|
908
|
+
emit()
|
|
909
|
+
}
|
|
740
910
|
async function pasteToNote(sel) {
|
|
741
911
|
let clip = ''
|
|
742
912
|
try {
|
|
@@ -762,16 +932,28 @@ window.__ModuleLoader__.load({
|
|
|
762
932
|
function buildSendPayload(draft) {
|
|
763
933
|
// 引用文本不再粘贴进消息(明面上不展示):引用统一存 host 端,AI 通过 get_referenced_snippets
|
|
764
934
|
// 工具读取;消息里只附一行「标签级」指针,让 AI 知道有引用可读。
|
|
935
|
+
// 整文件引用(无行号):直接给出绝对路径,AI 用自己的读取工具打开。
|
|
765
936
|
const text = draft || ''
|
|
937
|
+
if (text.indexOf('[code UI] 已标记') >= 0) return text // 幂等:已带指针不重复附加
|
|
766
938
|
const citeRefs = store.references.filter((r) => r.kind === 'cite')
|
|
767
939
|
const trRefs = store.references.filter((r) => r.kind === 'translate')
|
|
940
|
+
const codeRefs = citeRefs.filter((r) => r.lineStart)
|
|
941
|
+
const fileRefs = citeRefs.filter((r) => !r.lineStart)
|
|
768
942
|
const parts = []
|
|
769
|
-
if (
|
|
943
|
+
if (codeRefs.length > 0) parts.push(codeRefs.length + ' 段代码引用')
|
|
944
|
+
if (fileRefs.length > 0) parts.push(fileRefs.length + ' 个文件引用')
|
|
770
945
|
if (trRefs.length > 0) parts.push(trRefs.length + ' 条翻译请求')
|
|
771
946
|
if (parts.length === 0) return text
|
|
947
|
+
let ptr = '[code UI] 已标记 ' + parts.join('、') + '。'
|
|
948
|
+
if (fileRefs.length > 0) {
|
|
949
|
+
ptr += '\n文件引用(整个文件,请用你的文件读取工具打开以下绝对路径):\n'
|
|
950
|
+
+ fileRefs.map((r) => '- ' + r.path).join('\n')
|
|
951
|
+
}
|
|
952
|
+
if (codeRefs.length > 0 || trRefs.length > 0) {
|
|
953
|
+
ptr += '\n(代码/翻译引用内容未粘贴在消息里,请调用 get_referenced_snippets 工具读取)。'
|
|
954
|
+
}
|
|
772
955
|
const sep = text.trim() ? '\n\n' : ''
|
|
773
|
-
return text + sep +
|
|
774
|
-
+ '(引用内容未粘贴在消息里,请调用 get_referenced_snippets 工具读取)。'
|
|
956
|
+
return text + sep + ptr
|
|
775
957
|
}
|
|
776
958
|
|
|
777
959
|
/* ============ §6 Pointer 处理层(窗口拖拽 / 缩放 / 模式条移动) ============ */
|
|
@@ -842,6 +1024,7 @@ window.__ModuleLoader__.load({
|
|
|
842
1024
|
}
|
|
843
1025
|
|
|
844
1026
|
/* ============ §7 组件层(V.* 命名空间) ============ */
|
|
1027
|
+
let selfSending = false // AiInput 副本自己发送时置位,让挂钩的 submit 不重复附加指针
|
|
845
1028
|
function InputBridge(props) {
|
|
846
1029
|
const useInput = props.useInput
|
|
847
1030
|
const inputState = typeof useInput === 'function' ? useInput((s) => s) : undefined
|
|
@@ -854,6 +1037,27 @@ window.__ModuleLoader__.load({
|
|
|
854
1037
|
}
|
|
855
1038
|
emit()
|
|
856
1039
|
})
|
|
1040
|
+
// 挂钩 DSH 原生输入条的发送:存在引用时把指针附进草稿再发(整文件=绝对路径)。
|
|
1041
|
+
// actions 对象「每会话一个稳定身份」,补丁安全;卸载/换会话时还原原方法。
|
|
1042
|
+
React.useEffect(() => {
|
|
1043
|
+
if (!inputActions || typeof inputActions.submit !== 'function') return
|
|
1044
|
+
if (inputActions.__cdexPatched) return
|
|
1045
|
+
const orig = inputActions.submit
|
|
1046
|
+
inputActions.__cdexPatched = true
|
|
1047
|
+
inputActions.submit = function () {
|
|
1048
|
+
try {
|
|
1049
|
+
const hasRefs = store.references.length > 0
|
|
1050
|
+
const draft = store.inputDraft || ''
|
|
1051
|
+
if (hasRefs && !selfSending && draft.indexOf('[code UI]') < 0) {
|
|
1052
|
+
inputActions.setDraft(buildSendPayload(draft))
|
|
1053
|
+
}
|
|
1054
|
+
} catch (e) {}
|
|
1055
|
+
return orig.apply(inputActions, arguments)
|
|
1056
|
+
}
|
|
1057
|
+
return () => {
|
|
1058
|
+
try { inputActions.submit = orig; delete inputActions.__cdexPatched } catch (e) {}
|
|
1059
|
+
}
|
|
1060
|
+
}, [inputActions])
|
|
857
1061
|
return null
|
|
858
1062
|
}
|
|
859
1063
|
function AiInput() {
|
|
@@ -873,8 +1077,11 @@ window.__ModuleLoader__.load({
|
|
|
873
1077
|
const text = taEl.value
|
|
874
1078
|
if (!text.trim() && refs.length === 0) return
|
|
875
1079
|
const payload = buildSendPayload(text)
|
|
876
|
-
|
|
877
|
-
|
|
1080
|
+
selfSending = true
|
|
1081
|
+
try {
|
|
1082
|
+
st.inputActions.setDraft(payload)
|
|
1083
|
+
st.inputActions.submit()
|
|
1084
|
+
} finally { selfSending = false }
|
|
878
1085
|
taEl.value = ''
|
|
879
1086
|
}
|
|
880
1087
|
return h('div', { className: 'cdex-aiinput-wrap' },
|
|
@@ -933,7 +1140,8 @@ window.__ModuleLoader__.load({
|
|
|
933
1140
|
if (!p) return
|
|
934
1141
|
callHost('stat-path', { path: p }).then((res) => {
|
|
935
1142
|
if (!res || res.error) { showToast('无法访问该路径:' + ((res && res.error) || '未知错误')); return }
|
|
936
|
-
|
|
1143
|
+
// 手动输入 + 回车 = 直接认定:目录设为工作区根,文件直接打开
|
|
1144
|
+
if (res.isDir) { setWorkspaceRoot(res.path || p) }
|
|
937
1145
|
else if (res.isFile) { pickFile(res.path || p) }
|
|
938
1146
|
// 兼容回退:宿主 isDir 字段缺失但路径确实存在时,按目录尝试浏览(list-dir 对非目录会给出真实错误)
|
|
939
1147
|
else if (res.exists) { browseDir(p) }
|
|
@@ -955,7 +1163,7 @@ window.__ModuleLoader__.load({
|
|
|
955
1163
|
),
|
|
956
1164
|
h('div', { className: 'cdex-fp-bar' },
|
|
957
1165
|
h('button', { className: 'cdex-btn-ghost', onClick: goUp, title: '上级目录' }, '↑'),
|
|
958
|
-
h('input', { className: 'cdex-fp-input', defaultValue: curDir || '', placeholder: '
|
|
1166
|
+
h('input', { className: 'cdex-fp-input', defaultValue: curDir || '', placeholder: '输入路径后回车:文件夹=设为根,文件=打开', onKeyDown: goTo }),
|
|
959
1167
|
),
|
|
960
1168
|
h('div', { className: 'cdex-fp-list' },
|
|
961
1169
|
dirs.map((d) => h('div', { className: 'cdex-fp-item', key: d.path, onClick: () => browseDir(d.path) },
|
|
@@ -967,7 +1175,7 @@ window.__ModuleLoader__.load({
|
|
|
967
1175
|
dirs.length === 0 && files.length === 0 ? h('div', { className: 'cdex-empty' }, st.error ? String(st.error) : '空目录') : null,
|
|
968
1176
|
),
|
|
969
1177
|
h('div', { className: 'cdex-fp-actions' },
|
|
970
|
-
h('span', { className: 'cdex-fp-hint' }, '
|
|
1178
|
+
h('span', { className: 'cdex-fp-hint' }, '输入路径回车即认定:文件夹设为根、文件直接打开;点条目仅浏览,此时用右下按钮确认'),
|
|
971
1179
|
h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
|
|
972
1180
|
h('button', { className: 'cdex-btn', onClick: confirm, disabled: !curDir }, '以此文件夹为根'),
|
|
973
1181
|
),
|
|
@@ -1001,7 +1209,7 @@ window.__ModuleLoader__.load({
|
|
|
1001
1209
|
const cls = 'cdex-node' + (st.activePath === node.path ? ' active' : '')
|
|
1002
1210
|
const children = node.isDir && node.expanded ? (node.children || []).map((c) => h(TreeNode, { key: c, path: c, depth: props.depth + 1 })) : null
|
|
1003
1211
|
return h('div', null,
|
|
1004
|
-
h('div', { className: cls, style: style, onClick: () => { node.isDir ? toggleExpand(node.path) : openFile(node.path) } },
|
|
1212
|
+
h('div', { className: cls, style: style, onClick: () => { node.isDir ? toggleExpand(node.path) : openFile(node.path) }, onContextMenu: (e) => openNodeMenu(e, node) },
|
|
1005
1213
|
node.isDir ? h('span', { className: 'cdex-arrow' }, node.expanded ? '▾' : '▸') : h('span', { className: 'cdex-arrow' }, ''),
|
|
1006
1214
|
h('span', { className: node.isDir ? 'cdex-folder' : 'cdex-file' }, node.isDir ? '📁 ' : '📄 ', node.name),
|
|
1007
1215
|
),
|
|
@@ -1019,7 +1227,7 @@ window.__ModuleLoader__.load({
|
|
|
1019
1227
|
h('button', { className: 'cdex-btn-ghost', onClick: () => { store.sidebarCollapsed = true; emit() }, title: '收起侧栏' }, '«'),
|
|
1020
1228
|
),
|
|
1021
1229
|
),
|
|
1022
|
-
h('div', { className: 'cdex-tree' },
|
|
1230
|
+
h('div', { className: 'cdex-tree', onContextMenu: (e) => { if (!e.target.closest || !e.target.closest('.cdex-node')) openTreeBlankMenu(e) } },
|
|
1023
1231
|
st.error ? h('div', { className: 'cdex-error' }, String(st.error)) : null,
|
|
1024
1232
|
st.rootPath ? h(TreeNode, { path: st.rootPath, depth: 0 }) : h('div', { className: 'cdex-empty' }, '正在加载目录…'),
|
|
1025
1233
|
),
|
|
@@ -1028,10 +1236,10 @@ window.__ModuleLoader__.load({
|
|
|
1028
1236
|
function RefItem(r) {
|
|
1029
1237
|
return h('div', { className: 'cdex-ref', key: r.id },
|
|
1030
1238
|
h('div', { className: 'cdex-ref-head' },
|
|
1031
|
-
h('span', { className: 'cdex-ref-path' }, r.path, r.lineStart ? (' :' + r.lineStart + '-' + r.lineEnd) : ''),
|
|
1239
|
+
h('span', { className: 'cdex-ref-path' }, r.path, r.lineStart ? (' :' + r.lineStart + '-' + r.lineEnd) : (r.kind === 'cite' ? ' (整个文件)' : '')),
|
|
1032
1240
|
h('button', { className: 'cdex-btn-ghost', onClick: () => delReference(r.id) }, '✕'),
|
|
1033
1241
|
),
|
|
1034
|
-
h('div', { className: 'cdex-ref-code' }, r.text.slice(0, 500)),
|
|
1242
|
+
h('div', { className: 'cdex-ref-code' }, r.text ? r.text.slice(0, 500) : '(AI 将按绝对路径自行读取整个文件)'),
|
|
1035
1243
|
)
|
|
1036
1244
|
}
|
|
1037
1245
|
function ReferencesView() {
|
|
@@ -1158,22 +1366,90 @@ window.__ModuleLoader__.load({
|
|
|
1158
1366
|
const st = useStore()
|
|
1159
1367
|
const m = st.ctxMenu
|
|
1160
1368
|
if (!m) return null
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1369
|
+
const items = m.items || []
|
|
1370
|
+
// 视口边界自适应:菜单在点击点放不下时收进视口(底部/右侧溢出钳位),
|
|
1371
|
+
// 避免在最后一行右键时菜单被浏览器底边截断
|
|
1372
|
+
const ITEM_H = 33
|
|
1373
|
+
const estH = items.length * ITEM_H + 10
|
|
1374
|
+
const estW = 230
|
|
1375
|
+
let mx = m.x, my = m.y
|
|
1376
|
+
try {
|
|
1377
|
+
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1200
|
|
1378
|
+
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800
|
|
1379
|
+
if (my + estH > vh - 8) my = Math.max(8, vh - estH - 8)
|
|
1380
|
+
if (mx + estW > vw - 8) mx = Math.max(8, vw - estW - 8)
|
|
1381
|
+
} catch (e) {}
|
|
1169
1382
|
return h('div', { className: 'cdex-menu-backdrop', onClick: closeMenu },
|
|
1170
|
-
h('div', { className: 'cdex-menu', style: { left:
|
|
1171
|
-
items.map((it, i) => h('div', { key: i, className: 'cdex-menu-item', onClick: it.act },
|
|
1383
|
+
h('div', { className: 'cdex-menu', style: { left: mx, top: my }, onClick: (e) => e.stopPropagation() },
|
|
1384
|
+
items.map((it, i) => h('div', { key: i, className: 'cdex-menu-item', onClick: () => { try { it.act() } finally { closeMenu() } } },
|
|
1172
1385
|
h('span', null, it.icon), h('span', null, it.label),
|
|
1173
1386
|
)),
|
|
1174
1387
|
),
|
|
1175
1388
|
)
|
|
1176
1389
|
}
|
|
1390
|
+
let nameInputEl = null
|
|
1391
|
+
// 新建文件/文件夹、重命名 的输入弹窗(Enter 确认 / Esc 取消)
|
|
1392
|
+
function NameDialog() {
|
|
1393
|
+
const st = useStore()
|
|
1394
|
+
const d = st.nameDialog
|
|
1395
|
+
if (!d) return null
|
|
1396
|
+
const close = () => { st.nameDialog = null; nameInputEl = null; emit() }
|
|
1397
|
+
const titles = { newfile: '新建文件', newdir: '新建文件夹', rename: d.isDir ? '重命名文件夹' : '重命名文件' }
|
|
1398
|
+
const confirm = async () => {
|
|
1399
|
+
const name = (nameInputEl && nameInputEl.value || '').trim()
|
|
1400
|
+
if (!name) { showToast('请输入名称'); return }
|
|
1401
|
+
if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) { showToast('名称不能包含路径分隔符'); return }
|
|
1402
|
+
if (d.mode === 'rename') {
|
|
1403
|
+
const to = joinPath(d.dir, name)
|
|
1404
|
+
const res = await callHost('fs-rename', { from: d.path, to: to })
|
|
1405
|
+
if (res && res.error) { showToast('重命名失败:' + res.error); close(); return }
|
|
1406
|
+
renameTabSync(d.path, to)
|
|
1407
|
+
if (store.nodes[d.dir]) await reloadDir(d.dir)
|
|
1408
|
+
else await refresh()
|
|
1409
|
+
showToast('已重命名为:' + name)
|
|
1410
|
+
} else {
|
|
1411
|
+
const full = joinPath(d.dir, name)
|
|
1412
|
+
const res = await callHost(d.mode === 'newfile' ? 'fs-createfile' : 'fs-mkdir', { path: full })
|
|
1413
|
+
if (res && res.error) { showToast('创建失败:' + res.error); close(); return }
|
|
1414
|
+
if (store.nodes[d.dir]) await reloadDir(d.dir)
|
|
1415
|
+
else await refresh()
|
|
1416
|
+
if (d.mode === 'newfile') await openFile(full)
|
|
1417
|
+
showToast('已创建:' + name)
|
|
1418
|
+
}
|
|
1419
|
+
close()
|
|
1420
|
+
}
|
|
1421
|
+
React.useEffect(() => {
|
|
1422
|
+
if (nameInputEl) { try { nameInputEl.focus(); nameInputEl.select() } catch (e) {} }
|
|
1423
|
+
}, [])
|
|
1424
|
+
return h('div', { className: 'cdex-modal', onClick: close },
|
|
1425
|
+
h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
|
|
1426
|
+
h('div', { className: 'cdex-modal-title' }, titles[d.mode] || '输入名称', h('span', { style: { color: '#9d9d9d', fontWeight: 400 } }, ' — ', d.mode === 'rename' ? d.dir : d.dir || '')),
|
|
1427
|
+
h('input', { className: 'cdex-input', ref: (el) => { nameInputEl = el }, defaultValue: d.curName || '', placeholder: d.mode === 'rename' ? '输入新名称' : (d.mode === 'newfile' ? '例如 utils.js' : '例如 components'), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); confirm() } if (e.key === 'Escape') { e.preventDefault(); close() } } }),
|
|
1428
|
+
h('div', { className: 'cdex-modal-actions' },
|
|
1429
|
+
h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
|
|
1430
|
+
h('button', { className: 'cdex-btn', onClick: confirm }, d.mode === 'rename' ? '重命名' : '创建'),
|
|
1431
|
+
),
|
|
1432
|
+
),
|
|
1433
|
+
)
|
|
1434
|
+
}
|
|
1435
|
+
// 危险操作确认弹窗(删除等)
|
|
1436
|
+
function ConfirmBox() {
|
|
1437
|
+
const st = useStore()
|
|
1438
|
+
const d = st.confirmBox
|
|
1439
|
+
if (!d) return null
|
|
1440
|
+
const close = () => { st.confirmBox = null; emit() }
|
|
1441
|
+
const ok = async () => { const fn = d.onOk; close(); if (typeof fn === 'function') { try { await fn() } catch (e) {} } }
|
|
1442
|
+
return h('div', { className: 'cdex-modal', onClick: close },
|
|
1443
|
+
h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
|
|
1444
|
+
h('div', { className: 'cdex-modal-title' }, '⚠ 确认操作'),
|
|
1445
|
+
h('div', { className: 'cdex-confirm-msg' }, d.msg),
|
|
1446
|
+
h('div', { className: 'cdex-modal-actions' },
|
|
1447
|
+
h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
|
|
1448
|
+
h('button', { className: 'cdex-btn cdex-btn-danger', onClick: ok }, '确定'),
|
|
1449
|
+
),
|
|
1450
|
+
),
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1177
1453
|
let noteTa = null
|
|
1178
1454
|
function NoteEditor() {
|
|
1179
1455
|
const st = useStore()
|
|
@@ -1243,7 +1519,9 @@ window.__ModuleLoader__.load({
|
|
|
1243
1519
|
st.filePickerOpen ? h(FilePicker) : null,
|
|
1244
1520
|
st.ctxMenu ? h(ContextMenu) : null,
|
|
1245
1521
|
st.noteEditor ? h(NoteEditor) : null,
|
|
1246
|
-
st.
|
|
1522
|
+
st.nameDialog ? h(NameDialog) : null,
|
|
1523
|
+
st.confirmBox ? h(ConfirmBox) : null,
|
|
1524
|
+
st.toast ? h(Toast) : null,
|
|
1247
1525
|
)
|
|
1248
1526
|
}
|
|
1249
1527
|
|
|
@@ -1355,7 +1633,9 @@ window.__ModuleLoader__.load({
|
|
|
1355
1633
|
st.filePickerOpen ? h(FilePicker) : null,
|
|
1356
1634
|
st.ctxMenu ? h(ContextMenu) : null,
|
|
1357
1635
|
st.noteEditor ? h(NoteEditor) : null,
|
|
1358
|
-
st.
|
|
1636
|
+
st.nameDialog ? h(NameDialog) : null,
|
|
1637
|
+
st.confirmBox ? h(ConfirmBox) : null,
|
|
1638
|
+
st.toast ? h(Toast) : null,
|
|
1359
1639
|
)
|
|
1360
1640
|
}
|
|
1361
1641
|
|
|
@@ -1464,8 +1744,7 @@ window.__ModuleLoader__.load({
|
|
|
1464
1744
|
const [localPath, setLocalPath] = React.useState('')
|
|
1465
1745
|
const [enabling, setEnabling] = React.useState(false)
|
|
1466
1746
|
const [theming, setTheming] = React.useState(false)
|
|
1467
|
-
const [
|
|
1468
|
-
const [uiModeing, setUiModeing] = React.useState(false)
|
|
1747
|
+
const [uishing, setUishing] = React.useState(false)
|
|
1469
1748
|
const [toggling, setToggling] = React.useState(false)
|
|
1470
1749
|
const lastPayloadRef = React.useRef(null)
|
|
1471
1750
|
const pollDisposeRef = React.useRef(null)
|
|
@@ -1486,36 +1765,46 @@ window.__ModuleLoader__.load({
|
|
|
1486
1765
|
})
|
|
1487
1766
|
}
|
|
1488
1767
|
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1768
|
+
// 编辑器界面(三态,合并原「布局模式 × 界面模式」):
|
|
1769
|
+
// dock=右侧停靠 / float=悬浮窗口 / hidden=隐藏(dsh UI,内部记住当前布局,切回原样恢复)
|
|
1770
|
+
function setUiShape(v) {
|
|
1771
|
+
if (!self || uishing) return
|
|
1772
|
+
const curLayout = (self.layout === 'float') ? 'float' : 'dock'
|
|
1773
|
+
const curHidden = self.uiMode === 'native'
|
|
1774
|
+
if (v === 'hidden') {
|
|
1775
|
+
if (curHidden) return
|
|
1776
|
+
setUishing(true)
|
|
1777
|
+
callHost('set-uimode', { uiMode: 'native' }).then((r) => {
|
|
1778
|
+
setUishing(false)
|
|
1779
|
+
if (r && !r.error) {
|
|
1780
|
+
setSelf(Object.assign({}, self, { uiMode: 'native' }))
|
|
1781
|
+
applyUiMode('native')
|
|
1782
|
+
showToast('编辑器已隐藏(dsh UI)')
|
|
1783
|
+
} else { showToast('切换失败:' + ((r && r.error) || '未知错误')) }
|
|
1784
|
+
})
|
|
1785
|
+
return
|
|
1786
|
+
}
|
|
1787
|
+
const needLayout = curLayout !== v
|
|
1788
|
+
const needShow = curHidden
|
|
1789
|
+
if (!needLayout && !needShow) return
|
|
1790
|
+
setUishing(true)
|
|
1791
|
+
const next = Object.assign({}, self, { layout: v, uiMode: 'codeui' })
|
|
1792
|
+
const step = () => {
|
|
1793
|
+
callHost('set-uimode', { uiMode: 'codeui' }).then((r2) => {
|
|
1794
|
+
setUishing(false)
|
|
1795
|
+
if (r2 && !r2.error) {
|
|
1796
|
+
setSelf(next)
|
|
1797
|
+
applyUiMode('codeui')
|
|
1798
|
+
showToast('编辑器界面:' + (v === 'dock' ? '右侧停靠' : '悬浮窗口'))
|
|
1799
|
+
} else { showToast('切换失败:' + ((r2 && r2.error) || '未知错误')) }
|
|
1800
|
+
})
|
|
1801
|
+
}
|
|
1802
|
+
if (needLayout) {
|
|
1803
|
+
callHost('set-layout', { layout: v }).then((r1) => {
|
|
1804
|
+
if (r1 && !r1.error) { applyLayout(v); step() }
|
|
1805
|
+
else { setUishing(false); showToast('切换失败:' + ((r1 && r1.error) || '未知错误')) }
|
|
1806
|
+
})
|
|
1807
|
+
} else { step() }
|
|
1519
1808
|
}
|
|
1520
1809
|
|
|
1521
1810
|
// 切换按钮样式:写 host settings 持久化;成功后立即换一种切换按钮
|
|
@@ -1597,6 +1886,8 @@ window.__ModuleLoader__.load({
|
|
|
1597
1886
|
|
|
1598
1887
|
const cur = (self && self.version) || (chk && chk.current) || '?'
|
|
1599
1888
|
const started = !!(run && !run.done)
|
|
1889
|
+
// 更新已成功、等待重启:更新类按钮文案临时改为「需重启 DSH 生效」,用户一眼可见
|
|
1890
|
+
const updated = !!(run && run.done && run.ok)
|
|
1600
1891
|
let checkLine = null
|
|
1601
1892
|
if (chk && chk.checking) checkLine = h('p', { className: 'cdx-pc-note' }, '检查中…')
|
|
1602
1893
|
else if (chk && chk.error) checkLine = h('p', { className: 'cdx-pc-err' }, chk.error)
|
|
@@ -1652,37 +1943,25 @@ window.__ModuleLoader__.load({
|
|
|
1652
1943
|
),
|
|
1653
1944
|
h('p', { className: 'cdx-pc-note' }, '「跟随 DSH」随 DSH 整体外观(默认);「跟随系统」随操作系统浅色/深色实时切换;浅色/深色为强制指定。切换立即生效,仅作用于编辑器界面,选择持久化保存。'),
|
|
1654
1945
|
),
|
|
1655
|
-
//
|
|
1946
|
+
// 字段:编辑器界面(三态,合并原「布局模式 × 界面模式」)
|
|
1656
1947
|
h('div', { className: 'cdx-pc-field' },
|
|
1657
1948
|
h('div', { className: 'cdx-pc-head' },
|
|
1658
|
-
h('span', { className: 'cdx-pc-label' }, '
|
|
1949
|
+
h('span', { className: 'cdx-pc-label' }, '编辑器界面'),
|
|
1659
1950
|
h('span', { className: 'cdx-pc-head' },
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
// 字段:界面模式
|
|
1672
|
-
h('div', { className: 'cdx-pc-field' },
|
|
1673
|
-
h('div', { className: 'cdx-pc-head' },
|
|
1674
|
-
h('span', { className: 'cdx-pc-label' }, '界面模式'),
|
|
1675
|
-
h('span', { className: 'cdx-pc-head' },
|
|
1676
|
-
['codeui', 'native'].map((m) => h('button', {
|
|
1677
|
-
key: m,
|
|
1678
|
-
type: 'button',
|
|
1679
|
-
className: 'cdx-pc-btn ' + (((self && self.uiMode) || 'codeui') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
|
|
1680
|
-
disabled: !self || uiModeing,
|
|
1681
|
-
onClick: () => setUiMode(m),
|
|
1682
|
-
}, m === 'codeui' ? 'code UI' : 'dsh UI')),
|
|
1951
|
+
(function () {
|
|
1952
|
+
const curLayout = ((self && self.layout) === 'float') ? 'float' : 'dock'
|
|
1953
|
+
const cur = (self && self.uiMode) === 'native' ? 'hidden' : curLayout
|
|
1954
|
+
return ['dock', 'float', 'hidden'].map((m) => h('button', {
|
|
1955
|
+
key: m,
|
|
1956
|
+
type: 'button',
|
|
1957
|
+
className: 'cdx-pc-btn ' + (cur === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
|
|
1958
|
+
disabled: !self || uishing,
|
|
1959
|
+
onClick: () => setUiShape(m),
|
|
1960
|
+
}, m === 'dock' ? '右侧停靠' : m === 'float' ? '悬浮窗口' : '隐藏(dsh UI)'))
|
|
1961
|
+
})(),
|
|
1683
1962
|
),
|
|
1684
1963
|
),
|
|
1685
|
-
h('p', { className: 'cdx-pc-note' }, '
|
|
1964
|
+
h('p', { className: 'cdx-pc-note' }, '「隐藏(dsh UI)」为默认:新装不自动占屏,⌨ / 模式条 / ✕ 随时唤回,隐藏期间记住当前布局、切回原样恢复;「右侧停靠」code UI 固定占屏幕右侧、与聊天并排(默认布局);「悬浮窗口」为可拖拽的浮动编辑器窗口、含自带输入框。切换即时生效,持久化保存。'),
|
|
1686
1965
|
),
|
|
1687
1966
|
// 字段:切换按钮
|
|
1688
1967
|
h('div', { className: 'cdx-pc-field' },
|
|
@@ -1712,14 +1991,14 @@ window.__ModuleLoader__.load({
|
|
|
1712
1991
|
chk && chk.updateAvailable ? h('div', { className: 'cdx-pc-field' },
|
|
1713
1992
|
h('div', { className: 'cdx-pc-head' },
|
|
1714
1993
|
h('span', { className: 'cdx-pc-label' }, h('span', { className: 'cdx-pc-new' }, '🆕 有新版本:v' + chk.current + ' -> v' + chk.latest)),
|
|
1715
|
-
h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', disabled: started, onClick: () => startUpdate({ source: 'registry', version: chk.latest }) }, '一键更新到 v' + chk.latest),
|
|
1994
|
+
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),
|
|
1716
1995
|
),
|
|
1717
1996
|
) : null,
|
|
1718
1997
|
// 字段三:本地包更新
|
|
1719
1998
|
h('div', { className: 'cdx-pc-field' },
|
|
1720
1999
|
h('div', { className: 'cdx-pc-head' },
|
|
1721
2000
|
h('span', { className: 'cdx-pc-label' }, '本地包更新(开发期 / 未发布时)'),
|
|
1722
|
-
h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: started || !localPath.trim(), onClick: () => startUpdate({ source: 'local', path: localPath.trim() }) }, '安装本地包'),
|
|
2001
|
+
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 生效' : '安装本地包'),
|
|
1723
2002
|
),
|
|
1724
2003
|
h('div', { className: 'cdx-pc-head' },
|
|
1725
2004
|
h('input', {
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
|
7
7
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
8
8
|
import z from "@deepseek-ai/schemastery";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { existsSync, readFileSync, watch } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname, isAbsolute, join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
@@ -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
|
-
// 界面模式:
|
|
104
|
-
uiMode: z.string().default("
|
|
103
|
+
// 界面模式:native(隐藏编辑器,默认——新装不自动占屏,由 ⌨ 按钮唤出) / codeui(显示编辑器)
|
|
104
|
+
uiMode: z.string().default("native"),
|
|
105
105
|
// 切换按钮样式:header(会话头 ⌨ 按钮,默认) / bar(悬浮模式条)
|
|
106
106
|
toggle: z.string().default("header"),
|
|
107
107
|
});
|
|
108
108
|
// 解析后的当前值:settings 服务未挂载/未写入时保持默认
|
|
109
|
-
const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "
|
|
109
|
+
const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "native", toggle: "header" };
|
|
110
110
|
// settings 服务引用(「启用/禁用」开关写入用;由 installSelfSettings 捕获)
|
|
111
111
|
let settingsServiceRef = null;
|
|
112
112
|
function installSelfSettings(ctx) {
|
|
@@ -473,6 +473,74 @@ function rpcHandler(ctx) {
|
|
|
473
473
|
return ok({ events: events });
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
// —— 文件树磁盘操作(真实文件系统,右键菜单用) ——
|
|
477
|
+
if (endpoint === "fs-mkdir") {
|
|
478
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
479
|
+
try {
|
|
480
|
+
mkdirSync(args.path, { recursive: true });
|
|
481
|
+
return ok({ ok: true, path: args.path });
|
|
482
|
+
} catch (err) {
|
|
483
|
+
return ok({ error: "创建目录失败:" + ((err && err.message) || String(err)) });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (endpoint === "fs-createfile") {
|
|
488
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
489
|
+
if (existsSync(args.path)) return ok({ error: "已存在同名文件/目录" });
|
|
490
|
+
try {
|
|
491
|
+
writeSuppress.set(args.path, Date.now() + 1500);
|
|
492
|
+
writeFileSync(args.path, "", "utf8");
|
|
493
|
+
return ok({ ok: true, path: args.path });
|
|
494
|
+
} catch (err) {
|
|
495
|
+
return ok({ error: "创建文件失败:" + ((err && err.message) || String(err)) });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (endpoint === "fs-rename") {
|
|
500
|
+
if (!args.from || !args.to) return ok({ error: "需要 from 与 to 路径" });
|
|
501
|
+
if (!existsSync(args.from)) return ok({ error: "源路径不存在" });
|
|
502
|
+
if (existsSync(args.to)) return ok({ error: "目标已存在同名文件/目录" });
|
|
503
|
+
try {
|
|
504
|
+
// 若重命名的正是被监听的文件:停掉旧 watcher(客户端重开时会按新路径重新 watch)
|
|
505
|
+
unwatchFilePath(args.from);
|
|
506
|
+
writeSuppress.set(args.from, Date.now() + 1500);
|
|
507
|
+
renameSync(args.from, args.to);
|
|
508
|
+
return ok({ ok: true, from: args.from, to: args.to });
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return ok({ error: "重命名失败:" + ((err && err.message) || String(err)) });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (endpoint === "fs-delete") {
|
|
515
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
516
|
+
if (!existsSync(args.path)) return ok({ error: "路径不存在" });
|
|
517
|
+
try {
|
|
518
|
+
// 被监听的文件被删除:停 watcher + 抑制回声事件
|
|
519
|
+
if (fileWatchers.has(args.path)) unwatchFilePath(args.path);
|
|
520
|
+
writeSuppress.set(args.path, Date.now() + 1500);
|
|
521
|
+
rmSync(args.path, { recursive: !!args.recursive, force: false });
|
|
522
|
+
return ok({ ok: true });
|
|
523
|
+
} catch (err) {
|
|
524
|
+
return ok({ error: "删除失败:" + ((err && err.message) || String(err)) });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (endpoint === "fs-reveal") {
|
|
529
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
530
|
+
try {
|
|
531
|
+
if (process.platform === "win32") {
|
|
532
|
+
spawn("explorer", ["/select," + args.path], { detached: true, stdio: "ignore" }).unref();
|
|
533
|
+
} else if (process.platform === "darwin") {
|
|
534
|
+
spawn("open", ["-R", args.path], { detached: true, stdio: "ignore" }).unref();
|
|
535
|
+
} else {
|
|
536
|
+
spawn("xdg-open", [dirname(args.path)], { detached: true, stdio: "ignore" }).unref();
|
|
537
|
+
}
|
|
538
|
+
return ok({ ok: true });
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return ok({ error: "打开文件管理器失败:" + ((err && err.message) || String(err)) });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
476
544
|
if (endpoint === "add-reference") {
|
|
477
545
|
const id = "r" + (idSeq++);
|
|
478
546
|
const ref = {
|
|
@@ -691,7 +759,7 @@ function registerSnippetsTool(tools) {
|
|
|
691
759
|
defineTool({
|
|
692
760
|
name: "get_referenced_snippets",
|
|
693
761
|
description:
|
|
694
|
-
"读取用户在代码编辑器中通过「引用/翻译」标记的代码片段(含文件路径、行号、语言与内容),供 AI
|
|
762
|
+
"读取用户在代码编辑器中通过「引用/翻译」标记的代码片段(含文件路径、行号、语言与内容),供 AI 直接使用。文件树右键「引用给 AI」标记的整文件引用只含绝对路径,请用你自己的文件读取工具打开。当用户在对话框提到「我引用的代码」「翻译我引用的内容」时调用。",
|
|
695
763
|
parameters: {
|
|
696
764
|
kind: { type: "string", description: "筛选类型:cite(代码引用) / translate(翻译请求) / all(全部,默认)。" },
|
|
697
765
|
},
|
|
@@ -702,10 +770,14 @@ function registerSnippetsTool(tools) {
|
|
|
702
770
|
const refs = Array.isArray(v.references) ? v.references : [];
|
|
703
771
|
if (refs.length === 0) return [{ type: "text", text: "当前没有已标记的代码片段。" }];
|
|
704
772
|
const parts = refs.map((r) => {
|
|
705
|
-
const
|
|
706
|
-
const
|
|
773
|
+
const whole = !r.lineStart;
|
|
774
|
+
const label = r.kind === "translate" ? "翻译请求" : whole ? "文件引用" : "代码引用";
|
|
775
|
+
const loc = (r.path || "") + (whole ? "(整个文件)" : ":" + r.lineStart + "-" + r.lineEnd);
|
|
707
776
|
const lang = r.language ? " [" + r.language + "]" : "";
|
|
708
|
-
|
|
777
|
+
const body = whole
|
|
778
|
+
? "整个文件的引用:请用你的文件读取工具打开上述绝对路径自行读取。"
|
|
779
|
+
: (r.text || "");
|
|
780
|
+
return "【" + label + "】" + loc + lang + "\n" + body;
|
|
709
781
|
});
|
|
710
782
|
return [{ type: "text", text: "共 " + refs.length + " 条标记:\n\n" + parts.join("\n\n") }];
|
|
711
783
|
},
|