@windypro-rourou/dsh-eda 0.1.2 → 0.1.4

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 CHANGED
@@ -1,172 +1,205 @@
1
1
  /**
2
- * dsh-lichuang-eda — browser half. Runs inside the dsh web GUI.
2
+ * dsh-lichuang-eda — browser half (DSH 0.1.x / 0.2 desktop adapted).
3
3
  *
4
- * 平台仪表盘 + 监看器(生成在对话里由 agent 驱动官方 eda.* API 完成):
5
- * - header(品牌 + 状态版本)
6
- * - 连接卡:状态 + 一键动作(装/启/打开编辑器/扩展直达)
7
- * - 连接教程(可折叠,默认收起,精简步骤)
8
- * - 活动监视(主体):当前会话 agent 对官方 API 的实时调用流(动作/耗时/结果)
9
- * - 离线导出(底部小结):仅兜底,导出可导入的标准版 JSON
4
+ * Design read: a technical side-panel for the shell's overlay layer — dark-first,
5
+ * editorial and restrained, wearing the host's own tokens + UI primitives so it
6
+ * reads as native chrome rather than a bolted-on widget.
10
7
  *
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 the DSH client-bundle contract.
8
+ * 0.2 adaptation (the "挡到" fix):
9
+ * - the panel is registered into the `shell.overlay` SLOT (position:absolute,
10
+ * z-index 30) instead of a fixed body overlay with a 9999 z-index — it no
11
+ * longer covers the conversation, other panels, or the desktop chrome;
12
+ * - the entry point is a `conversation.session.header.utilities` pill (0.2's
13
+ * left sidebar is slot-owned); the 0.1.x sidebar DOM entry is kept as a
14
+ * compatibility path and the two never fight;
15
+ * - the panel is drag-resizable (120–760px, persisted) and follows the host
16
+ * theme through `--dsw-alias-*` tokens.
17
+ *
18
+ * Bundle format: `window.__ModuleLoader__.load({id, factory})` (lazy CJS).
14
19
  */
