sh3-core 0.15.2 → 0.15.3
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/api.d.ts +4 -1
- package/dist/api.js +2 -0
- package/dist/apps/lifecycle.d.ts +21 -2
- package/dist/apps/lifecycle.js +13 -7
- package/dist/apps/lifecycle.test.js +18 -0
- package/dist/boot/satelliteMode.d.ts +7 -0
- package/dist/boot/satelliteMode.js +22 -0
- package/dist/boot/satelliteMode.test.d.ts +1 -0
- package/dist/boot/satelliteMode.test.js +55 -0
- package/dist/boot/satellitePayload.d.ts +17 -0
- package/dist/boot/satellitePayload.js +60 -0
- package/dist/boot/satellitePayload.test.d.ts +1 -0
- package/dist/boot/satellitePayload.test.js +53 -0
- package/dist/createShell.js +72 -25
- package/dist/host.d.ts +13 -0
- package/dist/host.js +36 -0
- package/dist/layout/store.svelte.d.ts +11 -0
- package/dist/layout/store.svelte.js +15 -0
- package/dist/overlays/FloatFrame.svelte +36 -0
- package/dist/runtime/runVerb-shell.test.js +0 -39
- package/dist/runtime/runVerb.test.js +17 -0
- package/dist/satellite/SatelliteShell.svelte +60 -0
- package/dist/satellite/SatelliteShell.svelte.d.ts +9 -0
- package/dist/satellite/seed.d.ts +3 -0
- package/dist/satellite/seed.js +20 -0
- package/dist/satellite/seed.test.d.ts +1 -0
- package/dist/satellite/seed.test.js +38 -0
- package/dist/satellite/walkShards.d.ts +2 -0
- package/dist/satellite/walkShards.js +44 -0
- package/dist/satellite/walkShards.test.d.ts +1 -0
- package/dist/satellite/walkShards.test.js +65 -0
- package/dist/sh3core-shard/appActions.js +51 -0
- package/dist/shards/activate.svelte.d.ts +2 -2
- package/dist/shards/activate.svelte.js +1 -1
- package/dist/shards/registry.d.ts +2 -1
- package/dist/shards/registry.js +13 -4
- package/dist/shards/registry.test.js +22 -1
- package/dist/shards/types.d.ts +1 -0
- package/dist/shell-shard/CommandLine.svelte +3 -0
- package/dist/shell-shard/CommandLine.svelte.d.ts +1 -0
- package/dist/shell-shard/InputLine.svelte +4 -1
- package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
- package/dist/shell-shard/Terminal.svelte +24 -0
- package/dist/shell-shard/dispatch-to-terminal.d.ts +13 -0
- package/dist/shell-shard/dispatch-to-terminal.js +37 -0
- package/dist/shell-shard/dispatch-to-terminal.test.d.ts +1 -0
- package/dist/shell-shard/dispatch-to-terminal.test.js +79 -0
- package/dist/shell-shard/shellApi.js +2 -0
- package/dist/shell-shard/terminal-registry.d.ts +25 -0
- package/dist/shell-shard/terminal-registry.js +62 -0
- package/dist/shell-shard/terminal-registry.test.d.ts +1 -0
- package/dist/shell-shard/terminal-registry.test.js +88 -0
- package/dist/shellApi/window.d.ts +15 -0
- package/dist/shellApi/window.js +43 -0
- package/dist/shellApi/window.test.d.ts +1 -0
- package/dist/shellApi/window.test.js +19 -0
- package/dist/verbs/types.d.ts +15 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
|
18
18
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
19
19
|
export { pushNavEntry } from './navigation';
|
|
20
20
|
export type { NavEntry, NavEntryHandle } from './navigation';
|
|
21
|
+
export { spawnSatellite } from './shellApi/window';
|
|
22
|
+
export type { SpawnSatelliteOptions } from './shellApi/window';
|
|
23
|
+
export type { SatellitePayload } from './boot/satellitePayload';
|
|
21
24
|
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, popoutView, dockFloat, locateSlot, } from './layout/inspection';
|
|
22
25
|
export type { DocumentHandle, DocumentHandleOptions, DocumentFormat, DocumentMeta, DocumentChange, AutosaveController, } from './documents/types';
|
|
23
26
|
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
|
@@ -46,7 +49,7 @@ export declare const capabilities: {
|
|
|
46
49
|
readonly hotInstall: boolean;
|
|
47
50
|
};
|
|
48
51
|
export type { ServerShard, ServerShardContext, TenantDocumentAPI } from './server-shard/types';
|
|
49
|
-
export type { Verb, VerbContext, ShellApi, VerbSchema, PortableJSONSchema, } from './verbs/types';
|
|
52
|
+
export type { Verb, VerbContext, ShellApi, VerbSchema, PortableJSONSchema, DispatchToTerminalResult, } from './verbs/types';
|
|
50
53
|
export type { Scrollback } from './shell-shard/scrollback.svelte';
|
|
51
54
|
export type { SessionClient } from './shell-shard/session-client.svelte';
|
|
52
55
|
export { listVerbs } from './shards/registry';
|
package/dist/api.js
CHANGED
|
@@ -29,6 +29,8 @@ export { listRegisteredApps, getActiveApp } from './apps/registry.svelte';
|
|
|
29
29
|
export { launchApp, returnToHome, unregisterApp } from './apps/lifecycle';
|
|
30
30
|
// Navigation — apps push in-app nav entries; framework drives back/forward.
|
|
31
31
|
export { pushNavEntry } from './navigation';
|
|
32
|
+
// Multi-window — spawn a satellite native window from the host shell.
|
|
33
|
+
export { spawnSatellite } from './shellApi/window';
|
|
32
34
|
// Layout inspection / mutation for advanced shards (diagnostic, etc.).
|
|
33
35
|
export { inspectActiveLayout, spliceIntoActiveLayout, dockIntoActiveLayout, focusTab, focusView, collapseChild, expandChild, closeTab, popoutView, dockFloat, locateSlot, } from './layout/inspection';
|
|
34
36
|
export { PERMISSION_DOCUMENTS_BROWSE, PERMISSION_DOCUMENTS_READ, PERMISSION_DOCUMENTS_WRITE, } from './documents/types';
|
package/dist/apps/lifecycle.d.ts
CHANGED
|
@@ -11,6 +11,21 @@ export declare function readLastApp(): string | null;
|
|
|
11
11
|
* instead of looping into the same failure.
|
|
12
12
|
*/
|
|
13
13
|
export declare function clearLastApp(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Options for `launchApp`. Both flags are primarily intended for satellite
|
|
16
|
+
* mode (Task 8) where the satellite window manages its own persistence and
|
|
17
|
+
* starts from an empty layout rather than a home view.
|
|
18
|
+
*/
|
|
19
|
+
export interface LaunchAppOptions {
|
|
20
|
+
/** When true, do not persist this launch as the lastApp slot. */
|
|
21
|
+
skipLastApp?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* When true, do not call switchToHome() before attaching the new app.
|
|
24
|
+
* Use this when the satellite's layout is already empty and there is no
|
|
25
|
+
* "home" view to return to first.
|
|
26
|
+
*/
|
|
27
|
+
skipSwitchToHome?: boolean;
|
|
28
|
+
}
|
|
14
29
|
/**
|
|
15
30
|
* Launch an app by id. Activates all required shards (idempotent for
|
|
16
31
|
* already-active shards), attaches the app's layout, calls `App.activate`,
|
|
@@ -21,17 +36,21 @@ export declare function clearLastApp(): void;
|
|
|
21
36
|
* back from the home view if needed.
|
|
22
37
|
*
|
|
23
38
|
* @param id - The `AppManifest.id` of the app to launch. Must be registered.
|
|
39
|
+
* @param opts - Optional launch flags (see {@link LaunchAppOptions}).
|
|
24
40
|
* @throws If the app is not registered or a required shard is not registered.
|
|
25
41
|
*/
|
|
26
|
-
export declare function launchApp(id: string): Promise<void>;
|
|
42
|
+
export declare function launchApp(id: string, opts?: LaunchAppOptions): Promise<void>;
|
|
27
43
|
/**
|
|
28
44
|
* Unload an active app. Calls `App.deactivate`, detaches the layout, and
|
|
29
45
|
* deactivates the app's non-self-starting required shards. Switches the
|
|
30
46
|
* rendered root to home. No-op if the app is not currently active.
|
|
31
47
|
*
|
|
32
48
|
* @param id - The `AppManifest.id` of the app to unload.
|
|
49
|
+
* @param skipSwitchToHome - When true, skip the switchToHome() call. Used by
|
|
50
|
+
* launchApp when the caller has opted out of the home transition (e.g.
|
|
51
|
+
* satellite mode).
|
|
33
52
|
*/
|
|
34
|
-
export declare function unloadApp(id: string): void;
|
|
53
|
+
export declare function unloadApp(id: string, skipSwitchToHome?: boolean): void;
|
|
35
54
|
/**
|
|
36
55
|
* Unregister an app and remove it from the registry.
|
|
37
56
|
*
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -77,7 +77,6 @@ function getOrCreateAppContext(appId, scopeId) {
|
|
|
77
77
|
}
|
|
78
78
|
return ctx;
|
|
79
79
|
}
|
|
80
|
-
// ---------- launch --------------------------------------------------------
|
|
81
80
|
/**
|
|
82
81
|
* Launch an app by id. Activates all required shards (idempotent for
|
|
83
82
|
* already-active shards), attaches the app's layout, calls `App.activate`,
|
|
@@ -88,9 +87,10 @@ function getOrCreateAppContext(appId, scopeId) {
|
|
|
88
87
|
* back from the home view if needed.
|
|
89
88
|
*
|
|
90
89
|
* @param id - The `AppManifest.id` of the app to launch. Must be registered.
|
|
90
|
+
* @param opts - Optional launch flags (see {@link LaunchAppOptions}).
|
|
91
91
|
* @throws If the app is not registered or a required shard is not registered.
|
|
92
92
|
*/
|
|
93
|
-
export async function launchApp(id) {
|
|
93
|
+
export async function launchApp(id, opts = {}) {
|
|
94
94
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
95
95
|
const app = getRegisteredApp(id);
|
|
96
96
|
if (!app) {
|
|
@@ -101,7 +101,7 @@ export async function launchApp(id) {
|
|
|
101
101
|
// we only swap the rendered root back to 'app' in case the user was
|
|
102
102
|
// on home.
|
|
103
103
|
if (activeApp.id && activeApp.id !== id) {
|
|
104
|
-
unloadApp(activeApp.id);
|
|
104
|
+
unloadApp(activeApp.id, opts.skipSwitchToHome);
|
|
105
105
|
}
|
|
106
106
|
else if (activeApp.id === id) {
|
|
107
107
|
// Re-entering the same app from Home — fire resume hooks.
|
|
@@ -114,7 +114,8 @@ export async function launchApp(id) {
|
|
|
114
114
|
void ((_b = app.resume) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
|
|
115
115
|
switchToApp();
|
|
116
116
|
void ((_c = app.onAppReady) === null || _c === void 0 ? void 0 : _c.call(app, getOrCreateAppContext(id)));
|
|
117
|
-
|
|
117
|
+
if (!opts.skipLastApp)
|
|
118
|
+
writeLastApp(id);
|
|
118
119
|
breadcrumbApp.id = id;
|
|
119
120
|
setActiveApp(id, new Set((_d = app.manifest.requiredShards) !== null && _d !== void 0 ? _d : []));
|
|
120
121
|
void loadUserBindings(id).then(setUserBindings);
|
|
@@ -161,7 +162,8 @@ export async function launchApp(id) {
|
|
|
161
162
|
void loadUserBindings(id).then(setUserBindings);
|
|
162
163
|
switchToApp();
|
|
163
164
|
void ((_h = app.onAppReady) === null || _h === void 0 ? void 0 : _h.call(app, getOrCreateAppContext(id)));
|
|
164
|
-
|
|
165
|
+
if (!opts.skipLastApp)
|
|
166
|
+
writeLastApp(id);
|
|
165
167
|
breadcrumbApp.id = id;
|
|
166
168
|
}
|
|
167
169
|
// ---------- unload --------------------------------------------------------
|
|
@@ -171,8 +173,11 @@ export async function launchApp(id) {
|
|
|
171
173
|
* rendered root to home. No-op if the app is not currently active.
|
|
172
174
|
*
|
|
173
175
|
* @param id - The `AppManifest.id` of the app to unload.
|
|
176
|
+
* @param skipSwitchToHome - When true, skip the switchToHome() call. Used by
|
|
177
|
+
* launchApp when the caller has opted out of the home transition (e.g.
|
|
178
|
+
* satellite mode).
|
|
174
179
|
*/
|
|
175
|
-
export function unloadApp(id) {
|
|
180
|
+
export function unloadApp(id, skipSwitchToHome = false) {
|
|
176
181
|
var _a;
|
|
177
182
|
if (activeApp.id !== id)
|
|
178
183
|
return;
|
|
@@ -184,7 +189,8 @@ export function unloadApp(id) {
|
|
|
184
189
|
// the next microtask for any slots that no longer have a renderer).
|
|
185
190
|
// Switch to home first so LayoutRenderer stops reading the app's
|
|
186
191
|
// tree before detachApp drops its references.
|
|
187
|
-
|
|
192
|
+
if (!skipSwitchToHome)
|
|
193
|
+
switchToHome();
|
|
188
194
|
detachApp();
|
|
189
195
|
// Deactivate this app's required shards IF no other consumer needs
|
|
190
196
|
// them. Phase 8 has at most one app active at a time, so "no other
|
|
@@ -612,3 +612,21 @@ describe('lifecycle — clears nav entries', () => {
|
|
|
612
612
|
expect(onPop).not.toHaveBeenCalled();
|
|
613
613
|
});
|
|
614
614
|
});
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// Scenario D.1 — launchApp opts.skipLastApp suppresses lastApp persistence
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
describe('launchApp — scenario D.1 skipLastApp option', () => {
|
|
619
|
+
beforeEach(resetFramework);
|
|
620
|
+
it('does NOT persist lastApp when skipLastApp: true', async () => {
|
|
621
|
+
const { readLastApp } = await import('./lifecycle');
|
|
622
|
+
registerApp(makeApp({ manifest: makeAppManifest({ id: 'app-skip-last' }) }));
|
|
623
|
+
await launchApp('app-skip-last', { skipLastApp: true });
|
|
624
|
+
expect(readLastApp()).toBeNull();
|
|
625
|
+
});
|
|
626
|
+
it('DOES persist lastApp by default (regression guard)', async () => {
|
|
627
|
+
const { readLastApp } = await import('./lifecycle');
|
|
628
|
+
registerApp(makeApp({ manifest: makeAppManifest({ id: 'app-default-last' }) }));
|
|
629
|
+
await launchApp('app-default-last');
|
|
630
|
+
expect(readLastApp()).toBe('app-default-last');
|
|
631
|
+
});
|
|
632
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Satellite mode detection — runs once during createShell() before zone
|
|
3
|
+
* initialization. Reads location.search; returns null for main mode,
|
|
4
|
+
* { payload } for valid satellite, { error } for malformed.
|
|
5
|
+
*/
|
|
6
|
+
import { decodePayload } from './satellitePayload';
|
|
7
|
+
export function detectSatelliteMode() {
|
|
8
|
+
if (typeof window === 'undefined')
|
|
9
|
+
return null;
|
|
10
|
+
const params = new URLSearchParams(window.location.search);
|
|
11
|
+
if (params.get('mode') !== 'satellite')
|
|
12
|
+
return null;
|
|
13
|
+
const payload = params.get('payload');
|
|
14
|
+
if (!payload)
|
|
15
|
+
return { error: 'Satellite mode requires payload param' };
|
|
16
|
+
try {
|
|
17
|
+
return { payload: decodePayload(payload) };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return { error: err.message };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { detectSatelliteMode } from './satelliteMode';
|
|
3
|
+
import { encodePayload } from './satellitePayload';
|
|
4
|
+
function withSearch(search, fn) {
|
|
5
|
+
const original = window.location.search;
|
|
6
|
+
Object.defineProperty(window.location, 'search', {
|
|
7
|
+
value: search,
|
|
8
|
+
configurable: true,
|
|
9
|
+
});
|
|
10
|
+
try {
|
|
11
|
+
fn();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
Object.defineProperty(window.location, 'search', {
|
|
15
|
+
value: original,
|
|
16
|
+
configurable: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
describe('detectSatelliteMode', () => {
|
|
21
|
+
it('returns null for empty search', () => {
|
|
22
|
+
withSearch('', () => {
|
|
23
|
+
expect(detectSatelliteMode()).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
it('returns null when mode is not satellite', () => {
|
|
27
|
+
withSearch('?mode=other', () => {
|
|
28
|
+
expect(detectSatelliteMode()).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it('returns null when satellite mode without payload', () => {
|
|
32
|
+
withSearch('?mode=satellite', () => {
|
|
33
|
+
expect(detectSatelliteMode()).toEqual({
|
|
34
|
+
error: 'Satellite mode requires payload param',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('returns the decoded payload on a well-formed URL', () => {
|
|
39
|
+
const payload = {
|
|
40
|
+
kind: 'app',
|
|
41
|
+
appId: 'sh3-store',
|
|
42
|
+
activateShards: [],
|
|
43
|
+
};
|
|
44
|
+
withSearch(`?mode=satellite&payload=${encodePayload(payload)}`, () => {
|
|
45
|
+
const result = detectSatelliteMode();
|
|
46
|
+
expect(result).toEqual({ payload });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('returns an error object on malformed payload', () => {
|
|
50
|
+
withSearch('?mode=satellite&payload=!!!notbase64!!!', () => {
|
|
51
|
+
const result = detectSatelliteMode();
|
|
52
|
+
expect(result).toMatchObject({ error: expect.stringContaining('Invalid') });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LayoutNode } from '../layout/types';
|
|
2
|
+
export type SatellitePayload = {
|
|
3
|
+
kind: 'float';
|
|
4
|
+
content: LayoutNode;
|
|
5
|
+
title?: string;
|
|
6
|
+
size: {
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
};
|
|
10
|
+
activateShards: string[];
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'app';
|
|
13
|
+
appId: string;
|
|
14
|
+
activateShards: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare function encodePayload(payload: SatellitePayload): string;
|
|
17
|
+
export declare function decodePayload(encoded: string): SatellitePayload;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Satellite payload — what a host shell tells a freshly-spawned satellite
|
|
3
|
+
* window to render. Encoded as URL-safe base64 JSON in the `payload` query
|
|
4
|
+
* param of the satellite's URL.
|
|
5
|
+
*/
|
|
6
|
+
export function encodePayload(payload) {
|
|
7
|
+
// URL-safe base64: replace +/= with -_ and strip padding so the result
|
|
8
|
+
// can be embedded in a query string without further percent-encoding.
|
|
9
|
+
// Plain btoa() output uses + which URLSearchParams decodes as space.
|
|
10
|
+
return btoa(JSON.stringify(payload))
|
|
11
|
+
.replace(/\+/g, '-')
|
|
12
|
+
.replace(/\//g, '_')
|
|
13
|
+
.replace(/=+$/, '');
|
|
14
|
+
}
|
|
15
|
+
export function decodePayload(encoded) {
|
|
16
|
+
let json;
|
|
17
|
+
try {
|
|
18
|
+
const restored = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
|
19
|
+
json = atob(restored);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
throw new Error(`Invalid satellite payload encoding: ${err.message}`);
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(json);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new Error(`Invalid satellite payload JSON: ${err.message}`);
|
|
30
|
+
}
|
|
31
|
+
return validate(parsed);
|
|
32
|
+
}
|
|
33
|
+
function validate(value) {
|
|
34
|
+
if (!value || typeof value !== 'object') {
|
|
35
|
+
throw new Error('Satellite payload must be an object');
|
|
36
|
+
}
|
|
37
|
+
const v = value;
|
|
38
|
+
if (v.kind === 'float') {
|
|
39
|
+
if (!v.content || typeof v.content !== 'object') {
|
|
40
|
+
throw new Error('Float payload missing content');
|
|
41
|
+
}
|
|
42
|
+
if (!v.size || typeof v.size !== 'object') {
|
|
43
|
+
throw new Error('Float payload missing size');
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(v.activateShards)) {
|
|
46
|
+
throw new Error('Float payload missing activateShards');
|
|
47
|
+
}
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
if (v.kind === 'app') {
|
|
51
|
+
if (typeof v.appId !== 'string') {
|
|
52
|
+
throw new Error('App payload missing appId');
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(v.activateShards)) {
|
|
55
|
+
throw new Error('App payload missing activateShards');
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Unknown satellite payload kind: ${String(v.kind)}`);
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { encodePayload, decodePayload, } from './satellitePayload';
|
|
3
|
+
describe('satellite payload encode/decode', () => {
|
|
4
|
+
it('round-trips a float payload', () => {
|
|
5
|
+
const payload = {
|
|
6
|
+
kind: 'float',
|
|
7
|
+
content: { type: 'slot', slotId: 's:1', viewId: 'foo:bar' },
|
|
8
|
+
title: 'Hello',
|
|
9
|
+
size: { w: 800, h: 600 },
|
|
10
|
+
activateShards: ['foo'],
|
|
11
|
+
};
|
|
12
|
+
const enc = encodePayload(payload);
|
|
13
|
+
expect(typeof enc).toBe('string');
|
|
14
|
+
expect(decodePayload(enc)).toEqual(payload);
|
|
15
|
+
});
|
|
16
|
+
it('round-trips an app payload', () => {
|
|
17
|
+
const payload = {
|
|
18
|
+
kind: 'app',
|
|
19
|
+
appId: 'sh3-store',
|
|
20
|
+
activateShards: ['sh3-core', 'sh3-store'],
|
|
21
|
+
};
|
|
22
|
+
expect(decodePayload(encodePayload(payload))).toEqual(payload);
|
|
23
|
+
});
|
|
24
|
+
it('throws on garbage input', () => {
|
|
25
|
+
expect(() => decodePayload('!!!not-base64!!!')).toThrow();
|
|
26
|
+
});
|
|
27
|
+
it('throws on a base64 string that is not valid JSON', () => {
|
|
28
|
+
const notJson = btoa('hello world');
|
|
29
|
+
expect(() => decodePayload(notJson)).toThrow();
|
|
30
|
+
});
|
|
31
|
+
it('throws when decoded JSON is missing required fields', () => {
|
|
32
|
+
const bad = btoa(JSON.stringify({ kind: 'float' }));
|
|
33
|
+
expect(() => decodePayload(bad)).toThrow();
|
|
34
|
+
});
|
|
35
|
+
it('throws on unknown kind', () => {
|
|
36
|
+
const bad = btoa(JSON.stringify({ kind: 'mystery' }));
|
|
37
|
+
expect(() => decodePayload(bad)).toThrow();
|
|
38
|
+
});
|
|
39
|
+
it('produces a URL-safe encoding (no + / =)', () => {
|
|
40
|
+
// A long-ish payload increases the chance the underlying base64
|
|
41
|
+
// would contain non-URL-safe characters from the standard alphabet.
|
|
42
|
+
const payload = {
|
|
43
|
+
kind: 'float',
|
|
44
|
+
content: { type: 'slot', slotId: 's:111', viewId: 'foo:bar' },
|
|
45
|
+
title: '????>>>~~~`',
|
|
46
|
+
size: { w: 1234, h: 567 },
|
|
47
|
+
activateShards: ['some.long.shard.id', 'another-shard', 'third_shard'],
|
|
48
|
+
};
|
|
49
|
+
const encoded = encodePayload(payload);
|
|
50
|
+
expect(encoded).not.toMatch(/[+/=]/);
|
|
51
|
+
expect(decodePayload(encoded)).toEqual(payload);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/dist/createShell.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mount, unmount } from 'svelte';
|
|
10
10
|
import { Shell } from './index';
|
|
11
|
-
import { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner, } from './host';
|
|
11
|
+
import { registerShard, registerApp, bootstrap, bootstrapSatellite, __setBackend, setLocalOwner, } from './host';
|
|
12
12
|
import { resolvePlatform } from './platform/index';
|
|
13
13
|
import { hydrateTokenOverrides } from './theme';
|
|
14
14
|
import { __setEnvServerUrl } from './env/index';
|
|
@@ -18,8 +18,11 @@ import SignInWall from './auth/SignInWall.svelte';
|
|
|
18
18
|
import { loadBundleModule } from './registry/loader';
|
|
19
19
|
import { registerLoadedBundle } from './registry/register';
|
|
20
20
|
import { attachGlobalListeners } from './actions/listeners';
|
|
21
|
+
import { detectSatelliteMode } from './boot/satelliteMode';
|
|
22
|
+
import { MemoryBackend } from './state/backends';
|
|
23
|
+
import SatelliteShell from './satellite/SatelliteShell.svelte';
|
|
21
24
|
export async function createShell(config) {
|
|
22
|
-
var _a, _b
|
|
25
|
+
var _a, _b;
|
|
23
26
|
const sUrl = (_a = config === null || config === void 0 ? void 0 : config.serverUrl) !== null && _a !== void 0 ? _a : '';
|
|
24
27
|
// 1. Platform detection
|
|
25
28
|
const platform = await resolvePlatform();
|
|
@@ -39,6 +42,41 @@ export async function createShell(config) {
|
|
|
39
42
|
if (!target) {
|
|
40
43
|
throw new Error('SH3: mount target not found');
|
|
41
44
|
}
|
|
45
|
+
// 2b. Satellite-mode short-circuit. A Tauri webview opened by the host's
|
|
46
|
+
// sh3_spawn_satellite command lands here via ?mode=satellite&payload=...
|
|
47
|
+
// and skips auth, boot config, framework autostart, and lastApp launch.
|
|
48
|
+
// The host's workspace zone is forced to MemoryBackend so the satellite's
|
|
49
|
+
// layout/floats are isolated; user and document zones stay shared so
|
|
50
|
+
// theme prefs and document data follow the user across windows.
|
|
51
|
+
const satellite = detectSatelliteMode();
|
|
52
|
+
if (satellite) {
|
|
53
|
+
if ('error' in satellite) {
|
|
54
|
+
target.textContent = `Invalid satellite payload: ${satellite.error}`;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
__setBackend('workspace', new MemoryBackend());
|
|
58
|
+
// Document zone needs a scope; in localOwner (Tauri/dev) it's 'local'
|
|
59
|
+
// matching the host. Web satellites would need a tenant from /api/boot,
|
|
60
|
+
// but pop-out is currently a Tauri-only POC so we don't fetch it.
|
|
61
|
+
if (platform.localOwner)
|
|
62
|
+
__setActiveScope('local');
|
|
63
|
+
if (config === null || config === void 0 ? void 0 : config.shards)
|
|
64
|
+
for (const shard of config.shards)
|
|
65
|
+
registerShard(shard);
|
|
66
|
+
if (config === null || config === void 0 ? void 0 : config.apps)
|
|
67
|
+
for (const app of config.apps)
|
|
68
|
+
registerApp(app);
|
|
69
|
+
// Server-discovered packages must be loaded here too — apps installed
|
|
70
|
+
// via /api/packages aren't in IndexedDB on the satellite's view of the
|
|
71
|
+
// world unless we fetch and register them, same as the main path.
|
|
72
|
+
await loadDiscoveredPackages(config === null || config === void 0 ? void 0 : config.discoveredPackages);
|
|
73
|
+
await bootstrapSatellite({
|
|
74
|
+
activateShardIds: satellite.payload.activateShards,
|
|
75
|
+
});
|
|
76
|
+
attachGlobalListeners();
|
|
77
|
+
mount(SatelliteShell, { target, props: { payload: satellite.payload } });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
42
80
|
// 3. Fetch boot config (skip for local-owner platforms like Tauri/dev)
|
|
43
81
|
let bootConfig = null;
|
|
44
82
|
if (!platform.localOwner) {
|
|
@@ -48,7 +86,7 @@ export async function createShell(config) {
|
|
|
48
86
|
bootConfig = await res.json();
|
|
49
87
|
}
|
|
50
88
|
}
|
|
51
|
-
catch (
|
|
89
|
+
catch (_c) {
|
|
52
90
|
// Server unreachable — boot without auth (offline mode)
|
|
53
91
|
}
|
|
54
92
|
}
|
|
@@ -75,28 +113,7 @@ export async function createShell(config) {
|
|
|
75
113
|
}
|
|
76
114
|
}
|
|
77
115
|
// 5. Load server-discovered packages
|
|
78
|
-
|
|
79
|
-
for (const pkg of config.discoveredPackages) {
|
|
80
|
-
try {
|
|
81
|
-
const res = await fetch(pkg.bundleUrl);
|
|
82
|
-
if (!res.ok) {
|
|
83
|
-
console.warn(`[sh3] Failed to fetch discovered package "${pkg.id}": HTTP ${res.status}`);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
const bytes = await res.arrayBuffer();
|
|
87
|
-
const loaded = await loadBundleModule(bytes);
|
|
88
|
-
registerLoadedBundle(loaded, {
|
|
89
|
-
version: pkg.version,
|
|
90
|
-
sourceRegistry: (_d = pkg.sourceRegistry) !== null && _d !== void 0 ? _d : '',
|
|
91
|
-
contractVersion: (_e = pkg.contractVersion) !== null && _e !== void 0 ? _e : '',
|
|
92
|
-
});
|
|
93
|
-
console.log(`[sh3] Loaded discovered package: ${pkg.id}`);
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
console.warn(`[sh3] Failed to load discovered package "${pkg.id}":`, err);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
116
|
+
await loadDiscoveredPackages(config === null || config === void 0 ? void 0 : config.discoveredPackages);
|
|
100
117
|
// 6. Register consumer-provided shards and apps
|
|
101
118
|
if (config === null || config === void 0 ? void 0 : config.shards) {
|
|
102
119
|
for (const shard of config.shards)
|
|
@@ -116,6 +133,36 @@ export async function createShell(config) {
|
|
|
116
133
|
// 9. Mount the shell
|
|
117
134
|
mount(Shell, { target });
|
|
118
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Fetch and register every package the server reported via /api/packages.
|
|
138
|
+
* Shared by main-mode boot and the satellite-mode short-circuit so popped-out
|
|
139
|
+
* windows see the same installed apps as the host.
|
|
140
|
+
*/
|
|
141
|
+
async function loadDiscoveredPackages(packages) {
|
|
142
|
+
var _a, _b;
|
|
143
|
+
if (!(packages === null || packages === void 0 ? void 0 : packages.length))
|
|
144
|
+
return;
|
|
145
|
+
for (const pkg of packages) {
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(pkg.bundleUrl);
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
console.warn(`[sh3] Failed to fetch discovered package "${pkg.id}": HTTP ${res.status}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const bytes = await res.arrayBuffer();
|
|
153
|
+
const loaded = await loadBundleModule(bytes);
|
|
154
|
+
registerLoadedBundle(loaded, {
|
|
155
|
+
version: pkg.version,
|
|
156
|
+
sourceRegistry: (_a = pkg.sourceRegistry) !== null && _a !== void 0 ? _a : '',
|
|
157
|
+
contractVersion: (_b = pkg.contractVersion) !== null && _b !== void 0 ? _b : '',
|
|
158
|
+
});
|
|
159
|
+
console.log(`[sh3] Loaded discovered package: ${pkg.id}`);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
console.warn(`[sh3] Failed to load discovered package "${pkg.id}":`, err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
119
166
|
/**
|
|
120
167
|
* Show the sign-in wall and wait until the user authenticates.
|
|
121
168
|
* Returns a promise that resolves after successful login.
|
package/dist/host.d.ts
CHANGED
|
@@ -12,4 +12,17 @@ export interface BootstrapConfig {
|
|
|
12
12
|
excludeShards?: string[];
|
|
13
13
|
}
|
|
14
14
|
export declare function bootstrap(config?: BootstrapConfig): Promise<void>;
|
|
15
|
+
export interface BootstrapSatelliteConfig {
|
|
16
|
+
/** Shard ids the satellite needs activated to render its payload. */
|
|
17
|
+
activateShardIds: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Boot the satellite path: register the same framework shards and apps as
|
|
21
|
+
* the host, load installed packages, then activate exactly the shards the
|
|
22
|
+
* caller requested. Skips the host-only steps:
|
|
23
|
+
* - workspace zone migrations (host already ran them; we use MemoryBackend anyway)
|
|
24
|
+
* - autostart sweep (we activate exactly what the payload requires)
|
|
25
|
+
* - lastApp auto-launch (satellites have no last-app concept)
|
|
26
|
+
*/
|
|
27
|
+
export declare function bootstrapSatellite(config: BootstrapSatelliteConfig): Promise<void>;
|
|
15
28
|
export { installPackage, listInstalledPackages } from './registry/installer';
|
package/dist/host.js
CHANGED
|
@@ -122,4 +122,40 @@ export async function bootstrap(config) {
|
|
|
122
122
|
installWebEmitter();
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Boot the satellite path: register the same framework shards and apps as
|
|
127
|
+
* the host, load installed packages, then activate exactly the shards the
|
|
128
|
+
* caller requested. Skips the host-only steps:
|
|
129
|
+
* - workspace zone migrations (host already ran them; we use MemoryBackend anyway)
|
|
130
|
+
* - autostart sweep (we activate exactly what the payload requires)
|
|
131
|
+
* - lastApp auto-launch (satellites have no last-app concept)
|
|
132
|
+
*/
|
|
133
|
+
export async function bootstrapSatellite(config) {
|
|
134
|
+
// 1. Framework-owned shards (same list as bootstrap, no excludes for satellites)
|
|
135
|
+
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard, layoutsShard];
|
|
136
|
+
for (const shard of frameworkShards) {
|
|
137
|
+
registerShardInternal(shard);
|
|
138
|
+
}
|
|
139
|
+
// 2. Framework-shipped apps
|
|
140
|
+
const frameworkApps = [storeApp, adminApp];
|
|
141
|
+
for (const app of frameworkApps) {
|
|
142
|
+
registerApp(app);
|
|
143
|
+
}
|
|
144
|
+
// 3. Load any packages installed in a previous session from IndexedDB
|
|
145
|
+
await loadInstalledPackages();
|
|
146
|
+
// 4. Activate exactly the requested shards.
|
|
147
|
+
for (const id of config.activateShardIds) {
|
|
148
|
+
if (registeredShards.has(id)) {
|
|
149
|
+
try {
|
|
150
|
+
await activateShard(id, { phase: 'satellite' });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error(`[sh3] satellite activation of "${id}" failed:`, err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.warn(`[sh3] satellite requested shard "${id}" but it is not registered`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
125
161
|
export { installPackage, listInstalledPackages } from './registry/installer';
|
|
@@ -77,6 +77,17 @@ export declare const layoutStore: {
|
|
|
77
77
|
readonly tree: LayoutTree;
|
|
78
78
|
readonly floats: FloatEntry[];
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Satellite-mode layout seed. Replaces the HOME_TREE's docked node with
|
|
82
|
+
* the supplied LayoutNode so that `layoutStore.root` (and LayoutRenderer)
|
|
83
|
+
* immediately reflects the satellite payload's content without going through
|
|
84
|
+
* the workspace-zone-backed app attach path.
|
|
85
|
+
*
|
|
86
|
+
* Only for float payloads — app payloads call `launchApp` which handles the
|
|
87
|
+
* full attach/switch lifecycle itself. Must be called before LayoutRenderer
|
|
88
|
+
* mounts (i.e. at component instantiation time in SatelliteShell).
|
|
89
|
+
*/
|
|
90
|
+
export declare function seedSatelliteLayout(node: LayoutNode): void;
|
|
80
91
|
/**
|
|
81
92
|
* Test-only reset. Restores the layout store to its boot state: no app
|
|
82
93
|
* attached, active root = 'home'. Not exported from `src/index.ts` —
|
|
@@ -318,6 +318,21 @@ export const layoutStore = {
|
|
|
318
318
|
return activeTree.floats;
|
|
319
319
|
},
|
|
320
320
|
};
|
|
321
|
+
/**
|
|
322
|
+
* Satellite-mode layout seed. Replaces the HOME_TREE's docked node with
|
|
323
|
+
* the supplied LayoutNode so that `layoutStore.root` (and LayoutRenderer)
|
|
324
|
+
* immediately reflects the satellite payload's content without going through
|
|
325
|
+
* the workspace-zone-backed app attach path.
|
|
326
|
+
*
|
|
327
|
+
* Only for float payloads — app payloads call `launchApp` which handles the
|
|
328
|
+
* full attach/switch lifecycle itself. Must be called before LayoutRenderer
|
|
329
|
+
* mounts (i.e. at component instantiation time in SatelliteShell).
|
|
330
|
+
*/
|
|
331
|
+
export function seedSatelliteLayout(node) {
|
|
332
|
+
HOME_TREE.docked = node;
|
|
333
|
+
HOME_TREE.floats = [];
|
|
334
|
+
activeRoot = 'home';
|
|
335
|
+
}
|
|
321
336
|
/**
|
|
322
337
|
* Test-only reset. Restores the layout store to its boot state: no app
|
|
323
338
|
* attached, active root = 'home'. Not exported from `src/index.ts` —
|