sh3-core 0.14.0 → 0.15.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.
Files changed (63) hide show
  1. package/dist/api.d.ts +3 -1
  2. package/dist/api.js +4 -0
  3. package/dist/contributions/index.d.ts +1 -1
  4. package/dist/contributions/index.js +1 -1
  5. package/dist/contributions/registry.d.ts +7 -0
  6. package/dist/contributions/registry.js +24 -4
  7. package/dist/contributions/registry.test.js +56 -1
  8. package/dist/contributions/types.d.ts +9 -0
  9. package/dist/layout/LayoutRenderer.svelte +1 -1
  10. package/dist/layout/tree-walk.js +6 -1
  11. package/dist/layout/types.d.ts +7 -0
  12. package/dist/overlays/FloatFrame.svelte +8 -2
  13. package/dist/overlays/float.js +6 -3
  14. package/dist/overlays/float.test.js +71 -0
  15. package/dist/primitives/widgets/IconToggleGroup.svelte +4 -1
  16. package/dist/primitives/widgets/Segmented.svelte +4 -1
  17. package/dist/runtime/index.d.ts +2 -0
  18. package/dist/runtime/index.js +1 -0
  19. package/dist/runtime/runVerb.d.ts +10 -0
  20. package/dist/runtime/runVerb.js +97 -0
  21. package/dist/runtime/runVerb.test.d.ts +1 -0
  22. package/dist/runtime/runVerb.test.js +132 -0
  23. package/dist/sh3core-shard/AppInfoView.svelte +154 -0
  24. package/dist/sh3core-shard/AppInfoView.svelte.d.ts +11 -0
  25. package/dist/sh3core-shard/appActions.js +23 -5
  26. package/dist/shards/activate-contributions.test.js +31 -0
  27. package/dist/shards/activate-runtime.test.d.ts +1 -0
  28. package/dist/shards/activate-runtime.test.js +179 -0
  29. package/dist/shards/activate.svelte.js +20 -3
  30. package/dist/shards/registry.d.ts +11 -1
  31. package/dist/shards/registry.js +16 -4
  32. package/dist/shards/registry.test.js +24 -16
  33. package/dist/shards/types.d.ts +38 -1
  34. package/dist/shell-shard/ScrollbackView.svelte +40 -19
  35. package/dist/shell-shard/Terminal.svelte +55 -4
  36. package/dist/shell-shard/contract.d.ts +34 -0
  37. package/dist/shell-shard/dispatch-custom.test.js +48 -0
  38. package/dist/shell-shard/dispatch-gating.test.d.ts +1 -0
  39. package/dist/shell-shard/dispatch-gating.test.js +63 -0
  40. package/dist/shell-shard/dispatch-invoke.test.d.ts +1 -0
  41. package/dist/shell-shard/dispatch-invoke.test.js +214 -0
  42. package/dist/shell-shard/dispatch.d.ts +9 -1
  43. package/dist/shell-shard/dispatch.js +73 -2
  44. package/dist/shell-shard/output.d.ts +8 -1
  45. package/dist/shell-shard/output.js +17 -1
  46. package/dist/shell-shard/output.test.js +24 -5
  47. package/dist/shell-shard/registry-resolve.test.d.ts +1 -0
  48. package/dist/shell-shard/registry-resolve.test.js +26 -0
  49. package/dist/shell-shard/registry.d.ts +12 -1
  50. package/dist/shell-shard/registry.js +12 -1
  51. package/dist/shell-shard/shellApi.d.ts +3 -0
  52. package/dist/shell-shard/shellApi.js +142 -0
  53. package/dist/shell-shard/shellShard.svelte.d.ts +1 -7
  54. package/dist/shell-shard/shellShard.svelte.js +8 -163
  55. package/dist/shell-shard/terminal-dispatch.test.js +10 -3
  56. package/dist/shell-shard/toolbar/slots/BusySlot.svelte +35 -0
  57. package/dist/shell-shard/toolbar/slots/BusySlot.svelte.d.ts +7 -0
  58. package/dist/shell-shard/verbs/clear.js +1 -0
  59. package/dist/shell-shard/verbs/mode.js +1 -0
  60. package/dist/verbs/types.d.ts +68 -0
  61. package/dist/version.d.ts +1 -1
  62. package/dist/version.js +1 -1
  63. package/package.json +1 -1
