sh3-core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/Shell.svelte +20 -14
  2. package/dist/api.d.ts +5 -3
  3. package/dist/app/admin/adminApp.js +2 -1
  4. package/dist/app/admin/adminShard.svelte.js +2 -1
  5. package/dist/app/store/StoreView.svelte +11 -5
  6. package/dist/app/store/storeApp.js +2 -1
  7. package/dist/app/store/storeShard.svelte.js +9 -4
  8. package/dist/apps/terminal/manifest.js +2 -1
  9. package/dist/apps/types.d.ts +28 -7
  10. package/dist/build.d.ts +5 -2
  11. package/dist/build.js +21 -10
  12. package/dist/env/client.d.ts +10 -2
  13. package/dist/env/client.js +13 -2
  14. package/dist/layout/LayoutRenderer.svelte +21 -9
  15. package/dist/layout/LayoutRenderer.svelte.d.ts +2 -0
  16. package/dist/layout/SlotDropZone.svelte +4 -1
  17. package/dist/layout/SlotDropZone.svelte.d.ts +2 -0
  18. package/dist/layout/drag.svelte.d.ts +5 -2
  19. package/dist/layout/drag.svelte.js +43 -11
  20. package/dist/layout/floats.d.ts +35 -0
  21. package/dist/layout/floats.js +73 -0
  22. package/dist/layout/floats.test.d.ts +1 -0
  23. package/dist/layout/floats.test.js +114 -0
  24. package/dist/layout/inspection.d.ts +2 -2
  25. package/dist/layout/inspection.js +6 -6
  26. package/dist/layout/ops.d.ts +14 -1
  27. package/dist/layout/ops.js +17 -0
  28. package/dist/layout/ops.test.d.ts +1 -0
  29. package/dist/layout/ops.test.js +36 -0
  30. package/dist/layout/presets.d.ts +2 -0
  31. package/dist/layout/presets.js +49 -0
  32. package/dist/layout/presets.test.d.ts +1 -0
  33. package/dist/layout/presets.test.js +71 -0
  34. package/dist/layout/store.svelte.d.ts +17 -13
  35. package/dist/layout/store.svelte.js +98 -36
  36. package/dist/layout/tree-walk.d.ts +12 -1
  37. package/dist/layout/tree-walk.js +13 -0
  38. package/dist/layout/tree-walk.test.d.ts +1 -0
  39. package/dist/layout/tree-walk.test.js +41 -0
  40. package/dist/layout/types.d.ts +96 -6
  41. package/dist/layout/types.js +1 -1
  42. package/dist/overlays/FloatFrame.svelte +141 -0
  43. package/dist/overlays/FloatFrame.svelte.d.ts +7 -0
  44. package/dist/overlays/FloatLayer.svelte +28 -0
  45. package/dist/overlays/FloatLayer.svelte.d.ts +3 -0
  46. package/dist/overlays/float.d.ts +29 -0
  47. package/dist/overlays/float.js +119 -0
  48. package/dist/overlays/float.test.d.ts +1 -0
  49. package/dist/overlays/float.test.js +37 -0
  50. package/dist/overlays/presets.d.ts +21 -0
  51. package/dist/overlays/presets.js +63 -0
  52. package/dist/overlays/presets.test.d.ts +1 -0
  53. package/dist/overlays/presets.test.js +40 -0
  54. package/dist/registry/client.d.ts +14 -0
  55. package/dist/registry/client.js +37 -0
  56. package/dist/registry/client.test.d.ts +1 -0
  57. package/dist/registry/client.test.js +54 -0
  58. package/dist/registry/installer.js +18 -5
  59. package/dist/registry/schema.js +5 -0
  60. package/dist/registry/types.d.ts +9 -0
  61. package/dist/shards/types.d.ts +27 -4
  62. package/dist/shell-shard/Terminal.svelte +14 -8
  63. package/dist/shell-shard/manifest.js +2 -1
  64. package/dist/shell-shard/shellShard.svelte.js +2 -1
  65. package/dist/shellRuntime.svelte.d.ts +6 -0
  66. package/dist/shellRuntime.svelte.js +4 -0
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +6 -3
@@ -0,0 +1,119 @@
1
+ /*
2
+ * Float manager — layer 1 of the overlay stack.
3
+ *
4
+ * Public API (shell.float):
5
+ * open(viewId, options?) → floatId
6
+ * close(floatId)
7
+ * list() → FloatEntry[]
8
+ * focus(floatId) — raise to top of z-order within layer 1
9
+ *
10
+ * Semantics (see docs/design/layout.md layer stack and docs/superpowers/spec/
11
+ * 2026-04-11-layout-topology-design.md):
12
+ * - Float content is drawn by FloatLayer.svelte via the same LayoutRenderer
13
+ * used by the docked tree. The manager only mutates data; rendering is
14
+ * a pure reaction to the LayoutTree's `floats` array.
15
+ * - Cascade default position, default size = max(600x400, computed min).
16
+ * - Click-to-raise z-order: the most recently focused float is the last
17
+ * element of `floats[]` and therefore drawn on top.
18
+ * - Auto-close on empty is enforced by the drag commit path (see Task 3.3),
19
+ * not by this manager.
20
+ * - Multiple floats of the same viewId are allowed; toggle semantics are
21
+ * userland (caller checks list() and decides).
22
+ *
23
+ * Binding:
24
+ * The manager is bound to a live FloatEntry[] (the active LayoutTree's
25
+ * floats) by `bindFloatStore()` during Shell boot. Before binding, an
26
+ * in-memory fallback array is used — this is both the test environment
27
+ * and the pre-boot state.
28
+ */
29
+ import { computeMinSize, cascadePosition, generateFloatId } from '../layout/floats';
30
+ // ----- storage binding ---------------------------------------------------
31
+ let fallbackFloats = [];
32
+ let boundFloats = null;
33
+ let getTreeBounds = () => ({ w: 1600, h: 900 });
34
+ /**
35
+ * Bind the manager to the active LayoutTree's `floats` array. Called
36
+ * from Shell.svelte during boot. `getBounds` returns the current
37
+ * tree-allocated area for cascade-position wraparound.
38
+ */
39
+ export function bindFloatStore(floats, getBounds) {
40
+ boundFloats = floats;
41
+ getTreeBounds = getBounds;
42
+ }
43
+ export function unbindFloatStore() {
44
+ boundFloats = null;
45
+ getTreeBounds = () => ({ w: 1600, h: 900 });
46
+ }
47
+ /** Test-only reset. Clears in-memory fallback and unbinds any store. */
48
+ export function __resetFloatManagerForTest() {
49
+ fallbackFloats = [];
50
+ boundFloats = null;
51
+ getTreeBounds = () => ({ w: 1600, h: 900 });
52
+ }
53
+ function activeStore() {
54
+ return boundFloats !== null && boundFloats !== void 0 ? boundFloats : fallbackFloats;
55
+ }
56
+ // ----- slot id minting ---------------------------------------------------
57
+ let floatSlotCounter = 0;
58
+ function mintFloatSlotId(viewId) {
59
+ floatSlotCounter += 1;
60
+ return `float:${viewId}:${floatSlotCounter}`;
61
+ }
62
+ // ----- API ---------------------------------------------------------------
63
+ const DEFAULT_SIZE = { w: 600, h: 400 };
64
+ function maxSize(a, b) {
65
+ return { w: Math.max(a.w, b.w), h: Math.max(a.h, b.h) };
66
+ }
67
+ function openFloat(viewId, options = {}) {
68
+ var _a, _b, _c;
69
+ const store = activeStore();
70
+ const id = generateFloatId();
71
+ // Wrap the slot in a single-tab TabsNode so the tab strip acts as a
72
+ // drag handle — that's the only way to drag the view back into the
73
+ // docked tree. The TabsNode's tab strip appears at the top of the
74
+ // float body; the frame header still moves the float as a whole.
75
+ const slotId = mintFloatSlotId(viewId);
76
+ const label = (_a = options.title) !== null && _a !== void 0 ? _a : viewId;
77
+ const content = {
78
+ type: 'tabs',
79
+ tabs: [{ slotId, viewId, label }],
80
+ activeTab: 0,
81
+ };
82
+ const computedMin = computeMinSize(content);
83
+ const size = (_b = options.size) !== null && _b !== void 0 ? _b : maxSize(DEFAULT_SIZE, computedMin);
84
+ const position = (_c = options.position) !== null && _c !== void 0 ? _c : cascadePosition(store, getTreeBounds());
85
+ const entry = {
86
+ id,
87
+ content,
88
+ position,
89
+ size,
90
+ title: options.title,
91
+ };
92
+ store.push(entry);
93
+ return id;
94
+ }
95
+ function closeFloat(floatId) {
96
+ const store = activeStore();
97
+ const idx = store.findIndex((f) => f.id === floatId);
98
+ if (idx < 0)
99
+ return;
100
+ store.splice(idx, 1);
101
+ }
102
+ function listFloats() {
103
+ // Return a snapshot so callers can iterate without racing mutations.
104
+ return activeStore().slice();
105
+ }
106
+ function focusFloat(floatId) {
107
+ const store = activeStore();
108
+ const idx = store.findIndex((f) => f.id === floatId);
109
+ if (idx < 0 || idx === store.length - 1)
110
+ return;
111
+ const [entry] = store.splice(idx, 1);
112
+ store.push(entry);
113
+ }
114
+ export const floatManager = {
115
+ open: openFloat,
116
+ close: closeFloat,
117
+ list: listFloats,
118
+ focus: focusFloat,
119
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { floatManager, __resetFloatManagerForTest } from './float';
3
+ describe('floatManager', () => {
4
+ beforeEach(() => {
5
+ __resetFloatManagerForTest();
6
+ });
7
+ it('open() returns a stable id and list() includes the new float', () => {
8
+ const id = floatManager.open('test:view');
9
+ expect(typeof id).toBe('string');
10
+ expect(id.length).toBeGreaterThan(0);
11
+ const listed = floatManager.list();
12
+ expect(listed).toHaveLength(1);
13
+ expect(listed[0].id).toBe(id);
14
+ });
15
+ it('close(id) removes the float from list()', () => {
16
+ const id = floatManager.open('test:view');
17
+ floatManager.close(id);
18
+ expect(floatManager.list()).toHaveLength(0);
19
+ });
20
+ it('focus(id) moves the float to the end of the list (top of z-order)', () => {
21
+ const a = floatManager.open('view:a');
22
+ const b = floatManager.open('view:b');
23
+ const c = floatManager.open('view:c');
24
+ floatManager.focus(a);
25
+ const order = floatManager.list().map((f) => f.id);
26
+ expect(order).toEqual([b, c, a]);
27
+ });
28
+ it('open() respects options.position and options.size', () => {
29
+ const id = floatManager.open('test:view', {
30
+ position: { x: 100, y: 200 },
31
+ size: { w: 800, h: 500 },
32
+ });
33
+ const f = floatManager.list().find((e) => e.id === id);
34
+ expect(f.position).toEqual({ x: 100, y: 200 });
35
+ expect(f.size).toEqual({ w: 800, h: 500 });
36
+ });
37
+ });
@@ -0,0 +1,21 @@
1
+ import type { AppLayoutBlob } from '../layout/types';
2
+ export interface PresetManager {
3
+ /** All known preset names in declaration order. */
4
+ list(): string[];
5
+ /** Currently active preset name. */
6
+ active(): string;
7
+ /** Switch to the named preset. Throws if unknown. */
8
+ switch(name: string): void;
9
+ }
10
+ /**
11
+ * Bind the manager to the attached app's `AppLayoutBlob` workspace-zone
12
+ * proxy. Called from `attachApp` in the layout store.
13
+ */
14
+ export declare function bindPresetBlob(blob: AppLayoutBlob): void;
15
+ /** Unbind on detach. Called from `detachApp`. */
16
+ export declare function unbindPresetBlob(): void;
17
+ /** Test-only bind alias for tests that build a synthetic blob. */
18
+ export declare function __bindPresetBlobForTest(blob: AppLayoutBlob): void;
19
+ /** Test-only reset. Clears the binding. */
20
+ export declare function __resetPresetManagerForTest(): void;
21
+ export declare const presetManager: PresetManager;
@@ -0,0 +1,63 @@
1
+ /*
2
+ * Preset manager — controls which named LayoutPreset is currently active
3
+ * for the attached app. Mutations flow through the bound `AppLayoutBlob`
4
+ * proxy (workspace state zone), so switches persist automatically.
5
+ *
6
+ * v1 always reads/writes the 'default' variant; non-default keys sit inert
7
+ * until the rescoped DF10 selection policy lands (ADR-012).
8
+ *
9
+ * Saved-state-per-preset semantics:
10
+ * On switch-out, the current tree is already live-bound to
11
+ * blob.presets[activePreset].default via the layout store — customizations
12
+ * persist without any explicit save step. On switch-in, updating
13
+ * blob.activePreset causes `activeLayout()` in the store to re-drill into
14
+ * the new preset's default variant; reactivity propagates to renderers.
15
+ *
16
+ * Binding lifecycle mirrors the float manager: attachApp() calls
17
+ * bindPresetBlob(proxy), detachApp() calls unbindPresetBlob(). Before binding,
18
+ * all methods throw — there is no pre-boot fallback.
19
+ */
20
+ let boundBlob = null;
21
+ /**
22
+ * Bind the manager to the attached app's `AppLayoutBlob` workspace-zone
23
+ * proxy. Called from `attachApp` in the layout store.
24
+ */
25
+ export function bindPresetBlob(blob) {
26
+ boundBlob = blob;
27
+ }
28
+ /** Unbind on detach. Called from `detachApp`. */
29
+ export function unbindPresetBlob() {
30
+ boundBlob = null;
31
+ }
32
+ /** Test-only bind alias for tests that build a synthetic blob. */
33
+ export function __bindPresetBlobForTest(blob) {
34
+ boundBlob = blob;
35
+ }
36
+ /** Test-only reset. Clears the binding. */
37
+ export function __resetPresetManagerForTest() {
38
+ boundBlob = null;
39
+ }
40
+ function requireBlob() {
41
+ if (!boundBlob) {
42
+ throw new Error('presetManager: no app attached — bind an AppLayoutBlob first');
43
+ }
44
+ return boundBlob;
45
+ }
46
+ function listPresets() {
47
+ return Object.keys(requireBlob().presets);
48
+ }
49
+ function activePreset() {
50
+ return requireBlob().activePreset;
51
+ }
52
+ function switchPreset(name) {
53
+ const blob = requireBlob();
54
+ if (!(name in blob.presets)) {
55
+ throw new Error(`presetManager.switch: unknown preset "${name}"`);
56
+ }
57
+ blob.activePreset = name;
58
+ }
59
+ export const presetManager = {
60
+ list: listPresets,
61
+ active: activePreset,
62
+ switch: switchPreset,
63
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { presetManager, __bindPresetBlobForTest, __resetPresetManagerForTest, } from './presets';
3
+ const makeBlob = (activePreset, names) => ({
4
+ layoutVersion: 1,
5
+ activePreset,
6
+ presets: Object.fromEntries(names.map((n) => [
7
+ n,
8
+ {
9
+ default: {
10
+ docked: { type: 'slot', slotId: `${n}-s`, viewId: 'v' },
11
+ floats: [],
12
+ },
13
+ },
14
+ ])),
15
+ });
16
+ describe('presetManager', () => {
17
+ beforeEach(() => __resetPresetManagerForTest());
18
+ it('list() returns all preset names in insertion order', () => {
19
+ __bindPresetBlobForTest(makeBlob('author', ['author', 'review', 'inspect']));
20
+ expect(presetManager.list()).toEqual(['author', 'review', 'inspect']);
21
+ });
22
+ it('active() returns the active preset name', () => {
23
+ __bindPresetBlobForTest(makeBlob('review', ['author', 'review']));
24
+ expect(presetManager.active()).toBe('review');
25
+ });
26
+ it('switch(name) updates active preset', () => {
27
+ const blob = makeBlob('author', ['author', 'review']);
28
+ __bindPresetBlobForTest(blob);
29
+ presetManager.switch('review');
30
+ expect(blob.activePreset).toBe('review');
31
+ expect(presetManager.active()).toBe('review');
32
+ });
33
+ it('switch(name) throws if preset name is unknown', () => {
34
+ __bindPresetBlobForTest(makeBlob('author', ['author']));
35
+ expect(() => presetManager.switch('nope')).toThrow(/unknown preset/);
36
+ });
37
+ it('list() throws when no blob is bound', () => {
38
+ expect(() => presetManager.list()).toThrow(/no app attached/);
39
+ });
40
+ });
@@ -60,6 +60,20 @@ export declare function fetchRegistries(urls: string[]): Promise<ResolvedPackage
60
60
  * @throws If the fetch fails, the server returns a non-OK status, or the integrity check fails.
61
61
  */
62
62
  export declare function fetchBundle(version: PackageVersion, sourceRegistry?: string): Promise<ArrayBuffer>;
63
+ /**
64
+ * Download a server-side bundle and optionally verify its SRI integrity.
65
+ *
66
+ * Mirrors `fetchBundle` except `serverIntegrity` is optional in contract v1
67
+ * (see ADR-015 proposal). When absent, the download is returned without
68
+ * verification and a warning is logged so operators notice provisional
69
+ * registries. When present, an integrity mismatch throws.
70
+ *
71
+ * @param version - The `PackageVersion` describing the server bundle.
72
+ * @param sourceRegistry - The registry URL (used to resolve relative paths).
73
+ * @returns Raw server bundle bytes.
74
+ * @throws If `serverBundleUrl` is absent, the fetch fails, or an integrity mismatch occurs.
75
+ */
76
+ export declare function fetchServerBundle(version: PackageVersion, sourceRegistry?: string): Promise<ArrayBuffer>;
63
77
  /**
64
78
  * Build a `PackageMeta` record from a resolved package and a chosen version.
65
79
  *
@@ -93,6 +93,41 @@ export async function fetchBundle(version, sourceRegistry) {
93
93
  await verifyIntegrity(data, version.integrity);
94
94
  return data;
95
95
  }
96
+ /**
97
+ * Download a server-side bundle and optionally verify its SRI integrity.
98
+ *
99
+ * Mirrors `fetchBundle` except `serverIntegrity` is optional in contract v1
100
+ * (see ADR-015 proposal). When absent, the download is returned without
101
+ * verification and a warning is logged so operators notice provisional
102
+ * registries. When present, an integrity mismatch throws.
103
+ *
104
+ * @param version - The `PackageVersion` describing the server bundle.
105
+ * @param sourceRegistry - The registry URL (used to resolve relative paths).
106
+ * @returns Raw server bundle bytes.
107
+ * @throws If `serverBundleUrl` is absent, the fetch fails, or an integrity mismatch occurs.
108
+ */
109
+ export async function fetchServerBundle(version, sourceRegistry) {
110
+ if (!version.serverBundleUrl) {
111
+ throw new Error('fetchServerBundle called on a version with no serverBundleUrl');
112
+ }
113
+ let url = version.serverBundleUrl;
114
+ if (sourceRegistry && !/^https?:\/\//i.test(url)) {
115
+ url = new URL(url, sourceRegistry).href;
116
+ }
117
+ const response = await fetch(url);
118
+ if (!response.ok) {
119
+ throw new Error(`Server bundle fetch failed: HTTP ${response.status} ${response.statusText} from ${url}`);
120
+ }
121
+ const data = await response.arrayBuffer();
122
+ if (version.serverIntegrity) {
123
+ await verifyIntegrity(data, version.serverIntegrity);
124
+ }
125
+ else {
126
+ console.warn(`[sh3] Server bundle at ${url} has no serverIntegrity declared — skipping SRI check. `
127
+ + 'This will become an error once the formal registry spec (ADR-015) lands.');
128
+ }
129
+ return data;
130
+ }
96
131
  /**
97
132
  * Build a `PackageMeta` record from a resolved package and a chosen version.
98
133
  *
@@ -113,5 +148,7 @@ export function buildPackageMeta(resolved, version) {
113
148
  sourceRegistry: resolved.sourceRegistry,
114
149
  integrity: version.integrity,
115
150
  requires: version.requires,
151
+ hasServerBundle: Boolean(version.serverBundleUrl),
152
+ serverIntegrity: version.serverIntegrity,
116
153
  };
117
154
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildPackageMeta } from './client.js';
3
+ function makeResolved(version) {
4
+ return {
5
+ entry: {
6
+ id: 'test-pkg',
7
+ type: 'shard',
8
+ label: 'Test',
9
+ description: 'd',
10
+ author: { name: 'a' },
11
+ versions: [version],
12
+ },
13
+ latest: version,
14
+ sourceRegistry: 'https://example.com/registry.json',
15
+ };
16
+ }
17
+ describe('buildPackageMeta', () => {
18
+ it('sets hasServerBundle false when no serverBundleUrl', () => {
19
+ const v = {
20
+ version: '1.0.0',
21
+ contractVersion: '0.1.0',
22
+ bundleUrl: '/b.js',
23
+ integrity: 'sha384-xxx',
24
+ };
25
+ const meta = buildPackageMeta(makeResolved(v), v);
26
+ expect(meta.hasServerBundle).toBe(false);
27
+ expect(meta.serverIntegrity).toBeUndefined();
28
+ });
29
+ it('sets hasServerBundle true and propagates serverIntegrity when present', () => {
30
+ const v = {
31
+ version: '1.0.0',
32
+ contractVersion: '0.1.0',
33
+ bundleUrl: '/b.js',
34
+ integrity: 'sha384-xxx',
35
+ serverBundleUrl: '/s.js',
36
+ serverIntegrity: 'sha384-yyy',
37
+ };
38
+ const meta = buildPackageMeta(makeResolved(v), v);
39
+ expect(meta.hasServerBundle).toBe(true);
40
+ expect(meta.serverIntegrity).toBe('sha384-yyy');
41
+ });
42
+ it('sets hasServerBundle true even when serverIntegrity is missing', () => {
43
+ const v = {
44
+ version: '1.0.0',
45
+ contractVersion: '0.1.0',
46
+ bundleUrl: '/b.js',
47
+ integrity: 'sha384-xxx',
48
+ serverBundleUrl: '/s.js',
49
+ };
50
+ const meta = buildPackageMeta(makeResolved(v), v);
51
+ expect(meta.hasServerBundle).toBe(true);
52
+ expect(meta.serverIntegrity).toBeUndefined();
53
+ });
54
+ });
@@ -88,13 +88,20 @@ export async function installPackage(bundle, meta) {
88
88
  error: `Failed to persist package: ${err instanceof Error ? err.message : String(err)}`,
89
89
  };
90
90
  }
91
- // 5. Register all shards and apps from the bundle.
91
+ // 5. Stamp loader-assigned version (ADR-013) then register all shards
92
+ // and apps from the bundle. External package authors omit `version`
93
+ // from their source manifests; the authoritative value is the
94
+ // registry entry's `PackageVersion.version`, carried on `meta.version`.
92
95
  let hotLoaded = true;
93
96
  try {
94
- for (const shard of loaded.shards)
97
+ for (const shard of loaded.shards) {
98
+ shard.manifest = Object.assign(Object.assign({}, shard.manifest), { version: meta.version });
95
99
  registerShard(shard);
96
- for (const app of loaded.apps)
100
+ }
101
+ for (const app of loaded.apps) {
102
+ app.manifest = Object.assign(Object.assign({}, app.manifest), { version: meta.version });
97
103
  registerApp(app);
104
+ }
98
105
  }
99
106
  catch (err) {
100
107
  console.warn(`[sh3] Package "${meta.id}" installed but registration failed (will retry on next boot):`, err instanceof Error ? err.message : err);
@@ -153,10 +160,16 @@ export async function loadInstalledPackages() {
153
160
  continue;
154
161
  }
155
162
  const loaded = await loadBundleModule(bytes);
156
- for (const shard of loaded.shards)
163
+ // Stamp loader-assigned version (ADR-013) from the persisted
164
+ // InstalledPackage record before registration.
165
+ for (const shard of loaded.shards) {
166
+ shard.manifest = Object.assign(Object.assign({}, shard.manifest), { version: pkg.version });
157
167
  registerShard(shard);
158
- for (const app of loaded.apps)
168
+ }
169
+ for (const app of loaded.apps) {
170
+ app.manifest = Object.assign(Object.assign({}, app.manifest), { version: pkg.version });
159
171
  registerApp(app);
172
+ }
160
173
  if (loaded.shards.length === 0 && loaded.apps.length === 0) {
161
174
  console.warn(`[sh3] Package "${pkg.id}" contains no valid shards or apps, skipping`);
162
175
  }
@@ -119,6 +119,10 @@ function validatePackageVersion(data, path) {
119
119
  if ('serverBundleUrl' in obj && obj.serverBundleUrl !== undefined) {
120
120
  requireString(obj, 'serverBundleUrl', path);
121
121
  }
122
+ // Optional server bundle integrity hash — provisional, see ADR-015.
123
+ if ('serverIntegrity' in obj && obj.serverIntegrity !== undefined) {
124
+ requireString(obj, 'serverIntegrity', path);
125
+ }
122
126
  let requires;
123
127
  if (obj['requires'] !== undefined) {
124
128
  if (!Array.isArray(obj['requires'])) {
@@ -132,6 +136,7 @@ function validatePackageVersion(data, path) {
132
136
  bundleUrl: obj['bundleUrl'],
133
137
  integrity: obj['integrity'],
134
138
  serverBundleUrl: typeof obj['serverBundleUrl'] === 'string' ? obj['serverBundleUrl'] : undefined,
139
+ serverIntegrity: typeof obj['serverIntegrity'] === 'string' ? obj['serverIntegrity'] : undefined,
135
140
  requires,
136
141
  };
137
142
  }
@@ -117,6 +117,15 @@ export interface PackageVersion {
117
117
  * manifest.
118
118
  */
119
119
  serverBundleUrl?: string;
120
+ /**
121
+ * SRI integrity hash for the server bundle. Same format as `integrity`.
122
+ * Optional in contract v1 for back-compat with registries that predate
123
+ * server bundles. Will become required when the formal registry spec
124
+ * lands (see ADR-015 proposal). When absent, the client skips the SRI
125
+ * check at download time and logs a warning — the unverified bundle is
126
+ * still installed.
127
+ */
128
+ serverIntegrity?: string;
120
129
  /**
121
130
  * Other shards that must be installed and active before this package
122
131
  * can be loaded. Optional — omit if the package has no dependencies.
@@ -77,16 +77,18 @@ export interface ViewDeclaration {
77
77
  icon?: string;
78
78
  }
79
79
  /**
80
- * Static description of a shard. Declared once and read by the framework
81
- * before `activate` runs, so the shell can enumerate a shard's capabilities
82
- * without executing any shard code.
80
+ * Static description of a shard as observed by the framework and consumers
81
+ * at runtime. `version` is always present here: for externally installed
82
+ * packages it is stamped by the installer from the registry entry, for
83
+ * in-tree shards it is set from `VERSION` (the auto-generated constant
84
+ * derived from `sh3-core`'s own `package.json`). See ADR-013.
83
85
  */
84
86
  export interface ShardManifest {
85
87
  /** Unique shard identifier. Used as the state namespace and view id prefix. */
86
88
  id: string;
87
89
  /** Human-readable display name. */
88
90
  label: string;
89
- /** Semver version string for the shard package. */
91
+ /** Semver version string. Always present at load time — see `SourceShardManifest` for what authors write. */
90
92
  version: string;
91
93
  /**
92
94
  * Static list of the view ids this shard provides. Every id listed here
@@ -111,6 +113,17 @@ export interface ShardManifest {
111
113
  */
112
114
  permissions?: string[];
113
115
  }
116
+ /**
117
+ * Source-declared shape of a shard manifest — what external package authors
118
+ * write in their own code. `version` is omitted because it would duplicate
119
+ * `package.json.version`; the framework injects it at load time. See ADR-013.
120
+ *
121
+ * Authors who want a source file type can use `SourceShard` (which carries
122
+ * `manifest: SourceShardManifest`). In-tree shards that import `VERSION`
123
+ * from `sh3-core`'s auto-generated `version.ts` can keep using the full
124
+ * `Shard` / `ShardManifest` types directly.
125
+ */
126
+ export type SourceShardManifest = Omit<ShardManifest, 'version'>;
114
127
  /**
115
128
  * Handed to `shard.activate`. The shard uses it to declare state and
116
129
  * register contributions. `state` is pre-bound to the shard's id so the
@@ -205,3 +218,13 @@ export interface Shard {
205
218
  */
206
219
  resume?(ctx: ShardContext): void | Promise<void>;
207
220
  }
221
+ /**
222
+ * Source-level shape of a shard as written by external package authors.
223
+ * Carries a `SourceShardManifest` (no `version` field). The framework
224
+ * injects `version` from the registry entry's `PackageVersion.version`
225
+ * at install time, after which the object is observed as a full `Shard`.
226
+ * See ADR-013.
227
+ */
228
+ export interface SourceShard extends Omit<Shard, 'manifest'> {
229
+ manifest: SourceShardManifest;
230
+ }
@@ -71,14 +71,20 @@
71
71
  scrollback.push({ kind: 'text', stream: 'stderr', chunks: [e.data], ts: e.ts });
72
72
  break;
73
73
  case 'exit':
74
- scrollback.push({
75
- kind: 'status',
76
- text: e.signal
77
- ? `shell: process exited (${e.signal})`
78
- : `shell: process exited (${e.code ?? 0})`,
79
- level: e.code === 0 || e.code === null ? 'info' : 'error',
80
- ts: e.ts,
81
- });
74
+ // Match real-shell UX: stay silent on clean exit. Only surface
75
+ // a status line on non-zero exit codes or signal-terminated
76
+ // processes (SIGINT, spawn errors, etc.). `code === null` with
77
+ // no signal happens on clean close too — treat as success.
78
+ if (e.signal || (e.code !== null && e.code !== 0)) {
79
+ scrollback.push({
80
+ kind: 'status',
81
+ text: e.signal
82
+ ? `shell: process exited (${e.signal})`
83
+ : `shell: process exited (${e.code})`,
84
+ level: 'error',
85
+ ts: e.ts,
86
+ });
87
+ }
82
88
  locked = false;
83
89
  break;
84
90
  case 'status':
@@ -1,7 +1,8 @@
1
+ import { VERSION } from '../version';
1
2
  export const manifest = {
2
3
  id: 'shell',
3
4
  label: 'Shell',
4
- version: '0.1.0',
5
+ version: VERSION,
5
6
  views: [{ id: 'shell:terminal', label: 'Shell' }],
6
7
  // serverBundle intentionally omitted — this shard is a framework built-in
7
8
  // and is statically mounted at sh3-server boot. The existing contract in
@@ -62,7 +62,7 @@ function makeShellApi(_ctx) {
62
62
  listViewsInCurrentLayout() {
63
63
  try {
64
64
  const { root } = inspectActiveLayout();
65
- return collectTabEntries(root).map((t) => {
65
+ return collectTabEntries(root.docked).map((t) => {
66
66
  var _a;
67
67
  return ({
68
68
  slotId: t.slotId,
@@ -128,6 +128,7 @@ export const shellShard = {
128
128
  unmount() {
129
129
  unmount(instance);
130
130
  },
131
+ closable: true,
131
132
  };
132
133
  },
133
134
  };
@@ -3,6 +3,8 @@ import type { ZoneSchema } from './state/types';
3
3
  import { type ModalManager } from './overlays/modal';
4
4
  import { type PopupManager } from './overlays/popup';
5
5
  import { type ToastManager } from './overlays/toast';
6
+ import { type FloatManager } from './overlays/float';
7
+ import { type PresetManager } from './overlays/presets';
6
8
  /**
7
9
  * The process-wide shell singleton exposed to shards and the shell's own
8
10
  * internal code. Provides state zone creation and overlay managers.
@@ -22,6 +24,10 @@ export interface Shell {
22
24
  popup: PopupManager;
23
25
  /** Auto-dismissing notification toasts. */
24
26
  toast: ToastManager;
27
+ /** Detached floating panels on overlay layer 1. See overlays/float.ts. */
28
+ float: FloatManager;
29
+ /** Named layout presets per app. See overlays/presets.ts. */
30
+ presets: PresetManager;
25
31
  }
26
32
  /** The process-wide shell instance. Framework-internal code uses this directly; shards receive a scoped view via `ShardContext`. */
27
33
  export declare const shell: Shell;
@@ -18,10 +18,14 @@ import { createStateZones } from './state/zones.svelte';
18
18
  import { modalManager } from './overlays/modal';
19
19
  import { popupManager } from './overlays/popup';
20
20
  import { toastManager } from './overlays/toast';
21
+ import { floatManager } from './overlays/float';
22
+ import { presetManager } from './overlays/presets';
21
23
  /** The process-wide shell instance. Framework-internal code uses this directly; shards receive a scoped view via `ShardContext`. */
22
24
  export const shell = {
23
25
  state: createStateZones,
24
26
  modal: modalManager,
25
27
  popup: popupManager,
26
28
  toast: toastManager,
29
+ float: floatManager,
30
+ presets: presetManager,
27
31
  };
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.6.0";
2
+ export declare const VERSION = "0.7.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.6.0';
2
+ export const VERSION = '0.7.0';