dsh-continual-evolve 0.14.0 → 0.14.2
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/README.md +7 -6
- package/README.zh.md +7 -6
- package/client/client.js +247 -41
- package/lib/card-routes.d.ts +2 -2
- package/lib/card-routes.js +21 -13
- package/lib/index.d.ts +7 -6
- package/lib/index.js +41 -9
- package/lib/workspace-catalog.d.ts +30 -0
- package/lib/workspace-catalog.js +149 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ English | [中文](README.zh.md)
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -23,7 +23,7 @@ Reusable experience gathered in one session (user preferences, pitfalls, project
|
|
|
23
23
|
1. **Session-start injection** — the plugin registers a single system-prompt section: the `MEMORY.md` index content (whole-line truncated over budget, with a read-the-directory hint), the absolute store path, and the when_to_save guide. The section is computed once per session and reused byte-for-byte, keeping the system prompt stable and the prompt cache warm; an empty store costs zero tokens.
|
|
24
24
|
2. **Native read/write** — DSH fully permits file access inside the workspace (reads are never fenced; the write fence only covers paths outside the workspace, see [`docs/FAQ.md`](docs/FAQ.md) #6). The model reads, writes, and edits memory files and maintains the index directly; the store is bootstrapped on first use.
|
|
25
25
|
3. **Governance is the file** — no versions, no snapshots, no approvals, no background extraction: a bad memory is a visible file in the workspace, and deleting it is the retirement path. Memories are personal context — on bootstrap in a git workspace the store appends `.evolve/` to the workspace `.gitignore` (non-git workspaces are left untouched).
|
|
26
|
-
4. **Read-only card** — the plugin mounts a read-only memory card on its own page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card (collapsed by default, header toggle with a rotating chevron, hover/open frame states) and aligned with ZCode's Settings → Memory viewer: one workspace at a time
|
|
26
|
+
4. **Read-only card** — the plugin mounts a read-only memory card on its own page in the official plugin manager (the `plugins.bundle.config` slot; hosts without it silently degrade to no card), shaped like the market's own settings card (collapsed by default, header toggle with a rotating chevron, hover/open frame states) and aligned with ZCode's Settings → Memory viewer: one workspace at a time behind a dropdown scope selector fed by the host workspace registry, a file search box, per-file relative updated times, click-to-preview file bodies (5 MiB cap), plus index-drift warnings (unindexed files, index rows whose file is gone). The plugin's display name and description localize through the host's package `locale/*.json` metadata channel. The card follows the market's look — host theme tokens, the `Button` primitive for refresh, and zh/en copy that tracks the DSH language (plain themed elements on old hosts). The card is a request-time projection of the files on disk — the single source of truth remains `.evolve/memory/`; the card has no edit path, edit the files directly.
|
|
27
27
|
|
|
28
28
|
Memory file format (same as ZCode): frontmatter with `name` / `description` (the hook that decides whether a future session recalls it) / `metadata.type` (`user | feedback | project | reference`); `feedback` bodies must carry **Why:** and **How to apply:** lines.
|
|
29
29
|
|
|
@@ -66,9 +66,9 @@ Profile patch example:
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
69
|
-
pnpm test # vitest (
|
|
69
|
+
pnpm test # vitest (92 tests)
|
|
70
70
|
pnpm test:coverage # v8 coverage, CI-enforced thresholds
|
|
71
|
-
pnpm lint # oxlint src test
|
|
71
|
+
pnpm lint # oxlint src test client
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Layout:
|
|
@@ -79,12 +79,13 @@ Layout:
|
|
|
79
79
|
│ ├── memory-section.ts # injection: read index / bootstrap / truncate / session freeze
|
|
80
80
|
│ ├── memory-guide.ts # guide: when_to_save and upkeep (adapted from ZCode)
|
|
81
81
|
│ ├── memory-snapshot.ts # card: read-only store projection (entries / drift / error degrade)
|
|
82
|
-
│ ├──
|
|
82
|
+
│ ├── workspace-catalog.ts# card: workspace list from the host registry (the API read fence)
|
|
83
|
+
│ ├── known-workspaces.ts# card: in-process LRU fallback source for the catalogue
|
|
83
84
|
│ └── card-routes.ts # card: GET-only read API on the host webServer
|
|
84
85
|
├── client/
|
|
85
86
|
│ └── client.js # hand-written client bundle: read-only card in the plugin manager
|
|
86
87
|
├── locale/ # host package metadata: localized display name + description (zh/en)
|
|
87
|
-
├── test/ # vitest suite (
|
|
88
|
+
├── test/ # vitest suite (8 files)
|
|
88
89
|
├── lib/ # build output (tsc)
|
|
89
90
|
├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
|
|
90
91
|
└── .agents/ # AI collaboration layer (AGENTS.md, skills, ADR notes)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -23,7 +23,7 @@ Agent 每个会话积累的可复用经验(用户偏好、踩坑教训、项
|
|
|
23
23
|
1. **开局注入** —— 插件只注册一个 system-prompt section:注入 `MEMORY.md` 索引正文(超预算按整行截断并提示读目录)、记忆目录绝对路径、when_to_save 指南。该 section 每会话只算一次并逐字节复用,system prompt 稳定、prompt cache 持续命中;空 store 零 token。
|
|
24
24
|
2. **原生读写** —— DSH 对工作区内文件读写全放行(读路径从不设防,写围栏只围工作区外,见 [`docs/FAQ.md`](docs/FAQ.md) #6)。模型直接 Read/Write/Edit 记忆文件并维护索引;store 不存在时自动 bootstrap。
|
|
25
25
|
3. **治理即文件** —— 无版本、无快照、无审批、无后台提取:一条坏记忆就是工作区里一个可见的文件,删掉它就是退役。记忆是个人上下文——bootstrap 时若工作区是 git 仓库会自动把 `.evolve/` 追加进 `.gitignore`(非 git 工作区不碰任何文件)。
|
|
26
|
-
4. **只读卡片** —— 插件在官方插件管理的本 bundle 页面挂一张只读记忆卡片(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡(默认收起、头部整行可点 + 旋转 chevron、hover/展开框态),内容对齐 ZCode
|
|
26
|
+
4. **只读卡片** —— 插件在官方插件管理的本 bundle 页面挂一张只读记忆卡片(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡(默认收起、头部整行可点 + 旋转 chevron、hover/展开框态),内容对齐 ZCode 设置的记忆选项卡——工作区一次选一个(下拉作用域选择器,目录取宿主工作区注册表)、文件搜索框、每个文件的相对更新时间、点击行内展开文件内容预览(5 MiB 上限),外加索引漂移警告(未索引文件与索引失联文件)。插件的显示名与描述走宿主包元数据 `locale/*.json` 通道双语切换。卡片与市场同款——宿主主题 token、`Button` 原语刷新按钮、中英文案跟随 DSH 语言(老宿主降级为同主题裸元素)。卡片是磁盘文件的请求时投影——单一事实源永远是 `.evolve/memory/` 里的文件,卡片没有编辑路径,编辑请直接改文件。
|
|
27
27
|
|
|
28
28
|
记忆文件格式(与 ZCode 一致):frontmatter 带 `name` / `description`(决定未来会话会不会想起它)/ `metadata.type`(`user | feedback | project | reference`);`feedback` 正文必带 **Why:** 与 **How to apply:**。
|
|
29
29
|
|
|
@@ -66,9 +66,9 @@ profile patch 示例:
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
69
|
-
pnpm test # vitest(
|
|
69
|
+
pnpm test # vitest(92 例)
|
|
70
70
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
71
|
-
pnpm lint # oxlint src test
|
|
71
|
+
pnpm lint # oxlint src test client
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
目录结构:
|
|
@@ -79,12 +79,13 @@ pnpm lint # oxlint src test
|
|
|
79
79
|
│ ├── memory-section.ts # 注入:读索引 / bootstrap / 截断 / 会话冻结
|
|
80
80
|
│ ├── memory-guide.ts # 指南:when_to_save 与维护纪律(改编自 ZCode)
|
|
81
81
|
│ ├── memory-snapshot.ts # 卡片:记忆库只读投影(条目 / 漂移 / 错误降级)
|
|
82
|
-
│ ├──
|
|
82
|
+
│ ├── workspace-catalog.ts# 卡片:工作区目录(宿主注册表;API 读围栏)
|
|
83
|
+
│ ├── known-workspaces.ts# 卡片:目录的进程内 LRU 回退源
|
|
83
84
|
│ └── card-routes.ts # 卡片:webServer 上的 GET-only 只读 API
|
|
84
85
|
├── client/
|
|
85
86
|
│ └── client.js # 手写 client bundle:官方插件管理内只读卡片
|
|
86
87
|
├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
|
|
87
|
-
├── test/ # vitest 测试(
|
|
88
|
+
├── test/ # vitest 测试(8 个文件)
|
|
88
89
|
├── lib/ # 构建产物(tsc)
|
|
89
90
|
├── docs/ # design.md(设计)· FAQ.md(踩坑)
|
|
90
91
|
└── .agents/ # AI 协作层(AGENTS.md、技能、ADR 笔记)
|
package/client/client.js
CHANGED
|
@@ -25,13 +25,15 @@
|
|
|
25
25
|
* reads as another product). The stylesheet is injected once, like the
|
|
26
26
|
* host's own CSS-module tags.
|
|
27
27
|
*
|
|
28
|
-
* Content (v0.14): aligned with ZCode's Settings → Memory
|
|
29
|
-
* .evolve store shape was itself aligned with — one workspace
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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.
|
|
35
37
|
* Locale/theme/primitives wiring unchanged from v0.13: `ctx.locale.register`
|
|
36
38
|
* + `bind`, `var(--dsw-alias-*)` tokens, primitives with plain-element
|
|
37
39
|
* fallbacks so an old host never blanks the settings dialog. Read-only
|
|
@@ -74,6 +76,7 @@ window.__ModuleLoader__.load({
|
|
|
74
76
|
var createElement = react.createElement;
|
|
75
77
|
var useState = react.useState;
|
|
76
78
|
var useEffect = react.useEffect;
|
|
79
|
+
var useRef = react.useRef;
|
|
77
80
|
|
|
78
81
|
var NS = "dsh-continual-evolve";
|
|
79
82
|
var SLOT = "plugins.bundle.config";
|
|
@@ -87,11 +90,12 @@ window.__ModuleLoader__.load({
|
|
|
87
90
|
hint: "事实源是各工作区 .evolve/memory/ 下的 markdown 文件——模型原生读写;本卡片只展示,编辑请直接改文件。",
|
|
88
91
|
loading: "读取中…",
|
|
89
92
|
loadFailed: "卡片数据加载失败:",
|
|
90
|
-
empty: "
|
|
93
|
+
empty: "还没有任何工作区有记忆库——在某个工作区开一次会话后回来刷新。",
|
|
91
94
|
absent: "该工作区尚无记忆库(开一次会话自动创建)。",
|
|
92
95
|
readFailed: "记忆库读取失败:",
|
|
93
96
|
stats: "{files} 条记忆 · 索引 {index} 行",
|
|
94
97
|
filesHeading: "文件",
|
|
98
|
+
scopeLabel: "选择工作区",
|
|
95
99
|
searchPlaceholder: "搜索记忆文件…",
|
|
96
100
|
searchEmpty: "没有匹配的记忆文件。",
|
|
97
101
|
missing: "索引失联(索引引用但文件已删):",
|
|
@@ -114,11 +118,12 @@ window.__ModuleLoader__.load({
|
|
|
114
118
|
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.",
|
|
115
119
|
loading: "Loading…",
|
|
116
120
|
loadFailed: "Failed to load card data: ",
|
|
117
|
-
empty: "
|
|
121
|
+
empty: "No workspace has a memory store yet — open a session in a workspace, then come back and refresh.",
|
|
118
122
|
absent: "No memory store in this workspace yet (one is created when you open a session).",
|
|
119
123
|
readFailed: "Failed to read the memory store: ",
|
|
120
124
|
stats: "{files} memories · {index} index rows",
|
|
121
125
|
filesHeading: "Files",
|
|
126
|
+
scopeLabel: "Choose workspace",
|
|
122
127
|
searchPlaceholder: "Search memory files…",
|
|
123
128
|
searchEmpty: "No matching memory files.",
|
|
124
129
|
missing: "Dangling index (indexed but deleted): ",
|
|
@@ -178,7 +183,7 @@ window.__ModuleLoader__.load({
|
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
/**
|
|
181
|
-
* Assemble the card's view model: the workspace
|
|
186
|
+
* Assemble the card's view model: the workspace catalogue, then one
|
|
182
187
|
* snapshot per workspace. Network or API failure per workspace degrades
|
|
183
188
|
* to an `error` string on that workspace's entry — one bad store never
|
|
184
189
|
* blanks the whole card.
|
|
@@ -196,17 +201,20 @@ window.__ModuleLoader__.load({
|
|
|
196
201
|
var entries = await Promise.all(
|
|
197
202
|
workspaces.map(async function (workspace) {
|
|
198
203
|
var root = typeof workspace.root === "string" ? workspace.root : "";
|
|
199
|
-
var
|
|
204
|
+
var label =
|
|
205
|
+
typeof workspace.label === "string" && workspace.label.trim().length > 0
|
|
206
|
+
? workspace.label
|
|
207
|
+
: workspaceLabel(root);
|
|
200
208
|
try {
|
|
201
209
|
var snapshotResponse = await fetchImpl(API_ROOT + "/memory?root=" + encodeURIComponent(root));
|
|
202
210
|
if (!snapshotResponse.ok) {
|
|
203
|
-
return { root: root,
|
|
211
|
+
return { root: root, label: label, snapshot: null, error: "HTTP " + snapshotResponse.status };
|
|
204
212
|
}
|
|
205
|
-
return { root: root,
|
|
213
|
+
return { root: root, label: label, snapshot: await snapshotResponse.json(), error: null };
|
|
206
214
|
} catch (error) {
|
|
207
215
|
return {
|
|
208
216
|
root: root,
|
|
209
|
-
|
|
217
|
+
label: label,
|
|
210
218
|
snapshot: null,
|
|
211
219
|
error: error instanceof Error ? error.message : String(error),
|
|
212
220
|
};
|
|
@@ -290,9 +298,17 @@ window.__ModuleLoader__.load({
|
|
|
290
298
|
".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}",
|
|
291
299
|
".dce-hint{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}",
|
|
292
300
|
".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}",
|
|
293
|
-
".dce-
|
|
294
|
-
".dce-
|
|
295
|
-
|
|
301
|
+
".dce-scope-wrap{position:relative;display:inline-flex;min-width:0;max-width:100%;align-self:flex-start}",
|
|
302
|
+
".dce-scope-pill{border-radius:999px}",
|
|
303
|
+
".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}",
|
|
304
|
+
".dce-scope:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}",
|
|
305
|
+
".dce-scope:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:1px}",
|
|
306
|
+
".dce-scope-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
307
|
+
".dce-scope-caret{display:inline-flex;color:var(--dsw-alias-label-tertiary,#8b93a1)}",
|
|
308
|
+
".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)}",
|
|
309
|
+
".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}",
|
|
310
|
+
".dce-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.03))}",
|
|
311
|
+
'.dce-menu-item[data-active="true"]{background:var(--dsw-alias-bg-module-platform,#eef1f4)}',
|
|
296
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}",
|
|
297
313
|
".dce-search::placeholder{color:var(--dsw-alias-label-tertiary,#8b93a1)}",
|
|
298
314
|
".dce-search:focus-visible{border-color:var(--dsw-alias-brand-primary,#4f6ef7)}",
|
|
@@ -305,6 +321,7 @@ window.__ModuleLoader__.load({
|
|
|
305
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}",
|
|
306
322
|
".dce-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.03))}",
|
|
307
323
|
".dce-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}",
|
|
324
|
+
".dce-file-icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,#eef1f4)}",
|
|
308
325
|
".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}",
|
|
309
326
|
".dce-file-time{flex:none;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px}",
|
|
310
327
|
".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)}",
|
|
@@ -353,9 +370,20 @@ window.__ModuleLoader__.load({
|
|
|
353
370
|
var expanded = props.expanded;
|
|
354
371
|
var preview = props.preview;
|
|
355
372
|
var onToggle = props.onToggle;
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
373
|
+
var UI = props.UI;
|
|
374
|
+
var rowChildren = [];
|
|
375
|
+
if (UI !== null && UI !== undefined && UI.FileIcon !== undefined && UI.FileIcon !== null) {
|
|
376
|
+
// ZCode's memory rows open with a category tile; the host classifies
|
|
377
|
+
// the file itself, so a memory .md gets the markdown glyph.
|
|
378
|
+
rowChildren.push(
|
|
379
|
+
createElement(
|
|
380
|
+
"span",
|
|
381
|
+
{ key: "icon", className: "dce-file-icon" },
|
|
382
|
+
createElement(UI.FileIcon, { path: file.file, size: 16 }),
|
|
383
|
+
),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
rowChildren.push(createElement("span", { key: "name", className: "dce-file-name" }, file.file));
|
|
359
387
|
var time = formatRelative(file.updatedAt, now, t);
|
|
360
388
|
if (time !== "") {
|
|
361
389
|
rowChildren.push(createElement("span", { key: "time", className: "dce-file-time" }, time));
|
|
@@ -393,47 +421,215 @@ window.__ModuleLoader__.load({
|
|
|
393
421
|
return createElement("li", { className: "dce-item" }, children);
|
|
394
422
|
}
|
|
395
423
|
|
|
396
|
-
/**
|
|
397
|
-
function
|
|
398
|
-
var
|
|
424
|
+
/** The host's folder marker for a workspace, or nothing on an old host. */
|
|
425
|
+
function FolderMark(props) {
|
|
426
|
+
var UI = props.UI;
|
|
427
|
+
if (UI !== null && UI !== undefined && UI.Folder !== undefined && UI.Folder !== null) {
|
|
428
|
+
return createElement(UI.Folder, { size: 16, "aria-hidden": "true" });
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The workspace scope selector, ZCode's Settings → Memory shape: one
|
|
435
|
+
* scope at a time behind a dropdown (folder marker + current workspace
|
|
436
|
+
* label + chevron), never a row of chips — a chip row can only render
|
|
437
|
+
* when several workspaces are known, so in the common single-workspace
|
|
438
|
+
* case it renders nothing at all and the capability reads as missing.
|
|
439
|
+
*
|
|
440
|
+
* The list is the host's own `Menu` primitive whenever the host provides
|
|
441
|
+
* it: the same surface the rest of the settings UI uses, and it owns
|
|
442
|
+
* outside-click/Escape handling. A hand-rolled document listener gets
|
|
443
|
+
* that wrong — React flushes this component's effect synchronously for
|
|
444
|
+
* the discrete click that opens the list, so a listener attached there
|
|
445
|
+
* catches that very click and closes the list again (the button then
|
|
446
|
+
* looks dead). The hand-rolled list stays as the old-host fallback and
|
|
447
|
+
* closes on a pointerdown outside its wrapper, which cannot be the click
|
|
448
|
+
* that opened it.
|
|
449
|
+
*/
|
|
450
|
+
function WorkspaceScopeMenu(props) {
|
|
451
|
+
var workspaces = props.workspaces;
|
|
399
452
|
var selected = props.selected;
|
|
400
453
|
var onSelect = props.onSelect;
|
|
401
454
|
var t = props.t;
|
|
402
455
|
var UI = props.UI;
|
|
403
|
-
var
|
|
404
|
-
var
|
|
405
|
-
var
|
|
406
|
-
var
|
|
407
|
-
var
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
456
|
+
var openState = useState(false);
|
|
457
|
+
var open = openState[0];
|
|
458
|
+
var setOpen = openState[1];
|
|
459
|
+
var wrapRef = useRef(null);
|
|
460
|
+
var hostMenu =
|
|
461
|
+
UI !== null && UI !== undefined && UI.Menu !== undefined && UI.Menu !== null ? UI.Menu : null;
|
|
462
|
+
useEffect(
|
|
463
|
+
function () {
|
|
464
|
+
if (hostMenu !== null || !open || typeof document === "undefined") {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
var onPointerDown = function (event) {
|
|
468
|
+
var node = wrapRef.current;
|
|
469
|
+
var target = event.target;
|
|
470
|
+
var inside =
|
|
471
|
+
node !== null &&
|
|
472
|
+
node !== undefined &&
|
|
473
|
+
typeof node.contains === "function" &&
|
|
474
|
+
target !== null &&
|
|
475
|
+
target !== undefined &&
|
|
476
|
+
node.contains(target);
|
|
477
|
+
if (!inside) {
|
|
478
|
+
setOpen(false);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var onKeyDown = function (event) {
|
|
482
|
+
if (event.key === "Escape") {
|
|
483
|
+
setOpen(false);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
487
|
+
document.addEventListener("keydown", onKeyDown);
|
|
488
|
+
return function () {
|
|
489
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
490
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
[open, hostMenu],
|
|
494
|
+
);
|
|
495
|
+
var current = null;
|
|
496
|
+
for (var index = 0; index < workspaces.length; index++) {
|
|
497
|
+
if (workspaces[index].root === selected) {
|
|
498
|
+
current = workspaces[index];
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (current === null && workspaces.length > 0) {
|
|
503
|
+
current = workspaces[0];
|
|
504
|
+
}
|
|
505
|
+
var label = current === null ? "" : current.label || workspaceLabel(current.root);
|
|
506
|
+
var scopeChildren = [
|
|
507
|
+
createElement("span", { key: "label", className: "dce-scope-label" }, label),
|
|
508
|
+
createElement("span", { key: "caret", className: "dce-scope-caret" }, createElement(Chevron, null)),
|
|
509
|
+
];
|
|
510
|
+
var toggle = function () {
|
|
511
|
+
setOpen(!open);
|
|
512
|
+
};
|
|
513
|
+
var trigger = null;
|
|
514
|
+
if (UI !== null && UI !== undefined && typeof UI.Button !== "undefined" && UI.Button !== null) {
|
|
515
|
+
trigger = createElement(
|
|
516
|
+
UI.Button,
|
|
517
|
+
{
|
|
518
|
+
key: "trigger",
|
|
519
|
+
variant: "outline",
|
|
520
|
+
size: "md",
|
|
521
|
+
icon: createElement(FolderMark, { UI: UI }),
|
|
522
|
+
className: "dce-scope-pill",
|
|
523
|
+
type: "button",
|
|
524
|
+
"aria-haspopup": "menu",
|
|
525
|
+
"aria-expanded": open ? "true" : "false",
|
|
526
|
+
"aria-label": t("scopeLabel"),
|
|
527
|
+
onClick: toggle,
|
|
528
|
+
},
|
|
529
|
+
scopeChildren,
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
trigger = createElement(
|
|
533
|
+
"button",
|
|
534
|
+
{
|
|
535
|
+
key: "trigger",
|
|
536
|
+
type: "button",
|
|
537
|
+
className: "dce-scope",
|
|
538
|
+
"aria-haspopup": "menu",
|
|
539
|
+
"aria-expanded": open ? "true" : "false",
|
|
540
|
+
"aria-label": t("scopeLabel"),
|
|
541
|
+
onClick: toggle,
|
|
542
|
+
},
|
|
543
|
+
scopeChildren,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (hostMenu !== null) {
|
|
547
|
+
return createElement(hostMenu, {
|
|
548
|
+
key: "scope",
|
|
549
|
+
open: open,
|
|
550
|
+
anchor: trigger,
|
|
551
|
+
items: workspaces.map(function (workspace) {
|
|
552
|
+
return {
|
|
553
|
+
id: workspace.root,
|
|
554
|
+
label: workspace.label || workspaceLabel(workspace.root),
|
|
555
|
+
icon: createElement(FolderMark, { UI: UI }),
|
|
556
|
+
};
|
|
557
|
+
}),
|
|
558
|
+
selectedId: selected,
|
|
559
|
+
onSelect: function (id) {
|
|
560
|
+
setOpen(false);
|
|
561
|
+
if (id !== selected) {
|
|
562
|
+
onSelect(id);
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
onClose: function () {
|
|
566
|
+
setOpen(false);
|
|
567
|
+
},
|
|
568
|
+
align: "start",
|
|
569
|
+
side: "bottom",
|
|
570
|
+
portal: true,
|
|
571
|
+
selection: "check",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
var menu = null;
|
|
575
|
+
if (open) {
|
|
576
|
+
menu = createElement(
|
|
413
577
|
"div",
|
|
414
|
-
{ key: "
|
|
415
|
-
|
|
578
|
+
{ key: "menu", className: "dce-menu", role: "menu", "aria-label": t("scopeLabel") },
|
|
579
|
+
workspaces.map(function (workspace) {
|
|
416
580
|
return createElement(
|
|
417
581
|
"button",
|
|
418
582
|
{
|
|
419
583
|
key: workspace.root,
|
|
420
584
|
type: "button",
|
|
421
|
-
|
|
585
|
+
role: "menuitemradio",
|
|
586
|
+
"aria-checked": workspace.root === selected ? "true" : "false",
|
|
587
|
+
className: "dce-menu-item",
|
|
422
588
|
"data-active": workspace.root === selected ? "true" : "false",
|
|
423
589
|
onClick: function () {
|
|
424
|
-
|
|
590
|
+
setOpen(false);
|
|
591
|
+
if (workspace.root !== selected) {
|
|
592
|
+
onSelect(workspace.root);
|
|
593
|
+
}
|
|
425
594
|
},
|
|
426
595
|
},
|
|
427
|
-
workspaceLabel(workspace.root),
|
|
596
|
+
workspace.label || workspaceLabel(workspace.root),
|
|
428
597
|
);
|
|
429
598
|
}),
|
|
430
599
|
);
|
|
431
600
|
}
|
|
601
|
+
return createElement("div", { key: "scope", className: "dce-scope-wrap", ref: wrapRef }, trigger, menu);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** One workspace's memory store: stats, drift warnings, searchable file list with previews. */
|
|
605
|
+
function WorkspaceBlock(props) {
|
|
606
|
+
var entry = props.entry;
|
|
607
|
+
var selected = props.selected;
|
|
608
|
+
var onSelect = props.onSelect;
|
|
609
|
+
var t = props.t;
|
|
610
|
+
var UI = props.UI;
|
|
611
|
+
var now = props.now;
|
|
612
|
+
var expandedFile = props.expandedFile;
|
|
613
|
+
var preview = props.preview;
|
|
614
|
+
var onToggleFile = props.onToggleFile;
|
|
615
|
+
var onRefresh = props.onRefresh;
|
|
616
|
+
var query = props.query;
|
|
617
|
+
var onQueryChange = props.onQueryChange;
|
|
618
|
+
var scope = createElement(WorkspaceScopeMenu, {
|
|
619
|
+
key: "scope",
|
|
620
|
+
workspaces: entry.workspaces.map(function (workspace) {
|
|
621
|
+
return { root: workspace.root, label: workspace.label };
|
|
622
|
+
}),
|
|
623
|
+
selected: selected,
|
|
624
|
+
onSelect: onSelect,
|
|
625
|
+
t: t,
|
|
626
|
+
UI: UI,
|
|
627
|
+
});
|
|
432
628
|
var active = entry.workspaces.find(function (workspace) {
|
|
433
629
|
return workspace.root === selected;
|
|
434
630
|
});
|
|
435
631
|
if (active === undefined) {
|
|
436
|
-
return createElement("div", { key: "block" },
|
|
632
|
+
return createElement("div", { key: "block", className: "dce-workspace" }, scope);
|
|
437
633
|
}
|
|
438
634
|
var body = null;
|
|
439
635
|
if (active.error !== null && active.error !== undefined) {
|
|
@@ -524,6 +720,7 @@ window.__ModuleLoader__.load({
|
|
|
524
720
|
file: file,
|
|
525
721
|
now: now,
|
|
526
722
|
t: t,
|
|
723
|
+
UI: UI,
|
|
527
724
|
expanded: expandedFile === file.file,
|
|
528
725
|
preview: preview,
|
|
529
726
|
onToggle: function () {
|
|
@@ -536,7 +733,7 @@ window.__ModuleLoader__.load({
|
|
|
536
733
|
}
|
|
537
734
|
body = parts;
|
|
538
735
|
}
|
|
539
|
-
return createElement("div", { key: "block", className: "dce-workspace" },
|
|
736
|
+
return createElement("div", { key: "block", className: "dce-workspace" }, scope, body);
|
|
540
737
|
}
|
|
541
738
|
|
|
542
739
|
/**
|
|
@@ -651,6 +848,7 @@ window.__ModuleLoader__.load({
|
|
|
651
848
|
setSelection({ root: root, query: "", expandedFile: null });
|
|
652
849
|
setPreview({ file: null, status: "loading", content: "", message: "" });
|
|
653
850
|
},
|
|
851
|
+
onRefresh: refresh,
|
|
654
852
|
t: t,
|
|
655
853
|
UI: UI,
|
|
656
854
|
now: now,
|
|
@@ -750,7 +948,15 @@ window.__ModuleLoader__.load({
|
|
|
750
948
|
}
|
|
751
949
|
var UI = null;
|
|
752
950
|
if (primitives !== undefined && primitives !== null && typeof primitives.Button !== "undefined") {
|
|
753
|
-
|
|
951
|
+
// The scope selector prefers the host's own Menu and folder icon;
|
|
952
|
+
// each stays optional so a host with only Button still gets the
|
|
953
|
+
// hand-rolled list and a plain trigger.
|
|
954
|
+
UI = {
|
|
955
|
+
Button: primitives.Button,
|
|
956
|
+
Menu: primitives.Menu,
|
|
957
|
+
Folder: primitives.IconFolderOpenOutlineRegular,
|
|
958
|
+
FileIcon: primitives.FileTypeIcon,
|
|
959
|
+
};
|
|
754
960
|
}
|
|
755
961
|
ctx.slots.inject(SLOT, function () {
|
|
756
962
|
return ctx.slots.register(
|
package/lib/card-routes.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkspaceCatalog } from "./workspace-catalog.js";
|
|
2
2
|
/** URL prefix of the card API (kind-exact routes registered under it). */
|
|
3
3
|
export declare const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
|
|
4
4
|
export declare const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
|
|
@@ -29,5 +29,5 @@ export interface CardWebServer {
|
|
|
29
29
|
* shape the host hands back (or none, as on some host versions), the
|
|
30
30
|
* disposer calls only the functions among them.
|
|
31
31
|
*/
|
|
32
|
-
export declare function mountCardRoutes(webServer: CardWebServer,
|
|
32
|
+
export declare function mountCardRoutes(webServer: CardWebServer, catalog: WorkspaceCatalog): () => void;
|
|
33
33
|
//# sourceMappingURL=card-routes.d.ts.map
|
package/lib/card-routes.js
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mounted through the host's `webServer` service (same registration shape
|
|
5
5
|
* dshmarket uses: `kind: "exact"` routes on the profile's web server), the
|
|
6
|
-
* three endpoints are GET-only projections: the workspace
|
|
6
|
+
* three endpoints are GET-only projections: the workspace catalogue, the
|
|
7
7
|
* memory snapshot for a known root, and one memory file's body for the card's
|
|
8
|
-
* content preview. The
|
|
9
|
-
* resolve their `root` parameter only against
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* content preview. The catalogue IS the security fence — the memory endpoints
|
|
9
|
+
* resolve their `root` parameter only against workspaces the host's registry
|
|
10
|
+
* (or, on hosts without one, a root this process has served) knows about, and
|
|
11
|
+
* the file endpoint additionally accepts only plain .md entries of that
|
|
12
|
+
* workspace's memory directory, so the API cannot read arbitrary paths.
|
|
13
|
+
* Handlers are synchronous and never throw past the response.
|
|
13
14
|
*/
|
|
14
15
|
import { resolve } from "node:path";
|
|
15
16
|
import { memoryFileContent, memorySnapshot } from "./memory-snapshot.js";
|
|
@@ -30,7 +31,7 @@ function isGet(request) {
|
|
|
30
31
|
* shape the host hands back (or none, as on some host versions), the
|
|
31
32
|
* disposer calls only the functions among them.
|
|
32
33
|
*/
|
|
33
|
-
export function mountCardRoutes(webServer,
|
|
34
|
+
export function mountCardRoutes(webServer, catalog) {
|
|
34
35
|
const disposers = [];
|
|
35
36
|
disposers.push(webServer.register({
|
|
36
37
|
kind: "exact",
|
|
@@ -41,7 +42,9 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
41
42
|
response.end();
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
// The listed set is the display projection (workspaces with a
|
|
46
|
+
// memory directory); the read fence is `known` below.
|
|
47
|
+
sendJson(response, 200, { workspaces: catalog.list() });
|
|
45
48
|
},
|
|
46
49
|
}));
|
|
47
50
|
disposers.push(webServer.register({
|
|
@@ -58,10 +61,10 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
58
61
|
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
61
|
-
const known =
|
|
64
|
+
const known = knownEntryOf(catalog, root);
|
|
62
65
|
if (known === undefined) {
|
|
63
|
-
// The
|
|
64
|
-
//
|
|
66
|
+
// The catalogue answers 404 without distinguishing "not a
|
|
67
|
+
// workspace here" from "wrong path": no filesystem oracle.
|
|
65
68
|
sendJson(response, 404, { error: "unknown workspace root" });
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
@@ -87,10 +90,10 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
87
90
|
sendJson(response, 400, { error: "file query parameter is required" });
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
90
|
-
const known =
|
|
93
|
+
const known = knownEntryOf(catalog, root);
|
|
91
94
|
if (known === undefined) {
|
|
92
95
|
// Same fence as the snapshot route: 404 without distinguishing
|
|
93
|
-
// "
|
|
96
|
+
// "not a workspace here" from "wrong path".
|
|
94
97
|
sendJson(response, 404, { error: "unknown workspace root" });
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
@@ -126,6 +129,11 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
126
129
|
}
|
|
127
130
|
};
|
|
128
131
|
}
|
|
132
|
+
/** The catalogued workspace a `root` query parameter resolves to, if any. */
|
|
133
|
+
function knownEntryOf(catalog, root) {
|
|
134
|
+
const target = resolve(root);
|
|
135
|
+
return catalog.known().find((workspace) => workspace.root === target);
|
|
136
|
+
}
|
|
129
137
|
/** The decoded `root` query parameter, or null when absent/relative. */
|
|
130
138
|
function rootParamOf(request) {
|
|
131
139
|
try {
|
package/lib/index.d.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* background extraction, and no governance. See
|
|
10
10
|
* .agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md.
|
|
11
11
|
*
|
|
12
|
-
* The 2026-10-04 plugin-management card adds a read-only surface: the
|
|
13
|
-
* workspace
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md
|
|
12
|
+
* The 2026-10-04 plugin-management card adds a read-only surface: the host's
|
|
13
|
+
* workspace registry feeds a catalogue, and a `webServer` route triple projects
|
|
14
|
+
* any catalogued workspace's store for the card in the official plugin manager
|
|
15
|
+
* (`plugins.bundle.config`, client bundle in client/client.js). The card is a
|
|
16
|
+
* viewer — see
|
|
17
|
+
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md and
|
|
18
|
+
* .agents/notes/implemented/feature/2026-10-05-zcode-workspace-switcher.md.
|
|
18
19
|
*
|
|
19
20
|
* The host API surface is declared locally (the minimal shape this plugin
|
|
20
21
|
* touches) instead of importing the DSH type packages — the section call is
|
package/lib/index.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* background extraction, and no governance. See
|
|
10
10
|
* .agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md.
|
|
11
11
|
*
|
|
12
|
-
* The 2026-10-04 plugin-management card adds a read-only surface: the
|
|
13
|
-
* workspace
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md
|
|
12
|
+
* The 2026-10-04 plugin-management card adds a read-only surface: the host's
|
|
13
|
+
* workspace registry feeds a catalogue, and a `webServer` route triple projects
|
|
14
|
+
* any catalogued workspace's store for the card in the official plugin manager
|
|
15
|
+
* (`plugins.bundle.config`, client bundle in client/client.js). The card is a
|
|
16
|
+
* viewer — see
|
|
17
|
+
* .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md and
|
|
18
|
+
* .agents/notes/implemented/feature/2026-10-05-zcode-workspace-switcher.md.
|
|
18
19
|
*
|
|
19
20
|
* The host API surface is declared locally (the minimal shape this plugin
|
|
20
21
|
* touches) instead of importing the DSH type packages — the section call is
|
|
@@ -24,6 +25,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
24
25
|
import { mountCardRoutes } from "./card-routes.js";
|
|
25
26
|
import { createKnownWorkspaces } from "./known-workspaces.js";
|
|
26
27
|
import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
|
|
28
|
+
import { createWorkspaceCatalog } from "./workspace-catalog.js";
|
|
27
29
|
export const name = "continual-evolve";
|
|
28
30
|
export const inject = ["systemPrompt"];
|
|
29
31
|
export const Config = z.object({
|
|
@@ -53,9 +55,12 @@ export const Config = z.object({
|
|
|
53
55
|
/**
|
|
54
56
|
* Mount the card routes on the host web server through a nested inject, so
|
|
55
57
|
* the plugin stays mountable on hosts without that service. Called only when
|
|
56
|
-
* the card is enabled.
|
|
58
|
+
* the card is enabled. The workspace registry is looked up inside the scoped
|
|
59
|
+
* context (nothing else injects it: a host without the service must still get
|
|
60
|
+
* the card) and read per request, so a workspace created while the card is
|
|
61
|
+
* open appears on the next refresh.
|
|
57
62
|
*/
|
|
58
|
-
function mountCardWhenAvailable(host,
|
|
63
|
+
function mountCardWhenAvailable(host, served) {
|
|
59
64
|
const nested = host;
|
|
60
65
|
if (typeof nested.inject !== "function") {
|
|
61
66
|
host.logger("continual-evolve").warn("host context exposes no inject — memory card routes not mounted (memory section unaffected)");
|
|
@@ -67,9 +72,36 @@ function mountCardWhenAvailable(host, workspaces) {
|
|
|
67
72
|
host.logger("continual-evolve").warn("webServer service present but empty — memory card routes not mounted (memory section unaffected)");
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
|
|
75
|
+
const catalog = createWorkspaceCatalog({
|
|
76
|
+
registry: registryLookup(scoped),
|
|
77
|
+
served: served,
|
|
78
|
+
homeDir: process.env.DSH_HOME,
|
|
79
|
+
});
|
|
80
|
+
mountCardRoutes(webServer, catalog);
|
|
71
81
|
});
|
|
72
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* A live `ctx.workspaceRegistry` getter over the scoped context. The service
|
|
85
|
+
* is a sibling, so it is reached with `ctx.get` (property access only walks
|
|
86
|
+
* fiber ancestors) and treated as optional: an absent, foreign or throwing
|
|
87
|
+
* service degrades the catalogue to its fallback sources instead of taking the
|
|
88
|
+
* card down.
|
|
89
|
+
*/
|
|
90
|
+
function registryLookup(scoped) {
|
|
91
|
+
const context = scoped;
|
|
92
|
+
return () => {
|
|
93
|
+
if (typeof context.get !== "function") {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const service = context.get("workspaceRegistry");
|
|
98
|
+
return service === undefined || service === null ? undefined : service;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
73
105
|
export function apply(host, config) {
|
|
74
106
|
if (config.memoryIndex?.enabled === false) {
|
|
75
107
|
return;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { KnownWorkspaces } from "./known-workspaces.js";
|
|
2
|
+
/** One workspace the card knows: canonical root plus its display label. */
|
|
3
|
+
export interface CatalogEntry {
|
|
4
|
+
root: string;
|
|
5
|
+
/** Display title from the registry, else the directory name. */
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
/** The read fence (`known`) and the listed set (`list`). */
|
|
9
|
+
export interface WorkspaceCatalog {
|
|
10
|
+
/** Every workspace the card may read, in registry order. */
|
|
11
|
+
known(): CatalogEntry[];
|
|
12
|
+
/** The known workspaces that currently have a memory directory. */
|
|
13
|
+
list(): CatalogEntry[];
|
|
14
|
+
}
|
|
15
|
+
/** The slice of `ctx.workspaceRegistry` this plugin consumes. */
|
|
16
|
+
export interface WorkspaceRegistryLike {
|
|
17
|
+
list(): unknown;
|
|
18
|
+
}
|
|
19
|
+
/** Where the catalog gets its workspace records, in precedence order. */
|
|
20
|
+
export interface WorkspaceCatalogOptions {
|
|
21
|
+
/** Live registry lookup; undefined when the host exposes no registry. */
|
|
22
|
+
registry?: (() => WorkspaceRegistryLike | undefined) | undefined;
|
|
23
|
+
/** The roots this process served — the last-resort source. */
|
|
24
|
+
served: KnownWorkspaces;
|
|
25
|
+
/** Harness home; enables the `storages/workspace.json` fallback when set. */
|
|
26
|
+
homeDir?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Create the catalog over the given sources. */
|
|
29
|
+
export declare function createWorkspaceCatalog(options: WorkspaceCatalogOptions): WorkspaceCatalog;
|
|
30
|
+
//# sourceMappingURL=workspace-catalog.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The card's workspace catalog — which workspaces the card may read, and which
|
|
3
|
+
* of them it lists.
|
|
4
|
+
*
|
|
5
|
+
* The list comes from the host's workspace registry (`ctx.workspaceRegistry`,
|
|
6
|
+
* the durable records the workspace picker itself renders), not from the
|
|
7
|
+
* sessions this process happened to serve: a card that only knows its own
|
|
8
|
+
* session history can never offer ZCode's workspace switching — with one
|
|
9
|
+
* served root the selector has nothing to switch to. The served-roots map
|
|
10
|
+
* stays as a source (a session in a workspace the registry does not know about
|
|
11
|
+
* still shows up), and `storages/workspace.json` is read only when the
|
|
12
|
+
* registry service yields nothing: that file is an internal format, so it is a
|
|
13
|
+
* fallback, never the primary.
|
|
14
|
+
*
|
|
15
|
+
* Two questions, deliberately answered by two methods. `known()` is the read
|
|
16
|
+
* fence: can this root be read at all? `list()` is the display projection:
|
|
17
|
+
* which workspaces have a memory directory right now? ZCode's viewer lists
|
|
18
|
+
* only workspaces that have one, but the fence cannot be that filter — a store
|
|
19
|
+
* deleted between listing and read must degrade to the empty state on its own
|
|
20
|
+
* entry, not turn the read into a 404.
|
|
21
|
+
*/
|
|
22
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
23
|
+
import { basename, join, resolve } from "node:path";
|
|
24
|
+
import { memoryDirFor } from "./memory-section.js";
|
|
25
|
+
/** The registry stores `fs.realpath` already; resolve the rest the same way. */
|
|
26
|
+
function canonicalPath(path) {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(path);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// A vanished or unreadable directory keeps its resolved spelling: the
|
|
32
|
+
// registry itself never rewrites a record when its directory disappears.
|
|
33
|
+
return resolve(path);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** One catalogue entry from a path/title pair, or undefined when unusable. */
|
|
37
|
+
function entryOf(path, title) {
|
|
38
|
+
if (typeof path !== "string" || path.trim().length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const root = canonicalPath(path);
|
|
42
|
+
const label = typeof title === "string" && title.trim().length > 0 ? title.trim() : basename(root);
|
|
43
|
+
return { root, label };
|
|
44
|
+
}
|
|
45
|
+
/** Records from the live registry service. */
|
|
46
|
+
function registryEntries(registry) {
|
|
47
|
+
if (registry === undefined) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
let records;
|
|
51
|
+
try {
|
|
52
|
+
records = registry.list();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// A registry that throws is a host fault the card must not inherit:
|
|
56
|
+
// the next source still fills the list.
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(records)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const record of records) {
|
|
64
|
+
if (typeof record !== "object" || record === null) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const entry = entryOf(record.path, record.title);
|
|
68
|
+
if (entry !== undefined) {
|
|
69
|
+
entries.push(entry);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
/** Records from the registry's own durable file, for hosts without the service. */
|
|
75
|
+
function fileEntries(homeDir) {
|
|
76
|
+
if (homeDir === undefined || homeDir.trim().length === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(readFileSync(join(homeDir, "storages", "workspace.json"), "utf8"));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Missing or unreadable file: no workspaces from this source, no throw.
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const table = parsed?.tables?.workspaces;
|
|
88
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const entries = [];
|
|
92
|
+
for (const record of Object.values(table)) {
|
|
93
|
+
if (typeof record !== "object" || record === null) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const entry = entryOf(record.path, record.title);
|
|
97
|
+
if (entry !== undefined) {
|
|
98
|
+
entries.push(entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return entries;
|
|
102
|
+
}
|
|
103
|
+
/** Records from the roots this process served, most recent first. */
|
|
104
|
+
function servedEntries(served) {
|
|
105
|
+
return served.list().map(({ root }) => {
|
|
106
|
+
const canonical = canonicalPath(root);
|
|
107
|
+
return { root: canonical, label: basename(canonical) };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** ZCode's memory-directory rule: a plain directory, never a symlink. */
|
|
111
|
+
function hasMemoryDirectory(root) {
|
|
112
|
+
try {
|
|
113
|
+
const stat = lstatSync(memoryDirFor(root));
|
|
114
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Deduplicate by canonical root, keeping the first (highest-precedence) entry. */
|
|
121
|
+
function dedupe(entries) {
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
const unique = [];
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (seen.has(entry.root)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
seen.add(entry.root);
|
|
129
|
+
unique.push(entry);
|
|
130
|
+
}
|
|
131
|
+
return unique;
|
|
132
|
+
}
|
|
133
|
+
/** Create the catalog over the given sources. */
|
|
134
|
+
export function createWorkspaceCatalog(options) {
|
|
135
|
+
const known = () => {
|
|
136
|
+
const live = registryEntries(options.registry?.());
|
|
137
|
+
if (live.length > 0) {
|
|
138
|
+
return dedupe([...live, ...servedEntries(options.served)]);
|
|
139
|
+
}
|
|
140
|
+
return dedupe([...fileEntries(options.homeDir), ...servedEntries(options.served)]);
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
known,
|
|
144
|
+
list() {
|
|
145
|
+
return known().filter((entry) => hasMemoryDirectory(entry.root));
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=workspace-catalog.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "ZCode-style workspace memory for DeepSeek Harness: a per-workspace markdown store with session-start index injection.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test": "vitest run",
|
|
61
61
|
"test:coverage": "vitest run --coverage",
|
|
62
62
|
"test:watch": "vitest",
|
|
63
|
-
"lint": "oxlint src test",
|
|
63
|
+
"lint": "oxlint src test client",
|
|
64
64
|
"clean": "rm -rf lib",
|
|
65
65
|
"check:docs": "tsx scripts/run-gates.ts docs",
|
|
66
66
|
"check:handoff": "tsx scripts/verify-handoff-structure.ts --self-test && tsx scripts/verify-handoff-structure.ts",
|