sh3-core 0.14.3 → 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.
@@ -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
  });
@@ -3,7 +3,8 @@ import type { ZoneSchema, ZoneManager } from '../state/types';
3
3
  import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
4
4
  import type { BrowseCapability } from '../documents/browse';
5
5
  import type { EnvState } from '../env/types';
6
- import type { Verb } from '../verbs/types';
6
+ import type { Verb, VerbSchema } from '../verbs/types';
7
+ import type { ScrollbackEntry } from '../shell-shard/scrollback.svelte';
7
8
  import type { ShardContextKeys } from '../keys/types';
8
9
  import type { ContributionsApi } from '../contributions/types';
9
10
  import type { ActionsApi } from '../actions/types';
@@ -251,6 +252,42 @@ export interface ShardContext {
251
252
  * entries). Actions are auto-unregistered when the shard deactivates.
252
253
  */
253
254
  actions: ActionsApi;
255
+ /**
256
+ * Read-only snapshot of every verb registered across every active shard.
257
+ * Returned entries include the contributing `shardId`, the prefixed
258
+ * `name`, the verb's `summary`, and (when present) its `schema`.
259
+ * Order is undefined.
260
+ *
261
+ * No permission gate — verb names + summaries are already visible via
262
+ * the `help` verb. Diagnostic and AI-class shards (sh3-ai, sh3-diagnostic)
263
+ * use this to enumerate the host's action surface.
264
+ */
265
+ listVerbs(): Array<{
266
+ shardId: string;
267
+ name: string;
268
+ summary: string;
269
+ schema?: VerbSchema;
270
+ }>;
271
+ /**
272
+ * Programmatically dispatch a verb by `(shardId, name)`. Resolves with
273
+ * `{ result, scrollback }` where `scrollback` is the array of entries
274
+ * the verb pushed during invocation. Rejects on:
275
+ * - unknown shardId,
276
+ * - unknown verb,
277
+ * - target verb not opted in via `programmatic: true`,
278
+ * - any error thrown by the verb's `run`.
279
+ *
280
+ * Pass `opts.structured` to populate `ctx.structuredArgs` for verbs
281
+ * that declare `schema.input`. Pass `opts.signal` for cooperative
282
+ * cancellation (verbs must opt in to honor it).
283
+ */
284
+ runVerb(shardId: string, name: string, args: string[], opts?: {
285
+ signal?: AbortSignal;
286
+ structured?: unknown;
287
+ }): Promise<{
288
+ result: unknown;
289
+ scrollback: ScrollbackEntry[];
290
+ }>;
254
291
  }
