@lmzhen/dsh-evolution-skill-history 0.12.0 → 0.13.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 +13 -4
- package/lib/client.js +394 -146
- package/lib/index.js +75 -7
- package/lib/types/client/Panel.d.ts +14 -11
- package/lib/types/client/api.d.ts +34 -1
- package/lib/types/client/messages.d.ts +12 -3
- package/lib/types/client/styles.d.ts +26 -0
- package/lib/types/routes.d.ts +2 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lmzhen/dsh-evolution-skill-history
|
|
2
2
|
|
|
3
|
-
The web surface for skill content history: a left-sidebar panel row (**Skill history**) that lists the versions every skill write records, and the
|
|
3
|
+
The web surface for skill content history: a left-sidebar panel row (**Skill history**) that lists the versions every skill write records, and the five loopback host routes it reads.
|
|
4
4
|
|
|
5
5
|
The **facts** belong to `@lmzhen/dsh-evolution-core` (which artifact a version holds, how the body chain and the support-file chain split) and the **only write** is `curator.undo`. This package maps a route to that seam and draws it; it owns no state of its own.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Two registrations, one id (`skill-history`), because that is the sidebar's contr
|
|
|
10
10
|
|
|
11
11
|
ctx.slots.inject('sidebar.panellist', function* () {
|
|
12
12
|
yield ctx.slots.register({ name: 'sidebar.panellist', id: PANEL_ID, order: 35, label: () => t('entry.label') }, PanelIcon)
|
|
13
|
-
yield ctx.slots.register({ name: 'main', key: PANEL_ID, inject: () => ({ t, loadSkills, loadVersions, undo }) }, SkillHistoryPanel)
|
|
13
|
+
yield ctx.slots.register({ name: 'main', key: PANEL_ID, inject: () => ({ t, format, loadSkills, loadVersions, loadDiff, undo }) }, SkillHistoryPanel)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
The label is a **thunk**: the sidebar re-reads it on every projection, so a language switch follows without re-registering the row. The body receives plain callbacks through its inject face — no service handle, no subscription, and the component never sees the context.
|
|
@@ -22,6 +22,7 @@ The label is a **thunk**: the sidebar re-reads it on every projection, so a lang
|
|
|
22
22
|
| GET | `/api/dsh-evolution/skill-history/skills` | the skills that recorded at least one version, with their counts |
|
|
23
23
|
| GET | `/api/dsh-evolution/skill-history/versions?name=` | the body chain and the support-file chain, split, each body row carrying its own `undoable` verdict |
|
|
24
24
|
| POST | `/api/dsh-evolution/skill-history/undo` | `curator.undo(name, v?)` — the only write |
|
|
25
|
+
| GET | `/api/dsh-evolution/skill-history/versions/diff?name=&v=` | one body version against its recorded predecessor, as line counts plus the windowed changed region |
|
|
25
26
|
| GET | `/api/dsh-evolution/skill-history/health` | liveness probe for the client |
|
|
26
27
|
|
|
27
28
|
Every route sits behind the platform's own /api fence, kept local because the canonical implementation is not part of that package's published surface: the **socket** must be loopback (authoritative; `X-Forwarded-For` is never trusted), the **Host** header must name a loopback authority, an explicit `sec-fetch-site: cross-site` marker is refused, and an attached **Origin** must be exactly this authority.
|
|
@@ -32,7 +33,13 @@ The row is **inert without a web server** (`ctx.get('webServer')`, not a declare
|
|
|
32
33
|
|
|
33
34
|
## What the panel shows
|
|
34
35
|
|
|
35
|
-
The
|
|
36
|
+
**The left column** lists the skills that recorded versions, one row per skill: its name, how many versions it has, its own one-line description (the listing already carried it) and the management state the marker probe reported. A **search box** filters that list by name or description — client state only, no route behind it.
|
|
37
|
+
|
|
38
|
+
**The right pane** shows the selected skill's versions as two groups: **Body versions** (restorable) and **File versions** (history only, because a restore rewrites `SKILL.md`). A body row reads like a sentence rather than a dump — `v3 targeted edit 2026-09-27 18:20 · 3 hours ago (+1363 chars)` — with a *current* capsule on the live content, **Restore this version** (an inline two-click confirmation) on the others, and **Diff** expanding that version against its predecessor (lines added and removed, plus the changed region, marked truncated when it is long). After a restore the rows are read again — the restore itself is a new version — while the curator's result sentence stays on screen. **Refresh** re-reads the skill list and the open skill.
|
|
39
|
+
|
|
40
|
+
**The panel owns no rule and no arithmetic.** The host reports every row with its whole verdict — `undoable` (evolution-core's artifact classification against the live bytes), `actionKind` (a closed vocabulary key), `age` (a bucket plus a count), `charsDelta`, `summary` — and this half substitutes words from its locale dictionary and renders. The time, the action words and the sentences are therefore localized; the facts are computed once, host-side, so the panel and the slash commands cannot drift.
|
|
41
|
+
|
|
42
|
+
**Appearance follows the platform's tokens, not its modules.** The panel carries its own stylesheet (`src/client/styles.ts`, injected once behind `data-plugin-css`) and copies its metrics from the platform's own components — the command card's `<pre>` for the diff block, the sidebar row's hover and focus ring for the controls — while every size is a `--dsh-content-font-size*` token or a `calc()` over one, so the panel follows the deployment's content size the way the platform's panels do. Importing the platform's control package is NOT possible from here without splitting this package's compiler faces: the specifier is path-mapped to source, and `tsc -b` then refuses the foreign project (host/client face split, `react` devDependencies and a CI graph change would all be needed). The roll call of what the panel deliberately does not do is in `## Known limitations and deferred work`.
|
|
36
43
|
|
|
37
44
|
## Build
|
|
38
45
|
|
|
@@ -46,6 +53,8 @@ This package adds **no model-visible content**: no prompt section, no tool schem
|
|
|
46
53
|
|
|
47
54
|
- **No rendered spec.** The family's specs are Node-level and this package's client half cannot be rendered there, so the panel is covered by `tsc`, the client bundle build, the pure `tests/client-api.spec.ts` and a live pass on the installed artifact. A rendered spec (jsdom plus a driven fixture runtime) is the follow-up that would catch wiring in CI — 0.11.3 is the cost of not having one: the first installed pass restored the content correctly and re-read the rows correctly while the curator's result sentence never appeared, because the reload that follows a restore cleared the note state it had just set. No Node-level spec and no `tsc` can see the order of two state updates; only clicking the panel showed it.
|
|
48
55
|
- **The list shows skills with recorded versions only.** A skill that never wrote through the library has no history to browse, so it does not appear; the tree-wide listing stays with the skill catalog.
|
|
49
|
-
- **No
|
|
56
|
+
- **No quality and no last-use in the list.** Both are real and both are wanted, but they live in the curator's health view and the usage store; joining them into this listing would make the route a second home for facts it does not own. The row shows what the listing already holds (description, managed/protected markers, version count).
|
|
57
|
+
- **No rendered component spec yet.** The panel's state machine (result sentence, two-click confirmation, the read ticket that discards a late reply, the lazy diff) is pinned by `tsc`, the bundle build and live passes; a jsdom lane is the follow-up, and it needs `react` in this package's devDependencies plus one `pnpm install` in the tree the specs run in.
|
|
58
|
+
|
|
50
59
|
- **Support-file bytes are history only.** They share the index with the body, so they are listed and labelled, but `undo` refuses them by name — the bytes stay in the blob store for a hand copy.
|
|
51
60
|
- **No occupancy row.** `.history` growth is documented in the evolution-core README; showing the size in the panel would promise a cleanup path the family has not designed (a whole-library reference count belongs to the curator).
|
package/lib/client.js
CHANGED
|
@@ -18,6 +18,7 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
const HOST_ROUTES = {
|
|
19
19
|
skills: "/api/dsh-evolution/skill-history/skills",
|
|
20
20
|
versions: "/api/dsh-evolution/skill-history/versions",
|
|
21
|
+
diff: "/api/dsh-evolution/skill-history/versions/diff",
|
|
21
22
|
undo: "/api/dsh-evolution/skill-history/undo"
|
|
22
23
|
};
|
|
23
24
|
/** A refusal the host reported, carrying its own sentence (the curator's, not ours). */
|
|
@@ -38,6 +39,7 @@ window.__ModuleLoader__.load({
|
|
|
38
39
|
return {
|
|
39
40
|
skills: async () => await request(HOST_ROUTES.skills),
|
|
40
41
|
versions: async (name) => await request(HOST_ROUTES.versions + "?name=" + encodeURIComponent(name)),
|
|
42
|
+
diff: async (name, v) => await request(HOST_ROUTES.diff + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
|
|
41
43
|
undo: async (name, v) => {
|
|
42
44
|
return (await request(HOST_ROUTES.undo, {
|
|
43
45
|
method: "POST",
|
|
@@ -53,10 +55,11 @@ window.__ModuleLoader__.load({
|
|
|
53
55
|
//#endregion
|
|
54
56
|
//#region src/client/messages.ts
|
|
55
57
|
/**
|
|
56
|
-
* Panel copy, in a zh/en dictionary registered through the platform
|
|
58
|
+
* Panel copy, in a zh/en dictionary registered through the platform’s locale seat.
|
|
57
59
|
*
|
|
58
|
-
* Client copy is locale-owned: the entry label, the group headings, the actions
|
|
59
|
-
*
|
|
60
|
+
* Client copy is locale-owned: the entry label, the group headings, the actions, the version facts
|
|
61
|
+
* (action words, relative time, deltas) and the status lines all resolve here, and the panel carries
|
|
62
|
+
* no literal string of its own. Facts come from the host; only the WORDS live here.
|
|
60
63
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
61
64
|
*/
|
|
62
65
|
/** The locale namespace this bundle registers. */
|
|
@@ -66,6 +69,9 @@ window.__ModuleLoader__.load({
|
|
|
66
69
|
"entry.label": "技能历史",
|
|
67
70
|
title: "技能历史",
|
|
68
71
|
hint: "每次技能写入留下的内容版本。回退只改 SKILL.md 正文,标记、使用计数与策展状态不变。",
|
|
72
|
+
"hint.current": "内容相同的版本会同时标「当前」。",
|
|
73
|
+
search: "按名字或描述筛选",
|
|
74
|
+
"search.none": "没有匹配的技能。",
|
|
69
75
|
"group.content": "正文版本",
|
|
70
76
|
"group.support": "文件版本(不可回退)",
|
|
71
77
|
"group.support.note": "附带文件的字节与正文共用一份索引;回退不恢复它们。",
|
|
@@ -74,18 +80,60 @@ window.__ModuleLoader__.load({
|
|
|
74
80
|
loading: "读取中…",
|
|
75
81
|
refresh: "刷新",
|
|
76
82
|
undo: "回退到此版",
|
|
77
|
-
"undo.confirm": "
|
|
78
|
-
"undo.yes": "回退",
|
|
83
|
+
"undo.confirm": "确认回退",
|
|
79
84
|
"undo.no": "取消",
|
|
80
85
|
current: "当前",
|
|
86
|
+
"versions.count": "个版本",
|
|
87
|
+
"row.first": "初始版本",
|
|
88
|
+
"row.summary.missing": "(未生成摘要)",
|
|
89
|
+
"row.delta.up": "(+{n} 字符)",
|
|
90
|
+
"row.delta.down": "(-{n} 字符)",
|
|
91
|
+
"diff.show": "差异",
|
|
92
|
+
"diff.hide": "收起差异",
|
|
93
|
+
"diff.loading": "读取差异…",
|
|
94
|
+
"diff.against": "与 v{n} 比较",
|
|
95
|
+
"diff.first": "这是第一版,整篇都是新增",
|
|
96
|
+
"diff.truncated": "差异过长,只显示前一部分",
|
|
97
|
+
"diff.added": "新增 {n} 行",
|
|
98
|
+
"diff.removed": "删除 {n} 行",
|
|
99
|
+
"state.managed": "家族管理",
|
|
100
|
+
"state.foreign": "非家族管理",
|
|
101
|
+
"state.protected": "受保护",
|
|
102
|
+
"state.unknown": "标记不可读",
|
|
103
|
+
"action.baseline": "初始快照",
|
|
104
|
+
"action.create": "新建",
|
|
105
|
+
"action.patch": "定点修改",
|
|
106
|
+
"action.update": "整篇更新",
|
|
107
|
+
"action.restore": "回退",
|
|
108
|
+
"action.delete": "删除(存的是被删的正文)",
|
|
109
|
+
"action.archive": "归档(存的是被归档的正文)",
|
|
110
|
+
"action.consolidate": "合并",
|
|
111
|
+
"action.restructure": "结构调整",
|
|
112
|
+
"action.support-write": "写入附带文件",
|
|
113
|
+
"action.support-remove": "删除附带文件",
|
|
114
|
+
"action.other": "其他写入",
|
|
115
|
+
"time.now": "刚刚",
|
|
116
|
+
"time.minutes": "{n} 分钟前",
|
|
117
|
+
"time.minutes.one": "1 分钟前",
|
|
118
|
+
"time.hours": "{n} 小时前",
|
|
119
|
+
"time.hours.one": "1 小时前",
|
|
120
|
+
"time.days": "{n} 天前",
|
|
121
|
+
"time.days.one": "1 天前",
|
|
122
|
+
"time.months": "{n} 个月前",
|
|
123
|
+
"time.months.one": "1 个月前",
|
|
124
|
+
"time.years": "{n} 年前",
|
|
125
|
+
"time.years.one": "1 年前",
|
|
81
126
|
error: "读取失败",
|
|
82
|
-
|
|
127
|
+
"error.hint": "面板读不到宿主路由:通常是插件版本与宿主不匹配,重启 dsh web 后重试。"
|
|
83
128
|
};
|
|
84
129
|
/** English dictionary: the same keys, or the panel falls back to the key itself. */
|
|
85
130
|
const en = {
|
|
86
131
|
"entry.label": "Skill history",
|
|
87
132
|
title: "Skill history",
|
|
88
133
|
hint: "The content versions every skill write leaves behind. Restoring changes the SKILL.md body only: markers, usage counts and curation state stay as they are.",
|
|
134
|
+
"hint.current": "Versions holding the same content are all marked “current”.",
|
|
135
|
+
search: "Filter by name or description",
|
|
136
|
+
"search.none": "No skill matches.",
|
|
89
137
|
"group.content": "Body versions",
|
|
90
138
|
"group.support": "File versions (not restorable)",
|
|
91
139
|
"group.support.note": "Support-file bytes share this index with the body; restoring the body does not bring them back.",
|
|
@@ -94,147 +142,258 @@ window.__ModuleLoader__.load({
|
|
|
94
142
|
loading: "Loading…",
|
|
95
143
|
refresh: "Refresh",
|
|
96
144
|
undo: "Restore this version",
|
|
97
|
-
"undo.confirm": "
|
|
98
|
-
"undo.yes": "Restore",
|
|
145
|
+
"undo.confirm": "Confirm restore",
|
|
99
146
|
"undo.no": "Cancel",
|
|
100
147
|
current: "Current",
|
|
148
|
+
"versions.count": "versions",
|
|
149
|
+
"row.first": "First version",
|
|
150
|
+
"row.summary.missing": "(no summary generated)",
|
|
151
|
+
"row.delta.up": "(+{n} chars)",
|
|
152
|
+
"row.delta.down": "(-{n} chars)",
|
|
153
|
+
"diff.show": "Diff",
|
|
154
|
+
"diff.hide": "Hide diff",
|
|
155
|
+
"diff.loading": "Loading the diff…",
|
|
156
|
+
"diff.against": "compared with v{n}",
|
|
157
|
+
"diff.first": "first version: everything is new",
|
|
158
|
+
"diff.truncated": "the change is long; only its beginning is shown",
|
|
159
|
+
"diff.added": "{n} lines added",
|
|
160
|
+
"diff.removed": "{n} lines removed",
|
|
161
|
+
"state.managed": "family-managed",
|
|
162
|
+
"state.foreign": "not family-managed",
|
|
163
|
+
"state.protected": "protected",
|
|
164
|
+
"state.unknown": "marker unreadable",
|
|
165
|
+
"action.baseline": "initial snapshot",
|
|
166
|
+
"action.create": "created",
|
|
167
|
+
"action.patch": "targeted edit",
|
|
168
|
+
"action.update": "whole-body update",
|
|
169
|
+
"action.restore": "restored",
|
|
170
|
+
"action.delete": "deleted (holds the removed body)",
|
|
171
|
+
"action.archive": "archived (holds the archived body)",
|
|
172
|
+
"action.consolidate": "merged",
|
|
173
|
+
"action.restructure": "restructured",
|
|
174
|
+
"action.support-write": "support file written",
|
|
175
|
+
"action.support-remove": "support file removed",
|
|
176
|
+
"action.other": "other write",
|
|
177
|
+
"time.now": "just now",
|
|
178
|
+
"time.minutes": "{n} minutes ago",
|
|
179
|
+
"time.minutes.one": "1 minute ago",
|
|
180
|
+
"time.hours": "{n} hours ago",
|
|
181
|
+
"time.hours.one": "1 hour ago",
|
|
182
|
+
"time.days": "{n} days ago",
|
|
183
|
+
"time.days.one": "1 day ago",
|
|
184
|
+
"time.months": "{n} months ago",
|
|
185
|
+
"time.months.one": "1 month ago",
|
|
186
|
+
"time.years": "{n} years ago",
|
|
187
|
+
"time.years.one": "1 year ago",
|
|
101
188
|
error: "Could not load",
|
|
102
|
-
|
|
189
|
+
"error.hint": "The panel could not reach the host routes — usually a plugin/host version mismatch; restart dsh web and retry."
|
|
103
190
|
};
|
|
191
|
+
/**
|
|
192
|
+
* Fill one TEMPLATE’s `{n}` slots. The seat resolves the key, this fills it, so the panel needs
|
|
193
|
+
* only the seat plus this rule and never learns which language it is rendering.
|
|
194
|
+
* @param template - the copy, with `{slot}` placeholders.
|
|
195
|
+
* @param values - the substitutions, keyed by the slot name inside the braces.
|
|
196
|
+
* @returns the filled copy.
|
|
197
|
+
*/
|
|
198
|
+
function fill(template, values) {
|
|
199
|
+
return template.replace(/\{(\w+)\}/g, (match, slot) => {
|
|
200
|
+
const value = values[slot];
|
|
201
|
+
return value === void 0 ? match : String(value);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/client/styles.ts
|
|
206
|
+
/**
|
|
207
|
+
* The panel’s stylesheet, injected once behind a `<style data-plugin-css=…>` tag.
|
|
208
|
+
*
|
|
209
|
+
* The bundle is built outside the platform’s CSS-Modules pipeline, so it carries its own string and
|
|
210
|
+
* injects it the same way the family’s settings section does. Every rule reads the platform’s design
|
|
211
|
+
* tokens, and the metrics are COPIED from the platform’s own components rather than invented:
|
|
212
|
+
*
|
|
213
|
+
* - the code block (`--dsw-font-markdown-code-block-small`, `.5px` border, `12px`, radius `12px`,
|
|
214
|
+
* `max-height: 260px`) is the platform command card’s `<pre>` (client/ui-chat/…/GenericCommandCard.module.css);
|
|
215
|
+
* - hover / focus (`--dsw-alias-interactive-bg-hover`, a 2px inset focus ring) follow the platform
|
|
216
|
+
* sidebar row (client/ui-sidebar/…/SidebarRoot.module.css);
|
|
217
|
+
* - the panel’s type scale is the platform TOKEN rather than a magic number, so it follows the
|
|
218
|
+
* deployment’s content size the way the platform’s own panels do;
|
|
219
|
+
* - the capsule and the field metrics follow the family’s settings card, which copied them from the
|
|
220
|
+
* platform’s settings fields in the first place.
|
|
221
|
+
*
|
|
222
|
+
* Geometry (widths, paddings, radii, the 260px scroll cap) is a plain number here because the platform's
|
|
223
|
+
* own components write geometry the same way; what must never be a literal is TYPE and COLOUR, and
|
|
224
|
+
* every size and colour below is a token or a `calc()` over one.
|
|
225
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
226
|
+
*/
|
|
227
|
+
/** Tag id that makes the injection idempotent. */
|
|
228
|
+
const CSS_TAG_ID = "@lmzhen/dsh-evolution-skill-history/panel.css";
|
|
229
|
+
/** The stylesheet the panel injects once. */
|
|
230
|
+
const CSS = [
|
|
231
|
+
".evo-hist-root{display:flex;height:100%;min-height:0;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);color:var(--dsw-alias-label-primary)}",
|
|
232
|
+
".evo-hist-aside{display:flex;flex-direction:column;flex:0 0 264px;width:264px;min-height:0;border-right:.5px solid var(--dsw-alias-border-l2)}",
|
|
233
|
+
".evo-hist-search{padding:10px 12px 4px}",
|
|
234
|
+
".evo-hist-search input{width:100%;box-sizing:border-box;padding:6px 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit}",
|
|
235
|
+
".evo-hist-search input:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:-2px}",
|
|
236
|
+
".evo-hist-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 6px 10px}",
|
|
237
|
+
".evo-hist-empty{padding:8px 12px;color:var(--dsw-alias-label-tertiary)}",
|
|
238
|
+
".evo-hist-skill{display:block;width:100%;box-sizing:border-box;padding:7px 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer}",
|
|
239
|
+
".evo-hist-skill:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
240
|
+
".evo-hist-skill:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:-2px}",
|
|
241
|
+
".evo-hist-skill[aria-current=\"true\"]{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
242
|
+
".evo-hist-skill-line{display:flex;align-items:baseline;gap:6px}",
|
|
243
|
+
".evo-hist-skill-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
244
|
+
".evo-hist-skill-count{flex:none;margin-left:auto;color:var(--dsw-alias-label-tertiary)}",
|
|
245
|
+
".evo-hist-skill-desc{margin-top:2px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
246
|
+
".evo-hist-main{flex:1 1 auto;min-width:0;min-height:0;overflow-y:auto;padding:12px 14px}",
|
|
247
|
+
".evo-hist-head{display:flex;align-items:center;gap:10px;margin:0 0 4px}",
|
|
248
|
+
".evo-hist-title{flex:1 1 auto;margin:0;font-size:inherit;font-weight:600}",
|
|
249
|
+
".evo-hist-hint{margin:0 0 10px;color:var(--dsw-alias-label-secondary);line-height:1.5}",
|
|
250
|
+
".evo-hist-note{margin:0 0 10px;color:var(--dsw-alias-label-secondary);line-height:1.5;white-space:pre-wrap}",
|
|
251
|
+
".evo-hist-note[data-error=\"true\"]{color:var(--dsw-alias-state-error-primary)}",
|
|
252
|
+
".evo-hist-group{margin:10px 0 2px;font-size:inherit;font-weight:600}",
|
|
253
|
+
".evo-hist-row{display:flex;align-items:flex-start;gap:8px;padding:8px 0;border-top:.5px solid var(--dsw-alias-border-l3)}",
|
|
254
|
+
".evo-hist-row-body{flex:1 1 auto;min-width:0}",
|
|
255
|
+
".evo-hist-row-title{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
|
|
256
|
+
".evo-hist-row-meta{color:var(--dsw-alias-label-tertiary)}",
|
|
257
|
+
".evo-hist-row-summary{margin-top:2px;color:var(--dsw-alias-label-secondary);line-height:1.5}",
|
|
258
|
+
".evo-hist-actions{display:flex;flex:none;align-items:center;gap:6px}",
|
|
259
|
+
".evo-hist-capsule{flex:none;padding:1px 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-secondary)}",
|
|
260
|
+
".evo-hist-button{flex:none;font:inherit;padding:3px 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:inherit;cursor:pointer}",
|
|
261
|
+
".evo-hist-button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
262
|
+
".evo-hist-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:-2px}",
|
|
263
|
+
".evo-hist-button[data-tone=\"primary\"]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}",
|
|
264
|
+
".evo-hist-pre{margin:6px 0 2px;padding:12px 16px;max-height:260px;overflow:auto;border:.5px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
|
|
265
|
+
".evo-hist-pre-add{color:var(--dsw-alias-brand-primary)}",
|
|
266
|
+
".evo-hist-pre-del{color:var(--dsw-alias-state-error-primary)}"
|
|
267
|
+
].join("\n");
|
|
104
268
|
//#endregion
|
|
105
269
|
//#region src/client/Panel.ts
|
|
106
270
|
/**
|
|
107
271
|
* The skill-history panel: the skills that recorded versions, the two chains of the selected skill,
|
|
108
272
|
* and one action per body version (restore, behind an inline confirmation).
|
|
109
273
|
*
|
|
110
|
-
* The panel owns no rule: the host reports each
|
|
111
|
-
* the
|
|
112
|
-
*
|
|
274
|
+
* The panel owns no rule and no arithmetic: the host reports each row with its whole verdict
|
|
275
|
+
* (`undoable`, the vocabulary key `actionKind`, the `age` bucket, `charsDelta`, `summary`) and this
|
|
276
|
+
* file substitutes WORDS through the locale dictionary and renders. It never subscribes to anything
|
|
277
|
+
* and never sees the context.
|
|
113
278
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* version's action within reach of the version it acts on.
|
|
279
|
+
* State rules, each pinned by a live pass or a component spec: a restore's result sentence stays on
|
|
280
|
+
* screen while the rows are read again; nothing is read on a poll (mount, opening a skill, the
|
|
281
|
+
* refresh button, after a restore); a late reply for a skill the operator has already left is
|
|
282
|
+
* discarded by its ticket; and the diff is fetched only when a row is expanded.
|
|
119
283
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
120
284
|
*/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
border: "1px solid var(--dsw-alias-border-l2)",
|
|
206
|
-
background: "transparent",
|
|
207
|
-
color: "inherit",
|
|
208
|
-
cursor: "pointer"
|
|
209
|
-
};
|
|
210
|
-
const note = {
|
|
211
|
-
margin: "0 0 10px",
|
|
212
|
-
fontSize: "12px",
|
|
213
|
-
color: "var(--dsw-alias-label-secondary)",
|
|
214
|
-
whiteSpace: "pre-wrap"
|
|
215
|
-
};
|
|
216
|
-
/** One version row: the facts, the current/plain chip, and the restore action when the row allows it. */
|
|
217
|
-
function versionEntry(face, entry) {
|
|
218
|
-
const confirming = face.confirming === entry.v;
|
|
219
|
-
return (0, react.createElement)("div", {
|
|
220
|
-
key: String(entry.v),
|
|
221
|
-
style: row
|
|
222
|
-
}, (0, react.createElement)("span", { style: rowMeta }, "v" + String(entry.v) + " " + entry.at + " " + entry.action + " " + String(entry.chars) + " chars " + entry.hash.slice(0, 12)), entry.undoable === false ? (0, react.createElement)("span", { style: chip }, face.t("current")) : null, entry.undoable === true ? (0, react.createElement)("button", {
|
|
285
|
+
/** Cut a description to one line's worth of characters (the CSS ellipsises the rest). */
|
|
286
|
+
function oneLine(text) {
|
|
287
|
+
const firstLine = text.split("\n")[0] ?? "";
|
|
288
|
+
return firstLine.length > 120 ? firstLine.slice(0, 119) + "…" : firstLine;
|
|
289
|
+
}
|
|
290
|
+
/** The local time an ISO timestamp names, as the operator's own clock shows it. */
|
|
291
|
+
function localTime(at) {
|
|
292
|
+
const parsed = Date.parse(at);
|
|
293
|
+
if (Number.isNaN(parsed)) return at;
|
|
294
|
+
const date = new Date(parsed);
|
|
295
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
296
|
+
return String(date.getFullYear()) + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes());
|
|
297
|
+
}
|
|
298
|
+
/** The state word one skill row carries, from the facts the listing reported. */
|
|
299
|
+
function stateKey(skill) {
|
|
300
|
+
if (skill.protectionUnknown) return "state.unknown";
|
|
301
|
+
if (skill.protectedBy !== null) return "state.protected";
|
|
302
|
+
return skill.managed ? "state.managed" : "state.foreign";
|
|
303
|
+
}
|
|
304
|
+
/** The relative-time sentence for one row's bucket. */
|
|
305
|
+
function ageText(face, age) {
|
|
306
|
+
if (age.unit === "now") return face.t("time.now");
|
|
307
|
+
return age.n === 1 ? face.t("time." + age.unit + ".one") : face.format("time." + age.unit, { n: age.n });
|
|
308
|
+
}
|
|
309
|
+
/** The character delta of one row, or null when it is the first version. */
|
|
310
|
+
function deltaText(face, row) {
|
|
311
|
+
if (row.charsDelta === void 0 || row.charsDelta === 0) return null;
|
|
312
|
+
return row.charsDelta > 0 ? face.format("row.delta.up", { n: row.charsDelta }) : face.format("row.delta.down", { n: -row.charsDelta });
|
|
313
|
+
}
|
|
314
|
+
/** One `<pre>` rendering of a diff: added lines marked `+`, removed ones `-`. */
|
|
315
|
+
function diffBlock(diff) {
|
|
316
|
+
const rows = [];
|
|
317
|
+
for (const hunk of diff.hunks) {
|
|
318
|
+
for (const [index, line] of hunk.oldText.split("\n").entries()) {
|
|
319
|
+
if (hunk.oldText === "" && line === "") continue;
|
|
320
|
+
rows.push((0, react.createElement)("div", {
|
|
321
|
+
key: "old-" + String(index),
|
|
322
|
+
className: "evo-hist-pre-del"
|
|
323
|
+
}, "- " + line));
|
|
324
|
+
}
|
|
325
|
+
for (const [index, line] of hunk.newText.split("\n").entries()) {
|
|
326
|
+
if (hunk.newText === "" && line === "") continue;
|
|
327
|
+
rows.push((0, react.createElement)("div", {
|
|
328
|
+
key: "new-" + String(index),
|
|
329
|
+
className: "evo-hist-pre-add"
|
|
330
|
+
}, "+ " + line));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return (0, react.createElement)("pre", { className: "evo-hist-pre" }, rows.length === 0 ? "±" : rows);
|
|
334
|
+
}
|
|
335
|
+
/** One version row: what happened, when, how big, and what it is against. */
|
|
336
|
+
function versionRow(face, row, confirming, openDiff, diff, restore, toggleDiff) {
|
|
337
|
+
const parts = [
|
|
338
|
+
(0, react.createElement)("span", {
|
|
339
|
+
key: "v",
|
|
340
|
+
className: "evo-hist-row-meta"
|
|
341
|
+
}, "v" + String(row.v)),
|
|
342
|
+
(0, react.createElement)("span", { key: "action" }, face.t("action." + row.actionKind)),
|
|
343
|
+
(0, react.createElement)("span", {
|
|
344
|
+
key: "time",
|
|
345
|
+
className: "evo-hist-row-meta"
|
|
346
|
+
}, localTime(row.at) + " · " + ageText(face, row.age))
|
|
347
|
+
];
|
|
348
|
+
const delta = deltaText(face, row);
|
|
349
|
+
if (delta !== null) parts.push((0, react.createElement)("span", {
|
|
350
|
+
key: "delta",
|
|
351
|
+
className: "evo-hist-row-meta"
|
|
352
|
+
}, delta));
|
|
353
|
+
const actions = [];
|
|
354
|
+
if (row.undoable === false) actions.push((0, react.createElement)("span", {
|
|
355
|
+
key: "current",
|
|
356
|
+
className: "evo-hist-capsule"
|
|
357
|
+
}, face.t("current")));
|
|
358
|
+
if (row.undoable === true) actions.push((0, react.createElement)("button", {
|
|
359
|
+
key: "undo",
|
|
360
|
+
type: "button",
|
|
361
|
+
className: "evo-hist-button",
|
|
362
|
+
"data-tone": confirming === row.v ? "primary" : void 0,
|
|
363
|
+
onClick: () => {
|
|
364
|
+
restore(row.v);
|
|
365
|
+
}
|
|
366
|
+
}, confirming === row.v ? face.t("undo.confirm") : face.t("undo")));
|
|
367
|
+
if (row.undoable === true || row.charsDelta !== void 0) actions.push((0, react.createElement)("button", {
|
|
368
|
+
key: "diff",
|
|
223
369
|
type: "button",
|
|
224
|
-
|
|
370
|
+
className: "evo-hist-button",
|
|
225
371
|
onClick: () => {
|
|
226
|
-
|
|
372
|
+
toggleDiff(row.v);
|
|
227
373
|
}
|
|
228
|
-
},
|
|
374
|
+
}, openDiff === row.v ? face.t("diff.hide") : face.t("diff.show")));
|
|
375
|
+
return (0, react.createElement)("div", {
|
|
376
|
+
key: String(row.v),
|
|
377
|
+
className: "evo-hist-row"
|
|
378
|
+
}, (0, react.createElement)("div", { className: "evo-hist-row-body" }, (0, react.createElement)("div", { className: "evo-hist-row-title" }, parts), row.summary === void 0 || row.summary === "" ? null : (0, react.createElement)("div", { className: "evo-hist-row-summary" }, row.summary), openDiff === row.v && diff !== void 0 ? diffBody(face, diff) : null), (0, react.createElement)("div", { className: "evo-hist-actions" }, actions));
|
|
379
|
+
}
|
|
380
|
+
/** The expanded body of one row: the diff, a loading line, or the refusal. */
|
|
381
|
+
function diffBody(face, diff) {
|
|
382
|
+
if (diff.kind === "loading") return (0, react.createElement)("p", { className: "evo-hist-note" }, face.t("diff.loading"));
|
|
383
|
+
if (diff.kind === "failed") return (0, react.createElement)("p", {
|
|
384
|
+
className: "evo-hist-note",
|
|
385
|
+
"data-error": "true"
|
|
386
|
+
}, diff.message);
|
|
387
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("p", { className: "evo-hist-note" }, (diff.diff.against === null ? face.t("diff.first") : face.format("diff.against", { n: diff.diff.against })) + " · " + face.format("diff.added", { n: diff.diff.linesAdded }) + " · " + face.format("diff.removed", { n: diff.diff.linesRemoved }) + (diff.diff.truncated ? " · " + face.t("diff.truncated") : "")), diffBlock(diff.diff));
|
|
229
388
|
}
|
|
230
389
|
/** One group: a heading, an optional note, and its rows. */
|
|
231
|
-
function group(face, title, entries, noteText) {
|
|
390
|
+
function group(face, title, entries, noteText, state) {
|
|
232
391
|
if (entries.length === 0) return null;
|
|
233
|
-
return (0, react.createElement)("section", { key: title }, (0, react.createElement)("h3", {
|
|
392
|
+
return (0, react.createElement)("section", { key: title }, (0, react.createElement)("h3", { className: "evo-hist-group" }, title), noteText === void 0 ? null : (0, react.createElement)("p", { className: "evo-hist-hint" }, noteText), ...entries.map((entry) => versionRow(face, entry, state.confirming, state.openDiff, state.diffs.get(entry.v), state.restore, state.toggleDiff)));
|
|
234
393
|
}
|
|
235
394
|
/**
|
|
236
395
|
* The panel body.
|
|
237
|
-
* @param face - copy
|
|
396
|
+
* @param face - copy plus the data callbacks.
|
|
238
397
|
* @returns the panel element.
|
|
239
398
|
*/
|
|
240
399
|
function SkillHistoryPanel(face) {
|
|
@@ -242,11 +401,19 @@ window.__ModuleLoader__.load({
|
|
|
242
401
|
const [selected, setSelected] = (0, react.useState)(void 0);
|
|
243
402
|
const [payload, setPayload] = (0, react.useState)(void 0);
|
|
244
403
|
const [noteText, setNoteText] = (0, react.useState)(void 0);
|
|
404
|
+
const [noteError, setNoteError] = (0, react.useState)(false);
|
|
245
405
|
const [pending, setPending] = (0, react.useState)(void 0);
|
|
406
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
407
|
+
const [openDiff, setOpenDiff] = (0, react.useState)(void 0);
|
|
408
|
+
const [diffs, setDiffs] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
246
409
|
const readTicket = (0, react.useRef)(0);
|
|
247
|
-
const failureText = (error) => face.t("error") + ": " + (error instanceof Error ? error.message : String(error));
|
|
248
410
|
const failed = (error) => {
|
|
249
|
-
|
|
411
|
+
setNoteError(true);
|
|
412
|
+
setNoteText(face.t("error.hint") + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
|
|
413
|
+
};
|
|
414
|
+
const succeeded = (message) => {
|
|
415
|
+
setNoteError(false);
|
|
416
|
+
setNoteText(message);
|
|
250
417
|
};
|
|
251
418
|
/** Read the skills again, keeping the sentence: a re-read is not an answer to anything. */
|
|
252
419
|
const reloadSkills = () => {
|
|
@@ -256,7 +423,7 @@ window.__ModuleLoader__.load({
|
|
|
256
423
|
* Show one skill's chains, keeping the sentence. A re-read that FAILS reports through the caller's
|
|
257
424
|
* own handler, so the failure of a read can never erase the write it was reading after.
|
|
258
425
|
* @param name - the skill to open.
|
|
259
|
-
* @param onFailure - what to do with a failed read
|
|
426
|
+
* @param onFailure - what to do with a failed read.
|
|
260
427
|
*/
|
|
261
428
|
const showVersions = (name, onFailure = failed) => {
|
|
262
429
|
const ticket = readTicket.current + 1;
|
|
@@ -264,6 +431,8 @@ window.__ModuleLoader__.load({
|
|
|
264
431
|
setSelected(name);
|
|
265
432
|
setPayload(void 0);
|
|
266
433
|
setPending(void 0);
|
|
434
|
+
setOpenDiff(void 0);
|
|
435
|
+
setDiffs(/* @__PURE__ */ new Map());
|
|
267
436
|
face.loadVersions(name).then((next) => {
|
|
268
437
|
if (readTicket.current === ticket) setPayload(next);
|
|
269
438
|
}).catch((error) => {
|
|
@@ -272,7 +441,10 @@ window.__ModuleLoader__.load({
|
|
|
272
441
|
};
|
|
273
442
|
/** Open a skill from the list: another skill's sentence described another skill, so it goes away. */
|
|
274
443
|
const open = (name) => {
|
|
275
|
-
if (name !== selected)
|
|
444
|
+
if (name !== selected) {
|
|
445
|
+
setNoteText(void 0);
|
|
446
|
+
setNoteError(false);
|
|
447
|
+
}
|
|
276
448
|
showVersions(name);
|
|
277
449
|
};
|
|
278
450
|
/** What the refresh button does: both reads again, and the sentence stays. */
|
|
@@ -290,33 +462,74 @@ window.__ModuleLoader__.load({
|
|
|
290
462
|
const name = selected;
|
|
291
463
|
face.undo(name, v).then((message) => {
|
|
292
464
|
showVersions(name, (error) => {
|
|
293
|
-
|
|
465
|
+
setNoteError(true);
|
|
466
|
+
setNoteText(message + "\n" + face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
|
|
294
467
|
});
|
|
295
|
-
|
|
468
|
+
succeeded(message);
|
|
296
469
|
}).catch(failed);
|
|
297
470
|
};
|
|
471
|
+
const toggleDiff = (v) => {
|
|
472
|
+
if (selected === void 0) return;
|
|
473
|
+
if (openDiff === v) {
|
|
474
|
+
setOpenDiff(void 0);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
setOpenDiff(v);
|
|
478
|
+
if (diffs.has(v)) return;
|
|
479
|
+
const name = selected;
|
|
480
|
+
const ticket = readTicket.current + 1;
|
|
481
|
+
readTicket.current = ticket;
|
|
482
|
+
setDiffs((current) => new Map(current).set(v, { kind: "loading" }));
|
|
483
|
+
face.loadDiff(name, v).then((diff) => {
|
|
484
|
+
if (readTicket.current !== ticket) return;
|
|
485
|
+
setDiffs((current) => new Map(current).set(v, {
|
|
486
|
+
kind: "ready",
|
|
487
|
+
diff
|
|
488
|
+
}));
|
|
489
|
+
}).catch((error) => {
|
|
490
|
+
if (readTicket.current !== ticket) return;
|
|
491
|
+
const message = face.t("error") + ": " + (error instanceof Error ? error.message : String(error));
|
|
492
|
+
setDiffs((current) => new Map(current).set(v, {
|
|
493
|
+
kind: "failed",
|
|
494
|
+
message
|
|
495
|
+
}));
|
|
496
|
+
});
|
|
497
|
+
};
|
|
298
498
|
(0, react.useEffect)(reloadSkills, []);
|
|
499
|
+
const needle = query.trim().toLowerCase();
|
|
500
|
+
const visible = (0, react.useMemo)(() => needle === "" ? skills : skills.filter((skill) => skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle)), [skills, needle]);
|
|
299
501
|
const loaded = payload !== void 0;
|
|
300
|
-
|
|
502
|
+
const rowState = {
|
|
503
|
+
confirming: pending,
|
|
504
|
+
openDiff,
|
|
505
|
+
diffs,
|
|
506
|
+
restore,
|
|
507
|
+
toggleDiff
|
|
508
|
+
};
|
|
509
|
+
return (0, react.createElement)("div", { className: "evo-hist-root" }, (0, react.createElement)("aside", { className: "evo-hist-aside" }, (0, react.createElement)("div", { className: "evo-hist-search" }, (0, react.createElement)("input", {
|
|
510
|
+
type: "search",
|
|
511
|
+
value: query,
|
|
512
|
+
placeholder: face.t("search"),
|
|
513
|
+
"aria-label": face.t("search"),
|
|
514
|
+
onChange: (event) => {
|
|
515
|
+
setQuery(event.target.value);
|
|
516
|
+
}
|
|
517
|
+
})), (0, react.createElement)("div", { className: "evo-hist-list" }, skills.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-empty" }, face.t("empty.skills")) : visible.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-empty" }, face.t("search.none")) : visible.map((skill) => (0, react.createElement)("button", {
|
|
301
518
|
key: skill.name,
|
|
302
519
|
type: "button",
|
|
303
|
-
|
|
520
|
+
className: "evo-hist-skill",
|
|
521
|
+
"aria-current": skill.name === selected ? "true" : void 0,
|
|
304
522
|
onClick: () => {
|
|
305
523
|
open(skill.name);
|
|
306
524
|
}
|
|
307
|
-
}, skill.name
|
|
525
|
+
}, (0, react.createElement)("span", { className: "evo-hist-skill-line" }, (0, react.createElement)("span", { className: "evo-hist-skill-name" }, skill.name), (0, react.createElement)("span", { className: "evo-hist-skill-count" }, String(skill.versions) + " " + face.t("versions.count"))), (0, react.createElement)("span", { className: "evo-hist-skill-desc" }, oneLine(skill.description) + " · " + face.t(stateKey(skill))))))), (0, react.createElement)("div", { className: "evo-hist-main" }, (0, react.createElement)("div", { className: "evo-hist-head" }, (0, react.createElement)("h2", { className: "evo-hist-title" }, face.t("title")), (0, react.createElement)("button", {
|
|
308
526
|
type: "button",
|
|
309
|
-
|
|
527
|
+
className: "evo-hist-button",
|
|
310
528
|
onClick: refresh
|
|
311
|
-
}, face.t("refresh"))), (0, react.createElement)("p", {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}, face.t("group.content"), payload.content), group({
|
|
316
|
-
...face,
|
|
317
|
-
restore,
|
|
318
|
-
confirming: pending
|
|
319
|
-
}, face.t("group.support"), payload.support, face.t("group.support.note")), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { style: hint }, face.t("empty.versions")) : null) : null));
|
|
529
|
+
}, face.t("refresh"))), (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("hint") + " " + face.t("hint.current")), noteText === void 0 ? null : (0, react.createElement)("p", {
|
|
530
|
+
className: "evo-hist-note",
|
|
531
|
+
"data-error": noteError ? "true" : void 0
|
|
532
|
+
}, noteText), selected === void 0 ? null : loaded ? null : (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("loading")), loaded ? (0, react.createElement)("div", null, group(face, face.t("group.content"), payload.content, void 0, rowState), group(face, face.t("group.support"), payload.support, face.t("group.support.note"), rowState), payload.content.length === 0 && payload.support.length === 0 ? (0, react.createElement)("p", { className: "evo-hist-hint" }, face.t("empty.versions")) : null) : null));
|
|
320
533
|
}
|
|
321
534
|
//#endregion
|
|
322
535
|
//#region src/client/seam.ts
|
|
@@ -337,9 +550,30 @@ window.__ModuleLoader__.load({
|
|
|
337
550
|
const inject = ["slots", "locale"];
|
|
338
551
|
/** Row order inside the global panel list (beside the platform's own rows). */
|
|
339
552
|
const PANEL_ORDER = 35;
|
|
340
|
-
/**
|
|
553
|
+
/**
|
|
554
|
+
* The row's icon: a platform-style line glyph rather than an emoji.
|
|
555
|
+
*
|
|
556
|
+
* An emoji cannot follow `currentColor`, cannot match the 16/18px the sidebar hands its glyph slot,
|
|
557
|
+
* and renders differently on every platform font — the sidebar's other rows are 1.5px-stroke line
|
|
558
|
+
* icons, and this one now is too. The label carries the accessible name, so the glyph stays
|
|
559
|
+
* decoration.
|
|
560
|
+
*/
|
|
341
561
|
function PanelIcon() {
|
|
342
|
-
return (0, react.createElement)("
|
|
562
|
+
return (0, react.createElement)("svg", {
|
|
563
|
+
"aria-hidden": "true",
|
|
564
|
+
width: "100%",
|
|
565
|
+
height: "100%",
|
|
566
|
+
viewBox: "0 0 16 16",
|
|
567
|
+
fill: "none",
|
|
568
|
+
stroke: "currentColor",
|
|
569
|
+
strokeWidth: "1.5",
|
|
570
|
+
strokeLinecap: "round",
|
|
571
|
+
strokeLinejoin: "round"
|
|
572
|
+
}, (0, react.createElement)("circle", {
|
|
573
|
+
cx: "8",
|
|
574
|
+
cy: "8",
|
|
575
|
+
r: "5.75"
|
|
576
|
+
}), (0, react.createElement)("path", { d: "M8 4.75V8l2.25 1.5" }));
|
|
343
577
|
}
|
|
344
578
|
/**
|
|
345
579
|
* Register the locale namespace, the panel row and the panel body.
|
|
@@ -355,6 +589,18 @@ window.__ModuleLoader__.load({
|
|
|
355
589
|
});
|
|
356
590
|
}, "evolution-skill-history: locale");
|
|
357
591
|
const t = seam.locale.bind(NS);
|
|
592
|
+
ctx.effect(() => {
|
|
593
|
+
if (document.querySelector("style[data-plugin-css=\"@lmzhen/dsh-evolution-skill-history/panel.css\"]") === null) {
|
|
594
|
+
const tag = document.createElement("style");
|
|
595
|
+
tag.setAttribute("data-plugin-css", CSS_TAG_ID);
|
|
596
|
+
tag.textContent = CSS;
|
|
597
|
+
document.head.append(tag);
|
|
598
|
+
return () => {
|
|
599
|
+
tag.remove();
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return () => {};
|
|
603
|
+
}, "evolution-skill-history: styles");
|
|
358
604
|
seam.slots.inject("sidebar.panellist", function* () {
|
|
359
605
|
yield seam.slots.register({
|
|
360
606
|
name: "sidebar.panellist",
|
|
@@ -367,8 +613,10 @@ window.__ModuleLoader__.load({
|
|
|
367
613
|
key: PANEL_ID,
|
|
368
614
|
inject: () => ({
|
|
369
615
|
t,
|
|
616
|
+
format: (key, values) => fill(t(key), values),
|
|
370
617
|
loadSkills: api.skills,
|
|
371
618
|
loadVersions: api.versions,
|
|
619
|
+
loadDiff: api.diff,
|
|
372
620
|
undo: api.undo
|
|
373
621
|
})
|
|
374
622
|
}, SkillHistoryPanel);
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { contentHash, errorText, partitionVersions } from "@lmzhen/dsh-evolution-core";
|
|
1
|
+
import { contentHash, elapsedSince, errorText, partitionVersions, textDiffFacts, versionActionKind } from "@lmzhen/dsh-evolution-core";
|
|
2
2
|
//#region lib/types/routes.js
|
|
3
3
|
/**
|
|
4
|
-
* The skill-history HTTP surface:
|
|
4
|
+
* The skill-history HTTP surface: five loopback routes over the curator's read/write seam (four reads plus the one write).
|
|
5
5
|
*
|
|
6
6
|
* Layering: this module maps a route to a USE CASE and does nothing else. "Which artifact does this
|
|
7
7
|
* version hold" and "how do the two chains split" live in evolution-core; the only write is
|
|
@@ -12,6 +12,7 @@ import { contentHash, errorText, partitionVersions } from "@lmzhen/dsh-evolution
|
|
|
12
12
|
const SKILL_HISTORY_ROUTES = {
|
|
13
13
|
skills: "/api/dsh-evolution/skill-history/skills",
|
|
14
14
|
versions: "/api/dsh-evolution/skill-history/versions",
|
|
15
|
+
diff: "/api/dsh-evolution/skill-history/versions/diff",
|
|
15
16
|
undo: "/api/dsh-evolution/skill-history/undo",
|
|
16
17
|
health: "/api/dsh-evolution/skill-history/health"
|
|
17
18
|
};
|
|
@@ -140,9 +141,14 @@ function makeSkillHistoryRoutes(services) {
|
|
|
140
141
|
const withHistory = [];
|
|
141
142
|
for (const skill of listed) {
|
|
142
143
|
const versions = await curator.skills.listVersions(skill.name);
|
|
143
|
-
if (versions.length
|
|
144
|
+
if (versions.length === 0) continue;
|
|
145
|
+
withHistory.push({
|
|
144
146
|
name: skill.name,
|
|
145
|
-
versions: versions.length
|
|
147
|
+
versions: versions.length,
|
|
148
|
+
description: skill.description,
|
|
149
|
+
managed: skill.managed,
|
|
150
|
+
protectedBy: skill.protectedBy,
|
|
151
|
+
protectionUnknown: skill.protectionUnknown
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
writeJson(res, 200, {
|
|
@@ -170,20 +176,82 @@ function makeSkillHistoryRoutes(services) {
|
|
|
170
176
|
const groups = partitionVersions(await curator.history(name));
|
|
171
177
|
const live = await curator.skills.read(name);
|
|
172
178
|
const liveHash = live === null ? null : contentHash(live);
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
const decorate = (entry, index, chain, withDelta) => {
|
|
181
|
+
const previous = index === 0 ? void 0 : chain[index - 1];
|
|
182
|
+
return {
|
|
183
|
+
...entry,
|
|
184
|
+
actionKind: versionActionKind(entry.action),
|
|
185
|
+
age: elapsedSince(entry.at, now),
|
|
186
|
+
...!withDelta || previous === void 0 ? {} : { charsDelta: entry.chars - previous.chars }
|
|
187
|
+
};
|
|
188
|
+
};
|
|
173
189
|
writeJson(res, 200, {
|
|
174
190
|
ok: true,
|
|
175
191
|
data: {
|
|
176
|
-
content: groups.content.map((entry) => ({
|
|
177
|
-
...entry,
|
|
192
|
+
content: groups.content.map((entry, index) => ({
|
|
193
|
+
...decorate(entry, index, groups.content, true),
|
|
178
194
|
undoable: entry.hash !== liveHash
|
|
179
195
|
})),
|
|
180
|
-
support: groups.support,
|
|
196
|
+
support: groups.support.map((entry, index) => decorate(entry, index, groups.support, false)),
|
|
181
197
|
liveHash
|
|
182
198
|
}
|
|
183
199
|
});
|
|
184
200
|
});
|
|
185
201
|
}
|
|
186
202
|
},
|
|
203
|
+
{
|
|
204
|
+
kind: "exact",
|
|
205
|
+
path: SKILL_HISTORY_ROUTES.diff,
|
|
206
|
+
handler: async (req, res) => {
|
|
207
|
+
if (!guard(req, res, "GET")) return;
|
|
208
|
+
const params = new URL(req.url ?? "/", "http://loopback").searchParams;
|
|
209
|
+
const name = params.get("name")?.trim() ?? "";
|
|
210
|
+
const rawV = params.get("v")?.trim() ?? "";
|
|
211
|
+
const v = Number.parseInt(rawV, 10);
|
|
212
|
+
if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) {
|
|
213
|
+
writeJson(res, 400, {
|
|
214
|
+
ok: false,
|
|
215
|
+
code: "bad-request",
|
|
216
|
+
message: "name and a positive integer v are required"
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
await withCurator(res, async (curator) => {
|
|
221
|
+
const groups = partitionVersions(await curator.history(name));
|
|
222
|
+
const index = groups.content.findIndex((entry) => entry.v === v);
|
|
223
|
+
if (index === -1) {
|
|
224
|
+
writeJson(res, 404, {
|
|
225
|
+
ok: false,
|
|
226
|
+
code: "not-found",
|
|
227
|
+
message: "that version is not in the recorded history of \"" + name + "\""
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const entry = groups.content[index];
|
|
232
|
+
const previous = (entry === void 0 || entry.beforeHash === void 0 ? void 0 : groups.content.find((candidate) => candidate.hash === entry.beforeHash)) ?? (index === 0 ? void 0 : groups.content[index - 1]);
|
|
233
|
+
const after = entry === void 0 ? null : await curator.skills.readVersion(name, v);
|
|
234
|
+
const before = previous === void 0 ? "" : await curator.skills.readVersion(name, previous.v);
|
|
235
|
+
if (after === null || before === null) {
|
|
236
|
+
writeJson(res, 404, {
|
|
237
|
+
ok: false,
|
|
238
|
+
code: "not-found",
|
|
239
|
+
message: "the stored content of that version could not be read"
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const facts = textDiffFacts(before, after, "SKILL.md");
|
|
244
|
+
writeJson(res, 200, {
|
|
245
|
+
ok: true,
|
|
246
|
+
data: {
|
|
247
|
+
v,
|
|
248
|
+
against: previous?.v ?? null,
|
|
249
|
+
...facts
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
},
|
|
187
255
|
{
|
|
188
256
|
kind: "exact",
|
|
189
257
|
path: SKILL_HISTORY_ROUTES.undo,
|
|
@@ -2,29 +2,32 @@
|
|
|
2
2
|
* The skill-history panel: the skills that recorded versions, the two chains of the selected skill,
|
|
3
3
|
* and one action per body version (restore, behind an inline confirmation).
|
|
4
4
|
*
|
|
5
|
-
* The panel owns no rule: the host reports each
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
5
|
+
* The panel owns no rule and no arithmetic: the host reports each row with its whole verdict
|
|
6
|
+
* (`undoable`, the vocabulary key `actionKind`, the `age` bucket, `charsDelta`, `summary`) and this
|
|
7
|
+
* file substitutes WORDS through the locale dictionary and renders. It never subscribes to anything
|
|
8
|
+
* and never sees the context.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* version's action within reach of the version it acts on.
|
|
10
|
+
* State rules, each pinned by a live pass or a component spec: a restore's result sentence stays on
|
|
11
|
+
* screen while the rows are read again; nothing is read on a poll (mount, opening a skill, the
|
|
12
|
+
* refresh button, after a restore); a late reply for a skill the operator has already left is
|
|
13
|
+
* discarded by its ticket; and the diff is fetched only when a row is expanded.
|
|
14
14
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
15
15
|
*/
|
|
16
16
|
import { type ReactNode } from 'react';
|
|
17
|
-
import type { SkillRow, VersionsPayload } from './api.ts';
|
|
18
|
-
/** The face the plugin hands the component: copy plus
|
|
17
|
+
import type { SkillRow, VersionDiffRow, VersionsPayload } from './api.ts';
|
|
18
|
+
/** The face the plugin hands the component: copy plus callbacks, never a service handle. */
|
|
19
19
|
export interface PanelFace {
|
|
20
20
|
readonly t: (key: string) => string;
|
|
21
|
+
/** Fill one copy key's `{n}` slots (the dictionaries own the sentences). */
|
|
22
|
+
readonly format: (key: string, values: Record<string, string | number>) => string;
|
|
21
23
|
readonly loadSkills: () => Promise<readonly SkillRow[]>;
|
|
22
24
|
readonly loadVersions: (name: string) => Promise<VersionsPayload>;
|
|
25
|
+
readonly loadDiff: (name: string, v: number) => Promise<VersionDiffRow>;
|
|
23
26
|
readonly undo: (name: string, v?: number) => Promise<string>;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* The panel body.
|
|
27
|
-
* @param face - copy
|
|
30
|
+
* @param face - copy plus the data callbacks.
|
|
28
31
|
* @returns the panel element.
|
|
29
32
|
*/
|
|
30
33
|
export declare function SkillHistoryPanel(face: PanelFace): ReactNode;
|
|
@@ -10,8 +10,25 @@
|
|
|
10
10
|
export declare const HOST_ROUTES: {
|
|
11
11
|
readonly skills: "/api/dsh-evolution/skill-history/skills";
|
|
12
12
|
readonly versions: "/api/dsh-evolution/skill-history/versions";
|
|
13
|
+
readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
|
|
13
14
|
readonly undo: "/api/dsh-evolution/skill-history/undo";
|
|
14
15
|
};
|
|
16
|
+
/** One changed region, as the host reports it (the platform diff card's own shape). */
|
|
17
|
+
export interface DiffHunkRow {
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly oldText: string;
|
|
20
|
+
readonly newText: string;
|
|
21
|
+
}
|
|
22
|
+
/** One version's diff, as the host computes it. */
|
|
23
|
+
export interface VersionDiffRow {
|
|
24
|
+
readonly v: number;
|
|
25
|
+
/** The version this one replaced, or null for the first recorded version. */
|
|
26
|
+
readonly against: number | null;
|
|
27
|
+
readonly linesAdded: number;
|
|
28
|
+
readonly linesRemoved: number;
|
|
29
|
+
readonly hunks: readonly DiffHunkRow[];
|
|
30
|
+
readonly truncated: boolean;
|
|
31
|
+
}
|
|
15
32
|
/** One recorded version, as the host reports it. */
|
|
16
33
|
export interface VersionRow {
|
|
17
34
|
readonly v: number;
|
|
@@ -21,11 +38,26 @@ export interface VersionRow {
|
|
|
21
38
|
readonly chars: number;
|
|
22
39
|
/** Present on body rows: false when this version already IS the live content. */
|
|
23
40
|
readonly undoable?: boolean;
|
|
41
|
+
/** Characters this version added to (or removed from) the one before it; absent on the first. */
|
|
42
|
+
readonly charsDelta?: number;
|
|
43
|
+
/** Which kind of write produced it (the vocabulary key the panel turns into words). */
|
|
44
|
+
readonly actionKind: string;
|
|
45
|
+
/** How long ago the host recorded it, as a bucket plus a count — never a sentence. */
|
|
46
|
+
readonly age: {
|
|
47
|
+
readonly unit: string;
|
|
48
|
+
readonly n: number;
|
|
49
|
+
};
|
|
50
|
+
/** The one line the optional summarizer wrote, when the deployment turned it on. */
|
|
51
|
+
readonly summary?: string;
|
|
24
52
|
}
|
|
25
|
-
/** One skill that has recorded versions. */
|
|
53
|
+
/** One skill that has recorded versions, with what the listing already knows about it. */
|
|
26
54
|
export interface SkillRow {
|
|
27
55
|
readonly name: string;
|
|
28
56
|
readonly versions: number;
|
|
57
|
+
readonly description: string;
|
|
58
|
+
readonly managed: boolean;
|
|
59
|
+
readonly protectedBy: string | null;
|
|
60
|
+
readonly protectionUnknown: boolean;
|
|
29
61
|
}
|
|
30
62
|
/** The two chains, plus the live body hash the panel marks as "current". */
|
|
31
63
|
export interface VersionsPayload {
|
|
@@ -38,6 +70,7 @@ export interface SkillHistoryApi {
|
|
|
38
70
|
readonly skills: () => Promise<readonly SkillRow[]>;
|
|
39
71
|
readonly versions: (name: string) => Promise<VersionsPayload>;
|
|
40
72
|
readonly undo: (name: string, v?: number) => Promise<string>;
|
|
73
|
+
readonly diff: (name: string, v: number) => Promise<VersionDiffRow>;
|
|
41
74
|
}
|
|
42
75
|
/** A refusal the host reported, carrying its own sentence (the curator's, not ours). */
|
|
43
76
|
export declare class SkillHistoryRefusal extends Error {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Panel copy, in a zh/en dictionary registered through the platform
|
|
2
|
+
* Panel copy, in a zh/en dictionary registered through the platform’s locale seat.
|
|
3
3
|
*
|
|
4
|
-
* Client copy is locale-owned: the entry label, the group headings, the actions
|
|
5
|
-
*
|
|
4
|
+
* Client copy is locale-owned: the entry label, the group headings, the actions, the version facts
|
|
5
|
+
* (action words, relative time, deltas) and the status lines all resolve here, and the panel carries
|
|
6
|
+
* no literal string of its own. Facts come from the host; only the WORDS live here.
|
|
6
7
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
7
8
|
*/
|
|
8
9
|
/** The locale namespace this bundle registers. */
|
|
@@ -18,4 +19,12 @@ export declare const en: Record<string, string>;
|
|
|
18
19
|
* @returns the copy, or the key itself when neither dictionary carries it.
|
|
19
20
|
*/
|
|
20
21
|
export declare function message(language: 'zh' | 'en', key: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Fill one TEMPLATE’s `{n}` slots. The seat resolves the key, this fills it, so the panel needs
|
|
24
|
+
* only the seat plus this rule and never learns which language it is rendering.
|
|
25
|
+
* @param template - the copy, with `{slot}` placeholders.
|
|
26
|
+
* @param values - the substitutions, keyed by the slot name inside the braces.
|
|
27
|
+
* @returns the filled copy.
|
|
28
|
+
*/
|
|
29
|
+
export declare function fill(template: string, values: Record<string, string | number>): string;
|
|
21
30
|
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The panel’s stylesheet, injected once behind a `<style data-plugin-css=…>` tag.
|
|
3
|
+
*
|
|
4
|
+
* The bundle is built outside the platform’s CSS-Modules pipeline, so it carries its own string and
|
|
5
|
+
* injects it the same way the family’s settings section does. Every rule reads the platform’s design
|
|
6
|
+
* tokens, and the metrics are COPIED from the platform’s own components rather than invented:
|
|
7
|
+
*
|
|
8
|
+
* - the code block (`--dsw-font-markdown-code-block-small`, `.5px` border, `12px`, radius `12px`,
|
|
9
|
+
* `max-height: 260px`) is the platform command card’s `<pre>` (client/ui-chat/…/GenericCommandCard.module.css);
|
|
10
|
+
* - hover / focus (`--dsw-alias-interactive-bg-hover`, a 2px inset focus ring) follow the platform
|
|
11
|
+
* sidebar row (client/ui-sidebar/…/SidebarRoot.module.css);
|
|
12
|
+
* - the panel’s type scale is the platform TOKEN rather than a magic number, so it follows the
|
|
13
|
+
* deployment’s content size the way the platform’s own panels do;
|
|
14
|
+
* - the capsule and the field metrics follow the family’s settings card, which copied them from the
|
|
15
|
+
* platform’s settings fields in the first place.
|
|
16
|
+
*
|
|
17
|
+
* Geometry (widths, paddings, radii, the 260px scroll cap) is a plain number here because the platform's
|
|
18
|
+
* own components write geometry the same way; what must never be a literal is TYPE and COLOUR, and
|
|
19
|
+
* every size and colour below is a token or a `calc()` over one.
|
|
20
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
21
|
+
*/
|
|
22
|
+
/** Tag id that makes the injection idempotent. */
|
|
23
|
+
export declare const CSS_TAG_ID = "@lmzhen/dsh-evolution-skill-history/panel.css";
|
|
24
|
+
/** The stylesheet the panel injects once. */
|
|
25
|
+
export declare const CSS: string;
|
|
26
|
+
//# sourceMappingURL=styles.d.ts.map
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The skill-history HTTP surface:
|
|
2
|
+
* The skill-history HTTP surface: five loopback routes over the curator's read/write seam (four reads plus the one write).
|
|
3
3
|
*
|
|
4
4
|
* Layering: this module maps a route to a USE CASE and does nothing else. "Which artifact does this
|
|
5
5
|
* version hold" and "how do the two chains split" live in evolution-core; the only write is
|
|
@@ -13,6 +13,7 @@ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
|
13
13
|
export declare const SKILL_HISTORY_ROUTES: {
|
|
14
14
|
readonly skills: "/api/dsh-evolution/skill-history/skills";
|
|
15
15
|
readonly versions: "/api/dsh-evolution/skill-history/versions";
|
|
16
|
+
readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
|
|
16
17
|
readonly undo: "/api/dsh-evolution/skill-history/undo";
|
|
17
18
|
readonly health: "/api/dsh-evolution/skill-history/health";
|
|
18
19
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-skill-history",
|
|
3
3
|
"description": "Web surface for skill content history: loopback host routes plus the sidebar panel (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,21 +30,21 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
33
|
+
"@lmzhen/dsh-evolution-core": "^0.13.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
37
37
|
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
38
38
|
"react": "^18.2.0",
|
|
39
|
-
"@lmzhen/dsh-evolution-curator": "^0.
|
|
39
|
+
"@lmzhen/dsh-evolution-curator": "^0.13.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
43
43
|
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
44
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
45
|
-
"@lmzhen/dsh-evolution-curator": "^0.
|
|
46
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
47
|
-
"@lmzhen/dsh-evolution-io-node": "^0.
|
|
44
|
+
"@lmzhen/dsh-evolution-core": "^0.13.0",
|
|
45
|
+
"@lmzhen/dsh-evolution-curator": "^0.13.0",
|
|
46
|
+
"@lmzhen/dsh-evolution-io": "^0.13.0",
|
|
47
|
+
"@lmzhen/dsh-evolution-io-node": "^0.13.0"
|
|
48
48
|
},
|
|
49
49
|
"dsh": {
|
|
50
50
|
"client": {
|