@lmzhen/dsh-evolution-skill-history 0.11.2 → 0.12.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 CHANGED
@@ -32,7 +32,7 @@ The row is **inert without a web server** (`ctx.get('webServer')`, not a declare
32
32
 
33
33
  ## What the panel shows
34
34
 
35
- 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 offers **Restore this version** unless it already IS the live content (the host marks those), and the button turns into an inline confirmation first. After a restore the list is read again, because the restore itself is a new version.
35
+ 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 offers **Restore this version** unless it already IS the live content (the host marks those), and the button turns into an inline confirmation first. 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; before it, seeing what another writer recorded in the meantime meant opening the panel again for the list, or clicking the open skill again for its rows (the aside counts are read with the list, not per row).
36
36
 
37
37
  ## Build
38
38
 
@@ -44,7 +44,7 @@ This package adds **no model-visible content**: no prompt section, no tool schem
44
44
 
45
45
  ## Known limitations and deferred work
46
46
 
47
- - **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.
47
+ - **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
48
  - **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
49
  - **No diff view.** The routes return entries, not bodies; a two-version diff would need a second read route and a client-side differ.
50
50
  - **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.
package/lib/client.js CHANGED
@@ -110,12 +110,19 @@ window.__ModuleLoader__.load({
110
110
  * The panel owns no rule: the host reports each body row with a verdict (`undoable`) computed from
111
111
  * the version facts in evolution-core, and the curator owns the words a refusal uses. This file is
112
112
  * presentation plus local state — it never subscribes to anything and it never sees the context.
113
+ *
114
+ * Two state rules, both pinned by a live pass rather than by a test (a browser half has no unit test
115
+ * seat in this family): a restore's result sentence stays on screen while the rows are read again, and
116
+ * nothing is read on a poll — the skill list is read on mount and by the refresh button, the rows when
117
+ * a skill opens, when the refresh button is pressed and after a restore. The width cap keeps a
118
+ * version's action within reach of the version it acts on.
113
119
  * @module @lmzhen/dsh-evolution-skill-history/client
114
120
  */
115
121
  const root = {
116
122
  display: "flex",
117
123
  height: "100%",
118
124
  minHeight: "0",
125
+ maxWidth: "960px",
119
126
  fontSize: "13px",
120
127
  color: "var(--dsw-alias-label-primary)"
121
128
  };
@@ -132,6 +139,12 @@ window.__ModuleLoader__.load({
132
139
  overflowY: "auto",
133
140
  padding: "12px 14px"
134
141
  };
142
+ const header = {
143
+ display: "flex",
144
+ alignItems: "center",
145
+ gap: "10px",
146
+ margin: "0 0 4px"
147
+ };
135
148
  const skillButton = {
136
149
  display: "block",
137
150
  width: "100%",
@@ -149,9 +162,10 @@ window.__ModuleLoader__.load({
149
162
  fontWeight: 600
150
163
  };
151
164
  const heading = {
152
- margin: "0 0 4px",
165
+ margin: "0",
153
166
  fontSize: "13px",
154
- fontWeight: 600
167
+ fontWeight: 600,
168
+ flex: "1 1 auto"
155
169
  };
156
170
  const hint = {
157
171
  margin: "0 0 10px",
@@ -229,19 +243,42 @@ window.__ModuleLoader__.load({
229
243
  const [payload, setPayload] = (0, react.useState)(void 0);
230
244
  const [noteText, setNoteText] = (0, react.useState)(void 0);
231
245
  const [pending, setPending] = (0, react.useState)(void 0);
246
+ const readTicket = (0, react.useRef)(0);
247
+ const failureText = (error) => face.t("error") + ": " + (error instanceof Error ? error.message : String(error));
232
248
  const failed = (error) => {
233
- setNoteText(face.t("error") + ": " + (error instanceof Error ? error.message : String(error)));
249
+ setNoteText(failureText(error));
234
250
  };
235
- const refresh = () => {
236
- setSkills([]);
251
+ /** Read the skills again, keeping the sentence: a re-read is not an answer to anything. */
252
+ const reloadSkills = () => {
237
253
  face.loadSkills().then(setSkills).catch(failed);
238
254
  };
239
- const open = (name) => {
255
+ /**
256
+ * Show one skill's chains, keeping the sentence. A re-read that FAILS reports through the caller's
257
+ * own handler, so the failure of a read can never erase the write it was reading after.
258
+ * @param name - the skill to open.
259
+ * @param onFailure - what to do with a failed read; the panel shows it as its own sentence.
260
+ */
261
+ const showVersions = (name, onFailure = failed) => {
262
+ const ticket = readTicket.current + 1;
263
+ readTicket.current = ticket;
240
264
  setSelected(name);
241
265
  setPayload(void 0);
242
266
  setPending(void 0);
243
- setNoteText(void 0);
244
- face.loadVersions(name).then(setPayload).catch(failed);
267
+ face.loadVersions(name).then((next) => {
268
+ if (readTicket.current === ticket) setPayload(next);
269
+ }).catch((error) => {
270
+ if (readTicket.current === ticket) onFailure(error);
271
+ });
272
+ };
273
+ /** Open a skill from the list: another skill's sentence described another skill, so it goes away. */
274
+ const open = (name) => {
275
+ if (name !== selected) setNoteText(void 0);
276
+ showVersions(name);
277
+ };
278
+ /** What the refresh button does: both reads again, and the sentence stays. */
279
+ const refresh = () => {
280
+ reloadSkills();
281
+ if (selected !== void 0) showVersions(selected);
245
282
  };
246
283
  const restore = (v) => {
247
284
  if (selected === void 0) return;
@@ -250,12 +287,15 @@ window.__ModuleLoader__.load({
250
287
  return;
251
288
  }
252
289
  setPending(void 0);
253
- face.undo(selected, v).then((message) => {
290
+ const name = selected;
291
+ face.undo(name, v).then((message) => {
292
+ showVersions(name, (error) => {
293
+ setNoteText(message + "\n" + failureText(error));
294
+ });
254
295
  setNoteText(message);
255
- open(selected);
256
296
  }).catch(failed);
257
297
  };
258
- (0, react.useEffect)(refresh, []);
298
+ (0, react.useEffect)(reloadSkills, []);
259
299
  const loaded = payload !== void 0;
260
300
  return (0, react.createElement)("div", { style: root }, (0, react.createElement)("aside", { style: aside }, skills.length === 0 ? (0, react.createElement)("p", { style: hint }, face.t("empty.skills")) : skills.map((skill) => (0, react.createElement)("button", {
261
301
  key: skill.name,
@@ -264,7 +304,11 @@ window.__ModuleLoader__.load({
264
304
  onClick: () => {
265
305
  open(skill.name);
266
306
  }
267
- }, skill.name + " (" + String(skill.versions) + ")"))), (0, react.createElement)("div", { style: main }, (0, react.createElement)("h2", { style: heading }, face.t("title")), (0, react.createElement)("p", { style: hint }, face.t("hint")), noteText === void 0 ? null : (0, react.createElement)("p", { style: note }, noteText), selected === void 0 ? null : loaded ? null : (0, react.createElement)("p", { style: hint }, face.t("loading")), loaded ? (0, react.createElement)("div", null, group({
307
+ }, skill.name + " (" + String(skill.versions) + ")"))), (0, react.createElement)("div", { style: main }, (0, react.createElement)("div", { style: header }, (0, react.createElement)("h2", { style: heading }, face.t("title")), (0, react.createElement)("button", {
308
+ type: "button",
309
+ style: action,
310
+ onClick: refresh
311
+ }, face.t("refresh"))), (0, react.createElement)("p", { style: hint }, face.t("hint")), noteText === void 0 ? null : (0, react.createElement)("p", { style: note }, noteText), selected === void 0 ? null : loaded ? null : (0, react.createElement)("p", { style: hint }, face.t("loading")), loaded ? (0, react.createElement)("div", null, group({
268
312
  ...face,
269
313
  restore,
270
314
  confirming: pending
@@ -5,6 +5,12 @@
5
5
  * The panel owns no rule: the host reports each body row with a verdict (`undoable`) computed from
6
6
  * the version facts in evolution-core, and the curator owns the words a refusal uses. This file is
7
7
  * presentation plus local state — it never subscribes to anything and it never sees the context.
8
+ *
9
+ * Two state rules, both pinned by a live pass rather than by a test (a browser half has no unit test
10
+ * seat in this family): a restore's result sentence stays on screen while the rows are read again, and
11
+ * nothing is read on a poll — the skill list is read on mount and by the refresh button, the rows when
12
+ * a skill opens, when the refresh button is pressed and after a restore. The width cap keeps a
13
+ * version's action within reach of the version it acts on.
8
14
  * @module @lmzhen/dsh-evolution-skill-history/client
9
15
  */
10
16
  import { type ReactNode } from 'react';
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.11.2",
4
+ "version": "0.12.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.11.2"
33
+ "@lmzhen/dsh-evolution-core": "^0.12.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.11.2"
39
+ "@lmzhen/dsh-evolution-curator": "^0.12.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.11.2",
45
- "@lmzhen/dsh-evolution-curator": "^0.11.2",
46
- "@lmzhen/dsh-evolution-io": "^0.11.2",
47
- "@lmzhen/dsh-evolution-io-node": "^0.11.2"
44
+ "@lmzhen/dsh-evolution-core": "^0.12.0",
45
+ "@lmzhen/dsh-evolution-curator": "^0.12.0",
46
+ "@lmzhen/dsh-evolution-io": "^0.12.0",
47
+ "@lmzhen/dsh-evolution-io-node": "^0.12.0"
48
48
  },
49
49
  "dsh": {
50
50
  "client": {