dsh-continual-evolve 0.13.0 → 0.14.1

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/client/client.js CHANGED
@@ -17,12 +17,27 @@
17
17
  * call with null (the bundle list row shows the package description; the
18
18
  * card is the page).
19
19
  *
20
- * Market parity (v0.13): locale/theme/primitives wiring copied from
21
- * dshmarket's client — `ctx.locale.register(NS, { zh, en })` + `bind`,
22
- * `var(--dsw-alias-*)` theme tokens, `Button` primitive for the refresh
23
- * control. Missing primitives/locale degrade to themed plain elements, so an
24
- * old host never blanks the settings dialog. The card stays a read-only
25
- * projection: fetched at render time from this plugin's host routes
20
+ * Card form (v0.14): the dshmarket SettingsCard shape, one for one — a
21
+ * collapsed-by-default framed card whose header is the toggle button
22
+ * (name + one-liner + rotating chevron), body rendered only when open, hover
23
+ * and open states on the frame (see that bundle's "why the chrome is
24
+ * hand-built" note: a flat, always-expanded box next to rows that collapse
25
+ * reads as another product). The stylesheet is injected once, like the
26
+ * host's own CSS-module tags.
27
+ *
28
+ * Content (v0.14, scope selector v0.15): aligned with ZCode's Settings → Memory
29
+ * viewer, which the .evolve store shape was itself aligned with — one workspace
30
+ * selected at a time behind a dropdown scope selector (workspaces come from the
31
+ * host registry, so the selector lists the same workspaces the workspace picker
32
+ * does), a file search box, per-file relative updated times, and a
33
+ * click-to-preview body served by the host's content endpoint (5 MiB cap,
34
+ * deleted/changed guards). The drift warnings (dangling index rows / unindexed
35
+ * files) stay: they are this plugin's OBSERVATION surface and a deliberate
36
+ * superset of ZCode's viewer.
37
+ * Locale/theme/primitives wiring unchanged from v0.13: `ctx.locale.register`
38
+ * + `bind`, `var(--dsw-alias-*)` tokens, primitives with plain-element
39
+ * fallbacks so an old host never blanks the settings dialog. Read-only
40
+ * projection, fetched at render time from this plugin's host routes
26
41
  * (same-origin). No editor, no write path, no cached state.
27
42
  */
