dsh-code-ui 1.1.5 → 1.3.0

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