255
292
  /**
256
293
  * A shard module. Shards are the fundamental unit of contribution in SH3.
@@ -6,8 +6,8 @@ const globalVerb = { name: 'clear', summary: '', globalVerb: true, async run() {
6
6
  describe('VerbRegistry.resolve — globalOnly option', () => {
7
7
  beforeEach(() => {
8
8
  __resetViewRegistryForTest();
9
- registerVerb('apps', sh3Verb);
10
- registerVerb('clear', globalVerb);
9
+ registerVerb('apps', sh3Verb, 'shell');
10
+ registerVerb('clear', globalVerb, 'shell');
11
11
  });
12
12
  it('without globalOnly resolves any registered verb', () => {
13
13
  const r = new VerbRegistry();
@@ -0,0 +1,3 @@
1
+ import type { ShellApi } from './registry';
2
+ export declare function makeShellApiHeadless(): ShellApi;
3
+ export declare function makeShellApiForTest(): ShellApi;
@@ -0,0 +1,142 @@
1
+ /*
2
+ * Headless ShellApi factory.
3
+ *
4
+ * Built only from framework primitives (apps registry, layout inspection,
5
+ * auth, float manager). No Svelte component imports — safe to load from
6
+ * any test project, including the node-only project, and from
7
+ * runtime/runVerb.ts which builds a synthesized VerbContext.
8
+ *
9
+ * Mode-related methods (`setMode`, `listModes`) stub to false / [];
10
+ * Terminal.svelte wraps this with mode-aware closures.
11
+ */
12
+ import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
13
+ import { launchApp } from '../apps/lifecycle';
14
+ import { registeredShards, listStandaloneViews } from '../shards/activate.svelte';
15
+ import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIntoActiveLayout, locateSlot as locateSlotInActiveLayout, } from '../layout/inspection';
16
+ import { floatManager } from '../overlays/float';
17
+ import { getUser, isAdmin } from '../auth/index';
18
+ function collectTabEntries(node) {
19
+ if (node.type === 'tabs') {
20
+ return node.tabs.filter((t) => t.viewId !== null);
21
+ }
22
+ if (node.type === 'split') {
23
+ return node.children.flatMap(collectTabEntries);
24
+ }
25
+ if (node.viewId !== null) {
26
+ return [{ slotId: node.slotId, viewId: node.viewId, label: node.viewId }];
27
+ }
28
+ return [];
29
+ }
30
+ export function makeShellApiHeadless() {
31
+ return {
32
+ listApps() {
33
+ return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
34
+ },
35
+ getActiveApp() {
36
+ const m = getActiveApp();
37
+ return m ? { id: m.id, label: m.label } : null;
38
+ },
39
+ launchApp(id) {
40
+ void launchApp(id);
41
+ },
42
+ listShards() {
43
+ return Array.from(registeredShards.values()).map((s) => ({
44
+ id: s.manifest.id,
45
+ label: s.manifest.label,
46
+ version: s.manifest.version,
47
+ }));
48
+ },
49
+ listViewsInCurrentLayout() {
50
+ try {
51
+ const { root } = inspectActiveLayout();
52
+ return collectTabEntries(root.docked).map((t) => {
53
+ var _a;
54
+ return ({
55
+ slotId: t.slotId,
56
+ viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
57
+ label: t.label,
58
+ });
59
+ });
60
+ }
61
+ catch (_a) {
62
+ return [];
63
+ }
64
+ },
65
+ openViewInCurrentLayout(viewId) {
66
+ try {
67
+ if (focusView(viewId))
68
+ return { ok: true };
69
+ const standalone = listStandaloneViews().find((v) => v.viewId === viewId);
70
+ if (standalone) {
71
+ const slotId = `standalone:${viewId}:${Date.now()}`;
72
+ const ok = dockIntoActiveLayout({ slotId, viewId, label: standalone.label });
73
+ return ok ? { ok: true } : { ok: false, error: `could not dock "${viewId}" — no available slot` };
74
+ }
75
+ return { ok: false, error: `view "${viewId}" not found in current layout` };
76
+ }
77
+ catch (err) {
78
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
79
+ }
80
+ },
81
+ listStandaloneViews() {
82
+ return listStandaloneViews();
83
+ },
84
+ popoutSlot(slotId) {
85
+ try {
86
+ const floatId = popoutView(slotId);
87
+ return floatId ? { ok: true, floatId } : { ok: false, error: `slot "${slotId}" not found in docked tree` };
88
+ }
89
+ catch (err) {
90
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
91
+ }
92
+ },
93
+ dockFloat(floatId) {
94
+ try {
95
+ const ok = dockFloat(floatId);
96
+ return ok ? { ok: true } : { ok: false, error: `float "${floatId}" not found or has no dockable content` };
97
+ }
98
+ catch (err) {
99
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
100
+ }
101
+ },
102
+ locateSlot(slotId) {
103
+ try {
104
+ return locateSlotInActiveLayout(slotId);
105
+ }
106
+ catch (_a) {
107
+ return null;
108
+ }
109
+ },
110
+ listFloats() {
111
+ return floatManager.list().map((f) => {
112
+ var _a, _b, _c, _d, _e;
113
+ const tabs = f.content.type === 'tabs' ? f.content : null;
114
+ const active = tabs ? (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0] : null;
115
+ return {
116
+ floatId: f.id,
117
+ viewId: (_c = active === null || active === void 0 ? void 0 : active.viewId) !== null && _c !== void 0 ? _c : null,
118
+ label: (_e = (_d = f.title) !== null && _d !== void 0 ? _d : active === null || active === void 0 ? void 0 : active.label) !== null && _e !== void 0 ? _e : f.id,
119
+ };
120
+ });
121
+ },
122
+ closeSlot(slotId) {
123
+ void closeTab(slotId);
124
+ return { ok: true };
125
+ },
126
+ listZones(_shardId) { return []; },
127
+ readZone(_shardId, _zoneName) { return null; },
128
+ whoAmI() {
129
+ var _a;
130
+ const user = getUser();
131
+ return {
132
+ userId: (_a = user === null || user === void 0 ? void 0 : user.id) !== null && _a !== void 0 ? _a : 'guest',
133
+ admin: isAdmin(),
134
+ };
135
+ },
136
+ setMode(_id) { return false; },
137
+ listModes() { return []; },
138
+ };
139
+ }
140
+ export function makeShellApiForTest() {
141
+ return makeShellApiHeadless();
142
+ }
@@ -1,9 +1,3 @@
1
1
  import type { Shard } from '../api';