28
43
  window.__ModuleLoader__.load({
@@ -65,34 +80,63 @@ window.__ModuleLoader__.load({
65
80
  var NS = "dsh-continual-evolve";
66
81
  var SLOT = "plugins.bundle.config";
67
82
  var API_ROOT = "/dsh-continual-evolve/api/v1";
83
+ var STYLE_ID = "dsh-continual-evolve-card-css";
68
84
 
69
85
  var zh = {
70
- title: "工作区记忆(只读投影)",
86
+ title: "工作区记忆",
87
+ desc: "各工作区 .evolve/memory/ 的只读投影。",
71
88
  refresh: "刷新",
72
89
  hint: "事实源是各工作区 .evolve/memory/ 下的 markdown 文件——模型原生读写;本卡片只展示,编辑请直接改文件。",
73
90
  loading: "读取中…",
74
91
  loadFailed: "卡片数据加载失败:",
75
- empty: "本进程还没有服务过任何工作区会话——在某个工作区开一次会话后回来刷新。",
92
+ empty: "还没有任何工作区有记忆库——在某个工作区开一次会话后回来刷新。",
76
93
  absent: "该工作区尚无记忆库(开一次会话自动创建)。",
77
94
  readFailed: "记忆库读取失败:",
78
95
  stats: "{files} 条记忆 · 索引 {index} 行",
96
+ filesHeading: "文件",
97
+ scopeLabel: "选择工作区",
98
+ searchPlaceholder: "搜索记忆文件…",
99
+ searchEmpty: "没有匹配的记忆文件。",
79
100
  missing: "索引失联(索引引用但文件已删):",
80
101
  unindexed: "未入索引(文件存在但索引没有行):",
81
102
  readError: "读取失败:",
103
+ previewLoading: "正在加载文件…",
104
+ previewTooLarge: "该记忆文件超过 5 MiB 预览上限。",
105
+ previewDeleted: "该记忆文件已被删除,请刷新文件列表。",
106
+ previewFailed: "内容读取失败:",
107
+ justNow: "刚刚",
108
+ minutesAgo: "{count} 分钟前",
109
+ today: "今天 {time}",
110
+ yesterday: "昨天 {time}",
111
+ localeTag: "zh-CN",
82
112
  };
83
113
  var en = {
84
- title: "Workspace memory (read-only)",
114
+ title: "Workspace Memory",
115
+ desc: "Read-only projection of each workspace's .evolve/memory/.",
85
116
  refresh: "Refresh",
86
117
  hint: "The source of truth is the markdown files under each workspace's .evolve/memory/ — read and written by the model; this card only displays them, edit the files directly.",
87
118
  loading: "Loading…",
88
119
  loadFailed: "Failed to load card data: ",
89
- empty: "This process has not served any workspace session yet — open a session in a workspace, then come back and refresh.",
120
+ empty: "No workspace has a memory store yet — open a session in a workspace, then come back and refresh.",
90
121
  absent: "No memory store in this workspace yet (one is created when you open a session).",
91
122
  readFailed: "Failed to read the memory store: ",
92
123
  stats: "{files} memories · {index} index rows",
124
+ filesHeading: "Files",
125
+ scopeLabel: "Choose workspace",
126
+ searchPlaceholder: "Search memory files…",
127
+ searchEmpty: "No matching memory files.",
93
128
  missing: "Dangling index (indexed but deleted): ",
94
129
  unindexed: "Unindexed (on disk, missing from the index): ",
95
130
  readError: "Read failed: ",
131
+ previewLoading: "Loading file…",
132
+ previewTooLarge: "This memory file exceeds the 5 MiB preview limit.",
133
+ previewDeleted: "This memory file has been deleted — refresh the file list.",
134
+ previewFailed: "Failed to read the content: ",
135
+ justNow: "Just now",
136
+ minutesAgo: "{count} min ago",
137
+ today: "Today {time}",
138
+ yesterday: "Yesterday {time}",
139
+ localeTag: "en-US",
96
140
  };
97
141
 
98
142
  function format(template, params) {
@@ -103,7 +147,42 @@ window.__ModuleLoader__.load({
103
147
  }
104
148
 
105
149
  /**
106
- * Assemble the card's view model: the workspace allowlist, then one
150
+ * Relative updated time, ZCode viewer style: just now / N minutes ago /
151
+ * today / yesterday, then a calendar rendering via Intl in the active
152
+ * language (the `localeTag` dictionary key carries that language out).
153
+ */
154
+ function formatRelative(updatedAt, now, t) {
155
+ if (typeof updatedAt !== "number" || !isFinite(updatedAt) || updatedAt <= 0) {
156
+ return "";
157
+ }
158
+ var minute = 60000;
159
+ var diff = now - updatedAt;
160
+ if (diff < minute) {
161
+ return t("justNow");
162
+ }
163
+ if (diff < 60 * minute) {
164
+ return t("minutesAgo", { count: Math.floor(diff / minute) });
165
+ }
166
+ var date = new Date(updatedAt);
167
+ var time = new Intl.DateTimeFormat(t("localeTag"), { hour: "2-digit", minute: "2-digit" }).format(date);
168
+ var startOfDay = new Date(now);
169
+ startOfDay.setHours(0, 0, 0, 0);
170
+ if (updatedAt >= startOfDay.getTime()) {
171
+ return t("today", { time: time });
172
+ }
173
+ if (updatedAt >= startOfDay.getTime() - 24 * 60 * 60 * 1000) {
174
+ return t("yesterday", { time: time });
175
+ }
176
+ var sameYear = date.getFullYear() === new Date(now).getFullYear();
177
+ return new Intl.DateTimeFormat(t("localeTag"), {
178
+ month: "numeric",
179
+ day: "numeric",
180
+ year: sameYear ? undefined : "numeric",
181
+ }).format(date);
182
+ }
183
+
184
+ /**
185
+ * Assemble the card's view model: the workspace catalogue, then one
107
186
  * snapshot per workspace. Network or API failure per workspace degrades
108
187
  * to an `error` string on that workspace's entry — one bad store never
109
188
  * blanks the whole card.
@@ -121,17 +200,20 @@ window.__ModuleLoader__.load({
121
200
  var entries = await Promise.all(
122
201
  workspaces.map(async function (workspace) {
123
202
  var root = typeof workspace.root === "string" ? workspace.root : "";
124
- var lastSeen = typeof workspace.lastSeen === "string" ? workspace.lastSeen : "";
203
+ var label =
204
+ typeof workspace.label === "string" && workspace.label.trim().length > 0
205
+ ? workspace.label
206
+ : workspaceLabel(root);
125
207
  try {
126
208
  var snapshotResponse = await fetchImpl(API_ROOT + "/memory?root=" + encodeURIComponent(root));
127
209
  if (!snapshotResponse.ok) {
128
- return { root: root, lastSeen: lastSeen, snapshot: null, error: "HTTP " + snapshotResponse.status };
210
+ return { root: root, label: label, snapshot: null, error: "HTTP " + snapshotResponse.status };
129
211
  }
130
- return { root: root, lastSeen: lastSeen, snapshot: await snapshotResponse.json(), error: null };
212
+ return { root: root, label: label, snapshot: await snapshotResponse.json(), error: null };
131
213
  } catch (error) {
132
214
  return {
133
215
  root: root,
134
- lastSeen: lastSeen,
216
+ label: label,
135
217
  snapshot: null,
136
218
  error: error instanceof Error ? error.message : String(error),
137
219
  };
@@ -141,157 +223,493 @@ window.__ModuleLoader__.load({
141
223
  return { workspaces: entries };
142
224
  }
143
225
 
144
- var CARD_STYLE = {
145
- background: "var(--dsw-alias-bg-layer-1,#fff)",
146
- border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)",
147
- borderRadius: "12px",
148
- padding: "12px 14px",
149
- display: "flex",
150
- flexDirection: "column",
151
- gap: "12px",
152
- };
153
- var TITLE_STYLE = {
154
- color: "var(--dsw-alias-label-primary,#1f2328)",
155
- fontSize: "15px",
156
- fontWeight: 600,
157
- lineHeight: "22px",
158
- };
159
- var HINT_STYLE = {
160
- color: "var(--dsw-alias-label-tertiary,#8b93a1)",
161
- fontSize: "12px",
162
- lineHeight: "18px",
163
- };
164
- var SECTION_STYLE = {
165
- borderTop: "1px solid var(--dsw-alias-border-l2,#f0f1f3)",
166
- paddingTop: "8px",
167
- display: "flex",
168
- flexDirection: "column",
169
- gap: "6px",
170
- };
171
- var ROOT_STYLE = {
172
- fontFamily: "ui-monospace,Menlo,monospace",
173
- fontSize: "11px",
174
- color: "var(--dsw-alias-label-secondary,#6b7280)",
175
- wordBreak: "break-all",
176
- };
177
- var STATS_STYLE = {
178
- fontSize: "12px",
179
- lineHeight: "18px",
180
- color: "var(--dsw-alias-label-secondary,#6b7280)",
181
- };
182
- var WARN_STYLE = {
183
- fontSize: "12px",
184
- lineHeight: "18px",
185
- color: "var(--dsw-alias-state-warn-primary,#b45309)",
186
- };
187
- var ERROR_STYLE = {
188
- fontSize: "12px",
189
- lineHeight: "18px",
190
- color: "var(--dsw-alias-state-error-primary,#dc2626)",
191
- };
192
- var LIST_STYLE = { margin: "4px 0 0", paddingLeft: "18px", fontSize: "12px", lineHeight: "18px" };
193
- var FALLBACK_BUTTON_STYLE = {
194
- background: "var(--dsw-alias-bg-layer-2,#f3f4f6)",
195
- color: "var(--dsw-alias-label-primary,#1f2328)",
196
- border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)",
197
- borderRadius: "6px",
198
- padding: "3px 10px",
199
- fontSize: "12px",
200
- lineHeight: "18px",
201
- cursor: "pointer",
202
- };
226
+ /**
227
+ * Fetch one memory file's body for the preview. Outcomes mirror the
228
+ * endpoint's status mapping so the card can show the ZCode viewer's
229
+ * deleted / too-large / failed states instead of a raw error.
230
+ *
231
+ * @param {typeof fetch} fetchImpl - injectable for tests.
232
+ * @param {string} root - known workspace root.
233
+ * @param {string} file - plain .md file name inside that memory store.
234
+ * @returns {Promise<{status: "ready"|"deleted"|"tooLarge"|"failed", content: string, message: string}>}
235
+ */
236
+ async function loadFileContent(fetchImpl, root, file) {
237
+ var url = API_ROOT + "/memory/file?root=" + encodeURIComponent(root) + "&file=" + encodeURIComponent(file);
238
+ try {
239
+ var response = await fetchImpl(url);
240
+ if (response.ok) {
241
+ var body = await response.json();
242
+ return {
243
+ status: "ready",
244
+ content: typeof body.content === "string" ? body.content : "",
245
+ message: "",
246
+ };
247
+ }
248
+ if (response.status === 404) {
249
+ return { status: "deleted", content: "", message: "" };
250
+ }
251
+ if (response.status === 413) {
252
+ return { status: "tooLarge", content: "", message: "" };
253
+ }
254
+ return { status: "failed", content: "", message: "HTTP " + response.status };
255
+ } catch (error) {
256
+ return {
257
+ status: "failed",
258
+ content: "",
259
+ message: error instanceof Error ? error.message : String(error),
260
+ };
261
+ }
262
+ }
263
+
264
+ /** Display label for a workspace: its directory name (ZCode shows a project name, not a path). */
265
+ function workspaceLabel(root) {
266
+ var normalized = String(root).replace(/[\\/]+$/, "");
267
+ var index = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\"));
268
+ return index >= 0 ? normalized.slice(index + 1) : normalized;
269
+ }
270
+
271
+ /**
272
+ * Inject the card stylesheet once, the way the host's own CSS-module
273
+ * tags work. The classes are prefixed `dce-` and every value falls back
274
+ * to a fixed color after the token, so an old theme never blanks text.
275
+ */
276
+ function injectCardStyle() {
277
+ if (typeof document === "undefined") {
278
+ return;
279
+ }
280
+ if (document.getElementById(STYLE_ID) !== null) {
281
+ return;
282
+ }
283
+ var style = document.createElement("style");
284
+ style.id = STYLE_ID;
285
+ style.dataset.plugin = NS;
286
+ style.textContent = [
287
+ ".dce-card{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-3,#fff);border-radius:12px;transition:border-color .16s,background .16s}",
288
+ ".dce-card:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}",
289
+ '.dce-card[data-open="true"]{background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}',
290
+ ".dce-head{appearance:none;-webkit-appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
291
+ ".dce-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}",
292
+ ".dce-head-text{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
293
+ ".dce-name{color:var(--dsw-alias-label-primary,#1f2328);font-size:15px;font-weight:600;line-height:1.4}",
294
+ ".dce-desc{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:13px;line-height:1.5}",
295
+ ".dce-chevron{color:var(--dsw-alias-label-tertiary,#8b93a1);flex:none;transition:transform .16s;display:inline-flex}",
296
+ '.dce-chevron[data-open="true"]{transform:rotate(180deg)}',
297
+ ".dce-body{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);margin:0 16px;padding:12px 0 14px;display:flex;flex-direction:column;gap:10px}",
298
+ ".dce-hint{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}",
299
+ ".dce-empty{border:1px dashed var(--dsw-alias-border-l3,#d1d9e0);border-radius:10px;padding:16px;text-align:center;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:13px;line-height:20px}",
300
+ ".dce-scope-wrap{position:relative;display:inline-flex;min-width:0;max-width:100%;align-self:flex-start}",
301
+ ".dce-scope{display:inline-flex;align-items:center;gap:8px;max-width:100%;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);font:inherit;font-size:12px;line-height:18px;cursor:pointer}",
302
+ ".dce-scope:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}",
303
+ ".dce-scope:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:1px}",
304
+ ".dce-scope-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
305
+ ".dce-scope-caret{display:inline-flex;color:var(--dsw-alias-label-tertiary,#8b93a1)}",
306
+ ".dce-menu{position:absolute;top:calc(100% + 6px);left:0;z-index:20;min-width:240px;max-width:min(420px,80vw);max-height:320px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 8px 24px rgba(0,0,0,.12)}",
307
+ ".dce-menu-item{display:flex;flex-direction:column;gap:2px;width:100%;padding:6px 10px;border:0;border-radius:8px;background:0 0;font:inherit;color:inherit;text-align:left;cursor:pointer}",
308
+ ".dce-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.03))}",
309
+ '.dce-menu-item[data-active="true"]{background:var(--dsw-alias-bg-module-platform,#eef1f4)}',
310
+ ".dce-menu-label{color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
311
+ ".dce-menu-path{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
312
+ ".dce-search{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l4,#d8dee4);border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-layer-1,#fff);height:32px;color:var(--dsw-alias-label-primary,#1f2328);font:inherit;font-size:12px;padding:0 10px;outline:none}",
313
+ ".dce-search::placeholder{color:var(--dsw-alias-label-tertiary,#8b93a1)}",
314
+ ".dce-search:focus-visible{border-color:var(--dsw-alias-brand-primary,#4f6ef7)}",
315
+ ".dce-files-head{display:flex;align-items:center;gap:10px;min-height:28px}",
316
+ ".dce-files-title{color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;font-weight:600;line-height:18px}",
317
+ ".dce-files-count{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}",
318
+ ".dce-spacer{flex:1}",
319
+ ".dce-list{margin:0;padding:0;list-style:none;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1,#fff)}",
320
+ ".dce-item+.dce-item{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb)}",
321
+ ".dce-row{display:flex;width:100%;align-items:center;gap:10px;padding:8px 12px;background:0 0;border:0;font:inherit;color:inherit;text-align:left;cursor:pointer}",
322
+ ".dce-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.03))}",
323
+ ".dce-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}",
324
+ ".dce-file-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;font-weight:500;line-height:18px}",
325
+ ".dce-file-time{flex:none;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px}",
326
+ ".dce-preview{margin:0;padding:8px 12px 10px;background:var(--dsw-alias-bg-module-platform,#eef1f4);border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb)}",
327
+ ".dce-pre{margin:0;max-height:240px;overflow:auto;font-family:var(--ds-font-family-code,ui-monospace,Menlo,monospace);font-size:11px;line-height:17px;color:var(--dsw-alias-label-primary,#1f2328);white-space:pre-wrap;word-break:break-word}",
328
+ ".dce-workspace{display:flex;flex-direction:column;gap:10px}",
329
+ ".dce-stats{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:18px}",
330
+ ".dce-warn{color:var(--dsw-alias-state-warn-primary,#b45309);font-size:12px;line-height:18px}",
331
+ ".dce-error{color:var(--dsw-alias-state-error-primary,#dc2626);font-size:12px;line-height:18px}",
332
+ ".dce-fallback-btn{background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-primary,#1f2328);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:6px;padding:3px 10px;font-size:12px;line-height:18px;cursor:pointer}",
333
+ ].join("\n");
334
+ document.head.appendChild(style);
335
+ }
336
+
337
+ /** Chevron from the host primitives, or a plain glyph when they are absent. */
338
+ function Chevron() {
339
+ if (
340
+ primitives !== undefined &&
341
+ primitives !== null &&
342
+ typeof primitives.IconChevronDownOutlineRegular !== "undefined" &&
343
+ primitives.IconChevronDownOutlineRegular !== null
344
+ ) {
345
+ return createElement(primitives.IconChevronDownOutlineRegular, { size: 14, "aria-hidden": "true" });
346
+ }
347
+ return createElement("span", { "aria-hidden": "true" }, "▾");
348
+ }
203
349
 
204
350
  function RefreshButton(props) {
205
351
  var UI = props.UI;
206
352
  var t = props.t;
207
353
  var onClick = props.onClick;
208
354
  if (UI !== null && UI !== undefined && typeof UI.Button !== "undefined" && UI.Button !== null) {
209
- return createElement(
210
- UI.Button,
211
- { variant: "outline", size: "sm", onClick: onClick },
212
- t("refresh"),
213
- );
355
+ return createElement(UI.Button, { variant: "outline", size: "sm", onClick: onClick }, t("refresh"));
214
356
  }
215
- return createElement("button", { type: "button", style: FALLBACK_BUTTON_STYLE, onClick: onClick }, t("refresh"));
357
+ return createElement(
358
+ "button",
359
+ { type: "button", className: "dce-fallback-btn", onClick: onClick },
360
+ t("refresh"),
361
+ );
216
362
  }
217
363
 
218
- /** One workspace block: root, stats, drift warnings, per-file lines. */
219
- function WorkspaceBlock(props) {
220
- var entry = props.entry;
364
+ /** One memory file row: name + relative updated time, click toggles the body preview. */
365
+ function FileRow(props) {
366
+ var file = props.file;
367
+ var now = props.now;
221
368
  var t = props.t;
222
- if (entry.error !== null && entry.error !== undefined) {
223
- return createElement(
224
- "div",
225
- { style: SECTION_STYLE },
226
- createElement("div", { style: ROOT_STYLE }, entry.root),
227
- createElement("div", { style: ERROR_STYLE }, t("readError") + entry.error),
228
- );
229
- }
230
- var snapshot = entry.snapshot;
231
- if (snapshot === null || snapshot === undefined || !snapshot.exists) {
232
- return createElement(
233
- "div",
234
- { style: SECTION_STYLE },
235
- createElement("div", { style: ROOT_STYLE }, entry.root),
236
- createElement("div", { style: HINT_STYLE }, t("absent")),
237
- );
238
- }
239
- if (snapshot.readError !== null && snapshot.readError !== undefined) {
240
- return createElement(
241
- "div",
242
- { style: SECTION_STYLE },
243
- createElement("div", { style: ROOT_STYLE }, entry.root),
244
- createElement("div", { style: ERROR_STYLE }, t("readFailed") + snapshot.readError),
245
- );
369
+ var expanded = props.expanded;
370
+ var preview = props.preview;
371
+ var onToggle = props.onToggle;
372
+ var rowChildren = [
373
+ createElement("span", { key: "name", className: "dce-file-name" }, file.file),
374
+ ];
375
+ var time = formatRelative(file.updatedAt, now, t);
376
+ if (time !== "") {
377
+ rowChildren.push(createElement("span", { key: "time", className: "dce-file-time" }, time));
246
378
  }
247
379
  var children = [
248
- createElement("div", { key: "root", style: ROOT_STYLE }, entry.root),
249
380
  createElement(
250
- "div",
251
- { key: "stats", style: STATS_STYLE },
252
- t("stats", { files: snapshot.fileCount, index: snapshot.indexEntryCount }),
381
+ "button",
382
+ {
383
+ key: "row",
384
+ type: "button",
385
+ className: "dce-row",
386
+ "aria-expanded": expanded ? "true" : "false",
387
+ onClick: onToggle,
388
+ },
389
+ rowChildren,
253
390
  ),
254
391
  ];
255
- if (snapshot.missingFiles.length > 0) {
256
- children.push(
257
- createElement("div", { key: "missing", style: WARN_STYLE }, t("missing") + snapshot.missingFiles.join("、")),
392
+ if (expanded) {
393
+ var body = null;
394
+ if (preview === null || preview.file !== file.file) {
395
+ body = createElement("div", { className: "dce-hint" }, t("previewLoading"));
396
+ } else if (preview.status === "loading") {
397
+ body = createElement("div", { className: "dce-hint" }, t("previewLoading"));
398
+ } else if (preview.status === "ready") {
399
+ body = createElement("pre", { className: "dce-pre" }, preview.content);
400
+ } else if (preview.status === "deleted") {
401
+ body = createElement("div", { className: "dce-hint" }, t("previewDeleted"));
402
+ } else if (preview.status === "tooLarge") {
403
+ body = createElement("div", { className: "dce-hint" }, t("previewTooLarge"));
404
+ } else {
405
+ body = createElement("div", { className: "dce-error" }, t("previewFailed") + preview.message);
406
+ }
407
+ children.push(createElement("div", { key: "preview", className: "dce-preview" }, body));
408
+ }
409
+ return createElement("li", { className: "dce-item" }, children);
410
+ }
411
+
412
+ /** Leading "switch scope" marker from the host primitives, or nothing. */
413
+ function ScopeMark() {
414
+ if (
415
+ primitives !== undefined &&
416
+ primitives !== null &&
417
+ typeof primitives.IconChevronsUpDownOutlineRegular !== "undefined" &&
418
+ primitives.IconChevronsUpDownOutlineRegular !== null
419
+ ) {
420
+ return createElement(primitives.IconChevronsUpDownOutlineRegular, { size: 14, "aria-hidden": "true" });
421
+ }
422
+ return null;
423
+ }
424
+
425
+ /**
426
+ * The workspace scope selector, ZCode's Settings → Memory shape: one
427
+ * scope at a time behind a dropdown (leading marker + current workspace
428
+ * label + chevron), never a row of chips. A chip row can only render
429
+ * when several workspaces are known, so in the common single-workspace
430
+ * case it renders nothing at all and the capability reads as missing.
431
+ * The menu closes on select, on Escape and on a click outside.
432
+ */
433
+ function WorkspaceScopeMenu(props) {
434
+ var workspaces = props.workspaces;
435
+ var selected = props.selected;
436
+ var onSelect = props.onSelect;
437
+ var t = props.t;
438
+ var UI = props.UI;
439
+ var openState = useState(false);
440
+ var open = openState[0];
441
+ var setOpen = openState[1];
442
+ useEffect(
443
+ function () {
444
+ if (!open || typeof document === "undefined") {
445
+ return undefined;
446
+ }
447
+ var close = function () {
448
+ setOpen(false);
449
+ };
450
+ var onKeyDown = function (event) {
451
+ if (event.key === "Escape") {
452
+ close();
453
+ }
454
+ };
455
+ document.addEventListener("click", close);
456
+ document.addEventListener("keydown", onKeyDown);
457
+ return function () {
458
+ document.removeEventListener("click", close);
459
+ document.removeEventListener("keydown", onKeyDown);
460
+ };
461
+ },
462
+ [open],
463
+ );
464
+ var current = null;
465
+ for (var index = 0; index < workspaces.length; index++) {
466
+ if (workspaces[index].root === selected) {
467
+ current = workspaces[index];
468
+ break;
469
+ }
470
+ }
471
+ if (current === null && workspaces.length > 0) {
472
+ current = workspaces[0];
473
+ }
474
+ var label = current === null ? "" : current.label || workspaceLabel(current.root);
475
+ var scopeChildren = [
476
+ createElement(ScopeMark, { key: "mark" }),
477
+ createElement("span", { key: "label", className: "dce-scope-label" }, label),
478
+ createElement("span", { key: "caret", className: "dce-scope-caret" }, createElement(Chevron, null)),
479
+ ];
480
+ var triggerProps = {
481
+ type: "button",
482
+ "aria-haspopup": "menu",
483
+ "aria-expanded": open ? "true" : "false",
484
+ "aria-label": t("scopeLabel"),
485
+ onClick: function () {
486
+ setOpen(!open);
487
+ },
488
+ };
489
+ var trigger = null;
490
+ if (UI !== null && UI !== undefined && typeof UI.Button !== "undefined" && UI.Button !== null) {
491
+ trigger = createElement(
492
+ UI.Button,
493
+ {
494
+ key: "trigger",
495
+ variant: "outline",
496
+ size: "sm",
497
+ type: triggerProps.type,
498
+ "aria-haspopup": triggerProps["aria-haspopup"],
499
+ "aria-expanded": triggerProps["aria-expanded"],
500
+ "aria-label": triggerProps["aria-label"],
501
+ onClick: triggerProps.onClick,
502
+ },
503
+ scopeChildren,
258
504
  );
505
+ } else {
506
+ trigger = createElement(
507
+ "button",
508
+ {
509
+ key: "trigger",
510
+ type: triggerProps.type,
511
+ className: "dce-scope",
512
+ "aria-haspopup": triggerProps["aria-haspopup"],
513
+ "aria-expanded": triggerProps["aria-expanded"],
514
+ "aria-label": triggerProps["aria-label"],
515
+ onClick: triggerProps.onClick,
516
+ },
517
+ scopeChildren,
518
+ );
519
+ }
520
+ var menu = null;
521
+ if (open) {
522
+ menu = createElement(
523
+ "div",
524
+ { key: "menu", className: "dce-menu", role: "menu", "aria-label": t("scopeLabel") },
525
+ workspaces.map(function (workspace) {
526
+ return createElement(
527
+ "button",
528
+ {
529
+ key: workspace.root,
530
+ type: "button",
531
+ role: "menuitemradio",
532
+ "aria-checked": workspace.root === selected ? "true" : "false",
533
+ className: "dce-menu-item",
534
+ "data-active": workspace.root === selected ? "true" : "false",
535
+ onClick: function () {
536
+ setOpen(false);
537
+ if (workspace.root !== selected) {
538
+ onSelect(workspace.root);
539
+ }
540
+ },
541
+ },
542
+ // The registry allows duplicate titles, so the root
543
+ // disambiguates two workspaces that read the same.
544
+ createElement("span", { key: "label", className: "dce-menu-label" }, workspace.label || workspaceLabel(workspace.root)),
545
+ createElement("span", { key: "root", className: "dce-menu-path" }, workspace.root),
546
+ );
547
+ }),
548
+ );
549
+ }
550
+ return createElement("div", { key: "scope", className: "dce-scope-wrap" }, trigger, menu);
551
+ }
552
+
553
+ /** One workspace's memory store: stats, drift warnings, searchable file list with previews. */
554
+ function WorkspaceBlock(props) {
555
+ var entry = props.entry;
556
+ var selected = props.selected;
557
+ var onSelect = props.onSelect;
558
+ var t = props.t;
559
+ var UI = props.UI;
560
+ var now = props.now;
561
+ var expandedFile = props.expandedFile;
562
+ var preview = props.preview;
563
+ var onToggleFile = props.onToggleFile;
564
+ var onRefresh = props.onRefresh;
565
+ var query = props.query;
566
+ var onQueryChange = props.onQueryChange;
567
+ var scope = createElement(WorkspaceScopeMenu, {
568
+ key: "scope",
569
+ workspaces: entry.workspaces.map(function (workspace) {
570
+ return { root: workspace.root, label: workspace.label };
571
+ }),
572
+ selected: selected,
573
+ onSelect: onSelect,
574
+ t: t,
575
+ UI: UI,
576
+ });
577
+ var active = entry.workspaces.find(function (workspace) {
578
+ return workspace.root === selected;
579
+ });
580
+ if (active === undefined) {
581
+ return createElement("div", { key: "block", className: "dce-workspace" }, scope);
259
582
  }
260
- if (snapshot.unindexedFiles.length > 0) {
261
- children.push(
583
+ var body = null;
584
+ if (active.error !== null && active.error !== undefined) {
585
+ body = [
586
+ createElement("div", { key: "root", className: "dce-stats" }, active.root),
587
+ createElement("div", { key: "err", className: "dce-error" }, t("readError") + active.error),
588
+ ];
589
+ } else if (active.snapshot === null || active.snapshot === undefined || !active.snapshot.exists) {
590
+ body = createElement("div", { className: "dce-empty" }, t("absent"));
591
+ } else if (active.snapshot.readError !== null && active.snapshot.readError !== undefined) {
592
+ body = [
593
+ createElement("div", { key: "root", className: "dce-stats" }, active.root),
594
+ createElement("div", { key: "err", className: "dce-error" }, t("readFailed") + active.snapshot.readError),
595
+ ];
596
+ } else {
597
+ var snapshot = active.snapshot;
598
+ var normalizedQuery = String(query).trim().toLocaleLowerCase();
599
+ var files = snapshot.files.filter(function (file) {
600
+ if (normalizedQuery.length === 0) {
601
+ return true;
602
+ }
603
+ return [file.file, file.name, file.description, file.type]
604
+ .filter(function (value) {
605
+ return typeof value === "string";
606
+ })
607
+ .some(function (value) {
608
+ return value.toLocaleLowerCase().includes(normalizedQuery);
609
+ });
610
+ });
611
+ var parts = [
262
612
  createElement(
263
613
  "div",
264
- { key: "unindexed", style: WARN_STYLE },
265
- t("unindexed") + snapshot.unindexedFiles.join("、"),
614
+ { key: "stats", className: "dce-stats" },
615
+ t("stats", { files: snapshot.fileCount, index: snapshot.indexEntryCount }),
266
616
  ),
617
+ ];
618
+ if (snapshot.missingFiles.length > 0) {
619
+ parts.push(
620
+ createElement("div", { key: "missing", className: "dce-warn" }, t("missing") + snapshot.missingFiles.join("、")),
621
+ );
622
+ }
623
+ if (snapshot.unindexedFiles.length > 0) {
624
+ parts.push(
625
+ createElement(
626
+ "div",
627
+ { key: "unindexed", className: "dce-warn" },
628
+ t("unindexed") + snapshot.unindexedFiles.join("、"),
629
+ ),
630
+ );
631
+ }
632
+ parts.push(
633
+ createElement("input", {
634
+ key: "search",
635
+ type: "search",
636
+ className: "dce-search",
637
+ value: query,
638
+ placeholder: t("searchPlaceholder"),
639
+ "aria-label": t("searchPlaceholder"),
640
+ onChange: function (event) {
641
+ onQueryChange(event.currentTarget.value);
642
+ },
643
+ }),
267
644
  );
645
+ parts.push(
646
+ createElement(
647
+ "div",
648
+ { key: "head", className: "dce-files-head" },
649
+ createElement("span", { className: "dce-files-title" }, t("filesHeading")),
650
+ createElement("span", { className: "dce-files-count" }, String(files.length)),
651
+ createElement("span", { className: "dce-spacer" }),
652
+ createElement(RefreshButton, {
653
+ UI: UI,
654
+ t: t,
655
+ onClick: onRefresh,
656
+ }),
657
+ ),
658
+ );
659
+ if (files.length === 0) {
660
+ parts.push(createElement("div", { key: "none", className: "dce-empty" }, t("searchEmpty")));
661
+ } else {
662
+ parts.push(
663
+ createElement(
664
+ "ul",
665
+ { key: "files", className: "dce-list" },
666
+ files.map(function (file) {
667
+ return createElement(FileRow, {
668
+ key: file.file,
669
+ file: file,
670
+ now: now,
671
+ t: t,
672
+ expanded: expandedFile === file.file,
673
+ preview: preview,
674
+ onToggle: function () {
675
+ onToggleFile(file.file);
676
+ },
677
+ });
678
+ }),
679
+ ),
680
+ );
681
+ }
682
+ body = parts;
268
683
  }
269
- children.push(
270
- createElement(
271
- "ul",
272
- { key: "files", style: LIST_STYLE },
273
- snapshot.files.map(function (file) {
274
- return createElement(
275
- "li",
276
- { key: file.file },
277
- createElement("strong", null, file.file),
278
- file.description !== "" ? " — " + file.description : "",
279
- );
280
- }),
281
- ),
282
- );
283
- return createElement("div", { style: SECTION_STYLE }, children);
684
+ return createElement("div", { key: "block", className: "dce-workspace" }, scope, body);
284
685
  }
285
686
 
286
- /** The card page: refresh control + one block per known workspace. */
687
+ /**
688
+ * The card: a collapsed-by-default framed disclosure (dshmarket
689
+ * SettingsCard shape) whose open body is the ZCode-memory-viewer-shaped
690
+ * read-only projection.
691
+ */
287
692
  function MemoryCard(props) {
288
693
  var t = props.t;
289
694
  var UI = props.UI;
695
+ injectCardStyle();
696
+ var openState = useState(false);
697
+ var open = openState[0];
698
+ var setOpen = openState[1];
290
699
  var state = useState({ status: "loading", model: null, message: "" });
291
700
  var model = state[0];
292
701
  var setModel = state[1];
293
702
  var reloadState = useState(0);
294
703
  var reload = reloadState[1];
704
+ var selectionState = useState({ root: null, query: "", expandedFile: null });
705
+ var selection = selectionState[0];
706
+ var setSelection = selectionState[1];
707
+ var previewState = useState({ file: null, status: "loading", content: "", message: "" });
708
+ var preview = previewState[0];
709
+ var setPreview = previewState[1];
710
+ var nowState = useState(Date.now());
711
+ var now = nowState[0];
712
+ var setNow = nowState[1];
295
713
  useEffect(
296
714
  function () {
297
715
  var alive = true;
@@ -311,42 +729,117 @@ window.__ModuleLoader__.load({
311
729
  },
312
730
  [reloadState[0]],
313
731
  );
732
+ useEffect(
733
+ function () {
734
+ if (open) setNow(Date.now());
735
+ },
736
+ [open],
737
+ );
738
+ var refresh = function () {
739
+ setModel({ status: "loading", model: null, message: "" });
740
+ setSelection(function (current) {
741
+ return { root: current.root, query: "", expandedFile: null };
742
+ });
743
+ setPreview({ file: null, status: "loading", content: "", message: "" });
744
+ reload(function (n) {
745
+ return n + 1;
746
+ });
747
+ };
748
+ var header = createElement(
749
+ "button",
750
+ {
751
+ type: "button",
752
+ className: "dce-head",
753
+ "aria-expanded": open ? "true" : "false",
754
+ onClick: function () {
755
+ setOpen(!open);
756
+ },
757
+ },
758
+ createElement(
759
+ "div",
760
+ { className: "dce-head-text" },
761
+ createElement("div", { className: "dce-name" }, t("title")),
762
+ createElement("div", { className: "dce-desc" }, t("desc")),
763
+ ),
764
+ createElement(
765
+ "span",
766
+ { className: "dce-chevron", "data-open": open ? "true" : "false" },
767
+ createElement(Chevron, null),
768
+ ),
769
+ );
314
770
  var body = null;
771
+ if (!open) {
772
+ return createElement("div", { className: "dce-card", "data-open": "false" }, header);
773
+ }
774
+ var inner = null;
315
775
  if (model.status === "loading") {
316
- body = createElement("div", { key: "body", style: HINT_STYLE }, t("loading"));
776
+ inner = createElement("div", { key: "inner", className: "dce-hint" }, t("loading"));
317
777
  } else if (model.status === "error") {
318
- body = createElement("div", { key: "body", style: ERROR_STYLE }, t("loadFailed") + model.message);
778
+ inner = createElement("div", { key: "inner", className: "dce-error" }, t("loadFailed") + model.message);
319
779
  } else if (model.model.workspaces.length === 0) {
320
- body = createElement("div", { key: "body", style: HINT_STYLE }, t("empty"));
780
+ inner = createElement("div", { key: "inner", className: "dce-empty" }, t("empty"));
321
781
  } else {
322
- body = createElement(
323
- "div",
324
- { key: "body" },
325
- model.model.workspaces.map(function (entry) {
326
- return createElement(WorkspaceBlock, { key: entry.root, entry: entry, t: t, UI: UI });
327
- }),
328
- );
782
+ var selected = selection.root;
783
+ if (
784
+ selected === null ||
785
+ !model.model.workspaces.some(function (workspace) {
786
+ return workspace.root === selected;
787
+ })
788
+ ) {
789
+ selected = model.model.workspaces[0].root;
790
+ }
791
+ inner = createElement(WorkspaceBlock, {
792
+ key: "inner",
793
+ entry: model.model,
794
+ selected: selected,
795
+ onSelect: function (root) {
796
+ setSelection({ root: root, query: "", expandedFile: null });
797
+ setPreview({ file: null, status: "loading", content: "", message: "" });
798
+ },
799
+ onRefresh: refresh,
800
+ t: t,
801
+ UI: UI,
802
+ now: now,
803
+ expandedFile: selection.expandedFile,
804
+ preview: preview,
805
+ onToggleFile: function (file) {
806
+ if (selection.expandedFile === file) {
807
+ setSelection(function (current) {
808
+ return { root: current.root, query: current.query, expandedFile: null };
809
+ });
810
+ return;
811
+ }
812
+ setSelection(function (current) {
813
+ return { root: current.root, query: current.query, expandedFile: file };
814
+ });
815
+ setPreview({ file: file, status: "loading", content: "", message: "" });
816
+ loadFileContent(fetch, selected, file).then(function (result) {
817
+ setPreview(function (current) {
818
+ if (current.file !== file) {
819
+ return current;
820
+ }
821
+ return { file: file, status: result.status, content: result.content, message: result.message };
822
+ });
823
+ });
824
+ },
825
+ query: selection.query,
826
+ onQueryChange: function (value) {
827
+ setSelection(function (current) {
828
+ return { root: current.root, query: value, expandedFile: current.expandedFile };
829
+ });
830
+ },
831
+ });
329
832
  }
833
+ body = createElement(
834
+ "div",
835
+ { key: "body", className: "dce-body" },
836
+ createElement("div", { className: "dce-hint" }, t("hint")),
837
+ inner,
838
+ );
330
839
  return createElement(
331
840
  "div",
332
- { style: CARD_STYLE },
333
- createElement(
334
- "div",
335
- { style: { display: "flex", alignItems: "center", gap: "12px" } },
336
- createElement("div", { style: TITLE_STYLE }, t("title")),
337
- createElement("span", { style: { flex: 1 } }),
338
- createElement(RefreshButton, {
339
- UI: UI,
340
- t: t,
341
- onClick: function () {
342
- setModel({ status: "loading", model: null, message: "" });
343
- reload(function (n) {
344
- return n + 1;
345
- });
346
- },
347
- }),
348
- ),
349
- createElement("div", { style: HINT_STYLE }, t("hint")),
841
+ { className: "dce-card", "data-open": "true" },
842
+ header,
350
843
  body,
351
844
  );
352
845
  }
@@ -354,6 +847,7 @@ window.__ModuleLoader__.load({
354
847
  exports.name = NS;
355
848
  exports.inject = ["slots", "locale", "theme"];
356
849
  exports.loadCardModel = loadCardModel;
850
+ exports.loadFileContent = loadFileContent;
357
851
  exports.apply = function apply(ctx) {
358
852
  var tRaw = null;
359
853
  try {