dsh-graft-plugin 0.1.2 → 0.2.1
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 +12 -7
- package/package.json +1 -1
- package/scripts/test-harness.mjs +45 -1
- package/src/client.js +88 -7
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ It does four things:
|
|
|
9
9
|
- **A chip in the composer** naming the graft index the current session is actually serving, whether it still matches the code, and how many tokens graft has saved in that repo.
|
|
10
10
|
- **Five tools for the model** — `graft_ask`, `graft_grep`, `graft_callers`, `graft_skeleton`, `graft_map` — resolved per session, so one dsh can serve several workspaces at once.
|
|
11
11
|
- **`/graft`** to build or rebuild the index, and **auto-rebuild** after the model edits files.
|
|
12
|
-
- **A `graft viz` button** that opens the graph
|
|
12
|
+
- **A `graft viz` button** that opens the graph in dsh's built-in right sidebar (0.1.5+), in [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) if you still run it, and in a browser tab as the last resort.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -50,15 +50,17 @@ graft build
|
|
|
50
50
|
|
|
51
51
|
### The sidebar tab
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
On dsh `0.1.5` and later the button opens the graph in **dsh's own right sidebar** — a tab titled Graft, also listed on the sidebar's guide page as "The graft graph for this workspace" — and a second click focuses the tab that is already open. No install needed for that path: the registration is soft, so an older dsh without the right sidebar falls through to `dsh-better-sidebar` when it is mounted, and to a browser tab when neither is there.
|
|
54
|
+
|
|
55
|
+
If your profile still runs `dsh-better-sidebar`, that path is tried **first** and behaves exactly as before. It ships as a dependency of this plugin (0.18.x), so it is already downloaded — but dsh only mounts what the profile lists:
|
|
54
56
|
|
|
55
57
|
```sh
|
|
56
58
|
dsh plugin --profile web add dsh-better-sidebar
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
> **Compatibility.** `dsh-better-sidebar` 0.18.x requires `@deepseek-ai/dsh-client-ui-primitives`, which dsh removed in `0.1.5-rc.2`. On that version or later
|
|
61
|
+
> **Compatibility.** `dsh-better-sidebar` 0.18.x requires `@deepseek-ai/dsh-client-ui-primitives`, which dsh removed in `0.1.5-rc.2`. On that version or later it will not mount — which is exactly the profiles where the built-in right sidebar takes over instead. Everything else in this plugin — chip, tools, `/graft`, auto-rebuild — is unaffected.
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
The chip, the tools, `/graft` and the auto-rebuild all work without any sidebar; the button simply falls back to a browser tab when neither one is present.
|
|
62
64
|
|
|
63
65
|
> This plugin does **not** insert a `dsh-better-sidebar` row itself, on purpose. That package's own bundle row carries a guard that disables it when another enabled row already mounts the same package. A second row with a different id makes that guard circular, and the losing outcome is *your existing sidebar silently disabling itself*. One explicit command is worth more than that risk.
|
|
64
66
|
|
|
@@ -82,7 +84,7 @@ The root is resolved from **the session's own workspace**, not from wherever dsh
|
|
|
82
84
|
|
|
83
85
|
### The visualiser
|
|
84
86
|
|
|
85
|
-
The `graft viz` button opens the graph as a tab in [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar). The context view lays out the whole graph:
|
|
87
|
+
The `graft viz` button opens the graph as a tab — in dsh's built-in right sidebar on `0.1.5`+, in [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) if that is what you run. The context view lays out the whole graph:
|
|
86
88
|
|
|
87
89
|

|
|
88
90
|
|
|
@@ -131,8 +133,9 @@ All optional, all on the plugin's row in your profile's `cordis.patch.yml`:
|
|
|
131
133
|
|
|
132
134
|
| dsh | status |
|
|
133
135
|
|---|---|
|
|
134
|
-
| `0.1.2-rc.1` – `0.1.0-rc.7` | tested, full feature set |
|
|
135
|
-
| `0.1.5-rc.2` | tested — chip, tools, `/graft` and auto-rebuild all work; the `graft viz`
|
|
136
|
+
| `0.1.2-rc.1` – `0.1.0-rc.7` | tested, full feature set — the `graft viz` tab via `dsh-better-sidebar` |
|
|
137
|
+
| `0.1.5-rc.2` / `0.1.5` | tested — chip, tools, `/graft` and auto-rebuild all work; on `0.1.5` the `graft viz` button opens the graph in the built-in right sidebar |
|
|
138
|
+
| `0.2.0-rc.2` | needs plugin `0.2.1`+ — earlier releases fail to activate (see Troubleshooting) |
|
|
136
139
|
|
|
137
140
|
- Node 22+
|
|
138
141
|
- the `graft` CLI on PATH (`npm install -g @nanonets/graft`)
|
|
@@ -166,6 +169,8 @@ registering "dsh-graft-plugin" via __ModuleLoader__.load
|
|
|
166
169
|
|
|
167
170
|
**No chip appears and dsh started without errors.** The plugin is in the profile's `package.json` `dependencies` but not in `dsh.profile.bundles` — which happens under pnpm 10+ when the `add` ends non-zero on `ERR_PNPM_IGNORED_BUILDS`; dsh skips the bundle row in that case and starts up looking healthy. Fix: `pnpm approve-builds` in the profile directory (tick `dsh-graft-plugin`), then re-run `dsh plugin --profile web add dsh-graft-plugin`. If `pnpm approve-builds` reports nothing awaiting approval, check `pnpm-workspace.yaml` for an unanswered `dsh-graft-plugin` entry (pnpm's approval placeholder) and set its value to `true` first.
|
|
168
171
|
|
|
172
|
+
**On dsh 0.2 the plugin fails to load with `strict codec has no create() factory`, and dsh offers to remove it.** dsh 0.2 requires every strict remote codec to carry a `create()` schema factory; plugin releases before `0.2.1` only had the 0.1 `schema` key. Update to `0.2.1` or later, which carries both and so loads on 0.1 and 0.2.
|
|
173
|
+
|
|
169
174
|
**`/graft` says the graft CLI is not on PATH.** The postinstall was blocked or skipped (see Install): `npm install -g @nanonets/graft`.
|
|
170
175
|
|
|
171
176
|
## Licence
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-graft-plugin",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Shows which graft index a dsh session is serving and whether it still matches the code, and gives the model graft_ask, graft_grep, graft_callers, graft_skeleton and graft_map as native tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/scripts/test-harness.mjs
CHANGED
|
@@ -246,7 +246,51 @@ check(
|
|
|
246
246
|
clientSource.includes('ctx.inject(["betterSidebar"]') && /const inject = \[[^\]]*\]/.exec(clientSource)?.[0].includes('betterSidebar') === false,
|
|
247
247
|
/const inject = \[[^\]]*\]/.exec(clientSource)?.[0],
|
|
248
248
|
)
|
|
249
|
-
|
|
249
|
+
// The open order: better-sidebar if a profile still has it, then dsh's own
|
|
250
|
+
// right sidebar (0.1.5+), and a browser tab only when neither is there.
|
|
251
|
+
check('dsh right sidebar comes after better-sidebar', clientSource.indexOf('ctx.get("betterSidebar")') < clientSource.indexOf('ctx.get("sidebarRight")'))
|
|
252
|
+
check('it opens by kind, which also reveals the column', clientSource.includes('right.openTab(VIZ_KIND)'))
|
|
253
|
+
check('a kind nothing registered falls through, not dead', clientSource.includes('openTab throws for a kind nothing registered'))
|
|
254
|
+
check('the type and its body register under the same id', clientSource.includes('sidebarRightTabs.register({') && clientSource.includes('id: TAB_TYPE,') && clientSource.includes('name: "sidebar.right.pane.tab", key: TAB_TYPE'))
|
|
255
|
+
check('that dependency is soft too', clientSource.includes('ctx.inject(["sidebarRightTabs", "slots"]') && !/const inject = [[]/.test('') && clientSource.indexOf('const inject = ["slots", "remote"]') !== -1)
|
|
256
|
+
check('the body reads the session from either host', clientSource.includes('props?.sessionId ?? props?.scope?.sessionId'))
|
|
257
|
+
check('the browser tab survives only as the last resort', clientSource.includes('neither sidebar'))
|
|
258
|
+
|
|
259
|
+
// The remote descriptors must survive dsh 0.2's typert codec validation, which
|
|
260
|
+
// runs inside `ctx.remote.$mount`: a strict codec needs a typeSymbol and a
|
|
261
|
+
// `create()` factory returning a schema with `parse`. The 0.1 shape (`schema`
|
|
262
|
+
// only) threw "strict codec has no create() factory", rejected apply(), and
|
|
263
|
+
// took the whole plugin down. The client may only use strict codecs for
|
|
264
|
+
// parameters. The descriptor block is lifted out of client.js and evaluated,
|
|
265
|
+
// so this tests the shipped objects, not a copy.
|
|
266
|
+
console.log('\nremote descriptors (dsh 0.2 codec contract)')
|
|
267
|
+
const descriptorBlock = /function identity\(value\) \{[\s\S]*?\r?\n const CONTRIBUTION = \{[\s\S]*?\r?\n \};/.exec(clientSource)?.[0]
|
|
268
|
+
check('descriptor block found in client.js', descriptorBlock !== undefined)
|
|
269
|
+
const CONTRIBUTION = descriptorBlock ? new Function(`${descriptorBlock}; return CONTRIBUTION;`)() : { descriptors: [] }
|
|
270
|
+
function codecProblem(codec, { parameter }) {
|
|
271
|
+
if (!codec || typeof codec !== 'object') return 'missing codec'
|
|
272
|
+
if (codec.mode === 'src-json') return parameter ? 'client parameters must be strict' : undefined
|
|
273
|
+
if (codec.mode !== 'strict') return `unknown mode ${codec.mode}`
|
|
274
|
+
if (typeof codec.typeSymbol !== 'string' || codec.typeSymbol === '') return 'empty typeSymbol'
|
|
275
|
+
if (typeof codec.create !== 'function') return 'strict codec has no create() factory'
|
|
276
|
+
const schema = codec.create()
|
|
277
|
+
if (typeof schema?.parse !== 'function') return 'create() returned no parse()'
|
|
278
|
+
if (codec.create() !== schema) return 'create() is not memoized'
|
|
279
|
+
// dsh 0.1 read `schema` directly; keep it so older hosts still load.
|
|
280
|
+
if (typeof codec.schema?.parse !== 'function') return 'no 0.1 schema fallback'
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
check('declares the status and viz descriptors', CONTRIBUTION.descriptors.map((d) => d.method).join() === 'status,viz')
|
|
284
|
+
for (const d of CONTRIBUTION.descriptors) {
|
|
285
|
+
for (const p of d.parameters) {
|
|
286
|
+
const problem = codecProblem(p.codec, { parameter: true })
|
|
287
|
+
check(`${d.id} parameter ${p.name} codec is valid`, problem === undefined, problem)
|
|
288
|
+
}
|
|
289
|
+
const problem = codecProblem(d.result, { parameter: false })
|
|
290
|
+
check(`${d.id} result codec is valid`, problem === undefined, problem)
|
|
291
|
+
const sample = { ok: true, value: { root: '/r', nodeCount: 1 } }
|
|
292
|
+
check(`${d.id} result decodes as a pass-through`, d.result.create().parse(sample) === sample)
|
|
293
|
+
}
|
|
250
294
|
|
|
251
295
|
await rm(fixture, { recursive: true, force: true })
|
|
252
296
|
|
package/src/client.js
CHANGED
|
@@ -50,7 +50,21 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
function identity(value) {
|
|
51
51
|
return value;
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
/*
|
|
54
|
+
* Pass-through codec carrying both codec shapes: dsh 0.2's typert
|
|
55
|
+
* validator requires a strict codec to expose a `create()` factory
|
|
56
|
+
* returning the schema (it throws "strict codec has no create() factory"
|
|
57
|
+
* otherwise, failing `$mount` and with it the whole plugin), while dsh 0.1
|
|
58
|
+
* read `schema` directly. The host still runs assertJsonValue on every
|
|
59
|
+
* decoded value, so identity is safe.
|
|
60
|
+
*/
|
|
61
|
+
const IDENTITY_SCHEMA = { parse: identity };
|
|
62
|
+
const CODEC = {
|
|
63
|
+
mode: "strict",
|
|
64
|
+
typeSymbol: "graft-status/json",
|
|
65
|
+
schema: IDENTITY_SCHEMA,
|
|
66
|
+
create: () => IDENTITY_SCHEMA,
|
|
67
|
+
};
|
|
54
68
|
const descriptor = (method, names) => ({
|
|
55
69
|
id: "graft-status#graftStatus/" + method,
|
|
56
70
|
service: "graftStatus",
|
|
@@ -58,7 +72,7 @@ window.__ModuleLoader__.load({
|
|
|
58
72
|
method,
|
|
59
73
|
invocation: { kind: "direct" },
|
|
60
74
|
parameters: names.map((name) => ({ name, wire: name, source: "json", codec: CODEC })),
|
|
61
|
-
result:
|
|
75
|
+
result: CODEC,
|
|
62
76
|
sourceLocation: { file: "graft-status/client.js", line: 1, column: 1 },
|
|
63
77
|
});
|
|
64
78
|
const CONTRIBUTION = {
|
|
@@ -324,9 +338,19 @@ window.__ModuleLoader__.load({
|
|
|
324
338
|
].join("");
|
|
325
339
|
}
|
|
326
340
|
|
|
327
|
-
/**
|
|
341
|
+
/** This viewer's identity: better-sidebar's tab type, and dsh's tab id. */
|
|
328
342
|
const TAB_TYPE = "graft-status:viz";
|
|
329
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The kind dsh's own right sidebar opens this page type by.
|
|
346
|
+
*
|
|
347
|
+
* dsh 0.1.5 splits a tab type in two: a static registration naming the kind
|
|
348
|
+
* (`ctx.sidebarRightTabs`), and the body under the registration's id in the
|
|
349
|
+
* keyed `sidebar.right.pane.tab` seat. `ctx.sidebarRight.openTab(kind)`
|
|
350
|
+
* then opens it AND expands a collapsed column in the same step.
|
|
351
|
+
*/
|
|
352
|
+
const VIZ_KIND = "graft-viz";
|
|
353
|
+
|
|
330
354
|
/**
|
|
331
355
|
* The sidebar tab: `graft viz` in an iframe.
|
|
332
356
|
*
|
|
@@ -370,10 +394,14 @@ window.__ModuleLoader__.load({
|
|
|
370
394
|
const [status, setStatus] = React.useState(null);
|
|
371
395
|
const [reload, setReload] = React.useState(0);
|
|
372
396
|
const [busy, setBusy] = React.useState(false);
|
|
397
|
+
// Visible by default: better-sidebar passes `visible: false` for a tab
|
|
398
|
+
// that exists but is not focused (and asks live views to pause then);
|
|
399
|
+
// dsh's sidebar mounts only what it draws and passes nothing.
|
|
373
400
|
const visible = props?.visible !== false;
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
401
|
+
// Which workspace to resolve. Both hosts supply the session, in their
|
|
402
|
+
// own shape: dsh passes `sessionId` straight to the body, while
|
|
403
|
+
// better-sidebar wraps it in the tab's `scope`.
|
|
404
|
+
const sessionId = props?.sessionId ?? props?.scope?.sessionId;
|
|
377
405
|
|
|
378
406
|
React.useEffect(() => {
|
|
379
407
|
if (!visible || viz !== null) return;
|
|
@@ -630,7 +658,25 @@ window.__ModuleLoader__.load({
|
|
|
630
658
|
return;
|
|
631
659
|
}
|
|
632
660
|
|
|
633
|
-
//
|
|
661
|
+
// Next: dsh's own right sidebar (0.1.5+). `openTab` names the KIND,
|
|
662
|
+
// reveals the column, and reuses the tab that is already open, so a
|
|
663
|
+
// second click focuses the graph rather than stacking another.
|
|
664
|
+
// The body starts the server itself on mount, so nothing is awaited
|
|
665
|
+
// here and no popup-blocker timing applies.
|
|
666
|
+
const right = ctx.get("sidebarRight");
|
|
667
|
+
if (right !== undefined && right !== null && typeof right.openTab === "function") {
|
|
668
|
+
try {
|
|
669
|
+
right.openTab(VIZ_KIND);
|
|
670
|
+
return;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
// openTab throws for a kind nothing registered — which is the
|
|
673
|
+
// case when the tab registry was absent at load. Fall through to
|
|
674
|
+
// the browser tab rather than leaving the click dead.
|
|
675
|
+
console.warn("[graft-status] right sidebar refused the viz tab:", error);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Last resort, for a profile with neither sidebar: a browser tab.
|
|
634
680
|
|
|
635
681
|
// Opened SYNCHRONOUSLY, before any await. A tab opened from a
|
|
636
682
|
// resolved promise is a popup as far as the browser is concerned and
|
|
@@ -812,6 +858,41 @@ window.__ModuleLoader__.load({
|
|
|
812
858
|
// plugin — chip included — from activating in a profile that does not
|
|
813
859
|
// have the sidebar installed. This way the tab appears when the service
|
|
814
860
|
// does, and the button falls back to a browser tab when it never comes.
|
|
861
|
+
// dsh's own right sidebar (0.1.5+). Two registrations: the static type,
|
|
862
|
+
// and the body in the keyed seat under the SAME id. `ctx.inject` rather
|
|
863
|
+
// than the module-level list for the same reason as below — a profile
|
|
864
|
+
// without this sidebar must still get the chip and the button.
|
|
865
|
+
ctx.inject(["sidebarRightTabs", "slots"], (scope) => {
|
|
866
|
+
scope.effect(
|
|
867
|
+
() =>
|
|
868
|
+
scope.sidebarRightTabs.register({
|
|
869
|
+
id: TAB_TYPE,
|
|
870
|
+
kind: VIZ_KIND,
|
|
871
|
+
// A page type, opened by kind: it recognises no resource address,
|
|
872
|
+
// so it declares no patterns. "extension" is the band for a type
|
|
873
|
+
// from outside the product, and the default.
|
|
874
|
+
priority: "extension",
|
|
875
|
+
title: () => "Graft",
|
|
876
|
+
guide: [
|
|
877
|
+
{
|
|
878
|
+
order: 60,
|
|
879
|
+
title: () => "Graft",
|
|
880
|
+
description: () => "The graft graph for this workspace",
|
|
881
|
+
icon: GraphIcon,
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
}),
|
|
885
|
+
"graft-status: right sidebar tab type",
|
|
886
|
+
);
|
|
887
|
+
scope.effect(
|
|
888
|
+
() =>
|
|
889
|
+
scope.slots.inject("sidebar.right.pane.tab", () =>
|
|
890
|
+
scope.slots.register({ name: "sidebar.right.pane.tab", key: TAB_TYPE }, makeVizTab(ctx)),
|
|
891
|
+
),
|
|
892
|
+
"graft-status: right sidebar tab body",
|
|
893
|
+
);
|
|
894
|
+
});
|
|
895
|
+
|
|
815
896
|
ctx.inject(["betterSidebar"], (scope) => {
|
|
816
897
|
scope.effect(
|
|
817
898
|
() =>
|