@@ -0,0 +1,154 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Read-only details modal for an app, opened by the home-card `app.info`
4
+ * action. Pulls the manifest from listRegisteredApps() and — when the app
5
+ * was installed from a registry — augments it with the catalog entry
6
+ * (description, author) and the InstalledPackage record (source registry,
7
+ * install date, contract version). Built-in apps show only manifest fields.
8
+ */
9
+
10
+ import type { AppManifest } from '../apps/types';
11
+ import type { InstalledPackage, PackageEntry } from '../registry/types';
12
+
13
+ interface Props {
14
+ manifest: AppManifest;
15
+ installed: InstalledPackage | null;
16
+ catalogEntry: PackageEntry | null;
17
+ close: () => void;
18
+ }
19
+
20
+ let { manifest, installed, catalogEntry, close }: Props = $props();
21
+
22
+ let installedDate = $derived.by(() => {
23
+ if (!installed) return null;
24
+ const d = new Date(installed.installedAt);
25
+ return isNaN(d.getTime()) ? installed.installedAt : d.toLocaleString();
26
+ });
27
+ </script>
28
+
29
+ <div class="app-info-modal">
30
+ <header>
31
+ <h2>{manifest.label}</h2>
32
+ <code class="id">{manifest.id}</code>
33
+ </header>
34
+
35
+ <p class="version">
36
+ <span class="label">Version</span>
37
+ <code>v{manifest.version}</code>
38
+ {#if !installed}<span class="badge built-in">built-in</span>{/if}
39
+ </p>
40
+
41
+ {#if catalogEntry?.description}
42
+ <section class="description">
43
+ <span class="label">Description</span>
44
+ <p>{catalogEntry.description}</p>
45
+ </section>
46
+ {/if}
47
+
48
+ {#if catalogEntry?.author?.name}
49
+ <p class="row">
50
+ <span class="label">Author</span>
51
+ <span>{catalogEntry.author.name}</span>
52
+ </p>
53
+ {/if}
54
+
55
+ {#if manifest.requiredShards.length > 0}
56
+ <section class="row">
57
+ <span class="label">Required shards</span>
58
+ <ul class="chips">
59
+ {#each manifest.requiredShards as shard}
60
+ <li><code>{shard}</code></li>
61
+ {/each}
62
+ </ul>
63
+ </section>
64
+ {/if}
65
+
66
+ {#if manifest.permissions && manifest.permissions.length > 0}
67
+ <section class="row">
68
+ <span class="label">Permissions</span>
69
+ <ul class="chips">
70
+ {#each manifest.permissions as perm}
71
+ <li><code>{perm}</code></li>
72
+ {/each}
73
+ </ul>
74
+ </section>
75
+ {/if}
76
+
77
+ {#if installed}
78
+ <section class="installed">
79
+ <p class="row"><span class="label">Source registry</span><code>{installed.sourceRegistry}</code></p>
80
+ {#if installedDate}
81
+ <p class="row"><span class="label">Installed</span><span>{installedDate}</span></p>
82
+ {/if}
83
+ <p class="row"><span class="label">Contract</span><code>v{installed.contractVersion}</code></p>
84
+ </section>
85
+ {/if}
86
+
87
+ <div class="actions">
88
+ <button type="button" onclick={close}>Close</button>
89
+ </div>
90
+ </div>
91
+
92
+ <style>
93
+ .app-info-modal {
94
+ padding: 16px 20px;
95
+ min-width: 360px;
96
+ max-width: 520px;
97
+ color: var(--shell-fg);
98
+ background: var(--shell-bg);
99
+ font: inherit;
100
+ }
101
+ header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
102
+ header h2 { margin: 0; font-size: 16px; }
103
+ header .id {
104
+ font-size: 12px;
105
+ color: var(--shell-fg-muted);
106
+ }
107
+ .version { margin: 4px 0 12px; font-size: 13px; display: flex; align-items: center; gap: 8px; }
108
+ .description { margin: 8px 0 12px; }
109
+ .description p {
110
+ margin: 4px 0 0;
111
+ font-size: 13px;
112
+ line-height: 1.5;
113
+ white-space: pre-wrap;
114
+ }
115
+ .row { margin: 6px 0; font-size: 13px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
116
+ .label {
117
+ color: var(--shell-fg-muted);
118
+ font-size: 11px;
119
+ text-transform: uppercase;
120
+ letter-spacing: 0.04em;
121
+ flex: 0 0 auto;
122
+ min-width: 90px;
123
+ }
124
+ .chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; }
125
+ .chips code {
126
+ padding: 1px 6px;
127
+ border: 1px solid var(--shell-border);
128
+ border-radius: var(--shell-radius-sm, 3px);
129
+ }
130
+ .badge {
131
+ font-size: 11px;
132
+ padding: 1px 6px;
133
+ border-radius: var(--shell-radius-sm, 3px);
134
+ background: var(--shell-bg-elevated);
135
+ color: var(--shell-fg-muted);
136
+ border: 1px solid var(--shell-border);
137
+ }
138
+ .installed { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--shell-border); }
139
+ code {
140
+ font-family: var(--shell-font-mono, monospace);
141
+ background: var(--shell-bg-elevated);
142
+ padding: 0 4px;
143
+ border-radius: var(--shell-radius-sm, 3px);
144
+ }
145
+ .actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
146
+ .actions button {
147
+ background: var(--shell-bg-elevated);
148
+ color: var(--shell-fg);
149
+ border: 1px solid var(--shell-border);
150
+ border-radius: var(--shell-radius-sm, 3px);
151
+ padding: 6px 14px; font: inherit; cursor: pointer;
152
+ }
153
+ .actions button:hover { border-color: var(--shell-accent); }
154
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { AppManifest } from '../apps/types';
2
+ import type { InstalledPackage, PackageEntry } from '../registry/types';
3
+ interface Props {
4
+ manifest: AppManifest;
5
+ installed: InstalledPackage | null;
6
+ catalogEntry: PackageEntry | null;
7
+ close: () => void;
8
+ }
9
+ declare const AppInfoView: import("svelte").Component<Props, {}, "">;
10
+ type AppInfoView = ReturnType<typeof AppInfoView>;
11
+ export default AppInfoView;
@@ -5,7 +5,8 @@
5
5
  * 'app' (see ShellHome.svelte).
6
6
  *
7
7
  * Three actions are registered here:
8
- * - app.info : info-only row showing "<id> v<version>" (always disabled).
8
+ * - app.info : open the AppInfoView modal with manifest + (when
9
+ * installed) catalog/install metadata. Always enabled.
9
10
  * - app.checkUpdate : refresh catalog, then prompt update or toast up-to-date.
10
11
  * - app.uninstall : open uninstall confirm dialog.
11
12
  *
@@ -25,6 +26,7 @@ import { modalManager } from '../overlays/modal';
25
26
  import { toastManager } from '../overlays/toast';
26
27
  import AppUpdateAvailableModal from '../app/store/AppUpdateAvailableModal.svelte';
27
28
  import UninstallAppDialog from '../app/store/UninstallAppDialog.svelte';
29
+ import AppInfoView from './AppInfoView.svelte';
28
30
  export function computeAppActionDisabled(g) {
29
31
  return !g.admin || g.builtin;
30
32
  }
@@ -50,6 +52,22 @@ function isBuiltin(appId) {
50
52
  function gateFor(appId) {
51
53
  return { admin: isAdmin(), builtin: isBuiltin(appId) };
52
54
  }
55
+ function runShowInfo(_ctx) {
56
+ var _a, _b, _c;
57
+ const ref = readSelection();
58
+ if (!ref)
59
+ return;
60
+ const manifest = findApp(ref.appId);
61
+ if (!manifest)
62
+ return;
63
+ const installed = (_a = storeContext.state.ephemeral.installed.find((p) => p.id === ref.appId)) !== null && _a !== void 0 ? _a : null;
64
+ // Catalog entry only exists for apps fetched from a registry — built-in
65
+ // apps will fall through to null and the modal will hide registry-only
66
+ // fields (description, author).
67
+ const catalogEntry = (_c = (_b = storeContext.state.ephemeral.catalog.find((p) => p.entry.id === ref.appId)) === null || _b === void 0 ? void 0 : _b.entry) !== null && _c !== void 0 ? _c : null;
68
+ const props = { manifest, installed, catalogEntry };
69
+ modalManager.open(AppInfoView, props);
70
+ }
53
71
  async function runCheckUpdate(_ctx) {
54
72
  var _a, _b;
55
73
  const ref = readSelection();
@@ -124,11 +142,11 @@ export function registerAppActions(ctx) {
124
142
  const infoLabel = () => {
125
143
  const ref = readSelection();
126
144
  if (!ref)
127
- return '';
145
+ return 'Info…';
128
146
  const m = findApp(ref.appId);
129
147
  if (!m)
130
- return ref.appId;
131
- return `${m.id} v${m.version}`;
148
+ return `Info — ${ref.appId}`;
149
+ return `Info — ${m.label} v${m.version}`;
132
150
  };
133
151
  const updateLabel = () => {
134
152
  const ref = readSelection();
@@ -155,7 +173,7 @@ export function registerAppActions(ctx) {
155
173
  scope: { element: 'app' },
156
174
  contextItem: true,
157
175
  group: 'info',
158
- disabled: true,
176
+ run: runShowInfo,
159
177
  },
160
178
  {
161
179
  id: 'app.checkUpdate',
@@ -107,4 +107,35 @@ describe('ctx.contributions', () => {
107
107
  // Deactivate must not throw when the entry is already gone.
108
108
  expect(() => deactivateShard('p')).not.toThrow();
109
109
  });
110
+ it('onAnyChange fires for register/unregister at any point and auto-unsubscribes on deactivate', async () => {
111
+ const cb = vi.fn();
112
+ registerShard({
113
+ manifest: { id: 'watcher', label: 'W', version: '0.0.0', views: [] },
114
+ activate(ctx) {
115
+ ctx.contributions.onAnyChange(cb);
116
+ },
117
+ });
118
+ await activateShard('watcher');
119
+ registerShard({
120
+ manifest: { id: 'p1', label: 'P1', version: '0.0.0', views: [] },
121
+ activate(ctx) {
122
+ ctx.contributions.register('point.a', { id: 'x' });
123
+ ctx.contributions.register('point.b', { id: 'y' });
124
+ },
125
+ });
126
+ await activateShard('p1');
127
+ expect(cb).toHaveBeenCalledTimes(2);
128
+ expect(cb).toHaveBeenCalledWith('point.a');
129
+ expect(cb).toHaveBeenCalledWith('point.b');
130
+ deactivateShard('watcher');
131
+ cb.mockClear();
132
+ registerShard({
133
+ manifest: { id: 'p2', label: 'P2', version: '0.0.0', views: [] },
134
+ activate(ctx) {
135
+ ctx.contributions.register('point.c', { id: 'z' });
136
+ },
137
+ });
138
+ await activateShard('p2');
139
+ expect(cb).not.toHaveBeenCalled();
140
+ });
110
141
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { MemoryDocumentBackend } from '../documents/backends';
3
+ import { __setDocumentBackend, __setTenantId } from '../documents/config';
4
+ import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
5
+ import { __resetViewRegistryForTest } from './registry';
6
+ function programmaticVerb(name, summary, body) {
7
+ return {
8
+ name,
9
+ summary,
10
+ programmatic: true,
11
+ async run(vctx, args) {
12
+ if (body)
13
+ await body(vctx, args);
14
+ },
15
+ };
16
+ }
17
+ function plainVerb(name, summary) {
18
+ return { name, summary, async run() { } };
19
+ }
20
+ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
21
+ beforeEach(() => {
22
+ __resetShardRegistryForTest();
23
+ __resetViewRegistryForTest();
24
+ __setDocumentBackend(new MemoryDocumentBackend());
25
+ __setTenantId('tenant-test');
26
+ });
27
+ it('listVerbs returns every verb across active shards with shardId', async () => {
28
+ registerShard({
29
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
30
+ activate(ctx) {
31
+ ctx.registerVerb(programmaticVerb('a', 'first verb'));
32
+ ctx.registerVerb(plainVerb('b', 'second verb'));
33
+ },
34
+ });
35
+ let consumerCtx = null;
36
+ registerShard({
37
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
38
+ activate(ctx) {
39
+ consumerCtx = ctx;
40
+ },
41
+ });
42
+ await activateShard('host');
43
+ await activateShard('consumer');
44
+ const list = consumerCtx.listVerbs();
45
+ const a = list.find((v) => v.name === 'host:a');
46
+ const b = list.find((v) => v.name === 'host:b');
47
+ expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', schema: undefined });
48
+ expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', schema: undefined });
49
+ });
50
+ it('runVerb dispatches a programmatic verb and returns captured scrollback', async () => {
51
+ registerShard({
52
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
53
+ activate(ctx) {
54
+ ctx.registerVerb(programmaticVerb('echo', 'echoes args', async (vctx, args) => {
55
+ vctx.scrollback.push({ kind: 'status', text: `echo: ${args.join(' ')}`, level: 'info', ts: 0 });
56
+ }));
57
+ },
58
+ });
59
+ let consumerCtx = null;
60
+ registerShard({
61
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
62
+ activate(ctx) {
63
+ consumerCtx = ctx;
64
+ },
65
+ });
66
+ await activateShard('host');
67
+ await activateShard('consumer');
68
+ const out = await consumerCtx.runVerb('host', 'host:echo', ['hello']);
69
+ expect(out.scrollback).toHaveLength(1);
70
+ expect(out.scrollback[0]).toMatchObject({ kind: 'status', text: 'echo: hello' });
71
+ });
72
+ it('runVerb rejects when the target verb is not programmatic', async () => {
73
+ registerShard({
74
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
75
+ activate(ctx) {
76
+ ctx.registerVerb(plainVerb('plain', 'no opt-in'));
77
+ },
78
+ });
79
+ let consumerCtx = null;
80
+ registerShard({
81
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
82
+ activate(ctx) {
83
+ consumerCtx = ctx;
84
+ },
85
+ });
86
+ await activateShard('host');
87
+ await activateShard('consumer');
88
+ await expect(consumerCtx.runVerb('host', 'host:plain', [])).rejects.toThrow('verb "host:plain" is not programmatic');
89
+ });
90
+ it('runVerb populates ctx.structuredArgs when opts.structured is set', async () => {
91
+ let observed = undefined;
92
+ registerShard({
93
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
94
+ activate(ctx) {
95
+ ctx.registerVerb(programmaticVerb('schemaCheck', 'reads structuredArgs', async (vctx) => {
96
+ observed = vctx.structuredArgs;
97
+ }));
98
+ },
99
+ });
100
+ let consumerCtx = null;
101
+ registerShard({
102
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
103
+ activate(ctx) {
104
+ consumerCtx = ctx;
105
+ },
106
+ });
107
+ await activateShard('host');
108
+ await activateShard('consumer');
109
+ await consumerCtx.runVerb('host', 'host:schemaCheck', [], { structured: { foo: 'bar' } });
110
+ expect(observed).toEqual({ foo: 'bar' });
111
+ });
112
+ it('runVerb rejects on unknown shardId', async () => {
113
+ let consumerCtx = null;
114
+ registerShard({
115
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
116
+ activate(ctx) {
117
+ consumerCtx = ctx;
118
+ },
119
+ });
120
+ await activateShard('consumer');
121
+ await expect(consumerCtx.runVerb('missing', 'x', [])).rejects.toThrow('unknown shard: missing');
122
+ });
123
+ it('runVerb rejects on unknown verb', async () => {
124
+ registerShard({
125
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
126
+ activate(ctx) {
127
+ ctx.registerVerb(programmaticVerb('present', 'exists'));
128
+ },
129
+ });
130
+ let consumerCtx = null;
131
+ registerShard({
132
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
133
+ activate(ctx) {
134
+ consumerCtx = ctx;
135
+ },
136
+ });
137
+ await activateShard('host');
138
+ await activateShard('consumer');
139
+ await expect(consumerCtx.runVerb('host', 'host:absent', [])).rejects.toThrow('unknown verb: host:absent');
140
+ });
141
+ it('verbs declaring schema.input expose it via listVerbs', async () => {
142
+ registerShard({
143
+ manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
144
+ activate(ctx) {
145
+ ctx.registerVerb({
146
+ name: 'typed',
147
+ summary: 'has schema',
148
+ programmatic: true,
149
+ schema: {
150
+ input: {
151
+ type: 'object',
152
+ properties: { msg: { type: 'string', description: 'message' } },
153
+ required: ['msg'],
154
+ },
155
+ },
156
+ async run() { },
157
+ });
158
+ },
159
+ });
160
+ let consumerCtx = null;
161
+ registerShard({
162
+ manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
163
+ activate(ctx) {
164
+ consumerCtx = ctx;
165
+ },
166
+ });
167
+ await activateShard('host');
168
+ await activateShard('consumer');
169
+ const list = consumerCtx.listVerbs();
170
+ const typed = list.find((v) => v.name === 'host:typed');
171
+ expect(typed === null || typed === void 0 ? void 0 : typed.schema).toEqual({
172
+ input: {
173
+ type: 'object',
174
+ properties: { msg: { type: 'string', description: 'message' } },
175
+ required: ['msg'],
176
+ },
177
+ });
178
+ });
179
+ });
@@ -17,7 +17,8 @@
17
17
  * stays in `registeredShards` — it's still known, just not running.
18
18
  */
19
19
  import { shell } from '../shellRuntime.svelte';
20
- import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb } from './registry';
20
+ import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb, listVerbsWithShard } from './registry';
21
+ import { runVerbProgrammatic } from '../runtime/runVerb';
21
22
  import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
22
23
  import { fetchEnvState, putEnvState } from '../env/client';
23
24
  import { isAdmin as checkIsAdmin } from '../auth/index';
@@ -28,7 +29,7 @@ import { createBrowseCapability } from '../documents/browse';
28
29
  import { createShardKeysApi } from '../keys/client';
29
30
  import { PERMISSION_KEYS_MINT } from '../keys/types';
30
31
  import { subscribe } from '../keys/revocation-bus.svelte';
31
- import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, } from '../contributions';
32
+ import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
32
33
  import { registerAction } from '../actions/registry';
33
34
  import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
34
35
  import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette } from '../actions/listeners';
@@ -122,6 +123,11 @@ export async function activateShard(id, opts) {
122
123
  entry.cleanupFns.push(async () => off());
123
124
  return off;
124
125
  },
126
+ onAnyChange(cb) {
127
+ const off = contributionsOnAnyChange(cb);
128
+ entry.cleanupFns.push(async () => off());
129
+ return off;
130
+ },
125
131
  };
126
132
  const ctx = {
127
133
  state: (schema) => shell.state(id, schema),
@@ -131,7 +137,7 @@ export async function activateShard(id, opts) {
131
137
  },
132
138
  registerVerb: (verb) => {
133
139
  const prefixed = id === 'shell' ? verb.name : `${id}:${verb.name}`;
134
- fwRegisterVerb(prefixed, Object.assign(Object.assign({}, verb), { name: prefixed }));
140
+ fwRegisterVerb(prefixed, Object.assign(Object.assign({}, verb), { name: prefixed }), id);
135
141
  entry.verbNames.add(prefixed);
136
142
  },
137
143
  documents: (options) => {
@@ -195,6 +201,17 @@ export async function activateShard(id, opts) {
195
201
  openContextMenu(opts) { shellOpenContextMenu(opts); },
196
202
  openPalette(opts) { shellOpenPalette(opts); },
197
203
  },
204
+ listVerbs() {
205
+ return listVerbsWithShard().map(({ verb, shardId }) => ({
206
+ shardId,
207
+ name: verb.name,
208
+ summary: verb.summary,
209
+ schema: verb.schema,
210
+ }));
211
+ },
212
+ runVerb(shardId, name, args, opts) {
213
+ return runVerbProgrammatic(shardId, name, args, opts);
214
+ },
198
215
  };
199
216
  entry.ctx = ctx;
200
217
  // Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
@@ -5,9 +5,19 @@ export declare function registerView(viewId: string, factory: ViewFactory): void
5
5
  export declare function getView(viewId: string): ViewFactory | undefined;
6
6
  export declare function unregisterView(viewId: string): void;
7
7
  import type { Verb } from '../verbs/types';
8
- export declare function registerVerb(name: string, verb: Verb): void;
8
+ export declare function registerVerb(name: string, verb: Verb, shardId: string): void;
9
9
  export declare function getVerb(name: string): Verb | undefined;
10
10
  export declare function unregisterVerb(name: string): void;
11
11
  export declare function listVerbs(): Verb[];
12
+ /**
13
+ * Like `listVerbs`, but also exposes the `shardId` each verb belongs to.
14
+ * Used by `ctx.listVerbs()` so callers don't have to parse the
15
+ * shardId-prefixed verb name. Order is undefined — callers that want
16
+ * sorted output sort themselves.
17
+ */
18
+ export declare function listVerbsWithShard(): Array<{
19
+ verb: Verb;
20
+ shardId: string;
21
+ }>;
12
22
  /** Test-only reset: clear the view and verb registries. */
13
23
  export declare function __resetViewRegistryForTest(): void;
@@ -38,20 +38,32 @@ export function unregisterView(viewId) {
38
38
  views.delete(viewId);
39
39
  }
40
40
  const verbs = new Map();
41
- export function registerVerb(name, verb) {
41
+ export function registerVerb(name, verb, shardId) {
42
42
  if (verbs.has(name)) {
43
43
  throw new Error(`Verb "${name}" is already registered`);
44
44
  }
45
- verbs.set(name, verb);
45
+ verbs.set(name, { verb, shardId });
46
46
  }
47
47
  export function getVerb(name) {
48
- return verbs.get(name);
48
+ var _a;
49
+ return (_a = verbs.get(name)) === null || _a === void 0 ? void 0 : _a.verb;
49
50
  }
50
51
  export function unregisterVerb(name) {
51
52
  verbs.delete(name);
52
53
  }
53
54
  export function listVerbs() {
54
- return Array.from(verbs.values()).sort((a, b) => a.name.localeCompare(b.name));
55
+ return Array.from(verbs.values())
56
+ .map((entry) => entry.verb)
57
+ .sort((a, b) => a.name.localeCompare(b.name));
58
+ }
59
+ /**
60
+ * Like `listVerbs`, but also exposes the `shardId` each verb belongs to.
61
+ * Used by `ctx.listVerbs()` so callers don't have to parse the
62
+ * shardId-prefixed verb name. Order is undefined — callers that want
63
+ * sorted output sort themselves.
64
+ */
65
+ export function listVerbsWithShard() {
66
+ return Array.from(verbs.values()).map((entry) => (Object.assign({}, entry)));
55
67
  }
56
68
  /** Test-only reset: clear the view and verb registries. */
57
69
  export function __resetViewRegistryForTest() {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { registerVerb, getVerb, unregisterVerb, listVerbs, } from './registry';
2
+ import { registerVerb, getVerb, unregisterVerb, listVerbs, listVerbsWithShard, } from './registry';
3
3
  function makeStubVerb(name) {
4
4
  return { name, summary: `stub ${name}`, run: async () => { } };
5
5
  }
@@ -10,53 +10,61 @@ describe('verb registry', () => {
10
10
  unregisterVerb(name);
11
11
  registered.length = 0;
12
12
  });
13
- function trackVerb(name, verb) {
14
- registerVerb(name, verb);
13
+ function trackVerb(name, verb, shardId) {
14
+ registerVerb(name, verb, shardId);
15
15
  registered.push(name);
16
16
  }
17
17
  it('registers and retrieves a verb by name', () => {
18
18
  const verb = makeStubVerb('foo');
19
- trackVerb('foo', verb);
19
+ trackVerb('foo', verb, 'shell');
20
20
  expect(getVerb('foo')).toBe(verb);
21
21
  });
22
22
  it('returns undefined for unknown verb', () => {
23
23
  expect(getVerb('nope')).toBeUndefined();
24
24
  });
25
25
  it('throws on duplicate verb name', () => {
26
- trackVerb('dup', makeStubVerb('dup'));
27
- expect(() => trackVerb('dup', makeStubVerb('dup'))).toThrowError('Verb "dup" is already registered');
26
+ trackVerb('dup', makeStubVerb('dup'), 'shell');
27
+ expect(() => trackVerb('dup', makeStubVerb('dup'), 'shell')).toThrowError('Verb "dup" is already registered');
28
28
  });
29
29
  it('unregisters a verb', () => {
30
- trackVerb('gone', makeStubVerb('gone'));
30
+ trackVerb('gone', makeStubVerb('gone'), 'shell');
31
31
  unregisterVerb('gone');
32
32
  registered.pop();
33
33
  expect(getVerb('gone')).toBeUndefined();
34
34
  });
35
- it('lists verbs sorted by name', () => {
36
- trackVerb('zeta', makeStubVerb('zeta'));
37
- trackVerb('alpha', makeStubVerb('alpha'));
38
- trackVerb('mid', makeStubVerb('mid'));
35
+ it('lists verbs sorted by name (legacy shape)', () => {
36
+ trackVerb('zeta', makeStubVerb('zeta'), 'shell');
37
+ trackVerb('alpha', makeStubVerb('alpha'), 'shell');
38
+ trackVerb('mid', makeStubVerb('mid'), 'shell');
39
39
  const names = listVerbs().map((v) => v.name);
40
40
  expect(names).toEqual(['alpha', 'mid', 'zeta']);
41
41
  });
42
+ it('listVerbsWithShard returns shardId per entry', () => {
43
+ trackVerb('apps', makeStubVerb('apps'), 'shell');
44
+ trackVerb('sh3-store:install', makeStubVerb('sh3-store:install'), 'sh3-store');
45
+ const result = listVerbsWithShard();
46
+ const apps = result.find((e) => e.verb.name === 'apps');
47
+ const install = result.find((e) => e.verb.name === 'sh3-store:install');
48
+ expect(apps === null || apps === void 0 ? void 0 : apps.shardId).toBe('shell');
49
+ expect(install === null || install === void 0 ? void 0 : install.shardId).toBe('sh3-store');
50
+ });
42
51
  it('stores prefixed name inside verb object (mirrors activate auto-prefix)', () => {
43
- // activate.svelte.ts does: { ...verb, name: prefixed }
44
52
  const original = makeStubVerb('install');
45
53
  const prefixed = Object.assign(Object.assign({}, original), { name: 'registry:install' });
46
- trackVerb('registry:install', prefixed);
54
+ trackVerb('registry:install', prefixed, 'registry');
47
55
  const found = getVerb('registry:install');
48
56
  expect(found === null || found === void 0 ? void 0 : found.name).toBe('registry:install');
49
57
  expect(found === null || found === void 0 ? void 0 : found.summary).toBe('stub install');
50
58
  });
51
59
  it('bulk unregister simulates deactivate cleanup', () => {
52
- // activate.svelte.ts tracks verbNames and unregisters on deactivate
53
60
  const names = ['registry:install', 'registry:search', 'registry:info'];
54
61
  for (const name of names)
55
- trackVerb(name, makeStubVerb(name));
62
+ trackVerb(name, makeStubVerb(name), 'registry');
56
63
  expect(listVerbs()).toHaveLength(3);
57
64
  for (const name of names)
58
65
  unregisterVerb(name);
59
- registered.length = 0; // already cleaned
66
+ registered.length = 0;
60
67
  expect(listVerbs()).toHaveLength(0);
68
+ expect(listVerbsWithShard()).toHaveLength(0);
61
69
  });
62
70
  });