dsh-continual-evolve 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/README.zh.md +6 -5
- package/client/client.js +616 -96
- package/lib/card-routes.d.ts +1 -0
- package/lib/card-routes.js +74 -6
- package/lib/memory-snapshot.d.ts +28 -0
- package/lib/memory-snapshot.js +57 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@ 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
|
+
[]()
|
|
12
|
+
[]()
|
|
13
13
|
[]()
|
|
14
14
|
|
|
15
15
|
A workspace-memory plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`): one plain-markdown memory store per workspace (`<workspace>/.evolve/memory/`), the index injected at session start, used by the model with its **native file tools** — and nothing else.
|
|
@@ -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):
|
|
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 (directory-name chips), 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,7 +66,7 @@ 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 (80 tests)
|
|
70
70
|
pnpm test:coverage # v8 coverage, CI-enforced thresholds
|
|
71
71
|
pnpm lint # oxlint src test
|
|
72
72
|
```
|
|
@@ -83,6 +83,7 @@ Layout:
|
|
|
83
83
|
│ └── card-routes.ts # card: GET-only read API on the host webServer
|
|
84
84
|
├── client/
|
|
85
85
|
│ └── client.js # hand-written client bundle: read-only card in the plugin manager
|
|
86
|
+
├── locale/ # host package metadata: localized display name + description (zh/en)
|
|
86
87
|
├── test/ # vitest suite (7 files)
|
|
87
88
|
├── lib/ # build output (tsc)
|
|
88
89
|
├── docs/ # design.md (design) · FAQ.md (real-world pitfalls)
|
package/README.zh.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
|
+
[]()
|
|
12
|
+
[]()
|
|
13
13
|
[]()
|
|
14
14
|
|
|
15
15
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的工作区记忆插件:每个工作区一个纯 markdown 记忆库(`<workspace>/.evolve/memory/`),会话开局注入索引,模型用**原生文件读写工具**直接使用——仅此而已。
|
|
@@ -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
|
|
26
|
+
4. **只读卡片** —— 插件在官方插件管理的本 bundle 页面挂一张只读记忆卡片(`plugins.bundle.config` slot,老宿主自动降级为无卡片):形态对齐市场自家设置卡(默认收起、头部整行可点 + 旋转 chevron、hover/展开框态),内容对齐 ZCode 设置的记忆选项卡——工作区一次选一个(目录名 chips)、文件搜索框、每个文件的相对更新时间、点击行内展开文件内容预览(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,7 +66,7 @@ profile patch 示例:
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
69
|
-
pnpm test # vitest(
|
|
69
|
+
pnpm test # vitest(80 例)
|
|
70
70
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
71
71
|
pnpm lint # oxlint src test
|
|
72
72
|
```
|
|
@@ -83,6 +83,7 @@ pnpm lint # oxlint src test
|
|
|
83
83
|
│ └── card-routes.ts # 卡片:webServer 上的 GET-only 只读 API
|
|
84
84
|
├── client/
|
|
85
85
|
│ └── client.js # 手写 client bundle:官方插件管理内只读卡片
|
|
86
|
+
├── locale/ # 宿主包元数据:显示名 + 描述双语(zh/en)
|
|
86
87
|
├── test/ # vitest 测试(7 个文件)
|
|
87
88
|
├── lib/ # 构建产物(tsc)
|
|
88
89
|
├── docs/ # design.md(设计)· FAQ.md(踩坑)
|
package/client/client.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Hand-written (no bundler: docs/coding-standard.md forbids a second one) in
|
|
5
5
|
* the host loader's factory shape, verified against dshmarket's built
|
|
6
6
|
* bundle: `window.__ModuleLoader__.load({ id, factory })`, where the factory
|
|
7
|
-
* resolves host-provided modules through the passed `require` (here:
|
|
8
|
-
*
|
|
9
|
-
* package.json `dsh.client` + the `./client`
|
|
7
|
+
* resolves host-provided modules through the passed `require` (here: react
|
|
8
|
+
* plus, optionally, the UI primitives) and returns its `module.exports`.
|
|
9
|
+
* The host mounts the bundle via package.json `dsh.client` + the `./client`
|
|
10
|
+
* export.
|
|
10
11
|
*
|
|
11
12
|
* The card registers one slot, `plugins.bundle.config` — the seat the 0.1.7+
|
|
12
13
|
* plugin manager declares for a bundle's own page. Slot detection is the
|
|
@@ -16,9 +17,26 @@
|
|
|
16
17
|
* call with null (the bundle list row shows the package description; the
|
|
17
18
|
* card is the page).
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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): aligned with ZCode's Settings → Memory viewer, which the
|
|
29
|
+
* .evolve store shape was itself aligned with — one workspace selected at a
|
|
30
|
+
* time (chips; the known-workspace LRU is bounded at 8), a file search box,
|
|
31
|
+
* per-file relative updated times, and a click-to-preview body served by the
|
|
32
|
+
* host's content endpoint (5 MiB cap, deleted/changed guards). The drift
|
|
33
|
+
* warnings (dangling index rows / unindexed files) stay: they are this
|
|
34
|
+
* plugin's OBSERVATION surface and a deliberate superset of ZCode's viewer.
|
|
35
|
+
* Locale/theme/primitives wiring unchanged from v0.13: `ctx.locale.register`
|
|
36
|
+
* + `bind`, `var(--dsw-alias-*)` tokens, primitives with plain-element
|
|
37
|
+
* fallbacks so an old host never blanks the settings dialog. Read-only
|
|
38
|
+
* projection, fetched at render time from this plugin's host routes
|
|
39
|
+
* (same-origin). No editor, no write path, no cached state.
|
|
22
40
|
*/
|
|
23
41
|
window.__ModuleLoader__.load({
|
|
24
42
|
id: "dsh-continual-evolve",
|
|
@@ -44,6 +62,15 @@ window.__ModuleLoader__.load({
|
|
|
44
62
|
return module.exports;
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
var primitives = undefined;
|
|
66
|
+
try {
|
|
67
|
+
primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Old host without the primitives package: themed plain elements below.
|
|
70
|
+
void error;
|
|
71
|
+
primitives = undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
var createElement = react.createElement;
|
|
48
75
|
var useState = react.useState;
|
|
49
76
|
var useEffect = react.useEffect;
|
|
@@ -51,6 +78,104 @@ window.__ModuleLoader__.load({
|
|
|
51
78
|
var NS = "dsh-continual-evolve";
|
|
52
79
|
var SLOT = "plugins.bundle.config";
|
|
53
80
|
var API_ROOT = "/dsh-continual-evolve/api/v1";
|
|
81
|
+
var STYLE_ID = "dsh-continual-evolve-card-css";
|
|
82
|
+
|
|
83
|
+
var zh = {
|
|
84
|
+
title: "工作区记忆",
|
|
85
|
+
desc: "各工作区 .evolve/memory/ 的只读投影。",
|
|
86
|
+
refresh: "刷新",
|
|
87
|
+
hint: "事实源是各工作区 .evolve/memory/ 下的 markdown 文件——模型原生读写;本卡片只展示,编辑请直接改文件。",
|
|
88
|
+
loading: "读取中…",
|
|
89
|
+
loadFailed: "卡片数据加载失败:",
|
|
90
|
+
empty: "本进程还没有服务过任何工作区会话——在某个工作区开一次会话后回来刷新。",
|
|
91
|
+
absent: "该工作区尚无记忆库(开一次会话自动创建)。",
|
|
92
|
+
readFailed: "记忆库读取失败:",
|
|
93
|
+
stats: "{files} 条记忆 · 索引 {index} 行",
|
|
94
|
+
filesHeading: "文件",
|
|
95
|
+
searchPlaceholder: "搜索记忆文件…",
|
|
96
|
+
searchEmpty: "没有匹配的记忆文件。",
|
|
97
|
+
missing: "索引失联(索引引用但文件已删):",
|
|
98
|
+
unindexed: "未入索引(文件存在但索引没有行):",
|
|
99
|
+
readError: "读取失败:",
|
|
100
|
+
previewLoading: "正在加载文件…",
|
|
101
|
+
previewTooLarge: "该记忆文件超过 5 MiB 预览上限。",
|
|
102
|
+
previewDeleted: "该记忆文件已被删除,请刷新文件列表。",
|
|
103
|
+
previewFailed: "内容读取失败:",
|
|
104
|
+
justNow: "刚刚",
|
|
105
|
+
minutesAgo: "{count} 分钟前",
|
|
106
|
+
today: "今天 {time}",
|
|
107
|
+
yesterday: "昨天 {time}",
|
|
108
|
+
localeTag: "zh-CN",
|
|
109
|
+
};
|
|
110
|
+
var en = {
|
|
111
|
+
title: "Workspace Memory",
|
|
112
|
+
desc: "Read-only projection of each workspace's .evolve/memory/.",
|
|
113
|
+
refresh: "Refresh",
|
|
114
|
+
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
|
+
loading: "Loading…",
|
|
116
|
+
loadFailed: "Failed to load card data: ",
|
|
117
|
+
empty: "This process has not served any workspace session yet — open a session in a workspace, then come back and refresh.",
|
|
118
|
+
absent: "No memory store in this workspace yet (one is created when you open a session).",
|
|
119
|
+
readFailed: "Failed to read the memory store: ",
|
|
120
|
+
stats: "{files} memories · {index} index rows",
|
|
121
|
+
filesHeading: "Files",
|
|
122
|
+
searchPlaceholder: "Search memory files…",
|
|
123
|
+
searchEmpty: "No matching memory files.",
|
|
124
|
+
missing: "Dangling index (indexed but deleted): ",
|
|
125
|
+
unindexed: "Unindexed (on disk, missing from the index): ",
|
|
126
|
+
readError: "Read failed: ",
|
|
127
|
+
previewLoading: "Loading file…",
|
|
128
|
+
previewTooLarge: "This memory file exceeds the 5 MiB preview limit.",
|
|
129
|
+
previewDeleted: "This memory file has been deleted — refresh the file list.",
|
|
130
|
+
previewFailed: "Failed to read the content: ",
|
|
131
|
+
justNow: "Just now",
|
|
132
|
+
minutesAgo: "{count} min ago",
|
|
133
|
+
today: "Today {time}",
|
|
134
|
+
yesterday: "Yesterday {time}",
|
|
135
|
+
localeTag: "en-US",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function format(template, params) {
|
|
139
|
+
return String(template).replace(/\{(\w+)\}/g, function (match, key) {
|
|
140
|
+
var value = params[key];
|
|
141
|
+
return value === undefined || value === null ? match : String(value);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Relative updated time, ZCode viewer style: just now / N minutes ago /
|
|
147
|
+
* today / yesterday, then a calendar rendering via Intl in the active
|
|
148
|
+
* language (the `localeTag` dictionary key carries that language out).
|
|
149
|
+
*/
|
|
150
|
+
function formatRelative(updatedAt, now, t) {
|
|
151
|
+
if (typeof updatedAt !== "number" || !isFinite(updatedAt) || updatedAt <= 0) {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
var minute = 60000;
|
|
155
|
+
var diff = now - updatedAt;
|
|
156
|
+
if (diff < minute) {
|
|
157
|
+
return t("justNow");
|
|
158
|
+
}
|
|
159
|
+
if (diff < 60 * minute) {
|
|
160
|
+
return t("minutesAgo", { count: Math.floor(diff / minute) });
|
|
161
|
+
}
|
|
162
|
+
var date = new Date(updatedAt);
|
|
163
|
+
var time = new Intl.DateTimeFormat(t("localeTag"), { hour: "2-digit", minute: "2-digit" }).format(date);
|
|
164
|
+
var startOfDay = new Date(now);
|
|
165
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
166
|
+
if (updatedAt >= startOfDay.getTime()) {
|
|
167
|
+
return t("today", { time: time });
|
|
168
|
+
}
|
|
169
|
+
if (updatedAt >= startOfDay.getTime() - 24 * 60 * 60 * 1000) {
|
|
170
|
+
return t("yesterday", { time: time });
|
|
171
|
+
}
|
|
172
|
+
var sameYear = date.getFullYear() === new Date(now).getFullYear();
|
|
173
|
+
return new Intl.DateTimeFormat(t("localeTag"), {
|
|
174
|
+
month: "numeric",
|
|
175
|
+
day: "numeric",
|
|
176
|
+
year: sameYear ? undefined : "numeric",
|
|
177
|
+
}).format(date);
|
|
178
|
+
}
|
|
54
179
|
|
|
55
180
|
/**
|
|
56
181
|
* Assemble the card's view model: the workspace allowlist, then one
|
|
@@ -64,7 +189,7 @@ window.__ModuleLoader__.load({
|
|
|
64
189
|
async function loadCardModel(fetchImpl) {
|
|
65
190
|
var listResponse = await fetchImpl(API_ROOT + "/workspaces");
|
|
66
191
|
if (!listResponse.ok) {
|
|
67
|
-
throw new Error("
|
|
192
|
+
throw new Error("HTTP " + listResponse.status);
|
|
68
193
|
}
|
|
69
194
|
var listBody = await listResponse.json();
|
|
70
195
|
var workspaces = Array.isArray(listBody.workspaces) ? listBody.workspaces : [];
|
|
@@ -91,74 +216,355 @@ window.__ModuleLoader__.load({
|
|
|
91
216
|
return { workspaces: entries };
|
|
92
217
|
}
|
|
93
218
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Fetch one memory file's body for the preview. Outcomes mirror the
|
|
221
|
+
* endpoint's status mapping so the card can show the ZCode viewer's
|
|
222
|
+
* deleted / too-large / failed states instead of a raw error.
|
|
223
|
+
*
|
|
224
|
+
* @param {typeof fetch} fetchImpl - injectable for tests.
|
|
225
|
+
* @param {string} root - known workspace root.
|
|
226
|
+
* @param {string} file - plain .md file name inside that memory store.
|
|
227
|
+
* @returns {Promise<{status: "ready"|"deleted"|"tooLarge"|"failed", content: string, message: string}>}
|
|
228
|
+
*/
|
|
229
|
+
async function loadFileContent(fetchImpl, root, file) {
|
|
230
|
+
var url = API_ROOT + "/memory/file?root=" + encodeURIComponent(root) + "&file=" + encodeURIComponent(file);
|
|
231
|
+
try {
|
|
232
|
+
var response = await fetchImpl(url);
|
|
233
|
+
if (response.ok) {
|
|
234
|
+
var body = await response.json();
|
|
235
|
+
return {
|
|
236
|
+
status: "ready",
|
|
237
|
+
content: typeof body.content === "string" ? body.content : "",
|
|
238
|
+
message: "",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (response.status === 404) {
|
|
242
|
+
return { status: "deleted", content: "", message: "" };
|
|
243
|
+
}
|
|
244
|
+
if (response.status === 413) {
|
|
245
|
+
return { status: "tooLarge", content: "", message: "" };
|
|
246
|
+
}
|
|
247
|
+
return { status: "failed", content: "", message: "HTTP " + response.status };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
status: "failed",
|
|
251
|
+
content: "",
|
|
252
|
+
message: error instanceof Error ? error.message : String(error),
|
|
253
|
+
};
|
|
104
254
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Display label for a workspace: its directory name (ZCode shows a project name, not a path). */
|
|
258
|
+
function workspaceLabel(root) {
|
|
259
|
+
var normalized = String(root).replace(/[\\/]+$/, "");
|
|
260
|
+
var index = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\"));
|
|
261
|
+
return index >= 0 ? normalized.slice(index + 1) : normalized;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Inject the card stylesheet once, the way the host's own CSS-module
|
|
266
|
+
* tags work. The classes are prefixed `dce-` and every value falls back
|
|
267
|
+
* to a fixed color after the token, so an old theme never blanks text.
|
|
268
|
+
*/
|
|
269
|
+
function injectCardStyle() {
|
|
270
|
+
if (typeof document === "undefined") {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (document.getElementById(STYLE_ID) !== null) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
var style = document.createElement("style");
|
|
277
|
+
style.id = STYLE_ID;
|
|
278
|
+
style.dataset.plugin = NS;
|
|
279
|
+
style.textContent = [
|
|
280
|
+
".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}",
|
|
281
|
+
".dce-card:hover{border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}",
|
|
282
|
+
'.dce-card[data-open="true"]{background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-color:var(--dsw-alias-label-dimmed,#c8ccd4)}',
|
|
283
|
+
".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}",
|
|
284
|
+
".dce-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}",
|
|
285
|
+
".dce-head-text{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
|
|
286
|
+
".dce-name{color:var(--dsw-alias-label-primary,#1f2328);font-size:15px;font-weight:600;line-height:1.4}",
|
|
287
|
+
".dce-desc{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:13px;line-height:1.5}",
|
|
288
|
+
".dce-chevron{color:var(--dsw-alias-label-tertiary,#8b93a1);flex:none;transition:transform .16s;display:inline-flex}",
|
|
289
|
+
'.dce-chevron[data-open="true"]{transform:rotate(180deg)}',
|
|
290
|
+
".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
|
+
".dce-hint{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}",
|
|
292
|
+
".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-chips{display:flex;flex-wrap:wrap;gap:6px}",
|
|
294
|
+
".dce-chip{font:inherit;cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:var(--dsw-radius-md,8px);color:var(--dsw-alias-label-secondary,#6b7280);padding:3px 10px;font-size:12px;line-height:18px}",
|
|
295
|
+
'.dce-chip[data-active="true"]{background:var(--dsw-alias-bg-module-platform,#eef1f4);border-color:var(--dsw-alias-border-l3,#d1d9e0);color:var(--dsw-alias-label-primary,#1f2328)}',
|
|
296
|
+
".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
|
+
".dce-search::placeholder{color:var(--dsw-alias-label-tertiary,#8b93a1)}",
|
|
298
|
+
".dce-search:focus-visible{border-color:var(--dsw-alias-brand-primary,#4f6ef7)}",
|
|
299
|
+
".dce-files-head{display:flex;align-items:center;gap:10px;min-height:28px}",
|
|
300
|
+
".dce-files-title{color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;font-weight:600;line-height:18px}",
|
|
301
|
+
".dce-files-count{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:12px;line-height:18px}",
|
|
302
|
+
".dce-spacer{flex:1}",
|
|
303
|
+
".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)}",
|
|
304
|
+
".dce-item+.dce-item{border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb)}",
|
|
305
|
+
".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
|
+
".dce-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.03))}",
|
|
307
|
+
".dce-row:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:-2px}",
|
|
308
|
+
".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
|
+
".dce-file-time{flex:none;color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px}",
|
|
310
|
+
".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)}",
|
|
311
|
+
".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}",
|
|
312
|
+
".dce-workspace{display:flex;flex-direction:column;gap:10px}",
|
|
313
|
+
".dce-stats{color:var(--dsw-alias-label-secondary,#6b7280);font-size:12px;line-height:18px}",
|
|
314
|
+
".dce-warn{color:var(--dsw-alias-state-warn-primary,#b45309);font-size:12px;line-height:18px}",
|
|
315
|
+
".dce-error{color:var(--dsw-alias-state-error-primary,#dc2626);font-size:12px;line-height:18px}",
|
|
316
|
+
".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}",
|
|
317
|
+
].join("\n");
|
|
318
|
+
document.head.appendChild(style);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Chevron from the host primitives, or a plain glyph when they are absent. */
|
|
322
|
+
function Chevron() {
|
|
323
|
+
if (
|
|
324
|
+
primitives !== undefined &&
|
|
325
|
+
primitives !== null &&
|
|
326
|
+
typeof primitives.IconChevronDownOutlineRegular !== "undefined" &&
|
|
327
|
+
primitives.IconChevronDownOutlineRegular !== null
|
|
328
|
+
) {
|
|
329
|
+
return createElement(primitives.IconChevronDownOutlineRegular, { size: 14, "aria-hidden": "true" });
|
|
330
|
+
}
|
|
331
|
+
return createElement("span", { "aria-hidden": "true" }, "▾");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function RefreshButton(props) {
|
|
335
|
+
var UI = props.UI;
|
|
336
|
+
var t = props.t;
|
|
337
|
+
var onClick = props.onClick;
|
|
338
|
+
if (UI !== null && UI !== undefined && typeof UI.Button !== "undefined" && UI.Button !== null) {
|
|
339
|
+
return createElement(UI.Button, { variant: "outline", size: "sm", onClick: onClick }, t("refresh"));
|
|
108
340
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
341
|
+
return createElement(
|
|
342
|
+
"button",
|
|
343
|
+
{ type: "button", className: "dce-fallback-btn", onClick: onClick },
|
|
344
|
+
t("refresh"),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** One memory file row: name + relative updated time, click toggles the body preview. */
|
|
349
|
+
function FileRow(props) {
|
|
350
|
+
var file = props.file;
|
|
351
|
+
var now = props.now;
|
|
352
|
+
var t = props.t;
|
|
353
|
+
var expanded = props.expanded;
|
|
354
|
+
var preview = props.preview;
|
|
355
|
+
var onToggle = props.onToggle;
|
|
356
|
+
var rowChildren = [
|
|
357
|
+
createElement("span", { key: "name", className: "dce-file-name" }, file.file),
|
|
358
|
+
];
|
|
359
|
+
var time = formatRelative(file.updatedAt, now, t);
|
|
360
|
+
if (time !== "") {
|
|
361
|
+
rowChildren.push(createElement("span", { key: "time", className: "dce-file-time" }, time));
|
|
112
362
|
}
|
|
113
|
-
children
|
|
363
|
+
var children = [
|
|
114
364
|
createElement(
|
|
115
|
-
"
|
|
116
|
-
{
|
|
117
|
-
|
|
365
|
+
"button",
|
|
366
|
+
{
|
|
367
|
+
key: "row",
|
|
368
|
+
type: "button",
|
|
369
|
+
className: "dce-row",
|
|
370
|
+
"aria-expanded": expanded ? "true" : "false",
|
|
371
|
+
onClick: onToggle,
|
|
372
|
+
},
|
|
373
|
+
rowChildren,
|
|
118
374
|
),
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
375
|
+
];
|
|
376
|
+
if (expanded) {
|
|
377
|
+
var body = null;
|
|
378
|
+
if (preview === null || preview.file !== file.file) {
|
|
379
|
+
body = createElement("div", { className: "dce-hint" }, t("previewLoading"));
|
|
380
|
+
} else if (preview.status === "loading") {
|
|
381
|
+
body = createElement("div", { className: "dce-hint" }, t("previewLoading"));
|
|
382
|
+
} else if (preview.status === "ready") {
|
|
383
|
+
body = createElement("pre", { className: "dce-pre" }, preview.content);
|
|
384
|
+
} else if (preview.status === "deleted") {
|
|
385
|
+
body = createElement("div", { className: "dce-hint" }, t("previewDeleted"));
|
|
386
|
+
} else if (preview.status === "tooLarge") {
|
|
387
|
+
body = createElement("div", { className: "dce-hint" }, t("previewTooLarge"));
|
|
388
|
+
} else {
|
|
389
|
+
body = createElement("div", { className: "dce-error" }, t("previewFailed") + preview.message);
|
|
390
|
+
}
|
|
391
|
+
children.push(createElement("div", { key: "preview", className: "dce-preview" }, body));
|
|
392
|
+
}
|
|
393
|
+
return createElement("li", { className: "dce-item" }, children);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** One workspace's memory store: stats, drift warnings, searchable file list with previews. */
|
|
397
|
+
function WorkspaceBlock(props) {
|
|
398
|
+
var entry = props.entry;
|
|
399
|
+
var selected = props.selected;
|
|
400
|
+
var onSelect = props.onSelect;
|
|
401
|
+
var t = props.t;
|
|
402
|
+
var UI = props.UI;
|
|
403
|
+
var now = props.now;
|
|
404
|
+
var expandedFile = props.expandedFile;
|
|
405
|
+
var preview = props.preview;
|
|
406
|
+
var onToggleFile = props.onToggleFile;
|
|
407
|
+
var onRefresh = props.onRefresh;
|
|
408
|
+
var query = props.query;
|
|
409
|
+
var onQueryChange = props.onQueryChange;
|
|
410
|
+
var chips = [];
|
|
411
|
+
if (entry.workspaces.length > 1) {
|
|
412
|
+
chips = createElement(
|
|
413
|
+
"div",
|
|
414
|
+
{ key: "chips", className: "dce-chips", role: "tablist", "aria-label": t("filesHeading") },
|
|
415
|
+
entry.workspaces.map(function (workspace) {
|
|
416
|
+
return createElement(
|
|
417
|
+
"button",
|
|
418
|
+
{
|
|
419
|
+
key: workspace.root,
|
|
420
|
+
type: "button",
|
|
421
|
+
className: "dce-chip",
|
|
422
|
+
"data-active": workspace.root === selected ? "true" : "false",
|
|
423
|
+
onClick: function () {
|
|
424
|
+
onSelect(workspace.root);
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
workspaceLabel(workspace.root),
|
|
428
|
+
);
|
|
429
|
+
}),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
var active = entry.workspaces.find(function (workspace) {
|
|
433
|
+
return workspace.root === selected;
|
|
434
|
+
});
|
|
435
|
+
if (active === undefined) {
|
|
436
|
+
return createElement("div", { key: "block" }, chips);
|
|
437
|
+
}
|
|
438
|
+
var body = null;
|
|
439
|
+
if (active.error !== null && active.error !== undefined) {
|
|
440
|
+
body = [
|
|
441
|
+
createElement("div", { key: "root", className: "dce-stats" }, active.root),
|
|
442
|
+
createElement("div", { key: "err", className: "dce-error" }, t("readError") + active.error),
|
|
443
|
+
];
|
|
444
|
+
} else if (active.snapshot === null || active.snapshot === undefined || !active.snapshot.exists) {
|
|
445
|
+
body = createElement("div", { className: "dce-empty" }, t("absent"));
|
|
446
|
+
} else if (active.snapshot.readError !== null && active.snapshot.readError !== undefined) {
|
|
447
|
+
body = [
|
|
448
|
+
createElement("div", { key: "root", className: "dce-stats" }, active.root),
|
|
449
|
+
createElement("div", { key: "err", className: "dce-error" }, t("readFailed") + active.snapshot.readError),
|
|
450
|
+
];
|
|
451
|
+
} else {
|
|
452
|
+
var snapshot = active.snapshot;
|
|
453
|
+
var normalizedQuery = String(query).trim().toLocaleLowerCase();
|
|
454
|
+
var files = snapshot.files.filter(function (file) {
|
|
455
|
+
if (normalizedQuery.length === 0) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
return [file.file, file.name, file.description, file.type]
|
|
459
|
+
.filter(function (value) {
|
|
460
|
+
return typeof value === "string";
|
|
461
|
+
})
|
|
462
|
+
.some(function (value) {
|
|
463
|
+
return value.toLocaleLowerCase().includes(normalizedQuery);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
var parts = [
|
|
122
467
|
createElement(
|
|
123
468
|
"div",
|
|
124
|
-
{ key: "
|
|
125
|
-
"
|
|
469
|
+
{ key: "stats", className: "dce-stats" },
|
|
470
|
+
t("stats", { files: snapshot.fileCount, index: snapshot.indexEntryCount }),
|
|
126
471
|
),
|
|
472
|
+
];
|
|
473
|
+
if (snapshot.missingFiles.length > 0) {
|
|
474
|
+
parts.push(
|
|
475
|
+
createElement("div", { key: "missing", className: "dce-warn" }, t("missing") + snapshot.missingFiles.join("、")),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (snapshot.unindexedFiles.length > 0) {
|
|
479
|
+
parts.push(
|
|
480
|
+
createElement(
|
|
481
|
+
"div",
|
|
482
|
+
{ key: "unindexed", className: "dce-warn" },
|
|
483
|
+
t("unindexed") + snapshot.unindexedFiles.join("、"),
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
parts.push(
|
|
488
|
+
createElement("input", {
|
|
489
|
+
key: "search",
|
|
490
|
+
type: "search",
|
|
491
|
+
className: "dce-search",
|
|
492
|
+
value: query,
|
|
493
|
+
placeholder: t("searchPlaceholder"),
|
|
494
|
+
"aria-label": t("searchPlaceholder"),
|
|
495
|
+
onChange: function (event) {
|
|
496
|
+
onQueryChange(event.currentTarget.value);
|
|
497
|
+
},
|
|
498
|
+
}),
|
|
127
499
|
);
|
|
128
|
-
|
|
129
|
-
if (snapshot.unindexedFiles.length > 0) {
|
|
130
|
-
children.push(
|
|
500
|
+
parts.push(
|
|
131
501
|
createElement(
|
|
132
502
|
"div",
|
|
133
|
-
{ key: "
|
|
134
|
-
"
|
|
503
|
+
{ key: "head", className: "dce-files-head" },
|
|
504
|
+
createElement("span", { className: "dce-files-title" }, t("filesHeading")),
|
|
505
|
+
createElement("span", { className: "dce-files-count" }, String(files.length)),
|
|
506
|
+
createElement("span", { className: "dce-spacer" }),
|
|
507
|
+
createElement(RefreshButton, {
|
|
508
|
+
UI: UI,
|
|
509
|
+
t: t,
|
|
510
|
+
onClick: onRefresh,
|
|
511
|
+
}),
|
|
135
512
|
),
|
|
136
513
|
);
|
|
514
|
+
if (files.length === 0) {
|
|
515
|
+
parts.push(createElement("div", { key: "none", className: "dce-empty" }, t("searchEmpty")));
|
|
516
|
+
} else {
|
|
517
|
+
parts.push(
|
|
518
|
+
createElement(
|
|
519
|
+
"ul",
|
|
520
|
+
{ key: "files", className: "dce-list" },
|
|
521
|
+
files.map(function (file) {
|
|
522
|
+
return createElement(FileRow, {
|
|
523
|
+
key: file.file,
|
|
524
|
+
file: file,
|
|
525
|
+
now: now,
|
|
526
|
+
t: t,
|
|
527
|
+
expanded: expandedFile === file.file,
|
|
528
|
+
preview: preview,
|
|
529
|
+
onToggle: function () {
|
|
530
|
+
onToggleFile(file.file);
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
}),
|
|
534
|
+
),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
body = parts;
|
|
137
538
|
}
|
|
138
|
-
|
|
139
|
-
createElement(
|
|
140
|
-
"ul",
|
|
141
|
-
{ key: "files", style: { margin: "4px 0 0", paddingLeft: "18px" } },
|
|
142
|
-
snapshot.files.map(function (file) {
|
|
143
|
-
return createElement(
|
|
144
|
-
"li",
|
|
145
|
-
{ key: file.file },
|
|
146
|
-
createElement("strong", null, file.file) +
|
|
147
|
-
(file.description !== "" ? " — " + file.description : ""),
|
|
148
|
-
);
|
|
149
|
-
}),
|
|
150
|
-
),
|
|
151
|
-
);
|
|
152
|
-
return createElement("div", { style: { marginBottom: "12px" } }, children);
|
|
539
|
+
return createElement("div", { key: "block", className: "dce-workspace" }, chips, body);
|
|
153
540
|
}
|
|
154
541
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
542
|
+
/**
|
|
543
|
+
* The card: a collapsed-by-default framed disclosure (dshmarket
|
|
544
|
+
* SettingsCard shape) whose open body is the ZCode-memory-viewer-shaped
|
|
545
|
+
* read-only projection.
|
|
546
|
+
*/
|
|
547
|
+
function MemoryCard(props) {
|
|
548
|
+
var t = props.t;
|
|
549
|
+
var UI = props.UI;
|
|
550
|
+
injectCardStyle();
|
|
551
|
+
var openState = useState(false);
|
|
552
|
+
var open = openState[0];
|
|
553
|
+
var setOpen = openState[1];
|
|
157
554
|
var state = useState({ status: "loading", model: null, message: "" });
|
|
158
555
|
var model = state[0];
|
|
159
556
|
var setModel = state[1];
|
|
160
557
|
var reloadState = useState(0);
|
|
161
558
|
var reload = reloadState[1];
|
|
559
|
+
var selectionState = useState({ root: null, query: "", expandedFile: null });
|
|
560
|
+
var selection = selectionState[0];
|
|
561
|
+
var setSelection = selectionState[1];
|
|
562
|
+
var previewState = useState({ file: null, status: "loading", content: "", message: "" });
|
|
563
|
+
var preview = previewState[0];
|
|
564
|
+
var setPreview = previewState[1];
|
|
565
|
+
var nowState = useState(Date.now());
|
|
566
|
+
var now = nowState[0];
|
|
567
|
+
var setNow = nowState[1];
|
|
162
568
|
useEffect(
|
|
163
569
|
function () {
|
|
164
570
|
var alive = true;
|
|
@@ -178,72 +584,186 @@ window.__ModuleLoader__.load({
|
|
|
178
584
|
},
|
|
179
585
|
[reloadState[0]],
|
|
180
586
|
);
|
|
181
|
-
|
|
587
|
+
useEffect(
|
|
588
|
+
function () {
|
|
589
|
+
if (open) setNow(Date.now());
|
|
590
|
+
},
|
|
591
|
+
[open],
|
|
592
|
+
);
|
|
593
|
+
var refresh = function () {
|
|
594
|
+
setModel({ status: "loading", model: null, message: "" });
|
|
595
|
+
setSelection(function (current) {
|
|
596
|
+
return { root: current.root, query: "", expandedFile: null };
|
|
597
|
+
});
|
|
598
|
+
setPreview({ file: null, status: "loading", content: "", message: "" });
|
|
599
|
+
reload(function (n) {
|
|
600
|
+
return n + 1;
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
var header = createElement(
|
|
604
|
+
"button",
|
|
605
|
+
{
|
|
606
|
+
type: "button",
|
|
607
|
+
className: "dce-head",
|
|
608
|
+
"aria-expanded": open ? "true" : "false",
|
|
609
|
+
onClick: function () {
|
|
610
|
+
setOpen(!open);
|
|
611
|
+
},
|
|
612
|
+
},
|
|
182
613
|
createElement(
|
|
183
614
|
"div",
|
|
184
|
-
{
|
|
185
|
-
createElement("
|
|
186
|
-
createElement(
|
|
187
|
-
"button",
|
|
188
|
-
{
|
|
189
|
-
type: "button",
|
|
190
|
-
onClick: function () {
|
|
191
|
-
setModel({ status: "loading", model: null, message: "" });
|
|
192
|
-
reload(function (n) {
|
|
193
|
-
return n + 1;
|
|
194
|
-
});
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
"刷新",
|
|
198
|
-
),
|
|
615
|
+
{ className: "dce-head-text" },
|
|
616
|
+
createElement("div", { className: "dce-name" }, t("title")),
|
|
617
|
+
createElement("div", { className: "dce-desc" }, t("desc")),
|
|
199
618
|
),
|
|
200
619
|
createElement(
|
|
201
|
-
"
|
|
202
|
-
{
|
|
203
|
-
|
|
620
|
+
"span",
|
|
621
|
+
{ className: "dce-chevron", "data-open": open ? "true" : "false" },
|
|
622
|
+
createElement(Chevron, null),
|
|
204
623
|
),
|
|
205
|
-
|
|
624
|
+
);
|
|
625
|
+
var body = null;
|
|
626
|
+
if (!open) {
|
|
627
|
+
return createElement("div", { className: "dce-card", "data-open": "false" }, header);
|
|
628
|
+
}
|
|
629
|
+
var inner = null;
|
|
206
630
|
if (model.status === "loading") {
|
|
207
|
-
|
|
631
|
+
inner = createElement("div", { key: "inner", className: "dce-hint" }, t("loading"));
|
|
208
632
|
} else if (model.status === "error") {
|
|
209
|
-
|
|
633
|
+
inner = createElement("div", { key: "inner", className: "dce-error" }, t("loadFailed") + model.message);
|
|
210
634
|
} else if (model.model.workspaces.length === 0) {
|
|
211
|
-
|
|
212
|
-
createElement(
|
|
213
|
-
"div",
|
|
214
|
-
{ key: "body" },
|
|
215
|
-
"本进程还没有服务过任何工作区会话——在某个工作区开一次会话后回来刷新。",
|
|
216
|
-
),
|
|
217
|
-
);
|
|
635
|
+
inner = createElement("div", { key: "inner", className: "dce-empty" }, t("empty"));
|
|
218
636
|
} else {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
637
|
+
var selected = selection.root;
|
|
638
|
+
if (
|
|
639
|
+
selected === null ||
|
|
640
|
+
!model.model.workspaces.some(function (workspace) {
|
|
641
|
+
return workspace.root === selected;
|
|
642
|
+
})
|
|
643
|
+
) {
|
|
644
|
+
selected = model.model.workspaces[0].root;
|
|
645
|
+
}
|
|
646
|
+
inner = createElement(WorkspaceBlock, {
|
|
647
|
+
key: "inner",
|
|
648
|
+
entry: model.model,
|
|
649
|
+
selected: selected,
|
|
650
|
+
onSelect: function (root) {
|
|
651
|
+
setSelection({ root: root, query: "", expandedFile: null });
|
|
652
|
+
setPreview({ file: null, status: "loading", content: "", message: "" });
|
|
653
|
+
},
|
|
654
|
+
t: t,
|
|
655
|
+
UI: UI,
|
|
656
|
+
now: now,
|
|
657
|
+
expandedFile: selection.expandedFile,
|
|
658
|
+
preview: preview,
|
|
659
|
+
onToggleFile: function (file) {
|
|
660
|
+
if (selection.expandedFile === file) {
|
|
661
|
+
setSelection(function (current) {
|
|
662
|
+
return { root: current.root, query: current.query, expandedFile: null };
|
|
663
|
+
});
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
setSelection(function (current) {
|
|
667
|
+
return { root: current.root, query: current.query, expandedFile: file };
|
|
668
|
+
});
|
|
669
|
+
setPreview({ file: file, status: "loading", content: "", message: "" });
|
|
670
|
+
loadFileContent(fetch, selected, file).then(function (result) {
|
|
671
|
+
setPreview(function (current) {
|
|
672
|
+
if (current.file !== file) {
|
|
673
|
+
return current;
|
|
674
|
+
}
|
|
675
|
+
return { file: file, status: result.status, content: result.content, message: result.message };
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
},
|
|
679
|
+
query: selection.query,
|
|
680
|
+
onQueryChange: function (value) {
|
|
681
|
+
setSelection(function (current) {
|
|
682
|
+
return { root: current.root, query: value, expandedFile: current.expandedFile };
|
|
683
|
+
});
|
|
684
|
+
},
|
|
685
|
+
});
|
|
228
686
|
}
|
|
229
|
-
|
|
687
|
+
body = createElement(
|
|
688
|
+
"div",
|
|
689
|
+
{ key: "body", className: "dce-body" },
|
|
690
|
+
createElement("div", { className: "dce-hint" }, t("hint")),
|
|
691
|
+
inner,
|
|
692
|
+
);
|
|
693
|
+
return createElement(
|
|
694
|
+
"div",
|
|
695
|
+
{ className: "dce-card", "data-open": "true" },
|
|
696
|
+
header,
|
|
697
|
+
body,
|
|
698
|
+
);
|
|
230
699
|
}
|
|
231
700
|
|
|
232
701
|
exports.name = NS;
|
|
233
|
-
exports.inject = ["slots"];
|
|
702
|
+
exports.inject = ["slots", "locale", "theme"];
|
|
234
703
|
exports.loadCardModel = loadCardModel;
|
|
704
|
+
exports.loadFileContent = loadFileContent;
|
|
235
705
|
exports.apply = function apply(ctx) {
|
|
706
|
+
var tRaw = null;
|
|
707
|
+
try {
|
|
708
|
+
if (ctx.locale !== undefined && ctx.locale !== null) {
|
|
709
|
+
var register = function () {
|
|
710
|
+
return ctx.locale.register(NS, { zh: zh, en: en });
|
|
711
|
+
};
|
|
712
|
+
if (typeof ctx.effect === "function") {
|
|
713
|
+
ctx.effect(register, "dsh-continual-evolve: dictionaries");
|
|
714
|
+
} else if (typeof ctx.locale.register === "function") {
|
|
715
|
+
register();
|
|
716
|
+
}
|
|
717
|
+
if (typeof ctx.locale.bind === "function") {
|
|
718
|
+
tRaw = ctx.locale.bind(NS);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} catch (error) {
|
|
722
|
+
void error;
|
|
723
|
+
tRaw = null;
|
|
724
|
+
}
|
|
725
|
+
var fallbackT = function (key, params) {
|
|
726
|
+
var template = Object.prototype.hasOwnProperty.call(zh, key) ? zh[key] : key;
|
|
727
|
+
if (params !== undefined) {
|
|
728
|
+
return format(template, params);
|
|
729
|
+
}
|
|
730
|
+
return template;
|
|
731
|
+
};
|
|
732
|
+
var t = fallbackT;
|
|
733
|
+
if (typeof tRaw === "function") {
|
|
734
|
+
t = function (key, params) {
|
|
735
|
+
var rendered = null;
|
|
736
|
+
try {
|
|
737
|
+
rendered = tRaw(key);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
void error;
|
|
740
|
+
rendered = null;
|
|
741
|
+
}
|
|
742
|
+
if (typeof rendered !== "string") {
|
|
743
|
+
return fallbackT(key, params);
|
|
744
|
+
}
|
|
745
|
+
if (params !== undefined) {
|
|
746
|
+
return format(rendered, params);
|
|
747
|
+
}
|
|
748
|
+
return rendered;
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
var UI = null;
|
|
752
|
+
if (primitives !== undefined && primitives !== null && typeof primitives.Button !== "undefined") {
|
|
753
|
+
UI = { Button: primitives.Button };
|
|
754
|
+
}
|
|
236
755
|
ctx.slots.inject(SLOT, function () {
|
|
237
756
|
return ctx.slots.register(
|
|
238
757
|
{
|
|
239
758
|
name: SLOT,
|
|
240
759
|
key: NS,
|
|
760
|
+
locale: NS,
|
|
241
761
|
inject: function () {
|
|
242
|
-
return {};
|
|
762
|
+
return { t: typeof tRaw === "function" ? tRaw : fallbackT };
|
|
243
763
|
},
|
|
244
764
|
},
|
|
245
765
|
function (ownerProps) {
|
|
246
|
-
return ownerProps && ownerProps.view === "summary" ? null : createElement(MemoryCard);
|
|
766
|
+
return ownerProps && ownerProps.view === "summary" ? null : createElement(MemoryCard, { t: t, UI: UI });
|
|
247
767
|
},
|
|
248
768
|
);
|
|
249
769
|
});
|
package/lib/card-routes.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { KnownWorkspaces } from "./known-workspaces.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";
|
|
5
|
+
export declare const CARD_API_MEMORY_FILE_PATH = "/dsh-continual-evolve/api/v1/memory/file";
|
|
5
6
|
/** Minimal request shape the handlers touch (Node IncomingMessage subset). */
|
|
6
7
|
export interface CardRequest {
|
|
7
8
|
method?: string;
|
package/lib/card-routes.js
CHANGED
|
@@ -3,17 +3,20 @@
|
|
|
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
|
-
*
|
|
7
|
-
* known root,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* three endpoints are GET-only projections: the workspace allowlist, the
|
|
7
|
+
* memory snapshot for a known root, and one memory file's body for the card's
|
|
8
|
+
* content preview. The allowlist IS the security fence — the memory endpoints
|
|
9
|
+
* resolve their `root` parameter only against roots the running process has
|
|
10
|
+
* actually served, and the file endpoint additionally accepts only plain .md
|
|
11
|
+
* entries of that workspace's memory directory, so the API cannot read
|
|
12
|
+
* arbitrary paths. Handlers are synchronous and never throw past the response.
|
|
11
13
|
*/
|
|
12
14
|
import { resolve } from "node:path";
|
|
13
|
-
import { memorySnapshot } from "./memory-snapshot.js";
|
|
15
|
+
import { memoryFileContent, memorySnapshot } from "./memory-snapshot.js";
|
|
14
16
|
/** URL prefix of the card API (kind-exact routes registered under it). */
|
|
15
17
|
export const CARD_API_WORKSPACES_PATH = "/dsh-continual-evolve/api/v1/workspaces";
|
|
16
18
|
export const CARD_API_MEMORY_PATH = "/dsh-continual-evolve/api/v1/memory";
|
|
19
|
+
export const CARD_API_MEMORY_FILE_PATH = "/dsh-continual-evolve/api/v1/memory/file";
|
|
17
20
|
function sendJson(response, status, body) {
|
|
18
21
|
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
19
22
|
response.end(JSON.stringify(body));
|
|
@@ -65,6 +68,56 @@ export function mountCardRoutes(webServer, workspaces) {
|
|
|
65
68
|
sendJson(response, 200, memorySnapshot(known.root));
|
|
66
69
|
},
|
|
67
70
|
}));
|
|
71
|
+
disposers.push(webServer.register({
|
|
72
|
+
kind: "exact",
|
|
73
|
+
path: CARD_API_MEMORY_FILE_PATH,
|
|
74
|
+
handler(request, response) {
|
|
75
|
+
if (!isGet(request)) {
|
|
76
|
+
response.writeHead(405, { allow: "GET" });
|
|
77
|
+
response.end();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const root = rootParamOf(request);
|
|
81
|
+
if (root === null) {
|
|
82
|
+
sendJson(response, 400, { error: "root query parameter is required and must be an absolute path" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const file = fileParamOf(request);
|
|
86
|
+
if (file === null) {
|
|
87
|
+
sendJson(response, 400, { error: "file query parameter is required" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const known = workspaces.list().find((workspace) => workspace.root === resolve(root));
|
|
91
|
+
if (known === undefined) {
|
|
92
|
+
// Same fence as the snapshot route: 404 without distinguishing
|
|
93
|
+
// "never served" from "wrong path".
|
|
94
|
+
sendJson(response, 404, { error: "unknown workspace root" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const content = memoryFileContent(known.root, file);
|
|
98
|
+
if (!content.ok) {
|
|
99
|
+
// The fence reasons stay indistinguishable from absence in status
|
|
100
|
+
// terms only where safe: "outside" answers 400 (a client bug),
|
|
101
|
+
// absence 404, oversized 413 — never a body leak either way.
|
|
102
|
+
if (content.reason === "outside") {
|
|
103
|
+
sendJson(response, 400, { error: "file must be a plain .md entry of the memory directory" });
|
|
104
|
+
}
|
|
105
|
+
else if (content.reason === "too-large") {
|
|
106
|
+
sendJson(response, 413, { error: "memory file exceeds the 5 MiB preview limit" });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
sendJson(response, 404, { error: "memory file not found" });
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
sendJson(response, 200, {
|
|
114
|
+
file: content.file,
|
|
115
|
+
content: content.content,
|
|
116
|
+
mtimeMs: content.mtimeMs,
|
|
117
|
+
changed: content.changed,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
68
121
|
return () => {
|
|
69
122
|
for (const disposer of disposers) {
|
|
70
123
|
if (typeof disposer === "function") {
|
|
@@ -89,4 +142,19 @@ function rootParamOf(request) {
|
|
|
89
142
|
return null;
|
|
90
143
|
}
|
|
91
144
|
}
|
|
145
|
+
/** The decoded `file` query parameter, or null when absent. */
|
|
146
|
+
function fileParamOf(request) {
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(request.url ?? "/", "http://dsh-card.invalid");
|
|
149
|
+
const file = url.searchParams.get("file");
|
|
150
|
+
if (file === null || file.trim().length === 0) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return file;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
void error;
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
92
160
|
//# sourceMappingURL=card-routes.js.map
|
package/lib/memory-snapshot.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/** The index file name (same store contract as the injection path). */
|
|
2
2
|
export declare const MEMORY_SNAPSHOT_INDEX_FILE = "MEMORY.md";
|
|
3
|
+
/**
|
|
4
|
+
* Preview size cap for the file content endpoint — files above it answer
|
|
5
|
+
* `too-large` instead of their body (ZCode's memory viewer uses the same 5 MiB).
|
|
6
|
+
*/
|
|
7
|
+
export declare const MEMORY_FILE_PREVIEW_LIMIT: number;
|
|
3
8
|
/** One memory file with its frontmatter fields ("" when absent). */
|
|
4
9
|
export interface MemoryFileInfo {
|
|
5
10
|
file: string;
|
|
6
11
|
name: string;
|
|
7
12
|
description: string;
|
|
8
13
|
type: string;
|
|
14
|
+
/** File modification time in epoch milliseconds (the card's "updated" line). */
|
|
15
|
+
updatedAt: number;
|
|
9
16
|
}
|
|
10
17
|
/** One parsed index row: `- [title](file.md) — hook`. */
|
|
11
18
|
export interface MemoryIndexRow {
|
|
@@ -51,4 +58,25 @@ export declare function parseMemoryFrontmatter(text: string): MemoryFrontmatter;
|
|
|
51
58
|
* card must observe, never break.
|
|
52
59
|
*/
|
|
53
60
|
export declare function memorySnapshot(root: string): MemorySnapshot;
|
|
61
|
+
/** The content endpoint's outcomes: the body, or why there is none. */
|
|
62
|
+
export type MemoryFileContent = {
|
|
63
|
+
ok: true;
|
|
64
|
+
file: string;
|
|
65
|
+
content: string;
|
|
66
|
+
mtimeMs: number;
|
|
67
|
+
changed: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: "outside" | "absent" | "too-large";
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Read one memory file's body for the card's content preview — the viewer
|
|
74
|
+
* side of the same read-only fence as `memorySnapshot`. The requested file
|
|
75
|
+
* name must be a plain entry of the workspace's memory directory (no path
|
|
76
|
+
* separators, no traversal, resolved location stays inside the directory);
|
|
77
|
+
* anything else answers `outside` without touching the filesystem, so the
|
|
78
|
+
* endpoint cannot become a path oracle. Oversized files answer `too-large`
|
|
79
|
+
* with the body unread.
|
|
80
|
+
*/
|
|
81
|
+
export declare function memoryFileContent(root: string, file: string): MemoryFileContent;
|
|
54
82
|
//# sourceMappingURL=memory-snapshot.d.ts.map
|
package/lib/memory-snapshot.js
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* card is a viewer, never a second store: no caching, no writes, no bootstrap
|
|
5
5
|
* (unlike the injection path, a missing store is reported, not created).
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import { join, resolve } from "node:path";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
9
9
|
import { memoryDirFor } from "./memory-section.js";
|
|
10
10
|
/** The index file name (same store contract as the injection path). */
|
|
11
11
|
export const MEMORY_SNAPSHOT_INDEX_FILE = "MEMORY.md";
|
|
12
|
+
/**
|
|
13
|
+
* Preview size cap for the file content endpoint — files above it answer
|
|
14
|
+
* `too-large` instead of their body (ZCode's memory viewer uses the same 5 MiB).
|
|
15
|
+
*/
|
|
16
|
+
export const MEMORY_FILE_PREVIEW_LIMIT = 5 * 1024 * 1024;
|
|
12
17
|
/**
|
|
13
18
|
* Parse `- [title](file.md) …` rows out of an index text. Anything else
|
|
14
19
|
* (headings, the bootstrap hint comment, blank lines) is ignored.
|
|
@@ -67,7 +72,8 @@ function readStoreState(memoryDir) {
|
|
|
67
72
|
const diskFiles = listStoreFiles(memoryDir);
|
|
68
73
|
const files = diskFiles.map((file) => {
|
|
69
74
|
const frontmatter = parseMemoryFrontmatter(readFileSync(join(memoryDir, file), "utf8"));
|
|
70
|
-
|
|
75
|
+
const updatedAt = statSync(join(memoryDir, file)).mtimeMs;
|
|
76
|
+
return { file, name: frontmatter.name, description: frontmatter.description, type: frontmatter.type, updatedAt };
|
|
71
77
|
});
|
|
72
78
|
return { indexRows: parseMemoryIndexRows(indexText), diskFiles, files };
|
|
73
79
|
}
|
|
@@ -122,4 +128,52 @@ export function memorySnapshot(root) {
|
|
|
122
128
|
readError: null,
|
|
123
129
|
};
|
|
124
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Read one memory file's body for the card's content preview — the viewer
|
|
133
|
+
* side of the same read-only fence as `memorySnapshot`. The requested file
|
|
134
|
+
* name must be a plain entry of the workspace's memory directory (no path
|
|
135
|
+
* separators, no traversal, resolved location stays inside the directory);
|
|
136
|
+
* anything else answers `outside` without touching the filesystem, so the
|
|
137
|
+
* endpoint cannot become a path oracle. Oversized files answer `too-large`
|
|
138
|
+
* with the body unread.
|
|
139
|
+
*/
|
|
140
|
+
export function memoryFileContent(root, file) {
|
|
141
|
+
const memoryDir = memoryDirFor(resolve(root));
|
|
142
|
+
if (file.length === 0 ||
|
|
143
|
+
!file.endsWith(".md") ||
|
|
144
|
+
file.includes("/") ||
|
|
145
|
+
file.includes("\\") ||
|
|
146
|
+
basename(file) !== file) {
|
|
147
|
+
return { ok: false, reason: "outside" };
|
|
148
|
+
}
|
|
149
|
+
const target = resolve(memoryDir, file);
|
|
150
|
+
if (!target.startsWith(memoryDir + "/") && target !== memoryDir) {
|
|
151
|
+
return { ok: false, reason: "outside" };
|
|
152
|
+
}
|
|
153
|
+
let before;
|
|
154
|
+
try {
|
|
155
|
+
before = statSync(target);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { ok: false, reason: "absent" };
|
|
159
|
+
}
|
|
160
|
+
if (!before.isFile()) {
|
|
161
|
+
return { ok: false, reason: "absent" };
|
|
162
|
+
}
|
|
163
|
+
if (before.size > MEMORY_FILE_PREVIEW_LIMIT) {
|
|
164
|
+
return { ok: false, reason: "too-large" };
|
|
165
|
+
}
|
|
166
|
+
let content;
|
|
167
|
+
try {
|
|
168
|
+
content = readFileSync(target, "utf8");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return { ok: false, reason: "absent" };
|
|
172
|
+
}
|
|
173
|
+
// The file may have been rewritten between stat and read — surface that
|
|
174
|
+
// instead of silently showing stale bytes (same guard shape as ZCode's
|
|
175
|
+
// memory viewer's "changed during read" state).
|
|
176
|
+
const after = statSync(target);
|
|
177
|
+
return { ok: true, file, content, mtimeMs: after.mtimeMs, changed: after.mtimeMs !== before.mtimeMs };
|
|
178
|
+
}
|
|
125
179
|
//# sourceMappingURL=memory-snapshot.js.map
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ZCode-style workspace memory for DeepSeek Harness: a per-workspace markdown store
|
|
3
|
+
"version": "0.14.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"default": "./lib/index.js"
|
|
21
21
|
},
|
|
22
22
|
"./client": "./client/client.js",
|
|
23
|
-
"./package.json": "./package.json"
|
|
23
|
+
"./package.json": "./package.json",
|
|
24
|
+
"./locale/*.json": "./locale/*.json"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"lib/**/*.js",
|
|
27
28
|
"lib/**/*.d.ts",
|
|
28
29
|
"client/client.js",
|
|
29
30
|
"cordis.patch.yml",
|
|
31
|
+
"locale/*.json",
|
|
30
32
|
"README.md",
|
|
31
33
|
"LICENSE"
|
|
32
34
|
],
|
|
@@ -35,7 +37,11 @@
|
|
|
35
37
|
"patch": "./cordis.patch.yml"
|
|
36
38
|
},
|
|
37
39
|
"client": {
|
|
38
|
-
"inject": [
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-locale",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-theme"
|
|
44
|
+
],
|
|
39
45
|
"platform": "web"
|
|
40
46
|
}
|
|
41
47
|
},
|