dsh-code-ui 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +120 -109
  2. package/lib/client.js +1780 -1335
  3. package/lib/index.js +746 -618
  4. package/package.json +81 -77
  5. package/README.en.md +0 -38
package/lib/client.js CHANGED
@@ -1,1335 +1,1780 @@
1
- // Browser half of dsh-code-ui — standard bundle plugin.
2
- // Hand-written JS, no build. Registered via __ModuleLoader__; host RPC goes
3
- // through the shared Connection service channel "/dsh-code-ui".
4
- window.__ModuleLoader__.load({
5
- id: "dsh-code-ui",
6
- factory: (require) => {
7
- var module = { exports: {} };
8
- var exports = module.exports;
9
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
10
-
11
- const React = require("react");
12
- // 宿主种子模块(与 react 同级,所有 client bundle 可 require):用和其他卡片同款的 chevron 图标
13
- let IconChevronDown = null;
14
- try {
15
- const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
16
- if (primitives && primitives.IconChevronDownOutline14) IconChevronDown = primitives.IconChevronDownOutline14;
17
- } catch (e) {}
18
- const CHANNEL = "/dsh-code-ui";
19
-
20
- function __apply(ctx) {
21
- // --- begin original apply body ---
22
-
23
- /* ============ §1 依赖与服务 ============ */
24
- const { rpc } = (ctx.get('connection') || {});
25
- // connection.rpc.call 返回 DSH 的 RpcResult 封套({ ok: true, value } / { ok: false, error }),
26
- // 协议失败不会 reject(只有传输层 HTTP 失败才 throw)。这里统一解包成「裸值」,
27
- // 协议错误折叠为 { error: message },让下方各调用点沿用自己的 res.error 判断
28
- // (对齐 dsh-updater 的 res.ok / res.value 消费约定)。
29
- // 修复:迁移期版本直接读 res.entries / res.root 等字段,在封套下恒为 undefined,
30
- // 导致 get-root / 文件树 / 路径跳转(stat-path)全部静默失效。
31
- function callHost(endpoint, payload) {
32
- return rpc.call(CHANNEL, endpoint, payload === undefined ? null : payload)
33
- .then((res) => {
34
- if (!res || typeof res !== 'object') return { error: 'RPC 返回异常' }
35
- if (res.ok === false) {
36
- const e = res.error || {}
37
- return { error: e.message || e.code || 'RPC 调用失败' }
38
- }
39
- const v = res.value
40
- if (v && typeof v === 'object' && v.error) return v
41
- return v == null ? {} : v
42
- })
43
- .catch((err) => ({ error: (err && err.message) ? err.message : String(err) }))
44
- }
45
- const slots = ctx.get('slots');
46
- if (slots === undefined) return
47
- const timer = ctx.get('timer')
48
- const h = React.createElement
49
- const vw = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1200
50
- const vh = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800
51
-
52
- /* ============ §2 样式注入 ============ */
53
- ctx.on('theme/change', () => { emit() })
54
- ctx.effect(() => { const tag = document.createElement('style'); tag.setAttribute('data-plugin', 'dsh-code-ui'); tag.textContent = `
55
- .cdex-modebar{position:fixed;z-index:10005;display:flex;gap:2px;background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l2,#454545);border-radius:20px;padding:3px;box-shadow:0 6px 20px rgba(0,0,0,.45);pointer-events:auto;user-select:none;}
56
- .cdex-mode-grip{display:flex;align-items:center;padding:0 4px 0 8px;color:var(--dsw-alias-label-secondary,#6f6f6f);cursor:grab;font-size:14px;}
57
- .cdex-mode-grip:active{cursor:grabbing;}
58
- .cdex-mode-tab{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#9d9d9d);padding:6px 16px;border-radius:16px;cursor:pointer;font-size:12.5px;font-weight:500;white-space:nowrap;}
59
- .cdex-mode-tab.active{background:var(--dsw-alias-brand-primary,#0e639c);color:#ffffff;}
60
- .cdex-mode-tab:hover:not(.active){color:var(--dsw-alias-label-primary,#ffffff);}
61
- .cdex-workbench{position:fixed;z-index:10000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);font-family:-apple-system,'Segoe UI',system-ui,sans-serif;font-size:13px;pointer-events:auto;overflow:hidden;}
62
- .cdex-top{display:flex;align-items:center;height:34px;background:var(--dsw-alias-bg-layer-2,#323233);padding:0 10px;gap:8px;user-select:none;cursor:move;}
63
- .cdex-top-title{font-weight:600;color:var(--dsw-alias-label-primary,#cccccc);}
64
- .cdex-top-spacer{flex:1;}
65
- .cdex-top-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;}
66
- .cdex-main{display:flex;flex:1;min-height:0;}
67
- .cdex-activitybar{width:48px;background:var(--dsw-alias-bg-layer-1,#1b1b1f);display:flex;flex-direction:column;align-items:center;padding:6px 0;}
68
- .cdex-abar-btn{width:40px;height:40px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--dsw-alias-label-secondary,#9d9d9d);font-size:20px;border-radius:6px;cursor:pointer;margin:2px 0;position:relative;}
69
- .cdex-abar-btn:hover{color:var(--dsw-alias-label-primary,#ffffff);}
70
- .cdex-abar-btn.active{color:var(--dsw-alias-label-primary,#ffffff);}
71
- .cdex-abar-btn.active::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:2px;background:var(--dsw-alias-brand-primary,#6b8afd);border-radius:1px;}
72
- .cdex-abar-spacer{flex:1;}
73
- .cdex-sidebar{width:264px;background:var(--dsw-specific-sidebar-fill,#252526);display:flex;flex-direction:column;min-height:0;}
74
- .cdex-sidebar-head{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#bbbbbb);}
75
- .cdex-tree{flex:1;overflow:auto;padding:4px;}
76
- .cdex-node{display:flex;align-items:center;gap:5px;padding:2px 6px;border-radius:4px;cursor:pointer;white-space:nowrap;color:var(--dsw-alias-label-primary,#cccccc);font-size:13px;}
77
- .cdex-node:hover{background:var(--dsw-alias-bg-layer-1,#2a2d2e);}
78
- .cdex-node.active{background:var(--dsw-alias-bg-layer-2,#37373d);}
79
- .cdex-arrow{width:14px;flex:0 0 auto;color:var(--dsw-alias-label-secondary,#888888);text-align:center;font-size:11px;}
80
- .cdex-folder{color:#dcb67a;}
81
- .cdex-file{color:var(--dsw-alias-label-primary,#cccccc);}
82
- .cdex-editor{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--dsw-alias-bg-base,#1e1e1e);}
83
- .cdex-tabs{display:flex;align-items:center;height:36px;background:var(--dsw-specific-sidebar-fill,#252526);overflow-x:auto;overflow-y:hidden;}
84
- .cdex-tab{display:flex;align-items:center;gap:6px;padding:0 10px;height:36px;border-right:1px solid var(--dsw-alias-border-l1,#1e1e1e);cursor:pointer;color:var(--dsw-alias-label-secondary,#969696);font-size:13px;white-space:nowrap;background:var(--dsw-alias-bg-layer-1,#2d2d2d);}
85
- .cdex-tab.active{background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#ffffff);border-top:1px solid var(--dsw-alias-brand-primary,#6b8afd);}
86
- .cdex-tab-close{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#969696);cursor:pointer;font-size:12px;padding:0;}
87
- .cdex-tab-close:hover{color:var(--dsw-alias-label-primary,#ffffff);}
88
- .cdex-tab-actions{display:flex;align-items:center;gap:6px;padding:0 8px;}
89
- .cdex-codearea{flex:1;display:flex;min-height:0;background:var(--dsw-alias-bg-base,#1e1e1e);}
90
- .cdex-gutter{width:56px;flex:0 0 auto;overflow:hidden;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-secondary,#858585);text-align:right;padding:8px 0;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;user-select:none;}
91
- .cdex-textarea{flex:1;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);border:none;outline:none;resize:none;padding:8px 14px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;overflow:auto;}
92
- /* —— 内联语法高亮 overlay —— */
93
- .cdex-code-layer{position:relative;flex:1;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#1e1e1e);}
94
- .cdex-code-pre{position:absolute;inset:0;margin:0;padding:8px 14px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;color:var(--dsw-alias-label-primary,#d4d4d4);pointer-events:none;overflow:hidden;scrollbar-width:none;}
95
- .cdex-code-ta{position:absolute;inset:0;width:100%;height:100%;background:transparent;color:transparent;caret-color:var(--dsw-alias-label-primary,#d4d4d4);border:none;outline:none;resize:none;padding:8px 14px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;overflow:auto;box-sizing:border-box;}
96
- .cdex-code-ta::selection{background:rgba(107,138,253,.35);}
97
- .cdx-tk-kw{color:#c586c0;font-weight:600;}
98
- .cdx-tk-str{color:#ce9178;}
99
- .cdx-tk-com{color:#6a9955;font-style:italic;}
100
- .cdx-tk-num{color:#b5cea8;}
101
- .cdx-tk-fn{color:#dcdcaa;}
102
- .cdx-tk-tag{color:#569cd6;}
103
- .cdx-tk-props{color:#9cdcfe;}
104
- .cdx-tk-attr{color:#dcdcaa;}
105
- .cdx-tk-punc{color:#808080;}
106
- .cdx-tk-plain{color:var(--dsw-alias-label-primary,#d4d4d4);}
107
- .cdex-empty{color:var(--dsw-alias-label-secondary,#6f6f6f);padding:16px;font-size:13px;}
108
- .cdex-empty-root{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-bottom:10px;word-break:break-all;font-family:Consolas,'Cascadia Code',Menlo,monospace;}
109
- .cdex-empty-root b{color:var(--dsw-alias-label-primary,#d4d4d4);font-weight:600;}
110
- .cdex-error{color:var(--dsw-alias-state-error-primary,#f48771);padding:8px;font-size:12px;}
111
- .cdex-statusbar{display:flex;align-items:center;height:24px;background:var(--dsw-alias-brand-primary,#6b8afd);color:#ffffff;font-size:12px;padding:0 12px;gap:16px;user-select:none;}
112
- .cdex-statusbar .cdex-dirty{color:var(--dsw-alias-state-warn-primary,#ffe082);font-weight:600;}
113
- .cdex-btn{border:1px solid var(--dsw-alias-border-l2,#3e3e42);background:var(--dsw-alias-brand-primary,#0e639c);color:#ffffff;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;}
114
- .cdex-btn:hover{filter:brightness(1.1);}
115
- .cdex-btn:disabled{opacity:.5;cursor:not-allowed;}
116
- .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;}
117
- .cdex-btn-ghost:hover{background:var(--dsw-alias-bg-layer-1,#4a4a4a);}
118
- .cdex-menu-backdrop{position:fixed;inset:0;z-index:10001;pointer-events:auto;}
119
- .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);}
120
- .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);}
121
- .cdex-menu-item:hover{background:var(--dsw-alias-brand-primary,#0e639c);color:#ffffff;}
122
- .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;}
123
- .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;}
124
- .cdex-modal-title{font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-primary,#e8e8e8);word-break:break-all;}
125
- .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;}
126
- .cdex-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px;}
127
- .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;}
128
- .cdex-ref-head{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:12px;}
129
- .cdex-ref-path{flex:1;color:var(--dsw-alias-label-secondary,#9d9d9d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
130
- .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;}
131
- .cdex-section{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin:12px 0 6px;}
132
- .cdex-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-top:12px;line-height:1.6;}
133
- .cdex-toast{position:fixed;z-index:10003;bottom:36px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-brand-primary,#0e639c);color:#ffffff;padding:9px 16px;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.4);max-width:80vw;}
134
- .cdex-resize{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:10;}
135
- .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);}
136
- .cdex-aiinput-wrap{position:relative;padding:10px 14px 16px;background:transparent;border-top:1px solid var(--dsw-alias-border-l1,#3e3e42);}
137
- .cdex-aiinput-card{background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l1,#3e3e42);border-radius:14px;box-shadow:0 2px 12px rgba(0,0,0,.18);overflow:hidden;}
138
- .cdex-aiinput-tags{display:flex;flex-wrap:wrap;gap:6px;padding:10px 12px 0;}
139
- .cdex-aiinput-tag{display:inline-flex;align-items:center;gap:6px;background:var(--dsw-alias-brand-primary,#0e639c);color:#ffffff;border-radius:12px;padding:3px 10px 3px 12px;font-size:12px;}
140
- .cdex-aiinput-tag-label{white-space:nowrap;}
141
- .cdex-aiinput-tag-tr{background:#b45309;color:#fff;}
142
- .cdex-aiinput-tag-x{border:none;background:transparent;color:#ffffff;cursor:pointer;font-size:13px;padding:0;line-height:1;opacity:.85;}
143
- .cdex-aiinput-tag-x:hover{opacity:1;}
144
- .cdex-aiinput{display:flex;gap:10px;align-items:flex-end;padding:10px 12px 12px;}
145
- .cdex-aiinput-ta{flex:1;background:transparent;color:var(--dsw-alias-label-primary,#d4d4d4);border:none;outline:none;resize:none;font-family:inherit;font-size:13px;line-height:1.55;min-height:40px;max-height:160px;padding:4px 2px;}
146
- .cdex-aiinput-ta::placeholder{color:var(--dsw-alias-label-secondary,#8a8a8a);}
147
- .cdex-aiinput-ta:disabled{opacity:.55;}
148
- .cdex-aiinput-send{flex:0 0 auto;height:36px;min-width:76px;border:none;background:var(--dsw-alias-brand-primary,#0e639c);color:#ffffff;border-radius:999px;padding:0 18px;cursor:pointer;font-size:13px;font-weight:600;transition:filter .15s ease, opacity .15s ease;}
149
- .cdex-aiinput-send.ghosted{opacity:.4;cursor:not-allowed;}
150
- .cdex-aiinput-warn{margin:10px 12px 0;padding:6px 10px;border-radius:8px;background:rgba(230,162,60,.12);border:1px solid rgba(230,162,60,.4);color:#e6a23c;font-size:12px;line-height:1.5;}
151
- .cdex-aiinput-send:hover:not(:disabled){filter:brightness(1.12);}
152
- .cdex-aiinput-send:disabled{opacity:.4;cursor:not-allowed;}
153
- .cdex-aiinput-foot{display:flex;align-items:center;gap:12px;padding:8px 14px 12px;color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;border-top:1px solid transparent;}
154
- .cdex-fp-bar{display:flex;gap:6px;margin-bottom:10px;align-items:center;}
155
- .cdex-fp-input{flex:1;box-sizing:border-box;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:6px;padding:8px 10px;font-family:inherit;font-size:13px;outline:none;}
156
- .cdex-fp-list{max-height:420px;overflow:auto;}
157
- .cdex-fp-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;}
158
- .cdex-fp-item:hover{background:var(--dsw-alias-bg-layer-1,#2a2d2e);}
159
- .cdex-fp-name{color:var(--dsw-alias-label-primary,#d4d4d4);font-size:13px;}
160
- .cdex-fp-crumbs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-bottom:8px;min-height:22px;}
161
- .cdex-fp-crumb{color:var(--dsw-alias-label-secondary,#8a8a8a);cursor:pointer;border:none;background:transparent;padding:0;font-size:12.5px;}
162
- .cdex-fp-crumb:hover{color:var(--dsw-alias-label-primary,#ffffff);}
163
- .cdex-fp-crumb-sep{color:var(--dsw-alias-label-secondary,#6f6f6f);font-size:12px;}
164
- .cdex-fp-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px;}
165
- .cdex-fp-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-right:auto;align-self:center;}
166
- /* 插件配置卡片:样式逐条对齐宿主 PluginCard.module.css / fields.module.css(dsh-context 同源) */
167
- .cdx-pc-card{list-style:none;border:1px solid var(--dsw-alias-border-l2,#3e3e42);border-radius:12px;background:var(--dsw-alias-bg-layer-3,#2d2d2d);transition:border-color .16s,background .16s;}
168
- .cdx-pc-card:hover{border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
169
- .cdx-pc-card-open,.cdx-pc-card-open:hover{background:var(--dsw-alias-bg-layer-2,#37373d);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
170
- .cdx-pc-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px;}
171
- .cdx-pc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c6ef5);outline-offset:-2px;}
172
- .cdx-pc-headtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;}
173
- .cdx-pc-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,#d4d4d4);}
174
- .cdx-pc-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9d9d9d);}
175
- .cdx-pc-badge{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,#3a3a41);color:var(--dsw-alias-label-secondary,#9d9d9d);}
176
- .cdx-pc-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#888);transition:transform .16s;display:inline-flex;}
177
- .cdx-pc-chevron-open{transform:rotate(180deg);}
178
- .cdx-pc-body{border-top:1px solid var(--dsw-alias-border-l2,#3e3e42);margin:0 16px;padding-bottom:8px;}
179
- .cdx-pc-field{display:flex;flex-direction:column;gap:6px;padding:12px 0;}
180
- .cdx-pc-field + .cdx-pc-field{border-top:1px solid var(--dsw-alias-border-l2,#3e3e42);}
181
- .cdx-pc-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
182
- .cdx-pc-label{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary,#d4d4d4);}
183
- .cdx-pc-note{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9d9d9d);word-break:break-all;}
184
- .cdx-pc-ok{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-success,#4ec9b0));word-break:break-all;}
185
- .cdx-pc-err{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error,#f48771);word-break:break-all;}
186
- .cdx-pc-new{font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary,#4c6ef5);}
187
- .cdx-pc-btn{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;}
188
- .cdx-pc-btn-outline{border-color:var(--dsw-alias-border-l2,#3e3e42);background:none;color:var(--dsw-alias-label-secondary,#9d9d9d);}
189
- .cdx-pc-btn-outline:hover:not(:disabled){color:var(--dsw-alias-label-primary,#d4d4d4);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
190
- .cdx-pc-btn-primary{background:var(--dsw-alias-label-primary,#d4d4d4);color:var(--dsw-alias-bg-layer-3,#2d2d2d);}
191
- .cdx-pc-btn:disabled{opacity:.4;cursor:default;}
192
- .cdx-pc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c6ef5);outline-offset:1px;}
193
- .cdx-pc-input{flex:1;min-width:220px;box-sizing:border-box;height:34px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,#3e3e42);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2d2d2d);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#d4d4d4);outline:none;}
194
- .cdx-pc-input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4c6ef5);}
195
- .cdx-pc-input::placeholder{color:var(--dsw-alias-label-tertiary,#777);}
196
- .cdx-pc-log{margin:0;background:var(--dsw-alias-bg-layer-1,#252526);border:1px solid var(--dsw-alias-border-l1,#333);border-radius:8px;padding:10px 12px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,#9d9d9d);max-height:160px;overflow:auto;white-space:pre-wrap;word-break:break-all;}
197
- `; document.head.appendChild(tag); return () => tag.remove(); })
198
-
199
- /* ============ §3 状态层 ============ */
200
- const store = {
201
- open: false,
202
- view: 'files',
203
- modePos: null,
204
- windowed: true,
205
- win: { x: 16, y: 16, w: Math.round(vw * 0.72), h: Math.round(vh * 0.72) },
206
- rootPath: null,
207
- nodes: {},
208
- tabs: [],
209
- activePath: null,
210
- buffers: {},
211
- savedContent: {},
212
- dirty: {},
213
- loadSeq: {},
214
- activeNotes: [],
215
- references: [],
216
- inputActions: null,
217
- inputDraft: '',
218
- inputPhase: 'plain',
219
- filePickerOpen: false,
220
- browsePath: null,
221
- browseEntries: [],
222
- ctxMenu: null,
223
- noteEditor: null,
224
- toast: null,
225
- error: null,
226
- }
227
- const listeners = new Set()
228
- function emit() { listeners.forEach((fn) => fn()) }
229
- function useStore() {
230
- const [, setTick] = React.useState(0)
231
- React.useEffect(() => {
232
- const fn = () => setTick((t) => t + 1)
233
- listeners.add(fn)
234
- return () => { listeners.delete(fn) }
235
- }, [])
236
- return store
237
- }
238
-
239
- function showToast(msg) {
240
- store.toast = { msg }
241
- emit()
242
- if (timer && typeof timer.timeout === 'function') {
243
- timer.timeout(() => { store.toast = null; emit() }, 2800)
244
- }
245
- }
246
-
247
- /* ============ 纯工具函数 ============ */
248
- function baseName(p) {
249
- const parts = String(p || '').split(/[\\/]/).filter(Boolean)
250
- return parts.length ? parts[parts.length - 1] : String(p || '')
251
- }
252
- function tagLabel(r) {
253
- const name = baseName(r.path)
254
- if (r.lineStart && r.lineEnd) {
255
- return name + '(' + r.lineStart + '-' + r.lineEnd + ')'
256
- }
257
- return name
258
- }
259
- function parentPath(p) {
260
- const s = String(p).replace(/[\\/]+$/, '')
261
- const idx = Math.max(s.lastIndexOf('\\'), s.lastIndexOf('/'))
262
- if (idx <= 0) return null
263
- const parent = s.slice(0, idx)
264
- if (/^[a-zA-Z]:$/.test(parent)) return parent + '\\'
265
- return parent || null
266
- }
267
- function crumbsOf(p) {
268
- if (!p) return []
269
- const sep = String(p).includes('\\') ? '\\' : '/'
270
- const parts = String(p).split(/[\\/]/).filter(Boolean)
271
- const crumbs = []
272
- let acc = ''
273
- for (let i = 0; i < parts.length; i++) {
274
- const part = parts[i]
275
- if (i === 0 && /^[a-zA-Z]:$/.test(part)) {
276
- acc = part + sep
277
- } else {
278
- acc = acc + part + sep
279
- }
280
- crumbs.push({ name: part, path: acc })
281
- }
282
- return crumbs
283
- }
284
- function langOf(p) {
285
- const ext = String(p || '').split('.').pop().toLowerCase()
286
- const map = { js:'javascript', jsx:'jsx', ts:'typescript', tsx:'tsx', py:'python', rs:'rust', go:'go', java:'java', c:'c', cpp:'cpp', cc:'cpp', h:'c', hpp:'cpp', css:'css', scss:'scss', html:'html', htm:'html', json:'json', md:'markdown', yml:'yaml', yaml:'yaml', sh:'shell', bash:'shell', ps1:'powershell', txt:'text', xml:'xml', sql:'sql', toml:'toml', ini:'ini' }
287
- return map[ext] || 'text'
288
- }
289
- function copyText(text) {
290
- let done = false
291
- try {
292
- if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
293
- navigator.clipboard.writeText(text).catch(() => {})
294
- done = true
295
- }
296
- } catch (e) {}
297
- if (!done) {
298
- try {
299
- if (typeof document !== 'undefined') {
300
- const ta = document.createElement('textarea')
301
- ta.value = text
302
- ta.style.position = 'fixed'
303
- ta.style.opacity = '0'
304
- document.body.appendChild(ta)
305
- ta.focus(); ta.select()
306
- try { document.execCommand('copy') } catch (e2) {}
307
- document.body.removeChild(ta)
308
- }
309
- } catch (e) {}
310
- }
311
- }
312
- // —— 内联轻量语法高亮(自写 tokenizer,返回 HTML 字符串) ——
313
- function esc(s) {
314
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
315
- }
316
- function span(cls, text) { return '<span class="' + cls + '">' + text + '</span>' }
317
- function highlight(code, lang) {
318
- if (!code) return ''
319
- const L = String(lang || '').toLowerCase()
320
- // 行注释前缀(按语言)
321
- let lineComment = '//'
322
- if (L === 'python' || L === 'py') lineComment = '#'
323
- else if (L === 'shell' || L === 'bash' || L === 'sh') lineComment = '#'
324
- else if (L === 'html' || L === 'xml' || L === 'css' || L === 'scss') lineComment = null
325
- // 关键字(按语言簇)
326
- let keywords = /^(function|return|if|else|for|while|const|let|var|new|class|extends|import|export|from|async|await|try|catch|throw|switch|case|break|continue|typeof|instanceof|this|true|false|null|undefined|interface|type|enum|declare|readonly|default|in|of|yield|do|void|static|public|private|protected|using|as|is|super|delete|void)$/
327
- if (L === 'python' || L === 'py') keywords = /^(def|return|if|elif|else|for|while|import|from|as|class|try|except|finally|raise|with|lambda|pass|break|continue|and|or|not|in|is|True|False|None|global|nonlocal|yield|assert|del|async|await)$/
328
- if (L === 'json') keywords = /^(true|false|null)$/
329
- const out = []
330
- let i = 0
331
- const n = code.length
332
- while (i < n) {
333
- const ch = code[i]
334
- // 行注释
335
- if (lineComment && code.startsWith(lineComment, i)) {
336
- let j = code.indexOf('\n', i)
337
- if (j < 0) j = n
338
- out.push(span('cdx-tk-com', esc(code.slice(i, j))))
339
- i = j
340
- continue
341
- }
342
- // 块注释
343
- if (code.startsWith('/*', i)) {
344
- let j = code.indexOf('*/', i)
345
- j = j < 0 ? n : j + 2
346
- out.push(span('cdx-tk-com', esc(code.slice(i, j))))
347
- i = j
348
- continue
349
- }
350
- // 字符串(单引号 / 双引号 / 反引号)
351
- if ((ch === '"' || ch === '\'' || ch === '`')) {
352
- const quote = ch
353
- let j = i + 1
354
- while (j < n && code[j] !== quote) {
355
- if (code[j] === '\\') j++
356
- j++
357
- }
358
- if (j < n) j++
359
- out.push(span('cdx-tk-str', esc(code.slice(i, j))))
360
- i = j
361
- continue
362
- }
363
- // 数字
364
- if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < n && code[i + 1] >= '0' && code[i + 1] <= '9')) {
365
- let j = i
366
- while (j < n && /[0-9a-fA-FxXbBoO_eE.\-]/.test(code[j])) j++
367
- out.push(span('cdx-tk-num', esc(code.slice(i, j))))
368
- i = j
369
- continue
370
- }
371
- // HTML/XML 标签
372
- if ((L === 'html' || L === 'xml') && ch === '<') {
373
- if (code.startsWith('<!--', i)) {
374
- let j = code.indexOf('-->', i)
375
- j = j < 0 ? n : j + 3
376
- out.push(span('cdx-tk-com', esc(code.slice(i, j))))
377
- i = j
378
- continue
379
- }
380
- let j = i + 1
381
- const isClose = code[j] === '/'
382
- if (isClose) j++
383
- let tagName = ''
384
- while (j < n && /[a-zA-Z0-9-]/.test(code[j])) { tagName += code[j]; j++ }
385
- if (!tagName) { out.push(esc('<', ch + code.slice(i + 1, Math.min(i + 2, n)))); i++; continue }
386
- out.push('<span class="cdx-tk-punc">&lt;</span>' + (isClose ? '<span class="cdx-tk-punc">/</span>' : '') + span('cdx-tk-tag', tagName))
387
- // 遍历属性直到闭合
388
- while (j < n && code[j] !== '>' && !(code[j] === '/')) {
389
- if (/\s/.test(code[j])) { out.push(esc(code[j])); j++; continue }
390
- let ks = j
391
- while (j < n && code[j] !== '=' && !/\s/.test(code[j]) && code[j] !== '>' && code[j] !== '/') j++
392
- const key = code.slice(ks, j)
393
- if (key) out.push(span('cdx-tk-attr', key))
394
- // 跳过 = "值"
395
- if (code[j] === '=') {
396
- out.push(esc('='))
397
- j++
398
- if (code[j] === '"' || code[j] === '\'') {
399
- const q = code[j]; let e = j + 1
400
- while (e < n && code[e] !== q) { if (code[e] === '\\') e++; e++ }
401
- if (e < n) e++
402
- out.push(span('cdx-tk-str', esc(code.slice(j, e))))
403
- j = e
404
- } else {
405
- let e = j
406
- while (e < n && !/\s/.test(code[e]) && code[e] !== '>' && code[e] !== '/') e++
407
- out.push(span('cdx-tk-str', esc(code.slice(j, e))))
408
- j = e
409
- }
410
- }
411
- }
412
- if (code[j] === '/') { out.push(span('cdx-tk-punc', '/')); j++ }
413
- if (code[j] === '>') { out.push(span('cdx-tk-punc', '>')); j++ }
414
- i = j
415
- continue
416
- }
417
- // 标识符 / 关键字 / 函数
418
- if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
419
- let j = i
420
- while (j < n && /[\w$]/.test(code[j])) j++
421
- const word = code.slice(i, j)
422
- let cls = 'cdx-tk-plain'
423
- const kws = String(keywords)
424
- if (keywords.test(word)) cls = 'cdx-tk-kw'
425
- else {
426
- // 函数名:后跟 (
427
- let k = j
428
- while (k < n && /\s/.test(code[k])) k++
429
- if (code[k] === '(') cls = 'cdx-tk-fn'
430
- }
431
- out.push(span(cls, word))
432
- i = j
433
- continue
434
- }
435
- // 其他(含 CSS 属性)
436
- if (L === 'css' || L === 'scss') {
437
- if (ch === '#') {
438
- // # 后跟十六进制(#fff / #1e73be)整体按数字着色;修复:原实现漏掉 i 推进导致死循环
439
- let j = i + 1
440
- while (j < n && /[0-9a-fA-F]/.test(code[j])) j++
441
- out.push(span('cdx-tk-num', esc(code.slice(i, j))))
442
- i = j
443
- } else {
444
- out.push(esc(ch))
445
- i++
446
- }
447
- }
448
- // 标点 / 普通字符
449
- else {
450
- if ('(){}[]<>=!+-*/&|;:,.?$%^~'.indexOf(ch) >= 0) out.push(span('cdx-tk-punc', esc(ch)))
451
- else out.push(esc(ch))
452
- i++
453
- }
454
- }
455
- return out.join('')
456
- }
457
-
458
- /* ============ §4 RPC 操作层(文件树 / 标签页 / 引用备注) ============ */
459
- // —— 文件浏览器 ——
460
- async function browseDir(path) {
461
- if (!path) return
462
- store.browsePath = path
463
- store.browseEntries = []
464
- store.error = null
465
- emit()
466
- const res = await callHost('list-dir', { path })
467
- if (res && res.error) { store.error = res.error; emit(); return }
468
- store.browseEntries = (res && res.entries) || []
469
- emit()
470
- }
471
- function openFilePicker() {
472
- store.filePickerOpen = true
473
- emit()
474
- const start = store.browsePath || store.rootPath || null
475
- if (start) { browseDir(start); return }
476
- callHost('get-root').then((r) => {
477
- if (r && r.root) { browseDir(r.root) }
478
- else { store.error = '未定位到工作区根,请在输入框粘贴目录或文件路径后回车'; emit() }
479
- })
480
- }
481
- function pickFile(path) {
482
- store.filePickerOpen = false
483
- emit()
484
- openFile(path)
485
- }
486
- async function setWorkspaceRoot(path) {
487
- if (!path) return
488
- const res = await callHost('set-root', { path })
489
- if (res && res.error) { showToast('设置失败:' + res.error); return }
490
- const newRoot = (res && res.root) ? res.root : path
491
- store.rootPath = newRoot
492
- store.nodes = {}
493
- store.tabs = []
494
- store.activePath = null
495
- store.buffers = {}
496
- store.savedContent = {}
497
- store.dirty = {}
498
- store.activeNotes = []
499
- store.browsePath = null
500
- store.browseEntries = []
501
- store.nodes[newRoot] = { name: baseName(newRoot), path: newRoot, isDir: true, children: [], expanded: true, loaded: false }
502
- store.filePickerOpen = false
503
- emit()
504
- await toggleExpand(newRoot)
505
- showToast('工作区根目录:' + newRoot)
506
- }
507
- // —— 文件树 ——
508
- async function refresh() {
509
- store.error = null
510
- if (!store.rootPath) {
511
- const r = await callHost('get-root')
512
- store.rootPath = (r && r.root) || null
513
- if (!store.rootPath) { store.error = '未找到工作区根目录'; emit(); return }
514
- store.nodes = {}
515
- store.nodes[store.rootPath] = { name: baseName(store.rootPath), path: store.rootPath, isDir: true, children: [], expanded: true, loaded: false }
516
- emit()
517
- await toggleExpand(store.rootPath)
518
- } else {
519
- const node = store.nodes[store.rootPath]
520
- if (node) { node.loaded = false; node.children = []; node.expanded = true }
521
- await toggleExpand(store.rootPath)
522
- }
523
- }
524
- async function toggleExpand(path) {
525
- const node = store.nodes[path]
526
- if (!node) return
527
- if (node.expanded) { node.expanded = false; emit(); return }
528
- node.expanded = true
529
- if (!node.loaded) {
530
- node.loading = true; emit()
531
- const res = await callHost('list-dir', { path })
532
- node.loading = false; node.loaded = true
533
- if (res && res.error) { store.error = res.error; emit(); return }
534
- const entries = (res && res.entries) || []
535
- node.children = entries.map((e) => { store.nodes[e.path] = e; return e.path })
536
- emit()
537
- } else { emit() }
538
- }
539
- // —— 标签页 / 编辑 / 保存 ——
540
- async function openFile(path) {
541
- const name = baseName(path)
542
- if (!store.tabs.some((t) => t.path === path)) {
543
- store.tabs.push({ path, name })
544
- }
545
- store.activePath = path
546
- store.error = null
547
- if (!(path in store.buffers)) {
548
- store.buffers[path] = ''
549
- store.savedContent[path] = ''
550
- store.dirty[path] = false
551
- emit()
552
- const res = await callHost('read-file', { path })
553
- if (res && res.error) { store.error = res.error; emit(); return }
554
- store.buffers[path] = res.content
555
- store.savedContent[path] = res.content
556
- store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
557
- emit()
558
- } else {
559
- emit()
560
- }
561
- loadNotes(path)
562
- }
563
- function closeTab(path) {
564
- const i = store.tabs.findIndex((t) => t.path === path)
565
- if (i < 0) return
566
- store.tabs.splice(i, 1)
567
- if (store.activePath === path) {
568
- const next = store.tabs[i] || store.tabs[i - 1] || null
569
- store.activePath = next ? next.path : null
570
- if (next) { loadNotes(next.path) } else { store.activeNotes = [] }
571
- }
572
- emit()
573
- }
574
- async function saveFile(path) {
575
- const p = path || store.activePath
576
- if (!p) return
577
- const content = store.buffers[p]
578
- const res = await callHost('write-file', { path: p, content })
579
- if (res && res.error) { showToast('保存失败:' + res.error); return }
580
- store.savedContent[p] = content
581
- store.dirty[p] = false
582
- emit()
583
- showToast('已保存 ' + baseName(p))
584
- }
585
- // —— 引用 / 备注 读取 ——
586
- async function loadNotes(path) {
587
- const res = await callHost('list-notes', { path })
588
- if (res && res.notes) { store.activeNotes = res.notes; emit() }
589
- }
590
- async function loadReferences() {
591
- const res = await callHost('list-references')
592
- if (res && res.references) { store.references = res.references; emit() }
593
- }
594
-
595
- /* ============ §5 Actions 层(右键菜单 / 选区) ============ */
596
- function onEditorContext(e) {
597
- const ta = e.target
598
- const start = ta.selectionStart, end = ta.selectionEnd
599
- if (start === end) return
600
- e.preventDefault()
601
- const full = ta.value
602
- const text = full.slice(start, end)
603
- const lineStart = full.slice(0, start).split('\n').length
604
- const lineEnd = full.slice(0, end).split('\n').length
605
- store.ctxMenu = { x: e.clientX, y: e.clientY, sel: { text, lineStart, lineEnd, path: store.activePath, language: langOf(store.activePath) } }
606
- emit()
607
- }
608
- function closeMenu() { store.ctxMenu = null; emit() }
609
- async function doCite(sel) {
610
- const ref = await callHost('add-reference', { path: sel.path, language: sel.language, lineStart: sel.lineStart, lineEnd: sel.lineEnd, text: sel.text, kind: 'cite' })
611
- if (ref && ref.reference) { store.references.push(ref.reference); emit() }
612
- showToast('已引用:' + baseName(sel.path))
613
- }
614
- async function doTranslate(sel) {
615
- const ref = await callHost('add-reference', { path: sel.path, language: sel.language, lineStart: sel.lineStart, lineEnd: sel.lineEnd, text: sel.text, kind: 'translate' })
616
- if (ref && ref.reference) { store.references.push(ref.reference); emit() }
617
- showToast('已创建翻译引用(输入框上方已加 🌐 标签)。对 AI 说「翻译我引用的内容」即可。')
618
- }
619
- function openNoteEditor(sel) {
620
- store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: '' }
621
- emit()
622
- }
623
- async function pasteToNote(sel) {
624
- let clip = ''
625
- try {
626
- if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.readText) {
627
- clip = await navigator.clipboard.readText()
628
- }
629
- } catch (e) {}
630
- store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: clip }
631
- emit()
632
- showToast(clip ? '已粘贴到备注框,点击「保存」完成。' : '无法读取剪贴板,请在备注框内按 Ctrl+V 粘贴。')
633
- }
634
- async function delReference(id) {
635
- await callHost('delete-reference', { id })
636
- store.references = store.references.filter((r) => r.id !== id)
637
- emit()
638
- }
639
- async function delNote(id) {
640
- await callHost('delete-note', { id })
641
- store.activeNotes = store.activeNotes.filter((n) => n.id !== id)
642
- emit()
643
- }
644
- // —— 引用自动附带(AI 上下文) ——
645
- function buildSendPayload(draft) {
646
- // 引用文本不再粘贴进消息(明面上不展示):引用统一存 host 端,AI 通过 get_referenced_snippets
647
- // 工具读取;消息里只附一行「标签级」指针,让 AI 知道有引用可读。
648
- const text = draft || ''
649
- const citeRefs = store.references.filter((r) => r.kind === 'cite')
650
- const trRefs = store.references.filter((r) => r.kind === 'translate')
651
- const parts = []
652
- if (citeRefs.length > 0) parts.push(citeRefs.length + ' 段代码引用')
653
- if (trRefs.length > 0) parts.push(trRefs.length + ' 条翻译请求')
654
- if (parts.length === 0) return text
655
- const sep = text.trim() ? '\n\n' : ''
656
- return text + sep + '[Code UI] 已标记 ' + parts.join('、')
657
- + '(引用内容未粘贴在消息里,请调用 get_referenced_snippets 工具读取)。'
658
- }
659
-
660
- /* ============ §6 Pointer 处理层(窗口拖拽 / 缩放 / 模式条移动) ============ */
661
- function onBarPointerDown(e) {
662
- if (e.target && typeof e.target.closest === 'function' && e.target.closest('button')) return
663
- e.preventDefault()
664
- const bar = e.currentTarget
665
- const rect = bar.getBoundingClientRect()
666
- const startX = e.clientX, startY = e.clientY
667
- const baseX = store.modePos ? store.modePos.x : rect.left
668
- const baseY = store.modePos ? store.modePos.y : rect.top
669
- try { bar.setPointerCapture(e.pointerId) } catch (err) {}
670
- function move(ev) {
671
- store.modePos = { x: Math.max(0, baseX + (ev.clientX - startX)), y: Math.max(0, baseY + (ev.clientY - startY)) }
672
- emit()
673
- }
674
- function up() {
675
- bar.removeEventListener('pointermove', move)
676
- bar.removeEventListener('pointerup', up)
677
- bar.removeEventListener('pointercancel', up)
678
- }
679
- bar.addEventListener('pointermove', move)
680
- bar.addEventListener('pointerup', up)
681
- bar.addEventListener('pointercancel', up)
682
- }
683
- function onTopPointerDown(e) {
684
- if (!store.windowed) return
685
- if (e.target && typeof e.target.closest === 'function' && e.target.closest('button')) return
686
- e.preventDefault()
687
- const top = e.currentTarget
688
- const sx = store.win.x, sy = store.win.y
689
- const px = e.clientX, py = e.clientY
690
- try { top.setPointerCapture(e.pointerId) } catch (err) {}
691
- function move(ev) {
692
- store.win.x = Math.max(0, sx + (ev.clientX - px))
693
- store.win.y = Math.max(0, sy + (ev.clientY - py))
694
- emit()
695
- }
696
- function up() {
697
- top.removeEventListener('pointermove', move)
698
- top.removeEventListener('pointerup', up)
699
- top.removeEventListener('pointercancel', up)
700
- }
701
- top.addEventListener('pointermove', move)
702
- top.addEventListener('pointerup', up)
703
- top.addEventListener('pointercancel', up)
704
- }
705
- function onResizePointerDown(e) {
706
- if (!store.windowed) return
707
- e.preventDefault()
708
- const handle = e.currentTarget
709
- const sw = store.win.w, sh = store.win.h
710
- const px = e.clientX, py = e.clientY
711
- try { handle.setPointerCapture(e.pointerId) } catch (err) {}
712
- function move(ev) {
713
- store.win.w = Math.max(360, sw + (ev.clientX - px))
714
- store.win.h = Math.max(260, sh + (ev.clientY - py))
715
- emit()
716
- }
717
- function up() {
718
- handle.removeEventListener('pointermove', move)
719
- handle.removeEventListener('pointerup', up)
720
- handle.removeEventListener('pointercancel', up)
721
- }
722
- handle.addEventListener('pointermove', move)
723
- handle.addEventListener('pointerup', up)
724
- handle.addEventListener('pointercancel', up)
725
- }
726
-
727
- /* ============ §7 组件层(V.* 命名空间) ============ */
728
- function InputBridge(props) {
729
- const useInput = props.useInput
730
- const inputState = typeof useInput === 'function' ? useInput((s) => s) : undefined
731
- const inputActions = props.inputActions
732
- React.useEffect(() => {
733
- store.inputActions = inputActions || null
734
- if (inputState) {
735
- store.inputDraft = inputState.draft
736
- store.inputPhase = inputState.phase
737
- }
738
- emit()
739
- })
740
- return null
741
- }
742
- function AiInput() {
743
- const st = useStore()
744
- let taEl = null
745
- const disabled = !st.inputActions
746
- // 引用与翻译请求都只显示「标签」:内容不进输入框/消息正文,AI 经 get_referenced_snippets 工具读取
747
- const refs = st.references
748
- const citeCount = refs.filter((r) => r.kind === 'cite').length
749
- const trCount = refs.length - citeCount
750
- const submit = () => {
751
- if (!st.inputActions || !taEl) {
752
- // 无会话时明确告知原因,而不是无声无息
753
- if (!st.inputActions) showToast('请先打开一个会话再发送:在主界面新建或选择会话后,这里即可发送')
754
- return
755
- }
756
- const text = taEl.value
757
- if (!text.trim() && refs.length === 0) return
758
- const payload = buildSendPayload(text)
759
- st.inputActions.setDraft(payload)
760
- st.inputActions.submit()
761
- taEl.value = ''
762
- }
763
- return h('div', { className: 'cdex-aiinput-wrap' },
764
- h('div', { className: 'cdex-aiinput-card' },
765
- disabled ? h('div', { className: 'cdex-aiinput-warn' }, '⚠ 未打开会话:请先在主界面新建或选择一个会话,之后即可在此发送(引用标签会保留)') : null,
766
- refs.length > 0 ? h('div', { className: 'cdex-aiinput-tags' },
767
- refs.map((r) => h('span', { className: 'cdex-aiinput-tag' + (r.kind === 'translate' ? ' cdex-aiinput-tag-tr' : ''), key: r.id },
768
- h('span', { className: 'cdex-aiinput-tag-label' }, (r.kind === 'translate' ? '🌐 翻译 ' : '🔗 ') + tagLabel(r)),
769
- h('button', { className: 'cdex-aiinput-tag-x', title: r.kind === 'translate' ? '删除翻译引用' : '删除引用', onClick: () => delReference(r.id) }, '✕'),
770
- )),
771
- ) : null,
772
- h('div', { className: 'cdex-aiinput' },
773
- h('textarea', {
774
- className: 'cdex-aiinput-ta',
775
- ref: (el) => { taEl = el },
776
- placeholder: disabled ? '请先打开一个会话(在主界面新建或选择会话后即可发送)' : '给 AI 发消息…(Ctrl+Enter 发送)',
777
- disabled: disabled,
778
- onKeyDown: (e) => {
779
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
780
- e.preventDefault()
781
- submit()
782
- }
783
- },
784
- }),
785
- // 不用 HTML disabled(收不到点击):置灰样式 + 点击弹原因,点了就有反馈
786
- h('button', {
787
- className: 'cdex-aiinput-send' + (disabled ? ' ghosted' : ''),
788
- 'aria-disabled': disabled,
789
- title: disabled ? '请先打开一个会话' : '发送(Ctrl+Enter)',
790
- onClick: () => {
791
- if (disabled) { showToast('请先打开一个会话再发送:在主界面新建或选择会话后,这里即可发送'); return }
792
- submit()
793
- },
794
- }, '发送'),
795
- ),
796
- h('div', { className: 'cdex-aiinput-foot' },
797
- h('span', null, 'Ctrl+Enter 发送'),
798
- h('span', null, 'Shift+Enter 换行'),
799
- citeCount + trCount > 0 ? h('span', null, '已引用 ' + citeCount + ' 段代码 · ' + trCount + ' 条翻译(AI 经工具读取,不占消息正文)') : null,
800
- ),
801
- ),
802
- )
803
- }
804
- function FilePicker() {
805
- const st = useStore()
806
- if (!st.filePickerOpen) return null
807
- const dirs = st.browseEntries.filter((x) => x.isDir)
808
- const files = st.browseEntries.filter((x) => !x.isDir)
809
- const crumbs = crumbsOf(st.browsePath)
810
- const close = () => { st.filePickerOpen = false; emit() }
811
- const goUp = () => { if (st.browsePath) { const p = parentPath(st.browsePath); if (p) browseDir(p) } }
812
- const goTo = (e) => {
813
- if (e.key === 'Enter') {
814
- const raw = e.target.value
815
- const p = raw && raw.trim()
816
- if (!p) return
817
- callHost('stat-path', { path: p }).then((res) => {
818
- if (!res || res.error) { showToast('无法访问该路径:' + ((res && res.error) || '未知错误')); return }
819
- if (res.isDir) { browseDir(res.path || p) }
820
- else if (res.isFile) { pickFile(res.path || p) }
821
- // 兼容回退:宿主 isDir 字段缺失但路径确实存在时,按目录尝试浏览(list-dir 对非目录会给出真实错误)
822
- else if (res.exists) { browseDir(p) }
823
- else { showToast('路径不存在或不可用:' + p) }
824
- })
825
- }
826
- }
827
- // 当前浏览到的文件夹;未浏览时回退到已识别的工作区根,保证按钮不无谓置灰
828
- const curDir = st.browsePath || st.rootPath
829
- const confirm = () => { if (curDir) setWorkspaceRoot(curDir) }
830
- return h('div', { className: 'cdex-modal', onClick: close },
831
- h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
832
- h('div', { className: 'cdex-modal-title' }, '打开文件'),
833
- h('div', { className: 'cdex-fp-crumbs' },
834
- crumbs.map((c, i) => h('span', { key: i },
835
- h('button', { className: 'cdex-fp-crumb', onClick: () => browseDir(c.path) }, c.name),
836
- i < crumbs.length - 1 ? h('span', { className: 'cdex-fp-crumb-sep' }, '›') : null,
837
- )),
838
- ),
839
- h('div', { className: 'cdex-fp-bar' },
840
- h('button', { className: 'cdex-btn-ghost', onClick: goUp, title: '上级目录' }, '↑'),
841
- h('input', { className: 'cdex-fp-input', defaultValue: curDir || '', placeholder: '输入绝对路径后回车跳转', onKeyDown: goTo }),
842
- ),
843
- h('div', { className: 'cdex-fp-list' },
844
- dirs.map((d) => h('div', { className: 'cdex-fp-item', key: d.path, onClick: () => browseDir(d.path) },
845
- h('span', { className: 'cdex-fp-name' }, '📁 ' + d.name),
846
- )),
847
- files.map((f) => h('div', { className: 'cdex-fp-item', key: f.path, onClick: () => pickFile(f.path) },
848
- h('span', { className: 'cdex-fp-name' }, '📄 ' + f.name),
849
- )),
850
- dirs.length === 0 && files.length === 0 ? h('div', { className: 'cdex-empty' }, st.error ? String(st.error) : '空目录') : null,
851
- ),
852
- h('div', { className: 'cdex-fp-actions' },
853
- h('span', { className: 'cdex-fp-hint' }, '浏览到目标文件夹后,点「以此文件夹为根」'),
854
- h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
855
- h('button', { className: 'cdex-btn', onClick: confirm, disabled: !curDir }, '以此文件夹为根'),
856
- ),
857
- ),
858
- )
859
- }
860
- function ModeTabs() {
861
- const st = useStore()
862
- const style = st.modePos ? { top: st.modePos.y, left: st.modePos.x } : { top: 10, left: '50%', transform: 'translateX(-50%)' }
863
- return h('div', { className: 'cdex-modebar', style: style, onPointerDown: onBarPointerDown },
864
- h('span', { className: 'cdex-mode-grip', title: '拖拽移动' }, '⠿'),
865
- h('button', { className: 'cdex-mode-tab' + (st.open ? ' active' : ''), onClick: () => { if (!st.open) { st.open = true; emit() } } }, 'Code UI'),
866
- h('button', { className: 'cdex-mode-tab' + (!st.open ? ' active' : ''), onClick: () => { if (st.open) { st.open = false; emit() } } }, '原始风格-ui'),
867
- )
868
- }
869
- function ActivityBar() {
870
- const st = useStore()
871
- return h('div', { className: 'cdex-activitybar' },
872
- h('button', { className: 'cdex-abar-btn' + (st.view === 'files' ? ' active' : ''), title: '资源管理器', onClick: () => { st.view = 'files'; emit() } }, '📁'),
873
- h('button', { className: 'cdex-abar-btn' + (st.view === 'refs' ? ' active' : ''), title: '引用 / 备注', onClick: () => { st.view = 'refs'; emit() } }, '📎'),
874
- h('div', { className: 'cdex-abar-spacer' }),
875
- h('button', { className: 'cdex-abar-btn', title: '帮助', onClick: () => showToast('选中代码右键可:复制 / 粘贴 / 备注 / 翻译 / 引用给 AI。Ctrl+S 保存。Ctrl+P 打开文件。') }, 'ⓘ'),
876
- h('button', { className: 'cdex-abar-btn', title: '退出编辑器', onClick: () => { st.open = false; emit() } }, '↩'),
877
- )
878
- }
879
- function TreeNode(props) {
880
- const st = useStore()
881
- const node = st.nodes[props.path]
882
- if (!node) return null
883
- const style = { paddingLeft: (6 + props.depth * 14) + 'px' }
884
- const cls = 'cdex-node' + (st.activePath === node.path ? ' active' : '')
885
- const children = node.isDir && node.expanded ? (node.children || []).map((c) => h(TreeNode, { key: c, path: c, depth: props.depth + 1 })) : null
886
- return h('div', null,
887
- h('div', { className: cls, style: style, onClick: () => { node.isDir ? toggleExpand(node.path) : openFile(node.path) } },
888
- node.isDir ? h('span', { className: 'cdex-arrow' }, node.expanded ? '▾' : '▸') : h('span', { className: 'cdex-arrow' }, ''),
889
- h('span', { className: node.isDir ? 'cdex-folder' : 'cdex-file' }, node.isDir ? '📁 ' : '📄 ', node.name),
890
- ),
891
- children,
892
- )
893
- }
894
- function FileTree() {
895
- const st = useStore()
896
- return h('div', { className: 'cdex-sidebar' },
897
- h('div', { className: 'cdex-sidebar-head' },
898
- h('span', { title: st.rootPath || '' }, st.rootPath ? ('资源管理器 · ' + baseName(st.rootPath)) : '资源管理器'),
899
- h('button', { className: 'cdex-btn-ghost', onClick: refresh, title: '刷新' }, '⟳'),
900
- ),
901
- h('div', { className: 'cdex-tree' },
902
- st.error ? h('div', { className: 'cdex-error' }, String(st.error)) : null,
903
- st.rootPath ? h(TreeNode, { path: st.rootPath, depth: 0 }) : h('div', { className: 'cdex-empty' }, '正在加载目录…'),
904
- ),
905
- )
906
- }
907
- function RefItem(r) {
908
- return h('div', { className: 'cdex-ref', key: r.id },
909
- h('div', { className: 'cdex-ref-head' },
910
- h('span', { className: 'cdex-ref-path' }, r.path, r.lineStart ? (' :' + r.lineStart + '-' + r.lineEnd) : ''),
911
- h('button', { className: 'cdex-btn-ghost', onClick: () => delReference(r.id) }, '✕'),
912
- ),
913
- h('div', { className: 'cdex-ref-code' }, r.text.slice(0, 500)),
914
- )
915
- }
916
- function ReferencesView() {
917
- const st = useStore()
918
- const citeList = st.references.filter((r) => r.kind === 'cite')
919
- const trList = st.references.filter((r) => r.kind === 'translate')
920
- return h('div', { className: 'cdex-sidebar' },
921
- h('div', { className: 'cdex-sidebar-head' }, h('span', null, '引用 / 备注')),
922
- h('div', { className: 'cdex-tree' },
923
- h('div', { className: 'cdex-section' }, '🔗 代码引用 (' + citeList.length + ')'),
924
- citeList.length === 0 ? h('div', { className: 'cdex-empty' }, '选中代码后右键「引用」。') : citeList.map((r) => RefItem(r)),
925
- h('div', { className: 'cdex-section' }, '🌐 翻译请求 (' + trList.length + ')'),
926
- trList.length === 0 ? h('div', { className: 'cdex-empty' }, '选中代码后右键「翻译」。') : trList.map((r) => RefItem(r)),
927
- h('div', { className: 'cdex-section' }, '📝 备注 (' + st.activeNotes.length + ')'),
928
- st.activeNotes.length === 0 ? h('div', { className: 'cdex-empty' }, '当前文件暂无备注。') : st.activeNotes.map((n) => h('div', { className: 'cdex-ref', key: n.id },
929
- h('div', { className: 'cdex-ref-head' },
930
- h('span', { className: 'cdex-ref-path' }, n.path, n.lineStart ? (' :' + n.lineStart + '-' + n.lineEnd) : ''),
931
- h('button', { className: 'cdex-btn-ghost', onClick: () => delNote(n.id) }, '✕'),
932
- ),
933
- h('div', { className: 'cdex-ref-code' }, n.note),
934
- )),
935
- h('div', { className: 'cdex-hint' }, '在输入框对 AI 说「读取/翻译我引用的代码」,AI 会调用 get_referenced_snippets 工具获取内容。'),
936
- ),
937
- )
938
- }
939
- let gutterEl = null
940
- function syncGutter(ta) {
941
- if (gutterEl) gutterEl.scrollTop = ta.scrollTop
942
- }
943
- function TabBar() {
944
- const st = useStore()
945
- return h('div', { className: 'cdex-tabs' },
946
- st.tabs.map((t) => h('div', { key: t.path, className: 'cdex-tab' + (st.activePath === t.path ? ' active' : ''), onClick: () => { st.activePath = t.path; loadNotes(t.path); emit() } },
947
- h('span', null, t.name + (st.dirty[t.path] ? ' ●' : '')),
948
- h('button', { className: 'cdex-tab-close', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '✕'),
949
- )),
950
- h('div', { className: 'cdex-tab-actions' },
951
- h('button', { className: 'cdex-btn', title: '保存 (Ctrl+S)', onClick: () => saveFile() }, '保存'),
952
- ),
953
- )
954
- }
955
- function Editor() {
956
- const st = useStore()
957
- if (!st.activePath) {
958
- // 空态时显式展示当前工作区根,避免「以此文件夹为根」后误认为未切换
959
- return h('div', { className: 'cdex-editor' },
960
- h('div', { className: 'cdex-empty' },
961
- st.rootPath ? h('div', { className: 'cdex-empty-root' }, '当前工作区:', h('b', null, st.rootPath)) : null,
962
- '从左侧资源管理器打开文件,或 ',
963
- h('button', { className: 'cdex-btn', onClick: openFilePicker }, '📂 打开文件'),
964
- ),
965
- )
966
- }
967
- const p = st.activePath
968
- const content = st.buffers[p] != null ? st.buffers[p] : ''
969
- const lineCount = content.split('\n').length
970
- const gutter = []
971
- for (let i = 1; i <= lineCount; i++) gutter.push(String(i))
972
- // 高亮 HTML(overlay 下层展示)
973
- const html = highlight(content, langOf(p))
974
- // 滚动同步:把 pre 滚动到与 textarea 相同
975
- const syncOverlayScroll = (taEl) => {
976
- if (!taEl) return
977
- const preEl = taEl.parentNode ? taEl.parentNode.querySelector('.cdex-code-pre') : null
978
- const gutEl = taEl.parentNode ? taEl.parentNode.parentNode.querySelector('.cdex-gutter') : null
979
- if (preEl) { preEl.scrollTop = taEl.scrollTop; preEl.scrollLeft = taEl.scrollLeft }
980
- if (gutterEl) gutterEl.scrollTop = taEl.scrollTop
981
- }
982
- return h('div', { className: 'cdex-editor' },
983
- h(TabBar),
984
- h('div', { className: 'cdex-codearea' },
985
- h('div', { className: 'cdex-gutter', ref: (el) => { gutterEl = el } }, gutter.join('\n')),
986
- h('div', { className: 'cdex-code-layer' },
987
- h('pre', { className: 'cdex-code-pre', 'aria-hidden': true, dangerouslySetInnerHTML: { __html: html } }),
988
- h('textarea', {
989
- // key 拼上加载序号:内容异步读回后 key 变化触发重挂载,defaultValue 才会生效(否则编辑区空白、与高亮层脱节)
990
- key: p + ':' + (st.loadSeq[p] || 0),
991
- className: 'cdex-code-ta',
992
- defaultValue: content,
993
- spellCheck: false,
994
- wrap: 'off',
995
- onScroll: (e) => syncOverlayScroll(e.target),
996
- onContextMenu: onEditorContext,
997
- onChange: (e) => {
998
- store.buffers[p] = e.target.value
999
- store.dirty[p] = e.target.value !== store.savedContent[p]
1000
- emit()
1001
- },
1002
- onKeyDown: (e) => {
1003
- if ((e.ctrlKey || e.metaKey) && (e.key === 'p' || e.key === 'P')) {
1004
- e.preventDefault()
1005
- openFilePicker()
1006
- }
1007
- if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
1008
- e.preventDefault()
1009
- saveFile()
1010
- }
1011
- },
1012
- }),
1013
- ),
1014
- ),
1015
- )
1016
- }
1017
- function StatusBar() {
1018
- const st = useStore()
1019
- const p = st.activePath
1020
- const content = p && st.buffers[p] != null ? st.buffers[p] : ''
1021
- const lines = content ? content.split('\n').length : 0
1022
- const dirty = p && st.dirty[p]
1023
- return h('div', { className: 'cdex-statusbar' },
1024
- h('span', null, p ? p : '未打开文件'),
1025
- h('span', null, p ? langOf(p) : ''),
1026
- h('span', null, lines ? (lines + ' 行') : ''),
1027
- h('span', null, '引用 ' + st.references.length),
1028
- dirty ? h('span', { className: 'cdex-dirty' }, '● 未保存') : null,
1029
- )
1030
- }
1031
- function ContextMenu() {
1032
- const st = useStore()
1033
- const m = st.ctxMenu
1034
- if (!m) return null
1035
- const sel = m.sel
1036
- const items = [
1037
- { icon: '📋', label: '复制', act: () => { copyText(sel.text); showToast('已复制'); closeMenu() } },
1038
- { icon: '📥', label: '粘贴到备注', act: () => { pasteToNote(sel); closeMenu() } },
1039
- { icon: '📝', label: '添加备注', act: () => { openNoteEditor(sel); closeMenu() } },
1040
- { icon: '🌐', label: '翻译', act: () => { doTranslate(sel); closeMenu() } },
1041
- { icon: '🔗', label: '引用(给 AI)', act: () => { doCite(sel); closeMenu() } },
1042
- ]
1043
- return h('div', { className: 'cdex-menu-backdrop', onClick: closeMenu },
1044
- h('div', { className: 'cdex-menu', style: { left: m.x, top: m.y }, onClick: (e) => e.stopPropagation() },
1045
- items.map((it, i) => h('div', { key: i, className: 'cdex-menu-item', onClick: it.act },
1046
- h('span', null, it.icon), h('span', null, it.label),
1047
- )),
1048
- ),
1049
- )
1050
- }
1051
- let noteTa = null
1052
- function NoteEditor() {
1053
- const st = useStore()
1054
- const ne = st.noteEditor
1055
- if (!ne) return null
1056
- const close = () => { st.noteEditor = null; emit() }
1057
- const save = async () => {
1058
- const note = noteTa ? noteTa.value : ne.note
1059
- await callHost('add-note', { path: ne.path, lineStart: ne.lineStart, lineEnd: ne.lineEnd, note })
1060
- close()
1061
- loadNotes(ne.path)
1062
- showToast('备注已保存')
1063
- }
1064
- const loc = ne.lineStart ? (' (第 ' + ne.lineStart + '-' + ne.lineEnd + ' 行)') : ''
1065
- return h('div', { className: 'cdex-modal', onClick: close },
1066
- h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
1067
- h('div', { className: 'cdex-modal-title' }, '添加备注', h('span', { style: { color: '#9d9d9d', fontWeight: 400 } }, ' — ', ne.path, loc)),
1068
- h('textarea', { className: 'cdex-textarea2', ref: (el) => { noteTa = el }, defaultValue: ne.note, placeholder: '在此输入备注内容…' }),
1069
- h('div', { className: 'cdex-modal-actions' },
1070
- h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
1071
- h('button', { className: 'cdex-btn', onClick: save }, '保存'),
1072
- ),
1073
- ),
1074
- )
1075
- }
1076
- function Workbench() {
1077
- const st = useStore()
1078
- React.useEffect(() => { refresh(); loadReferences() }, [])
1079
- if (!st.open) return null
1080
- const style = st.windowed
1081
- ? { left: st.win.x, top: st.win.y, width: st.win.w, height: st.win.h, borderRadius: '10px', border: '1px solid var(--dsw-alias-border-l2,#454545)', boxShadow: '0 18px 60px rgba(0,0,0,.45)' }
1082
- : { left: 0, top: 0, width: '100vw', height: '100vh' }
1083
- return h('div', { className: 'cdex-workbench', style: style },
1084
- h('div', { className: 'cdex-top', onPointerDown: onTopPointerDown },
1085
- h('span', { className: 'cdex-top-title', title: st.rootPath || '' }, st.rootPath ? ('AI 代码编辑器 - ' + baseName(st.rootPath)) : 'AI 代码编辑器'),
1086
- h('div', { className: 'cdex-top-spacer' }),
1087
- h('span', { className: 'cdex-top-hint' }, st.windowed ? '拖动标题栏移动 · 右下角缩放' : ''),
1088
- h('button', { className: 'cdex-btn-ghost', title: '打开文件 (Ctrl+P)', onClick: openFilePicker }, '📂'),
1089
- h('button', { className: 'cdex-btn-ghost', title: st.windowed ? '全屏' : '还原窗口', onClick: () => { st.windowed = !st.windowed; emit() } }, st.windowed ? '⛶' : '🗗'),
1090
- ),
1091
- h('div', { className: 'cdex-main' },
1092
- h(ActivityBar),
1093
- st.view === 'files' ? h(FileTree) : h(ReferencesView),
1094
- h(Editor),
1095
- ),
1096
- h(AiInput),
1097
- h(StatusBar),
1098
- st.windowed ? h('div', { className: 'cdex-resize', onPointerDown: onResizePointerDown }) : null,
1099
- st.filePickerOpen ? h(FilePicker) : null,
1100
- st.ctxMenu ? h(ContextMenu) : null,
1101
- st.noteEditor ? h(NoteEditor) : null,
1102
- st.toast ? h('div', { className: 'cdex-toast' }, st.toast.msg) : null,
1103
- )
1104
- }
1105
-
1106
- /* ============ §8 装配与 slot 注册(受「启用开关」控制,可动态挂/卸载) ============ */
1107
- // 开关存 host 端 settings(enabled 字段,持久化)。默认启用:
1108
- // get-self 失败 / 字段缺失(旧 host)时绝不隐藏编辑器,只有明确 enabled===false 才不挂载。
1109
- let unmountEditorFns = null
1110
- function mountEditor() {
1111
- if (unmountEditorFns) return
1112
- const disposers = []
1113
- disposers.push(slots.inject('conversation.session.header.actions', () => slots.register(
1114
- { name: 'conversation.session.header.actions', id: 'cdex-input-bridge', order: 9999, label: '' },
1115
- (props) => h(InputBridge, props),
1116
- )))
1117
- disposers.push(slots.inject('shell.overlay', () => {
1118
- const d1 = slots.register(
1119
- { name: 'shell.overlay', id: 'cdex-mode-tabs', order: 5, label: '界面模式' },
1120
- () => h(ModeTabs, null),
1121
- )
1122
- const d2 = slots.register(
1123
- { name: 'shell.overlay', id: 'cdex-workbench', order: 10, label: '代码编辑器' },
1124
- () => h(Workbench, null),
1125
- )
1126
- return () => { if (d1) d1(); if (d2) d2() }
1127
- }))
1128
- unmountEditorFns = () => { disposers.forEach((d) => { try { d() } catch (e) {} }) }
1129
- }
1130
- function unmountEditor() {
1131
- if (!unmountEditorFns) return
1132
- try { unmountEditorFns() } catch (e) {}
1133
- unmountEditorFns = null
1134
- }
1135
- // 卡片里切换开关 -> set-enabled 成功后调用:立即挂/卸载编辑器(无需刷新页面、无需重启 DSH)
1136
- function applyEnabled(v) {
1137
- if (v) mountEditor()
1138
- else unmountEditor()
1139
- }
1140
- // 启动时先查开关再挂载(RPC 失败 = 默认启用)
1141
- callHost('get-self').then((r) => {
1142
- applyEnabled(!(r && !r.error && r.enabled === false))
1143
- }).catch(() => applyEnabled(true))
1144
-
1145
- /* ---- 插件配置卡片(settings.plugin.item):信息 + 检查更新 / 一键更新 / 本地包更新 ---- */
1146
- function PluginConfigCard() {
1147
- const [cardOpen, setCardOpen] = React.useState(false)
1148
- const [self, setSelf] = React.useState(null)
1149
- const [chk, setChk] = React.useState(null)
1150
- const [run, setRun] = React.useState(null)
1151
- const [localPath, setLocalPath] = React.useState('')
1152
- const [enabling, setEnabling] = React.useState(false)
1153
- const lastPayloadRef = React.useRef(null)
1154
- const pollDisposeRef = React.useRef(null)
1155
-
1156
- // 编辑器开关:写 host settings 持久化;成功后立即挂/卸载编辑器 UI
1157
- function toggleEnabled() {
1158
- if (!self || enabling) return
1159
- const next = self.enabled === false
1160
- setEnabling(true)
1161
- callHost('set-enabled', { enabled: next }).then((r) => {
1162
- setEnabling(false)
1163
- if (r && !r.error) {
1164
- setSelf(Object.assign({}, self, { enabled: next }))
1165
- applyEnabled(next)
1166
- showToast(next ? '已启用编辑器(立即生效)' : '已禁用编辑器(立即生效)')
1167
- } else {
1168
- showToast('切换失败:' + ((r && r.error) || '未知错误'))
1169
- }
1170
- })
1171
- }
1172
-
1173
- function stopPoll() {
1174
- if (pollDisposeRef.current) { try { pollDisposeRef.current() } catch (e) {} pollDisposeRef.current = null }
1175
- }
1176
- React.useEffect(() => stopPoll, [])
1177
- React.useEffect(() => {
1178
- if (!cardOpen) return
1179
- if (!self) callHost('get-self').then((r) => { if (r && !r.error) setSelf(r) })
1180
- if (!chk) doCheck()
1181
- }, [cardOpen])
1182
-
1183
- function doCheck() {
1184
- setChk({ checking: true })
1185
- callHost('check-update').then((r) => {
1186
- setChk(r && !r.error ? r : { error: (r && r.error) || '检查失败,host 端可能仍是旧版本(需重启 DSH)' })
1187
- })
1188
- }
1189
-
1190
- function startUpdate(payload) {
1191
- lastPayloadRef.current = payload
1192
- setRun({ active: true, done: false, phase: 'starting', lines: [] })
1193
- callHost('perform-update', payload).then((r) => {
1194
- if (!r || r.error) {
1195
- setRun({ active: false, done: true, ok: false, error: (r && r.error) || '无法启动更新', hint: r && r.error && /未知端点/.test(r.error) ? 'host 端仍是旧版本:请先重启 DSH 再使用更新功能' : '', lines: [] })
1196
- return
1197
- }
1198
- pollProgress()
1199
- })
1200
- }
1201
- function pollProgress() {
1202
- callHost('get-update-progress').then((r) => {
1203
- if (!r || r.error) return
1204
- setRun(r)
1205
- if (r.active) {
1206
- if (timer && typeof timer.timeout === 'function') {
1207
- stopPoll()
1208
- const d = timer.timeout(pollProgress, 900)
1209
- pollDisposeRef.current = (typeof d === 'function') ? d : null
1210
- }
1211
- } else {
1212
- stopPoll()
1213
- }
1214
- })
1215
- }
1216
-
1217
- const cur = (self && self.version) || (chk && chk.current) || '?'
1218
- const started = !!(run && !run.done)
1219
- let checkLine = null
1220
- if (chk && chk.checking) checkLine = h('p', { className: 'cdx-pc-note' }, '检查中…')
1221
- else if (chk && chk.error) checkLine = h('p', { className: 'cdx-pc-err' }, chk.error)
1222
- else if (chk && chk.notPublished) checkLine = h('p', { className: 'cdx-pc-note' }, '尚未发布到 npm(registry 404)。发布后可在线检查更新;当前可用下方「本地包更新」。')
1223
- else if (chk && chk.updateAvailable) checkLine = h('p', { className: 'cdx-pc-note' }, '有新版本可用,见下方。')
1224
- else if (chk && chk.latest) checkLine = h('p', { className: 'cdx-pc-ok' }, '✓ 已是最新(v' + chk.current + ')')
1225
-
1226
- // 结构与样式逐条对齐宿主 PluginCard:li + 头部按钮(名称/描述两行 + 徽章 + 旋转 chevron) + body(上边框、左右 16px 缩进)
1227
- return h('li', { className: 'cdx-pc-card' + (cardOpen ? ' cdx-pc-card-open' : '') },
1228
- h('button', {
1229
- type: 'button',
1230
- className: 'cdx-pc-header',
1231
- 'aria-expanded': cardOpen,
1232
- 'aria-label': (cardOpen ? '收起' : '展开') + ': dsh-code-ui',
1233
- onClick: () => setCardOpen(!cardOpen),
1234
- },
1235
- h('span', { className: 'cdx-pc-headtext' },
1236
- h('span', { className: 'cdx-pc-name' }, 'dsh-code-ui'),
1237
- h('span', { className: 'cdx-pc-desc' }, 'Cursor 风格 AI 代码编辑器插件:文件树、多标签、语法高亮、引用/翻译给 AI'),
1238
- ),
1239
- cur !== '?' ? h('span', { className: 'cdx-pc-badge' }, 'v' + cur) : null,
1240
- IconChevronDown
1241
- ? h(IconChevronDown, { className: 'cdx-pc-chevron' + (cardOpen ? ' cdx-pc-chevron-open' : '') })
1242
- : h('span', { className: 'cdx-pc-chevron' + (cardOpen ? ' cdx-pc-chevron-open' : '') }, '▾'),
1243
- ),
1244
- cardOpen ? h('div', { className: 'cdx-pc-body' },
1245
- // 字段零:编辑器启用开关
1246
- h('div', { className: 'cdx-pc-field' },
1247
- h('div', { className: 'cdx-pc-head' },
1248
- h('span', { className: 'cdx-pc-label' }, '编辑器开关(页面入口 + 编辑器整体)'),
1249
- h('button', {
1250
- type: 'button',
1251
- className: 'cdx-pc-btn ' + (self && self.enabled !== false ? 'cdx-pc-btn-outline' : 'cdx-pc-btn-primary'),
1252
- disabled: !self || enabling,
1253
- onClick: toggleEnabled,
1254
- }, enabling ? '切换中…' : (self && self.enabled !== false ? '已启用 · 点击禁用' : '已禁用 · 点击启用')),
1255
- ),
1256
- h('p', { className: 'cdx-pc-note' }, '切换立即生效(实时挂载/卸载编辑器),无需重启 DSH,也无需刷新页面;若个别界面未跟着变化,普通刷新(F5)即可。开关状态持久化保存。'),
1257
- ),
1258
- // 字段一:在线检查更新
1259
- h('div', { className: 'cdx-pc-field' },
1260
- h('div', { className: 'cdx-pc-head' },
1261
- h('span', { className: 'cdx-pc-label' }, '在线检查更新'),
1262
- h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: !!(chk && chk.checking) || started, onClick: doCheck }, '检查更新'),
1263
- ),
1264
- checkLine,
1265
- ),
1266
- // 字段二:一键更新(有新版本时)
1267
- chk && chk.updateAvailable ? h('div', { className: 'cdx-pc-field' },
1268
- h('div', { className: 'cdx-pc-head' },
1269
- h('span', { className: 'cdx-pc-label' }, h('span', { className: 'cdx-pc-new' }, '🆕 有新版本:v' + chk.current + ' -> v' + chk.latest)),
1270
- h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', disabled: started, onClick: () => startUpdate({ source: 'registry', version: chk.latest }) }, '一键更新到 v' + chk.latest),
1271
- ),
1272
- ) : null,
1273
- // 字段三:本地包更新
1274
- h('div', { className: 'cdx-pc-field' },
1275
- h('div', { className: 'cdx-pc-head' },
1276
- h('span', { className: 'cdx-pc-label' }, '本地包更新(开发期 / 未发布时)'),
1277
- h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: started || !localPath.trim(), onClick: () => startUpdate({ source: 'local', path: localPath.trim() }) }, '安装本地包'),
1278
- ),
1279
- h('div', { className: 'cdx-pc-head' },
1280
- h('input', {
1281
- className: 'cdx-pc-input',
1282
- placeholder: 'D:\\...\\dsh-code-ui-1.1.2.tgz(npm pack 产出的 .tgz 绝对路径)',
1283
- value: localPath,
1284
- onChange: (e) => setLocalPath(e.target.value),
1285
- onKeyDown: (e) => { if (e.key === 'Enter' && localPath.trim() && !started) startUpdate({ source: 'local', path: localPath.trim() }) },
1286
- }),
1287
- ),
1288
- h('p', { className: 'cdx-pc-note' }, '安装走 profile 的 pnpm(与在线更新同一通道),完成后需重启 DSH 生效。'),
1289
- ),
1290
- // 进度与结果
1291
- run && ((run.lines || []).length || run.active) ? h('div', { className: 'cdx-pc-field' },
1292
- h('p', { className: 'cdx-pc-note' }, run.active ? '更新进行中…(pnpm 安装,可能需要几十秒)' : '安装输出:'),
1293
- h('div', { className: 'cdx-pc-log' }, ((run.lines || []).slice(-14).join('\n')) || '正在启动 pnpm…'),
1294
- ) : null,
1295
- run && run.done && run.ok ? h('div', { className: 'cdx-pc-field' },
1296
- h('p', { className: 'cdx-pc-ok' }, '✓ 更新完成:v' + (run.beforeVersion || '?') + ' -> v' + (run.afterVersion || '?') + ',重启 DSH 后生效'),
1297
- ) : null,
1298
- run && run.done && !run.ok ? h('div', { className: 'cdx-pc-field' },
1299
- h('p', { className: 'cdx-pc-err' }, '✗ 更新失败:' + (run.error || '未知错误')),
1300
- run.hint ? h('p', { className: 'cdx-pc-note' }, run.hint) : null,
1301
- h('div', { className: 'cdx-pc-head' },
1302
- h('span', { className: 'cdx-pc-label' }),
1303
- h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', onClick: () => { if (lastPayloadRef.current) startUpdate(lastPayloadRef.current) } }, '重试'),
1304
- ),
1305
- ) : null,
1306
- // 底部信息
1307
- h('div', { className: 'cdx-pc-field' },
1308
- self && self.profileRoot ? h('p', { className: 'cdx-pc-note' }, 'profile:' + self.profileRoot + ' · 检查源:' + (self.updateRegistry || 'https://registry.npmjs.org')) : null,
1309
- h('p', { className: 'cdx-pc-note' }, '接口:connection.rpc /dsh-code-ui;编辑器入口:页面顶部 Code UI;选中代码右键可复制/备注/翻译/引用给 AI。'),
1310
- ),
1311
- ) : null,
1312
- )
1313
- }
1314
- // 注册到「插件配置」列表(settings.plugin.item,key = dsh-code-ui)。嵌套 inject settingsScope,
1315
- // 服务不存在则卡片静默不出现(照 dshmarket)。
1316
- const cardCtx = ctx
1317
- if (typeof cardCtx.inject === 'function') {
1318
- cardCtx.inject(['settingsScope'], (scoped) => {
1319
- if (!scoped || !scoped.slots) return
1320
- scoped.slots.inject('settings.plugin.item', () => scoped.slots.register({
1321
- name: 'settings.plugin.item',
1322
- key: 'dsh-code-ui',
1323
- label: 'dsh-code-ui',
1324
- }, () => h(PluginConfigCard, null)))
1325
- })
1326
- }
1327
- // --- end original apply body ---// --- end original apply body ---
1328
- }
1329
-
1330
- exports.apply = __apply;
1331
- exports.name = "dsh-code-ui";
1332
- exports.inject = ["slots", "connection"];
1333
- return module.exports;
1334
- },
1335
- });
1
+ // Browser half of dsh-code-ui — standard bundle plugin.
2
+ // Hand-written JS, no build. Registered via __ModuleLoader__; host RPC goes
3
+ // through the shared Connection service channel "/dsh-code-ui".
4
+ window.__ModuleLoader__.load({
5
+ id: "dsh-code-ui",
6
+ factory: (require) => {
7
+ var module = { exports: {} };
8
+ var exports = module.exports;
9
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
10
+
11
+ const React = require("react");
12
+ // 宿主种子模块(与 react 同级,所有 client bundle 可 require):用和其他卡片同款的 chevron 图标
13
+ let IconChevronDown = null;
14
+ try {
15
+ const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
16
+ if (primitives && primitives.IconChevronDownOutline14) IconChevronDown = primitives.IconChevronDownOutline14;
17
+ } catch (e) {}
18
+ const CHANNEL = "/dsh-code-ui";
19
+
20
+ function __apply(ctx) {
21
+ // --- begin original apply body ---
22
+
23
+ /* ============ §1 依赖与服务 ============ */
24
+ const { rpc } = (ctx.get('connection') || {});
25
+ // connection.rpc.call 返回 DSH 的 RpcResult 封套({ ok: true, value } / { ok: false, error }),
26
+ // 协议失败不会 reject(只有传输层 HTTP 失败才 throw)。这里统一解包成「裸值」,
27
+ // 协议错误折叠为 { error: message },让下方各调用点沿用自己的 res.error 判断
28
+ // (对齐 dsh-updater 的 res.ok / res.value 消费约定)。
29
+ // 修复:迁移期版本直接读 res.entries / res.root 等字段,在封套下恒为 undefined,
30
+ // 导致 get-root / 文件树 / 路径跳转(stat-path)全部静默失效。
31
+ function callHost(endpoint, payload) {
32
+ return rpc.call(CHANNEL, endpoint, payload === undefined ? null : payload)
33
+ .then((res) => {
34
+ if (!res || typeof res !== 'object') return { error: 'RPC 返回异常' }
35
+ if (res.ok === false) {
36
+ const e = res.error || {}
37
+ return { error: e.message || e.code || 'RPC 调用失败' }
38
+ }
39
+ const v = res.value
40
+ if (v && typeof v === 'object' && v.error) return v
41
+ return v == null ? {} : v
42
+ })
43
+ .catch((err) => ({ error: (err && err.message) ? err.message : String(err) }))
44
+ }
45
+ const slots = ctx.get('slots');
46
+ if (slots === undefined) return
47
+ const timer = ctx.get('timer')
48
+ const h = React.createElement
49
+ const vw = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1200
50
+ const vh = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800
51
+
52
+ /* ============ §2 样式注入 ============ */
53
+ ctx.on('theme/change', () => { emit() })
54
+ ctx.effect(() => { const tag = document.createElement('style'); tag.setAttribute('data-plugin', 'dsh-code-ui'); tag.textContent = `
55
+ .cdex-modebar{position:fixed;z-index:10005;display:flex;gap:2px;background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l2,#454545);border-radius:20px;padding:3px;box-shadow:0 6px 20px rgba(0,0,0,.45);pointer-events:auto;user-select:none;}
56
+ .cdex-mode-grip{display:flex;align-items:center;padding:0 4px 0 8px;color:var(--dsw-alias-label-secondary,#6f6f6f);cursor:grab;font-size:14px;}
57
+ .cdex-mode-grip:active{cursor:grabbing;}
58
+ .cdex-mode-tab{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#9d9d9d);padding:6px 16px;border-radius:16px;cursor:pointer;font-size:12.5px;font-weight:500;white-space:nowrap;}
59
+ .cdex-mode-tab.active{background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);}
60
+ .cdex-mode-tab:hover:not(.active){color:var(--dsw-alias-label-primary,#ffffff);}
61
+ .cdex-workbench{position:fixed;z-index:10000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);font-family:-apple-system,'Segoe UI',system-ui,sans-serif;font-size:13px;pointer-events:auto;overflow:hidden;}
62
+ .cdex-top{display:flex;align-items:center;height:34px;background:var(--dsw-alias-bg-layer-2,#323233);padding:0 10px;gap:8px;user-select:none;cursor:move;}
63
+ .cdex-top-title{font-weight:600;color:var(--dsw-alias-label-primary,#cccccc);}
64
+ .cdex-top-spacer{flex:1;}
65
+ .cdex-top-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;}
66
+ .cdex-main{display:flex;flex:1;min-height:0;}
67
+ .cdex-activitybar{width:48px;background:var(--dsw-alias-bg-layer-1,#1b1b1f);display:flex;flex-direction:column;align-items:center;padding:6px 0;}
68
+ .cdex-abar-btn{width:40px;height:40px;display:flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--dsw-alias-label-secondary,#9d9d9d);font-size:20px;border-radius:6px;cursor:pointer;margin:2px 0;position:relative;}
69
+ .cdex-abar-btn:hover{color:var(--dsw-alias-label-primary,#ffffff);}
70
+ .cdex-abar-btn.active{color:var(--dsw-alias-label-primary,#ffffff);}
71
+ .cdex-abar-btn.active::before{content:'';position:absolute;left:0;top:8px;bottom:8px;width:2px;background:var(--cdex-accent-bg,#0f1115);border-radius:1px;}
72
+ .cdex-abar-spacer{flex:1;}
73
+ .cdex-sidebar{width:264px;background:var(--dsw-specific-sidebar-fill,#252526);display:flex;flex-direction:column;min-height:0;position:relative;}
74
+ .cdex-sb-resize{position:absolute;right:-3px;top:0;bottom:0;width:7px;cursor:col-resize;z-index:5;}
75
+ .cdex-sb-resize:hover{background:var(--dsw-alias-interactive-bg-hover,#3a3a41);}
76
+ .cdex-sidebar-head{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,#bbbbbb);}
77
+ .cdex-sidebar-head > span:first-child{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}
78
+ .cdex-sidebar-head > :last-child{flex:none;}
79
+ /* 收起后的展开把手:贴在原侧栏位置,» 一眼可见 */
80
+ .cdex-sb-expand{width:18px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--dsw-alias-label-secondary,#9d9d9d);background:var(--dsw-specific-sidebar-fill,#252526);border-right:1px solid var(--dsw-alias-border-l1,#333333);font-size:15px;user-select:none;}
81
+ .cdex-sb-expand:hover{color:var(--dsw-alias-label-primary,#d4d4d4);background:var(--dsw-alias-interactive-bg-hover,#3a3a41);}
82
+ .cdex-tree{flex:1;overflow:auto;padding:4px;}
83
+ .cdex-node{display:flex;align-items:center;gap:5px;padding:2px 6px;border-radius:4px;cursor:pointer;white-space:nowrap;color:var(--dsw-alias-label-primary,#cccccc);font-size:13px;}
84
+ .cdex-node:hover{background:var(--dsw-alias-bg-layer-1,#2a2d2e);}
85
+ .cdex-node.active{background:var(--dsw-alias-bg-layer-2,#37373d);}
86
+ .cdex-arrow{width:14px;flex:0 0 auto;color:var(--dsw-alias-label-secondary,#888888);text-align:center;font-size:11px;}
87
+ .cdex-folder{color:#dcb67a;}
88
+ .cdex-file{color:var(--dsw-alias-label-primary,#cccccc);}
89
+ .cdex-editor{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--dsw-alias-bg-base,#1e1e1e);}
90
+ .cdex-tabs{display:flex;align-items:center;height:36px;background:var(--dsw-specific-sidebar-fill,#252526);overflow-x:auto;overflow-y:hidden;}
91
+ .cdex-tab{display:flex;align-items:center;gap:6px;padding:0 10px;height:36px;border-right:1px solid var(--dsw-alias-border-l1,#1e1e1e);cursor:pointer;color:var(--dsw-alias-label-secondary,#969696);font-size:13px;white-space:nowrap;background:var(--dsw-alias-bg-layer-1,#2d2d2d);}
92
+ .cdex-tab.active{background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#ffffff);border-top:1px solid var(--cdex-accent-bg,#0f1115);}
93
+ .cdex-tab-close{border:none;background:transparent;color:var(--dsw-alias-label-secondary,#969696);cursor:pointer;font-size:12px;padding:0;}
94
+ .cdex-tab-close:hover{color:var(--dsw-alias-label-primary,#ffffff);}
95
+ .cdex-tab-actions{display:flex;align-items:center;gap:6px;padding:0 8px;}
96
+ .cdex-codearea{flex:1;display:flex;min-height:0;background:var(--dsw-alias-bg-base,#1e1e1e);}
97
+ .cdex-gutter{width:56px;flex:0 0 auto;overflow:hidden;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-secondary,#858585);text-align:right;padding:8px 0;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;user-select:none;}
98
+ .cdex-textarea{flex:1;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);border:none;outline:none;resize:none;padding:8px 14px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;overflow:auto;}
99
+ /* —— 内联语法高亮 overlay —— */
100
+ .cdex-code-layer{position:relative;flex:1;min-width:0;overflow:hidden;background:var(--dsw-alias-bg-base,#1e1e1e);}
101
+ .cdex-code-pre{position:absolute;inset:0;margin:0;padding:8px 14px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;color:var(--dsw-alias-label-primary,#d4d4d4);pointer-events:none;overflow:hidden;scrollbar-width:none;}
102
+ .cdex-code-ta{position:absolute;inset:0;width:100%;height:100%;background:transparent;color:transparent;caret-color:var(--dsw-alias-label-primary,#d4d4d4);border:none;outline:none;resize:none;padding:8px 14px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:13px;line-height:20px;white-space:pre;overflow:auto;box-sizing:border-box;}
103
+ .cdex-code-ta::selection{background:rgba(107,138,253,.35);}
104
+ .cdx-tk-kw{color:var(--cdex-tk-kw,#c586c0);font-weight:600;}
105
+ .cdx-tk-str{color:var(--cdex-tk-str,#ce9178);}
106
+ .cdx-tk-com{color:var(--cdex-tk-com,#6a9955);font-style:italic;}
107
+ .cdx-tk-num{color:var(--cdex-tk-num,#b5cea8);}
108
+ .cdx-tk-fn{color:var(--cdex-tk-fn,#dcdcaa);}
109
+ .cdx-tk-tag{color:#569cd6;}
110
+ .cdx-tk-props{color:#9cdcfe;}
111
+ .cdx-tk-attr{color:#dcdcaa;}
112
+ .cdx-tk-punc{color:var(--cdex-tk-punc,#808080);}
113
+ .cdx-tk-plain{color:var(--dsw-alias-label-primary,#d4d4d4);}
114
+ .cdex-empty{color:var(--dsw-alias-label-secondary,#6f6f6f);padding:16px;font-size:13px;}
115
+ .cdex-empty-root{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-bottom:10px;word-break:break-all;font-family:Consolas,'Cascadia Code',Menlo,monospace;}
116
+ .cdex-empty-root b{color:var(--dsw-alias-label-primary,#d4d4d4);font-weight:600;}
117
+ .cdex-error{color:var(--dsw-alias-state-error-primary,#f48771);padding:8px;font-size:12px;}
118
+ .cdex-statusbar{display:flex;align-items:center;height:24px;background:var(--dsw-alias-bg-overlay,#252526);color:var(--dsw-alias-label-secondary,#9d9d9d);font-size:12px;padding:0 12px;gap:16px;user-select:none;border-top:1px solid var(--dsw-alias-border-l1,#333333);}
119
+ .cdex-statusbar .cdex-dirty{color:var(--dsw-alias-state-warn-primary,#ffe082);font-weight:600;}
120
+ .cdex-btn{border:1px solid var(--dsw-alias-border-l2,#3e3e42);background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px;}
121
+ .cdex-btn:hover{filter:brightness(1.1);}
122
+ .cdex-btn:disabled{opacity:.5;cursor:not-allowed;}
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
+ .cdex-btn-ghost:hover{background:var(--dsw-alias-bg-layer-1,#4a4a4a);}
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);}
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
+ .cdex-menu-item:hover{background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);}
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
+ .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
+ .cdex-modal-title{font-weight:600;margin-bottom:10px;color:var(--dsw-alias-label-primary,#e8e8e8);word-break:break-all;}
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-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px;}
134
+ .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
+ .cdex-ref-head{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:12px;}
136
+ .cdex-ref-path{flex:1;color:var(--dsw-alias-label-secondary,#9d9d9d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
137
+ .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
+ .cdex-section{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:11px;text-transform:uppercase;letter-spacing:.05em;margin:12px 0 6px;}
139
+ .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;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;}
141
+ .cdex-resize{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;z-index:10;}
142
+ .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
+ .cdex-aiinput-wrap{position:relative;padding:10px 14px 16px;background:transparent;border-top:1px solid var(--dsw-alias-border-l1,#3e3e42);}
144
+ .cdex-aiinput-card{background:var(--dsw-alias-bg-overlay,#252526);border:1px solid var(--dsw-alias-border-l1,#3e3e42);border-radius:14px;box-shadow:0 2px 12px rgba(0,0,0,.18);overflow:hidden;}
145
+ .cdex-aiinput-tags{display:flex;flex-wrap:wrap;gap:6px;padding:10px 12px 0;}
146
+ .cdex-aiinput-tag{display:inline-flex;align-items:center;gap:6px;background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);border-radius:12px;padding:3px 10px 3px 12px;font-size:12px;}
147
+ .cdex-aiinput-tag-label{white-space:nowrap;}
148
+ .cdex-aiinput-tag-tr{background:#b45309;color:#fff;}
149
+ .cdex-aiinput-tag-x{border:none;background:transparent;color:var(--cdex-accent-fg,#ffffff);cursor:pointer;font-size:13px;padding:0;line-height:1;opacity:.85;}
150
+ .cdex-aiinput-tag-x:hover{opacity:1;}
151
+ .cdex-aiinput{display:flex;gap:10px;align-items:flex-end;padding:10px 12px 12px;}
152
+ .cdex-aiinput-ta{flex:1;background:transparent;color:var(--dsw-alias-label-primary,#d4d4d4);border:none;outline:none;resize:none;font-family:inherit;font-size:13px;line-height:1.55;min-height:40px;max-height:160px;padding:4px 2px;}
153
+ .cdex-aiinput-ta::placeholder{color:var(--dsw-alias-label-secondary,#8a8a8a);}
154
+ .cdex-aiinput-ta:disabled{opacity:.55;}
155
+ .cdex-aiinput-send{flex:0 0 auto;height:36px;min-width:76px;border:none;background:var(--cdex-accent-bg,#0f1115);color:var(--cdex-accent-fg,#ffffff);border-radius:999px;padding:0 18px;cursor:pointer;font-size:13px;font-weight:600;transition:filter .15s ease, opacity .15s ease;}
156
+ .cdex-aiinput-send.ghosted{opacity:.4;cursor:not-allowed;}
157
+ .cdex-aiinput-warn{margin:10px 12px 0;padding:6px 10px;border-radius:8px;background:rgba(230,162,60,.12);border:1px solid rgba(230,162,60,.4);color:#e6a23c;font-size:12px;line-height:1.5;}
158
+ .cdex-aiinput-send:hover:not(:disabled){filter:brightness(1.12);}
159
+ .cdex-aiinput-send:disabled{opacity:.4;cursor:not-allowed;}
160
+ .cdex-aiinput-foot{display:flex;align-items:center;gap:12px;padding:8px 14px 12px;color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;border-top:1px solid transparent;}
161
+ .cdex-fp-bar{display:flex;gap:6px;margin-bottom:10px;align-items:center;}
162
+ .cdex-fp-input{flex:1;box-sizing:border-box;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:6px;padding:8px 10px;font-family:inherit;font-size:13px;outline:none;}
163
+ .cdex-fp-list{max-height:420px;overflow:auto;}
164
+ .cdex-fp-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;}
165
+ .cdex-fp-item:hover{background:var(--dsw-alias-bg-layer-1,#2a2d2e);}
166
+ .cdex-fp-name{color:var(--dsw-alias-label-primary,#d4d4d4);font-size:13px;}
167
+ .cdex-fp-crumbs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-bottom:8px;min-height:22px;}
168
+ .cdex-fp-crumb{color:var(--dsw-alias-label-secondary,#8a8a8a);cursor:pointer;border:none;background:transparent;padding:0;font-size:12.5px;}
169
+ .cdex-fp-crumb:hover{color:var(--dsw-alias-label-primary,#ffffff);}
170
+ .cdex-fp-crumb-sep{color:var(--dsw-alias-label-secondary,#6f6f6f);font-size:12px;}
171
+ .cdex-fp-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px;}
172
+ .cdex-fp-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-right:auto;align-self:center;}
173
+ /* 插件配置卡片:样式逐条对齐宿主 PluginCard.module.css / fields.module.css(dsh-context 同源) */
174
+ .cdx-pc-card{list-style:none;border:1px solid var(--dsw-alias-border-l2,#3e3e42);border-radius:12px;background:var(--dsw-alias-bg-layer-3,#2d2d2d);transition:border-color .16s,background .16s;}
175
+ .cdx-pc-card:hover{border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
176
+ .cdx-pc-card-open,.cdx-pc-card-open:hover{background:var(--dsw-alias-bg-layer-2,#37373d);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
177
+ .cdx-pc-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px;}
178
+ .cdx-pc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c6ef5);outline-offset:-2px;}
179
+ .cdx-pc-headtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;}
180
+ .cdx-pc-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,#d4d4d4);}
181
+ .cdx-pc-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9d9d9d);}
182
+ .cdx-pc-badge{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,#3a3a41);color:var(--dsw-alias-label-secondary,#9d9d9d);}
183
+ .cdx-pc-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#888);transition:transform .16s;display:inline-flex;}
184
+ .cdx-pc-chevron-open{transform:rotate(180deg);}
185
+ .cdx-pc-body{border-top:1px solid var(--dsw-alias-border-l2,#3e3e42);margin:0 16px;padding-bottom:8px;}
186
+ .cdx-pc-field{display:flex;flex-direction:column;gap:6px;padding:12px 0;}
187
+ .cdx-pc-field + .cdx-pc-field{border-top:1px solid var(--dsw-alias-border-l2,#3e3e42);}
188
+ .cdx-pc-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
189
+ .cdx-pc-label{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary,#d4d4d4);}
190
+ .cdx-pc-note{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9d9d9d);word-break:break-all;}
191
+ .cdx-pc-ok{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-success,#4ec9b0));word-break:break-all;}
192
+ .cdx-pc-err{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error,#f48771);word-break:break-all;}
193
+ .cdx-pc-new{font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary,#4c6ef5);}
194
+ .cdx-pc-btn{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;}
195
+ .cdx-pc-btn-outline{border-color:var(--dsw-alias-border-l2,#3e3e42);background:none;color:var(--dsw-alias-label-secondary,#9d9d9d);}
196
+ .cdx-pc-btn-outline:hover:not(:disabled){color:var(--dsw-alias-label-primary,#d4d4d4);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
197
+ .cdx-pc-btn-primary{background:var(--dsw-alias-label-primary,#d4d4d4);color:var(--dsw-alias-bg-layer-3,#2d2d2d);}
198
+ .cdx-pc-btn:disabled{opacity:.4;cursor:default;}
199
+ .cdx-pc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c6ef5);outline-offset:1px;}
200
+ .cdx-pc-input{flex:1;min-width:220px;box-sizing:border-box;height:34px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,#3e3e42);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2d2d2d);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#d4d4d4);outline:none;}
201
+ .cdx-pc-input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4c6ef5);}
202
+ .cdx-pc-input::placeholder{color:var(--dsw-alias-label-tertiary,#777);}
203
+ .cdx-pc-log{margin:0;background:var(--dsw-alias-bg-layer-1,#252526);border:1px solid var(--dsw-alias-border-l1,#333);border-radius:8px;padding:10px 12px;font-family:Consolas,'Cascadia Code',Menlo,monospace;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,#9d9d9d);max-height:160px;overflow:auto;white-space:pre-wrap;word-break:break-all;}
204
+ /* —— 编辑器主题三模式:根节点局部重定义语义变量(仅编辑器生效,不影响 DSH 其他界面)—— */
205
+ .cdex-theme-light{--dsw-alias-bg-base:#f8f8f8;--dsw-alias-bg-layer-1:#f0f0f0;--dsw-alias-bg-layer-2:#f5f5f5;--dsw-alias-bg-layer-3:#ffffff;--dsw-alias-bg-module-platform:#eff1f3;--dsw-alias-bg-overlay:#ffffff;--dsw-alias-border-l1:#e5e5e5;--dsw-alias-border-l2:#d6d6d6;--dsw-alias-brand-primary:#0e639c;--dsw-alias-interactive-bg-hover:#e9e9e9;--dsw-alias-label-primary:#1f2328;--dsw-alias-label-secondary:#59636e;--dsw-alias-label-tertiary:#8c959f;--dsw-alias-label-dimmed:#b6bec6;--dsw-alias-label-error:#cf222e;--dsw-alias-label-success:#1a7f37;--dsw-alias-state-error-primary:#cf222e;--dsw-alias-state-success-primary:#1a7f37;--dsw-alias-state-warn-primary:#9a6700;--dsw-specific-sidebar-fill:#f0f0f0;--cdex-accent-bg:#0f1115;--cdex-accent-fg:#ffffff;--cdex-tk-kw:#af00db;--cdex-tk-str:#a31515;--cdex-tk-com:#008000;--cdex-tk-num:#098658;--cdex-tk-fn:#795e26;--cdex-tk-punc:#555555;}
206
+ .cdex-theme-dark{--dsw-alias-bg-base:#1e1e1e;--dsw-alias-bg-layer-1:#252526;--dsw-alias-bg-layer-2:#37373d;--dsw-alias-bg-layer-3:#2d2d2d;--dsw-alias-bg-module-platform:#3a3a41;--dsw-alias-bg-overlay:#252526;--dsw-alias-border-l1:#333333;--dsw-alias-border-l2:#3e3e42;--dsw-alias-brand-primary:#0e639c;--dsw-alias-interactive-bg-hover:#3a3a41;--dsw-alias-label-primary:#d4d4d4;--dsw-alias-label-secondary:#9d9d9d;--dsw-alias-label-tertiary:#8a8a8a;--dsw-alias-label-dimmed:#6a6a70;--dsw-alias-label-error:#f48771;--dsw-alias-label-success:#4ec9b0;--dsw-alias-state-error-primary:#f48771;--dsw-alias-state-success-primary:#4ec9b0;--dsw-alias-state-warn-primary:#ffe082;--dsw-specific-sidebar-fill:#252526;--cdex-accent-bg:#ffffff;--cdex-accent-fg:#0f1115;--cdex-tk-kw:#c586c0;--cdex-tk-str:#ce9178;--cdex-tk-com:#6a9955;--cdex-tk-num:#b5cea8;--cdex-tk-fn:#dcdcaa;--cdex-tk-punc:#808080;}
207
+ /* 停靠面板顶栏(复用自原生视图预览) */
208
+ .cdex-nativeview-top{display:flex;align-items:center;height:34px;flex:0 0 auto;background:var(--dsw-alias-bg-layer-2,#323233);padding:0 10px;gap:8px;user-select:none;border-bottom:1px solid var(--dsw-alias-border-l1,#333333);}
209
+ /* 右侧停靠模式:固定占右缘,壳层帧被 margin-right 推挤 */
210
+ .cdex-dock{position:fixed;top:0;right:0;bottom:0;z-index:9000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,#1e1e1e);color:var(--dsw-alias-label-primary,#d4d4d4);font-family:-apple-system,'Segoe UI',system-ui,sans-serif;font-size:13px;overflow:hidden;box-sizing:border-box;border-left:1px solid var(--dsw-alias-border-l2,#3e3e42);}
211
+ .cdex-dock .cdex-main{flex:1;min-height:0;}
212
+ .cdex-dock-grip{position:absolute;left:0;top:0;bottom:0;width:7px;cursor:col-resize;z-index:10;}
213
+ .cdex-dock-grip:hover{background:var(--dsw-alias-interactive-bg-hover,#3a3a41);}
214
+ .cdex-dock-toggle{border:1px solid var(--dsw-alias-border-l2,#3e3e42);background:var(--dsw-alias-bg-layer-2,#37373d);color:var(--dsw-alias-label-secondary,#9d9d9d);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;white-space:nowrap;font-family:inherit;}
215
+ .cdex-dock-toggle:hover{color:var(--dsw-alias-label-primary,#d4d4d4);}
216
+ .cdex-dock-toggle.active{color:var(--dsw-alias-label-primary,#d4d4d4);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
217
+ /* dsh 模式(不挂调色板)时的反色按钮与 token 色:
218
+ 反色按钮直接用 DSH 的 brand-primary(浅色=黑 / 深色=白),文字色取 bg-base(自动反色);
219
+ token 色由 tokensClass() 检测 body[data-ds-dark-theme] 后挂 .cdex-tokens-light/dark */
220
+ .cdex-workbench,.cdex-modebar{--cdex-accent-bg:var(--dsw-alias-brand-primary,#0f1115);--cdex-accent-fg:var(--dsw-alias-bg-base,#ffffff);}
221
+ .cdex-tokens-light{--cdex-tk-kw:#af00db;--cdex-tk-str:#a31515;--cdex-tk-com:#008000;--cdex-tk-num:#098658;--cdex-tk-fn:#795e26;--cdex-tk-punc:#555555;}
222
+ .cdex-tokens-dark{--cdex-tk-kw:#c586c0;--cdex-tk-str:#ce9178;--cdex-tk-com:#6a9955;--cdex-tk-num:#b5cea8;--cdex-tk-fn:#dcdcaa;--cdex-tk-punc:#808080;}
223
+ `; document.head.appendChild(tag); return () => tag.remove(); })
224
+
225
+ /* ============ §3 状态层 ============ */
226
+ const store = {
227
+ open: false,
228
+ view: 'files',
229
+ modePos: null,
230
+ windowed: true,
231
+ win: { x: 16, y: 16, w: Math.round(vw * 0.72), h: Math.round(vh * 0.72) },
232
+ // 右侧停靠模式(feature/native-layout):dockOpen 开关 + dockW 面板宽度(px)
233
+ dockOpen: false,
234
+ dockW: Math.max(380, Math.min(Math.round(vw * 0.46), vw - 480)),
235
+ sidebarCollapsed: false,
236
+ sidebarW: 264,
237
+ rootPath: null,
238
+ nodes: {},
239
+ tabs: [],
240
+ activePath: null,
241
+ buffers: {},
242
+ savedContent: {},
243
+ dirty: {},
244
+ externalChanged: {},
245
+ loadSeq: {},
246
+ activeNotes: [],
247
+ references: [],
248
+ inputActions: null,
249
+ inputDraft: '',
250
+ inputPhase: 'plain',
251
+ filePickerOpen: false,
252
+ browsePath: null,
253
+ browseEntries: [],
254
+ ctxMenu: null,
255
+ noteEditor: null,
256
+ toast: null,
257
+ error: null,
258
+ }
259
+ const listeners = new Set()
260
+ function emit() { listeners.forEach((fn) => fn()) }
261
+ function useStore() {
262
+ const [, setTick] = React.useState(0)
263
+ React.useEffect(() => {
264
+ const fn = () => setTick((t) => t + 1)
265
+ listeners.add(fn)
266
+ return () => { listeners.delete(fn) }
267
+ }, [])
268
+ return store
269
+ }
270
+
271
+ function showToast(msg) {
272
+ store.toast = { msg }
273
+ emit()
274
+ if (timer && typeof timer.timeout === 'function') {
275
+ timer.timeout(() => { store.toast = null; emit() }, 2800)
276
+ }
277
+ }
278
+
279
+ /* ============ 编辑器主题四态:dsh(默认,跟随 DSH 宿主外观) / system(跟随 OS) / light / dark ============ */
280
+ // 原理:dsh 模式不挂任何类,直接继承 DSH 的 --dsw-alias-* 变量(v1.1.7 前的原始行为);
281
+ // 其余模式在编辑器根节点挂 .cdex-theme-light / .cdex-theme-dark 类,类内局部重定义
282
+ // 语义变量(CSS 自定义属性作用域),不影响 DSH 其他界面。
283
+ // system 模式用 matchMedia 监听操作系统外观切换,实时换肤。
284
+ let themeMode = 'dsh'
285
+ let mqDispose = null
286
+ let bodyObs = null
287
+ function systemPrefersDark() {
288
+ try { return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) } catch (e) { return true }
289
+ }
290
+ // DSH 自身外观:body[data-ds-dark-theme] 由 DSH 主题系统维护
291
+ function dshPrefersDark() {
292
+ try { return !!(document.body && document.body.hasAttribute && document.body.hasAttribute('data-ds-dark-theme')) } catch (e) { return true }
293
+ }
294
+ function themeClass() {
295
+ if (themeMode === 'light') return 'cdex-theme-light'
296
+ if (themeMode === 'dark') return 'cdex-theme-dark'
297
+ if (themeMode === 'system') return systemPrefersDark() ? 'cdex-theme-dark' : 'cdex-theme-light'
298
+ return '' // dsh:继承宿主变量
299
+ }
300
+ // 语法 token 色类:dsh 模式按宿主当前外观选(palette 类自带 token 定义,无需另挂)
301
+ function tokensClass() {
302
+ if (themeMode === 'dsh') return dshPrefersDark() ? 'cdex-tokens-dark' : 'cdex-tokens-light'
303
+ return ''
304
+ }
305
+ function setThemeMode(v) {
306
+ themeMode = (v === 'light' || v === 'dark' || v === 'system') ? v : 'dsh'
307
+ try {
308
+ if (mqDispose) { mqDispose(); mqDispose = null }
309
+ if (themeMode === 'system' && window.matchMedia) {
310
+ const mq = window.matchMedia('(prefers-color-scheme: dark)')
311
+ const onChange = () => emit()
312
+ if (typeof mq.addEventListener === 'function') {
313
+ mq.addEventListener('change', onChange)
314
+ mqDispose = () => mq.removeEventListener('change', onChange)
315
+ } else if (typeof mq.addListener === 'function') {
316
+ mq.addListener(onChange)
317
+ mqDispose = () => mq.removeListener(onChange)
318
+ }
319
+ }
320
+ // dsh 模式监听宿主外观切换(token 色需要跟着翻);其余模式断开
321
+ if (bodyObs) { try { bodyObs.disconnect() } catch (e) {} bodyObs = null }
322
+ if (themeMode === 'dsh' && typeof MutationObserver !== 'undefined' && document.body) {
323
+ bodyObs = new MutationObserver(() => emit())
324
+ bodyObs.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
325
+ }
326
+ } catch (e) {}
327
+ emit()
328
+ }
329
+
330
+ /* ============ 纯工具函数 ============ */
331
+ function baseName(p) {
332
+ const parts = String(p || '').split(/[\\/]/).filter(Boolean)
333
+ return parts.length ? parts[parts.length - 1] : String(p || '')
334
+ }
335
+ function tagLabel(r) {
336
+ const name = baseName(r.path)
337
+ if (r.lineStart && r.lineEnd) {
338
+ return name + '(' + r.lineStart + '-' + r.lineEnd + ')'
339
+ }
340
+ return name
341
+ }
342
+ function parentPath(p) {
343
+ const s = String(p).replace(/[\\/]+$/, '')
344
+ const idx = Math.max(s.lastIndexOf('\\'), s.lastIndexOf('/'))
345
+ if (idx <= 0) return null
346
+ const parent = s.slice(0, idx)
347
+ if (/^[a-zA-Z]:$/.test(parent)) return parent + '\\'
348
+ return parent || null
349
+ }
350
+ function crumbsOf(p) {
351
+ if (!p) return []
352
+ const sep = String(p).includes('\\') ? '\\' : '/'
353
+ const parts = String(p).split(/[\\/]/).filter(Boolean)
354
+ const crumbs = []
355
+ let acc = ''
356
+ for (let i = 0; i < parts.length; i++) {
357
+ const part = parts[i]
358
+ if (i === 0 && /^[a-zA-Z]:$/.test(part)) {
359
+ acc = part + sep
360
+ } else {
361
+ acc = acc + part + sep
362
+ }
363
+ crumbs.push({ name: part, path: acc })
364
+ }
365
+ return crumbs
366
+ }
367
+ function langOf(p) {
368
+ const ext = String(p || '').split('.').pop().toLowerCase()
369
+ const map = { js:'javascript', jsx:'jsx', ts:'typescript', tsx:'tsx', py:'python', rs:'rust', go:'go', java:'java', c:'c', cpp:'cpp', cc:'cpp', h:'c', hpp:'cpp', css:'css', scss:'scss', html:'html', htm:'html', json:'json', md:'markdown', yml:'yaml', yaml:'yaml', sh:'shell', bash:'shell', ps1:'powershell', txt:'text', xml:'xml', sql:'sql', toml:'toml', ini:'ini' }
370
+ return map[ext] || 'text'
371
+ }
372
+ function copyText(text) {
373
+ let done = false
374
+ try {
375
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
376
+ navigator.clipboard.writeText(text).catch(() => {})
377
+ done = true
378
+ }
379
+ } catch (e) {}
380
+ if (!done) {
381
+ try {
382
+ if (typeof document !== 'undefined') {
383
+ const ta = document.createElement('textarea')
384
+ ta.value = text
385
+ ta.style.position = 'fixed'
386
+ ta.style.opacity = '0'
387
+ document.body.appendChild(ta)
388
+ ta.focus(); ta.select()
389
+ try { document.execCommand('copy') } catch (e2) {}
390
+ document.body.removeChild(ta)
391
+ }
392
+ } catch (e) {}
393
+ }
394
+ }
395
+ // —— 内联轻量语法高亮(自写 tokenizer,返回 HTML 字符串) ——
396
+ function esc(s) {
397
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
398
+ }
399
+ function span(cls, text) { return '<span class="' + cls + '">' + text + '</span>' }
400
+ function highlight(code, lang) {
401
+ if (!code) return ''
402
+ const L = String(lang || '').toLowerCase()
403
+ // 行注释前缀(按语言)
404
+ let lineComment = '//'
405
+ if (L === 'python' || L === 'py') lineComment = '#'
406
+ else if (L === 'shell' || L === 'bash' || L === 'sh') lineComment = '#'
407
+ else if (L === 'html' || L === 'xml' || L === 'css' || L === 'scss') lineComment = null
408
+ // 关键字(按语言簇)
409
+ let keywords = /^(function|return|if|else|for|while|const|let|var|new|class|extends|import|export|from|async|await|try|catch|throw|switch|case|break|continue|typeof|instanceof|this|true|false|null|undefined|interface|type|enum|declare|readonly|default|in|of|yield|do|void|static|public|private|protected|using|as|is|super|delete|void)$/
410
+ if (L === 'python' || L === 'py') keywords = /^(def|return|if|elif|else|for|while|import|from|as|class|try|except|finally|raise|with|lambda|pass|break|continue|and|or|not|in|is|True|False|None|global|nonlocal|yield|assert|del|async|await)$/
411
+ if (L === 'json') keywords = /^(true|false|null)$/
412
+ const out = []
413
+ let i = 0
414
+ const n = code.length
415
+ while (i < n) {
416
+ const ch = code[i]
417
+ // 行注释
418
+ if (lineComment && code.startsWith(lineComment, i)) {
419
+ let j = code.indexOf('\n', i)
420
+ if (j < 0) j = n
421
+ out.push(span('cdx-tk-com', esc(code.slice(i, j))))
422
+ i = j
423
+ continue
424
+ }
425
+ // 块注释
426
+ if (code.startsWith('/*', i)) {
427
+ let j = code.indexOf('*/', i)
428
+ j = j < 0 ? n : j + 2
429
+ out.push(span('cdx-tk-com', esc(code.slice(i, j))))
430
+ i = j
431
+ continue
432
+ }
433
+ // 字符串(单引号 / 双引号 / 反引号)
434
+ if ((ch === '"' || ch === '\'' || ch === '`')) {
435
+ const quote = ch
436
+ let j = i + 1
437
+ while (j < n && code[j] !== quote) {
438
+ if (code[j] === '\\') j++
439
+ j++
440
+ }
441
+ if (j < n) j++
442
+ out.push(span('cdx-tk-str', esc(code.slice(i, j))))
443
+ i = j
444
+ continue
445
+ }
446
+ // 数字
447
+ if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < n && code[i + 1] >= '0' && code[i + 1] <= '9')) {
448
+ let j = i
449
+ while (j < n && /[0-9a-fA-FxXbBoO_eE.\-]/.test(code[j])) j++
450
+ out.push(span('cdx-tk-num', esc(code.slice(i, j))))
451
+ i = j
452
+ continue
453
+ }
454
+ // HTML/XML 标签
455
+ if ((L === 'html' || L === 'xml') && ch === '<') {
456
+ if (code.startsWith('<!--', i)) {
457
+ let j = code.indexOf('-->', i)
458
+ j = j < 0 ? n : j + 3
459
+ out.push(span('cdx-tk-com', esc(code.slice(i, j))))
460
+ i = j
461
+ continue
462
+ }
463
+ let j = i + 1
464
+ const isClose = code[j] === '/'
465
+ if (isClose) j++
466
+ let tagName = ''
467
+ while (j < n && /[a-zA-Z0-9-]/.test(code[j])) { tagName += code[j]; j++ }
468
+ if (!tagName) { out.push(esc('<', ch + code.slice(i + 1, Math.min(i + 2, n)))); i++; continue }
469
+ out.push('<span class="cdx-tk-punc">&lt;</span>' + (isClose ? '<span class="cdx-tk-punc">/</span>' : '') + span('cdx-tk-tag', tagName))
470
+ // 遍历属性直到闭合
471
+ while (j < n && code[j] !== '>' && !(code[j] === '/')) {
472
+ if (/\s/.test(code[j])) { out.push(esc(code[j])); j++; continue }
473
+ let ks = j
474
+ while (j < n && code[j] !== '=' && !/\s/.test(code[j]) && code[j] !== '>' && code[j] !== '/') j++
475
+ const key = code.slice(ks, j)
476
+ if (key) out.push(span('cdx-tk-attr', key))
477
+ // 跳过 = "值"
478
+ if (code[j] === '=') {
479
+ out.push(esc('='))
480
+ j++
481
+ if (code[j] === '"' || code[j] === '\'') {
482
+ const q = code[j]; let e = j + 1
483
+ while (e < n && code[e] !== q) { if (code[e] === '\\') e++; e++ }
484
+ if (e < n) e++
485
+ out.push(span('cdx-tk-str', esc(code.slice(j, e))))
486
+ j = e
487
+ } else {
488
+ let e = j
489
+ while (e < n && !/\s/.test(code[e]) && code[e] !== '>' && code[e] !== '/') e++
490
+ out.push(span('cdx-tk-str', esc(code.slice(j, e))))
491
+ j = e
492
+ }
493
+ }
494
+ }
495
+ if (code[j] === '/') { out.push(span('cdx-tk-punc', '/')); j++ }
496
+ if (code[j] === '>') { out.push(span('cdx-tk-punc', '>')); j++ }
497
+ i = j
498
+ continue
499
+ }
500
+ // 标识符 / 关键字 / 函数
501
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
502
+ let j = i
503
+ while (j < n && /[\w$]/.test(code[j])) j++
504
+ const word = code.slice(i, j)
505
+ let cls = 'cdx-tk-plain'
506
+ const kws = String(keywords)
507
+ if (keywords.test(word)) cls = 'cdx-tk-kw'
508
+ else {
509
+ // 函数名:后跟 (
510
+ let k = j
511
+ while (k < n && /\s/.test(code[k])) k++
512
+ if (code[k] === '(') cls = 'cdx-tk-fn'
513
+ }
514
+ out.push(span(cls, word))
515
+ i = j
516
+ continue
517
+ }
518
+ // 其他(含 CSS 属性)
519
+ if (L === 'css' || L === 'scss') {
520
+ if (ch === '#') {
521
+ // # 后跟十六进制(#fff / #1e73be)整体按数字着色;修复:原实现漏掉 i 推进导致死循环
522
+ let j = i + 1
523
+ while (j < n && /[0-9a-fA-F]/.test(code[j])) j++
524
+ out.push(span('cdx-tk-num', esc(code.slice(i, j))))
525
+ i = j
526
+ } else {
527
+ out.push(esc(ch))
528
+ i++
529
+ }
530
+ }
531
+ // 标点 / 普通字符
532
+ else {
533
+ if ('(){}[]<>=!+-*/&|;:,.?$%^~'.indexOf(ch) >= 0) out.push(span('cdx-tk-punc', esc(ch)))
534
+ else out.push(esc(ch))
535
+ i++
536
+ }
537
+ }
538
+ return out.join('')
539
+ }
540
+
541
+ /* ============ §4 RPC 操作层(文件树 / 标签页 / 引用备注) ============ */
542
+ // —— 文件浏览器 ——
543
+ async function browseDir(path) {
544
+ if (!path) return
545
+ store.browsePath = path
546
+ store.browseEntries = []
547
+ store.error = null
548
+ emit()
549
+ const res = await callHost('list-dir', { path })
550
+ if (res && res.error) { store.error = res.error; emit(); return }
551
+ store.browseEntries = (res && res.entries) || []
552
+ emit()
553
+ }
554
+ function openFilePicker() {
555
+ store.filePickerOpen = true
556
+ emit()
557
+ const start = store.browsePath || store.rootPath || null
558
+ if (start) { browseDir(start); return }
559
+ callHost('get-root').then((r) => {
560
+ if (r && r.root) { browseDir(r.root) }
561
+ else { store.error = '未定位到工作区根,请在输入框粘贴目录或文件路径后回车'; emit() }
562
+ })
563
+ }
564
+ function pickFile(path) {
565
+ store.filePickerOpen = false
566
+ emit()
567
+ openFile(path)
568
+ }
569
+ async function setWorkspaceRoot(path) {
570
+ if (!path) return
571
+ const res = await callHost('set-root', { path })
572
+ if (res && res.error) { showToast('设置失败:' + res.error); return }
573
+ const newRoot = (res && res.root) ? res.root : path
574
+ store.rootPath = newRoot
575
+ store.nodes = {}
576
+ store.tabs = []
577
+ store.activePath = null
578
+ store.buffers = {}
579
+ store.savedContent = {}
580
+ store.dirty = {}
581
+ store.activeNotes = []
582
+ store.browsePath = null
583
+ store.browseEntries = []
584
+ store.nodes[newRoot] = { name: baseName(newRoot), path: newRoot, isDir: true, children: [], expanded: true, loaded: false }
585
+ store.filePickerOpen = false
586
+ emit()
587
+ await toggleExpand(newRoot)
588
+ showToast('工作区根目录:' + newRoot)
589
+ }
590
+ // —— 文件树 ——
591
+ async function refresh() {
592
+ store.error = null
593
+ if (!store.rootPath) {
594
+ const r = await callHost('get-root')
595
+ store.rootPath = (r && r.root) || null
596
+ if (!store.rootPath) { store.error = '未找到工作区根目录'; emit(); return }
597
+ store.nodes = {}
598
+ store.nodes[store.rootPath] = { name: baseName(store.rootPath), path: store.rootPath, isDir: true, children: [], expanded: true, loaded: false }
599
+ emit()
600
+ await toggleExpand(store.rootPath)
601
+ } else {
602
+ const node = store.nodes[store.rootPath]
603
+ if (node) { node.loaded = false; node.children = []; node.expanded = true }
604
+ await toggleExpand(store.rootPath)
605
+ }
606
+ }
607
+ async function toggleExpand(path) {
608
+ const node = store.nodes[path]
609
+ if (!node) return
610
+ if (node.expanded) { node.expanded = false; emit(); return }
611
+ node.expanded = true
612
+ if (!node.loaded) {
613
+ node.loading = true; emit()
614
+ const res = await callHost('list-dir', { path })
615
+ node.loading = false; node.loaded = true
616
+ if (res && res.error) { store.error = res.error; emit(); return }
617
+ const entries = (res && res.entries) || []
618
+ node.children = entries.map((e) => { store.nodes[e.path] = e; return e.path })
619
+ emit()
620
+ } else { emit() }
621
+ }
622
+ // —— 标签页 / 编辑 / 保存 ——
623
+ async function openFile(path) {
624
+ const name = baseName(path)
625
+ if (!store.tabs.some((t) => t.path === path)) {
626
+ store.tabs.push({ path, name })
627
+ }
628
+ store.activePath = path
629
+ store.error = null
630
+ if (!(path in store.buffers)) {
631
+ store.buffers[path] = ''
632
+ store.savedContent[path] = ''
633
+ store.dirty[path] = false
634
+ emit()
635
+ const res = await callHost('read-file', { path })
636
+ if (res && res.error) { store.error = res.error; emit(); return }
637
+ store.buffers[path] = res.content
638
+ store.savedContent[path] = res.content
639
+ store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
640
+ store.externalChanged[path] = false
641
+ emit()
642
+ // 通知 host 建 fs.watch:外部修改进事件队列,编辑器 1 秒内自动同步
643
+ callHost('watch-file', { path }).then(() => {})
644
+ } else {
645
+ emit()
646
+ }
647
+ loadNotes(path)
648
+ }
649
+ function closeTab(path) {
650
+ const i = store.tabs.findIndex((t) => t.path === path)
651
+ if (i < 0) return
652
+ store.tabs.splice(i, 1)
653
+ if (store.activePath === path) {
654
+ const next = store.tabs[i] || store.tabs[i - 1] || null
655
+ store.activePath = next ? next.path : null
656
+ if (next) { loadNotes(next.path) } else { store.activeNotes = [] }
657
+ }
658
+ emit()
659
+ // 标签关闭:释放 host 端 watcher
660
+ callHost('unwatch-file', { path }).then(() => {})
661
+ }
662
+ // 外部修改同步:从磁盘重读单文件(脏缓冲不被自动覆盖,只有用户点 ⚡ 或确认后才走这里)
663
+ async function reloadFromFile(path) {
664
+ const res = await callHost('read-file', { path })
665
+ if (res && res.error) { showToast('读取失败:' + res.error); return }
666
+ store.buffers[path] = res.content
667
+ store.savedContent[path] = res.content
668
+ store.dirty[path] = false
669
+ store.externalChanged[path] = false
670
+ store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
671
+ emit()
672
+ }
673
+ // 每秒拉一次 host 的事件队列(fs.watch 真事件;无变化时返回空数组,开销极小)
674
+ async function drainFileEvents() {
675
+ if (store.tabs.length === 0) return
676
+ const res = await callHost('take-file-events')
677
+ if (!res || res.error) return
678
+ const events = res.events || []
679
+ for (let i = 0; i < events.length; i++) {
680
+ const p = events[i] && events[i].path
681
+ if (!p || !store.tabs.some((t) => t.path === p)) continue
682
+ if (store.dirty[p]) {
683
+ // 有未保存的本地修改:不覆盖,标记 ⚡ 等用户决定
684
+ if (!store.externalChanged[p]) { store.externalChanged[p] = true; emit() }
685
+ } else {
686
+ await reloadFromFile(p)
687
+ showToast('已同步外部修改:' + baseName(p))
688
+ }
689
+ }
690
+ }
691
+ async function saveFile(path) {
692
+ const p = path || store.activePath
693
+ if (!p) return
694
+ const content = store.buffers[p]
695
+ const res = await callHost('write-file', { path: p, content })
696
+ if (res && res.error) { showToast('保存失败:' + res.error); return }
697
+ store.savedContent[p] = content
698
+ store.dirty[p] = false
699
+ emit()
700
+ showToast('已保存 ' + baseName(p))
701
+ }
702
+ // —— 引用 / 备注 读取 ——
703
+ async function loadNotes(path) {
704
+ const res = await callHost('list-notes', { path })
705
+ if (res && res.notes) { store.activeNotes = res.notes; emit() }
706
+ }
707
+ async function loadReferences() {
708
+ const res = await callHost('list-references')
709
+ if (res && res.references) { store.references = res.references; emit() }
710
+ }
711
+
712
+ /* ============ §5 Actions 层(右键菜单 / 选区) ============ */
713
+ function onEditorContext(e) {
714
+ const ta = e.target
715
+ const start = ta.selectionStart, end = ta.selectionEnd
716
+ if (start === end) return
717
+ e.preventDefault()
718
+ const full = ta.value
719
+ const text = full.slice(start, end)
720
+ const lineStart = full.slice(0, start).split('\n').length
721
+ const lineEnd = full.slice(0, end).split('\n').length
722
+ store.ctxMenu = { x: e.clientX, y: e.clientY, sel: { text, lineStart, lineEnd, path: store.activePath, language: langOf(store.activePath) } }
723
+ emit()
724
+ }
725
+ function closeMenu() { store.ctxMenu = null; emit() }
726
+ async function doCite(sel) {
727
+ const ref = await callHost('add-reference', { path: sel.path, language: sel.language, lineStart: sel.lineStart, lineEnd: sel.lineEnd, text: sel.text, kind: 'cite' })
728
+ if (ref && ref.reference) { store.references.push(ref.reference); emit() }
729
+ showToast('已引用:' + baseName(sel.path))
730
+ }
731
+ async function doTranslate(sel) {
732
+ const ref = await callHost('add-reference', { path: sel.path, language: sel.language, lineStart: sel.lineStart, lineEnd: sel.lineEnd, text: sel.text, kind: 'translate' })
733
+ if (ref && ref.reference) { store.references.push(ref.reference); emit() }
734
+ showToast('已创建翻译引用(输入框上方已加 🌐 标签)。对 AI 说「翻译我引用的内容」即可。')
735
+ }
736
+ function openNoteEditor(sel) {
737
+ store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: '' }
738
+ emit()
739
+ }
740
+ async function pasteToNote(sel) {
741
+ let clip = ''
742
+ try {
743
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.readText) {
744
+ clip = await navigator.clipboard.readText()
745
+ }
746
+ } catch (e) {}
747
+ store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: clip }
748
+ emit()
749
+ showToast(clip ? '已粘贴到备注框,点击「保存」完成。' : '无法读取剪贴板,请在备注框内按 Ctrl+V 粘贴。')
750
+ }
751
+ async function delReference(id) {
752
+ await callHost('delete-reference', { id })
753
+ store.references = store.references.filter((r) => r.id !== id)
754
+ emit()
755
+ }
756
+ async function delNote(id) {
757
+ await callHost('delete-note', { id })
758
+ store.activeNotes = store.activeNotes.filter((n) => n.id !== id)
759
+ emit()
760
+ }
761
+ // —— 引用自动附带(AI 上下文) ——
762
+ function buildSendPayload(draft) {
763
+ // 引用文本不再粘贴进消息(明面上不展示):引用统一存 host 端,AI 通过 get_referenced_snippets
764
+ // 工具读取;消息里只附一行「标签级」指针,让 AI 知道有引用可读。
765
+ const text = draft || ''
766
+ const citeRefs = store.references.filter((r) => r.kind === 'cite')
767
+ const trRefs = store.references.filter((r) => r.kind === 'translate')
768
+ const parts = []
769
+ if (citeRefs.length > 0) parts.push(citeRefs.length + ' 段代码引用')
770
+ if (trRefs.length > 0) parts.push(trRefs.length + ' 条翻译请求')
771
+ if (parts.length === 0) return text
772
+ const sep = text.trim() ? '\n\n' : ''
773
+ return text + sep + '[code UI] 已标记 ' + parts.join('、')
774
+ + '(引用内容未粘贴在消息里,请调用 get_referenced_snippets 工具读取)。'
775
+ }
776
+
777
+ /* ============ §6 Pointer 处理层(窗口拖拽 / 缩放 / 模式条移动) ============ */
778
+ function onBarPointerDown(e) {
779
+ if (e.target && typeof e.target.closest === 'function' && e.target.closest('button')) return
780
+ e.preventDefault()
781
+ const bar = e.currentTarget
782
+ const rect = bar.getBoundingClientRect()
783
+ const startX = e.clientX, startY = e.clientY
784
+ const baseX = store.modePos ? store.modePos.x : rect.left
785
+ const baseY = store.modePos ? store.modePos.y : rect.top
786
+ try { bar.setPointerCapture(e.pointerId) } catch (err) {}
787
+ function move(ev) {
788
+ store.modePos = { x: Math.max(0, baseX + (ev.clientX - startX)), y: Math.max(0, baseY + (ev.clientY - startY)) }
789
+ emit()
790
+ }
791
+ function up() {
792
+ bar.removeEventListener('pointermove', move)
793
+ bar.removeEventListener('pointerup', up)
794
+ bar.removeEventListener('pointercancel', up)
795
+ }
796
+ bar.addEventListener('pointermove', move)
797
+ bar.addEventListener('pointerup', up)
798
+ bar.addEventListener('pointercancel', up)
799
+ }
800
+ function onTopPointerDown(e) {
801
+ if (!store.windowed) return
802
+ if (e.target && typeof e.target.closest === 'function' && e.target.closest('button')) return
803
+ e.preventDefault()
804
+ const top = e.currentTarget
805
+ const sx = store.win.x, sy = store.win.y
806
+ const px = e.clientX, py = e.clientY
807
+ try { top.setPointerCapture(e.pointerId) } catch (err) {}
808
+ function move(ev) {
809
+ store.win.x = Math.max(0, sx + (ev.clientX - px))
810
+ store.win.y = Math.max(0, sy + (ev.clientY - py))
811
+ emit()
812
+ }
813
+ function up() {
814
+ top.removeEventListener('pointermove', move)
815
+ top.removeEventListener('pointerup', up)
816
+ top.removeEventListener('pointercancel', up)
817
+ }
818
+ top.addEventListener('pointermove', move)
819
+ top.addEventListener('pointerup', up)
820
+ top.addEventListener('pointercancel', up)
821
+ }
822
+ function onResizePointerDown(e) {
823
+ if (!store.windowed) return
824
+ e.preventDefault()
825
+ const handle = e.currentTarget
826
+ const sw = store.win.w, sh = store.win.h
827
+ const px = e.clientX, py = e.clientY
828
+ try { handle.setPointerCapture(e.pointerId) } catch (err) {}
829
+ function move(ev) {
830
+ store.win.w = Math.max(360, sw + (ev.clientX - px))
831
+ store.win.h = Math.max(260, sh + (ev.clientY - py))
832
+ emit()
833
+ }
834
+ function up() {
835
+ handle.removeEventListener('pointermove', move)
836
+ handle.removeEventListener('pointerup', up)
837
+ handle.removeEventListener('pointercancel', up)
838
+ }
839
+ handle.addEventListener('pointermove', move)
840
+ handle.addEventListener('pointerup', up)
841
+ handle.addEventListener('pointercancel', up)
842
+ }
843
+
844
+ /* ============ §7 组件层(V.* 命名空间) ============ */
845
+ function InputBridge(props) {
846
+ const useInput = props.useInput
847
+ const inputState = typeof useInput === 'function' ? useInput((s) => s) : undefined
848
+ const inputActions = props.inputActions
849
+ React.useEffect(() => {
850
+ store.inputActions = inputActions || null
851
+ if (inputState) {
852
+ store.inputDraft = inputState.draft
853
+ store.inputPhase = inputState.phase
854
+ }
855
+ emit()
856
+ })
857
+ return null
858
+ }
859
+ function AiInput() {
860
+ const st = useStore()
861
+ let taEl = null
862
+ const disabled = !st.inputActions
863
+ // 引用与翻译请求都只显示「标签」:内容不进输入框/消息正文,AI 经 get_referenced_snippets 工具读取
864
+ const refs = st.references
865
+ const citeCount = refs.filter((r) => r.kind === 'cite').length
866
+ const trCount = refs.length - citeCount
867
+ const submit = () => {
868
+ if (!st.inputActions || !taEl) {
869
+ // 无会话时明确告知原因,而不是无声无息
870
+ if (!st.inputActions) showToast('请先打开一个会话再发送:在主界面新建或选择会话后,这里即可发送')
871
+ return
872
+ }
873
+ const text = taEl.value
874
+ if (!text.trim() && refs.length === 0) return
875
+ const payload = buildSendPayload(text)
876
+ st.inputActions.setDraft(payload)
877
+ st.inputActions.submit()
878
+ taEl.value = ''
879
+ }
880
+ return h('div', { className: 'cdex-aiinput-wrap' },
881
+ h('div', { className: 'cdex-aiinput-card' },
882
+ disabled ? h('div', { className: 'cdex-aiinput-warn' }, '⚠ 未打开会话:请先在主界面新建或选择一个会话,之后即可在此发送(引用标签会保留)') : null,
883
+ refs.length > 0 ? h('div', { className: 'cdex-aiinput-tags' },
884
+ refs.map((r) => h('span', { className: 'cdex-aiinput-tag' + (r.kind === 'translate' ? ' cdex-aiinput-tag-tr' : ''), key: r.id },
885
+ h('span', { className: 'cdex-aiinput-tag-label' }, (r.kind === 'translate' ? '🌐 翻译 ' : '🔗 ') + tagLabel(r)),
886
+ h('button', { className: 'cdex-aiinput-tag-x', title: r.kind === 'translate' ? '删除翻译引用' : '删除引用', onClick: () => delReference(r.id) }, '✕'),
887
+ )),
888
+ ) : null,
889
+ h('div', { className: 'cdex-aiinput' },
890
+ h('textarea', {
891
+ className: 'cdex-aiinput-ta',
892
+ ref: (el) => { taEl = el },
893
+ placeholder: disabled ? '请先打开一个会话(在主界面新建或选择会话后即可发送)' : '给 AI 发消息…(Ctrl+Enter 发送)',
894
+ disabled: disabled,
895
+ onKeyDown: (e) => {
896
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
897
+ e.preventDefault()
898
+ submit()
899
+ }
900
+ },
901
+ }),
902
+ // 不用 HTML disabled(收不到点击):置灰样式 + 点击弹原因,点了就有反馈
903
+ h('button', {
904
+ className: 'cdex-aiinput-send' + (disabled ? ' ghosted' : ''),
905
+ 'aria-disabled': disabled,
906
+ title: disabled ? '请先打开一个会话' : '发送(Ctrl+Enter)',
907
+ onClick: () => {
908
+ if (disabled) { showToast('请先打开一个会话再发送:在主界面新建或选择会话后,这里即可发送'); return }
909
+ submit()
910
+ },
911
+ }, '发送'),
912
+ ),
913
+ h('div', { className: 'cdex-aiinput-foot' },
914
+ h('span', null, 'Ctrl+Enter 发送'),
915
+ h('span', null, 'Shift+Enter 换行'),
916
+ citeCount + trCount > 0 ? h('span', null, '已引用 ' + citeCount + ' 段代码 · ' + trCount + ' 条翻译(AI 经工具读取,不占消息正文)') : null,
917
+ ),
918
+ ),
919
+ )
920
+ }
921
+ function FilePicker() {
922
+ const st = useStore()
923
+ if (!st.filePickerOpen) return null
924
+ const dirs = st.browseEntries.filter((x) => x.isDir)
925
+ const files = st.browseEntries.filter((x) => !x.isDir)
926
+ const crumbs = crumbsOf(st.browsePath)
927
+ const close = () => { st.filePickerOpen = false; emit() }
928
+ const goUp = () => { if (st.browsePath) { const p = parentPath(st.browsePath); if (p) browseDir(p) } }
929
+ const goTo = (e) => {
930
+ if (e.key === 'Enter') {
931
+ const raw = e.target.value
932
+ const p = raw && raw.trim()
933
+ if (!p) return
934
+ callHost('stat-path', { path: p }).then((res) => {
935
+ if (!res || res.error) { showToast('无法访问该路径:' + ((res && res.error) || '未知错误')); return }
936
+ if (res.isDir) { browseDir(res.path || p) }
937
+ else if (res.isFile) { pickFile(res.path || p) }
938
+ // 兼容回退:宿主 isDir 字段缺失但路径确实存在时,按目录尝试浏览(list-dir 对非目录会给出真实错误)
939
+ else if (res.exists) { browseDir(p) }
940
+ else { showToast('路径不存在或不可用:' + p) }
941
+ })
942
+ }
943
+ }
944
+ // 当前浏览到的文件夹;未浏览时回退到已识别的工作区根,保证按钮不无谓置灰
945
+ const curDir = st.browsePath || st.rootPath
946
+ const confirm = () => { if (curDir) setWorkspaceRoot(curDir) }
947
+ return h('div', { className: 'cdex-modal', onClick: close },
948
+ h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
949
+ h('div', { className: 'cdex-modal-title' }, '打开文件'),
950
+ h('div', { className: 'cdex-fp-crumbs' },
951
+ crumbs.map((c, i) => h('span', { key: i },
952
+ h('button', { className: 'cdex-fp-crumb', onClick: () => browseDir(c.path) }, c.name),
953
+ i < crumbs.length - 1 ? h('span', { className: 'cdex-fp-crumb-sep' }, '›') : null,
954
+ )),
955
+ ),
956
+ h('div', { className: 'cdex-fp-bar' },
957
+ h('button', { className: 'cdex-btn-ghost', onClick: goUp, title: '上级目录' }, '↑'),
958
+ h('input', { className: 'cdex-fp-input', defaultValue: curDir || '', placeholder: '输入绝对路径后回车跳转', onKeyDown: goTo }),
959
+ ),
960
+ h('div', { className: 'cdex-fp-list' },
961
+ dirs.map((d) => h('div', { className: 'cdex-fp-item', key: d.path, onClick: () => browseDir(d.path) },
962
+ h('span', { className: 'cdex-fp-name' }, '📁 ' + d.name),
963
+ )),
964
+ files.map((f) => h('div', { className: 'cdex-fp-item', key: f.path, onClick: () => pickFile(f.path) },
965
+ h('span', { className: 'cdex-fp-name' }, '📄 ' + f.name),
966
+ )),
967
+ dirs.length === 0 && files.length === 0 ? h('div', { className: 'cdex-empty' }, st.error ? String(st.error) : '空目录') : null,
968
+ ),
969
+ h('div', { className: 'cdex-fp-actions' },
970
+ h('span', { className: 'cdex-fp-hint' }, '浏览到目标文件夹后,点「以此文件夹为根」'),
971
+ h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
972
+ h('button', { className: 'cdex-btn', onClick: confirm, disabled: !curDir }, '以此文件夹为根'),
973
+ ),
974
+ ),
975
+ )
976
+ }
977
+ function ModeTabs() {
978
+ const st = useStore()
979
+ const style = st.modePos ? { top: st.modePos.y, left: st.modePos.x } : { top: 10, left: '50%', transform: 'translateX(-50%)' }
980
+ return h('div', { className: 'cdex-modebar ' + themeClass() + ' ' + tokensClass(), style: style, onPointerDown: onBarPointerDown },
981
+ h('span', { className: 'cdex-mode-grip', title: '拖拽移动' }, '⠿'),
982
+ h('button', { className: 'cdex-mode-tab' + (st.open ? ' active' : ''), onClick: () => { if (!st.open) toggleUiMode('codeui') } }, 'code UI'),
983
+ h('button', { className: 'cdex-mode-tab' + (!st.open ? ' active' : ''), onClick: () => { if (st.open) toggleUiMode('native') } }, 'dsh UI'),
984
+ )
985
+ }
986
+ function ActivityBar() {
987
+ const st = useStore()
988
+ return h('div', { className: 'cdex-activitybar' },
989
+ h('button', { className: 'cdex-abar-btn' + (st.view === 'files' ? ' active' : ''), title: st.sidebarCollapsed ? '展开资源管理器' : '资源管理器(再点一次收起)', onClick: () => { if (st.sidebarCollapsed) { st.sidebarCollapsed = false; st.view = 'files' } else if (st.view === 'files') { st.sidebarCollapsed = true } else { st.view = 'files' } emit() } }, '📁'),
990
+ h('button', { className: 'cdex-abar-btn' + (st.view === 'refs' ? ' active' : ''), title: '引用 / 备注', onClick: () => { st.view = 'refs'; emit() } }, '📎'),
991
+ h('div', { className: 'cdex-abar-spacer' }),
992
+ h('button', { className: 'cdex-abar-btn', title: '帮助', onClick: () => showToast('选中代码右键可:复制 / 粘贴 / 备注 / 翻译 / 引用给 AI。Ctrl+S 保存。Ctrl+P 打开文件。') }, 'ⓘ'),
993
+ h('button', { className: 'cdex-abar-btn', title: '退出编辑器(切回dsh UI)', onClick: () => toggleUiMode('native') }, '↩'),
994
+ )
995
+ }
996
+ function TreeNode(props) {
997
+ const st = useStore()
998
+ const node = st.nodes[props.path]
999
+ if (!node) return null
1000
+ const style = { paddingLeft: (6 + props.depth * 14) + 'px' }
1001
+ const cls = 'cdex-node' + (st.activePath === node.path ? ' active' : '')
1002
+ const children = node.isDir && node.expanded ? (node.children || []).map((c) => h(TreeNode, { key: c, path: c, depth: props.depth + 1 })) : null
1003
+ return h('div', null,
1004
+ h('div', { className: cls, style: style, onClick: () => { node.isDir ? toggleExpand(node.path) : openFile(node.path) } },
1005
+ node.isDir ? h('span', { className: 'cdex-arrow' }, node.expanded ? '▾' : '▸') : h('span', { className: 'cdex-arrow' }, ''),
1006
+ h('span', { className: node.isDir ? 'cdex-folder' : 'cdex-file' }, node.isDir ? '📁 ' : '📄 ', node.name),
1007
+ ),
1008
+ children,
1009
+ )
1010
+ }
1011
+ function FileTree() {
1012
+ const st = useStore()
1013
+ return h('div', { className: 'cdex-sidebar', style: { width: st.sidebarW } },
1014
+ h('div', { className: 'cdex-sb-resize', title: '拖拽调整宽度(拖到最窄松手即收起)', onPointerDown: onSbResizeDown }),
1015
+ h('div', { className: 'cdex-sidebar-head' },
1016
+ h('span', { title: st.rootPath || '' }, st.rootPath ? ('资源管理器 · ' + baseName(st.rootPath)) : '资源管理器'),
1017
+ h('span', null,
1018
+ h('button', { className: 'cdex-btn-ghost', onClick: refresh, title: '刷新' }, '⟳'),
1019
+ h('button', { className: 'cdex-btn-ghost', onClick: () => { store.sidebarCollapsed = true; emit() }, title: '收起侧栏' }, '«'),
1020
+ ),
1021
+ ),
1022
+ h('div', { className: 'cdex-tree' },
1023
+ st.error ? h('div', { className: 'cdex-error' }, String(st.error)) : null,
1024
+ st.rootPath ? h(TreeNode, { path: st.rootPath, depth: 0 }) : h('div', { className: 'cdex-empty' }, '正在加载目录…'),
1025
+ ),
1026
+ )
1027
+ }
1028
+ function RefItem(r) {
1029
+ return h('div', { className: 'cdex-ref', key: r.id },
1030
+ h('div', { className: 'cdex-ref-head' },
1031
+ h('span', { className: 'cdex-ref-path' }, r.path, r.lineStart ? (' :' + r.lineStart + '-' + r.lineEnd) : ''),
1032
+ h('button', { className: 'cdex-btn-ghost', onClick: () => delReference(r.id) }, '✕'),
1033
+ ),
1034
+ h('div', { className: 'cdex-ref-code' }, r.text.slice(0, 500)),
1035
+ )
1036
+ }
1037
+ function ReferencesView() {
1038
+ const st = useStore()
1039
+ const citeList = st.references.filter((r) => r.kind === 'cite')
1040
+ const trList = st.references.filter((r) => r.kind === 'translate')
1041
+ return h('div', { className: 'cdex-sidebar', style: { width: st.sidebarW } },
1042
+ h('div', { className: 'cdex-sb-resize', title: '拖拽调整宽度(拖到最窄松手即收起)', onPointerDown: onSbResizeDown }),
1043
+ h('div', { className: 'cdex-sidebar-head' },
1044
+ h('span', null, '引用 / 备注'),
1045
+ h('button', { className: 'cdex-btn-ghost', onClick: () => { store.sidebarCollapsed = true; emit() }, title: '收起侧栏' }, '«'),
1046
+ ),
1047
+ h('div', { className: 'cdex-tree' },
1048
+ h('div', { className: 'cdex-section' }, '🔗 代码引用 (' + citeList.length + ')'),
1049
+ citeList.length === 0 ? h('div', { className: 'cdex-empty' }, '选中代码后右键「引用」。') : citeList.map((r) => RefItem(r)),
1050
+ h('div', { className: 'cdex-section' }, '🌐 翻译请求 (' + trList.length + ')'),
1051
+ trList.length === 0 ? h('div', { className: 'cdex-empty' }, '选中代码后右键「翻译」。') : trList.map((r) => RefItem(r)),
1052
+ h('div', { className: 'cdex-section' }, '📝 备注 (' + st.activeNotes.length + ')'),
1053
+ st.activeNotes.length === 0 ? h('div', { className: 'cdex-empty' }, '当前文件暂无备注。') : st.activeNotes.map((n) => h('div', { className: 'cdex-ref', key: n.id },
1054
+ h('div', { className: 'cdex-ref-head' },
1055
+ h('span', { className: 'cdex-ref-path' }, n.path, n.lineStart ? (' :' + n.lineStart + '-' + n.lineEnd) : ''),
1056
+ h('button', { className: 'cdex-btn-ghost', onClick: () => delNote(n.id) }, '✕'),
1057
+ ),
1058
+ h('div', { className: 'cdex-ref-code' }, n.note),
1059
+ )),
1060
+ h('div', { className: 'cdex-hint' }, '在输入框对 AI 说「读取/翻译我引用的代码」,AI 会调用 get_referenced_snippets 工具获取内容。'),
1061
+ ),
1062
+ )
1063
+ }
1064
+ let gutterEl = null
1065
+ function syncGutter(ta) {
1066
+ if (gutterEl) gutterEl.scrollTop = ta.scrollTop
1067
+ }
1068
+ function TabBar() {
1069
+ const st = useStore()
1070
+ return h('div', { className: 'cdex-tabs' },
1071
+ st.tabs.map((t) => h('div', { key: t.path, className: 'cdex-tab' + (st.activePath === t.path ? ' active' : ''), onClick: () => { st.activePath = t.path; loadNotes(t.path); emit() } },
1072
+ h('span', null, (st.externalChanged[t.path] ? '⚡' : '') + t.name + (st.dirty[t.path] ? ' ●' : '')),
1073
+ st.externalChanged[t.path] ? h('button', { className: 'cdex-tab-close', title: '文件已在磁盘上被外部修改,点击加载新内容(会覆盖本地未保存的修改)', onClick: (e) => { e.stopPropagation(); reloadFromFile(t.path) } }, '⇩') : null,
1074
+ h('button', { className: 'cdex-tab-close', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '✕'),
1075
+ )),
1076
+ h('div', { className: 'cdex-tab-actions' },
1077
+ h('button', { className: 'cdex-btn', title: '保存 (Ctrl+S)', onClick: () => saveFile() }, '保存'),
1078
+ ),
1079
+ )
1080
+ }
1081
+ function Editor() {
1082
+ const st = useStore()
1083
+ if (!st.activePath) {
1084
+ // 空态时显式展示当前工作区根,避免「以此文件夹为根」后误认为未切换
1085
+ return h('div', { className: 'cdex-editor' },
1086
+ h('div', { className: 'cdex-empty' },
1087
+ st.rootPath ? h('div', { className: 'cdex-empty-root' }, '当前工作区:', h('b', null, st.rootPath)) : null,
1088
+ '从左侧资源管理器打开文件,或 ',
1089
+ h('button', { className: 'cdex-btn', onClick: openFilePicker }, '📂 打开文件'),
1090
+ ),
1091
+ )
1092
+ }
1093
+ const p = st.activePath
1094
+ const content = st.buffers[p] != null ? st.buffers[p] : ''
1095
+ const lineCount = content.split('\n').length
1096
+ const gutter = []
1097
+ for (let i = 1; i <= lineCount; i++) gutter.push(String(i))
1098
+ // 高亮 HTML(overlay 下层展示)
1099
+ const html = highlight(content, langOf(p))
1100
+ // 滚动同步:把 pre 滚动到与 textarea 相同
1101
+ const syncOverlayScroll = (taEl) => {
1102
+ if (!taEl) return
1103
+ const preEl = taEl.parentNode ? taEl.parentNode.querySelector('.cdex-code-pre') : null
1104
+ const gutEl = taEl.parentNode ? taEl.parentNode.parentNode.querySelector('.cdex-gutter') : null
1105
+ if (preEl) { preEl.scrollTop = taEl.scrollTop; preEl.scrollLeft = taEl.scrollLeft }
1106
+ if (gutterEl) gutterEl.scrollTop = taEl.scrollTop
1107
+ }
1108
+ return h('div', { className: 'cdex-editor' },
1109
+ h(TabBar),
1110
+ h('div', { className: 'cdex-codearea' },
1111
+ h('div', { className: 'cdex-gutter', ref: (el) => { gutterEl = el } }, gutter.join('\n')),
1112
+ h('div', { className: 'cdex-code-layer' },
1113
+ h('pre', { className: 'cdex-code-pre', 'aria-hidden': true, dangerouslySetInnerHTML: { __html: html } }),
1114
+ h('textarea', {
1115
+ // key 拼上加载序号:内容异步读回后 key 变化触发重挂载,defaultValue 才会生效(否则编辑区空白、与高亮层脱节)
1116
+ key: p + ':' + (st.loadSeq[p] || 0),
1117
+ className: 'cdex-code-ta',
1118
+ defaultValue: content,
1119
+ spellCheck: false,
1120
+ wrap: 'off',
1121
+ onScroll: (e) => syncOverlayScroll(e.target),
1122
+ onContextMenu: onEditorContext,
1123
+ onChange: (e) => {
1124
+ store.buffers[p] = e.target.value
1125
+ store.dirty[p] = e.target.value !== store.savedContent[p]
1126
+ emit()
1127
+ },
1128
+ onKeyDown: (e) => {
1129
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'p' || e.key === 'P')) {
1130
+ e.preventDefault()
1131
+ openFilePicker()
1132
+ }
1133
+ if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
1134
+ e.preventDefault()
1135
+ saveFile()
1136
+ }
1137
+ },
1138
+ }),
1139
+ ),
1140
+ ),
1141
+ )
1142
+ }
1143
+ function StatusBar() {
1144
+ const st = useStore()
1145
+ const p = st.activePath
1146
+ const content = p && st.buffers[p] != null ? st.buffers[p] : ''
1147
+ const lines = content ? content.split('\n').length : 0
1148
+ const dirty = p && st.dirty[p]
1149
+ return h('div', { className: 'cdex-statusbar' },
1150
+ h('span', null, p ? p : '未打开文件'),
1151
+ h('span', null, p ? langOf(p) : ''),
1152
+ h('span', null, lines ? (lines + ' 行') : ''),
1153
+ h('span', null, '引用 ' + st.references.length),
1154
+ dirty ? h('span', { className: 'cdex-dirty' }, '● 未保存') : null,
1155
+ )
1156
+ }
1157
+ function ContextMenu() {
1158
+ const st = useStore()
1159
+ const m = st.ctxMenu
1160
+ if (!m) return null
1161
+ const sel = m.sel
1162
+ const items = [
1163
+ { icon: '📋', label: '复制', act: () => { copyText(sel.text); showToast('已复制'); closeMenu() } },
1164
+ { icon: '📥', label: '粘贴到备注', act: () => { pasteToNote(sel); closeMenu() } },
1165
+ { icon: '📝', label: '添加备注', act: () => { openNoteEditor(sel); closeMenu() } },
1166
+ { icon: '🌐', label: '翻译', act: () => { doTranslate(sel); closeMenu() } },
1167
+ { icon: '🔗', label: '引用(给 AI)', act: () => { doCite(sel); closeMenu() } },
1168
+ ]
1169
+ return h('div', { className: 'cdex-menu-backdrop', onClick: closeMenu },
1170
+ h('div', { className: 'cdex-menu', style: { left: m.x, top: m.y }, onClick: (e) => e.stopPropagation() },
1171
+ items.map((it, i) => h('div', { key: i, className: 'cdex-menu-item', onClick: it.act },
1172
+ h('span', null, it.icon), h('span', null, it.label),
1173
+ )),
1174
+ ),
1175
+ )
1176
+ }
1177
+ let noteTa = null
1178
+ function NoteEditor() {
1179
+ const st = useStore()
1180
+ const ne = st.noteEditor
1181
+ if (!ne) return null
1182
+ const close = () => { st.noteEditor = null; emit() }
1183
+ const save = async () => {
1184
+ const note = noteTa ? noteTa.value : ne.note
1185
+ await callHost('add-note', { path: ne.path, lineStart: ne.lineStart, lineEnd: ne.lineEnd, note })
1186
+ close()
1187
+ loadNotes(ne.path)
1188
+ showToast('备注已保存')
1189
+ }
1190
+ const loc = ne.lineStart ? (' (第 ' + ne.lineStart + '-' + ne.lineEnd + ' 行)') : ''
1191
+ return h('div', { className: 'cdex-modal', onClick: close },
1192
+ h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
1193
+ h('div', { className: 'cdex-modal-title' }, '添加备注', h('span', { style: { color: '#9d9d9d', fontWeight: 400 } }, ' — ', ne.path, loc)),
1194
+ h('textarea', { className: 'cdex-textarea2', ref: (el) => { noteTa = el }, defaultValue: ne.note, placeholder: '在此输入备注内容…' }),
1195
+ h('div', { className: 'cdex-modal-actions' },
1196
+ h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
1197
+ h('button', { className: 'cdex-btn', onClick: save }, '保存'),
1198
+ ),
1199
+ ),
1200
+ )
1201
+ }
1202
+ function Workbench() {
1203
+ const st = useStore()
1204
+ React.useEffect(() => {
1205
+ refresh(); loadReferences()
1206
+ // 外部修改事件轮询:每 1 秒拉一次 host 的 fs.watch 事件队列(无打开标签时 drain 内部直接跳过)
1207
+ let alive = true
1208
+ let cancel = null
1209
+ const tick = () => {
1210
+ if (!alive) return
1211
+ drainFileEvents().then(() => {
1212
+ if (!alive || !timer || typeof timer.timeout !== 'function') return
1213
+ const d = timer.timeout(tick, 1000)
1214
+ if (typeof d === 'function') cancel = d
1215
+ })
1216
+ }
1217
+ if (timer && typeof timer.timeout === 'function') {
1218
+ const d0 = timer.timeout(tick, 1000)
1219
+ if (typeof d0 === 'function') cancel = d0
1220
+ }
1221
+ return () => { alive = false; if (cancel) { try { cancel() } catch (e) {} } }
1222
+ }, [])
1223
+ if (!st.open) return null
1224
+ const style = st.windowed
1225
+ ? { left: st.win.x, top: st.win.y, width: st.win.w, height: st.win.h, borderRadius: '10px', border: '1px solid var(--dsw-alias-border-l2,#454545)', boxShadow: '0 18px 60px rgba(0,0,0,.45)' }
1226
+ : { left: 0, top: 0, width: '100vw', height: '100vh' }
1227
+ return h('div', { className: 'cdex-workbench ' + themeClass() + ' ' + tokensClass(), style: style },
1228
+ h('div', { className: 'cdex-top', onPointerDown: onTopPointerDown },
1229
+ h('span', { className: 'cdex-top-title', title: st.rootPath || '' }, st.rootPath ? ('AI 代码编辑器 - ' + baseName(st.rootPath)) : 'AI 代码编辑器'),
1230
+ h('div', { className: 'cdex-top-spacer' }),
1231
+ h('span', { className: 'cdex-top-hint' }, st.windowed ? '拖动标题栏移动 · 右下角缩放' : ''),
1232
+ h('button', { className: 'cdex-btn-ghost', title: '打开文件 (Ctrl+P)', onClick: openFilePicker }, '📂'),
1233
+ h('button', { className: 'cdex-btn-ghost', title: st.windowed ? '全屏' : '还原窗口', onClick: () => { st.windowed = !st.windowed; emit() } }, st.windowed ? '⛶' : '🗗'),
1234
+ ),
1235
+ h('div', { className: 'cdex-main' },
1236
+ h(ActivityBar),
1237
+ st.sidebarCollapsed ? h('div', { className: 'cdex-sb-expand', title: '展开侧栏', onClick: () => { st.sidebarCollapsed = false; emit() } }, '»') : (st.view === 'files' ? h(FileTree) : h(ReferencesView)),
1238
+ h(Editor),
1239
+ ),
1240
+ h(AiInput),
1241
+ h(StatusBar),
1242
+ st.windowed ? h('div', { className: 'cdex-resize', onPointerDown: onResizePointerDown }) : null,
1243
+ st.filePickerOpen ? h(FilePicker) : null,
1244
+ st.ctxMenu ? h(ContextMenu) : null,
1245
+ st.noteEditor ? h(NoteEditor) : null,
1246
+ st.toast ? h('div', { className: 'cdex-toast' }, st.toast.msg) : null,
1247
+ )
1248
+ }
1249
+
1250
+ /* ============ 右侧停靠模式(feature/native-layout) ============
1251
+ code UI 固定占右侧;DSH 壳层帧([data-slot=root] 下带 grid-template-columns
1252
+ 内联样式的 Grid 帧)加 margin-right 推挤,聊天区留在左侧并排。
1253
+ 左缘手柄拖宽;会话头按钮开关。定位用语义属性 + 内联样式,不依赖哈希类名。 */
1254
+ function findShellFrame() {
1255
+ try {
1256
+ const root = document.querySelector('[data-slot=root]')
1257
+ return (root && root.querySelector('[style*="grid-template-columns"]')) || null
1258
+ } catch (e) { return null }
1259
+ }
1260
+ function onDockResizeDown(e) {
1261
+ e.preventDefault()
1262
+ const handle = e.currentTarget
1263
+ const sw = store.dockW
1264
+ const px = e.clientX
1265
+ try { handle.setPointerCapture(e.pointerId) } catch (err) {}
1266
+ function move(ev) {
1267
+ store.dockW = Math.max(340, Math.min(sw + (px - ev.clientX), vw - 420))
1268
+ emit()
1269
+ }
1270
+ function up() {
1271
+ handle.removeEventListener('pointermove', move)
1272
+ handle.removeEventListener('pointerup', up)
1273
+ handle.removeEventListener('pointercancel', up)
1274
+ }
1275
+ handle.addEventListener('pointermove', move)
1276
+ handle.addEventListener('pointerup', up)
1277
+ handle.addEventListener('pointercancel', up)
1278
+ }
1279
+
1280
+ // 文件树右缘拖宽:拖到足够窄(<140px)松手 -> 整体收起(宽度还原为拖前值,下次展开不残留)
1281
+ function onSbResizeDown(e) {
1282
+ e.preventDefault()
1283
+ const handle = e.currentTarget
1284
+ const sw = store.sidebarW
1285
+ const px = e.clientX
1286
+ try { handle.setPointerCapture(e.pointerId) } catch (err) {}
1287
+ function move(ev) {
1288
+ store.sidebarW = Math.max(60, sw + (ev.clientX - px))
1289
+ emit()
1290
+ }
1291
+ function up() {
1292
+ handle.removeEventListener('pointermove', move)
1293
+ handle.removeEventListener('pointerup', up)
1294
+ handle.removeEventListener('pointercancel', up)
1295
+ if (store.sidebarW < 140) {
1296
+ store.sidebarCollapsed = true
1297
+ store.sidebarW = Math.max(180, sw)
1298
+ }
1299
+ emit()
1300
+ }
1301
+ handle.addEventListener('pointermove', move)
1302
+ handle.addEventListener('pointerup', up)
1303
+ handle.addEventListener('pointercancel', up)
1304
+ }
1305
+
1306
+ function DockToggle() {
1307
+ const st = useStore()
1308
+ return h('button', {
1309
+ className: 'cdex-dock-toggle' + (st.dockOpen ? ' active' : ''),
1310
+ title: '打开 / 收起 code UI 编辑器',
1311
+ onClick: () => { toggleUiMode(st.dockOpen ? 'native' : 'codeui') },
1312
+ }, '⌨ code UI')
1313
+ }
1314
+ function DockPanel() {
1315
+ const st = useStore()
1316
+ React.useEffect(() => {
1317
+ refresh(); loadReferences()
1318
+ let alive = true
1319
+ let cancel = null
1320
+ const tick = () => {
1321
+ if (!alive) return
1322
+ drainFileEvents().then(() => {
1323
+ if (!alive || !timer || typeof timer.timeout !== 'function') return
1324
+ const d = timer.timeout(tick, 1000)
1325
+ if (typeof d === 'function') cancel = d
1326
+ })
1327
+ }
1328
+ if (timer && typeof timer.timeout === 'function') {
1329
+ const d0 = timer.timeout(tick, 1000)
1330
+ if (typeof d0 === 'function') cancel = d0
1331
+ }
1332
+ return () => { alive = false; if (cancel) { try { cancel() } catch (e) {} } }
1333
+ }, [])
1334
+ // 推挤 DSH 壳层帧:开 = 按面板宽度加 margin-right;关 / 卸载 = 还原
1335
+ React.useEffect(() => {
1336
+ const frame = findShellFrame()
1337
+ if (frame) frame.style.marginRight = st.dockOpen ? st.dockW + 'px' : ''
1338
+ return () => { const f = findShellFrame(); if (f) f.style.marginRight = '' }
1339
+ }, [st.dockOpen, st.dockW])
1340
+ if (!st.dockOpen) return null
1341
+ return h('div', { className: 'cdex-dock ' + themeClass() + ' ' + tokensClass(), style: { width: st.dockW } },
1342
+ h('div', { className: 'cdex-dock-grip', title: '拖拽调整宽度', onPointerDown: onDockResizeDown }),
1343
+ h('div', { className: 'cdex-nativeview-top' },
1344
+ h('span', { className: 'cdex-top-title', title: st.rootPath || '' }, st.rootPath ? ('code UI · ' + baseName(st.rootPath)) : 'code UI'),
1345
+ h('div', { className: 'cdex-top-spacer' }),
1346
+ h('button', { className: 'cdex-btn-ghost', title: '打开文件 (Ctrl+P)', onClick: openFilePicker }, '📂'),
1347
+ h('button', { className: 'cdex-btn-ghost', title: '收起 code UI', onClick: () => toggleUiMode('native') }, '✕'),
1348
+ ),
1349
+ h('div', { className: 'cdex-main' },
1350
+ h(ActivityBar),
1351
+ st.sidebarCollapsed ? h('div', { className: 'cdex-sb-expand', title: '展开侧栏', onClick: () => { st.sidebarCollapsed = false; emit() } }, '»') : (st.view === 'files' ? h(FileTree) : h(ReferencesView)),
1352
+ h(Editor),
1353
+ ),
1354
+ h(StatusBar),
1355
+ st.filePickerOpen ? h(FilePicker) : null,
1356
+ st.ctxMenu ? h(ContextMenu) : null,
1357
+ st.noteEditor ? h(NoteEditor) : null,
1358
+ st.toast ? h('div', { className: 'cdex-toast' }, st.toast.msg) : null,
1359
+ )
1360
+ }
1361
+
1362
+ /* ============ §8 装配与 slot 注册(受「启用开关」控制,可动态挂/卸载) ============ */
1363
+ // 开关存 host 端 settings(enabled 字段,持久化)。默认启用:
1364
+ // get-self 失败 / 字段缺失(旧 host)时绝不隐藏编辑器,只有明确 enabled===false 才不挂载。
1365
+ let unmountEditorFns = null
1366
+ let layoutMode = 'dock' // dock(右侧停靠,默认) / float(悬浮窗口):编辑器以哪种布局呈现
1367
+ let toggleStyle = 'header' // header(会话头 ⌨ 按钮,默认) / bar(悬浮模式条):用哪种按钮切换界面
1368
+ function mountEditor() {
1369
+ if (unmountEditorFns) return
1370
+ const disposers = []
1371
+ disposers.push(slots.inject('conversation.session.header.actions', () => slots.register(
1372
+ { name: 'conversation.session.header.actions', id: 'cdex-input-bridge', order: 9999, label: '' },
1373
+ (props) => h(InputBridge, props),
1374
+ )))
1375
+ // 切换按钮:按用户选择注册(与布局模式解耦,任意组合)
1376
+ if (toggleStyle === 'bar') {
1377
+ // 悬浮模式条:可拖动横条,两键切换 code UI / dsh UI
1378
+ disposers.push(slots.inject('shell.overlay', () => slots.register(
1379
+ { name: 'shell.overlay', id: 'cdex-mode-tabs', order: 5, label: '界面模式' },
1380
+ () => h(ModeTabs, null),
1381
+ )))
1382
+ } else {
1383
+ // 会话头 ⌨ 按钮:点击在 code UI / dsh UI 之间切换
1384
+ disposers.push(slots.inject('conversation.session.header.actions', () => slots.register(
1385
+ { name: 'conversation.session.header.actions', id: 'cdex-codeui-toggle', order: 9998, label: 'code UI' },
1386
+ () => h(DockToggle, null),
1387
+ )))
1388
+ }
1389
+ // 编辑器布局:按布局模式注册
1390
+ if (layoutMode === 'float') {
1391
+ // 悬浮窗口:可拖拽悬浮工作台(含自带 AI 输入框,悬浮时盖住 DSH 输入条)
1392
+ disposers.push(slots.inject('shell.overlay', () => slots.register(
1393
+ { name: 'shell.overlay', id: 'cdex-workbench', order: 10, label: '代码编辑器' },
1394
+ () => h(Workbench, null),
1395
+ )))
1396
+ } else {
1397
+ // 右侧停靠(默认):固定右缘停靠面板(AI 输入用 DSH 原生输入条)
1398
+ disposers.push(slots.inject('shell.overlay', () => slots.register(
1399
+ { name: 'shell.overlay', id: 'cdex-dock', order: 10, label: 'code UI 停靠面板' },
1400
+ () => h(DockPanel, null),
1401
+ )))
1402
+ }
1403
+ unmountEditorFns = () => { disposers.forEach((d) => { try { d() } catch (e) {} }) }
1404
+ }
1405
+ function unmountEditor() {
1406
+ if (!unmountEditorFns) return
1407
+ try { unmountEditorFns() } catch (e) {}
1408
+ unmountEditorFns = null
1409
+ }
1410
+ // 卡片里切换布局 -> set-layout 成功后调用:卸载旧挂载、按新模式重挂(即时生效,无需刷新)
1411
+ function applyLayout(v) {
1412
+ const next = (v === 'float') ? 'float' : 'dock'
1413
+ if (next === layoutMode) return
1414
+ layoutMode = next
1415
+ if (unmountEditorFns) { unmountEditor(); mountEditor() }
1416
+ }
1417
+ // 卡片里切换切换按钮样式 -> set-toggle 成功后调用:换一种切换按钮(即时生效,无需刷新)
1418
+ function applyToggleStyle(v) {
1419
+ const next = (v === 'bar') ? 'bar' : 'header'
1420
+ if (next === toggleStyle) return
1421
+ toggleStyle = next
1422
+ if (unmountEditorFns) { unmountEditor(); mountEditor() }
1423
+ }
1424
+ // 界面模式(codeui=显示编辑器 / native=dsh UI):统一驱动两种布局的显示状态。
1425
+ // applyUiMode 只改 store;toggleUiMode 供界面入口(模式条/⌨/✕/↩)用:改 store + 静默持久化。
1426
+ function applyUiMode(v) {
1427
+ const show = v !== 'native'
1428
+ store.open = show
1429
+ store.dockOpen = show
1430
+ emit()
1431
+ }
1432
+ function toggleUiMode(v) {
1433
+ applyUiMode(v)
1434
+ callHost('set-uimode', { uiMode: v }).catch(() => {})
1435
+ }
1436
+ // 卡片里切换开关 -> set-enabled 成功后调用:立即挂/卸载编辑器(无需刷新页面、无需重启 DSH)
1437
+ function applyEnabled(v) {
1438
+ if (v) mountEditor()
1439
+ else unmountEditor()
1440
+ }
1441
+ // 启动时先查开关/主题/布局/界面模式/切换按钮再挂载(RPC 失败 = 全默认:启用+跟随DSH+停靠+显示+会话头按钮)
1442
+ callHost('get-self').then((r) => {
1443
+ if (r && !r.error) {
1444
+ setThemeMode(r.theme)
1445
+ layoutMode = (r.layout === 'float') ? 'float' : 'dock'
1446
+ toggleStyle = (r.toggle === 'bar') ? 'bar' : 'header'
1447
+ applyUiMode(r.uiMode === 'native' ? 'native' : 'codeui')
1448
+ applyEnabled(!(r.enabled === false))
1449
+ } else {
1450
+ setThemeMode('dsh')
1451
+ layoutMode = 'dock'
1452
+ toggleStyle = 'header'
1453
+ applyUiMode('codeui')
1454
+ applyEnabled(true)
1455
+ }
1456
+ }).catch(() => { setThemeMode('dsh'); layoutMode = 'dock'; toggleStyle = 'header'; applyUiMode('codeui'); applyEnabled(true) })
1457
+
1458
+ /* ---- 插件配置卡片(settings.plugin.item):信息 + 检查更新 / 一键更新 / 本地包更新 ---- */
1459
+ function PluginConfigCard() {
1460
+ const [cardOpen, setCardOpen] = React.useState(false)
1461
+ const [self, setSelf] = React.useState(null)
1462
+ const [chk, setChk] = React.useState(null)
1463
+ const [run, setRun] = React.useState(null)
1464
+ const [localPath, setLocalPath] = React.useState('')
1465
+ const [enabling, setEnabling] = React.useState(false)
1466
+ const [theming, setTheming] = React.useState(false)
1467
+ const [layouting, setLayouting] = React.useState(false)
1468
+ const [uiModeing, setUiModeing] = React.useState(false)
1469
+ const [toggling, setToggling] = React.useState(false)
1470
+ const lastPayloadRef = React.useRef(null)
1471
+ const pollDisposeRef = React.useRef(null)
1472
+
1473
+ // 编辑器主题:写 host settings 持久化;成功后立即换肤
1474
+ function setTheme(m) {
1475
+ if (!self || theming) return
1476
+ setTheming(true)
1477
+ callHost('set-theme', { theme: m }).then((r) => {
1478
+ setTheming(false)
1479
+ if (r && !r.error) {
1480
+ setSelf(Object.assign({}, self, { theme: m }))
1481
+ setThemeMode(m)
1482
+ showToast('主题已切换:' + (m === 'system' ? '跟随系统' : m === 'light' ? '浅色' : '深色'))
1483
+ } else {
1484
+ showToast('切换失败:' + ((r && r.error) || '未知错误'))
1485
+ }
1486
+ })
1487
+ }
1488
+
1489
+ // 布局模式:写 host settings 持久化;成功后立即卸载旧挂载、按新模式重挂
1490
+ function setLayout(m) {
1491
+ if (!self || layouting) return
1492
+ setLayouting(true)
1493
+ callHost('set-layout', { layout: m }).then((r) => {
1494
+ setLayouting(false)
1495
+ if (r && !r.error) {
1496
+ setSelf(Object.assign({}, self, { layout: m }))
1497
+ applyLayout(m)
1498
+ showToast('布局已切换:' + (m === 'dock' ? '右侧停靠' : '悬浮窗口'))
1499
+ } else {
1500
+ showToast('切换失败:' + ((r && r.error) || '未知错误'))
1501
+ }
1502
+ })
1503
+ }
1504
+
1505
+ // 界面模式:写 host settings 持久化;成功后立即显示/隐藏编辑器(按当前布局)
1506
+ function setUiMode(m) {
1507
+ if (!self || uiModeing) return
1508
+ setUiModeing(true)
1509
+ callHost('set-uimode', { uiMode: m }).then((r) => {
1510
+ setUiModeing(false)
1511
+ if (r && !r.error) {
1512
+ setSelf(Object.assign({}, self, { uiMode: m }))
1513
+ applyUiMode(m)
1514
+ showToast('界面已切换:' + (m === 'codeui' ? 'code UI' : 'dsh UI'))
1515
+ } else {
1516
+ showToast('切换失败:' + ((r && r.error) || '未知错误'))
1517
+ }
1518
+ })
1519
+ }
1520
+
1521
+ // 切换按钮样式:写 host settings 持久化;成功后立即换一种切换按钮
1522
+ function setToggle(m) {
1523
+ if (!self || toggling) return
1524
+ setToggling(true)
1525
+ callHost('set-toggle', { toggle: m }).then((r) => {
1526
+ setToggling(false)
1527
+ if (r && !r.error) {
1528
+ setSelf(Object.assign({}, self, { toggle: m }))
1529
+ applyToggleStyle(m)
1530
+ showToast('切换按钮已改为:' + (m === 'bar' ? '悬浮模式条' : '会话头按钮'))
1531
+ } else {
1532
+ showToast('切换失败:' + ((r && r.error) || '未知错误'))
1533
+ }
1534
+ })
1535
+ }
1536
+
1537
+ // 编辑器开关:写 host settings 持久化;成功后立即挂/卸载编辑器 UI
1538
+ function toggleEnabled() {
1539
+ if (!self || enabling) return
1540
+ const next = self.enabled === false
1541
+ setEnabling(true)
1542
+ callHost('set-enabled', { enabled: next }).then((r) => {
1543
+ setEnabling(false)
1544
+ if (r && !r.error) {
1545
+ setSelf(Object.assign({}, self, { enabled: next }))
1546
+ applyEnabled(next)
1547
+ showToast(next ? '已启用编辑器(立即生效)' : '已禁用编辑器(立即生效)')
1548
+ } else {
1549
+ showToast('切换失败:' + ((r && r.error) || '未知错误'))
1550
+ }
1551
+ })
1552
+ }
1553
+
1554
+ function stopPoll() {
1555
+ if (pollDisposeRef.current) { try { pollDisposeRef.current() } catch (e) {} pollDisposeRef.current = null }
1556
+ }
1557
+ React.useEffect(() => stopPoll, [])
1558
+ React.useEffect(() => {
1559
+ if (!cardOpen) return
1560
+ if (!self) callHost('get-self').then((r) => { if (r && !r.error) setSelf(r) })
1561
+ if (!chk) doCheck()
1562
+ }, [cardOpen])
1563
+
1564
+ function doCheck() {
1565
+ setChk({ checking: true })
1566
+ callHost('check-update').then((r) => {
1567
+ setChk(r && !r.error ? r : { error: (r && r.error) || '检查失败,host 端可能仍是旧版本(需重启 DSH)' })
1568
+ })
1569
+ }
1570
+
1571
+ function startUpdate(payload) {
1572
+ lastPayloadRef.current = payload
1573
+ setRun({ active: true, done: false, phase: 'starting', lines: [] })
1574
+ callHost('perform-update', payload).then((r) => {
1575
+ if (!r || r.error) {
1576
+ setRun({ active: false, done: true, ok: false, error: (r && r.error) || '无法启动更新', hint: r && r.error && /未知端点/.test(r.error) ? 'host 端仍是旧版本:请先重启 DSH 再使用更新功能' : '', lines: [] })
1577
+ return
1578
+ }
1579
+ pollProgress()
1580
+ })
1581
+ }
1582
+ function pollProgress() {
1583
+ callHost('get-update-progress').then((r) => {
1584
+ if (!r || r.error) return
1585
+ setRun(r)
1586
+ if (r.active) {
1587
+ if (timer && typeof timer.timeout === 'function') {
1588
+ stopPoll()
1589
+ const d = timer.timeout(pollProgress, 900)
1590
+ pollDisposeRef.current = (typeof d === 'function') ? d : null
1591
+ }
1592
+ } else {
1593
+ stopPoll()
1594
+ }
1595
+ })
1596
+ }
1597
+
1598
+ const cur = (self && self.version) || (chk && chk.current) || '?'
1599
+ const started = !!(run && !run.done)
1600
+ let checkLine = null
1601
+ if (chk && chk.checking) checkLine = h('p', { className: 'cdx-pc-note' }, '检查中…')
1602
+ else if (chk && chk.error) checkLine = h('p', { className: 'cdx-pc-err' }, chk.error)
1603
+ else if (chk && chk.notPublished) checkLine = h('p', { className: 'cdx-pc-note' }, '尚未发布到 npm(registry 404)。发布后可在线检查更新;当前可用下方「本地包更新」。')
1604
+ else if (chk && chk.updateAvailable) checkLine = h('p', { className: 'cdx-pc-note' }, '有新版本可用,见下方。')
1605
+ else if (chk && chk.latest) checkLine = h('p', { className: 'cdx-pc-ok' }, '✓ 已是最新(v' + chk.current + ')')
1606
+
1607
+ // 结构与样式逐条对齐宿主 PluginCard:li + 头部按钮(名称/描述两行 + 徽章 + 旋转 chevron) + body(上边框、左右 16px 缩进)
1608
+ return h('li', { className: 'cdx-pc-card' + (cardOpen ? ' cdx-pc-card-open' : '') },
1609
+ h('button', {
1610
+ type: 'button',
1611
+ className: 'cdx-pc-header',
1612
+ 'aria-expanded': cardOpen,
1613
+ 'aria-label': (cardOpen ? '收起' : '展开') + ': dsh-code-ui',
1614
+ onClick: () => setCardOpen(!cardOpen),
1615
+ },
1616
+ h('span', { className: 'cdx-pc-headtext' },
1617
+ h('span', { className: 'cdx-pc-name' }, 'dsh-code-ui'),
1618
+ h('span', { className: 'cdx-pc-desc' }, 'Cursor 风格 AI 代码编辑器插件:文件树、多标签、语法高亮、引用/翻译给 AI'),
1619
+ ),
1620
+ cur !== '?' ? h('span', { className: 'cdx-pc-badge' }, 'v' + cur) : null,
1621
+ IconChevronDown
1622
+ ? h(IconChevronDown, { className: 'cdx-pc-chevron' + (cardOpen ? ' cdx-pc-chevron-open' : '') })
1623
+ : h('span', { className: 'cdx-pc-chevron' + (cardOpen ? ' cdx-pc-chevron-open' : '') }, '▾'),
1624
+ ),
1625
+ cardOpen ? h('div', { className: 'cdx-pc-body' },
1626
+ // 字段零:编辑器启用开关
1627
+ h('div', { className: 'cdx-pc-field' },
1628
+ h('div', { className: 'cdx-pc-head' },
1629
+ h('span', { className: 'cdx-pc-label' }, '编辑器开关(页面入口 + 编辑器整体)'),
1630
+ h('button', {
1631
+ type: 'button',
1632
+ className: 'cdx-pc-btn ' + (self && self.enabled !== false ? 'cdx-pc-btn-outline' : 'cdx-pc-btn-primary'),
1633
+ disabled: !self || enabling,
1634
+ onClick: toggleEnabled,
1635
+ }, enabling ? '切换中…' : (self && self.enabled !== false ? '已启用 · 点击禁用' : '已禁用 · 点击启用')),
1636
+ ),
1637
+ h('p', { className: 'cdx-pc-note' }, '切换立即生效(实时挂载/卸载编辑器),无需重启 DSH,也无需刷新页面;若个别界面未跟着变化,普通刷新(F5)即可。开关状态持久化保存。'),
1638
+ ),
1639
+ // 字段:编辑器主题
1640
+ h('div', { className: 'cdx-pc-field' },
1641
+ h('div', { className: 'cdx-pc-head' },
1642
+ h('span', { className: 'cdx-pc-label' }, '编辑器主题'),
1643
+ h('span', { className: 'cdx-pc-head' },
1644
+ ['dsh', 'system', 'light', 'dark'].map((m) => h('button', {
1645
+ key: m,
1646
+ type: 'button',
1647
+ className: 'cdx-pc-btn ' + (((self && self.theme) || 'dsh') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1648
+ disabled: !self || theming,
1649
+ onClick: () => setTheme(m),
1650
+ }, m === 'dsh' ? '跟随 DSH' : m === 'system' ? '跟随系统' : m === 'light' ? '浅色' : '深色')),
1651
+ ),
1652
+ ),
1653
+ h('p', { className: 'cdx-pc-note' }, '「跟随 DSH」随 DSH 整体外观(默认);「跟随系统」随操作系统浅色/深色实时切换;浅色/深色为强制指定。切换立即生效,仅作用于编辑器界面,选择持久化保存。'),
1654
+ ),
1655
+ // 字段:布局模式
1656
+ h('div', { className: 'cdx-pc-field' },
1657
+ h('div', { className: 'cdx-pc-head' },
1658
+ h('span', { className: 'cdx-pc-label' }, '布局模式'),
1659
+ h('span', { className: 'cdx-pc-head' },
1660
+ ['dock', 'float'].map((m) => h('button', {
1661
+ key: m,
1662
+ type: 'button',
1663
+ className: 'cdx-pc-btn ' + (((self && self.layout) || 'dock') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1664
+ disabled: !self || layouting,
1665
+ onClick: () => setLayout(m),
1666
+ }, m === 'dock' ? '右侧停靠' : '悬浮窗口')),
1667
+ ),
1668
+ ),
1669
+ h('p', { className: 'cdx-pc-note' }, '「右侧停靠」code UI 固定占屏幕右侧、与聊天并排,会话头 ⌨ 按钮开关(默认);「悬浮窗口」为可拖拽的浮动编辑器窗口,盖住页面、含自带输入框。切换即时生效,选择持久化保存。'),
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')),
1683
+ ),
1684
+ ),
1685
+ h('p', { className: 'cdx-pc-note' }, '「code UI」显示编辑器界面(默认);「dsh UI」隐藏编辑器、回归 DSH 原生界面(入口按钮保留,随时切回)。悬浮模式条 / ⌨ / ✕ / ↩ 的每次切换都会同步到此处,选择持久化保存。'),
1686
+ ),
1687
+ // 字段:切换按钮
1688
+ h('div', { className: 'cdx-pc-field' },
1689
+ h('div', { className: 'cdx-pc-head' },
1690
+ h('span', { className: 'cdx-pc-label' }, '切换按钮'),
1691
+ h('span', { className: 'cdx-pc-head' },
1692
+ ['header', 'bar'].map((m) => h('button', {
1693
+ key: m,
1694
+ type: 'button',
1695
+ className: 'cdx-pc-btn ' + (((self && self.toggle) || 'header') === m ? 'cdx-pc-btn-primary' : 'cdx-pc-btn-outline'),
1696
+ disabled: !self || toggling,
1697
+ onClick: () => setToggle(m),
1698
+ }, m === 'header' ? '会话头按钮' : '悬浮模式条')),
1699
+ ),
1700
+ ),
1701
+ h('p', { className: 'cdx-pc-note' }, '选择用哪种按钮在 code UI 与dsh UI 之间切换:「会话头按钮」在会话标题栏显示 ⌨ code UI(默认);「悬浮模式条」为屏幕上方可拖动的横条(code UI / dsh UI 两键)。与布局模式无关,可任意组合,选择持久化保存。'),
1702
+ ),
1703
+ // 字段一:在线检查更新
1704
+ h('div', { className: 'cdx-pc-field' },
1705
+ h('div', { className: 'cdx-pc-head' },
1706
+ h('span', { className: 'cdx-pc-label' }, '在线检查更新'),
1707
+ h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: !!(chk && chk.checking) || started, onClick: doCheck }, '检查更新'),
1708
+ ),
1709
+ checkLine,
1710
+ ),
1711
+ // 字段二:一键更新(有新版本时)
1712
+ chk && chk.updateAvailable ? h('div', { className: 'cdx-pc-field' },
1713
+ h('div', { className: 'cdx-pc-head' },
1714
+ 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),
1716
+ ),
1717
+ ) : null,
1718
+ // 字段三:本地包更新
1719
+ h('div', { className: 'cdx-pc-field' },
1720
+ h('div', { className: 'cdx-pc-head' },
1721
+ 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() }) }, '安装本地包'),
1723
+ ),
1724
+ h('div', { className: 'cdx-pc-head' },
1725
+ h('input', {
1726
+ className: 'cdx-pc-input',
1727
+ placeholder: 'D:\\...\\dsh-code-ui-1.1.2.tgz(npm pack 产出的 .tgz 绝对路径)',
1728
+ value: localPath,
1729
+ onChange: (e) => setLocalPath(e.target.value),
1730
+ onKeyDown: (e) => { if (e.key === 'Enter' && localPath.trim() && !started) startUpdate({ source: 'local', path: localPath.trim() }) },
1731
+ }),
1732
+ ),
1733
+ h('p', { className: 'cdx-pc-note' }, '安装走 profile 的 pnpm(与在线更新同一通道),完成后需重启 DSH 生效。'),
1734
+ ),
1735
+ // 进度与结果
1736
+ run && ((run.lines || []).length || run.active) ? h('div', { className: 'cdx-pc-field' },
1737
+ h('p', { className: 'cdx-pc-note' }, run.active ? '更新进行中…(pnpm 安装,可能需要几十秒)' : '安装输出:'),
1738
+ h('div', { className: 'cdx-pc-log' }, ((run.lines || []).slice(-14).join('\n')) || '正在启动 pnpm…'),
1739
+ ) : null,
1740
+ run && run.done && run.ok ? h('div', { className: 'cdx-pc-field' },
1741
+ h('p', { className: 'cdx-pc-ok' }, '✓ 更新完成:v' + (run.beforeVersion || '?') + ' -> v' + (run.afterVersion || '?') + ',重启 DSH 后生效'),
1742
+ ) : null,
1743
+ run && run.done && !run.ok ? h('div', { className: 'cdx-pc-field' },
1744
+ h('p', { className: 'cdx-pc-err' }, '✗ 更新失败:' + (run.error || '未知错误')),
1745
+ run.hint ? h('p', { className: 'cdx-pc-note' }, run.hint) : null,
1746
+ h('div', { className: 'cdx-pc-head' },
1747
+ h('span', { className: 'cdx-pc-label' }),
1748
+ h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', onClick: () => { if (lastPayloadRef.current) startUpdate(lastPayloadRef.current) } }, '重试'),
1749
+ ),
1750
+ ) : null,
1751
+ // 底部信息
1752
+ h('div', { className: 'cdx-pc-field' },
1753
+ self && self.profileRoot ? h('p', { className: 'cdx-pc-note' }, 'profile:' + self.profileRoot + ' · 检查源:' + (self.updateRegistry || 'https://registry.npmjs.org')) : null,
1754
+ h('p', { className: 'cdx-pc-note' }, '接口:connection.rpc /dsh-code-ui;编辑器入口:页面顶部 code UI;选中代码右键可复制/备注/翻译/引用给 AI。'),
1755
+ ),
1756
+ ) : null,
1757
+ )
1758
+ }
1759
+ // 注册到「插件配置」列表(settings.plugin.item,key = dsh-code-ui)。嵌套 inject settingsScope,
1760
+ // 服务不存在则卡片静默不出现(照 dshmarket)。
1761
+ const cardCtx = ctx
1762
+ if (typeof cardCtx.inject === 'function') {
1763
+ cardCtx.inject(['settingsScope'], (scoped) => {
1764
+ if (!scoped || !scoped.slots) return
1765
+ scoped.slots.inject('settings.plugin.item', () => scoped.slots.register({
1766
+ name: 'settings.plugin.item',
1767
+ key: 'dsh-code-ui',
1768
+ label: 'dsh-code-ui',
1769
+ }, () => h(PluginConfigCard, null)))
1770
+ })
1771
+ }
1772
+ // --- end original apply body ---// --- end original apply body ---
1773
+ }
1774
+
1775
+ exports.apply = __apply;
1776
+ exports.name = "dsh-code-ui";
1777
+ exports.inject = ["slots", "connection"];
1778
+ return module.exports;
1779
+ },
1780
+ });