@windypro-rourou/dsh-eda 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,656 @@
1
+ /**
2
+ * dsh-lichuang-eda — browser half. Runs inside the dsh web GUI.
3
+ *
4
+ * 平台仪表盘 + 监看器(生成在对话里由 agent 驱动官方 eda.* API 完成):
5
+ * - header(品牌 + 状态版本)
6
+ * - 连接卡:状态 + 一键动作(装/启/打开编辑器/扩展直达)
7
+ * - 连接教程(可折叠,默认收起,精简步骤)
8
+ * - 活动监视(主体):当前会话 agent 对官方 API 的实时调用流(动作/耗时/结果)
9
+ * - 离线导出(底部小结):仅兜底,导出可导入的标准版 JSON
10
+ *
11
+ * Bundle format: `window.__ModuleLoader__.load({id, factory})` (lazy CJS) —
12
+ * the only client bundle format the web shell materializes. The apply/inject
13
+ * export shape and ctx.effect disposer follow dsh-logcat's lib/client.js.
14
+ */
15
+ window.__ModuleLoader__.load({
16
+ id: "@dsh-lichuang/dsh-eda",
17
+ factory: (require) => {
18
+ var module = { exports: {} };
19
+ var exports = module.exports;
20
+
21
+ const { createElement: h, useEffect, useState } = require("react");
22
+ const { createRoot } = require("react-dom/client");
23
+
24
+ //#region styles
25
+ const STYLE = `
26
+ [data-dsh-eda-entry] {
27
+ display: flex; align-items: center; gap: 8px; width: 100%;
28
+ padding: 9px 12px; margin: 2px 0; border: 0; border-radius: 8px;
29
+ background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left;
30
+ }
31
+ [data-dsh-eda-entry]:hover { background: rgba(128,128,128,.14); }
32
+ [data-dsh-eda-entry][data-active] { background: rgba(128,128,128,.22); }
33
+ [data-dsh-eda-entry] .eda-entry-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; flex: none; }
34
+ [data-dsh-eda-entry] .eda-entry-label { font-size: 13px; line-height: 1.2; opacity: .92; }
35
+
36
+ .dsh-eda-view { position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 94vw); z-index: 9999; }
37
+ .dsh-eda-view[hidden] { display: none !important; }
38
+ .dsh-eda-panel {
39
+ position: absolute; inset: 0; background: light-dark(#fbfbfc, #121417);
40
+ color: light-dark(#1c1f24, #e3e6ea);
41
+ border-left: 1px solid light-dark(rgba(0,0,0,.09), rgba(255,255,255,.10));
42
+ font: 13px/1.6 -apple-system, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
43
+ color-scheme: light dark;
44
+ display: flex; flex-direction: column;
45
+ }
46
+ .eda-panel { display: flex; flex-direction: column; height: 100%; min-height: 0; }
47
+
48
+ .eda-head {
49
+ flex: none; display: flex; align-items: center; gap: 10px;
50
+ padding: 13px 16px; border-bottom: 1px solid light-dark(rgba(0,0,0,.08), rgba(255,255,255,.09));
51
+ }
52
+ .eda-head .eda-mark { width: 26px; height: 26px; border-radius: 6px; flex: none; background: #2563eb; color: #fff; display: grid; place-items: center; font-size: 12px; font-weight: 700; }
53
+ .eda-head .eda-tt { min-width: 0; flex: 1; }
54
+ .eda-head h1 { font-size: 14px; font-weight: 700; margin: 0; line-height: 1.3; }
55
+ .eda-head .eda-sub { font-size: 11px; opacity: .55; margin: 0; }
56
+ .eda-close { border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 15px; line-height: 1; padding: 5px 8px; border-radius: 6px; opacity: .6; }
57
+ .eda-close:hover { opacity: 1; background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.08)); }
58
+
59
+ .eda-scroll { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 13px 16px 18px; display: flex; flex-direction: column; gap: 12px; }
60
+
61
+ .eda-card { border: 1px solid light-dark(rgba(0,0,0,.09), rgba(255,255,255,.10)); border-radius: 10px; padding: 12px 14px; background: light-dark(#ffffff, #17191d); }
62
+ .eda-card-title { font-size: 12px; font-weight: 600; opacity: .75; margin: 0 0 8px; }
63
+
64
+ .eda-note { font-size: 11.5px; opacity: .62; margin: 8px 0 0; line-height: 1.6; }
65
+ .eda-muted { font-size: 11px; opacity: .5; }
66
+
67
+ .eda-status { display: flex; align-items: center; gap: 8px; }
68
+ .eda-status .eda-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
69
+ .eda-status .eda-dot.on { background: #16a34a; }
70
+ .eda-status .eda-dot.off { background: #d97706; }
71
+ .eda-status .eda-dot.wait { background: #9ca3af; animation: edaPulse 1.2s ease-in-out infinite; }
72
+ .eda-status .eda-txt { font-size: 13px; font-weight: 600; }
73
+ .eda-status .eda-txt small { font-weight: 400; opacity: .55; margin-left: 6px; }
74
+ @keyframes edaPulse { 0%,100% { opacity: .45; } 50% { opacity: 1; } }
75
+
76
+ .eda-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
77
+ .eda-btn {
78
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
79
+ border: 1px solid transparent; border-radius: 8px; padding: 6px 12px;
80
+ font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
81
+ transition: background .12s ease, border-color .12s ease;
82
+ }
83
+ .eda-btn.primary { background: #2563eb; color: #fff; }
84
+ .eda-btn.primary:hover { background: #1d4ed8; }
85
+ .eda-btn.primary:disabled { opacity: .5; cursor: default; }
86
+ .eda-btn.ghost { background: transparent; border-color: light-dark(rgba(0,0,0,.14), rgba(255,255,255,.16)); color: inherit; }
87
+ .eda-btn.ghost:hover { background: light-dark(rgba(0,0,0,.04), rgba(255,255,255,.06)); }
88
+ .eda-btn.ghost:disabled { opacity: .45; cursor: default; }
89
+ .eda-btn.big { width: 100%; padding: 8px 13px; font-size: 13px; }
90
+ .eda-link { color: #2563eb; text-decoration: none; font-size: 12px; }
91
+ .eda-link:hover { text-decoration: underline; }
92
+
93
+ /* connect wizard (collapsed by default, compact) */
94
+ .eda-steps { display: flex; flex-direction: column; gap: 9px; margin-top: 2px; }
95
+ .eda-step { display: flex; gap: 9px; }
96
+ .eda-step .eda-step-no { width: 19px; height: 19px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 10.5px; font-weight: 700; background: light-dark(#eef2f7, #24272c); opacity: .85; }
97
+ .eda-step .eda-step-body { min-width: 0; flex: 1; }
98
+ .eda-step .eda-step-name { font-size: 12.5px; font-weight: 600; }
99
+ .eda-step .eda-step-hint { font-size: 11px; opacity: .58; margin-top: 1px; line-height: 1.5; }
100
+ .eda-step .eda-step-action { margin-top: 5px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
101
+ .eda-step .eda-step-result { font-size: 11.5px; opacity: .7; }
102
+
103
+ /* activity monitor (main) */
104
+ .eda-act { display: flex; flex-direction: column; gap: 0; }
105
+ .eda-act-item { display: flex; gap: 9px; padding: 9px 0; border-bottom: 1px solid light-dark(rgba(0,0,0,.05), rgba(255,255,255,.06)); cursor: pointer; }
106
+ .eda-act-item:hover { background: light-dark(rgba(0,0,0,.03), rgba(255,255,255,.03)); }
107
+ .eda-act-item:last-child { border-bottom: 0; }
108
+ .eda-act-item .eda-act-dot { flex: none; margin-top: 5px; width: 7px; height: 7px; border-radius: 50%; background: #9ca3af; }
109
+ .eda-act-item.ok .eda-act-dot { background: #16a34a; }
110
+ .eda-act-item.err .eda-act-dot { background: #dc2626; }
111
+ .eda-act-item.pend .eda-act-dot { background: #f59e0b; animation: edaPulse 1s ease-in-out infinite; }
112
+ .eda-act-item .eda-act-body { min-width: 0; flex: 1; }
113
+ .eda-act-item .eda-act-line { display: flex; align-items: baseline; gap: 8px; }
114
+ .eda-act-item .eda-act-seq { font-family: ui-monospace, Consolas, monospace; font-size: 10px; opacity: .45; flex: none; }
115
+ .eda-act-item .eda-act-name { font-size: 12.5px; font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
116
+ .eda-act-item .eda-act-meta { font-size: 10.5px; opacity: .5; }
117
+ .eda-revoke { padding: 2px 8px !important; font-size: 10px !important; }
118
+ .eda-hints { display: flex; flex-direction: column; gap: 5px; margin-top: 2px; }
119
+ .eda-hint { font-size: 11px; opacity: .68; }
120
+ .eda-act-item .eda-act-meta .eda-pend { color: #f59e0b; opacity: 1; font-weight: 600; }
121
+ .eda-act-item .eda-act-code { font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; opacity: .55; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
122
+ .eda-act-item .eda-act-out { font-size: 11px; opacity: .65; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
123
+ .eda-act-item .eda-act-out.err { color: #dc2626; opacity: .9; }
124
+ .eda-act-item .eda-act-full {
125
+ white-space: pre-wrap; word-break: break-all; font-family: ui-monospace, Consolas, monospace;
126
+ font-size: 10.5px; line-height: 1.55; opacity: .8; margin-top: 5px; padding: 7px 9px;
127
+ background: light-dark(rgba(0,0,0,.045), rgba(255,255,255,.05)); border-radius: 7px;
128
+ max-height: 240px; overflow: auto;
129
+ }
130
+ .eda-act-item .eda-act-full .eda-act-full-label { font-family: inherit; font-size: 10px; opacity: .5; margin: 0 0 2px; }
131
+ .eda-act-empty { font-size: 11.5px; opacity: .5; padding: 6px 0; }
132
+
133
+ /* activity header row: title + session switcher */
134
+ .eda-act-head { display: flex; align-items: center; gap: 8px; }
135
+ .eda-act-head .eda-card-title { flex: 1; margin: 0; }
136
+ .eda-sess {
137
+ font: inherit; font-size: 10.5px; border: 1px solid light-dark(rgba(0,0,0,.14), rgba(255,255,255,.16));
138
+ border-radius: 6px; background: transparent; color: inherit; padding: 2px 6px; max-width: 130px; cursor: pointer;
139
+ }
140
+ .eda-act-hint { font-size: 10.5px; opacity: .5; margin: 6px 0 0; }
141
+
142
+ /* 醒目对话入口 (connected CTA) */
143
+ .eda-cta {
144
+ display: flex; align-items: center; gap: 10px; margin-top: 10px; padding: 9px 12px;
145
+ border: 1px solid rgba(37,99,235,.35); border-radius: 10px; cursor: pointer;
146
+ background: linear-gradient(180deg, rgba(37,99,235,.10), rgba(37,99,235,.03));
147
+ }
148
+ .eda-cta:hover { background: rgba(37,99,235,.14); }
149
+ .eda-cta .eda-cta-bubble { font-size: 16px; flex: none; }
150
+ .eda-cta .eda-cta-text { min-width: 0; flex: 1; }
151
+ .eda-cta .eda-cta-title { font-size: 10.5px; opacity: .6; }
152
+ .eda-cta .eda-cta-phrase { font-family: inherit; font-size: 12.5px; font-weight: 700; color: #2563eb; }
153
+ .eda-cta .eda-cta-copy { flex: none; font-size: 10.5px; opacity: .55; }
154
+ .eda-cta.ok .eda-cta-copy { color: #16a34a; opacity: 1; }
155
+
156
+ /* offline export (footer, small) */
157
+ .eda-prompt textarea {
158
+ width: 100%; box-sizing: border-box; resize: vertical; min-height: 56px; border-radius: 8px;
159
+ border: 1px solid light-dark(rgba(0,0,0,.14), rgba(255,255,255,.16));
160
+ background: light-dark(#fafafa, #121317); color: inherit; font: inherit; font-size: 12px; line-height: 1.6;
161
+ padding: 8px 10px; outline: none;
162
+ }
163
+ .eda-prompt textarea:focus { border-color: #2563eb; }
164
+ .eda-prompt textarea::placeholder { opacity: .45; }
165
+ .eda-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
166
+ .eda-chip { border: 1px solid light-dark(rgba(0,0,0,.11), rgba(255,255,255,.13)); border-radius: 999px; padding: 3px 9px; font-size: 11px; cursor: pointer; color: inherit; background: transparent; }
167
+ .eda-chip:hover { border-color: rgba(37,99,235,.55); }
168
+
169
+ .eda-foot { font-size: 11px; opacity: .5; text-align: center; margin: 2px 0 0; }
170
+ `;
171
+ //#endregion
172
+
173
+ const API_BASE = "/api/dsh-eda";
174
+ const LINKS = {
175
+ editor: "https://pro.lceda.cn/editor",
176
+ ext: "https://jlc-ext.com/item/oshwhub/run-api-gateway",
177
+ skill: "https://github.com/easyeda/easyeda-api-skill",
178
+ };
179
+
180
+ //#region data
181
+ const CONNECT_STEPS = [
182
+ { id: 0, name: "一键安装官方桥", hint: "官方 easyeda-api-skill → ~/.dsh/eda/bridge/(离线依赖,无需 npm)。" },
183
+ { id: 1, name: "启动官方桥", hint: "Bridge Server 监听 127.0.0.1:49620-49629,自动探测。" },
184
+ { id: 2, name: "网页版装扩展 Run API Gateway", hint: "pro.lceda.cn/editor →「高级」→「扩展管理器」→ 搜索安装;「已安装」里开「外部交互」+「显示在顶部菜单」。" },
185
+ { id: 3, name: "验证", hint: "刷新编辑器页面;面板状态变绿 = 已连上你的云画板。" },
186
+ ];
187
+ //#endregion
188
+
189
+ //#region hooks
190
+ function useStatus() {
191
+ const [status, setStatus] = useState(null);
192
+ const [error, setError] = useState(false);
193
+ useEffect(() => {
194
+ let alive = true;
195
+ const load = () => {
196
+ fetch(API_BASE + "/status")
197
+ .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
198
+ .then((b) => { if (alive) { setStatus(b); setError(false); } })
199
+ .catch(() => { if (alive) { setStatus(null); setError(true); } });
200
+ };
201
+ load();
202
+ const t = setInterval(load, 4000);
203
+ return () => { alive = false; clearInterval(t); };
204
+ }, []);
205
+ return [status, error];
206
+ }
207
+
208
+ /**
209
+ * Poll the agent-activity feed (what the conversation's agent is doing
210
+ * through the official API right now). `pinnedSid` = '' means "follow
211
+ * the freshest session" (the panel's default for 仅当前会话).
212
+ */
213
+ function useActivity() {
214
+ const [feed, setFeed] = useState({ activities: [], sessions: [], currentSid: '', error: false });
215
+ const [pinnedSid, setPinnedSid] = useState("");
216
+ useEffect(() => {
217
+ let alive = true;
218
+ const load = () => {
219
+ const q = pinnedSid ? "?sid=" + encodeURIComponent(pinnedSid) : "";
220
+ fetch(API_BASE + "/activity" + q)
221
+ .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
222
+ .then((b) => {
223
+ if (!alive) return;
224
+ setFeed({ activities: Array.isArray(b.activities) ? b.activities : [], sessions: Array.isArray(b.sessions) ? b.sessions : [], currentSid: b.currentSid ?? "", error: false });
225
+ })
226
+ .catch(() => { if (alive) setFeed((prev) => ({ ...prev, error: true })); });
227
+ };
228
+ load();
229
+ const t = setInterval(load, 3000);
230
+ return () => { alive = false; clearInterval(t); };
231
+ }, [pinnedSid]);
232
+ return { ...feed, setSid: setPinnedSid, pinned: pinnedSid };
233
+ }
234
+ //#endregion
235
+
236
+ //#region components
237
+ /** Compact connection card: state + one-click platform actions + dialog CTA. */
238
+ function ConnectCard({ status, error, onInstall, onStartBridge, installState, bridgeState }) {
239
+ const ready = status?.connected === true;
240
+ const [copied, setCopied] = useState(false);
241
+ const dot = status === null ? (error ? "off" : "wait") : (ready ? "on" : "off");
242
+ const state = status === null ? (error ? "状态服务未响应" : "检测中…") : (ready ? "已连接 · " + (status.port ? "127.0.0.1:" + status.port : "") : "未连接");
243
+ const win = status?.health?.match(/edaWindowCount:(\d+)/)?.[1] ?? null;
244
+ const inst = status?.bridgeInstalled === true;
245
+ const copyPrompt = () => {
246
+ const text = "嘉立创EDA,启动!帮我画一个 LED 点亮电路";
247
+ try { navigator.clipboard?.writeText(text); } catch { /* clipboard may be unavailable */ }
248
+ setCopied(true);
249
+ setTimeout(() => setCopied(false), 1600);
250
+ };
251
+ return h("div", { className: "eda-card" },
252
+ h("div", { className: "eda-status" },
253
+ h("span", { className: "eda-dot " + dot }),
254
+ h("div", { className: "eda-txt" }, state, win !== null ? h("small", null, "窗口 " + win) : null)),
255
+ h("div", { className: "eda-actions" },
256
+ h("button", {
257
+ className: "eda-btn " + (inst ? "ghost" : "primary"),
258
+ disabled: inst || installState?.busy,
259
+ onClick: onInstall,
260
+ title: inst ? "官方桥已安装(随本机持久)" : "",
261
+ }, inst ? "官方桥已安装 ✓" : (installState?.busy ? "安装中…" : "一键安装官方桥")),
262
+ h("button", { className: "eda-btn ghost", disabled: bridgeState?.busy, onClick: onStartBridge }, bridgeState?.busy ? "启动中…" : "启动官方桥"),
263
+ h("a", { className: "eda-link", href: LINKS.editor, target: "_blank", rel: "noreferrer" }, "打开编辑器"),
264
+ h("a", { className: "eda-link", href: LINKS.ext, target: "_blank", rel: "noreferrer" }, "扩展直达")),
265
+ ready
266
+ ? h("div", { className: "eda-cta" + (copied ? " ok" : ""), onClick: copyPrompt, title: "点击复制到剪贴板,然后在对话里发送" },
267
+ h("span", { className: "eda-cta-bubble" }, "💬"),
268
+ h("div", { className: "eda-cta-text" },
269
+ h("div", { className: "eda-cta-title" }, "去对话里说:"),
270
+ h("span", { className: "eda-cta-phrase" }, "「嘉立创EDA,启动!」")),
271
+ h("span", { className: "eda-cta-copy" }, copied ? "已复制 ✓" : "点击复制"))
272
+ : h("p", { className: "eda-note", style: { margin: "8px 0 0" } }, "连接好画板后,在对话里描述需求即可实时生成;下方「导出离线版」仅作兜底。"),
273
+ installState?.result ? h("p", { className: "eda-note" }, installState.result) : null,
274
+ bridgeState?.result ? h("p", { className: "eda-note" }, bridgeState.result) : null,
275
+ );
276
+ }
277
+
278
+ /** Collapsible connect guide (compact). */
279
+ function ConnectWizard({ open, setOpen }) {
280
+ if (!open) {
281
+ return h("button", { className: "eda-btn ghost", style: { width: "100%" }, onClick: () => setOpen(true) }, "连接教程(4 步 · 网页版)");
282
+ }
283
+ return h("div", { className: "eda-card" },
284
+ h("div", { className: "eda-card-title" }, "连接教程(官方栈 · 网页版)"),
285
+ h("div", { className: "eda-steps" }, CONNECT_STEPS.map((s) =>
286
+ h("div", { className: "eda-step", key: s.id },
287
+ h("div", { className: "eda-step-no" }, String(s.id + 1)),
288
+ h("div", { className: "eda-step-body" },
289
+ h("div", { className: "eda-step-name" }, s.name),
290
+ h("div", { className: "eda-step-hint" }, s.hint))))),
291
+ h("p", { className: "eda-note", style: { margin: "10px 0 0" } },
292
+ [LINKS.editor, LINKS.ext].map((l) => h("a", { className: "eda-link", href: l, target: "_blank", rel: "noreferrer", key: l, style: { marginRight: 12 } }, l)),
293
+ h("button", { className: "eda-link", onClick: () => setOpen(false), style: { border: 0, background: "transparent", cursor: "pointer", padding: 0 } }, "收起")));
294
+ }
295
+
296
+ /**
297
+ * The monitor: RECORD-STYLE timeline (参考 DSH 轨迹) — every step is
298
+ * numbered (persisted on disk, so it is never empty), each entry can be
299
+ * 撤回 (deletes the primitives that step created) or expanded; the card
300
+ * has 清空记录. When nothing has happened yet, a static capability
301
+ * preview keeps the panel informative (not blank).
302
+ */
303
+ function ActivityMonitor({ activities, sessions, currentSid, setSid, pinned, error, connected, onRevoke, busyId, onClear, clearing }) {
304
+ const [expanded, setExpanded] = useState(() => new Set());
305
+ const toggle = (id) => {
306
+ setExpanded((prev) => {
307
+ const next = new Set(prev);
308
+ if (next.has(id)) next.delete(id); else next.add(id);
309
+ return next;
310
+ });
311
+ };
312
+ const list = Array.isArray(activities) ? activities : [];
313
+ const sids = Array.isArray(sessions) ? sessions : [];
314
+ const hasMulti = sids.length > 1;
315
+ return h("div", { className: "eda-card" },
316
+ h("div", { className: "eda-act-head" },
317
+ h("div", { className: "eda-card-title" }, "Agent 正在使用官方 API"),
318
+ hasMulti
319
+ ? h("select", {
320
+ className: "eda-sess",
321
+ value: pinned ?? "",
322
+ onChange: (e) => setSid(e.target.value),
323
+ title: "切换会话(默认跟随最新)",
324
+ },
325
+ h("option", { value: "" }, "跟随最新会话"),
326
+ sids.map((s) => h("option", { value: s.sid ?? "", key: (s.sid || "(platform)") + s.lastTs }, s.label ?? "会话")))
327
+ : (currentSid && list.length > 0 ? h("span", { className: "eda-sess", style: { cursor: "default" } }, "当前会话") : null),
328
+ list.length > 0
329
+ ? h("button", { className: "eda-btn ghost", style: { padding: "3px 9px", fontSize: "10.5px" }, disabled: clearing, onClick: onClear, title: "清空时间线(含磁盘历史)" }, clearing ? "清空中…" : "清空记录")
330
+ : null),
331
+ list.length === 0
332
+ ? h("div", null,
333
+ h("div", { className: "eda-act-empty" }, error
334
+ ? "实时活动流暂不可用(服务端新模块待重启后启用);重启后这里会持续记录 agent 的每一步。"
335
+ : "还没有记录——历史会保存在 ~/.dsh/eda/activity.jsonl,面板永不空白。你可以让 agent:"),
336
+ h("div", { className: "eda-hints" },
337
+ ["放置任意大类元件(R/C/L/二极管/LED/MCU…)并框内定位", "引脚级连线 + VCC/GND 网络标志", "原理图/PCB 的 DRC、网表、BOM 导出", "PCB 元件/过孔/走线 + 现场截图", "随时「紧急保存」到本地并留动作日志"]
338
+ .map((t, i) => h("div", { className: "eda-hint", key: i }, "• " + t))))
339
+ : h("div", null,
340
+ h("div", { className: "eda-act" }, list.slice(0, 50).map((a, i) => {
341
+ const isPend = a.status === 'pending';
342
+ const isOpen = expanded.has(a.id);
343
+ const t = (a.ts ?? "").slice(11, 19);
344
+ const revokable = connected && !isPend && Array.isArray(a.revoke?.created) && a.revoke.created.length > 0;
345
+ return h("div", {
346
+ className: "eda-act-item " + (isPend ? "pend" : (a.ok === false ? "err" : (a.ok ? "ok" : ""))),
347
+ key: a.id ?? i,
348
+ onClick: () => toggle(a.id),
349
+ },
350
+ h("span", { className: "eda-act-dot" }),
351
+ h("div", { className: "eda-act-body" },
352
+ h("div", { className: "eda-act-line" },
353
+ h("span", { className: "eda-act-seq" }, "#" + (a.id ?? i)),
354
+ h("span", { className: "eda-act-name" }, a.action ?? a.tool ?? "调用 API"),
355
+ h("span", { className: "eda-act-meta" }, isPend
356
+ ? h("span", { className: "eda-pend" }, "执行中…")
357
+ : (t + (a.durationMs ? " · " + a.durationMs + "ms" : "") + " · " + (a.tool ?? ""))),
358
+ revokable
359
+ ? h("button", {
360
+ className: "eda-btn ghost eda-revoke",
361
+ disabled: busyId === a.id,
362
+ onClick: (e) => { e.stopPropagation(); onRevoke(a.id); },
363
+ title: "撤回该步(删除它新建的图元)",
364
+ }, busyId === a.id ? "撤回中…" : "撤回")
365
+ : null),
366
+ a.code ? h("div", { className: "eda-act-code" }, a.code) : null,
367
+ !isPend && a.ok === false && a.error ? h("div", { className: "eda-act-out err" }, a.error)
368
+ : (!isPend && a.result ? h("div", { className: "eda-act-out" }, a.result) : null),
369
+ isOpen ? h("div", { className: "eda-act-full" },
370
+ (a.code ? h("div", null, h("div", { className: "eda-act-full-label" }, "代码"), a.code) : null),
371
+ (a.revoke ? h("div", null, h("div", { className: "eda-act-full-label" }, "撤回数据"), `该步新建 ${a.revoke.created.length} 个图元` + (a.revoke.deletedCount ? `,删除 ${a.revoke.deletedCount} 个(不可恢复)` : "")) : null),
372
+ (a.result ? h("div", null, h("div", { className: "eda-act-full-label" }, "结果"), a.result) : null),
373
+ (a.error ? h("div", null, h("div", { className: "eda-act-full-label" }, "错误"), a.error) : null)) : null));
374
+ })),
375
+ h("div", { className: "eda-act-hint" }, (hasMulti ? "下拉切换会话 · " : "") + "点击条目查看完整内容 · 撤回仅删除该步新建图元")));
376
+ }
377
+
378
+ /**
379
+ * 紧急保存: pull the board state (官方 API .epro2 + SVG 预览 + 网表/BOM)
380
+ * AND the agent action log to local disk — a safety net if the cloud sync
381
+ * ever fails. Works (log-only) even when the bridge is disconnected.
382
+ */
383
+ function SnapshotCard({ connected, busy, onSave, lastOut }) {
384
+ return h("div", { className: "eda-card" },
385
+ h("div", { className: "eda-card-title" }, "紧急保存(画板断连也不丢工程)"),
386
+ h("button", { className: "eda-btn primary big", disabled: busy, onClick: onSave },
387
+ busy ? "保存中…" : "🛟 紧急保存"),
388
+ lastOut ? h("p", { className: "eda-note", style: { margin: "8px 0 0", wordBreak: "break-all" } }, lastOut) : null,
389
+ h("p", { className: "eda-muted", style: { margin: "8px 0 0" } },
390
+ connected
391
+ ? "把 agent 在画板上做的东西抓到本地:专业版 .epro2 完整恢复 + 通用预览 SVG + 网表/BOM。云端没同步上,也能找回最后的工程。"
392
+ : "画板未连接:仍会保存 agent 动作日志(每一步留档)。"));
393
+ }
394
+ //#endregion
395
+
396
+ //#region panel
397
+ function EdaPanel({ controller }) {
398
+ const [status, error] = useStatus();
399
+ const ready = status?.connected === true;
400
+ const activity = useActivity();
401
+ const [wizardOpen, setWizardOpen] = useState(false);
402
+ const [saveState, setSaveState] = useState({ busy: false, out: null });
403
+ const [installState, setInstallState] = useState({ busy: false, result: null });
404
+ const [bridgeState, setBridgeState] = useState({ busy: false, result: null });
405
+ const [revokeState, setRevokeState] = useState({ busyId: null, out: null });
406
+ const [clearing, setClearing] = useState(false);
407
+
408
+ const doRevoke = async (id) => {
409
+ if (revokeState.busyId !== null) return;
410
+ setRevokeState({ busyId: id, out: null });
411
+ try {
412
+ const res = await fetch(API_BASE + "/activity/revoke?id=" + encodeURIComponent(id), { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
413
+ if (!res.ok) { setRevokeState({ busyId: null, out: "撤回接口待重启后生效(当前旧模块)。" }); return; }
414
+ const out = await res.json();
415
+ setRevokeState({ busyId: null, out: out.ok ? `↩️ 已撤回:${out.note ?? ""}` : "⚠️ " + String(out.error ?? "撤回失败") });
416
+ } catch (err) { setRevokeState({ busyId: null, out: "请求失败:" + String(err) }); }
417
+ };
418
+ const doClear = async () => {
419
+ if (clearing) return;
420
+ setClearing(true);
421
+ try {
422
+ const res = await fetch(API_BASE + "/activity/clear", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
423
+ if (!res.ok) { setRevokeState({ busyId: null, out: "清空接口待重启后生效(当前旧模块)。" }); return; }
424
+ setRevokeState({ busyId: null, out: "🗑️ 时间线已清空" });
425
+ } catch (err) { setRevokeState({ busyId: null, out: "请求失败:" + String(err) }); }
426
+ finally { setClearing(false); }
427
+ };
428
+
429
+ const doInstall = async () => {
430
+ if (installState.busy) return;
431
+ setInstallState({ busy: true, result: null });
432
+ try {
433
+ const res = await fetch(API_BASE + "/install", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
434
+ if (!res.ok) { setInstallState({ busy: false, result: "官方桥已就绪 / 安装接口在下次重启后生效(当前旧模块)。官方桥源码:" + LINKS.skill }); return; }
435
+ const out = await res.json();
436
+ setInstallState({ busy: false, result: out.ok ? "✅ " + (out.script ?? "官方桥已安装") : "❌ " + (out.error ?? "安装失败") });
437
+ } catch (err) { setInstallState({ busy: false, result: "请求失败:" + String(err) }); }
438
+ };
439
+
440
+ const doStartBridge = async () => {
441
+ if (bridgeState.busy) return;
442
+ setBridgeState({ busy: true, result: null });
443
+ try {
444
+ const res = await fetch(API_BASE + "/bridge", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
445
+ if (!res.ok) { setBridgeState({ busy: false, result: "启动接口在下次重启后生效(当前旧模块);可对 AI 说「连接我的画板」。" }); return; }
446
+ const out = await res.json();
447
+ setBridgeState({ busy: false, result: out.ok ? `已启动(${out.state})` : `未启动:${out.error || out.note || "请先安装桥接"}` });
448
+ } catch (err) { setBridgeState({ busy: false, result: "请求失败:" + String(err) }); }
449
+ };
450
+
451
+ const doSave = async () => {
452
+ if (saveState.busy) return;
453
+ setSaveState({ busy: true, out: null });
454
+ try {
455
+ const res = await fetch(API_BASE + "/snapshot", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
456
+ if (!res.ok) { setSaveState({ busy: false, out: "紧急保存接口在下次重启后生效(当前旧模块);也可对 AI 说「紧急保存」。" }); return; }
457
+ const out = await res.json();
458
+ if (!out.ok) setSaveState({ busy: false, out: "❌ " + String(out.error ?? "保存失败") });
459
+ else setSaveState({ busy: false, out: `✅ 已保存 ${out.files?.length ?? 0} 个文件 → ${out.dir ?? ""}${(out.errors?.length ?? 0) > 0 ? `(${out.errors.length} 项降级:${out.errors.slice(0, 2).join(";")})` : ""}` });
460
+ } catch (err) { setSaveState({ busy: false, out: "请求失败:" + String(err) }); }
461
+ };
462
+
463
+ return h("div", { className: "eda-panel" },
464
+ h("div", { className: "eda-head" },
465
+ h("div", { className: "eda-mark" }, "E"),
466
+ h("div", { className: "eda-tt" },
467
+ h("h1", null, "嘉立创 EDA"),
468
+ h("p", { className: "eda-sub" }, "平台仪表盘 · v" + (status?.version ?? "0.1.0"))),
469
+ h("button", { className: "eda-close", onClick: () => controller.toggle(), "aria-label": "关闭" }, "✕")),
470
+ h("div", { className: "eda-scroll" },
471
+ h(ConnectCard, { status, error, onInstall: doInstall, onStartBridge: doStartBridge, installState, bridgeState }),
472
+ h(ConnectWizard, { open: wizardOpen, setOpen: setWizardOpen }),
473
+ h(ActivityMonitor, { activities: activity.activities, sessions: activity.sessions, currentSid: activity.currentSid, setSid: activity.setSid, pinned: activity.pinned, error: activity.error, connected: ready, onRevoke: doRevoke, busyId: revokeState.busyId, onClear: doClear, clearing }),
474
+ (revokeState.out || saveState.out) ? h("p", { className: "eda-note", style: { margin: "0", wordBreak: "break-all" } }, revokeState.out ?? saveState.out) : null,
475
+ h(SnapshotCard, { connected: ready, busy: saveState.busy, onSave: doSave, lastOut: saveState.out }),
476
+ h("p", { className: "eda-foot" }, "云端实时 = 对话里生成(官方 eda.* API) · 紧急保存 = 本地留档")));
477
+ }
478
+ //#endregion
479
+
480
+ //#region mounts
481
+ const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><path d="M3.5 2.5h6l3 3v8h-9z" stroke-linejoin="round"/><path d="M9.5 2.5v3h3M6 8h4M6 10.5h2.5" stroke-linecap="round"/></svg>';
482
+
483
+ function sidebarRoot() {
484
+ const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]');
485
+ if (column === null) return undefined;
486
+ const logoOwner = column.querySelector('[class*="logoRow"]')?.parentElement;
487
+ return logoOwner ?? (column.firstElementChild ?? undefined);
488
+ }
489
+
490
+ function newSessionButton(root) {
491
+ const nested = root.querySelector('button[class*="newSession"]');
492
+ if (nested !== null) return nested;
493
+ for (const child of root.children) {
494
+ if (child.tagName === "BUTTON") return child;
495
+ }
496
+ return undefined;
497
+ }
498
+
499
+ function createEntry(controller) {
500
+ const entry = document.createElement("button");
501
+ entry.type = "button";
502
+ entry.dataset.dshEdaEntry = "";
503
+ entry.setAttribute("aria-label", "嘉立创 EDA 助手");
504
+ entry.setAttribute("title", "嘉立创 EDA 助手(官方画板平台监控)");
505
+ entry.innerHTML = '<span class="eda-entry-icon">' + ICON + '</span><span class="eda-entry-label">嘉立创 EDA</span>';
506
+ entry.addEventListener("click", () => { controller.toggle(); });
507
+ return entry;
508
+ }
509
+
510
+ function placeEntry(root, entry) {
511
+ const button = newSessionButton(root);
512
+ if (button === undefined) return false;
513
+ if (entry.parentElement !== root) {
514
+ const row = button.closest('[class*="logoRow"]');
515
+ const base = (row !== null && row.parentElement === root) ? row : button;
516
+ const family = Array.from(root.children).filter(
517
+ (el) => el instanceof HTMLElement && el.matches("[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-logcat-entry], [data-dsh-eda-entry]"),
518
+ );
519
+ const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
520
+ root.insertBefore(entry, anchor);
521
+ }
522
+ return true;
523
+ }
524
+
525
+ function mountSidebarEntry(controller) {
526
+ const entry = createEntry(controller);
527
+ let root;
528
+ let placed = false;
529
+ let rootObserver;
530
+
531
+ const tryPlace = () => {
532
+ if (root !== undefined && !root.isConnected) {
533
+ rootObserver?.disconnect();
534
+ root = undefined;
535
+ placed = false;
536
+ }
537
+ if (placed) {
538
+ if (document.body.contains(entry)) return;
539
+ rootObserver?.disconnect();
540
+ root = undefined;
541
+ placed = false;
542
+ }
543
+ root ??= sidebarRoot();
544
+ if (root === undefined) return;
545
+ placed = placeEntry(root, entry);
546
+ if (placed) {
547
+ rootObserver = new MutationObserver(() => {
548
+ if (root === undefined || !root.isConnected) { placed = false; tryPlace(); return; }
549
+ if (!root.contains(entry)) placed = placeEntry(root, entry);
550
+ });
551
+ rootObserver.observe(root, { childList: true, subtree: true });
552
+ }
553
+ };
554
+
555
+ const waitObserver = new MutationObserver(() => { tryPlace(); });
556
+ waitObserver.observe(document.body, { childList: true, subtree: true });
557
+
558
+ const syncActive = () => {
559
+ if (controller.getSnapshot().panelOpen) entry.dataset.active = "true";
560
+ else delete entry.dataset.active;
561
+ };
562
+ const unsubscribe = controller.subscribe(syncActive);
563
+ syncActive();
564
+ tryPlace();
565
+
566
+ return () => {
567
+ waitObserver.disconnect();
568
+ rootObserver?.disconnect();
569
+ unsubscribe();
570
+ entry.remove();
571
+ };
572
+ }
573
+
574
+ function mountPanel(controller) {
575
+ let root;
576
+ let container;
577
+ const ensure = () => {
578
+ if (container !== undefined && container.isConnected) return;
579
+ root?.unmount();
580
+ root = undefined;
581
+ container?.remove();
582
+ container = document.createElement("div");
583
+ container.dataset.dshEdaView = "";
584
+ container.className = "dsh-eda-view";
585
+ container.hidden = true; // side-drawer: hidden until the sidebar entry is clicked
586
+ const panel = document.createElement("div");
587
+ panel.className = "dsh-eda-panel";
588
+ container.appendChild(panel);
589
+ document.body.appendChild(container);
590
+ root = createRoot(panel);
591
+ root.render(h(EdaPanel, { controller }));
592
+ };
593
+
594
+ ensure();
595
+
596
+ const applyOpen = () => {
597
+ if (container !== undefined) container.hidden = !controller.getSnapshot().panelOpen;
598
+ };
599
+ const unsubscribe = controller.subscribe(applyOpen);
600
+ applyOpen();
601
+
602
+ return () => {
603
+ unsubscribe();
604
+ root?.unmount();
605
+ root = undefined;
606
+ container?.remove();
607
+ container = undefined;
608
+ };
609
+ }
610
+ //#endregion
611
+
612
+ //#region controller
613
+ /** Minimal open/close controller: panelOpen + a tiny subscribe store. */
614
+ function PanelController() {
615
+ let panelOpen = false;
616
+ const listeners = new Set();
617
+ this.toggle = () => { panelOpen = !panelOpen; for (const fn of listeners) fn(); };
618
+ this.getSnapshot = () => ({ panelOpen });
619
+ this.subscribe = (fn) => { listeners.add(fn); return () => { listeners.delete(fn); }; };
620
+ }
621
+ //#endregion
622
+
623
+ //#region entry
624
+ /** Required services (fiber inject waiting — the runtime must be up first). */
625
+ const inject = ["slots"];
626
+
627
+ /**
628
+ * Mount the 嘉立创 EDA 助手 panel.
629
+ * @param ctx - client root context.
630
+ */
631
+ function apply(ctx) {
632
+ const style = document.createElement("style");
633
+ style.textContent = STYLE;
634
+ style.dataset.dshEdaStyle = "";
635
+ document.head.appendChild(style);
636
+
637
+ const controller = new PanelController();
638
+ const disposers = [];
639
+ try {
640
+ disposers.push(mountSidebarEntry(controller));
641
+ disposers.push(mountPanel(controller));
642
+ } catch (error) {
643
+ console.warn("[dsh-eda] mount failed:", error);
644
+ }
645
+ ctx.effect(() => () => {
646
+ for (const dispose of disposers.splice(0)) dispose();
647
+ style.remove();
648
+ }, "dsh-eda: ui mounts");
649
+ }
650
+ //#endregion
651
+
652
+ exports.apply = apply;
653
+ exports.inject = inject;
654
+ return module.exports;
655
+ }
656
+ });