2
- import type { ShellApi } from './registry';
3
- /**
4
- * Test-only ShellApi constructor. Bypasses the admin gate and uses a
5
- * stub ShardContext. Only methods that do not consult `ctx` are
6
- * guaranteed to work — `locateSlot`, `listFloats`, `listApps`, etc.
7
- */
8
- export declare function makeShellApiForTest(): ShellApi;
2
+ export { makeShellApiHeadless, makeShellApiForTest } from './shellApi';
9
3
  export declare const shellShard: Shard;
@@ -11,180 +11,25 @@
11
11
  *
12
12
  * autostart() is defined so the shard activates at boot without requiring
13
13
  * a dedicated app to launch it first, matching the __sh3core__ pattern.
14
+ *
15
+ * The headless ShellApi factory lives in ./shellApi.ts so non-DOM callers
16
+ * (node-only test project, runtime/runVerb) can build a ShellApi without
17
+ * pulling in Terminal.svelte.
14
18
  */
15
19
  import { mount, unmount } from 'svelte';
16
20
  import { manifest } from './manifest';
17
21
  import Terminal from './Terminal.svelte';
18
22
  import { registerV1Verbs } from './verbs';
19
- import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
20
- import { launchApp } from '../apps/lifecycle';
21
- import { registeredShards } from '../shards/activate.svelte';
22
- import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIntoActiveLayout, locateSlot as locateSlotInActiveLayout } from '../layout/inspection';
23
+ import { makeShellApiHeadless } from './shellApi';
24
+ import { focusView } from '../layout/inspection';
23
25
  import { floatManager } from '../overlays/float';
24
- import { listStandaloneViews } from '../shards/activate.svelte';
25
26
  import { getUser, isAdmin } from '../auth/index';
