dsh-code-ui 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1266 @@
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:hover:not(:disabled){filter:brightness(1.12);}
150
+ .cdex-aiinput-send:disabled{opacity:.4;cursor:not-allowed;}
151
+ .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;}
152
+ .cdex-fp-bar{display:flex;gap:6px;margin-bottom:10px;align-items:center;}
153
+ .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;}
154
+ .cdex-fp-list{max-height:420px;overflow:auto;}
155
+ .cdex-fp-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;}
156
+ .cdex-fp-item:hover{background:var(--dsw-alias-bg-layer-1,#2a2d2e);}
157
+ .cdex-fp-name{color:var(--dsw-alias-label-primary,#d4d4d4);font-size:13px;}
158
+ .cdex-fp-crumbs{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-bottom:8px;min-height:22px;}
159
+ .cdex-fp-crumb{color:var(--dsw-alias-label-secondary,#8a8a8a);cursor:pointer;border:none;background:transparent;padding:0;font-size:12.5px;}
160
+ .cdex-fp-crumb:hover{color:var(--dsw-alias-label-primary,#ffffff);}
161
+ .cdex-fp-crumb-sep{color:var(--dsw-alias-label-secondary,#6f6f6f);font-size:12px;}
162
+ .cdex-fp-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px;}
163
+ .cdex-fp-hint{color:var(--dsw-alias-label-secondary,#8a8a8a);font-size:12px;margin-right:auto;align-self:center;}
164
+ /* 插件配置卡片:样式逐条对齐宿主 PluginCard.module.css / fields.module.css(dsh-context 同源) */
165
+ .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;}
166
+ .cdx-pc-card:hover{border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
167
+ .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);}
168
+ .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;}
169
+ .cdx-pc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c6ef5);outline-offset:-2px;}
170
+ .cdx-pc-headtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;}
171
+ .cdx-pc-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,#d4d4d4);}
172
+ .cdx-pc-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9d9d9d);}
173
+ .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);}
174
+ .cdx-pc-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#888);transition:transform .16s;display:inline-flex;}
175
+ .cdx-pc-chevron-open{transform:rotate(180deg);}
176
+ .cdx-pc-body{border-top:1px solid var(--dsw-alias-border-l2,#3e3e42);margin:0 16px;padding-bottom:8px;}
177
+ .cdx-pc-field{display:flex;flex-direction:column;gap:6px;padding:12px 0;}
178
+ .cdx-pc-field + .cdx-pc-field{border-top:1px solid var(--dsw-alias-border-l2,#3e3e42);}
179
+ .cdx-pc-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
180
+ .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);}
181
+ .cdx-pc-note{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#9d9d9d);word-break:break-all;}
182
+ .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;}
183
+ .cdx-pc-err{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error,#f48771);word-break:break-all;}
184
+ .cdx-pc-new{font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary,#4c6ef5);}
185
+ .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;}
186
+ .cdx-pc-btn-outline{border-color:var(--dsw-alias-border-l2,#3e3e42);background:none;color:var(--dsw-alias-label-secondary,#9d9d9d);}
187
+ .cdx-pc-btn-outline:hover:not(:disabled){color:var(--dsw-alias-label-primary,#d4d4d4);border-color:var(--dsw-alias-label-dimmed,#6a6a70);}
188
+ .cdx-pc-btn-primary{background:var(--dsw-alias-label-primary,#d4d4d4);color:var(--dsw-alias-bg-layer-3,#2d2d2d);}
189
+ .cdx-pc-btn:disabled{opacity:.4;cursor:default;}
190
+ .cdx-pc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c6ef5);outline-offset:1px;}
191
+ .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;}
192
+ .cdx-pc-input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4c6ef5);}
193
+ .cdx-pc-input::placeholder{color:var(--dsw-alias-label-tertiary,#777);}
194
+ .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;}
195
+ `; document.head.appendChild(tag); return () => tag.remove(); })
196
+
197
+ /* ============ §3 状态层 ============ */
198
+ const store = {
199
+ open: false,
200
+ view: 'files',
201
+ modePos: null,
202
+ windowed: true,
203
+ win: { x: 16, y: 16, w: Math.round(vw * 0.72), h: Math.round(vh * 0.72) },
204
+ rootPath: null,
205
+ nodes: {},
206
+ tabs: [],
207
+ activePath: null,
208
+ buffers: {},
209
+ savedContent: {},
210
+ dirty: {},
211
+ loadSeq: {},
212
+ activeNotes: [],
213
+ references: [],
214
+ inputActions: null,
215
+ inputDraft: '',
216
+ inputPhase: 'plain',
217
+ filePickerOpen: false,
218
+ browsePath: null,
219
+ browseEntries: [],
220
+ ctxMenu: null,
221
+ noteEditor: null,
222
+ toast: null,
223
+ error: null,
224
+ }
225
+ const listeners = new Set()
226
+ function emit() { listeners.forEach((fn) => fn()) }
227
+ function useStore() {
228
+ const [, setTick] = React.useState(0)
229
+ React.useEffect(() => {
230
+ const fn = () => setTick((t) => t + 1)
231
+ listeners.add(fn)
232
+ return () => { listeners.delete(fn) }
233
+ }, [])
234
+ return store
235
+ }
236
+
237
+ function showToast(msg) {
238
+ store.toast = { msg }
239
+ emit()
240
+ if (timer && typeof timer.timeout === 'function') {
241
+ timer.timeout(() => { store.toast = null; emit() }, 2800)
242
+ }
243
+ }
244
+
245
+ /* ============ 纯工具函数 ============ */
246
+ function baseName(p) {
247
+ const parts = String(p || '').split(/[\\/]/).filter(Boolean)
248
+ return parts.length ? parts[parts.length - 1] : String(p || '')
249
+ }
250
+ function tagLabel(r) {
251
+ const name = baseName(r.path)
252
+ if (r.lineStart && r.lineEnd) {
253
+ return name + '(' + r.lineStart + '-' + r.lineEnd + ')'
254
+ }
255
+ return name
256
+ }
257
+ function parentPath(p) {
258
+ const s = String(p).replace(/[\\/]+$/, '')
259
+ const idx = Math.max(s.lastIndexOf('\\'), s.lastIndexOf('/'))
260
+ if (idx <= 0) return null
261
+ const parent = s.slice(0, idx)
262
+ if (/^[a-zA-Z]:$/.test(parent)) return parent + '\\'
263
+ return parent || null
264
+ }
265
+ function crumbsOf(p) {
266
+ if (!p) return []
267
+ const sep = String(p).includes('\\') ? '\\' : '/'
268
+ const parts = String(p).split(/[\\/]/).filter(Boolean)
269
+ const crumbs = []
270
+ let acc = ''
271
+ for (let i = 0; i < parts.length; i++) {
272
+ const part = parts[i]
273
+ if (i === 0 && /^[a-zA-Z]:$/.test(part)) {
274
+ acc = part + sep
275
+ } else {
276
+ acc = acc + part + sep
277
+ }
278
+ crumbs.push({ name: part, path: acc })
279
+ }
280
+ return crumbs
281
+ }
282
+ function langOf(p) {
283
+ const ext = String(p || '').split('.').pop().toLowerCase()
284
+ 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' }
285
+ return map[ext] || 'text'
286
+ }
287
+ function copyText(text) {
288
+ let done = false
289
+ try {
290
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
291
+ navigator.clipboard.writeText(text).catch(() => {})
292
+ done = true
293
+ }
294
+ } catch (e) {}
295
+ if (!done) {
296
+ try {
297
+ if (typeof document !== 'undefined') {
298
+ const ta = document.createElement('textarea')
299
+ ta.value = text
300
+ ta.style.position = 'fixed'
301
+ ta.style.opacity = '0'
302
+ document.body.appendChild(ta)
303
+ ta.focus(); ta.select()
304
+ try { document.execCommand('copy') } catch (e2) {}
305
+ document.body.removeChild(ta)
306
+ }
307
+ } catch (e) {}
308
+ }
309
+ }
310
+ // —— 内联轻量语法高亮(自写 tokenizer,返回 HTML 字符串) ——
311
+ function esc(s) {
312
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
313
+ }
314
+ function span(cls, text) { return '<span class="' + cls + '">' + text + '</span>' }
315
+ function highlight(code, lang) {
316
+ if (!code) return ''
317
+ const L = String(lang || '').toLowerCase()
318
+ // 行注释前缀(按语言)
319
+ let lineComment = '//'
320
+ if (L === 'python' || L === 'py') lineComment = '#'
321
+ else if (L === 'shell' || L === 'bash' || L === 'sh') lineComment = '#'
322
+ else if (L === 'html' || L === 'xml' || L === 'css' || L === 'scss') lineComment = null
323
+ // 关键字(按语言簇)
324
+ 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)$/
325
+ 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)$/
326
+ if (L === 'json') keywords = /^(true|false|null)$/
327
+ const out = []
328
+ let i = 0
329
+ const n = code.length
330
+ while (i < n) {
331
+ const ch = code[i]
332
+ // 行注释
333
+ if (lineComment && code.startsWith(lineComment, i)) {
334
+ let j = code.indexOf('\n', i)
335
+ if (j < 0) j = n
336
+ out.push(span('cdx-tk-com', esc(code.slice(i, j))))
337
+ i = j
338
+ continue
339
+ }
340
+ // 块注释
341
+ if (code.startsWith('/*', i)) {
342
+ let j = code.indexOf('*/', i)
343
+ j = j < 0 ? n : j + 2
344
+ out.push(span('cdx-tk-com', esc(code.slice(i, j))))
345
+ i = j
346
+ continue
347
+ }
348
+ // 字符串(单引号 / 双引号 / 反引号)
349
+ if ((ch === '"' || ch === '\'' || ch === '`')) {
350
+ const quote = ch
351
+ let j = i + 1
352
+ while (j < n && code[j] !== quote) {
353
+ if (code[j] === '\\') j++
354
+ j++
355
+ }
356
+ if (j < n) j++
357
+ out.push(span('cdx-tk-str', esc(code.slice(i, j))))
358
+ i = j
359
+ continue
360
+ }
361
+ // 数字
362
+ if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < n && code[i + 1] >= '0' && code[i + 1] <= '9')) {
363
+ let j = i
364
+ while (j < n && /[0-9a-fA-FxXbBoO_eE.\-]/.test(code[j])) j++
365
+ out.push(span('cdx-tk-num', esc(code.slice(i, j))))
366
+ i = j
367
+ continue
368
+ }
369
+ // HTML/XML 标签
370
+ if ((L === 'html' || L === 'xml') && ch === '<') {
371
+ if (code.startsWith('<!--', i)) {
372
+ let j = code.indexOf('-->', i)
373
+ j = j < 0 ? n : j + 3
374
+ out.push(span('cdx-tk-com', esc(code.slice(i, j))))
375
+ i = j
376
+ continue
377
+ }
378
+ let j = i + 1
379
+ const isClose = code[j] === '/'
380
+ if (isClose) j++
381
+ let tagName = ''
382
+ while (j < n && /[a-zA-Z0-9-]/.test(code[j])) { tagName += code[j]; j++ }
383
+ if (!tagName) { out.push(esc('<', ch + code.slice(i + 1, Math.min(i + 2, n)))); i++; continue }
384
+ out.push('<span class="cdx-tk-punc">&lt;</span>' + (isClose ? '<span class="cdx-tk-punc">/</span>' : '') + span('cdx-tk-tag', tagName))
385
+ // 遍历属性直到闭合
386
+ while (j < n && code[j] !== '>' && !(code[j] === '/')) {
387
+ if (/\s/.test(code[j])) { out.push(esc(code[j])); j++; continue }
388
+ let ks = j
389
+ while (j < n && code[j] !== '=' && !/\s/.test(code[j]) && code[j] !== '>' && code[j] !== '/') j++
390
+ const key = code.slice(ks, j)
391
+ if (key) out.push(span('cdx-tk-attr', key))
392
+ // 跳过 = "值"
393
+ if (code[j] === '=') {
394
+ out.push(esc('='))
395
+ j++
396
+ if (code[j] === '"' || code[j] === '\'') {
397
+ const q = code[j]; let e = j + 1
398
+ while (e < n && code[e] !== q) { if (code[e] === '\\') e++; e++ }
399
+ if (e < n) e++
400
+ out.push(span('cdx-tk-str', esc(code.slice(j, e))))
401
+ j = e
402
+ } else {
403
+ let e = j
404
+ while (e < n && !/\s/.test(code[e]) && code[e] !== '>' && code[e] !== '/') e++
405
+ out.push(span('cdx-tk-str', esc(code.slice(j, e))))
406
+ j = e
407
+ }
408
+ }
409
+ }
410
+ if (code[j] === '/') { out.push(span('cdx-tk-punc', '/')); j++ }
411
+ if (code[j] === '>') { out.push(span('cdx-tk-punc', '>')); j++ }
412
+ i = j
413
+ continue
414
+ }
415
+ // 标识符 / 关键字 / 函数
416
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
417
+ let j = i
418
+ while (j < n && /[\w$]/.test(code[j])) j++
419
+ const word = code.slice(i, j)
420
+ let cls = 'cdx-tk-plain'
421
+ const kws = String(keywords)
422
+ if (keywords.test(word)) cls = 'cdx-tk-kw'
423
+ else {
424
+ // 函数名:后跟 (
425
+ let k = j
426
+ while (k < n && /\s/.test(code[k])) k++
427
+ if (code[k] === '(') cls = 'cdx-tk-fn'
428
+ }
429
+ out.push(span(cls, word))
430
+ i = j
431
+ continue
432
+ }
433
+ // 其他(含 CSS 属性)
434
+ if (L === 'css' || L === 'scss') {
435
+ if (ch === '#') {
436
+ // # 后跟十六进制(#fff / #1e73be)整体按数字着色;修复:原实现漏掉 i 推进导致死循环
437
+ let j = i + 1
438
+ while (j < n && /[0-9a-fA-F]/.test(code[j])) j++
439
+ out.push(span('cdx-tk-num', esc(code.slice(i, j))))
440
+ i = j
441
+ } else {
442
+ out.push(esc(ch))
443
+ i++
444
+ }
445
+ }
446
+ // 标点 / 普通字符
447
+ else {
448
+ if ('(){}[]<>=!+-*/&|;:,.?$%^~'.indexOf(ch) >= 0) out.push(span('cdx-tk-punc', esc(ch)))
449
+ else out.push(esc(ch))
450
+ i++
451
+ }
452
+ }
453
+ return out.join('')
454
+ }
455
+
456
+ /* ============ §4 RPC 操作层(文件树 / 标签页 / 引用备注) ============ */
457
+ // —— 文件浏览器 ——
458
+ async function browseDir(path) {
459
+ if (!path) return
460
+ store.browsePath = path
461
+ store.browseEntries = []
462
+ store.error = null
463
+ emit()
464
+ const res = await callHost('list-dir', { path })
465
+ if (res && res.error) { store.error = res.error; emit(); return }
466
+ store.browseEntries = (res && res.entries) || []
467
+ emit()
468
+ }
469
+ function openFilePicker() {
470
+ store.filePickerOpen = true
471
+ emit()
472
+ const start = store.browsePath || store.rootPath || null
473
+ if (start) { browseDir(start); return }
474
+ callHost('get-root').then((r) => {
475
+ if (r && r.root) { browseDir(r.root) }
476
+ else { store.error = '未定位到工作区根,请在输入框粘贴目录或文件路径后回车'; emit() }
477
+ })
478
+ }
479
+ function pickFile(path) {
480
+ store.filePickerOpen = false
481
+ emit()
482
+ openFile(path)
483
+ }
484
+ async function setWorkspaceRoot(path) {
485
+ if (!path) return
486
+ const res = await callHost('set-root', { path })
487
+ if (res && res.error) { showToast('设置失败:' + res.error); return }
488
+ const newRoot = (res && res.root) ? res.root : path
489
+ store.rootPath = newRoot
490
+ store.nodes = {}
491
+ store.tabs = []
492
+ store.activePath = null
493
+ store.buffers = {}
494
+ store.savedContent = {}
495
+ store.dirty = {}
496
+ store.activeNotes = []
497
+ store.browsePath = null
498
+ store.browseEntries = []
499
+ store.nodes[newRoot] = { name: baseName(newRoot), path: newRoot, isDir: true, children: [], expanded: true, loaded: false }
500
+ store.filePickerOpen = false
501
+ emit()
502
+ await toggleExpand(newRoot)
503
+ showToast('工作区根目录:' + newRoot)
504
+ }
505
+ // —— 文件树 ——
506
+ async function refresh() {
507
+ store.error = null
508
+ if (!store.rootPath) {
509
+ const r = await callHost('get-root')
510
+ store.rootPath = (r && r.root) || null
511
+ if (!store.rootPath) { store.error = '未找到工作区根目录'; emit(); return }
512
+ store.nodes = {}
513
+ store.nodes[store.rootPath] = { name: baseName(store.rootPath), path: store.rootPath, isDir: true, children: [], expanded: true, loaded: false }
514
+ emit()
515
+ await toggleExpand(store.rootPath)
516
+ } else {
517
+ const node = store.nodes[store.rootPath]
518
+ if (node) { node.loaded = false; node.children = []; node.expanded = true }
519
+ await toggleExpand(store.rootPath)
520
+ }
521
+ }
522
+ async function toggleExpand(path) {
523
+ const node = store.nodes[path]
524
+ if (!node) return
525
+ if (node.expanded) { node.expanded = false; emit(); return }
526
+ node.expanded = true
527
+ if (!node.loaded) {
528
+ node.loading = true; emit()
529
+ const res = await callHost('list-dir', { path })
530
+ node.loading = false; node.loaded = true
531
+ if (res && res.error) { store.error = res.error; emit(); return }
532
+ const entries = (res && res.entries) || []
533
+ node.children = entries.map((e) => { store.nodes[e.path] = e; return e.path })
534
+ emit()
535
+ } else { emit() }
536
+ }
537
+ // —— 标签页 / 编辑 / 保存 ——
538
+ async function openFile(path) {
539
+ const name = baseName(path)
540
+ if (!store.tabs.some((t) => t.path === path)) {
541
+ store.tabs.push({ path, name })
542
+ }
543
+ store.activePath = path
544
+ store.error = null
545
+ if (!(path in store.buffers)) {
546
+ store.buffers[path] = ''
547
+ store.savedContent[path] = ''
548
+ store.dirty[path] = false
549
+ emit()
550
+ const res = await callHost('read-file', { path })
551
+ if (res && res.error) { store.error = res.error; emit(); return }
552
+ store.buffers[path] = res.content
553
+ store.savedContent[path] = res.content
554
+ store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
555
+ emit()
556
+ } else {
557
+ emit()
558
+ }
559
+ loadNotes(path)
560
+ }
561
+ function closeTab(path) {
562
+ const i = store.tabs.findIndex((t) => t.path === path)
563
+ if (i < 0) return
564
+ store.tabs.splice(i, 1)
565
+ if (store.activePath === path) {
566
+ const next = store.tabs[i] || store.tabs[i - 1] || null
567
+ store.activePath = next ? next.path : null
568
+ if (next) { loadNotes(next.path) } else { store.activeNotes = [] }
569
+ }
570
+ emit()
571
+ }
572
+ async function saveFile(path) {
573
+ const p = path || store.activePath
574
+ if (!p) return
575
+ const content = store.buffers[p]
576
+ const res = await callHost('write-file', { path: p, content })
577
+ if (res && res.error) { showToast('保存失败:' + res.error); return }
578
+ store.savedContent[p] = content
579
+ store.dirty[p] = false
580
+ emit()
581
+ showToast('已保存 ' + baseName(p))
582
+ }
583
+ // —— 引用 / 备注 读取 ——
584
+ async function loadNotes(path) {
585
+ const res = await callHost('list-notes', { path })
586
+ if (res && res.notes) { store.activeNotes = res.notes; emit() }
587
+ }
588
+ async function loadReferences() {
589
+ const res = await callHost('list-references')
590
+ if (res && res.references) { store.references = res.references; emit() }
591
+ }
592
+
593
+ /* ============ §5 Actions 层(右键菜单 / 选区) ============ */
594
+ function onEditorContext(e) {
595
+ const ta = e.target
596
+ const start = ta.selectionStart, end = ta.selectionEnd
597
+ if (start === end) return
598
+ e.preventDefault()
599
+ const full = ta.value
600
+ const text = full.slice(start, end)
601
+ const lineStart = full.slice(0, start).split('\n').length
602
+ const lineEnd = full.slice(0, end).split('\n').length
603
+ store.ctxMenu = { x: e.clientX, y: e.clientY, sel: { text, lineStart, lineEnd, path: store.activePath, language: langOf(store.activePath) } }
604
+ emit()
605
+ }
606
+ function closeMenu() { store.ctxMenu = null; emit() }
607
+ async function doCite(sel) {
608
+ const ref = await callHost('add-reference', { path: sel.path, language: sel.language, lineStart: sel.lineStart, lineEnd: sel.lineEnd, text: sel.text, kind: 'cite' })
609
+ if (ref && ref.reference) { store.references.push(ref.reference); emit() }
610
+ showToast('已引用:' + baseName(sel.path))
611
+ }
612
+ async function doTranslate(sel) {
613
+ const ref = await callHost('add-reference', { path: sel.path, language: sel.language, lineStart: sel.lineStart, lineEnd: sel.lineEnd, text: sel.text, kind: 'translate' })
614
+ if (ref && ref.reference) { store.references.push(ref.reference); emit() }
615
+ showToast('已创建翻译引用(输入框上方已加 🌐 标签)。对 AI 说「翻译我引用的内容」即可。')
616
+ }
617
+ function openNoteEditor(sel) {
618
+ store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: '' }
619
+ emit()
620
+ }
621
+ async function pasteToNote(sel) {
622
+ let clip = ''
623
+ try {
624
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.readText) {
625
+ clip = await navigator.clipboard.readText()
626
+ }
627
+ } catch (e) {}
628
+ store.noteEditor = { path: sel.path, lineStart: sel.lineStart, lineEnd: sel.lineEnd, note: clip }
629
+ emit()
630
+ showToast(clip ? '已粘贴到备注框,点击「保存」完成。' : '无法读取剪贴板,请在备注框内按 Ctrl+V 粘贴。')
631
+ }
632
+ async function delReference(id) {
633
+ await callHost('delete-reference', { id })
634
+ store.references = store.references.filter((r) => r.id !== id)
635
+ emit()
636
+ }
637
+ async function delNote(id) {
638
+ await callHost('delete-note', { id })
639
+ store.activeNotes = store.activeNotes.filter((n) => n.id !== id)
640
+ emit()
641
+ }
642
+ // —— 引用自动附带(AI 上下文) ——
643
+ function buildSendPayload(draft) {
644
+ // 引用文本不再粘贴进消息(明面上不展示):引用统一存 host 端,AI 通过 get_referenced_snippets
645
+ // 工具读取;消息里只附一行「标签级」指针,让 AI 知道有引用可读。
646
+ const text = draft || ''
647
+ const citeRefs = store.references.filter((r) => r.kind === 'cite')
648
+ const trRefs = store.references.filter((r) => r.kind === 'translate')
649
+ const parts = []
650
+ if (citeRefs.length > 0) parts.push(citeRefs.length + ' 段代码引用')
651
+ if (trRefs.length > 0) parts.push(trRefs.length + ' 条翻译请求')
652
+ if (parts.length === 0) return text
653
+ const sep = text.trim() ? '\n\n' : ''
654
+ return text + sep + '[Code UI] 已标记 ' + parts.join('、')
655
+ + '(引用内容未粘贴在消息里,请调用 get_referenced_snippets 工具读取)。'
656
+ }
657
+
658
+ /* ============ §6 Pointer 处理层(窗口拖拽 / 缩放 / 模式条移动) ============ */
659
+ function onBarPointerDown(e) {
660
+ if (e.target && typeof e.target.closest === 'function' && e.target.closest('button')) return
661
+ e.preventDefault()
662
+ const bar = e.currentTarget
663
+ const rect = bar.getBoundingClientRect()
664
+ const startX = e.clientX, startY = e.clientY
665
+ const baseX = store.modePos ? store.modePos.x : rect.left
666
+ const baseY = store.modePos ? store.modePos.y : rect.top
667
+ try { bar.setPointerCapture(e.pointerId) } catch (err) {}
668
+ function move(ev) {
669
+ store.modePos = { x: Math.max(0, baseX + (ev.clientX - startX)), y: Math.max(0, baseY + (ev.clientY - startY)) }
670
+ emit()
671
+ }
672
+ function up() {
673
+ bar.removeEventListener('pointermove', move)
674
+ bar.removeEventListener('pointerup', up)
675
+ bar.removeEventListener('pointercancel', up)
676
+ }
677
+ bar.addEventListener('pointermove', move)
678
+ bar.addEventListener('pointerup', up)
679
+ bar.addEventListener('pointercancel', up)
680
+ }
681
+ function onTopPointerDown(e) {
682
+ if (!store.windowed) return
683
+ if (e.target && typeof e.target.closest === 'function' && e.target.closest('button')) return
684
+ e.preventDefault()
685
+ const top = e.currentTarget
686
+ const sx = store.win.x, sy = store.win.y
687
+ const px = e.clientX, py = e.clientY
688
+ try { top.setPointerCapture(e.pointerId) } catch (err) {}
689
+ function move(ev) {
690
+ store.win.x = Math.max(0, sx + (ev.clientX - px))
691
+ store.win.y = Math.max(0, sy + (ev.clientY - py))
692
+ emit()
693
+ }
694
+ function up() {
695
+ top.removeEventListener('pointermove', move)
696
+ top.removeEventListener('pointerup', up)
697
+ top.removeEventListener('pointercancel', up)
698
+ }
699
+ top.addEventListener('pointermove', move)
700
+ top.addEventListener('pointerup', up)
701
+ top.addEventListener('pointercancel', up)
702
+ }
703
+ function onResizePointerDown(e) {
704
+ if (!store.windowed) return
705
+ e.preventDefault()
706
+ const handle = e.currentTarget
707
+ const sw = store.win.w, sh = store.win.h
708
+ const px = e.clientX, py = e.clientY
709
+ try { handle.setPointerCapture(e.pointerId) } catch (err) {}
710
+ function move(ev) {
711
+ store.win.w = Math.max(360, sw + (ev.clientX - px))
712
+ store.win.h = Math.max(260, sh + (ev.clientY - py))
713
+ emit()
714
+ }
715
+ function up() {
716
+ handle.removeEventListener('pointermove', move)
717
+ handle.removeEventListener('pointerup', up)
718
+ handle.removeEventListener('pointercancel', up)
719
+ }
720
+ handle.addEventListener('pointermove', move)
721
+ handle.addEventListener('pointerup', up)
722
+ handle.addEventListener('pointercancel', up)
723
+ }
724
+
725
+ /* ============ §7 组件层(V.* 命名空间) ============ */
726
+ function InputBridge(props) {
727
+ const useInput = props.useInput
728
+ const inputState = typeof useInput === 'function' ? useInput((s) => s) : undefined
729
+ const inputActions = props.inputActions
730
+ React.useEffect(() => {
731
+ store.inputActions = inputActions || null
732
+ if (inputState) {
733
+ store.inputDraft = inputState.draft
734
+ store.inputPhase = inputState.phase
735
+ }
736
+ emit()
737
+ })
738
+ return null
739
+ }
740
+ function AiInput() {
741
+ const st = useStore()
742
+ let taEl = null
743
+ const disabled = !st.inputActions
744
+ // 引用与翻译请求都只显示「标签」:内容不进输入框/消息正文,AI 经 get_referenced_snippets 工具读取
745
+ const refs = st.references
746
+ const citeCount = refs.filter((r) => r.kind === 'cite').length
747
+ const trCount = refs.length - citeCount
748
+ const submit = () => {
749
+ if (!st.inputActions || !taEl) return
750
+ const text = taEl.value
751
+ if (!text.trim() && refs.length === 0) return
752
+ const payload = buildSendPayload(text)
753
+ st.inputActions.setDraft(payload)
754
+ st.inputActions.submit()
755
+ taEl.value = ''
756
+ }
757
+ return h('div', { className: 'cdex-aiinput-wrap' },
758
+ h('div', { className: 'cdex-aiinput-card' },
759
+ refs.length > 0 ? h('div', { className: 'cdex-aiinput-tags' },
760
+ refs.map((r) => h('span', { className: 'cdex-aiinput-tag' + (r.kind === 'translate' ? ' cdex-aiinput-tag-tr' : ''), key: r.id },
761
+ h('span', { className: 'cdex-aiinput-tag-label' }, (r.kind === 'translate' ? '🌐 翻译 ' : '🔗 ') + tagLabel(r)),
762
+ h('button', { className: 'cdex-aiinput-tag-x', title: r.kind === 'translate' ? '删除翻译引用' : '删除引用', onClick: () => delReference(r.id) }, '✕'),
763
+ )),
764
+ ) : null,
765
+ h('div', { className: 'cdex-aiinput' },
766
+ h('textarea', {
767
+ className: 'cdex-aiinput-ta',
768
+ ref: (el) => { taEl = el },
769
+ placeholder: disabled ? '请先打开一个会话' : '给 AI 发消息…(Ctrl+Enter 发送)',
770
+ disabled: disabled,
771
+ onKeyDown: (e) => {
772
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
773
+ e.preventDefault()
774
+ submit()
775
+ }
776
+ },
777
+ }),
778
+ h('button', { className: 'cdex-aiinput-send', disabled: disabled, onClick: submit }, '发送'),
779
+ ),
780
+ h('div', { className: 'cdex-aiinput-foot' },
781
+ h('span', null, 'Ctrl+Enter 发送'),
782
+ h('span', null, 'Shift+Enter 换行'),
783
+ citeCount + trCount > 0 ? h('span', null, '已引用 ' + citeCount + ' 段代码 · ' + trCount + ' 条翻译(AI 经工具读取,不占消息正文)') : null,
784
+ ),
785
+ ),
786
+ )
787
+ }
788
+ function FilePicker() {
789
+ const st = useStore()
790
+ if (!st.filePickerOpen) return null
791
+ const dirs = st.browseEntries.filter((x) => x.isDir)
792
+ const files = st.browseEntries.filter((x) => !x.isDir)
793
+ const crumbs = crumbsOf(st.browsePath)
794
+ const close = () => { st.filePickerOpen = false; emit() }
795
+ const goUp = () => { if (st.browsePath) { const p = parentPath(st.browsePath); if (p) browseDir(p) } }
796
+ const goTo = (e) => {
797
+ if (e.key === 'Enter') {
798
+ const raw = e.target.value
799
+ const p = raw && raw.trim()
800
+ if (!p) return
801
+ callHost('stat-path', { path: p }).then((res) => {
802
+ if (!res || res.error) { showToast('无法访问该路径:' + ((res && res.error) || '未知错误')); return }
803
+ if (res.isDir) { browseDir(res.path || p) }
804
+ else if (res.isFile) { pickFile(res.path || p) }
805
+ // 兼容回退:宿主 isDir 字段缺失但路径确实存在时,按目录尝试浏览(list-dir 对非目录会给出真实错误)
806
+ else if (res.exists) { browseDir(p) }
807
+ else { showToast('路径不存在或不可用:' + p) }
808
+ })
809
+ }
810
+ }
811
+ // 当前浏览到的文件夹;未浏览时回退到已识别的工作区根,保证按钮不无谓置灰
812
+ const curDir = st.browsePath || st.rootPath
813
+ const confirm = () => { if (curDir) setWorkspaceRoot(curDir) }
814
+ return h('div', { className: 'cdex-modal', onClick: close },
815
+ h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
816
+ h('div', { className: 'cdex-modal-title' }, '打开文件'),
817
+ h('div', { className: 'cdex-fp-crumbs' },
818
+ crumbs.map((c, i) => h('span', { key: i },
819
+ h('button', { className: 'cdex-fp-crumb', onClick: () => browseDir(c.path) }, c.name),
820
+ i < crumbs.length - 1 ? h('span', { className: 'cdex-fp-crumb-sep' }, '›') : null,
821
+ )),
822
+ ),
823
+ h('div', { className: 'cdex-fp-bar' },
824
+ h('button', { className: 'cdex-btn-ghost', onClick: goUp, title: '上级目录' }, '↑'),
825
+ h('input', { className: 'cdex-fp-input', defaultValue: curDir || '', placeholder: '输入绝对路径后回车跳转', onKeyDown: goTo }),
826
+ ),
827
+ h('div', { className: 'cdex-fp-list' },
828
+ dirs.map((d) => h('div', { className: 'cdex-fp-item', key: d.path, onClick: () => browseDir(d.path) },
829
+ h('span', { className: 'cdex-fp-name' }, '📁 ' + d.name),
830
+ )),
831
+ files.map((f) => h('div', { className: 'cdex-fp-item', key: f.path, onClick: () => pickFile(f.path) },
832
+ h('span', { className: 'cdex-fp-name' }, '📄 ' + f.name),
833
+ )),
834
+ dirs.length === 0 && files.length === 0 ? h('div', { className: 'cdex-empty' }, st.error ? String(st.error) : '空目录') : null,
835
+ ),
836
+ h('div', { className: 'cdex-fp-actions' },
837
+ h('span', { className: 'cdex-fp-hint' }, '浏览到目标文件夹后,点「以此文件夹为根」'),
838
+ h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
839
+ h('button', { className: 'cdex-btn', onClick: confirm, disabled: !curDir }, '以此文件夹为根'),
840
+ ),
841
+ ),
842
+ )
843
+ }
844
+ function ModeTabs() {
845
+ const st = useStore()
846
+ const style = st.modePos ? { top: st.modePos.y, left: st.modePos.x } : { top: 10, left: '50%', transform: 'translateX(-50%)' }
847
+ return h('div', { className: 'cdex-modebar', style: style, onPointerDown: onBarPointerDown },
848
+ h('span', { className: 'cdex-mode-grip', title: '拖拽移动' }, '⠿'),
849
+ h('button', { className: 'cdex-mode-tab' + (st.open ? ' active' : ''), onClick: () => { if (!st.open) { st.open = true; emit() } } }, 'Code UI'),
850
+ h('button', { className: 'cdex-mode-tab' + (!st.open ? ' active' : ''), onClick: () => { if (st.open) { st.open = false; emit() } } }, '原始风格-ui'),
851
+ )
852
+ }
853
+ function ActivityBar() {
854
+ const st = useStore()
855
+ return h('div', { className: 'cdex-activitybar' },
856
+ h('button', { className: 'cdex-abar-btn' + (st.view === 'files' ? ' active' : ''), title: '资源管理器', onClick: () => { st.view = 'files'; emit() } }, '📁'),
857
+ h('button', { className: 'cdex-abar-btn' + (st.view === 'refs' ? ' active' : ''), title: '引用 / 备注', onClick: () => { st.view = 'refs'; emit() } }, '📎'),
858
+ h('div', { className: 'cdex-abar-spacer' }),
859
+ h('button', { className: 'cdex-abar-btn', title: '帮助', onClick: () => showToast('选中代码右键可:复制 / 粘贴 / 备注 / 翻译 / 引用给 AI。Ctrl+S 保存。Ctrl+P 打开文件。') }, 'ⓘ'),
860
+ h('button', { className: 'cdex-abar-btn', title: '退出编辑器', onClick: () => { st.open = false; emit() } }, '↩'),
861
+ )
862
+ }
863
+ function TreeNode(props) {
864
+ const st = useStore()
865
+ const node = st.nodes[props.path]
866
+ if (!node) return null
867
+ const style = { paddingLeft: (6 + props.depth * 14) + 'px' }
868
+ const cls = 'cdex-node' + (st.activePath === node.path ? ' active' : '')
869
+ const children = node.isDir && node.expanded ? (node.children || []).map((c) => h(TreeNode, { key: c, path: c, depth: props.depth + 1 })) : null
870
+ return h('div', null,
871
+ h('div', { className: cls, style: style, onClick: () => { node.isDir ? toggleExpand(node.path) : openFile(node.path) } },
872
+ node.isDir ? h('span', { className: 'cdex-arrow' }, node.expanded ? '▾' : '▸') : h('span', { className: 'cdex-arrow' }, ''),
873
+ h('span', { className: node.isDir ? 'cdex-folder' : 'cdex-file' }, node.isDir ? '📁 ' : '📄 ', node.name),
874
+ ),
875
+ children,
876
+ )
877
+ }
878
+ function FileTree() {
879
+ const st = useStore()
880
+ return h('div', { className: 'cdex-sidebar' },
881
+ h('div', { className: 'cdex-sidebar-head' },
882
+ h('span', { title: st.rootPath || '' }, st.rootPath ? ('资源管理器 · ' + baseName(st.rootPath)) : '资源管理器'),
883
+ h('button', { className: 'cdex-btn-ghost', onClick: refresh, title: '刷新' }, '⟳'),
884
+ ),
885
+ h('div', { className: 'cdex-tree' },
886
+ st.error ? h('div', { className: 'cdex-error' }, String(st.error)) : null,
887
+ st.rootPath ? h(TreeNode, { path: st.rootPath, depth: 0 }) : h('div', { className: 'cdex-empty' }, '正在加载目录…'),
888
+ ),
889
+ )
890
+ }
891
+ function RefItem(r) {
892
+ return h('div', { className: 'cdex-ref', key: r.id },
893
+ h('div', { className: 'cdex-ref-head' },
894
+ h('span', { className: 'cdex-ref-path' }, r.path, r.lineStart ? (' :' + r.lineStart + '-' + r.lineEnd) : ''),
895
+ h('button', { className: 'cdex-btn-ghost', onClick: () => delReference(r.id) }, '✕'),
896
+ ),
897
+ h('div', { className: 'cdex-ref-code' }, r.text.slice(0, 500)),
898
+ )
899
+ }
900
+ function ReferencesView() {
901
+ const st = useStore()
902
+ const citeList = st.references.filter((r) => r.kind === 'cite')
903
+ const trList = st.references.filter((r) => r.kind === 'translate')
904
+ return h('div', { className: 'cdex-sidebar' },
905
+ h('div', { className: 'cdex-sidebar-head' }, h('span', null, '引用 / 备注')),
906
+ h('div', { className: 'cdex-tree' },
907
+ h('div', { className: 'cdex-section' }, '🔗 代码引用 (' + citeList.length + ')'),
908
+ citeList.length === 0 ? h('div', { className: 'cdex-empty' }, '选中代码后右键「引用」。') : citeList.map((r) => RefItem(r)),
909
+ h('div', { className: 'cdex-section' }, '🌐 翻译请求 (' + trList.length + ')'),
910
+ trList.length === 0 ? h('div', { className: 'cdex-empty' }, '选中代码后右键「翻译」。') : trList.map((r) => RefItem(r)),
911
+ h('div', { className: 'cdex-section' }, '📝 备注 (' + st.activeNotes.length + ')'),
912
+ st.activeNotes.length === 0 ? h('div', { className: 'cdex-empty' }, '当前文件暂无备注。') : st.activeNotes.map((n) => h('div', { className: 'cdex-ref', key: n.id },
913
+ h('div', { className: 'cdex-ref-head' },
914
+ h('span', { className: 'cdex-ref-path' }, n.path, n.lineStart ? (' :' + n.lineStart + '-' + n.lineEnd) : ''),
915
+ h('button', { className: 'cdex-btn-ghost', onClick: () => delNote(n.id) }, '✕'),
916
+ ),
917
+ h('div', { className: 'cdex-ref-code' }, n.note),
918
+ )),
919
+ h('div', { className: 'cdex-hint' }, '在输入框对 AI 说「读取/翻译我引用的代码」,AI 会调用 get_referenced_snippets 工具获取内容。'),
920
+ ),
921
+ )
922
+ }
923
+ let gutterEl = null
924
+ function syncGutter(ta) {
925
+ if (gutterEl) gutterEl.scrollTop = ta.scrollTop
926
+ }
927
+ function TabBar() {
928
+ const st = useStore()
929
+ return h('div', { className: 'cdex-tabs' },
930
+ 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() } },
931
+ h('span', null, t.name + (st.dirty[t.path] ? ' ●' : '')),
932
+ h('button', { className: 'cdex-tab-close', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '✕'),
933
+ )),
934
+ h('div', { className: 'cdex-tab-actions' },
935
+ h('button', { className: 'cdex-btn', title: '保存 (Ctrl+S)', onClick: () => saveFile() }, '保存'),
936
+ ),
937
+ )
938
+ }
939
+ function Editor() {
940
+ const st = useStore()
941
+ if (!st.activePath) {
942
+ // 空态时显式展示当前工作区根,避免「以此文件夹为根」后误认为未切换
943
+ return h('div', { className: 'cdex-editor' },
944
+ h('div', { className: 'cdex-empty' },
945
+ st.rootPath ? h('div', { className: 'cdex-empty-root' }, '当前工作区:', h('b', null, st.rootPath)) : null,
946
+ '从左侧资源管理器打开文件,或 ',
947
+ h('button', { className: 'cdex-btn', onClick: openFilePicker }, '📂 打开文件'),
948
+ ),
949
+ )
950
+ }
951
+ const p = st.activePath
952
+ const content = st.buffers[p] != null ? st.buffers[p] : ''
953
+ const lineCount = content.split('\n').length
954
+ const gutter = []
955
+ for (let i = 1; i <= lineCount; i++) gutter.push(String(i))
956
+ // 高亮 HTML(overlay 下层展示)
957
+ const html = highlight(content, langOf(p))
958
+ // 滚动同步:把 pre 滚动到与 textarea 相同
959
+ const syncOverlayScroll = (taEl) => {
960
+ if (!taEl) return
961
+ const preEl = taEl.parentNode ? taEl.parentNode.querySelector('.cdex-code-pre') : null
962
+ const gutEl = taEl.parentNode ? taEl.parentNode.parentNode.querySelector('.cdex-gutter') : null
963
+ if (preEl) { preEl.scrollTop = taEl.scrollTop; preEl.scrollLeft = taEl.scrollLeft }
964
+ if (gutterEl) gutterEl.scrollTop = taEl.scrollTop
965
+ }
966
+ return h('div', { className: 'cdex-editor' },
967
+ h(TabBar),
968
+ h('div', { className: 'cdex-codearea' },
969
+ h('div', { className: 'cdex-gutter', ref: (el) => { gutterEl = el } }, gutter.join('\n')),
970
+ h('div', { className: 'cdex-code-layer' },
971
+ h('pre', { className: 'cdex-code-pre', 'aria-hidden': true, dangerouslySetInnerHTML: { __html: html } }),
972
+ h('textarea', {
973
+ // key 拼上加载序号:内容异步读回后 key 变化触发重挂载,defaultValue 才会生效(否则编辑区空白、与高亮层脱节)
974
+ key: p + ':' + (st.loadSeq[p] || 0),
975
+ className: 'cdex-code-ta',
976
+ defaultValue: content,
977
+ spellCheck: false,
978
+ wrap: 'off',
979
+ onScroll: (e) => syncOverlayScroll(e.target),
980
+ onContextMenu: onEditorContext,
981
+ onChange: (e) => {
982
+ store.buffers[p] = e.target.value
983
+ store.dirty[p] = e.target.value !== store.savedContent[p]
984
+ emit()
985
+ },
986
+ onKeyDown: (e) => {
987
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'p' || e.key === 'P')) {
988
+ e.preventDefault()
989
+ openFilePicker()
990
+ }
991
+ if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
992
+ e.preventDefault()
993
+ saveFile()
994
+ }
995
+ },
996
+ }),
997
+ ),
998
+ ),
999
+ )
1000
+ }
1001
+ function StatusBar() {
1002
+ const st = useStore()
1003
+ const p = st.activePath
1004
+ const content = p && st.buffers[p] != null ? st.buffers[p] : ''
1005
+ const lines = content ? content.split('\n').length : 0
1006
+ const dirty = p && st.dirty[p]
1007
+ return h('div', { className: 'cdex-statusbar' },
1008
+ h('span', null, p ? p : '未打开文件'),
1009
+ h('span', null, p ? langOf(p) : ''),
1010
+ h('span', null, lines ? (lines + ' 行') : ''),
1011
+ h('span', null, '引用 ' + st.references.length),
1012
+ dirty ? h('span', { className: 'cdex-dirty' }, '● 未保存') : null,
1013
+ )
1014
+ }
1015
+ function ContextMenu() {
1016
+ const st = useStore()
1017
+ const m = st.ctxMenu
1018
+ if (!m) return null
1019
+ const sel = m.sel
1020
+ const items = [
1021
+ { icon: '📋', label: '复制', act: () => { copyText(sel.text); showToast('已复制'); closeMenu() } },
1022
+ { icon: '📥', label: '粘贴到备注', act: () => { pasteToNote(sel); closeMenu() } },
1023
+ { icon: '📝', label: '添加备注', act: () => { openNoteEditor(sel); closeMenu() } },
1024
+ { icon: '🌐', label: '翻译', act: () => { doTranslate(sel); closeMenu() } },
1025
+ { icon: '🔗', label: '引用(给 AI)', act: () => { doCite(sel); closeMenu() } },
1026
+ ]
1027
+ return h('div', { className: 'cdex-menu-backdrop', onClick: closeMenu },
1028
+ h('div', { className: 'cdex-menu', style: { left: m.x, top: m.y }, onClick: (e) => e.stopPropagation() },
1029
+ items.map((it, i) => h('div', { key: i, className: 'cdex-menu-item', onClick: it.act },
1030
+ h('span', null, it.icon), h('span', null, it.label),
1031
+ )),
1032
+ ),
1033
+ )
1034
+ }
1035
+ let noteTa = null
1036
+ function NoteEditor() {
1037
+ const st = useStore()
1038
+ const ne = st.noteEditor
1039
+ if (!ne) return null
1040
+ const close = () => { st.noteEditor = null; emit() }
1041
+ const save = async () => {
1042
+ const note = noteTa ? noteTa.value : ne.note
1043
+ await callHost('add-note', { path: ne.path, lineStart: ne.lineStart, lineEnd: ne.lineEnd, note })
1044
+ close()
1045
+ loadNotes(ne.path)
1046
+ showToast('备注已保存')
1047
+ }
1048
+ const loc = ne.lineStart ? (' (第 ' + ne.lineStart + '-' + ne.lineEnd + ' 行)') : ''
1049
+ return h('div', { className: 'cdex-modal', onClick: close },
1050
+ h('div', { className: 'cdex-modal-box', onClick: (e) => e.stopPropagation() },
1051
+ h('div', { className: 'cdex-modal-title' }, '添加备注', h('span', { style: { color: '#9d9d9d', fontWeight: 400 } }, ' — ', ne.path, loc)),
1052
+ h('textarea', { className: 'cdex-textarea2', ref: (el) => { noteTa = el }, defaultValue: ne.note, placeholder: '在此输入备注内容…' }),
1053
+ h('div', { className: 'cdex-modal-actions' },
1054
+ h('button', { className: 'cdex-btn-ghost', onClick: close }, '取消'),
1055
+ h('button', { className: 'cdex-btn', onClick: save }, '保存'),
1056
+ ),
1057
+ ),
1058
+ )
1059
+ }
1060
+ function Workbench() {
1061
+ const st = useStore()
1062
+ React.useEffect(() => { refresh(); loadReferences() }, [])
1063
+ if (!st.open) return null
1064
+ const style = st.windowed
1065
+ ? { 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)' }
1066
+ : { left: 0, top: 0, width: '100vw', height: '100vh' }
1067
+ return h('div', { className: 'cdex-workbench', style: style },
1068
+ h('div', { className: 'cdex-top', onPointerDown: onTopPointerDown },
1069
+ h('span', { className: 'cdex-top-title', title: st.rootPath || '' }, st.rootPath ? ('AI 代码编辑器 - ' + baseName(st.rootPath)) : 'AI 代码编辑器'),
1070
+ h('div', { className: 'cdex-top-spacer' }),
1071
+ h('span', { className: 'cdex-top-hint' }, st.windowed ? '拖动标题栏移动 · 右下角缩放' : ''),
1072
+ h('button', { className: 'cdex-btn-ghost', title: '打开文件 (Ctrl+P)', onClick: openFilePicker }, '📂'),
1073
+ h('button', { className: 'cdex-btn-ghost', title: st.windowed ? '全屏' : '还原窗口', onClick: () => { st.windowed = !st.windowed; emit() } }, st.windowed ? '⛶' : '🗗'),
1074
+ ),
1075
+ h('div', { className: 'cdex-main' },
1076
+ h(ActivityBar),
1077
+ st.view === 'files' ? h(FileTree) : h(ReferencesView),
1078
+ h(Editor),
1079
+ ),
1080
+ h(AiInput),
1081
+ h(StatusBar),
1082
+ st.windowed ? h('div', { className: 'cdex-resize', onPointerDown: onResizePointerDown }) : null,
1083
+ st.filePickerOpen ? h(FilePicker) : null,
1084
+ st.ctxMenu ? h(ContextMenu) : null,
1085
+ st.noteEditor ? h(NoteEditor) : null,
1086
+ st.toast ? h('div', { className: 'cdex-toast' }, st.toast.msg) : null,
1087
+ )
1088
+ }
1089
+
1090
+ /* ============ §8 装配与 slot 注册 ============ */
1091
+ slots.inject('conversation.session.header.actions', () => slots.register(
1092
+ { name: 'conversation.session.header.actions', id: 'cdex-input-bridge', order: 9999, label: '' },
1093
+ (props) => h(InputBridge, props),
1094
+ ))
1095
+ slots.inject('shell.overlay', () => {
1096
+ const d1 = slots.register(
1097
+ { name: 'shell.overlay', id: 'cdex-mode-tabs', order: 5, label: '界面模式' },
1098
+ () => h(ModeTabs, null),
1099
+ )
1100
+ const d2 = slots.register(
1101
+ { name: 'shell.overlay', id: 'cdex-workbench', order: 10, label: '代码编辑器' },
1102
+ () => h(Workbench, null),
1103
+ )
1104
+ return () => { if (d1) d1(); if (d2) d2() }
1105
+ })
1106
+
1107
+ /* ---- 插件配置卡片(settings.plugin.item):信息 + 检查更新 / 一键更新 / 本地包更新 ---- */
1108
+ function PluginConfigCard() {
1109
+ const [cardOpen, setCardOpen] = React.useState(false)
1110
+ const [self, setSelf] = React.useState(null)
1111
+ const [chk, setChk] = React.useState(null)
1112
+ const [run, setRun] = React.useState(null)
1113
+ const [localPath, setLocalPath] = React.useState('')
1114
+ const lastPayloadRef = React.useRef(null)
1115
+ const pollDisposeRef = React.useRef(null)
1116
+
1117
+ function stopPoll() {
1118
+ if (pollDisposeRef.current) { try { pollDisposeRef.current() } catch (e) {} pollDisposeRef.current = null }
1119
+ }
1120
+ React.useEffect(() => stopPoll, [])
1121
+ React.useEffect(() => {
1122
+ if (!cardOpen) return
1123
+ if (!self) callHost('get-self').then((r) => { if (r && !r.error) setSelf(r) })
1124
+ if (!chk) doCheck()
1125
+ }, [cardOpen])
1126
+
1127
+ function doCheck() {
1128
+ setChk({ checking: true })
1129
+ callHost('check-update').then((r) => {
1130
+ setChk(r && !r.error ? r : { error: (r && r.error) || '检查失败,host 端可能仍是旧版本(需重启 DSH)' })
1131
+ })
1132
+ }
1133
+
1134
+ function startUpdate(payload) {
1135
+ lastPayloadRef.current = payload
1136
+ setRun({ active: true, done: false, phase: 'starting', lines: [] })
1137
+ callHost('perform-update', payload).then((r) => {
1138
+ if (!r || r.error) {
1139
+ setRun({ active: false, done: true, ok: false, error: (r && r.error) || '无法启动更新', hint: r && r.error && /未知端点/.test(r.error) ? 'host 端仍是旧版本:请先重启 DSH 再使用更新功能' : '', lines: [] })
1140
+ return
1141
+ }
1142
+ pollProgress()
1143
+ })
1144
+ }
1145
+ function pollProgress() {
1146
+ callHost('get-update-progress').then((r) => {
1147
+ if (!r || r.error) return
1148
+ setRun(r)
1149
+ if (r.active) {
1150
+ if (timer && typeof timer.timeout === 'function') {
1151
+ stopPoll()
1152
+ const d = timer.timeout(pollProgress, 900)
1153
+ pollDisposeRef.current = (typeof d === 'function') ? d : null
1154
+ }
1155
+ } else {
1156
+ stopPoll()
1157
+ }
1158
+ })
1159
+ }
1160
+
1161
+ const cur = (self && self.version) || (chk && chk.current) || '?'
1162
+ const started = !!(run && !run.done)
1163
+ let checkLine = null
1164
+ if (chk && chk.checking) checkLine = h('p', { className: 'cdx-pc-note' }, '检查中…')
1165
+ else if (chk && chk.error) checkLine = h('p', { className: 'cdx-pc-err' }, chk.error)
1166
+ else if (chk && chk.notPublished) checkLine = h('p', { className: 'cdx-pc-note' }, '尚未发布到 npm(registry 404)。发布后可在线检查更新;当前可用下方「本地包更新」。')
1167
+ else if (chk && chk.updateAvailable) checkLine = h('p', { className: 'cdx-pc-note' }, '有新版本可用,见下方。')
1168
+ else if (chk && chk.latest) checkLine = h('p', { className: 'cdx-pc-ok' }, '✓ 已是最新(v' + chk.current + ')')
1169
+
1170
+ // 结构与样式逐条对齐宿主 PluginCard:li + 头部按钮(名称/描述两行 + 徽章 + 旋转 chevron) + body(上边框、左右 16px 缩进)
1171
+ return h('li', { className: 'cdx-pc-card' + (cardOpen ? ' cdx-pc-card-open' : '') },
1172
+ h('button', {
1173
+ type: 'button',
1174
+ className: 'cdx-pc-header',
1175
+ 'aria-expanded': cardOpen,
1176
+ 'aria-label': (cardOpen ? '收起' : '展开') + ': dsh-code-ui',
1177
+ onClick: () => setCardOpen(!cardOpen),
1178
+ },
1179
+ h('span', { className: 'cdx-pc-headtext' },
1180
+ h('span', { className: 'cdx-pc-name' }, 'dsh-code-ui'),
1181
+ h('span', { className: 'cdx-pc-desc' }, 'Cursor 风格 AI 代码编辑器插件:文件树、多标签、语法高亮、引用/翻译给 AI'),
1182
+ ),
1183
+ cur !== '?' ? h('span', { className: 'cdx-pc-badge' }, 'v' + cur) : null,
1184
+ IconChevronDown
1185
+ ? h(IconChevronDown, { className: 'cdx-pc-chevron' + (cardOpen ? ' cdx-pc-chevron-open' : '') })
1186
+ : h('span', { className: 'cdx-pc-chevron' + (cardOpen ? ' cdx-pc-chevron-open' : '') }, '▾'),
1187
+ ),
1188
+ cardOpen ? h('div', { className: 'cdx-pc-body' },
1189
+ // 字段一:在线检查更新
1190
+ h('div', { className: 'cdx-pc-field' },
1191
+ h('div', { className: 'cdx-pc-head' },
1192
+ h('span', { className: 'cdx-pc-label' }, '在线检查更新'),
1193
+ h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: !!(chk && chk.checking) || started, onClick: doCheck }, '检查更新'),
1194
+ ),
1195
+ checkLine,
1196
+ ),
1197
+ // 字段二:一键更新(有新版本时)
1198
+ chk && chk.updateAvailable ? h('div', { className: 'cdx-pc-field' },
1199
+ h('div', { className: 'cdx-pc-head' },
1200
+ h('span', { className: 'cdx-pc-label' }, h('span', { className: 'cdx-pc-new' }, '🆕 有新版本:v' + chk.current + ' -> v' + chk.latest)),
1201
+ h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', disabled: started, onClick: () => startUpdate({ source: 'registry', version: chk.latest }) }, '一键更新到 v' + chk.latest),
1202
+ ),
1203
+ ) : null,
1204
+ // 字段三:本地包更新
1205
+ h('div', { className: 'cdx-pc-field' },
1206
+ h('div', { className: 'cdx-pc-head' },
1207
+ h('span', { className: 'cdx-pc-label' }, '本地包更新(开发期 / 未发布时)'),
1208
+ h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-outline', disabled: started || !localPath.trim(), onClick: () => startUpdate({ source: 'local', path: localPath.trim() }) }, '安装本地包'),
1209
+ ),
1210
+ h('div', { className: 'cdx-pc-head' },
1211
+ h('input', {
1212
+ className: 'cdx-pc-input',
1213
+ placeholder: 'D:\\...\\dsh-code-ui-1.1.2.tgz(npm pack 产出的 .tgz 绝对路径)',
1214
+ value: localPath,
1215
+ onChange: (e) => setLocalPath(e.target.value),
1216
+ onKeyDown: (e) => { if (e.key === 'Enter' && localPath.trim() && !started) startUpdate({ source: 'local', path: localPath.trim() }) },
1217
+ }),
1218
+ ),
1219
+ h('p', { className: 'cdx-pc-note' }, '安装走 profile 的 pnpm(与在线更新同一通道),完成后需重启 DSH 生效。'),
1220
+ ),
1221
+ // 进度与结果
1222
+ run && ((run.lines || []).length || run.active) ? h('div', { className: 'cdx-pc-field' },
1223
+ h('p', { className: 'cdx-pc-note' }, run.active ? '更新进行中…(pnpm 安装,可能需要几十秒)' : '安装输出:'),
1224
+ h('div', { className: 'cdx-pc-log' }, ((run.lines || []).slice(-14).join('\n')) || '正在启动 pnpm…'),
1225
+ ) : null,
1226
+ run && run.done && run.ok ? h('div', { className: 'cdx-pc-field' },
1227
+ h('p', { className: 'cdx-pc-ok' }, '✓ 更新完成:v' + (run.beforeVersion || '?') + ' -> v' + (run.afterVersion || '?') + ',重启 DSH 后生效'),
1228
+ ) : null,
1229
+ run && run.done && !run.ok ? h('div', { className: 'cdx-pc-field' },
1230
+ h('p', { className: 'cdx-pc-err' }, '✗ 更新失败:' + (run.error || '未知错误')),
1231
+ run.hint ? h('p', { className: 'cdx-pc-note' }, run.hint) : null,
1232
+ h('div', { className: 'cdx-pc-head' },
1233
+ h('span', { className: 'cdx-pc-label' }),
1234
+ h('button', { type: 'button', className: 'cdx-pc-btn cdx-pc-btn-primary', onClick: () => { if (lastPayloadRef.current) startUpdate(lastPayloadRef.current) } }, '重试'),
1235
+ ),
1236
+ ) : null,
1237
+ // 底部信息
1238
+ h('div', { className: 'cdx-pc-field' },
1239
+ self && self.profileRoot ? h('p', { className: 'cdx-pc-note' }, 'profile:' + self.profileRoot + ' · 检查源:' + (self.updateRegistry || 'https://registry.npmjs.org')) : null,
1240
+ h('p', { className: 'cdx-pc-note' }, '接口:connection.rpc /dsh-code-ui;编辑器入口:页面顶部 Code UI;选中代码右键可复制/备注/翻译/引用给 AI。'),
1241
+ ),
1242
+ ) : null,
1243
+ )
1244
+ }
1245
+ // 注册到「插件配置」列表(settings.plugin.item,key = dsh-code-ui)。嵌套 inject settingsScope,
1246
+ // 服务不存在则卡片静默不出现(照 dshmarket)。
1247
+ const cardCtx = ctx
1248
+ if (typeof cardCtx.inject === 'function') {
1249
+ cardCtx.inject(['settingsScope'], (scoped) => {
1250
+ if (!scoped || !scoped.slots) return
1251
+ scoped.slots.inject('settings.plugin.item', () => scoped.slots.register({
1252
+ name: 'settings.plugin.item',
1253
+ key: 'dsh-code-ui',
1254
+ label: 'dsh-code-ui',
1255
+ }, () => h(PluginConfigCard, null)))
1256
+ })
1257
+ }
1258
+ // --- end original apply body ---// --- end original apply body ---
1259
+ }
1260
+
1261
+ exports.apply = __apply;
1262
+ exports.name = "dsh-code-ui";
1263
+ exports.inject = ["slots", "connection"];
1264
+ return module.exports;
1265
+ },
1266
+ });