15
20
  window.__ModuleLoader__.load({
16
21
  id: "@windypro-rourou/dsh-eda",
17
22
  factory: (require) => {
18
23
  var module = { exports: {} };
19
24
  var exports = module.exports;
25
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
20
26
 
21
- const { createElement: h, useEffect, useState } = require("react");
27
+ const React = require("react");
28
+ const { createElement: h, useEffect, useMemo, useRef, useState } = React;
22
29
  const { createRoot } = require("react-dom/client");
23
30
 
31
+ // ── host UI kit (same components the native chrome uses); degrade to our
32
+ // own token-based markup when a build does not ship it. ────────────────
33
+ let P = null;
34
+ try { P = require("@deepseek-ai/dsh-client-ui-primitives"); } catch (e) { P = null; }
35
+ const dsButton = P && P.Button ? P.Button : null;
36
+ const dsPill = P && P.Pill ? P.Pill : null;
37
+ const dsTooltip = P && P.Tooltip ? P.Tooltip : null;
38
+ const dsIcon = (name) => (P && P[name] ? P[name] : null);
39
+
24
40
  //#region styles
41
+ /**
42
+ * Token-first styling: every colour comes from `--dsw-alias-*` with a
43
+ * dark-first fallback, so the panel inherits light/dark from the shell.
44
+ * Reused accent = one blue; no gradients, no heavy shadows (taste rule:
45
+ * restraint over decoration).
46
+ */
25
47
  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;
48
+ .dsh-eda-root {
49
+ --eda-bg: var(--dsw-alias-bg-layer-1, #151922);
50
+ --eda-bg-base: var(--dsw-alias-bg-base, #0f1115);
51
+ --eda-bg-soft: var(--dsw-alias-bg-layer-2, #1a1f2b);
52
+ --eda-fg: var(--dsw-alias-label-primary, #d7dde6);
53
+ --eda-fg-2: var(--dsw-alias-label-secondary, #b6bdc9);
54
+ --eda-fg-3: var(--dsw-alias-label-tertiary, #8b93a3);
55
+ --eda-line: var(--dsw-alias-border-l1, #262a33);
56
+ --eda-line-soft: var(--dsw-alias-border-l2, rgba(255,255,255,.06));
57
+ --eda-accent: #4c8dff;
58
+ --eda-ok: var(--dsw-alias-state-success-primary, #22c55e);
59
+ --eda-warn: var(--dsw-alias-state-warn-primary, #eab308);
60
+ --eda-err: var(--dsw-alias-state-error-primary, #ef4444);
61
+ --eda-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
62
+ --eda-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
63
+
64
+ position: absolute; top: 0; right: 0; bottom: 0;
65
+ width: var(--eda-w, 400px); max-width: 96vw;
66
+ display: flex; flex-direction: column; min-height: 0;
67
+ background: var(--eda-bg); color: var(--eda-fg);
68
+ border-left: 1px solid var(--eda-line);
69
+ font: 13px/1.55 var(--eda-sans);
70
+ font-variant-numeric: tabular-nums;
71
+ z-index: 30;
72
+ box-shadow: -14px 0 40px rgba(0,0,0,.22);
73
+ animation: edaIn .16s cubic-bezier(.22,.61,.36,1);
30
74
  }
75
+ .dsh-eda-root[hidden] { display: none !important; }
76
+ @keyframes edaIn { from { transform: translateX(14px); opacity: 0; } to { transform: none; opacity: 1; } }
77
+ @media (prefers-reduced-motion: reduce) { .dsh-eda-root { animation: none; } }
78
+
79
+ .dsh-eda-grip { position: absolute; left: -3px; top: 0; bottom: 0; width: 7px; cursor: col-resize; z-index: 6; }
80
+ .dsh-eda-grip:hover { background: linear-gradient(90deg, transparent, rgba(76,141,255,.35)); }
81
+
82
+ /* top bar — sticky, quiet, 48px */
83
+ .eda-top { flex: none; height: 48px; display: flex; align-items: center; gap: 10px; padding: 0 12px 0 14px;
84
+ border-bottom: 1px solid var(--eda-line); background: var(--eda-bg); }
85
+ .eda-mark { flex: none; width: 24px; height: 24px; border-radius: 7px; display: grid; place-items: center;
86
+ color: #fff; background: linear-gradient(160deg, #3b7bf6, #2b5fd0); font-size: 11px; font-weight: 700; letter-spacing: .02em; }
87
+ .eda-title { min-width: 0; flex: 1; }
88
+ .eda-title b { display: block; font-size: 13px; font-weight: 600; letter-spacing: .01em; }
89
+ .eda-title span { display: block; font-size: 10.5px; color: var(--eda-fg-3); font-family: var(--eda-mono); }
90
+ .eda-iconbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border-radius: 7px;
91
+ border: 1px solid transparent; background: transparent; color: var(--eda-fg-3); cursor: pointer; font-size: 12px; line-height: 1; }
92
+ .eda-iconbtn:hover { color: var(--eda-fg); background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
93
+
94
+ .eda-scroll { flex: 1 1 0; min-height: 0; overflow-y: auto; padding: 14px 14px 18px;
95
+ display: flex; flex-direction: column; gap: 12px; scrollbar-width: thin; }
96
+ .eda-scroll::-webkit-scrollbar { width: 9px; }
97
+ .eda-scroll::-webkit-scrollbar-thumb { background: var(--eda-line); border-radius: 9px; border: 2px solid transparent; background-clip: content-box; }
98
+
99
+ .eda-card { border: 1px solid var(--eda-line); border-radius: 10px; background: var(--eda-bg-soft); padding: 12px 13px; }
100
+ .eda-card-title { font-size: 10.5px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
101
+ color: var(--eda-fg-3); margin: 0 0 10px; }
102
+ .eda-note { font-size: 11.5px; color: var(--eda-fg-2); margin: 9px 0 0; line-height: 1.6; }
103
+ .eda-muted { font-size: 11px; color: var(--eda-fg-3); }
104
+ .eda-mono { font-family: var(--eda-mono); }
105
+
106
+ /* status row */
107
+ .eda-state { display: flex; align-items: center; gap: 9px; }
108
+ .eda-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--eda-fg-3); }
109
+ .eda-dot.on { background: var(--eda-ok); box-shadow: 0 0 0 3px rgba(34,197,94,.16); }
110
+ .eda-dot.off { background: var(--eda-warn); }
111
+ .eda-dot.wait { background: var(--eda-fg-3); animation: edaPulse 1.3s ease-in-out infinite; }
112
+ @keyframes edaPulse { 0%,100% { opacity: .4 } 50% { opacity: 1 } }
113
+ .eda-state b { font-size: 12.5px; font-weight: 600; }
114
+ .eda-state small { font-family: var(--eda-mono); font-size: 10.5px; color: var(--eda-fg-3); margin-left: 7px; }
115
+
116
+ .eda-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
117
+ .eda-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
118
+ border: 1px solid var(--eda-line); border-radius: 8px; padding: 5px 11px; background: transparent;
119
+ color: var(--eda-fg); font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
120
+ transition: background .12s ease, border-color .12s ease, opacity .12s ease; }
121
+ .eda-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
122
+ .eda-btn:disabled { opacity: .45; cursor: default; }
123
+ .eda-btn.primary { background: var(--eda-accent); border-color: transparent; color: #fff; }
124
+ .eda-btn.primary:hover:not(:disabled) { background: #3f80f5; }
125
+ .eda-btn.big { width: 100%; padding: 8px 12px; font-size: 12.5px; }
126
+ .eda-btn.tiny { padding: 2px 8px; font-size: 10.5px; font-weight: 500; }
127
+ .eda-link { color: var(--eda-accent); text-decoration: none; font-size: 11.5px; }
128
+ .eda-link:hover { text-decoration: underline; }
129
+
130
+ /* connected CTA */
131
+ .eda-cta { display: flex; align-items: center; gap: 10px; margin-top: 11px; padding: 9px 11px;
132
+ border: 1px solid color-mix(in srgb, var(--eda-accent) 34%, transparent); border-radius: 9px;
133
+ background: color-mix(in srgb, var(--eda-accent) 8%, transparent); cursor: pointer; }
134
+ .eda-cta:hover { background: color-mix(in srgb, var(--eda-accent) 13%, transparent); }
135
+ .eda-cta-t { min-width: 0; flex: 1; }
136
+ .eda-cta-t i { display: block; font-style: normal; font-size: 10px; color: var(--eda-fg-3); letter-spacing: .04em; }
137
+ .eda-cta-t b { font-size: 12.5px; font-weight: 600; color: var(--eda-accent); }
138
+ .eda-cta-c { flex: none; font-size: 10.5px; color: var(--eda-fg-3); }
139
+ .eda-cta.ok .eda-cta-c { color: var(--eda-ok); }
140
+
141
+ /* guide */
142
+ .eda-steps { display: flex; flex-direction: column; gap: 10px; }
143
+ .eda-step { display: flex; gap: 10px; }
144
+ .eda-step-no { flex: none; width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center;
145
+ font-family: var(--eda-mono); font-size: 10px; color: var(--eda-fg-3); border: 1px solid var(--eda-line); margin-top: 1px; }
146
+ .eda-step b { display: block; font-size: 12px; font-weight: 600; }
147
+ .eda-step p { margin: 2px 0 0; font-size: 11px; color: var(--eda-fg-3); line-height: 1.55; }
148
+
149
+ /* timeline */
150
+ .eda-tl-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
151
+ .eda-tl-head .eda-card-title { flex: 1; margin: 0; }
152
+ .eda-select { font: inherit; font-size: 10.5px; font-family: var(--eda-mono); color: var(--eda-fg-2);
153
+ background: var(--eda-bg-base); border: 1px solid var(--eda-line); border-radius: 6px; padding: 2px 5px; max-width: 132px; }
154
+ .eda-tl { position: relative; margin: 6px 0 0; }
155
+ .eda-row { position: relative; display: flex; gap: 10px; padding: 8px 0 8px 2px; cursor: pointer;
156
+ border-bottom: 1px solid var(--eda-line-soft); }
157
+ .eda-row:last-child { border-bottom: 0; }
158
+ .eda-row:hover { background: color-mix(in srgb, var(--eda-fg) 4%, transparent); }
159
+ .eda-row-dot { flex: none; width: 7px; height: 7px; margin-top: 6px; border-radius: 50%; background: var(--eda-fg-3); position: relative; z-index: 1; }
160
+ .eda-row.ok .eda-row-dot { background: var(--eda-ok); }
161
+ .eda-row.err .eda-row-dot { background: var(--eda-err); }
162
+ .eda-row.pend .eda-row-dot { background: var(--eda-warn); animation: edaPulse 1s ease-in-out infinite; }
163
+ .eda-row-body { min-width: 0; flex: 1; }
164
+ .eda-row-line { display: flex; align-items: baseline; gap: 8px; }
165
+ .eda-seq { flex: none; font-family: var(--eda-mono); font-size: 10px; color: var(--eda-fg-3); }
166
+ .eda-row-name { flex: 1; min-width: 0; font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
167
+ .eda-row-meta { flex: none; font-family: var(--eda-mono); font-size: 10px; color: var(--eda-fg-3); }
168
+ .eda-row-meta .eda-pend { color: var(--eda-warn); }
169
+ .eda-row-code, .eda-row-out { margin-top: 3px; font-family: var(--eda-mono); font-size: 10.5px; color: var(--eda-fg-3);
170
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
171
+ .eda-row-out.err { color: var(--eda-err); }
172
+ .eda-row-full { margin-top: 7px; padding: 8px 9px; border-radius: 8px; background: var(--eda-bg-base);
173
+ border: 1px solid var(--eda-line-soft); font-family: var(--eda-mono); font-size: 10.5px; line-height: 1.6;
174
+ color: var(--eda-fg-2); white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow: auto; }
175
+ .eda-row-full i { display: block; font-style: normal; font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
176
+ color: var(--eda-fg-3); margin: 6px 0 2px; }
177
+ .eda-row-full i:first-child { margin-top: 0; }
178
+ .eda-empty { font-size: 11.5px; color: var(--eda-fg-3); line-height: 1.6; }
179
+ .eda-hints { margin-top: 9px; display: flex; flex-direction: column; gap: 5px; }
180
+ .eda-hint { display: flex; gap: 7px; font-size: 11px; color: var(--eda-fg-2); }
181
+ .eda-hint s { text-decoration: none; color: var(--eda-fg-3); }
182
+ .eda-tl-foot { margin-top: 7px; font-size: 10.5px; color: var(--eda-fg-3); }
183
+
184
+ .eda-foot { flex: none; padding: 9px 14px; border-top: 1px solid var(--eda-line);
185
+ font-family: var(--eda-mono); font-size: 10px; color: var(--eda-fg-3); text-align: center; }
186
+
187
+ /* header pill (0.2 entry point) */
188
+ .eda-pill { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 9px;
189
+ border: 1px solid var(--eda-line); border-radius: 999px; background: transparent; color: inherit;
190
+ font: inherit; font-size: 11.5px; cursor: pointer; transition: background .12s ease; }
191
+ .eda-pill:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12)); }
192
+ .eda-pill[data-active="true"] { border-color: color-mix(in srgb, var(--eda-accent) 46%, transparent);
193
+ background: color-mix(in srgb, var(--eda-accent) 12%, transparent); }
194
+ .eda-pill .eda-dot { width: 6px; height: 6px; }
195
+ .eda-pill[hidden] { display: none !important; }
196
+
197
+ /* 0.1.x sidebar entry */
198
+ [data-dsh-eda-entry] { display: flex; align-items: center; gap: 8px; width: 100%; padding: 9px 12px; margin: 2px 0;
199
+ border: 0; border-radius: 8px; background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left; }
31
200
  [data-dsh-eda-entry]:hover { background: rgba(128,128,128,.14); }
32
201
  [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
202
  [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
203
  `;
171
204
  //#endregion
172
205
 
@@ -174,16 +207,37 @@ window.__ModuleLoader__.load({
174
207
  const LINKS = {
175
208
  editor: "https://pro.lceda.cn/editor",
176
209
  ext: "https://jlc-ext.com/item/oshwhub/run-api-gateway",
177
- skill: "https://github.com/easyeda/easyeda-api-skill",
178
210
  };
179
-
180
- //#region data
181
211
  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: "刷新编辑器页面;面板状态变绿 = 已连上你的云画板。" },
212
+ { n: 1, t: "一键安装官方桥", d: "官方 easyeda-api-skill → 本机 ~/.dsh/eda/bridge/(离线依赖,无需 npm)。" },
213
+ { n: 2, t: "启动官方桥", d: "Bridge Server 监听 127.0.0.1:49620-49629,插件自动探测。" },
214
+ { n: 3, t: "网页版装扩展 Run API Gateway", d: "编辑器 →「高级」→「扩展管理器」→ 安装;再开「外部交互」+「显示在顶部菜单」。" },
215
+ { n: 4, t: "验证", d: "刷新编辑器页面;状态点变绿 = 已连上你的云画板。" },
216
+ ];
217
+ const HINTS = [
218
+ "放置任意大类元件(R/C/L/二极管/LED/MCU…)并框内定位",
219
+ "引脚级连线 + VCC/GND 网络标志",
220
+ "原理图 / PCB 的 DRC、网表、BOM 导出",
221
+ "PCB 元件 / 过孔 / 走线(含底层铜)+ 现场截图",
222
+ "随时「紧急保存」到本地并留动作日志",
186
223
  ];
224
+ const ICON_EDA = "M3 3.6h7.2l2.8 2.8v6h-10z M10.2 3.6v2.8h2.8 M5 8.4h3 M5 10.4h2";
225
+
226
+ //#region store
227
+ /** Tiny external store: panel open/closed, shared by the pill and the overlay. */
228
+ function PanelStore() {
229
+ const listeners = new Set();
230
+ let open = false;
231
+ this.get = () => open;
232
+ this.set = (v) => { const next = !!v; if (next === open) return; open = next; for (const fn of listeners) fn(); };
233
+ this.toggle = () => this.set(!open);
234
+ this.subscribe = (fn) => { listeners.add(fn); return () => listeners.delete(fn); };
235
+ }
236
+ function useStore(store) {
237
+ const [v, setV] = useState(store.get());
238
+ useEffect(() => store.subscribe(() => setV(store.get())), [store]);
239
+ return v;
240
+ }
187
241
  //#endregion
188
242
 
189
243
  //#region hooks
@@ -192,12 +246,10 @@ window.__ModuleLoader__.load({
192
246
  const [error, setError] = useState(false);
193
247
  useEffect(() => {
194
248
  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
- };
249
+ const load = () => fetch(API_BASE + "/status")
250
+ .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
251
+ .then((b) => { if (alive) { setStatus(b); setError(false); } })
252
+ .catch(() => { if (alive) { setStatus(null); setError(true); } });
201
253
  load();
202
254
  const t = setInterval(load, 4000);
203
255
  return () => { alive = false; clearInterval(t); };
@@ -205,14 +257,8 @@ window.__ModuleLoader__.load({
205
257
  return [status, error];
206
258
  }
207
259
 
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("");
260
+ function useActivity(pinnedSid) {
261
+ const [feed, setFeed] = useState({ activities: [], sessions: [], currentSid: "", error: false });
216
262
  useEffect(() => {
217
263
  let alive = true;
218
264
  const load = () => {
@@ -221,7 +267,12 @@ window.__ModuleLoader__.load({
221
267
  .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
222
268
  .then((b) => {
223
269
  if (!alive) return;
224
- setFeed({ activities: Array.isArray(b.activities) ? b.activities : [], sessions: Array.isArray(b.sessions) ? b.sessions : [], currentSid: b.currentSid ?? "", error: false });
270
+ setFeed({
271
+ activities: Array.isArray(b.activities) ? b.activities : [],
272
+ sessions: Array.isArray(b.sessions) ? b.sessions : [],
273
+ currentSid: b.currentSid ?? "",
274
+ error: false,
275
+ });
225
276
  })
226
277
  .catch(() => { if (alive) setFeed((prev) => ({ ...prev, error: true })); });
227
278
  };
@@ -229,340 +280,385 @@ window.__ModuleLoader__.load({
229
280
  const t = setInterval(load, 3000);
230
281
  return () => { alive = false; clearInterval(t); };
231
282
  }, [pinnedSid]);
232
- return { ...feed, setSid: setPinnedSid, pinned: pinnedSid };
283
+ return feed;
233
284
  }
234
285
  //#endregion
235
286
 
236
287
  //#region components
237
- /** Compact connection card: state + one-click platform actions + dialog CTA. */
238
- function ConnectCard({ status, error, onInstall, onStartBridge, installState, bridgeState }) {
288
+ /** Brand mark: host icon when available, else a two-stroke chip glyph. */
289
+ function Mark() {
290
+ const Icon = dsIcon("IconBoardOutline16") || dsIcon("IconComponentOutline16");
291
+ if (Icon) return h("span", { className: "eda-mark" }, h(Icon, null));
292
+ return h("span", { className: "eda-mark" }, "EDA");
293
+ }
294
+
295
+ function SendIcon() {
296
+ return h("svg", { width: 13, height: 13, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": "true" },
297
+ h("path", { d: "M2.6 3.4l10.8 4.6-10.8 4.6 1.7-4.6z", strokeLinejoin: "round" }));
298
+ }
299
+
300
+ /** Header pill — the 0.2 entry point (slot-owned header utilities). */
301
+ function HeaderPill({ store }) {
302
+ const open = useStore(store);
303
+ const [status] = useStatus();
304
+ const ready = status?.connected === true;
305
+ const cls = "eda-pill";
306
+ const inner = [
307
+ h("span", { className: "eda-dot " + (status === null ? "wait" : ready ? "on" : "off"), key: "d" }),
308
+ h("span", { key: "t" }, "嘉立创 EDA"),
309
+ ];
310
+ if (dsButton) {
311
+ return h(dsButton, {
312
+ appearance: "ghost", size: "small", "data-active": open ? "true" : "false",
313
+ onClick: () => store.toggle(), title: "嘉立创 EDA 助手", className: cls,
314
+ }, inner);
315
+ }
316
+ return h("button", {
317
+ type: "button", className: cls, "data-active": open ? "true" : "false",
318
+ onClick: () => store.toggle(), title: "嘉立创 EDA 助手",
319
+ }, inner);
320
+ }
321
+
322
+ /**
323
+ * Connection card: one status line, then the three platform actions, then
324
+ * (when connected) the copy-the-prompt CTA. Errors stay inline and quiet.
325
+ */
326
+ function ConnectCard({ status, error, install, bridge, onInstall, onStart }) {
239
327
  const ready = status?.connected === true;
240
328
  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
329
  const inst = status?.bridgeInstalled === true;
245
- const copyPrompt = () => {
330
+ const dot = status === null ? (error ? "off" : "wait") : ready ? "on" : "off";
331
+ const label = status === null ? (error ? "状态服务未响应" : "检测中…") : ready ? "已连接" : "未连接";
332
+ const port = ready && status?.port ? "127.0.0.1:" + status.port : "";
333
+ const win = status?.health?.match(/edaWindowCount:(\d+)/)?.[1];
334
+ const copy = () => {
246
335
  const text = "嘉立创EDA,启动!帮我画一个 LED 点亮电路";
247
- try { navigator.clipboard?.writeText(text); } catch { /* clipboard may be unavailable */ }
336
+ try { navigator.clipboard?.writeText(text); } catch (e) { /* clipboard unavailable */ }
248
337
  setCopied(true);
249
338
  setTimeout(() => setCopied(false), 1600);
250
339
  };
251
340
  return h("div", { className: "eda-card" },
252
- h("div", { className: "eda-status" },
341
+ h("div", { className: "eda-state" },
253
342
  h("span", { className: "eda-dot " + dot }),
254
- h("div", { className: "eda-txt" }, state, win !== null ? h("small", null, "窗口 " + win) : null)),
343
+ h("b", null, label),
344
+ h("small", null, [port, win !== undefined ? "窗口 " + win : ""].filter(Boolean).join(" · "))),
255
345
  h("div", { className: "eda-actions" },
256
346
  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 ? "启动中…" : "启动官方桥"),
347
+ className: "eda-btn " + (inst ? "" : "primary"),
348
+ disabled: inst || install?.busy, onClick: onInstall,
349
+ title: inst ? "官方桥已安装(随本机持久)" : "下载官方 easyeda-api-skill 到本机",
350
+ }, inst ? "官方桥已装 ✓" : install?.busy ? "安装中…" : "一键安装官方桥"),
351
+ h("button", { className: "eda-btn", disabled: bridge?.busy, onClick: onStart },
352
+ bridge?.busy ? "启动中…" : "启动桥"),
263
353
  h("a", { className: "eda-link", href: LINKS.editor, target: "_blank", rel: "noreferrer" }, "打开编辑器"),
264
354
  h("a", { className: "eda-link", href: LINKS.ext, target: "_blank", rel: "noreferrer" }, "扩展直达")),
265
355
  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
- );
356
+ ? h("div", { className: "eda-cta" + (copied ? " ok" : ""), onClick: copy,
357
+ title: "点击复制,然后粘贴到对话里发送" },
358
+ h(SendIcon, null),
359
+ h("span", { className: "eda-cta-t" },
360
+ h("i", null, "去对话里说"),
361
+ h("b", null, "「嘉立创EDA,启动!」")),
362
+ h("span", { className: "eda-cta-c" }, copied ? "已复制 ✓" : "复制"))
363
+ : h("p", { className: "eda-note" }, "连上画板后,在对话里描述需求即可实时生成;离线导出仅作兜底。"),
364
+ install?.result ? h("p", { className: "eda-note" }, install.result) : null,
365
+ bridge?.result ? h("p", { className: "eda-note" }, bridge.result) : null);
276
366
  }
277
367
 
278
- /** Collapsible connect guide (compact). */
279
- function ConnectWizard({ open, setOpen }) {
368
+ function ConnectGuide({ open, setOpen }) {
280
369
  if (!open) {
281
- return h("button", { className: "eda-btn ghost", style: { width: "100%" }, onClick: () => setOpen(true) }, "连接教程(4 步 · 网页版)");
370
+ return h("button", { className: "eda-btn", style: { width: "100%" }, onClick: () => setOpen(true) },
371
+ "连接教程 · 4 步(网页版官方栈)");
282
372
  }
283
373
  return h("div", { className: "eda-card" },
284
- h("div", { className: "eda-card-title" }, "连接教程(官方栈 · 网页版)"),
374
+ h("div", { className: "eda-card-title" }, "连接教程"),
285
375
  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 } }, "收起")));
376
+ h("div", { className: "eda-step", key: s.n },
377
+ h("span", { className: "eda-step-no" }, String(s.n)),
378
+ h("div", null, h("b", null, s.t), h("p", null, s.d))))),
379
+ h("p", { className: "eda-note" },
380
+ h("a", { className: "eda-link", href: LINKS.editor, target: "_blank", rel: "noreferrer", style: { marginRight: 12 } }, "编辑器"),
381
+ h("a", { className: "eda-link", href: LINKS.ext, target: "_blank", rel: "noreferrer", style: { marginRight: 12 } }, "扩展"),
382
+ h("button", { className: "eda-link", onClick: () => setOpen(false),
383
+ style: { border: 0, background: "transparent", cursor: "pointer", padding: 0, font: "inherit" } }, "收起")));
294
384
  }
295
385
 
296
386
  /**
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).
387
+ * Record-style timeline (persisted on disk → never blank). Each step is
388
+ * numbered, expandable, and revocable while the bridge is connected.
302
389
  */
303
- function ActivityMonitor({ activities, sessions, currentSid, setSid, pinned, error, connected, onRevoke, busyId, onClear, clearing }) {
390
+ function Timeline({ feed, pinned, setSid, connected, onRevoke, busyId, onClear, clearing, note }) {
304
391
  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;
392
+ const toggle = (id) => setExpanded((prev) => {
393
+ const next = new Set(prev);
394
+ if (next.has(id)) next.delete(id); else next.add(id);
395
+ return next;
396
+ });
397
+ const list = feed.activities;
398
+ const sids = feed.sessions;
399
+ const multi = sids.length > 1;
315
400
  return h("div", { className: "eda-card" },
316
- h("div", { className: "eda-act-head" },
317
- h("div", { className: "eda-card-title" }, "Agent 正在使用官方 API"),
318
- hasMulti
401
+ h("div", { className: "eda-tl-head" },
402
+ h("div", { className: "eda-card-title" }, "Agent · 官方 API"),
403
+ multi
319
404
  ? h("select", {
320
- className: "eda-sess",
321
- value: pinned ?? "",
322
- onChange: (e) => setSid(e.target.value),
405
+ className: "eda-select", value: pinned ?? "", onChange: (e) => setSid(e.target.value),
323
406
  title: "切换会话(默认跟随最新)",
324
407
  },
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),
408
+ h("option", { value: "" }, "跟随最新"),
409
+ sids.map((s) => h("option", { value: s.sid ?? "", key: (s.sid || "platform") + s.lastTs }, s.label ?? "会话")))
410
+ : null,
328
411
  list.length > 0
329
- ? h("button", { className: "eda-btn ghost", style: { padding: "3px 9px", fontSize: "10.5px" }, disabled: clearing, onClick: onClear, title: "清空时间线(含磁盘历史)" }, clearing ? "清空中…" : "清空记录")
412
+ ? h("button", { className: "eda-btn tiny", disabled: clearing, onClick: onClear, title: "清空时间线(含磁盘历史)" },
413
+ clearing ? "清空…" : "清空")
330
414
  : null),
331
415
  list.length === 0
332
416
  ? 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))))
417
+ h("div", { className: "eda-empty" }, feed.error
418
+ ? "实时活动流未就绪(服务端模块未加载)。"
419
+ : "还没有记录。历史写入本机 activity.jsonl,面板不会空着。可以让 agent:"),
420
+ h("div", { className: "eda-hints" }, HINTS.map((t, i) =>
421
+ h("div", { className: "eda-hint", key: i }, h("s", null, "·"), h("span", null, t)))))
339
422
  : 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;
423
+ h("div", { className: "eda-tl" }, list.slice(0, 60).map((a, i) => {
424
+ const pend = a.status === "pending";
425
+ const open = expanded.has(a.id);
426
+ const revokable = connected && !pend && Array.isArray(a.revoke?.created) && a.revoke.created.length > 0;
427
+ const time = String(a.ts ?? "").slice(11, 19);
428
+ const meta = pend
429
+ ? h("span", { className: "eda-pend" }, "执行中…")
430
+ : [time, a.durationMs ? a.durationMs + "ms" : "", a.tool ?? ""].filter(Boolean).join(" · ");
345
431
  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),
432
+ className: "eda-row " + (pend ? "pend" : a.ok === false ? "err" : a.ok ? "ok" : ""),
433
+ key: a.id ?? i, onClick: () => toggle(a.id),
349
434
  },
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 ?? ""))),
435
+ h("span", { className: "eda-row-dot" }),
436
+ h("div", { className: "eda-row-body" },
437
+ h("div", { className: "eda-row-line" },
438
+ h("span", { className: "eda-seq" }, "#" + (a.id ?? i)),
439
+ h("span", { className: "eda-row-name" }, a.action ?? a.tool ?? "调用 API"),
440
+ h("span", { className: "eda-row-meta" }, meta),
358
441
  revokable
359
442
  ? h("button", {
360
- className: "eda-btn ghost eda-revoke",
361
- disabled: busyId === a.id,
443
+ className: "eda-btn tiny", disabled: busyId === a.id,
362
444
  onClick: (e) => { e.stopPropagation(); onRevoke(a.id); },
363
445
  title: "撤回该步(删除它新建的图元)",
364
- }, busyId === a.id ? "撤回中…" : "撤回")
446
+ }, busyId === a.id ? "撤回…" : "撤回")
365
447
  : 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));
448
+ a.code ? h("div", { className: "eda-row-code" }, a.code) : null,
449
+ !pend && a.ok === false && a.error
450
+ ? h("div", { className: "eda-row-out err" }, a.error)
451
+ : !pend && a.result ? h("div", { className: "eda-row-out" }, a.result) : null,
452
+ open ? h("div", { className: "eda-row-full" },
453
+ a.code ? h("div", null, h("i", null, "代码"), a.code) : null,
454
+ a.revoke ? h("div", null, h("i", null, "撤回数据"),
455
+ `新建 ${a.revoke.created.length} 图元` + (a.revoke.deletedCount ? ` · 删除 ${a.revoke.deletedCount}(不可恢复)` : "")) : null,
456
+ a.result ? h("div", null, h("i", null, "结果"), a.result) : null,
457
+ a.error ? h("div", null, h("i", null, "错误"), a.error) : null) : null));
374
458
  })),
375
- h("div", { className: "eda-act-hint" }, (hasMulti ? "下拉切换会话 · " : "") + "点击条目查看完整内容 · 撤回仅删除该步新建图元")));
459
+ h("div", { className: "eda-tl-foot" }, (multi ? "下拉切换会话 · " : "") + "点击条目展开 · 撤回只删该步新建图元")),
460
+ note ? h("p", { className: "eda-note", style: { wordBreak: "break-word" } }, note) : null);
376
461
  }
377
462
 
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 }) {
463
+ /** Emergency save: board state (.epro2 + SVG + netlist/BOM) + action log → disk. */
464
+ function SnapshotCard({ connected, state, onSave }) {
384
465
  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 动作日志(每一步留档)。"));
466
+ h("div", { className: "eda-card-title" }, "紧急保存"),
467
+ h("button", { className: "eda-btn primary big", disabled: state.busy, onClick: onSave },
468
+ state.busy ? "保存中…" : "🛟 保存画板到本地"),
469
+ h("p", { className: "eda-muted", style: { marginTop: 8 } }, connected
470
+ ? "专业版 .epro2(可完整恢复)+ 预览 SVG + 网表/BOM。云端没同步上也能找回最后的工程。"
471
+ : "画板未连接:仍会保存 agent 动作日志,每一步留档。"),
472
+ state.out ? h("p", { className: "eda-note", style: { wordBreak: "break-all" } }, state.out) : null);
393
473
  }
394
- //#endregion
395
474
 
396
- //#region panel
397
- function EdaPanel({ controller }) {
475
+ /** Panel shell: top bar → scroll body → footer. */
476
+ function EdaPanel({ store }) {
398
477
  const [status, error] = useStatus();
399
478
  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 });
479
+ const [pinned, setPinned] = useState("");
480
+ const feed = useActivity(pinned);
481
+ const [guide, setGuide] = useState(false);
482
+ const [install, setInstall] = useState({ busy: false, result: null });
483
+ const [bridge, setBridge] = useState({ busy: false, result: null });
484
+ const [save, setSave] = useState({ busy: false, out: null });
485
+ const [revoke, setRevoke] = useState({ busyId: null, out: null });
406
486
  const [clearing, setClearing] = useState(false);
407
487
 
408
- const doRevoke = async (id) => {
409
- if (revokeState.busyId !== null) return;
410
- setRevokeState({ busyId: id, out: null });
488
+ const post = (path) => fetch(API_BASE + path, {
489
+ method: "POST", headers: { "content-type": "application/json" }, body: "{}",
490
+ });
491
+
492
+ const doInstall = async () => {
493
+ if (install.busy) return;
494
+ setInstall({ busy: true, result: null });
411
495
  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; }
496
+ const res = await post("/install");
497
+ if (!res.ok) { setInstall({ busy: false, result: "安装接口未就绪(旧模块),可对 AI 说「连接我的画板」。" }); return; }
414
498
  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) }); }
499
+ setInstall({ busy: false, result: out.ok ? "✅ " + (out.script ?? "官方桥已安装") : "❌ " + (out.error ?? "安装失败") });
500
+ } catch (e) { setInstall({ busy: false, result: "请求失败:" + String(e) }); }
417
501
  };
418
- const doClear = async () => {
419
- if (clearing) return;
420
- setClearing(true);
502
+ const doStart = async () => {
503
+ if (bridge.busy) return;
504
+ setBridge({ busy: true, result: null });
421
505
  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); }
506
+ const res = await post("/bridge");
507
+ if (!res.ok) { setBridge({ busy: false, result: "启动接口未就绪(旧模块),可对 AI 说「连接我的画板」。" }); return; }
508
+ const out = await res.json();
509
+ setBridge({ busy: false, result: out.ok ? `已启动(${out.state})` : `未启动:${out.error || out.note || "请先安装桥接"}` });
510
+ } catch (e) { setBridge({ busy: false, result: "请求失败:" + String(e) }); }
427
511
  };
428
-
429
- const doInstall = async () => {
430
- if (installState.busy) return;
431
- setInstallState({ busy: true, result: null });
512
+ const doSave = async () => {
513
+ if (save.busy) return;
514
+ setSave({ busy: true, out: null });
432
515
  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; }
516
+ const res = await post("/snapshot");
517
+ if (!res.ok) { setSave({ busy: false, out: "紧急保存接口未就绪(旧模块),可对 AI 说「紧急保存」。" }); return; }
435
518
  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) }); }
519
+ setSave({
520
+ busy: false,
521
+ out: out.ok
522
+ ? `✅ ${out.files?.length ?? 0} 个文件 → ${out.dir ?? ""}` + ((out.errors?.length ?? 0) > 0 ? `(${out.errors.length} 项降级)` : "")
523
+ : "❌ " + String(out.error ?? "保存失败"),
524
+ });
525
+ } catch (e) { setSave({ busy: false, out: "请求失败:" + String(e) }); }
438
526
  };
439
-
440
- const doStartBridge = async () => {
441
- if (bridgeState.busy) return;
442
- setBridgeState({ busy: true, result: null });
527
+ const doRevoke = async (id) => {
528
+ if (revoke.busyId !== null) return;
529
+ setRevoke({ busyId: id, out: null });
443
530
  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; }
531
+ const res = await fetch(API_BASE + "/activity/revoke?id=" + encodeURIComponent(id), {
532
+ method: "POST", headers: { "content-type": "application/json" }, body: "{}",
533
+ });
534
+ if (!res.ok) { setRevoke({ busyId: null, out: "撤回接口未就绪(旧模块)。" }); return; }
446
535
  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) }); }
536
+ setRevoke({ busyId: null, out: out.ok ? "↩️ 已撤回:" + (out.note ?? "") : "⚠️ " + String(out.error ?? "撤回失败") });
537
+ } catch (e) { setRevoke({ busyId: null, out: "请求失败:" + String(e) }); }
449
538
  };
450
-
451
- const doSave = async () => {
452
- if (saveState.busy) return;
453
- setSaveState({ busy: true, out: null });
539
+ const doClear = async () => {
540
+ if (clearing) return;
541
+ setClearing(true);
454
542
  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) }); }
543
+ const res = await post("/activity/clear");
544
+ setRevoke({ busyId: null, out: res.ok ? "🗑️ 时间线已清空" : "清空接口未就绪(旧模块)。" });
545
+ } catch (e) { setRevoke({ busyId: null, out: "请求失败:" + String(e) }); }
546
+ finally { setClearing(false); }
461
547
  };
462
548
 
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": "关闭" }, "✕")),
549
+ return h("div", { className: "dsh-eda-root" },
550
+ h("div", { className: "eda-top" },
551
+ h(Mark, null),
552
+ h("div", { className: "eda-title" },
553
+ h("b", null, "嘉立创 EDA"),
554
+ h("span", null, "official bridge · v" + (status?.version ?? "—"))),
555
+ h("button", { className: "eda-iconbtn", onClick: () => store.set(false), title: "关闭面板", "aria-label": "关闭" }, "✕")),
470
556
  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) · 紧急保存 = 本地留档")));
557
+ h(ConnectCard, { status, error, install, bridge, onInstall: doInstall, onStart: doStart }),
558
+ h(ConnectGuide, { open: guide, setOpen: setGuide }),
559
+ h(Timeline, {
560
+ feed, pinned, setSid: setPinned, connected: ready,
561
+ onRevoke: doRevoke, busyId: revoke.busyId, onClear: doClear, clearing,
562
+ note: revoke.out,
563
+ }),
564
+ h(SnapshotCard, { connected: ready, state: save, onSave: doSave })),
565
+ h("div", { className: "eda-foot" }, "对话里生成(官方 eda.* API) · 本地留档(紧急保存)"));
477
566
  }
478
- //#endregion
479
567
 
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>';
568
+ /** Overlay wrapper: mounted in `shell.overlay`; `hidden` toggles visibility so
569
+ * the feed keeps running while the panel is closed (same surface as 0.1.x). */
570
+ function EdaOverlay({ store }) {
571
+ const open = useStore(store);
572
+ const [width, setWidth] = useState(() => {
573
+ try { return Math.min(760, Math.max(120, Number(localStorage.getItem("dsh-eda:width")) || 400)); } catch (e) { return 400; }
574
+ });
575
+ const drag = useRef(null);
576
+ const startDrag = (e) => {
577
+ e.preventDefault();
578
+ const move = (ev) => {
579
+ const next = Math.min(760, Math.max(120, window.innerWidth - ev.clientX));
580
+ drag.current = next;
581
+ setWidth(next);
582
+ };
583
+ const up = () => {
584
+ window.removeEventListener("mousemove", move);
585
+ window.removeEventListener("mouseup", up);
586
+ if (drag.current !== null) { try { localStorage.setItem("dsh-eda:width", String(drag.current)); } catch (err) { /* ignore */ } }
587
+ };
588
+ window.addEventListener("mousemove", move);
589
+ window.addEventListener("mouseup", up);
590
+ };
591
+ return h("div", { className: "dsh-eda-root", hidden: !open, style: { "--eda-w": width + "px" } },
592
+ h("div", { className: "dsh-eda-grip", onMouseDown: startDrag, title: "拖动调整宽度" }),
593
+ h(EdaPanel, { store }));
594
+ }
595
+ //#endregion
482
596
 
597
+ //#region mounts (0.1.x compatibility)
483
598
  function sidebarRoot() {
484
599
  const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]');
485
600
  if (column === null) return undefined;
486
601
  const logoOwner = column.querySelector('[class*="logoRow"]')?.parentElement;
487
602
  return logoOwner ?? (column.firstElementChild ?? undefined);
488
603
  }
489
-
490
604
  function newSessionButton(root) {
491
605
  const nested = root.querySelector('button[class*="newSession"]');
492
606
  if (nested !== null) return nested;
493
- for (const child of root.children) {
494
- if (child.tagName === "BUTTON") return child;
495
- }
607
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
496
608
  return undefined;
497
609
  }
498
-
499
- function createEntry(controller) {
610
+ function createEntry(store) {
500
611
  const entry = document.createElement("button");
501
612
  entry.type = "button";
502
613
  entry.dataset.dshEdaEntry = "";
503
614
  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(); });
615
+ entry.setAttribute("title", "嘉立创 EDA 助手(官方画板平台)");
616
+ entry.innerHTML = '<span class="eda-entry-icon"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><path d="' + ICON_EDA + '" stroke-linejoin="round" stroke-linecap="round"/></svg></span><span class="eda-entry-label">嘉立创 EDA</span>';
617
+ entry.addEventListener("click", () => store.toggle());
507
618
  return entry;
508
619
  }
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);
620
+ function mountSidebarEntry(store) {
621
+ const entry = createEntry(store);
527
622
  let root;
528
623
  let placed = false;
529
624
  let rootObserver;
530
-
531
- const tryPlace = () => {
532
- if (root !== undefined && !root.isConnected) {
533
- rootObserver?.disconnect();
534
- root = undefined;
535
- placed = false;
625
+ const place = () => {
626
+ const button = newSessionButton(root);
627
+ if (button === undefined) return false;
628
+ if (entry.parentElement !== root) {
629
+ const row = button.closest('[class*="logoRow"]');
630
+ const base = row !== null && row.parentElement === root ? row : button;
631
+ const family = Array.from(root.children).filter((el) =>
632
+ el instanceof HTMLElement && el.matches("[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-logcat-entry], [data-dsh-eda-entry]"));
633
+ const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
634
+ root.insertBefore(entry, anchor);
536
635
  }
537
- if (placed) {
538
- if (document.body.contains(entry)) return;
636
+ return true;
637
+ };
638
+ const tryPlace = () => {
639
+ if (root !== undefined && (!root.isConnected || (placed && !document.body.contains(entry)))) {
539
640
  rootObserver?.disconnect();
540
641
  root = undefined;
541
642
  placed = false;
542
643
  }
644
+ if (placed) return;
543
645
  root ??= sidebarRoot();
544
646
  if (root === undefined) return;
545
- placed = placeEntry(root, entry);
647
+ placed = place();
546
648
  if (placed) {
547
649
  rootObserver = new MutationObserver(() => {
548
650
  if (root === undefined || !root.isConnected) { placed = false; tryPlace(); return; }
549
- if (!root.contains(entry)) placed = placeEntry(root, entry);
651
+ if (!root.contains(entry)) placed = place();
550
652
  });
551
653
  rootObserver.observe(root, { childList: true, subtree: true });
552
654
  }
553
655
  };
554
-
555
- const waitObserver = new MutationObserver(() => { tryPlace(); });
656
+ const waitObserver = new MutationObserver(() => tryPlace());
556
657
  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();
658
+ const sync = () => { if (store.get()) entry.dataset.active = "true"; else delete entry.dataset.active; };
659
+ const unsubscribe = store.subscribe(sync);
660
+ sync();
564
661
  tryPlace();
565
-
566
662
  return () => {
567
663
  waitObserver.disconnect();
568
664
  rootObserver?.disconnect();
@@ -571,86 +667,81 @@ window.__ModuleLoader__.load({
571
667
  };
572
668
  }
573
669
 
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
-
670
+ /**
671
+ * Fallback mount for shells without the slots service (and for the unit
672
+ * test's DOM stub): a body-level overlay. Still absolute-in-shell friendly:
673
+ * z-index stays low (40) and the panel keeps its own close affordance.
674
+ */
675
+ function mountFallback(store) {
676
+ const container = document.createElement("div");
677
+ container.dataset.dshEdaView = "";
678
+ container.className = "dsh-eda-fallback";
679
+ const root = createRoot(container);
680
+ document.body.appendChild(container);
681
+ root.render(h(EdaOverlay, { store }));
602
682
  return () => {
603
- unsubscribe();
604
- root?.unmount();
605
- root = undefined;
606
- container?.remove();
607
- container = undefined;
683
+ root.unmount();
684
+ container.remove();
608
685
  };
609
686
  }
610
687
  //#endregion
611
688
 
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
689
  //#region entry
624
- /** Required services (fiber inject waiting — the runtime must be up first). */
625
690
  const inject = ["slots"];
626
691
 
627
692
  /**
628
- * Mount the 嘉立创 EDA 助手 panel.
693
+ * Mount the panel: 0.2 registers into `shell.overlay` + a header utility pill;
694
+ * 0.1.x keeps the sidebar DOM entry. Anything unavailable degrades quietly.
629
695
  * @param ctx - client root context.
630
696
  */
631
697
  function apply(ctx) {
632
698
  const style = document.createElement("style");
633
- style.textContent = STYLE;
699
+ style.textContent = STYLE + "\n.dsh-eda-fallback { position: fixed; inset: 0; pointer-events: none; z-index: 40; }\n.dsh-eda-fallback > .dsh-eda-root { pointer-events: auto; }";
634
700
  style.dataset.dshEdaStyle = "";
635
701
  document.head.appendChild(style);
636
702
 
637
- const controller = new PanelController();
703
+ const store = new PanelStore();
638
704
  const disposers = [];
705
+ let slotRegistered = false;
706
+
707
+ try {
708
+ const slots = typeof ctx.get === "function" ? ctx.get("slots") : undefined;
709
+ if (slots !== undefined && slots !== null) {
710
+ disposers.push(slots.inject("conversation.session.header.utilities", () => slots.register(
711
+ { name: "conversation.session.header.utilities", id: "eda-toggle", order: 30 },
712
+ () => h(HeaderPill, { store }),
713
+ )));
714
+ disposers.push(slots.inject("shell.overlay", () => slots.register(
715
+ { name: "shell.overlay", id: "eda", order: 90 },
716
+ () => h(EdaOverlay, { store }),
717
+ )));
718
+ slotRegistered = true;
719
+ }
720
+ } catch (error) {
721
+ console.warn("[dsh-eda] slot registration failed, falling back to DOM mount", error);
722
+ }
723
+
639
724
  try {
640
- disposers.push(mountSidebarEntry(controller));
641
- disposers.push(mountPanel(controller));
725
+ disposers.push(mountSidebarEntry(store));
642
726
  } catch (error) {
643
- console.warn("[dsh-eda] mount failed:", error);
727
+ console.warn("[dsh-eda] sidebar entry failed", error);
644
728
  }
729
+ if (!slotRegistered) {
730
+ try {
731
+ disposers.push(mountFallback(store));
732
+ } catch (error) {
733
+ console.warn("[dsh-eda] fallback mount failed", error);
734
+ }
735
+ }
736
+
645
737
  ctx.effect(() => () => {
646
738
  for (const dispose of disposers.splice(0)) dispose();
647
739
  style.remove();
648
- }, "dsh-eda: ui mounts");
740
+ }, "dsh-eda: browser UI");
649
741
  }
650
- //#endregion
651
742
 
652
743
  exports.apply = apply;
653
744
  exports.inject = inject;
654
745
  return module.exports;
655
- }
746
+ },
656
747
  });