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 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 as a tab in [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar).
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
- The `graft viz` button needs `dsh-better-sidebar` **mounted**. It ships as a dependency of this plugin (0.18.x), so it is already downloaded — but dsh only mounts what the profile lists:
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 the tab will not mount and the button reports the sidebar as unavailable. Everything else in this plugin — chip, tools, `/graft`, auto-rebuild — is unaffected.
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
- Skip it if you do not want the tab. The chip, the tools, `/graft` and the auto-rebuild all work without it; the button just reports that the sidebar is unavailable.
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
  ![The graph in the sidebar — context view](assets/viz-context.png)
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` tab does not mount (see the sidebar note above) |
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.2",
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",
@@ -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
- check('the browser tab survives only as the fallback', clientSource.includes('no dsh-better-sidebar'))
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
- const CODEC = { mode: "strict", typeSymbol: "graft-status/json", schema: { parse: identity } };
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: { mode: "strict", typeSymbol: "graft-status/json", schema: { parse: identity } },
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
- /** The tab type contributed to dsh-better-sidebar. */
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
- // The sidebar's own scope. Its `sessionId` is what tells the host WHICH
375
- // workspace to resolve — the same session whose cwd the sidebar shows.
376
- const sessionId = props?.scope?.sessionId;
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
- // Fallback for a profile with no dsh-better-sidebar: a browser tab.
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
  () =>