@lmzhen/dsh-evolution-skill-history 0.13.6 → 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 +16 -7
- package/lib/client.js +307 -28
- package/lib/index.js +53 -3
- package/lib/types/client/Panel.d.ts +5 -1
- package/lib/types/client/api.d.ts +23 -0
- package/lib/types/client/markdown.d.ts +23 -0
- package/lib/types/routes.d.ts +7 -1
- package/package.json +9 -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 six 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
|
|
|
@@ -23,6 +23,7 @@ The label is a **thunk**: the sidebar re-reads it on every projection, so a lang
|
|
|
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
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 |
|
|
26
|
+
| GET | `/api/dsh-evolution/skill-history/versions/body?name=&v=` | one version's whole text, for the panel's preview; bounded by `MAX_BODY_CHARS` and flagged `truncated` |
|
|
26
27
|
| GET | `/api/dsh-evolution/skill-history/health` | liveness probe for the client |
|
|
27
28
|
|
|
28
29
|
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.
|
|
@@ -33,15 +34,23 @@ The row is **inert without a web server** (`ctx.get('webServer')`, not a declare
|
|
|
33
34
|
|
|
34
35
|
## What the panel shows
|
|
35
36
|
|
|
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, clipped with an ellipsis (the listing already carried it)
|
|
37
|
+
**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, clipped with an ellipsis (the listing already carried it), the management state the marker probe reported and — on the description line, where it cannot squeeze the name — **when the content last changed**. That last fact comes from the index this route already reads (`latestVersionAt` across both chains); *last use* is a different fact and stays in the usage store. A **search box** filters that list by name or description — client state only, no route behind it.
|
|
37
38
|
|
|
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 3 hours ago (+1363 chars)`, where the absolute clock is the row's hover text — with a *current* capsule on the live content, **Restore this version** (an inline two-click confirmation, with a **Cancel** beside it once it is armed) 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
|
+
**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 support file's bytes land in the second group even when the entry wears the `baseline` label — a support write's predecessor is that FILE's previous content, not a body version — because the entry's `path` says which artifact it holds, and for an entry recorded before that field existed the link the support write left behind (`beforeHash`) says it instead. A body row reads like a sentence rather than a dump — `v3 targeted edit 3 hours ago (+1363 chars)`, where the absolute clock is the row's hover text — with a *current* capsule on the live content, **Restore this version** (an inline two-click confirmation, with a **Cancel** beside it once it is armed) 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). A support row names the file it holds (`references/notes.md`) once the entry records it, and says so plainly when it does not — entries written before the path was recorded have none, and the blob store cannot recover the name. 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.
|
|
40
|
+
|
|
41
|
+
**Preview** renders one version as a document, support files included (the body route above). Inside the diff view, **Source / Rendered** switches between the exact window the host computed and the same two sides rendered as Markdown — removed first, added second, each under a tinted edge. Block level on purpose: line-level interleaving of two rendered documents is a different, much larger problem, and the source view stays one click away. Only one row is expanded at a time (`expanded: {v, kind}`), and each kind keeps its own lazily-filled cache.
|
|
39
42
|
|
|
40
43
|
While nothing is selected the pane says so — a line pointing at the left column, or the empty listing when no skill has a history yet.
|
|
41
44
|
|
|
42
|
-
**The panel owns no rule and no arithmetic.** The host reports every row with its whole verdict — `undoable` (evolution-core's
|
|
45
|
+
**The panel owns no rule and no arithmetic.** The host reports every row with its whole verdict — `undoable` (evolution-core's `entryTarget` 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.
|
|
46
|
+
|
|
47
|
+
**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, and the theme's semantic state pair (`--dsw-alias-state-success-primary` / `--dsw-alias-state-error-primary`) for the diff's added and removed lines — while type follows the shell rather than the conversation: the panel is one relative step below the shell's chrome size (`calc(1em - 1px)`, never the conversation-content setting) and steps again inside itself — `calc(1em + 2px)` for the title, `calc(1em - 1px)` for a group label. Only the diff block takes the platform's code-block token — it renders content, not chrome. The setting behind `--dsh-content-font-size*` is documented as affecting conversation content only, so the panel deliberately does not follow it. Two platform seams are reached differently, and the difference is deliberate. **Controls and styles are drawn here**, because a STATIC import of the platform's control package cannot compile from this tree: the workspace tsconfig maps the specifier to platform sources (`TS6059`/`TS6307`) and this package's `react.d.ts` shadows the real React types (`TS2305`); an ambient `declare module` for that specifier loses to the paths mapping. **The Markdown renderer is borrowed at runtime instead**: `src/client/markdown.ts` is the one seam, taking `MarkdownText` from `@deepseek-ai/dsh-client-ui-primitives` — a `PLATFORM_MODULES` row every dynamic bundle can `require`, exactly like `react` — through the loader-provided `require` (the bundler keeps `@deepseek-ai/*` external, so nothing is inlined and no dependency is installed). It renders with the platform's own mdast pipeline and the shell's own stylesheet, and when the row is absent the seam reports `{ok: false}` and every caller falls back to plain text: a missing renderer degrades a view, never the panel. `verify-arch-guards` rule **N24** keeps the drawn half honest — no literal `font-size`/`font`/`font-family` and no literal colour under `src/client`, only `var(--…)`, `calc(…)` or a CSS-wide keyword. The roll call of what the panel deliberately does not do is in `## Known limitations and deferred work`.
|
|
48
|
+
|
|
49
|
+
## Where the boundaries are
|
|
50
|
+
|
|
51
|
+
**Restore is confirmed here and deliberately not in the CLI.** The panel's only write is a two-click confirmation with its own Cancel; `/evolution skill undo` keeps no prompt at all, because typing the command IS the intent and the face has to stay scriptable. That asymmetry is a decision, not an oversight — it is written down so the question does not reopen.
|
|
43
52
|
|
|
44
|
-
**
|
|
53
|
+
**Browsing belongs to the skill centre; versions and restore belong here.** The panel lists the skills that recorded versions and shows nothing about curation or lifecycle beyond the marker facts the listing already carries, so it cannot grow into a second skill-management surface. The third-party skill browser (`dsh-client-ui-skill-explorer`) covers discovery; this panel covers what changed and how to go back.
|
|
45
54
|
|
|
46
55
|
## Build
|
|
47
56
|
|
|
@@ -53,10 +62,10 @@ This package adds **no model-visible content**: no prompt section, no tool schem
|
|
|
53
62
|
|
|
54
63
|
## Known limitations and deferred work
|
|
55
64
|
|
|
56
|
-
- **
|
|
65
|
+
- **The rendered lane covers behaviour, not the assembled artifact.** `tests/panel.client.spec.ts` renders the panel under jsdom (result sentence survives the re-read, two-click confirmation with Cancel, a late rows reply for a skill the operator left is discarded, empty state and refusal) and `tests/client-apply.client.spec.ts` drives `apply` over a fake slot registry (one row, one keyed body, one locale namespace, and nothing left behind on dispose). What neither sees is the artifact the platform actually loads: module identity, the shell's stylesheet reaching the borrowed renderer, and the sidebar's own projection still need a live pass on an installed build.
|
|
57
66
|
- **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.
|
|
58
67
|
- **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).
|
|
59
|
-
- **
|
|
68
|
+
- **A support version can be read but not restored.** Its bytes and (from 0.14.0) its file name are recorded, and the preview shows them, but `undo` still refuses a support version by name: restoring one means deciding deletion semantics, path validation and what happens to a `scripts/` file, which is a design of its own.
|
|
60
69
|
|
|
61
70
|
- **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.
|
|
62
71
|
- **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
|
@@ -19,6 +19,7 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
skills: "/api/dsh-evolution/skill-history/skills",
|
|
20
20
|
versions: "/api/dsh-evolution/skill-history/versions",
|
|
21
21
|
diff: "/api/dsh-evolution/skill-history/versions/diff",
|
|
22
|
+
body: "/api/dsh-evolution/skill-history/versions/body",
|
|
22
23
|
undo: "/api/dsh-evolution/skill-history/undo"
|
|
23
24
|
};
|
|
24
25
|
/** A refusal the host reported, carrying its own sentence (the curator's, not ours). */
|
|
@@ -40,6 +41,7 @@ window.__ModuleLoader__.load({
|
|
|
40
41
|
skills: async () => await request(HOST_ROUTES.skills),
|
|
41
42
|
versions: async (name) => await request(HOST_ROUTES.versions + "?name=" + encodeURIComponent(name)),
|
|
42
43
|
diff: async (name, v) => await request(HOST_ROUTES.diff + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
|
|
44
|
+
body: async (name, v) => await request(HOST_ROUTES.body + "?name=" + encodeURIComponent(name) + "&v=" + String(v)),
|
|
43
45
|
undo: async (name, v) => {
|
|
44
46
|
return (await request(HOST_ROUTES.undo, {
|
|
45
47
|
method: "POST",
|
|
@@ -89,6 +91,15 @@ window.__ModuleLoader__.load({
|
|
|
89
91
|
"row.delta.up": "(+{n} 字符)",
|
|
90
92
|
"row.delta.down": "(-{n} 字符)",
|
|
91
93
|
"diff.show": "差异",
|
|
94
|
+
"diff.source": "源码",
|
|
95
|
+
"preview.show": "预览",
|
|
96
|
+
"preview.hide": "收起预览",
|
|
97
|
+
"preview.truncated": "正文过长,只显示前一部分",
|
|
98
|
+
"path.unrecorded": "(路径未记录)",
|
|
99
|
+
"diff.rendered": "渲染",
|
|
100
|
+
"markdown.copy": "复制",
|
|
101
|
+
"markdown.copied": "已复制",
|
|
102
|
+
"markdown.footnotes": "脚注",
|
|
92
103
|
"diff.hide": "收起差异",
|
|
93
104
|
"diff.loading": "读取差异…",
|
|
94
105
|
"diff.against": "与 v{n} 比较",
|
|
@@ -153,6 +164,15 @@ window.__ModuleLoader__.load({
|
|
|
153
164
|
"row.delta.up": "(+{n} chars)",
|
|
154
165
|
"row.delta.down": "(-{n} chars)",
|
|
155
166
|
"diff.show": "Diff",
|
|
167
|
+
"diff.source": "Source",
|
|
168
|
+
"preview.show": "Preview",
|
|
169
|
+
"preview.hide": "Hide preview",
|
|
170
|
+
"preview.truncated": "the body is long; only its beginning is shown",
|
|
171
|
+
"path.unrecorded": "(path not recorded)",
|
|
172
|
+
"diff.rendered": "Rendered",
|
|
173
|
+
"markdown.copy": "Copy",
|
|
174
|
+
"markdown.copied": "Copied",
|
|
175
|
+
"markdown.footnotes": "Footnotes",
|
|
156
176
|
"diff.hide": "Hide diff",
|
|
157
177
|
"diff.loading": "Loading the diff…",
|
|
158
178
|
"diff.against": "compared with v{n}",
|
|
@@ -268,10 +288,97 @@ window.__ModuleLoader__.load({
|
|
|
268
288
|
".evo-hist-button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:-2px}",
|
|
269
289
|
".evo-hist-button[data-tone=\"primary\"]{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}",
|
|
270
290
|
".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}",
|
|
291
|
+
".evo-hist-diff-head{display:flex;align-items:center;gap:8px}",
|
|
292
|
+
".evo-hist-diff-head .evo-hist-note{flex:1 1 auto;margin:0}",
|
|
293
|
+
".evo-hist-diff-toggle{display:flex;flex:none;align-items:center;gap:6px}",
|
|
294
|
+
".evo-hist-preview{margin:6px 0 2px;border:.5px solid var(--dsw-alias-border-l2);border-radius:12px;padding:4px 12px 12px;background:var(--dsw-alias-bg-layer-2);max-height:420px;overflow:auto}",
|
|
295
|
+
".evo-hist-render{display:flex;flex-direction:column;gap:8px;margin:6px 0 2px}",
|
|
296
|
+
".evo-hist-render-block{position:relative;padding:8px 12px 8px 22px;border:.5px solid var(--dsw-alias-border-l2);border-left-width:2px;border-radius:12px;background:var(--dsw-alias-markdown-code-block);overflow-x:auto}",
|
|
297
|
+
".evo-hist-render-del{border-left-color:var(--dsw-alias-state-error-primary)}",
|
|
298
|
+
".evo-hist-render-add{border-left-color:var(--dsw-alias-state-success-primary)}",
|
|
299
|
+
".evo-hist-render-tag{position:absolute;left:8px;top:8px;color:var(--dsw-alias-label-tertiary)}",
|
|
271
300
|
".evo-hist-pre-add{color:var(--dsw-alias-state-success-primary)}",
|
|
272
301
|
".evo-hist-pre-del{color:var(--dsw-alias-state-error-primary)}"
|
|
273
302
|
].join("\n");
|
|
274
303
|
//#endregion
|
|
304
|
+
//#region src/client/markdown.ts
|
|
305
|
+
/**
|
|
306
|
+
* The one seam onto the platform's Markdown renderer.
|
|
307
|
+
*
|
|
308
|
+
* Everything that renders Markdown in this panel goes through here, so the implementation can be
|
|
309
|
+
* swapped in one file: today it is the platform's own `MarkdownText` (reached through the client
|
|
310
|
+
* module table — zero bytes shipped, styles already in the shell, the same mdast pipeline the chat
|
|
311
|
+
* uses); if that ever stops being reachable, a self-drawn renderer replaces this module and nothing
|
|
312
|
+
* else changes.
|
|
313
|
+
*
|
|
314
|
+
* The words are NOT this module's: the face passes its locale dictionary in, because the platform
|
|
315
|
+
* component takes its chrome as props.
|
|
316
|
+
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
317
|
+
*/
|
|
318
|
+
/** The module-table specifier; kept in one place so the seam is the only reader. */
|
|
319
|
+
const PLATFORM_UI_MODULE = "@deepseek-ai/dsh-client-ui-primitives";
|
|
320
|
+
/**
|
|
321
|
+
* The platform component, when this deployment's module table carries one.
|
|
322
|
+
*
|
|
323
|
+
* A module that IS reachable without the export is not a renderer: reporting a render for it would
|
|
324
|
+
* move the failure into the render itself, where only the slot's error boundary would catch it.
|
|
325
|
+
* @returns the component, or undefined when it cannot be reached.
|
|
326
|
+
*/
|
|
327
|
+
function markdownText() {
|
|
328
|
+
try {
|
|
329
|
+
const row = require(PLATFORM_UI_MODULE);
|
|
330
|
+
return typeof row.MarkdownText === "function" ? row.MarkdownText : void 0;
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* One document, rendered by the platform component under a labels object that survives re-renders.
|
|
337
|
+
*
|
|
338
|
+
* The component is memoized and the platform's own call sites memoize their labels for that reason:
|
|
339
|
+
* this panel re-renders on every keystroke in its search field, and a fresh labels object per render
|
|
340
|
+
* would take the whole document through the render again.
|
|
341
|
+
* @param props - the resolved component, the Markdown source, and the localized words.
|
|
342
|
+
* @returns the rendered document.
|
|
343
|
+
*/
|
|
344
|
+
function MarkdownBlock(props) {
|
|
345
|
+
const { component: MarkdownText, text, words } = props;
|
|
346
|
+
const { copy, copied, footnotes } = words;
|
|
347
|
+
return (0, react.createElement)(MarkdownText, {
|
|
348
|
+
text,
|
|
349
|
+
labels: (0, react.useMemo)(() => ({
|
|
350
|
+
code: {
|
|
351
|
+
copyLabel: copy,
|
|
352
|
+
copiedLabel: copied
|
|
353
|
+
},
|
|
354
|
+
footnotes
|
|
355
|
+
}), [
|
|
356
|
+
copy,
|
|
357
|
+
copied,
|
|
358
|
+
footnotes
|
|
359
|
+
])
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Render one Markdown document to an element tree.
|
|
364
|
+
* @param text - the Markdown source (already bounded by the caller).
|
|
365
|
+
* @param words - the localized Markdown chrome.
|
|
366
|
+
* @returns the element, or `{ ok: false }` when the platform renderer is not reachable (the caller
|
|
367
|
+
* then keeps its own source view: a missing renderer degrades the view, never the panel).
|
|
368
|
+
*/
|
|
369
|
+
function renderMarkdown(text, words) {
|
|
370
|
+
const component = markdownText();
|
|
371
|
+
if (component === void 0) return { ok: false };
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
node: (0, react.createElement)(MarkdownBlock, {
|
|
375
|
+
component,
|
|
376
|
+
text,
|
|
377
|
+
words
|
|
378
|
+
})
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
275
382
|
//#region src/client/Panel.ts
|
|
276
383
|
/**
|
|
277
384
|
* The skill-history panel: the skills that recorded versions, the two chains of the selected skill,
|
|
@@ -318,6 +425,21 @@ window.__ModuleLoader__.load({
|
|
|
318
425
|
if (row.actionKind === "delete") return face.t("action.delete.note");
|
|
319
426
|
if (row.actionKind === "archive") return face.t("action.archive.note");
|
|
320
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* The lazy-read cache without one key.
|
|
430
|
+
*
|
|
431
|
+
* A read that a later toggle superseded must leave nothing behind: `loading` is not an answer, and
|
|
432
|
+
* the cache also serves as the "do not read twice" guard, so an abandoned entry would refuse every
|
|
433
|
+
* later read of that row and leave it saying "loading" until the page is reloaded.
|
|
434
|
+
* @param current - the cache.
|
|
435
|
+
* @param key - the version whose entry is dropped.
|
|
436
|
+
* @returns a new cache without that key.
|
|
437
|
+
*/
|
|
438
|
+
function without(current, key) {
|
|
439
|
+
const next = new Map(current);
|
|
440
|
+
next.delete(key);
|
|
441
|
+
return next;
|
|
442
|
+
}
|
|
321
443
|
/** The relative-time sentence for one row's bucket. */
|
|
322
444
|
function ageText(face, age) {
|
|
323
445
|
if (age.unit === "now") return face.t("time.now");
|
|
@@ -349,8 +471,47 @@ window.__ModuleLoader__.load({
|
|
|
349
471
|
}
|
|
350
472
|
return (0, react.createElement)("pre", { className: "evo-hist-pre" }, rows.length === 0 ? "±" : rows);
|
|
351
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* The same diff, both sides rendered as Markdown and stacked: the removed text first, then the
|
|
476
|
+
* added one. Block level on purpose — line-level interleaving of two rendered documents is a
|
|
477
|
+
* different (and much larger) problem, and the source view stays one click away for exact bytes.
|
|
478
|
+
* @param face - copy plus the data callbacks.
|
|
479
|
+
* @param diff - the windowed change the host reported.
|
|
480
|
+
* @returns the stacked blocks.
|
|
481
|
+
*/
|
|
482
|
+
function renderedBlocks(face, diff) {
|
|
483
|
+
const blocks = [];
|
|
484
|
+
for (const [index, hunk] of diff.hunks.entries()) {
|
|
485
|
+
if (hunk.oldText !== "") {
|
|
486
|
+
const side = renderedSide(face, "−", "del", hunk.oldText);
|
|
487
|
+
if (side === null) return null;
|
|
488
|
+
blocks.push((0, react.createElement)("div", { key: "del-" + String(index) }, side));
|
|
489
|
+
}
|
|
490
|
+
if (hunk.newText !== "") {
|
|
491
|
+
const side = renderedSide(face, "+", "add", hunk.newText);
|
|
492
|
+
if (side === null) return null;
|
|
493
|
+
blocks.push((0, react.createElement)("div", { key: "add-" + String(index) }, side));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return blocks.length === 0 ? null : blocks;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* One diff in the mode the reader asked for, degrading to the source view when the renderer is out.
|
|
500
|
+
* @param face - copy plus the data callbacks.
|
|
501
|
+
* @param diff - the windowed change the host reported.
|
|
502
|
+
* @param mode - source or rendered.
|
|
503
|
+
* @returns the element to draw.
|
|
504
|
+
*/
|
|
505
|
+
function diffView(face, diff, mode) {
|
|
506
|
+
if (mode === "rendered") {
|
|
507
|
+
const blocks = renderedBlocks(face, diff);
|
|
508
|
+
if (blocks !== null) return (0, react.createElement)("div", { className: "evo-hist-render" }, blocks);
|
|
509
|
+
}
|
|
510
|
+
return diffBlock(diff);
|
|
511
|
+
}
|
|
352
512
|
/** One version row: what happened, when, how big, and what it is against. */
|
|
353
|
-
function versionRow(face, row, confirming,
|
|
513
|
+
function versionRow(face, row, confirming, expanded, diff, body, restore, cancelRestore, toggle) {
|
|
514
|
+
const showing = (kind) => expanded !== void 0 && expanded.v === row.v && expanded.kind === kind;
|
|
354
515
|
const parts = [
|
|
355
516
|
(0, react.createElement)("span", {
|
|
356
517
|
key: "v",
|
|
@@ -366,6 +527,15 @@ window.__ModuleLoader__.load({
|
|
|
366
527
|
title: localTime(row.at)
|
|
367
528
|
}, ageText(face, row.age))
|
|
368
529
|
];
|
|
530
|
+
if (row.path !== void 0) parts.push((0, react.createElement)("span", {
|
|
531
|
+
key: "path",
|
|
532
|
+
className: "evo-hist-row-meta",
|
|
533
|
+
title: row.path
|
|
534
|
+
}, row.path));
|
|
535
|
+
else if (row.actionKind === "support-write" || row.actionKind === "support-remove") parts.push((0, react.createElement)("span", {
|
|
536
|
+
key: "path",
|
|
537
|
+
className: "evo-hist-row-meta"
|
|
538
|
+
}, face.t("path.unrecorded")));
|
|
369
539
|
const delta = deltaText(face, row);
|
|
370
540
|
if (delta !== null) parts.push((0, react.createElement)("span", {
|
|
371
541
|
key: "delta",
|
|
@@ -400,27 +570,90 @@ window.__ModuleLoader__.load({
|
|
|
400
570
|
type: "button",
|
|
401
571
|
className: "evo-hist-button",
|
|
402
572
|
onClick: () => {
|
|
403
|
-
|
|
573
|
+
toggle(row.v, "diff");
|
|
404
574
|
}
|
|
405
|
-
},
|
|
575
|
+
}, showing("diff") ? face.t("diff.hide") : face.t("diff.show")));
|
|
576
|
+
actions.push((0, react.createElement)("button", {
|
|
577
|
+
key: "preview",
|
|
578
|
+
type: "button",
|
|
579
|
+
className: "evo-hist-button",
|
|
580
|
+
onClick: () => {
|
|
581
|
+
toggle(row.v, "preview");
|
|
582
|
+
}
|
|
583
|
+
}, showing("preview") ? face.t("preview.hide") : face.t("preview.show")));
|
|
406
584
|
return (0, react.createElement)("div", {
|
|
407
585
|
key: String(row.v),
|
|
408
586
|
className: "evo-hist-row"
|
|
409
|
-
}, (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),
|
|
587
|
+
}, (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), showing("diff") && diff !== void 0 ? (0, react.createElement)(DiffBody, {
|
|
588
|
+
face,
|
|
589
|
+
diff
|
|
590
|
+
}) : null, showing("preview") && body !== void 0 ? (0, react.createElement)(BodyView, {
|
|
591
|
+
face,
|
|
592
|
+
body
|
|
593
|
+
}) : null), (0, react.createElement)("div", { className: "evo-hist-actions" }, actions));
|
|
594
|
+
}
|
|
595
|
+
/** One rendered side of a diff: an "old" or "new" block under a tinted edge. */
|
|
596
|
+
function renderedSide(face, mark, side, text) {
|
|
597
|
+
const rendered = renderMarkdown(text, face.markdownWords);
|
|
598
|
+
if (!rendered.ok) return null;
|
|
599
|
+
return (0, react.createElement)("div", { className: "evo-hist-render-block evo-hist-render-" + side }, (0, react.createElement)("span", { className: "evo-hist-render-tag" }, mark), rendered.node);
|
|
410
600
|
}
|
|
411
|
-
/**
|
|
412
|
-
|
|
601
|
+
/**
|
|
602
|
+
* One version read as a document: the platform renderer when it is there, plain text when it is not.
|
|
603
|
+
* A preview therefore degrades to the exact bytes instead of blanking.
|
|
604
|
+
* @param props - the face and the body state to draw.
|
|
605
|
+
* @returns the element.
|
|
606
|
+
*/
|
|
607
|
+
function BodyView(props) {
|
|
608
|
+
const { face, body } = props;
|
|
609
|
+
if (body.kind === "loading") return (0, react.createElement)("p", { className: "evo-hist-note" }, face.t("diff.loading"));
|
|
610
|
+
if (body.kind === "failed") return (0, react.createElement)("p", {
|
|
611
|
+
className: "evo-hist-note",
|
|
612
|
+
"data-error": "true"
|
|
613
|
+
}, body.message);
|
|
614
|
+
const rendered = renderMarkdown(body.body.text, face.markdownWords);
|
|
615
|
+
return (0, react.createElement)("div", { className: "evo-hist-preview" }, body.body.truncated ? (0, react.createElement)("p", { className: "evo-hist-note" }, face.t("preview.truncated")) : null, rendered.ok ? rendered.node : (0, react.createElement)("pre", { className: "evo-hist-pre" }, body.body.text));
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* The expanded body of one row: the diff, a loading line, or the refusal.
|
|
619
|
+
*
|
|
620
|
+
* The source/rendered toggle is LOCAL to this view: which row is open is the panel's business, but
|
|
621
|
+
* how one open diff is displayed is not, so the state does not climb into the parent.
|
|
622
|
+
* @param props - the face and the diff state to draw.
|
|
623
|
+
* @returns the element.
|
|
624
|
+
*/
|
|
625
|
+
function DiffBody(props) {
|
|
626
|
+
const { face, diff } = props;
|
|
627
|
+
const [mode, setMode] = (0, react.useState)("source");
|
|
413
628
|
if (diff.kind === "loading") return (0, react.createElement)("p", { className: "evo-hist-note" }, face.t("diff.loading"));
|
|
414
629
|
if (diff.kind === "failed") return (0, react.createElement)("p", {
|
|
415
630
|
className: "evo-hist-note",
|
|
416
631
|
"data-error": "true"
|
|
417
632
|
}, diff.message);
|
|
418
|
-
|
|
633
|
+
const against = diff.diff.against === null ? face.t("diff.first") : face.format("diff.against", { n: diff.diff.against });
|
|
634
|
+
const toggle = (0, react.createElement)("div", { className: "evo-hist-diff-toggle" }, (0, react.createElement)("button", {
|
|
635
|
+
key: "source",
|
|
636
|
+
type: "button",
|
|
637
|
+
className: "evo-hist-button",
|
|
638
|
+
"data-tone": mode === "source" ? "primary" : void 0,
|
|
639
|
+
onClick: () => {
|
|
640
|
+
setMode("source");
|
|
641
|
+
}
|
|
642
|
+
}, face.t("diff.source")), (0, react.createElement)("button", {
|
|
643
|
+
key: "rendered",
|
|
644
|
+
type: "button",
|
|
645
|
+
className: "evo-hist-button",
|
|
646
|
+
"data-tone": mode === "rendered" ? "primary" : void 0,
|
|
647
|
+
onClick: () => {
|
|
648
|
+
setMode("rendered");
|
|
649
|
+
}
|
|
650
|
+
}, face.t("diff.rendered")));
|
|
651
|
+
return (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "evo-hist-diff-head" }, (0, react.createElement)("p", { className: "evo-hist-note" }, against + " · " + face.format("diff.added", { n: diff.diff.linesAdded }) + " · " + face.format("diff.removed", { n: diff.diff.linesRemoved }) + (diff.diff.truncated ? " · " + face.t("diff.truncated") : "")), toggle), diffView(face, diff.diff, mode));
|
|
419
652
|
}
|
|
420
653
|
/** One group: a heading, an optional note, and its rows. */
|
|
421
654
|
function group(face, title, entries, noteText, state) {
|
|
422
655
|
if (entries.length === 0) return null;
|
|
423
|
-
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.
|
|
656
|
+
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.expanded, state.diffs.get(entry.v), state.bodies.get(entry.v), state.restore, state.cancelRestore, state.toggle)));
|
|
424
657
|
}
|
|
425
658
|
/**
|
|
426
659
|
* The panel body.
|
|
@@ -435,8 +668,9 @@ window.__ModuleLoader__.load({
|
|
|
435
668
|
const [noteError, setNoteError] = (0, react.useState)(false);
|
|
436
669
|
const [pending, setPending] = (0, react.useState)(void 0);
|
|
437
670
|
const [query, setQuery] = (0, react.useState)("");
|
|
438
|
-
const [
|
|
671
|
+
const [expanded, setExpanded] = (0, react.useState)(void 0);
|
|
439
672
|
const [diffs, setDiffs] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
673
|
+
const [bodies, setBodies] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
440
674
|
const readTicket = (0, react.useRef)(0);
|
|
441
675
|
const failed = (error) => {
|
|
442
676
|
setNoteError(true);
|
|
@@ -462,8 +696,9 @@ window.__ModuleLoader__.load({
|
|
|
462
696
|
setSelected(name);
|
|
463
697
|
setPayload(void 0);
|
|
464
698
|
setPending(void 0);
|
|
465
|
-
|
|
699
|
+
setExpanded(void 0);
|
|
466
700
|
setDiffs(/* @__PURE__ */ new Map());
|
|
701
|
+
setBodies(/* @__PURE__ */ new Map());
|
|
467
702
|
face.loadVersions(name).then((next) => {
|
|
468
703
|
if (readTicket.current === ticket) setPayload(next);
|
|
469
704
|
}).catch((error) => {
|
|
@@ -499,30 +734,67 @@ window.__ModuleLoader__.load({
|
|
|
499
734
|
succeeded(message);
|
|
500
735
|
}).catch(failed);
|
|
501
736
|
};
|
|
502
|
-
|
|
737
|
+
/**
|
|
738
|
+
* Expand one row, or collapse it: one row is open at a time, and each kind keeps its own lazy read.
|
|
739
|
+
* @param v - the version whose row was clicked.
|
|
740
|
+
* @param kind - which expansion the row should show.
|
|
741
|
+
*/
|
|
742
|
+
const toggle = (v, kind) => {
|
|
503
743
|
if (selected === void 0) return;
|
|
504
|
-
if (
|
|
505
|
-
|
|
744
|
+
if (expanded !== void 0 && expanded.v === v && expanded.kind === kind) {
|
|
745
|
+
setExpanded(void 0);
|
|
506
746
|
return;
|
|
507
747
|
}
|
|
508
|
-
|
|
509
|
-
|
|
748
|
+
setExpanded({
|
|
749
|
+
v,
|
|
750
|
+
kind
|
|
751
|
+
});
|
|
752
|
+
if (kind === "diff" ? diffs.has(v) : bodies.has(v)) return;
|
|
510
753
|
const name = selected;
|
|
511
754
|
const ticket = readTicket.current + 1;
|
|
512
755
|
readTicket.current = ticket;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
756
|
+
const failedMessage = (error) => face.t("error") + ": " + (error instanceof Error ? error.message : String(error));
|
|
757
|
+
if (kind === "diff") {
|
|
758
|
+
setDiffs((current) => new Map(current).set(v, { kind: "loading" }));
|
|
759
|
+
face.loadDiff(name, v).then((diff) => {
|
|
760
|
+
if (readTicket.current !== ticket) {
|
|
761
|
+
setDiffs((current) => without(current, v));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
setDiffs((current) => new Map(current).set(v, {
|
|
765
|
+
kind: "ready",
|
|
766
|
+
diff
|
|
767
|
+
}));
|
|
768
|
+
}).catch((error) => {
|
|
769
|
+
if (readTicket.current !== ticket) {
|
|
770
|
+
setDiffs((current) => without(current, v));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
setDiffs((current) => new Map(current).set(v, {
|
|
774
|
+
kind: "failed",
|
|
775
|
+
message: failedMessage(error)
|
|
776
|
+
}));
|
|
777
|
+
});
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
setBodies((current) => new Map(current).set(v, { kind: "loading" }));
|
|
781
|
+
face.loadBody(name, v).then((body) => {
|
|
782
|
+
if (readTicket.current !== ticket) {
|
|
783
|
+
setBodies((current) => without(current, v));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
setBodies((current) => new Map(current).set(v, {
|
|
517
787
|
kind: "ready",
|
|
518
|
-
|
|
788
|
+
body
|
|
519
789
|
}));
|
|
520
790
|
}).catch((error) => {
|
|
521
|
-
if (readTicket.current !== ticket)
|
|
522
|
-
|
|
523
|
-
|
|
791
|
+
if (readTicket.current !== ticket) {
|
|
792
|
+
setBodies((current) => without(current, v));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
setBodies((current) => new Map(current).set(v, {
|
|
524
796
|
kind: "failed",
|
|
525
|
-
message
|
|
797
|
+
message: failedMessage(error)
|
|
526
798
|
}));
|
|
527
799
|
});
|
|
528
800
|
};
|
|
@@ -536,11 +808,12 @@ window.__ModuleLoader__.load({
|
|
|
536
808
|
};
|
|
537
809
|
const rowState = {
|
|
538
810
|
confirming: pending,
|
|
539
|
-
|
|
811
|
+
expanded,
|
|
540
812
|
diffs,
|
|
813
|
+
bodies,
|
|
541
814
|
restore,
|
|
542
815
|
cancelRestore,
|
|
543
|
-
|
|
816
|
+
toggle
|
|
544
817
|
};
|
|
545
818
|
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", {
|
|
546
819
|
type: "search",
|
|
@@ -559,7 +832,7 @@ window.__ModuleLoader__.load({
|
|
|
559
832
|
onClick: () => {
|
|
560
833
|
open(skill.name);
|
|
561
834
|
}
|
|
562
|
-
}, (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", {
|
|
835
|
+
}, (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)) + (skill.age === null ? "" : " · " + ageText(face, skill.age))))))), (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", {
|
|
563
836
|
type: "button",
|
|
564
837
|
className: "evo-hist-button",
|
|
565
838
|
onClick: refresh
|
|
@@ -679,7 +952,13 @@ window.__ModuleLoader__.load({
|
|
|
679
952
|
loadSkills: api.skills,
|
|
680
953
|
loadVersions: api.versions,
|
|
681
954
|
loadDiff: api.diff,
|
|
682
|
-
|
|
955
|
+
loadBody: api.body,
|
|
956
|
+
undo: api.undo,
|
|
957
|
+
markdownWords: {
|
|
958
|
+
copy: t("markdown.copy"),
|
|
959
|
+
copied: t("markdown.copied"),
|
|
960
|
+
footnotes: t("markdown.footnotes")
|
|
961
|
+
}
|
|
683
962
|
})
|
|
684
963
|
}, SkillHistoryPanel);
|
|
685
964
|
});
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { contentHash, elapsedSince, errorText, partitionVersions, textDiffFacts, versionActionKind } from "@lmzhen/dsh-evolution-core";
|
|
1
|
+
import { contentHash, elapsedSince, errorText, latestVersionAt, 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: six loopback routes over the curator's read/write seam (five 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
|
|
@@ -13,9 +13,15 @@ const SKILL_HISTORY_ROUTES = {
|
|
|
13
13
|
skills: "/api/dsh-evolution/skill-history/skills",
|
|
14
14
|
versions: "/api/dsh-evolution/skill-history/versions",
|
|
15
15
|
diff: "/api/dsh-evolution/skill-history/versions/diff",
|
|
16
|
+
body: "/api/dsh-evolution/skill-history/versions/body",
|
|
16
17
|
undo: "/api/dsh-evolution/skill-history/undo",
|
|
17
18
|
health: "/api/dsh-evolution/skill-history/health"
|
|
18
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* How much of one version's body the read route hands back. A bound on the ANSWER, not on what the
|
|
22
|
+
* deployment may store: a long skill arrives as its head plus `truncated`, so the reader is told.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_BODY_CHARS = 2e4;
|
|
19
25
|
function header(request, name) {
|
|
20
26
|
const value = request.headers[name];
|
|
21
27
|
return typeof value === "string" ? value : void 0;
|
|
@@ -139,16 +145,20 @@ function makeSkillHistoryRoutes(services) {
|
|
|
139
145
|
await withCurator(res, async (curator) => {
|
|
140
146
|
const listed = await curator.skills.list();
|
|
141
147
|
const withHistory = [];
|
|
148
|
+
const now = Date.now();
|
|
142
149
|
for (const skill of listed) {
|
|
143
150
|
const versions = await curator.skills.listVersions(skill.name);
|
|
144
151
|
if (versions.length === 0) continue;
|
|
152
|
+
const lastAt = latestVersionAt(versions);
|
|
145
153
|
withHistory.push({
|
|
146
154
|
name: skill.name,
|
|
147
155
|
versions: versions.length,
|
|
148
156
|
description: skill.description,
|
|
149
157
|
managed: skill.managed,
|
|
150
158
|
protectedBy: skill.protectedBy,
|
|
151
|
-
protectionUnknown: skill.protectionUnknown
|
|
159
|
+
protectionUnknown: skill.protectionUnknown,
|
|
160
|
+
lastAt,
|
|
161
|
+
age: lastAt === null ? null : elapsedSince(lastAt, now)
|
|
152
162
|
});
|
|
153
163
|
}
|
|
154
164
|
writeJson(res, 200, {
|
|
@@ -252,6 +262,46 @@ function makeSkillHistoryRoutes(services) {
|
|
|
252
262
|
});
|
|
253
263
|
}
|
|
254
264
|
},
|
|
265
|
+
{
|
|
266
|
+
kind: "exact",
|
|
267
|
+
path: SKILL_HISTORY_ROUTES.body,
|
|
268
|
+
handler: async (req, res) => {
|
|
269
|
+
if (!guard(req, res, "GET")) return;
|
|
270
|
+
const params = new URL(req.url ?? "/", "http://loopback").searchParams;
|
|
271
|
+
const name = params.get("name")?.trim() ?? "";
|
|
272
|
+
const rawV = params.get("v")?.trim() ?? "";
|
|
273
|
+
const v = Number.parseInt(rawV, 10);
|
|
274
|
+
if (name === "" || !/^\d+$/.test(rawV) || !Number.isInteger(v) || v < 1) {
|
|
275
|
+
writeJson(res, 400, {
|
|
276
|
+
ok: false,
|
|
277
|
+
code: "bad-request",
|
|
278
|
+
message: "name and a positive integer v are required"
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await withCurator(res, async (curator) => {
|
|
283
|
+
const text = await curator.skills.readVersion(name, v);
|
|
284
|
+
if (text === null) {
|
|
285
|
+
writeJson(res, 404, {
|
|
286
|
+
ok: false,
|
|
287
|
+
code: "not-found",
|
|
288
|
+
message: "the stored content of that version could not be read"
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const truncated = text.length > MAX_BODY_CHARS;
|
|
293
|
+
writeJson(res, 200, {
|
|
294
|
+
ok: true,
|
|
295
|
+
data: {
|
|
296
|
+
v,
|
|
297
|
+
text: truncated ? text.slice(0, MAX_BODY_CHARS) : text,
|
|
298
|
+
chars: text.length,
|
|
299
|
+
truncated
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
},
|
|
255
305
|
{
|
|
256
306
|
kind: "exact",
|
|
257
307
|
path: SKILL_HISTORY_ROUTES.undo,
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* @module @lmzhen/dsh-evolution-skill-history/client
|
|
15
15
|
*/
|
|
16
16
|
import { type ReactNode } from 'react';
|
|
17
|
-
import type { SkillRow, VersionDiffRow, VersionsPayload } from './api.ts';
|
|
17
|
+
import type { SkillRow, VersionBodyRow, VersionDiffRow, VersionsPayload } from './api.ts';
|
|
18
|
+
import { type MarkdownWords } from './markdown.ts';
|
|
18
19
|
/** The face the plugin hands the component: copy plus callbacks, never a service handle. */
|
|
19
20
|
export interface PanelFace {
|
|
20
21
|
readonly t: (key: string) => string;
|
|
@@ -23,7 +24,10 @@ export interface PanelFace {
|
|
|
23
24
|
readonly loadSkills: () => Promise<readonly SkillRow[]>;
|
|
24
25
|
readonly loadVersions: (name: string) => Promise<VersionsPayload>;
|
|
25
26
|
readonly loadDiff: (name: string, v: number) => Promise<VersionDiffRow>;
|
|
27
|
+
readonly loadBody: (name: string, v: number) => Promise<VersionBodyRow>;
|
|
26
28
|
readonly undo: (name: string, v?: number) => Promise<string>;
|
|
29
|
+
/** The two words the platform Markdown renderer needs; the dictionary owns them. */
|
|
30
|
+
readonly markdownWords: MarkdownWords;
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
33
|
* The panel body.
|
|
@@ -11,6 +11,7 @@ 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
13
|
readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
|
|
14
|
+
readonly body: "/api/dsh-evolution/skill-history/versions/body";
|
|
14
15
|
readonly undo: "/api/dsh-evolution/skill-history/undo";
|
|
15
16
|
};
|
|
16
17
|
/** One changed region, as the host reports it (the platform diff card's own shape). */
|
|
@@ -49,6 +50,11 @@ export interface VersionRow {
|
|
|
49
50
|
};
|
|
50
51
|
/** The one line the optional summarizer wrote, when the deployment turned it on. */
|
|
51
52
|
readonly summary?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Which support file these bytes came from, relative to the skill directory. Absent on body rows,
|
|
55
|
+
* and absent on every support row recorded before the field existed (the bytes carry no name).
|
|
56
|
+
*/
|
|
57
|
+
readonly path?: string;
|
|
52
58
|
}
|
|
53
59
|
/** One skill that has recorded versions, with what the listing already knows about it. */
|
|
54
60
|
export interface SkillRow {
|
|
@@ -58,6 +64,22 @@ export interface SkillRow {
|
|
|
58
64
|
readonly managed: boolean;
|
|
59
65
|
readonly protectedBy: string | null;
|
|
60
66
|
readonly protectionUnknown: boolean;
|
|
67
|
+
/** When this skill's content last changed (either chain), or null when the index is empty. */
|
|
68
|
+
readonly lastAt: string | null;
|
|
69
|
+
/** That moment as a bucket plus a count; null exactly when `lastAt` is. */
|
|
70
|
+
readonly age: {
|
|
71
|
+
readonly unit: string;
|
|
72
|
+
readonly n: number;
|
|
73
|
+
} | null;
|
|
74
|
+
}
|
|
75
|
+
/** One version's whole body, as the read route hands it over. */
|
|
76
|
+
export interface VersionBodyRow {
|
|
77
|
+
readonly v: number;
|
|
78
|
+
/** The body, or its head when `truncated` — the reader is told rather than silently cut. */
|
|
79
|
+
readonly text: string;
|
|
80
|
+
/** How long the stored body is, whatever came back. */
|
|
81
|
+
readonly chars: number;
|
|
82
|
+
readonly truncated: boolean;
|
|
61
83
|
}
|
|
62
84
|
/** The two chains, plus the live body hash the panel marks as "current". */
|
|
63
85
|
export interface VersionsPayload {
|
|
@@ -71,6 +93,7 @@ export interface SkillHistoryApi {
|
|
|
71
93
|
readonly versions: (name: string) => Promise<VersionsPayload>;
|
|
72
94
|
readonly undo: (name: string, v?: number) => Promise<string>;
|
|
73
95
|
readonly diff: (name: string, v: number) => Promise<VersionDiffRow>;
|
|
96
|
+
readonly body: (name: string, v: number) => Promise<VersionBodyRow>;
|
|
74
97
|
}
|
|
75
98
|
/** A refusal the host reported, carrying its own sentence (the curator's, not ours). */
|
|
76
99
|
export declare class SkillHistoryRefusal extends Error {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The three localized words the platform renderer needs; the face owns them. */
|
|
2
|
+
export interface MarkdownWords {
|
|
3
|
+
readonly copy: string;
|
|
4
|
+
readonly copied: string;
|
|
5
|
+
/** The heading of the generated footnote section. */
|
|
6
|
+
readonly footnotes: string;
|
|
7
|
+
}
|
|
8
|
+
/** One render attempt: the element, or the news that this deployment has no renderer. */
|
|
9
|
+
export type MarkdownRender = {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly node: unknown;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Render one Markdown document to an element tree.
|
|
17
|
+
* @param text - the Markdown source (already bounded by the caller).
|
|
18
|
+
* @param words - the localized Markdown chrome.
|
|
19
|
+
* @returns the element, or `{ ok: false }` when the platform renderer is not reachable (the caller
|
|
20
|
+
* then keeps its own source view: a missing renderer degrades the view, never the panel).
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderMarkdown(text: string, words: MarkdownWords): MarkdownRender;
|
|
23
|
+
//# sourceMappingURL=markdown.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: six loopback routes over the curator's read/write seam (five 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
|
|
@@ -14,11 +14,17 @@ 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
16
|
readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
|
|
17
|
+
readonly body: "/api/dsh-evolution/skill-history/versions/body";
|
|
17
18
|
readonly undo: "/api/dsh-evolution/skill-history/undo";
|
|
18
19
|
readonly health: "/api/dsh-evolution/skill-history/health";
|
|
19
20
|
};
|
|
20
21
|
/** Largest request body these routes accept (the undo payload is a name and a number). */
|
|
21
22
|
export declare const MAX_REQUEST_BODY_BYTES: number;
|
|
23
|
+
/**
|
|
24
|
+
* How much of one version's body the read route hands back. A bound on the ANSWER, not on what the
|
|
25
|
+
* deployment may store: a long skill arrives as its head plus `truncated`, so the reader is told.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_BODY_CHARS = 20000;
|
|
22
28
|
/** The request facts the trust fence reads: a Node request, or the same fields in a spec fixture. */
|
|
23
29
|
export interface FenceRequest {
|
|
24
30
|
readonly headers: Record<string, string | string[] | undefined>;
|
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.14.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,21 +30,23 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
33
|
+
"@lmzhen/dsh-evolution-core": "^0.14.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.14.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
|
-
"@
|
|
45
|
-
"
|
|
46
|
-
"@lmzhen/dsh-evolution-
|
|
47
|
-
"@lmzhen/dsh-evolution-
|
|
44
|
+
"@types/react": "~18.3.1",
|
|
45
|
+
"react": "^18.2.0",
|
|
46
|
+
"@lmzhen/dsh-evolution-core": "^0.14.0",
|
|
47
|
+
"@lmzhen/dsh-evolution-curator": "^0.14.0",
|
|
48
|
+
"@lmzhen/dsh-evolution-io": "^0.14.0",
|
|
49
|
+
"@lmzhen/dsh-evolution-io-node": "^0.14.0"
|
|
48
50
|
},
|
|
49
51
|
"dsh": {
|
|
50
52
|
"client": {
|