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.
Files changed (60) hide show
  1. package/dist/api.d.ts +4 -1
  2. package/dist/api.js +2 -0
  3. package/dist/apps/lifecycle.d.ts +21 -2
  4. package/dist/apps/lifecycle.js +13 -7
  5. package/dist/apps/lifecycle.test.js +18 -0
  6. package/dist/boot/satelliteMode.d.ts +7 -0
  7. package/dist/boot/satelliteMode.js +22 -0
  8. package/dist/boot/satelliteMode.test.d.ts +1 -0
  9. package/dist/boot/satelliteMode.test.js +55 -0
  10. package/dist/boot/satellitePayload.d.ts +17 -0
  11. package/dist/boot/satellitePayload.js +60 -0
  12. package/dist/boot/satellitePayload.test.d.ts +1 -0
  13. package/dist/boot/satellitePayload.test.js +53 -0
  14. package/dist/createShell.js +72 -25
  15. package/dist/host.d.ts +13 -0
  16. package/dist/host.js +36 -0
  17. package/dist/layout/store.svelte.d.ts +11 -0
  18. package/dist/layout/store.svelte.js +15 -0
  19. package/dist/overlays/FloatFrame.svelte +36 -0
  20. package/dist/runtime/runVerb-shell.test.js +0 -39
  21. package/dist/runtime/runVerb.test.js +17 -0
  22. package/dist/satellite/SatelliteShell.svelte +60 -0
  23. package/dist/satellite/SatelliteShell.svelte.d.ts +9 -0
  24. package/dist/satellite/seed.d.ts +3 -0
  25. package/dist/satellite/seed.js +20 -0
  26. package/dist/satellite/seed.test.d.ts +1 -0
  27. package/dist/satellite/seed.test.js +38 -0
  28. package/dist/satellite/walkShards.d.ts +2 -0
  29. package/dist/satellite/walkShards.js +44 -0
  30. package/dist/satellite/walkShards.test.d.ts +1 -0
  31. package/dist/satellite/walkShards.test.js +65 -0
  32. package/dist/sh3core-shard/appActions.js +51 -0
  33. package/dist/shards/activate.svelte.d.ts +2 -2
  34. package/dist/shards/activate.svelte.js +1 -1
  35. package/dist/shards/registry.d.ts +2 -1
  36. package/dist/shards/registry.js +13 -4
  37. package/dist/shards/registry.test.js +22 -1
  38. package/dist/shards/types.d.ts +1 -0
  39. package/dist/shell-shard/CommandLine.svelte +3 -0
  40. package/dist/shell-shard/CommandLine.svelte.d.ts +1 -0
  41. package/dist/shell-shard/InputLine.svelte +4 -1
  42. package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
  43. package/dist/shell-shard/Terminal.svelte +24 -0
  44. package/dist/shell-shard/dispatch-to-terminal.d.ts +13 -0
  45. package/dist/shell-shard/dispatch-to-terminal.js +37 -0
  46. package/dist/shell-shard/dispatch-to-terminal.test.d.ts +1 -0
  47. package/dist/shell-shard/dispatch-to-terminal.test.js +79 -0
  48. package/dist/shell-shard/shellApi.js +2 -0
  49. package/dist/shell-shard/terminal-registry.d.ts +25 -0
  50. package/dist/shell-shard/terminal-registry.js +62 -0
  51. package/dist/shell-shard/terminal-registry.test.d.ts +1 -0
  52. package/dist/shell-shard/terminal-registry.test.js +88 -0
  53. package/dist/shellApi/window.d.ts +15 -0
  54. package/dist/shellApi/window.js +43 -0
  55. package/dist/shellApi/window.test.d.ts +1 -0
  56. package/dist/shellApi/window.test.js +19 -0
  57. package/dist/verbs/types.d.ts +15 -0
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. 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';
@@ -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
  *
@@ -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
- writeLastApp(id);
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
- writeLastApp(id);
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
- switchToHome();
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,7 @@
1
+ import { type SatellitePayload } from './satellitePayload';
2
+ export type SatelliteDetection = null | {
3
+ payload: SatellitePayload;
4
+ } | {
5
+ error: string;
6
+ };
7
+ export declare function detectSatelliteMode(): SatelliteDetection;
@@ -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
+ });
@@ -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, _c, _d, _e;
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 (_f) {
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
- if ((_c = config === null || config === void 0 ? void 0 : config.discoveredPackages) === null || _c === void 0 ? void 0 : _c.length) {
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` —