26
- /** Walk a layout tree and collect all tab entries (slotId + viewId + label). */
27
- function collectTabEntries(node) {
28
- if (node.type === 'tabs') {
29
- return node.tabs.filter((t) => t.viewId !== null);
30
- }
31
- if (node.type === 'split') {
32
- return node.children.flatMap(collectTabEntries);
33
- }
34
- // slot node: wrap as a synthetic tab entry if it has a viewId
35
- if (node.viewId !== null) {
36
- return [{ slotId: node.slotId, viewId: node.viewId, label: node.viewId }];
37
- }
38
- return [];
39
- }
40
- function makeShellApi(_ctx) {
41
- return {
42
- // → apps/registry.svelte: listRegisteredApps() returns AppManifest[]
43
- listApps() {
44
- return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
45
- },
46
- // → apps/registry.svelte: getActiveApp() returns AppManifest | null
47
- getActiveApp() {
48
- const m = getActiveApp();
49
- return m ? { id: m.id, label: m.label } : null;
50
- },
51
- // → apps/lifecycle: launchApp() is async; fire-and-forget to keep ShellApi sync.
52
- // Verb handlers display feedback independently via scrollback.
53
- launchApp(id) {
54
- void launchApp(id);
55
- },
56
- // → shards/activate.svelte: registeredShards reactive map
57
- listShards() {
58
- return Array.from(registeredShards.values()).map((s) => ({
59
- id: s.manifest.id,
60
- label: s.manifest.label,
61
- version: s.manifest.version,
62
- }));
63
- },
64
- // → layout/inspection: inspectActiveLayout() + tree walk
65
- listViewsInCurrentLayout() {
66
- try {
67
- const { root } = inspectActiveLayout();
68
- return collectTabEntries(root.docked).map((t) => {
69
- var _a;
70
- return ({
71
- slotId: t.slotId,
72
- viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
73
- label: t.label,
74
- });
75
- });
76
- }
77
- catch (_a) {
78
- return [];
79
- }
80
- },
81
- // → layout/inspection: focusView(viewId). Falls back to dockIntoActiveLayout
82
- // for standalone views that aren't mounted yet — this is the single
83
- // "summon" entry point wired behind the `open` verb.
84
- openViewInCurrentLayout(viewId) {
85
- try {
86
- if (focusView(viewId))
87
- return { ok: true };
88
- const standalone = listStandaloneViews().find((v) => v.viewId === viewId);
89
- if (standalone) {
90
- const slotId = `standalone:${viewId}:${Date.now()}`;
91
- const ok = dockIntoActiveLayout({ slotId, viewId, label: standalone.label });
92
- return ok ? { ok: true } : { ok: false, error: `could not dock "${viewId}" — no available slot` };
93
- }
94
- return { ok: false, error: `view "${viewId}" not found in current layout` };
95
- }
96
- catch (err) {
97
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
98
- }
99
- },
100
- // → shards/activate.svelte: listStandaloneViews() walks activeShards
101
- listStandaloneViews() {
102
- return listStandaloneViews();
103
- },
104
- // → layout/inspection: popoutView(slotId) returns floatId | null
105
- popoutSlot(slotId) {
106
- try {
107
- const floatId = popoutView(slotId);
108
- return floatId ? { ok: true, floatId } : { ok: false, error: `slot "${slotId}" not found in docked tree` };
109
- }
110
- catch (err) {
111
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
112
- }
113
- },
114
- // → layout/inspection: dockFloat(floatId) returns boolean
115
- dockFloat(floatId) {
116
- try {
117
- const ok = dockFloat(floatId);
118
- return ok ? { ok: true } : { ok: false, error: `float "${floatId}" not found or has no dockable content` };
119
- }
120
- catch (err) {
121
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
122
- }
123
- },
124
- // → layout/inspection: locateSlot(slotId) returns TreeRootRef | null
125
- locateSlot(slotId) {
126
- try {
127
- return locateSlotInActiveLayout(slotId);
128
- }
129
- catch (_a) {
130
- return null;
131
- }
132
- },
133
- // → overlays/float: floatManager.list() returns FloatEntry[]
134
- listFloats() {
135
- return floatManager.list().map((f) => {
136
- var _a, _b, _c, _d, _e;
137
- const tabs = f.content.type === 'tabs' ? f.content : null;
138
- const active = tabs ? (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0] : null;
139
- return {
140
- floatId: f.id,
141
- viewId: (_c = active === null || active === void 0 ? void 0 : active.viewId) !== null && _c !== void 0 ? _c : null,
142
- label: (_e = (_d = f.title) !== null && _d !== void 0 ? _d : active === null || active === void 0 ? void 0 : active.label) !== null && _e !== void 0 ? _e : f.id,
143
- };
144
- });
145
- },
146
- // → layout/inspection: closeTab(slotId) is async (guarded close).
147
- // Fire-and-forget; the tab disappears asynchronously. ShellApi stays sync.
148
- closeSlot(slotId) {
149
- void closeTab(slotId);
150
- return { ok: true };
151
- },
152
- // TODO Phase 10: wire to zone manager when state:manage permission is available.
153
- // The shell manifest declares permissions: [] so ctx.zones is undefined.
154
- // A future permission grant + ctx.zones.list() would power these.
155
- listZones(_shardId) { return []; },
156
- readZone(_shardId, _zoneName) { return null; },
157
- // → auth/index: getUser() + isAdmin()
158
- whoAmI() {
159
- var _a;
160
- const user = getUser();
161
- return {
162
- userId: (_a = user === null || user === void 0 ? void 0 : user.id) !== null && _a !== void 0 ? _a : 'guest',
163
- admin: isAdmin(),
164
- };
165
- },
166
- // Mode switching is per-view state owned by Terminal.svelte; the base
167
- // ShellApi cannot reach it from the shard scope. Terminal.svelte wraps
168
- // this object and overrides setMode/listModes with the live registry +
169
- // setMode closure. Verbs called outside a terminal context fall through
170
- // these stubs (no-op switch, empty list).
171
- setMode(_id) { return false; },
172
- listModes() { return []; },
173
- };
174
- }
175
- /**
176
- * Test-only ShellApi constructor. Bypasses the admin gate and uses a
177
- * stub ShardContext. Only methods that do not consult `ctx` are
178
- * guaranteed to work — `locateSlot`, `listFloats`, `listApps`, etc.
179
- */
180
- export function makeShellApiForTest() {
181
- return makeShellApi({});
182
- }
27
+ export { makeShellApiHeadless, makeShellApiForTest } from './shellApi';
183
28
  export const shellShard = {
184
29
  manifest,
185
30
  activate(ctx) {
186
31
  registerV1Verbs(ctx);
187
- const shell = makeShellApi(ctx);
32
+ const shell = makeShellApiHeadless();
188
33
  // The AZERTY `²` key (top-left on FR keyboards, below Escape) opens the
189
34
  // terminal view — focusing it if already mounted, floating it otherwise.
190
35
  // Migrated from Shell.svelte's inline keydown handler as proof-of-concept