sh3-core 0.16.0 → 0.17.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/dist/Sh3.svelte +2 -73
- package/dist/actions/ctx-actions.svelte.test.js +4 -4
- package/dist/api.d.ts +2 -0
- package/dist/api.js +1 -0
- package/dist/build.d.ts +27 -0
- package/dist/build.js +59 -1
- package/dist/build.test.d.ts +1 -0
- package/dist/build.test.js +31 -0
- package/dist/contributions/index.d.ts +1 -1
- package/dist/contributions/index.js +1 -1
- package/dist/contributions/registry.d.ts +17 -1
- package/dist/contributions/registry.js +50 -2
- package/dist/contributions/scope.test.d.ts +1 -0
- package/dist/contributions/scope.test.js +52 -0
- package/dist/contributions/types.d.ts +11 -3
- package/dist/createShell.js +7 -1
- package/dist/fields/address.d.ts +3 -0
- package/dist/fields/address.js +36 -0
- package/dist/fields/address.test.d.ts +1 -0
- package/dist/fields/address.test.js +34 -0
- package/dist/fields/decoration.d.ts +7 -0
- package/dist/fields/decoration.js +199 -0
- package/dist/fields/decoration.svelte.test.d.ts +1 -0
- package/dist/fields/decoration.svelte.test.js +177 -0
- package/dist/fields/dispatch.d.ts +22 -0
- package/dist/fields/dispatch.js +254 -0
- package/dist/fields/dispatch.test.d.ts +1 -0
- package/dist/fields/dispatch.test.js +175 -0
- package/dist/fields/types.d.ts +101 -0
- package/dist/fields/types.js +16 -0
- package/dist/fields/walker.svelte.test.d.ts +1 -0
- package/dist/fields/walker.svelte.test.js +138 -0
- package/dist/host.js +27 -2
- package/dist/host.svelte.test.d.ts +1 -0
- package/dist/host.svelte.test.js +92 -0
- package/dist/layout/slotHostPool.svelte.d.ts +8 -0
- package/dist/layout/slotHostPool.svelte.js +14 -1
- package/dist/overlays/OverlayRoots.svelte +86 -0
- package/dist/overlays/OverlayRoots.svelte.d.ts +3 -0
- package/dist/platform/tauri-backend.d.ts +3 -3
- package/dist/platform/tauri-backend.js +24 -3
- package/dist/projects/session-state.svelte.d.ts +3 -3
- package/dist/projects/session-state.svelte.js +5 -4
- package/dist/runtime/runVerb.js +2 -2
- package/dist/satellite/SatelliteShell.svelte +58 -11
- package/dist/satellite/SatelliteShell.svelte.test.d.ts +1 -0
- package/dist/satellite/SatelliteShell.svelte.test.js +61 -0
- package/dist/sh3Api/fields-walker.svelte.test.d.ts +1 -0
- package/dist/sh3Api/fields-walker.svelte.test.js +75 -0
- package/dist/sh3Api/headless.d.ts +9 -0
- package/dist/sh3Api/headless.js +163 -16
- package/dist/sh3Api/headless.svelte.test.js +9 -9
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -2
- package/dist/shards/activate-fields.svelte.test.d.ts +1 -0
- package/dist/shards/activate-fields.svelte.test.js +121 -0
- package/dist/shards/activate-runtime.test.js +8 -8
- package/dist/shards/activate.svelte.js +29 -35
- package/dist/shards/types.d.ts +14 -75
- package/dist/shell-shard/ScrollbackView.svelte +55 -9
- package/dist/shell-shard/Terminal.svelte +1 -1
- package/dist/shell-shard/scrollback-stick.d.ts +9 -0
- package/dist/shell-shard/scrollback-stick.js +21 -0
- package/dist/shell-shard/scrollback-stick.test.d.ts +1 -0
- package/dist/shell-shard/scrollback-stick.test.js +25 -0
- package/dist/verbs/types.d.ts +56 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/sh3Api/headless.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Mode-related methods (`setMode`, `listModes`) stub to false / [];
|
|
10
10
|
* Terminal.svelte wraps this with mode-aware closures.
|
|
11
|
+
*
|
|
12
|
+
* Caller identity (callerKind, callerShardId?) is plumbed through but not
|
|
13
|
+
* yet consulted — it exists so a future permission gate can be a one-liner
|
|
14
|
+
* inside this factory without changing the public Sh3Api type.
|
|
11
15
|
*/
|
|
12
16
|
import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
|
|
13
17
|
import { launchApp } from '../apps/lifecycle';
|
|
@@ -16,6 +20,17 @@ import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIn
|
|
|
16
20
|
import { floatManager } from '../overlays/float';
|
|
17
21
|
import { getUser, isAdmin } from '../auth/index';
|
|
18
22
|
import { makeDispatchToTerminal } from '../shell-shard/dispatch-to-terminal';
|
|
23
|
+
import { listVerbsWithShard } from '../shards/registry';
|
|
24
|
+
import { listActionsFromEntries } from '../actions/listActive';
|
|
25
|
+
import { listActions as listActionEntriesFromRegistry } from '../actions/registry';
|
|
26
|
+
import { dispatchActionProgrammatic } from '../actions/listeners';
|
|
27
|
+
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
28
|
+
import { runVerbProgrammatic } from '../runtime/runVerb';
|
|
29
|
+
import { peekSlotHost } from '../layout/slotHostPool.svelte';
|
|
30
|
+
import { listFields as listFieldsImpl, getField as getFieldImpl, setField as setFieldImpl, walkSlotContainer, writeElement, } from '../fields/dispatch';
|
|
31
|
+
import { attachDecoration as attachDecorationImpl } from '../fields/decoration';
|
|
32
|
+
import { onChange as onContributionsChange } from '../contributions';
|
|
33
|
+
import { FIELD_POINT_ID, WALKER_SHARD_ID } from '../fields/types';
|
|
19
34
|
const KNOWN_ZONES = ['ephemeral', 'session', 'workspace', 'user'];
|
|
20
35
|
function collectTabEntries(node) {
|
|
21
36
|
if (node.type === 'tabs') {
|
|
@@ -29,7 +44,123 @@ function collectTabEntries(node) {
|
|
|
29
44
|
}
|
|
30
45
|
return [];
|
|
31
46
|
}
|
|
32
|
-
export function
|
|
47
|
+
export function makeSh3Api(opts) {
|
|
48
|
+
// Caller identity is plumbed but not yet used by any gate. The arg exists
|
|
49
|
+
// so future gates can be one-liners inside this factory.
|
|
50
|
+
void (opts === null || opts === void 0 ? void 0 : opts.callerKind);
|
|
51
|
+
void (opts === null || opts === void 0 ? void 0 : opts.callerShardId);
|
|
52
|
+
const zones = opts === null || opts === void 0 ? void 0 : opts.zones;
|
|
53
|
+
function listViewsImpl() {
|
|
54
|
+
try {
|
|
55
|
+
const { root } = inspectActiveLayout();
|
|
56
|
+
return collectTabEntries(root.docked).map((t) => {
|
|
57
|
+
var _a;
|
|
58
|
+
return ({
|
|
59
|
+
slotId: t.slotId,
|
|
60
|
+
viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
|
|
61
|
+
label: t.label,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (_a) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function resolveWalkerField(addr) {
|
|
70
|
+
if (!addr.slotId) {
|
|
71
|
+
throw new Error(`walker field requires slotId: ${addr.shardId}::::${addr.fieldId}`);
|
|
72
|
+
}
|
|
73
|
+
const host = peekSlotHost(addr.slotId);
|
|
74
|
+
if (!(host === null || host === void 0 ? void 0 : host.host)) {
|
|
75
|
+
throw new Error(`unknown field: ${addr.shardId}::${addr.slotId}::${addr.fieldId}`);
|
|
76
|
+
}
|
|
77
|
+
const found = walkSlotContainer(addr.slotId, host.host)
|
|
78
|
+
.find((f) => f.fieldId === addr.fieldId);
|
|
79
|
+
if (!found || !found.element) {
|
|
80
|
+
throw new Error(`unknown field: ${addr.shardId}::${addr.slotId}::${addr.fieldId}`);
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
function readWalkerField(addr) {
|
|
85
|
+
var _a;
|
|
86
|
+
const field = resolveWalkerField(addr);
|
|
87
|
+
const el = field.element;
|
|
88
|
+
if (el instanceof HTMLInputElement && el.type === 'checkbox')
|
|
89
|
+
return el.checked;
|
|
90
|
+
if (el instanceof HTMLInputElement ||
|
|
91
|
+
el instanceof HTMLTextAreaElement ||
|
|
92
|
+
el instanceof HTMLSelectElement) {
|
|
93
|
+
return el.value;
|
|
94
|
+
}
|
|
95
|
+
// [data-sh3-field] custom element — text content is the closest analogue.
|
|
96
|
+
return (_a = el.textContent) !== null && _a !== void 0 ? _a : '';
|
|
97
|
+
}
|
|
98
|
+
function writeWalkerField(addr, value) {
|
|
99
|
+
const field = resolveWalkerField(addr);
|
|
100
|
+
const el = field.element;
|
|
101
|
+
if (el instanceof HTMLInputElement ||
|
|
102
|
+
el instanceof HTMLTextAreaElement ||
|
|
103
|
+
el instanceof HTMLSelectElement) {
|
|
104
|
+
writeElement(el, value);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`walker field "${addr.fieldId}" is a [data-sh3-field] custom element; ` +
|
|
108
|
+
`the framework cannot write into it. Use an explicit imperative registration with set().`);
|
|
109
|
+
}
|
|
110
|
+
const fields = {
|
|
111
|
+
list(listOpts) {
|
|
112
|
+
var _a;
|
|
113
|
+
const all = listFieldsImpl({
|
|
114
|
+
slotId: listOpts === null || listOpts === void 0 ? void 0 : listOpts.slotId,
|
|
115
|
+
shardId: listOpts === null || listOpts === void 0 ? void 0 : listOpts.shardId,
|
|
116
|
+
kind: listOpts === null || listOpts === void 0 ? void 0 : listOpts.kind,
|
|
117
|
+
});
|
|
118
|
+
const walkerMode = (_a = listOpts === null || listOpts === void 0 ? void 0 : listOpts.walker) !== null && _a !== void 0 ? _a : 'off';
|
|
119
|
+
if (walkerMode === 'off')
|
|
120
|
+
return all;
|
|
121
|
+
const targetSlots = (listOpts === null || listOpts === void 0 ? void 0 : listOpts.slotId)
|
|
122
|
+
? [listOpts.slotId]
|
|
123
|
+
: Array.from(new Set(all.map((f) => f.slotId).filter((s) => !!s)));
|
|
124
|
+
let result = all;
|
|
125
|
+
for (const sid of targetSlots) {
|
|
126
|
+
const host = peekSlotHost(sid);
|
|
127
|
+
if (!(host === null || host === void 0 ? void 0 : host.host))
|
|
128
|
+
continue;
|
|
129
|
+
const slotHasContributions = all.some((f) => f.slotId === sid);
|
|
130
|
+
if (walkerMode === 'fallback' && slotHasContributions)
|
|
131
|
+
continue;
|
|
132
|
+
result = result.concat(walkSlotContainer(sid, host.host));
|
|
133
|
+
}
|
|
134
|
+
if (listOpts === null || listOpts === void 0 ? void 0 : listOpts.kind)
|
|
135
|
+
result = result.filter((f) => f.kind === listOpts.kind);
|
|
136
|
+
return result;
|
|
137
|
+
},
|
|
138
|
+
get(addr) {
|
|
139
|
+
if (addr.shardId === WALKER_SHARD_ID) {
|
|
140
|
+
return readWalkerField(addr);
|
|
141
|
+
}
|
|
142
|
+
return getFieldImpl(addr);
|
|
143
|
+
},
|
|
144
|
+
async set(addr, value) {
|
|
145
|
+
if (addr.shardId === WALKER_SHARD_ID) {
|
|
146
|
+
writeWalkerField(addr, value);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
return setFieldImpl(addr, value);
|
|
150
|
+
},
|
|
151
|
+
walk(slotId) {
|
|
152
|
+
const host = peekSlotHost(slotId);
|
|
153
|
+
if (!(host === null || host === void 0 ? void 0 : host.host))
|
|
154
|
+
throw new Error(`unknown slot: ${slotId}`);
|
|
155
|
+
return walkSlotContainer(slotId, host.host);
|
|
156
|
+
},
|
|
157
|
+
onChange(cb) {
|
|
158
|
+
return onContributionsChange(FIELD_POINT_ID, cb);
|
|
159
|
+
},
|
|
160
|
+
attachDecoration(addr, factory) {
|
|
161
|
+
return attachDecorationImpl(addr, factory);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
33
164
|
return {
|
|
34
165
|
listApps() {
|
|
35
166
|
return listRegisteredApps().map((m) => ({ id: m.id, label: m.label }));
|
|
@@ -49,20 +180,7 @@ export function makeSh3ApiHeadless(zones) {
|
|
|
49
180
|
}));
|
|
50
181
|
},
|
|
51
182
|
listViewsInCurrentLayout() {
|
|
52
|
-
|
|
53
|
-
const { root } = inspectActiveLayout();
|
|
54
|
-
return collectTabEntries(root.docked).map((t) => {
|
|
55
|
-
var _a;
|
|
56
|
-
return ({
|
|
57
|
-
slotId: t.slotId,
|
|
58
|
-
viewId: (_a = t.viewId) !== null && _a !== void 0 ? _a : '',
|
|
59
|
-
label: t.label,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
catch (_a) {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
183
|
+
return listViewsImpl();
|
|
66
184
|
},
|
|
67
185
|
openViewInCurrentLayout(viewId) {
|
|
68
186
|
try {
|
|
@@ -166,8 +284,37 @@ export function makeSh3ApiHeadless(zones) {
|
|
|
166
284
|
listModes() { return []; },
|
|
167
285
|
getMode() { return { id: 'sh3', label: 'sh3' }; },
|
|
168
286
|
dispatchToTerminal: makeDispatchToTerminal({ headless: true }),
|
|
287
|
+
listVerbs(verbOpts) {
|
|
288
|
+
const programmaticOnly = (verbOpts === null || verbOpts === void 0 ? void 0 : verbOpts.programmaticOnly) === true;
|
|
289
|
+
const out = listVerbsWithShard().map(({ verb, shardId }) => ({
|
|
290
|
+
shardId,
|
|
291
|
+
name: verb.name,
|
|
292
|
+
summary: verb.summary,
|
|
293
|
+
programmatic: verb.programmatic,
|
|
294
|
+
schema: verb.schema,
|
|
295
|
+
}));
|
|
296
|
+
return programmaticOnly ? out.filter((v) => v.programmatic === true) : out;
|
|
297
|
+
},
|
|
298
|
+
async runVerb(shardId, name, args, runOpts) {
|
|
299
|
+
return runVerbProgrammatic(shardId, name, args, runOpts);
|
|
300
|
+
},
|
|
301
|
+
listActions(actionOpts) {
|
|
302
|
+
const all = listActionsFromEntries(listActionEntriesFromRegistry(), getLiveDispatcherState());
|
|
303
|
+
return (actionOpts === null || actionOpts === void 0 ? void 0 : actionOpts.activeOnly) ? all.filter((a) => a.active) : all;
|
|
304
|
+
},
|
|
305
|
+
runAction(id, runOpts) {
|
|
306
|
+
return dispatchActionProgrammatic(id, runOpts);
|
|
307
|
+
},
|
|
308
|
+
listViews() {
|
|
309
|
+
return listViewsImpl();
|
|
310
|
+
},
|
|
311
|
+
fields,
|
|
169
312
|
};
|
|
170
313
|
}
|
|
314
|
+
/** @deprecated Renamed to makeSh3Api(opts?). Kept for one minor cycle. */
|
|
315
|
+
export function makeSh3ApiHeadless(zones) {
|
|
316
|
+
return makeSh3Api({ callerKind: 'verb', zones });
|
|
317
|
+
}
|
|
171
318
|
export function makeSh3ApiForTest() {
|
|
172
|
-
return
|
|
319
|
+
return makeSh3Api({ callerKind: 'verb' });
|
|
173
320
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { makeSh3Api } from './headless';
|
|
3
3
|
function makeMockZoneManager() {
|
|
4
4
|
const data = {
|
|
5
5
|
ephemeral: {},
|
|
@@ -16,44 +16,44 @@ function makeMockZoneManager() {
|
|
|
16
16
|
}
|
|
17
17
|
describe('sh3Api listZones', () => {
|
|
18
18
|
it('returns empty when no ZoneManager provided (stub mode)', () => {
|
|
19
|
-
const api =
|
|
19
|
+
const api = makeSh3Api({ callerKind: 'verb' });
|
|
20
20
|
expect(api.listZones()).toEqual([]);
|
|
21
21
|
});
|
|
22
22
|
it('returns one row per shard with the zones it has data in', () => {
|
|
23
23
|
var _a, _b;
|
|
24
|
-
const api =
|
|
24
|
+
const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
|
|
25
25
|
const rows = api.listZones();
|
|
26
26
|
const byShard = new Map(rows.map((r) => [r.shardId, r.zones]));
|
|
27
27
|
expect((_a = byShard.get('shard-a')) === null || _a === void 0 ? void 0 : _a.sort()).toEqual(['session', 'workspace']);
|
|
28
28
|
expect((_b = byShard.get('shard-b')) === null || _b === void 0 ? void 0 : _b.sort()).toEqual(['user', 'workspace']);
|
|
29
29
|
});
|
|
30
30
|
it('filters by shardId when provided', () => {
|
|
31
|
-
const api =
|
|
31
|
+
const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
|
|
32
32
|
const rows = api.listZones('shard-a');
|
|
33
33
|
expect(rows).toHaveLength(1);
|
|
34
34
|
expect(rows[0].shardId).toBe('shard-a');
|
|
35
35
|
expect(rows[0].zones.sort()).toEqual(['session', 'workspace']);
|
|
36
36
|
});
|
|
37
37
|
it('returns empty array for unknown shardId', () => {
|
|
38
|
-
const api =
|
|
38
|
+
const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
|
|
39
39
|
expect(api.listZones('not-a-shard')).toEqual([]);
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
describe('sh3Api readZone', () => {
|
|
43
43
|
it('returns null when no ZoneManager provided', () => {
|
|
44
|
-
const api =
|
|
44
|
+
const api = makeSh3Api({ callerKind: 'verb' });
|
|
45
45
|
expect(api.readZone('shard-a', 'workspace')).toBe(null);
|
|
46
46
|
});
|
|
47
47
|
it('returns peeked value via ZoneManager', () => {
|
|
48
|
-
const api =
|
|
48
|
+
const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
|
|
49
49
|
expect(api.readZone('shard-a', 'workspace')).toEqual({ bar: 2 });
|
|
50
50
|
});
|
|
51
51
|
it('returns null for unknown zone name', () => {
|
|
52
|
-
const api =
|
|
52
|
+
const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
|
|
53
53
|
expect(api.readZone('shard-a', 'bogus')).toBe(null);
|
|
54
54
|
});
|
|
55
55
|
it('returns null for missing shard entry', () => {
|
|
56
|
-
const api =
|
|
56
|
+
const api = makeSh3Api({ callerKind: 'verb', zones: makeMockZoneManager() });
|
|
57
57
|
expect(api.readZone('not-a-shard', 'workspace')).toBe(null);
|
|
58
58
|
});
|
|
59
59
|
});
|
|
@@ -33,6 +33,7 @@ import { resetActivePresetToDefault } from '../layout/store.svelte';
|
|
|
33
33
|
import { modalManager } from '../overlays/modal';
|
|
34
34
|
import { floatManager } from '../overlays/float';
|
|
35
35
|
import { registerAppActions } from './appActions';
|
|
36
|
+
import { openPalette } from '../actions/listeners';
|
|
36
37
|
/**
|
|
37
38
|
* Build the palette-only float-maximize toggle action. Targets the topmost
|
|
38
39
|
* float (last entry in `floatManager.list()`); disabled when no floats are
|
|
@@ -79,8 +80,7 @@ export const sh3coreShard = {
|
|
|
79
80
|
contextItem: false,
|
|
80
81
|
paletteItem: false,
|
|
81
82
|
run(_dispatchCtx) {
|
|
82
|
-
|
|
83
|
-
import('../actions/listeners').then(({ openPalette }) => openPalette());
|
|
83
|
+
openPalette();
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
ctx.actions.register(buildToggleMaximizeAction());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { registerShard, activateShard, __resetShardRegistryForTest, } from './activate.svelte';
|
|
3
|
+
import { __resetViewRegistryForTest } from './registry';
|
|
4
|
+
import { __resetContributionsForTest } from '../contributions/registry';
|
|
5
|
+
import { __resetDecorationLayerForTest } from '../fields/decoration';
|
|
6
|
+
const makeShard = (overrides) => (Object.assign({ activate: () => { } }, overrides));
|
|
7
|
+
describe('ctx.sh3.fields integration', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
__resetShardRegistryForTest();
|
|
10
|
+
__resetViewRegistryForTest();
|
|
11
|
+
__resetContributionsForTest();
|
|
12
|
+
__resetDecorationLayerForTest();
|
|
13
|
+
document.body.innerHTML = '';
|
|
14
|
+
});
|
|
15
|
+
it('shard-scoped field is visible to a consumer via ctx.sh3.fields.list', async () => {
|
|
16
|
+
let value = 'dark';
|
|
17
|
+
registerShard(makeShard({
|
|
18
|
+
manifest: { id: 'producer', label: 'P', version: '0.0.0', views: [] },
|
|
19
|
+
activate(ctx) {
|
|
20
|
+
ctx.contributions.register('sh3.controllable-field', {
|
|
21
|
+
shape: 'imperative',
|
|
22
|
+
fieldId: 'theme',
|
|
23
|
+
label: 'Theme',
|
|
24
|
+
kind: 'enum',
|
|
25
|
+
enumValues: ['light', 'dark'],
|
|
26
|
+
get: () => value,
|
|
27
|
+
set: (v) => { value = v; },
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
let consumerCtx = null;
|
|
32
|
+
registerShard(makeShard({
|
|
33
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
34
|
+
activate(ctx) { consumerCtx = ctx; },
|
|
35
|
+
}));
|
|
36
|
+
await activateShard('producer');
|
|
37
|
+
await activateShard('consumer');
|
|
38
|
+
const list = consumerCtx.sh3.fields.list();
|
|
39
|
+
expect(list).toHaveLength(1);
|
|
40
|
+
expect(list[0]).toMatchObject({
|
|
41
|
+
shardId: 'producer',
|
|
42
|
+
fieldId: 'theme',
|
|
43
|
+
kind: 'enum',
|
|
44
|
+
readonly: false,
|
|
45
|
+
source: 'contributed',
|
|
46
|
+
});
|
|
47
|
+
expect(consumerCtx.sh3.fields.get({ shardId: 'producer', fieldId: 'theme' })).toBe('dark');
|
|
48
|
+
await consumerCtx.sh3.fields.set({ shardId: 'producer', fieldId: 'theme' }, 'light');
|
|
49
|
+
expect(value).toBe('light');
|
|
50
|
+
});
|
|
51
|
+
it('contenteditable-style imperative+element pattern works end-to-end (set, decorate, unmount)', async () => {
|
|
52
|
+
const editorEl = document.createElement('div');
|
|
53
|
+
editorEl.contentEditable = 'true';
|
|
54
|
+
editorEl.textContent = 'initial';
|
|
55
|
+
document.body.appendChild(editorEl);
|
|
56
|
+
editorEl.getBoundingClientRect = () => ({ x: 5, y: 5, width: 100, height: 50, top: 5, left: 5, right: 105, bottom: 55 });
|
|
57
|
+
const set = vi.fn().mockImplementation((v) => { editorEl.textContent = String(v); });
|
|
58
|
+
registerShard(makeShard({
|
|
59
|
+
manifest: { id: 'producer', label: 'P', version: '0.0.0', views: [] },
|
|
60
|
+
activate(ctx) {
|
|
61
|
+
ctx.contributions.register('sh3.controllable-field', {
|
|
62
|
+
shape: 'imperative',
|
|
63
|
+
fieldId: 'body',
|
|
64
|
+
label: 'Body',
|
|
65
|
+
kind: 'string',
|
|
66
|
+
get: () => { var _a; return (_a = editorEl.textContent) !== null && _a !== void 0 ? _a : ''; },
|
|
67
|
+
set,
|
|
68
|
+
element: editorEl,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
let consumerCtx = null;
|
|
73
|
+
registerShard(makeShard({
|
|
74
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
75
|
+
activate(ctx) { consumerCtx = ctx; },
|
|
76
|
+
}));
|
|
77
|
+
await activateShard('producer');
|
|
78
|
+
await activateShard('consumer');
|
|
79
|
+
await consumerCtx.sh3.fields.set({ shardId: 'producer', fieldId: 'body' }, 'hello');
|
|
80
|
+
expect(set).toHaveBeenCalledWith('hello');
|
|
81
|
+
expect(editorEl.textContent).toBe('hello');
|
|
82
|
+
const off = consumerCtx.sh3.fields.attachDecoration({ shardId: 'producer', fieldId: 'body' }, () => {
|
|
83
|
+
const badge = document.createElement('span');
|
|
84
|
+
badge.dataset.kind = 'sh3-ai-badge';
|
|
85
|
+
return badge;
|
|
86
|
+
});
|
|
87
|
+
expect(document.querySelector('[data-kind="sh3-ai-badge"]')).not.toBeNull();
|
|
88
|
+
off();
|
|
89
|
+
expect(document.querySelector('[data-kind="sh3-ai-badge"]')).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
it('listFields filters by shardId', async () => {
|
|
92
|
+
registerShard(makeShard({
|
|
93
|
+
manifest: { id: 'a', label: 'A', version: '0.0.0', views: [] },
|
|
94
|
+
activate(ctx) {
|
|
95
|
+
ctx.contributions.register('sh3.controllable-field', {
|
|
96
|
+
shape: 'imperative', fieldId: 'x', label: 'X', kind: 'string',
|
|
97
|
+
get: () => '1',
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
registerShard(makeShard({
|
|
102
|
+
manifest: { id: 'b', label: 'B', version: '0.0.0', views: [] },
|
|
103
|
+
activate(ctx) {
|
|
104
|
+
ctx.contributions.register('sh3.controllable-field', {
|
|
105
|
+
shape: 'imperative', fieldId: 'y', label: 'Y', kind: 'string',
|
|
106
|
+
get: () => '2',
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
}));
|
|
110
|
+
let consumerCtx = null;
|
|
111
|
+
registerShard(makeShard({
|
|
112
|
+
manifest: { id: 'c', label: 'C', version: '0.0.0', views: [] },
|
|
113
|
+
activate(ctx) { consumerCtx = ctx; },
|
|
114
|
+
}));
|
|
115
|
+
await activateShard('a');
|
|
116
|
+
await activateShard('b');
|
|
117
|
+
await activateShard('c');
|
|
118
|
+
expect(consumerCtx.sh3.fields.list({ shardId: 'a' }).map((f) => f.fieldId)).toEqual(['x']);
|
|
119
|
+
expect(consumerCtx.sh3.fields.list({ shardId: 'b' }).map((f) => f.fieldId)).toEqual(['y']);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -41,7 +41,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
41
41
|
});
|
|
42
42
|
await activateShard('host');
|
|
43
43
|
await activateShard('consumer');
|
|
44
|
-
const list = consumerCtx.listVerbs();
|
|
44
|
+
const list = consumerCtx.sh3.listVerbs();
|
|
45
45
|
const a = list.find((v) => v.name === 'host:a');
|
|
46
46
|
const b = list.find((v) => v.name === 'host:b');
|
|
47
47
|
expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', programmatic: true, schema: undefined });
|
|
@@ -64,7 +64,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
64
64
|
});
|
|
65
65
|
await activateShard('host');
|
|
66
66
|
await activateShard('consumer');
|
|
67
|
-
const list = consumerCtx.listVerbs({ programmaticOnly: true });
|
|
67
|
+
const list = consumerCtx.sh3.listVerbs({ programmaticOnly: true });
|
|
68
68
|
expect(list.find((v) => v.name === 'host:a')).toBeDefined();
|
|
69
69
|
expect(list.find((v) => v.name === 'host:b')).toBeUndefined();
|
|
70
70
|
expect(list.every((v) => v.programmatic === true)).toBe(true);
|
|
@@ -87,7 +87,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
87
87
|
});
|
|
88
88
|
await activateShard('host');
|
|
89
89
|
await activateShard('consumer');
|
|
90
|
-
const out = await consumerCtx.runVerb('host', 'host:echo', ['hello']);
|
|
90
|
+
const out = await consumerCtx.sh3.runVerb('host', 'host:echo', ['hello']);
|
|
91
91
|
expect(out.scrollback).toHaveLength(1);
|
|
92
92
|
expect(out.scrollback[0]).toMatchObject({ kind: 'status', text: 'echo: hello' });
|
|
93
93
|
});
|
|
@@ -107,7 +107,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
107
107
|
});
|
|
108
108
|
await activateShard('host');
|
|
109
109
|
await activateShard('consumer');
|
|
110
|
-
await expect(consumerCtx.runVerb('host', 'host:plain', [])).rejects.toThrow('verb "host:plain" is not programmatic');
|
|
110
|
+
await expect(consumerCtx.sh3.runVerb('host', 'host:plain', [])).rejects.toThrow('verb "host:plain" is not programmatic');
|
|
111
111
|
});
|
|
112
112
|
it('runVerb populates ctx.structuredArgs when opts.structured is set', async () => {
|
|
113
113
|
let observed = undefined;
|
|
@@ -128,7 +128,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
128
128
|
});
|
|
129
129
|
await activateShard('host');
|
|
130
130
|
await activateShard('consumer');
|
|
131
|
-
await consumerCtx.runVerb('host', 'host:schemaCheck', [], { structured: { foo: 'bar' } });
|
|
131
|
+
await consumerCtx.sh3.runVerb('host', 'host:schemaCheck', [], { structured: { foo: 'bar' } });
|
|
132
132
|
expect(observed).toEqual({ foo: 'bar' });
|
|
133
133
|
});
|
|
134
134
|
it('runVerb rejects on unknown shardId', async () => {
|
|
@@ -140,7 +140,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
140
140
|
},
|
|
141
141
|
});
|
|
142
142
|
await activateShard('consumer');
|
|
143
|
-
await expect(consumerCtx.runVerb('missing', 'x', [])).rejects.toThrow('unknown shard: missing');
|
|
143
|
+
await expect(consumerCtx.sh3.runVerb('missing', 'x', [])).rejects.toThrow('unknown shard: missing');
|
|
144
144
|
});
|
|
145
145
|
it('runVerb rejects on unknown verb', async () => {
|
|
146
146
|
registerShard({
|
|
@@ -158,7 +158,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
158
158
|
});
|
|
159
159
|
await activateShard('host');
|
|
160
160
|
await activateShard('consumer');
|
|
161
|
-
await expect(consumerCtx.runVerb('host', 'host:absent', [])).rejects.toThrow('unknown verb: host:absent');
|
|
161
|
+
await expect(consumerCtx.sh3.runVerb('host', 'host:absent', [])).rejects.toThrow('unknown verb: host:absent');
|
|
162
162
|
});
|
|
163
163
|
it('verbs declaring schema.input expose it via listVerbs', async () => {
|
|
164
164
|
registerShard({
|
|
@@ -188,7 +188,7 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
188
188
|
});
|
|
189
189
|
await activateShard('host');
|
|
190
190
|
await activateShard('consumer');
|
|
191
|
-
const list = consumerCtx.listVerbs();
|
|
191
|
+
const list = consumerCtx.sh3.listVerbs();
|
|
192
192
|
const typed = list.find((v) => v.name === 'host:typed');
|
|
193
193
|
expect(typed === null || typed === void 0 ? void 0 : typed.schema).toEqual({
|
|
194
194
|
input: {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* stays in `registeredShards` — it's still known, just not running.
|
|
18
18
|
*/
|
|
19
19
|
import { sh3 } from '../sh3Runtime.svelte';
|
|
20
|
-
import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb
|
|
21
|
-
import {
|
|
20
|
+
import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb } from './registry';
|
|
21
|
+
import { makeSh3Api } from '../sh3Api/headless';
|
|
22
22
|
import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
|
|
23
23
|
import { fetchEnvState, putEnvState } from '../env/client';
|
|
24
24
|
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
@@ -30,11 +30,9 @@ import { createShardKeysApi } from '../keys/client';
|
|
|
30
30
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
31
31
|
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
32
32
|
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
|
|
33
|
-
import { registerAction
|
|
33
|
+
import { registerAction } from '../actions/registry';
|
|
34
34
|
import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
|
|
35
|
-
import { openContextMenu as sh3OpenContextMenu, openPalette as sh3OpenPalette,
|
|
36
|
-
import { listActionsFromEntries } from '../actions/listActive';
|
|
37
|
-
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
35
|
+
import { openContextMenu as sh3OpenContextMenu, openPalette as sh3OpenPalette, } from '../actions/listeners';
|
|
38
36
|
/**
|
|
39
37
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
40
38
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -88,7 +86,7 @@ export function registerShard(shard) {
|
|
|
88
86
|
* @throws If the shard is not registered, if `shard.activate` throws, or if a manifest view has no factory after activation.
|
|
89
87
|
*/
|
|
90
88
|
export async function activateShard(id, opts) {
|
|
91
|
-
var _a, _b, _c, _d, _e, _f;
|
|
89
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
92
90
|
const shard = registeredShards.get(id);
|
|
93
91
|
if (!shard) {
|
|
94
92
|
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
@@ -109,8 +107,20 @@ export async function activateShard(id, opts) {
|
|
|
109
107
|
// global registry, and its disposer is pushed into entry.cleanupFns
|
|
110
108
|
// so deactivate auto-unregisters.
|
|
111
109
|
const contributions = {
|
|
112
|
-
register(pointId, descriptor) {
|
|
113
|
-
|
|
110
|
+
register(pointId, descriptor, contribOpts) {
|
|
111
|
+
var _a;
|
|
112
|
+
// Auto-wrap controllable-field descriptors with ownership metadata so
|
|
113
|
+
// fields/dispatch.ts can route addressed get/set without the user
|
|
114
|
+
// descriptor needing to know about the framework's wire format.
|
|
115
|
+
let stored = descriptor;
|
|
116
|
+
if (pointId === 'sh3.controllable-field') {
|
|
117
|
+
const owner = { shardId: id };
|
|
118
|
+
if (((_a = contribOpts === null || contribOpts === void 0 ? void 0 : contribOpts.scope) === null || _a === void 0 ? void 0 : _a.slotId) !== undefined) {
|
|
119
|
+
owner.slotId = contribOpts.scope.slotId;
|
|
120
|
+
}
|
|
121
|
+
stored = { owner, descriptor };
|
|
122
|
+
}
|
|
123
|
+
const dispose = contributionsRegister(pointId, stored, contribOpts);
|
|
114
124
|
entry.cleanupFns.push(async () => dispose());
|
|
115
125
|
return dispose;
|
|
116
126
|
},
|
|
@@ -203,29 +213,13 @@ export async function activateShard(id, opts) {
|
|
|
203
213
|
openContextMenu(opts) { sh3OpenContextMenu(opts); },
|
|
204
214
|
openPalette(opts) { sh3OpenPalette(opts); },
|
|
205
215
|
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
name: verb.name,
|
|
214
|
-
summary: verb.summary,
|
|
215
|
-
programmatic: verb.programmatic,
|
|
216
|
-
schema: verb.schema,
|
|
217
|
-
}));
|
|
218
|
-
},
|
|
219
|
-
runVerb(shardId, name, args, opts) {
|
|
220
|
-
return runVerbProgrammatic(shardId, name, args, opts);
|
|
221
|
-
},
|
|
222
|
-
listActions(opts) {
|
|
223
|
-
const all = listActionsFromEntries(listActionsFromRegistry(), getLiveDispatcherState());
|
|
224
|
-
return (opts === null || opts === void 0 ? void 0 : opts.activeOnly) ? all.filter((a) => a.active) : all;
|
|
225
|
-
},
|
|
226
|
-
runAction(actionId, opts) {
|
|
227
|
-
return dispatchActionProgrammatic(actionId, opts);
|
|
228
|
-
},
|
|
216
|
+
sh3: makeSh3Api({
|
|
217
|
+
callerKind: 'shard',
|
|
218
|
+
callerShardId: id,
|
|
219
|
+
zones: ((_e = shard.manifest.permissions) === null || _e === void 0 ? void 0 : _e.includes(PERMISSION_STATE_MANAGE))
|
|
220
|
+
? createZoneManager()
|
|
221
|
+
: undefined,
|
|
222
|
+
}),
|
|
229
223
|
};
|
|
230
224
|
entry.ctx = ctx;
|
|
231
225
|
// Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
|
|
@@ -271,7 +265,7 @@ export async function activateShard(id, opts) {
|
|
|
271
265
|
try {
|
|
272
266
|
void fn();
|
|
273
267
|
}
|
|
274
|
-
catch (
|
|
268
|
+
catch (_h) {
|
|
275
269
|
// intentionally swallowed: original error is what matters.
|
|
276
270
|
}
|
|
277
271
|
}
|
|
@@ -285,7 +279,7 @@ export async function activateShard(id, opts) {
|
|
|
285
279
|
erroredShards.set(id, {
|
|
286
280
|
id,
|
|
287
281
|
error: err,
|
|
288
|
-
phase: (
|
|
282
|
+
phase: (_f = opts === null || opts === void 0 ? void 0 : opts.phase) !== null && _f !== void 0 ? _f : 'launch',
|
|
289
283
|
timestamp: Date.now(),
|
|
290
284
|
});
|
|
291
285
|
console.error(`[sh3] Shard "${id}" failed to activate:`, err);
|
|
@@ -293,7 +287,7 @@ export async function activateShard(id, opts) {
|
|
|
293
287
|
}
|
|
294
288
|
// Activation succeeded — clear any prior error record for this shard.
|
|
295
289
|
erroredShards.delete(id);
|
|
296
|
-
void ((
|
|
290
|
+
void ((_g = shard.autostart) === null || _g === void 0 ? void 0 : _g.call(shard, ctx));
|
|
297
291
|
}
|
|
298
292
|
/**
|
|
299
293
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|