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/LICENSE +21 -0
- package/README.en.md +38 -0
- package/README.md +94 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1266 -0
- package/lib/index.js +595 -0
- package/package.json +77 -0
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
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"><</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
|
+
});
|