sh3-core 0.22.5 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Sh3.svelte +4 -4
- package/dist/actions/listActive.js +1 -0
- package/dist/actions/listActive.test.js +13 -0
- package/dist/actions/types.d.ts +12 -0
- package/dist/api.d.ts +3 -1
- package/dist/api.js +3 -1
- package/dist/app/admin/adminApp.js +2 -0
- package/dist/app/admin/adminShard.svelte.js +1 -0
- package/dist/app/store/StoreView.svelte +1 -1
- package/dist/app/store/storeApp.js +3 -1
- package/dist/app/store/storeShard.svelte.js +1 -0
- package/dist/app-appearance/appearanceShard.svelte.js +1 -0
- package/dist/apps/lifecycle.js +22 -10
- package/dist/apps/lifecycle.test.js +53 -1
- package/dist/apps/types.d.ts +9 -0
- package/dist/chrome/CompactChrome.svelte +11 -7
- package/dist/chrome/MenuSheet.svelte +19 -6
- package/dist/contributions/contextSource.d.ts +48 -0
- package/dist/contributions/contextSource.js +21 -0
- package/dist/createShell.js +40 -0
- package/dist/documents/picker-api.test.js +40 -0
- package/dist/documents/picker-primitive.d.ts +37 -8
- package/dist/documents/picker-primitive.js +5 -13
- package/dist/host.js +30 -7
- package/dist/layout/slotHostPool.svelte.d.ts +11 -0
- package/dist/layout/slotHostPool.svelte.js +41 -17
- package/dist/layout/slotHostPool.test.js +45 -1
- package/dist/layouts-shard/layoutsShard.svelte.js +1 -0
- package/dist/overlays/OverlayRoots.svelte +15 -4
- package/dist/overlays/__test__/OverlayBindHarness.svelte +20 -0
- package/dist/overlays/__test__/OverlayBindHarness.svelte.d.ts +3 -0
- package/dist/overlays/float-compact-bind.svelte.test.d.ts +1 -0
- package/dist/overlays/float-compact-bind.svelte.test.js +51 -0
- package/dist/overlays/modal.js +3 -0
- package/dist/overlays/modal.test.js +45 -0
- package/dist/overlays/types.d.ts +9 -0
- package/dist/primitives/widgets/DocumentFilePicker.svelte +9 -7
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +44 -27
- package/dist/primitives/widgets/ShardPicker.svelte +38 -0
- package/dist/primitives/widgets/ShardPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/_DocumentBrowser.svelte +15 -7
- package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +2 -0
- package/dist/projects/scope-gate.d.ts +4 -0
- package/dist/projects/scope-gate.js +51 -0
- package/dist/projects/scope-gate.test.d.ts +1 -0
- package/dist/projects/scope-gate.test.js +92 -0
- package/dist/projects-shard/ProjectManage.svelte +42 -2
- package/dist/projects-shard/ProjectManage.svelte.test.js +10 -9
- package/dist/projects-shard/projectsApi.d.ts +3 -2
- package/dist/projects-shard/projectsApi.test.js +1 -1
- package/dist/projects-shard/projectsShard.svelte.js +1 -0
- package/dist/runtime/runVerb.d.ts +9 -0
- package/dist/runtime/runVerb.js +4 -4
- package/dist/runtime/runVerb.test.js +29 -0
- package/dist/sh3Api/headless.d.ts +7 -0
- package/dist/sh3Api/headless.js +3 -1
- package/dist/sh3Api/headless.svelte.test.js +42 -0
- package/dist/sh3core-shard/Sh3Home.svelte +3 -4
- package/dist/sh3core-shard/sh3coreShard.svelte.js +1 -0
- package/dist/shards/lifecycle.svelte.d.ts +8 -2
- package/dist/shards/lifecycle.svelte.js +65 -7
- package/dist/shards/lifecycle.test.js +110 -1
- package/dist/shards/types.d.ts +13 -0
- package/dist/shell-shard/Terminal.svelte +1 -4
- package/dist/shell-shard/Terminal.svelte.d.ts +0 -2
- package/dist/shell-shard/dispatch.d.ts +0 -2
- package/dist/shell-shard/dispatch.js +0 -2
- package/dist/shell-shard/display-cwd.test.js +4 -4
- package/dist/shell-shard/manifest.js +1 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +1 -1
- package/dist/shell-shard/shellShard.svelte.js +9 -4
- package/dist/shell-shard/verbs/cat.js +3 -3
- package/dist/shell-shard/verbs/cat.test.js +1 -2
- package/dist/shell-shard/verbs/ls.js +2 -2
- package/dist/shell-shard/verbs/ls.test.js +1 -2
- package/dist/shell-shard/verbs/mkdir.js +3 -3
- package/dist/shell-shard/verbs/mkdir.test.js +1 -2
- package/dist/shell-shard/verbs/mv.js +3 -3
- package/dist/shell-shard/verbs/mv.test.js +1 -2
- package/dist/shell-shard/verbs/rm.js +3 -3
- package/dist/shell-shard/verbs/rm.test.js +1 -2
- package/dist/shell-shard/verbs/xfer.js +5 -5
- package/dist/shell-shard/verbs/xfer.test.js +2 -2
- package/dist/transport/apiFetch.js +21 -3
- package/dist/transport/apiFetch.test.js +63 -0
- package/dist/verbs/types.d.ts +10 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/Sh3.svelte
CHANGED
|
@@ -79,19 +79,19 @@
|
|
|
79
79
|
|
|
80
80
|
const edgePointers = new Set<number>();
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
83
83
|
const rect = el.getBoundingClientRect();
|
|
84
84
|
const local = e.clientX - rect.left;
|
|
85
85
|
if (local >= EDGE_PX && local <= rect.width - EDGE_PX) return;
|
|
86
86
|
const granted = claim(e.pointerId, { ownerId: 'sh3:edge', axis: 'x', priority: 'edge', depth: 0 });
|
|
87
87
|
if (granted) edgePointers.add(e.pointerId);
|
|
88
|
-
}
|
|
88
|
+
};
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
const onPointerEnd = (e: PointerEvent): void => {
|
|
91
91
|
if (!edgePointers.has(e.pointerId)) return;
|
|
92
92
|
revoke(e.pointerId, 'sh3:edge');
|
|
93
93
|
edgePointers.delete(e.pointerId);
|
|
94
|
-
}
|
|
94
|
+
};
|
|
95
95
|
|
|
96
96
|
el.addEventListener('pointerdown', onPointerDown);
|
|
97
97
|
el.addEventListener('pointerup', onPointerEnd);
|
|
@@ -50,6 +50,7 @@ export function listActionsFromEntries(entries, state) {
|
|
|
50
50
|
ownerShardId: entry.ownerShardId,
|
|
51
51
|
paletteItem: entry.action.paletteItem !== false,
|
|
52
52
|
contextItem: entry.action.contextItem !== false,
|
|
53
|
+
aiInvocable: entry.action.aiInvocable,
|
|
53
54
|
submenu: entry.action.submenu,
|
|
54
55
|
submenuOf: entry.action.submenuOf,
|
|
55
56
|
active,
|
|
@@ -57,6 +57,19 @@ describe('listActiveFromEntries', () => {
|
|
|
57
57
|
expect(out[0].paletteItem).toBe(false);
|
|
58
58
|
expect(out[0].contextItem).toBe(true); // defaults to true
|
|
59
59
|
});
|
|
60
|
+
it('propagates aiInvocable from the registered action, preserving undefined', () => {
|
|
61
|
+
const entries = [
|
|
62
|
+
mkEntry({ id: 'opt-out', scope: 'home', aiInvocable: false }),
|
|
63
|
+
mkEntry({ id: 'opt-in', scope: 'home', aiInvocable: true }),
|
|
64
|
+
mkEntry({ id: 'unset', scope: 'home' }),
|
|
65
|
+
];
|
|
66
|
+
const out = listActiveFromEntries(entries, mkState());
|
|
67
|
+
const byId = Object.fromEntries(out.map((d) => [d.id, d]));
|
|
68
|
+
expect(byId['opt-out'].aiInvocable).toBe(false);
|
|
69
|
+
expect(byId['opt-in'].aiInvocable).toBe(true);
|
|
70
|
+
// `undefined` is significant — consumers filter `=== false`, not falsy.
|
|
71
|
+
expect(byId['unset'].aiInvocable).toBeUndefined();
|
|
72
|
+
});
|
|
60
73
|
it('dedupes by action id', () => {
|
|
61
74
|
const entries = [
|
|
62
75
|
mkEntry({ id: 'dup', scope: 'home' }, 'shard.a'),
|
package/dist/actions/types.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface Action {
|
|
|
14
14
|
scope: ActionScope;
|
|
15
15
|
contextItem?: boolean;
|
|
16
16
|
paletteItem?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Opt-out flag for AI tool catalogs. Set `false` to hide this action
|
|
19
|
+
* from LLM-facing surfaces (e.g. `sh3-ai`'s action→tool adapter) — use
|
|
20
|
+
* for palette-only actions that need a UI picker and are meaningless
|
|
21
|
+
* to invoke programmatically. Defaults to `undefined` (catalog
|
|
22
|
+
* inclusion decided by the consumer).
|
|
23
|
+
*/
|
|
24
|
+
aiInvocable?: boolean;
|
|
17
25
|
/**
|
|
18
26
|
* Optional menu container id. When set and the active app's declared
|
|
19
27
|
* (or canonical fallback) menu list contains this id, the action
|
|
@@ -148,6 +156,8 @@ export interface ActiveActionDescriptor {
|
|
|
148
156
|
ownerShardId: string;
|
|
149
157
|
paletteItem: boolean;
|
|
150
158
|
contextItem: boolean;
|
|
159
|
+
/** Carried through from the registered action; see `Action.aiInvocable`. */
|
|
160
|
+
aiInvocable?: boolean;
|
|
151
161
|
/** True when this action is a submenu parent (children opened by drill). */
|
|
152
162
|
submenu?: true;
|
|
153
163
|
/** Parent action id when this action is a submenu child. */
|
|
@@ -187,6 +197,8 @@ export interface ActionDescriptor {
|
|
|
187
197
|
ownerShardId: string;
|
|
188
198
|
paletteItem: boolean;
|
|
189
199
|
contextItem: boolean;
|
|
200
|
+
/** Carried through from the registered action; see `Action.aiInvocable`. */
|
|
201
|
+
aiInvocable?: boolean;
|
|
190
202
|
/** True when this action is a submenu parent (children opened by drill). */
|
|
191
203
|
submenu?: true;
|
|
192
204
|
/** Parent action id when this action is a submenu child. */
|
package/dist/api.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type { ConflictItem, ConflictBranch as ConflictManagerBranch, ResolveOpti
|
|
|
38
38
|
export { CONFLICT_RENDERER_POINT, ConflictPermissionError, ConflictSessionOrphanedError, } from './conflicts/api';
|
|
39
39
|
export type { ColorPickOptions, ColorContribution, ColorApi, } from './color/api';
|
|
40
40
|
export { COLOR_PICKER_POINT } from './color/api';
|
|
41
|
-
export { registeredShards, activeShards, erroredShards } from './shards/lifecycle.svelte';
|
|
41
|
+
export { registeredShards, activeShards, erroredShards, listRegisteredShards } from './shards/lifecycle.svelte';
|
|
42
42
|
export type { ShardErrorEntry } from './shards/lifecycle.svelte';
|
|
43
43
|
export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, RemoteInstallRequest, } from './registry/types';
|
|
44
44
|
export type { ResolvedPackage } from './registry/client';
|
|
@@ -65,6 +65,8 @@ export type { RunVerbOpts, RunVerbResult } from './runtime';
|
|
|
65
65
|
export { registerShellMode } from './shell-shard/registerShellMode';
|
|
66
66
|
export type { ShellModeDescriptor, ShellModeOutput, ShellModeDispatchHandler, ShellModeDispatchInput, ShellModeRunsOn, RichEntryHandle, StreamHandle, } from './shell-shard/contract';
|
|
67
67
|
export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
|
|
68
|
+
export { CONTEXT_SOURCE_POINT_ID } from './contributions/contextSource';
|
|
69
|
+
export type { ContextSource } from './contributions/contextSource';
|
|
68
70
|
export type { GestureRegistry, GestureHandle } from './gestures';
|
|
69
71
|
export type { GestureType, Axis, ClaimPriority, ClaimEntry, PanEvent, ScrollEvent, ButtonEvent, PanOptions, DragOptions, ButtonOptions, ScrollOptions, } from './gestures/types';
|
|
70
72
|
export { VERSION } from './version';
|
package/dist/api.js
CHANGED
|
@@ -42,7 +42,7 @@ export { COLOR_PICKER_POINT } from './color/api';
|
|
|
42
42
|
// and tooling shards that need to visualize framework state. Phase 9
|
|
43
43
|
// addition: diagnostic used to reach `activate.svelte` directly via $lib;
|
|
44
44
|
// the package boundary requires routing through the public surface.
|
|
45
|
-
export { registeredShards, activeShards, erroredShards } from './shards/lifecycle.svelte';
|
|
45
|
+
export { registeredShards, activeShards, erroredShards, listRegisteredShards } from './shards/lifecycle.svelte';
|
|
46
46
|
export { fetchRegistries, fetchArchive, buildPackageMeta } from './registry/client';
|
|
47
47
|
export { validateRegistryIndex } from './registry/schema';
|
|
48
48
|
// Key mint/revoke types — client shards that declare `keys:mint` get ctx.keys.
|
|
@@ -65,6 +65,8 @@ export { runVerbProgrammatic } from './runtime';
|
|
|
65
65
|
// Sh3 mode contributions (external shards extend the sh3 with new modes).
|
|
66
66
|
export { registerShellMode } from './shell-shard/registerShellMode';
|
|
67
67
|
export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
|
|
68
|
+
// Context-source contributions (publishers register entries; consumers like sh3-ai pick them up).
|
|
69
|
+
export { CONTEXT_SOURCE_POINT_ID } from './contributions/contextSource';
|
|
68
70
|
// Package version.
|
|
69
71
|
export { VERSION } from './version';
|
|
70
72
|
// Framework shard IDs — shards that are always present (built-in to sh3-core).
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { storeContext } from './storeShard.svelte';
|
|
10
10
|
import { fetchArchive, buildPackageMeta } from '../../registry/client';
|
|
11
|
-
import { readFileFromArchive
|
|
11
|
+
import { readFileFromArchive } from '../../registry/archive';
|
|
12
12
|
import { installPackage } from '../../registry/installer';
|
|
13
13
|
import { loadBundleModule, type LoadedBundle } from '../../registry/loader';
|
|
14
14
|
import { extractBundlePermissions } from '../../registry/permission-descriptions';
|
|
@@ -10,7 +10,9 @@ import { VERSION } from '../../version';
|
|
|
10
10
|
export const storeApp = {
|
|
11
11
|
manifest: {
|
|
12
12
|
id: 'sh3-store-app',
|
|
13
|
-
label: 'Package
|
|
13
|
+
label: 'Package Manager',
|
|
14
|
+
icon: 'file-archive',
|
|
15
|
+
color: '#D16F19',
|
|
14
16
|
version: VERSION,
|
|
15
17
|
requiredShards: ['sh3-store'],
|
|
16
18
|
layoutVersion: 1,
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* `{ id: string | null }`. Writing happens on launch (id) and on
|
|
12
12
|
* return-to-home (null). Boot reads it to decide whether to auto-launch.
|
|
13
13
|
*/
|
|
14
|
+
import { flushSync } from 'svelte';
|
|
14
15
|
import { createStateZones } from '../state/zones.svelte';
|
|
15
16
|
import { createGestureRegistry } from '../gestures';
|
|
16
17
|
import { registeredShards, } from '../shards/lifecycle.svelte';
|
|
17
18
|
import { shardEntries, runAppActivate, runAppDeactivate, registerAllShards, erroredShards } from '../shards/lifecycle.svelte';
|
|
18
19
|
import { attachApp, acquireAppSlotHolds, detachApp, switchToApp, switchToHome, getActiveAppTree, } from '../layout/store.svelte';
|
|
20
|
+
import { flushPendingDestroys } from '../layout/slotHostPool.svelte';
|
|
19
21
|
import { activeApp, breadcrumbApp, getRegisteredApp, registeredApps } from './registry.svelte';
|
|
20
22
|
import { createZoneManager } from '../state/manage';
|
|
21
23
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
@@ -227,19 +229,29 @@ export function unloadApp(id, skipSwitchToHome = false) {
|
|
|
227
229
|
if (!app)
|
|
228
230
|
return;
|
|
229
231
|
void ((_a = app.deactivate) === null || _a === void 0 ? void 0 : _a.call(app));
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
232
|
+
// Teardown order is load-bearing. Views must be fully unmounted before
|
|
233
|
+
// any shard's onAppDeactivate runs, so shards can safely close stores /
|
|
234
|
+
// nullify reactive state in onAppDeactivate without tripping leaked
|
|
235
|
+
// $derived references in still-mounted views (the leaked-pool bug).
|
|
236
|
+
//
|
|
237
|
+
// 1) switchToHome flips the rendered root so LayoutRenderer stops
|
|
238
|
+
// reading the app's tree. SlotContainer cleanups are scheduled.
|
|
239
|
+
// 2) flushSync forces Svelte to run those cleanups now (drops the
|
|
240
|
+
// renderer's refcount on every app slot).
|
|
241
|
+
// 3) detachApp releases the per-app refcount holds. Pool entries for
|
|
242
|
+
// the app's slots are now at refcount 0 and queued in pendingDestroy.
|
|
243
|
+
// 4) flushPendingDestroys synchronously runs the destroy body for
|
|
244
|
+
// those entries — the Svelte components for the app's views are
|
|
245
|
+
// unmounted now, not on a deferred microtask.
|
|
246
|
+
// 5) Only now do we call runAppDeactivate on each required shard.
|
|
240
247
|
if (!skipSwitchToHome)
|
|
241
248
|
switchToHome();
|
|
249
|
+
flushSync();
|
|
242
250
|
detachApp();
|
|
251
|
+
flushPendingDestroys();
|
|
252
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
253
|
+
void runAppDeactivate(shardId, id);
|
|
254
|
+
}
|
|
243
255
|
activeApp.id = null;
|
|
244
256
|
setActiveApp(null, new Set());
|
|
245
257
|
clearSelectionUnconditional();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { resetFramework } from '../__test__/reset';
|
|
3
3
|
import { makeApp, makeShard, makeAppManifest, makeShardManifest, makeTabsNode, makeTabEntry, makeSlotNode, makeTree, } from '../__test__/fixtures';
|
|
4
|
-
import { launchApp, returnToHome, unregisterApp } from './lifecycle';
|
|
4
|
+
import { launchApp, returnToHome, unloadApp, unregisterApp } from './lifecycle';
|
|
5
5
|
import { registerApp } from './registry.svelte';
|
|
6
6
|
import { registerShard } from '../shards/lifecycle.svelte';
|
|
7
7
|
import { presetManager } from '../overlays/presets';
|
|
@@ -703,6 +703,58 @@ describe('unloadApp — onAppDeactivate hook', () => {
|
|
|
703
703
|
expect(deactivated).toEqual(['deact-app']);
|
|
704
704
|
});
|
|
705
705
|
});
|
|
706
|
+
describe('unloadApp — view unmount happens before onAppDeactivate', () => {
|
|
707
|
+
beforeEach(resetFramework);
|
|
708
|
+
it('unmounts all of the app\'s pooled views before any shard\'s onAppDeactivate fires', async () => {
|
|
709
|
+
const order = [];
|
|
710
|
+
const shard = makeShard({
|
|
711
|
+
manifest: makeShardManifest({ id: 'order-shard' }),
|
|
712
|
+
register() { },
|
|
713
|
+
onAppDeactivate() { order.push('onAppDeactivate'); },
|
|
714
|
+
});
|
|
715
|
+
registerShard(shard);
|
|
716
|
+
// Register two views so the assertion covers an active slot AND an
|
|
717
|
+
// inactive-but-held slot. Both must unmount before deactivate.
|
|
718
|
+
registerView('order:view-a', {
|
|
719
|
+
mount: () => ({ unmount: () => order.push('view-a:unmount') }),
|
|
720
|
+
});
|
|
721
|
+
registerView('order:view-b', {
|
|
722
|
+
mount: () => ({ unmount: () => order.push('view-b:unmount') }),
|
|
723
|
+
});
|
|
724
|
+
const app = makeApp({
|
|
725
|
+
manifest: makeAppManifest({ id: 'order-app', requiredShards: ['order-shard'] }),
|
|
726
|
+
initialLayout: [
|
|
727
|
+
{
|
|
728
|
+
name: 'default',
|
|
729
|
+
tree: makeTree({
|
|
730
|
+
type: 'split',
|
|
731
|
+
direction: 'horizontal',
|
|
732
|
+
children: [
|
|
733
|
+
makeSlotNode('a-slot', 'order:view-a'),
|
|
734
|
+
makeSlotNode('b-slot', 'order:view-b'),
|
|
735
|
+
],
|
|
736
|
+
sizes: [0.5, 0.5],
|
|
737
|
+
}),
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
});
|
|
741
|
+
registerApp(app);
|
|
742
|
+
await launchApp('order-app');
|
|
743
|
+
// Let the mount microtasks settle so factories have run.
|
|
744
|
+
await Promise.resolve();
|
|
745
|
+
await Promise.resolve();
|
|
746
|
+
unloadApp('order-app');
|
|
747
|
+
// Both views must have unmounted BEFORE onAppDeactivate ran.
|
|
748
|
+
const deactivateIdx = order.indexOf('onAppDeactivate');
|
|
749
|
+
const unmountAIdx = order.indexOf('view-a:unmount');
|
|
750
|
+
const unmountBIdx = order.indexOf('view-b:unmount');
|
|
751
|
+
expect(deactivateIdx).toBeGreaterThanOrEqual(0);
|
|
752
|
+
expect(unmountAIdx).toBeGreaterThanOrEqual(0);
|
|
753
|
+
expect(unmountBIdx).toBeGreaterThanOrEqual(0);
|
|
754
|
+
expect(unmountAIdx).toBeLessThan(deactivateIdx);
|
|
755
|
+
expect(unmountBIdx).toBeLessThan(deactivateIdx);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
706
758
|
describe('launchApp — onLayoutWillRestore / onLayoutRestored hooks', () => {
|
|
707
759
|
beforeEach(resetFramework);
|
|
708
760
|
it('calls onLayoutWillRestore before slot acquisition and onLayoutRestored after switchToApp', async () => {
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -46,6 +46,15 @@ export interface AppManifest {
|
|
|
46
46
|
* starting shards (diagnostic, __sh3core__) stay running.
|
|
47
47
|
*/
|
|
48
48
|
requiredShards: string[];
|
|
49
|
+
/**
|
|
50
|
+
* Optional list of shard ids that ship in the same bundle as this app
|
|
51
|
+
* (combo packages). Mirrors the server-side `project-allowlist`
|
|
52
|
+
* middleware's resolution rule — these are part of the project-scope
|
|
53
|
+
* closure when the app is allowlisted, but they are NOT activated
|
|
54
|
+
* automatically (use `requiredShards` for that). Defaults to empty
|
|
55
|
+
* when omitted.
|
|
56
|
+
*/
|
|
57
|
+
bundledShards?: string[];
|
|
49
58
|
/**
|
|
50
59
|
* Bump to invalidate persisted layouts. On launch, a persisted blob
|
|
51
60
|
* whose version doesn't match this is discarded and `initialLayout`
|
|
@@ -89,16 +89,20 @@
|
|
|
89
89
|
function toggleFloatsSheet() {
|
|
90
90
|
if (floatsOpen) return;
|
|
91
91
|
floatsOpen = true;
|
|
92
|
-
|
|
92
|
+
sh3.modal.open(
|
|
93
93
|
FloatsSheet,
|
|
94
94
|
{},
|
|
95
|
-
{
|
|
95
|
+
{
|
|
96
|
+
dismissOnBackdrop: true,
|
|
97
|
+
boxStyle: 'max-width: 320px;',
|
|
98
|
+
// Reset on every dismissal path. Wrapping handle.close after
|
|
99
|
+
// open() returns doesn't work — ModalFrame captured the close
|
|
100
|
+
// reference at mount and bypasses any later monkey-patch.
|
|
101
|
+
onClose: () => {
|
|
102
|
+
floatsOpen = false;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
96
105
|
);
|
|
97
|
-
const origClose = handle.close;
|
|
98
|
-
handle.close = () => {
|
|
99
|
-
origClose();
|
|
100
|
-
floatsOpen = false;
|
|
101
|
-
};
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
function toggleDrawer(anchor: DrawerAnchor) {
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
resolveMenuContainers,
|
|
16
16
|
resolveMenuItems,
|
|
17
17
|
resolveSubmenuItems,
|
|
18
|
-
type MenuBarItem,
|
|
19
18
|
} from '../actions/menuBarModel';
|
|
20
19
|
import { listActions } from '../actions/registry';
|
|
21
20
|
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
@@ -54,7 +53,16 @@
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
// --- derived items for current nav level ---------------------------
|
|
57
|
-
|
|
56
|
+
interface SheetItem {
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
isContainer: boolean;
|
|
60
|
+
isSubmenu: boolean;
|
|
61
|
+
shortcut: string | null;
|
|
62
|
+
disabled: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentItems = $derived.by<SheetItem[]>(() => {
|
|
58
66
|
const entries = listActions();
|
|
59
67
|
const nav = currentNav;
|
|
60
68
|
|
|
@@ -65,7 +73,10 @@
|
|
|
65
73
|
.map((c) => ({
|
|
66
74
|
id: c.id,
|
|
67
75
|
label: c.label,
|
|
68
|
-
isContainer: true
|
|
76
|
+
isContainer: true,
|
|
77
|
+
isSubmenu: false,
|
|
78
|
+
shortcut: null,
|
|
79
|
+
disabled: false,
|
|
69
80
|
}));
|
|
70
81
|
}
|
|
71
82
|
|
|
@@ -74,8 +85,9 @@
|
|
|
74
85
|
return items.map((item) => ({
|
|
75
86
|
id: item.id,
|
|
76
87
|
label: item.label,
|
|
77
|
-
|
|
88
|
+
isContainer: false,
|
|
78
89
|
isSubmenu: item.submenu === true,
|
|
90
|
+
shortcut: item.shortcut,
|
|
79
91
|
disabled: item.disabled,
|
|
80
92
|
}));
|
|
81
93
|
}
|
|
@@ -85,14 +97,15 @@
|
|
|
85
97
|
return items.map((item) => ({
|
|
86
98
|
id: item.id,
|
|
87
99
|
label: item.label,
|
|
100
|
+
isContainer: false,
|
|
101
|
+
isSubmenu: item.submenu === true,
|
|
88
102
|
shortcut: item.shortcut,
|
|
89
103
|
disabled: item.disabled,
|
|
90
|
-
isSubmenu: item.submenu === true,
|
|
91
104
|
}));
|
|
92
105
|
});
|
|
93
106
|
|
|
94
107
|
// --- actions --------------------------------------------------------
|
|
95
|
-
function handleTap(entry:
|
|
108
|
+
function handleTap(entry: SheetItem) {
|
|
96
109
|
if (entry.isContainer) {
|
|
97
110
|
const c = containers.find((x) => x.id === entry.id);
|
|
98
111
|
if (c) push({ kind: 'container', containerId: c.id, label: c.label });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution point id: shards register `ContextSource` descriptors here.
|
|
3
|
+
* Each registration adds one pickable entry in any consuming UI (e.g. the
|
|
4
|
+
* "SOURCES" section of the AI Edit modal when sh3-ai is installed) and may
|
|
5
|
+
* be picked up by future consumers. Lifecycle is publisher-owned — register
|
|
6
|
+
* when content becomes relevant (app activation, project load, selection),
|
|
7
|
+
* dispose when it stops being relevant.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONTEXT_SOURCE_POINT_ID = "sh3.contextSource";
|
|
10
|
+
/** A single context-source contribution. */
|
|
11
|
+
export interface ContextSource {
|
|
12
|
+
/**
|
|
13
|
+
* Globally unique. Convention: `<shardId>:<slug>`. Used as the picker
|
|
14
|
+
* selection key, so it must be stable across re-renders. Re-registering
|
|
15
|
+
* with an existing id silently replaces — generally dispose the prior
|
|
16
|
+
* registration first when swapping content.
|
|
17
|
+
*/
|
|
18
|
+
id: string;
|
|
19
|
+
/** Short display name shown in the picker row and the chip body. */
|
|
20
|
+
label: string;
|
|
21
|
+
/**
|
|
22
|
+
* Tooltip in any consuming UI. Consumers may also surface this to
|
|
23
|
+
* downstream tools (e.g. as the description sh3-ai exposes when
|
|
24
|
+
* chat-side context tools land).
|
|
25
|
+
*/
|
|
26
|
+
description?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Drives prompt formatting (when consumed by sh3-ai) and the chip kind tag.
|
|
29
|
+
* - `text` (default): value coerced to string, dumped raw.
|
|
30
|
+
* - `markdown`: value coerced to string, wrapped in fenced ```markdown``` block.
|
|
31
|
+
* - `json`: value `JSON.stringify`-ed with 2-space indent, wrapped in fenced ```json``` block.
|
|
32
|
+
*/
|
|
33
|
+
kind?: 'text' | 'markdown' | 'json';
|
|
34
|
+
/**
|
|
35
|
+
* Sub-header under the picker's SOURCES section (e.g. the consuming
|
|
36
|
+
* shard's display name). Entries without a group fall under an "Other"
|
|
37
|
+
* sub-header.
|
|
38
|
+
*/
|
|
39
|
+
group?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Lazy fetcher. Called when the user picks the chip (for the expand
|
|
42
|
+
* preview pane) and again at consume time. May be sync or async.
|
|
43
|
+
* Returning null/undefined signals "no content available right now" —
|
|
44
|
+
* entry is silently omitted but the chip remains. Throwing/rejecting
|
|
45
|
+
* surfaces a toast and skips the entry.
|
|
46
|
+
*/
|
|
47
|
+
get(): unknown | Promise<unknown>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public contract for context-source contributions. Shards register
|
|
3
|
+
* `ContextSource` descriptors at `CONTEXT_SOURCE_POINT_ID` via the
|
|
4
|
+
* standard `ctx.contributions.register` API; consumers (sh3-ai today,
|
|
5
|
+
* potentially inspectors / hover previews / chat-side context tools
|
|
6
|
+
* tomorrow) enumerate them via `ctx.contributions.list`.
|
|
7
|
+
*
|
|
8
|
+
* v1 has a single consumer (sh3-ai). The descriptor shape is hosted
|
|
9
|
+
* here so publisher shards do not need a devDependency on sh3-ai to
|
|
10
|
+
* contribute. Lifecycle is consumer-owned — see the JSDoc on
|
|
11
|
+
* `CONTEXT_SOURCE_POINT_ID` below.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Contribution point id: shards register `ContextSource` descriptors here.
|
|
15
|
+
* Each registration adds one pickable entry in any consuming UI (e.g. the
|
|
16
|
+
* "SOURCES" section of the AI Edit modal when sh3-ai is installed) and may
|
|
17
|
+
* be picked up by future consumers. Lifecycle is publisher-owned — register
|
|
18
|
+
* when content becomes relevant (app activation, project load, selection),
|
|
19
|
+
* dispose when it stops being relevant.
|
|
20
|
+
*/
|
|
21
|
+
export const CONTEXT_SOURCE_POINT_ID = 'sh3.contextSource';
|
package/dist/createShell.js
CHANGED
|
@@ -23,6 +23,9 @@ import { attachGlobalListeners } from './actions/listeners';
|
|
|
23
23
|
import { detectSatelliteMode } from './boot/satelliteMode';
|
|
24
24
|
import { MemoryBackend } from './state/backends';
|
|
25
25
|
import { sessionState, readPendingScope, PENDING_SCOPE_KEY } from './projects/session-state.svelte';
|
|
26
|
+
import { projectsApi } from './projects-shard/projectsApi';
|
|
27
|
+
import { projectsState } from './projects-shard/projectsShard.svelte';
|
|
28
|
+
import { toastManager } from './overlays/toast';
|
|
26
29
|
import SatelliteShell from './satellite/SatelliteShell.svelte';
|
|
27
30
|
export async function createShell(config) {
|
|
28
31
|
var _a, _b;
|
|
@@ -87,6 +90,19 @@ export async function createShell(config) {
|
|
|
87
90
|
if (satellite.payload.projectId) {
|
|
88
91
|
sessionState.activeProjectId = satellite.payload.projectId;
|
|
89
92
|
}
|
|
93
|
+
// Mirror main-mode: eager-fetch the active project so the register
|
|
94
|
+
// gate computed in bootstrapSatellite has the allowlist. Fail-closed
|
|
95
|
+
// identically — drop to personal scope on error.
|
|
96
|
+
if (sessionState.activeProjectId !== null) {
|
|
97
|
+
try {
|
|
98
|
+
const record = await projectsApi.get(sessionState.activeProjectId);
|
|
99
|
+
projectsState.projects = [record];
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn(`[sh3] Satellite: failed to load project "${sessionState.activeProjectId}"; dropping to personal scope:`, err instanceof Error ? err.message : err);
|
|
103
|
+
sessionState.activeProjectId = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
__setScopeResolver(() => sessionState.activeProjectId);
|
|
91
107
|
__setShardScopeResolver(() => sessionState.activeProjectId ? 'project' : 'tenant');
|
|
92
108
|
if (config === null || config === void 0 ? void 0 : config.shards)
|
|
@@ -154,6 +170,30 @@ export async function createShell(config) {
|
|
|
154
170
|
}
|
|
155
171
|
}
|
|
156
172
|
}
|
|
173
|
+
// 4b. Eager-fetch the active project record so the register gate
|
|
174
|
+
// (computed in bootstrap()) has the appAllowlist available.
|
|
175
|
+
// Fail-closed: a failed fetch drops us to personal scope rather
|
|
176
|
+
// than booting with no gate (which would leak shards into the
|
|
177
|
+
// project). The user sees a toast on next paint.
|
|
178
|
+
if (sessionState.activeProjectId !== null) {
|
|
179
|
+
try {
|
|
180
|
+
const record = await projectsApi.get(sessionState.activeProjectId);
|
|
181
|
+
projectsState.projects = [record];
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.warn(`[sh3] Failed to load project "${sessionState.activeProjectId}"; dropping to personal scope:`, err instanceof Error ? err.message : err);
|
|
185
|
+
sessionState.activeProjectId = null;
|
|
186
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
187
|
+
sessionStorage.removeItem(PENDING_SCOPE_KEY);
|
|
188
|
+
}
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
try {
|
|
191
|
+
toastManager.notify('Could not load that project; dropped to personal scope.', { level: 'error', duration: 6000 });
|
|
192
|
+
}
|
|
193
|
+
catch ( /* overlay not ready; swallow */_a) { /* overlay not ready; swallow */ }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
157
197
|
// 5. Load server-discovered packages
|
|
158
198
|
await loadDiscoveredPackages(config === null || config === void 0 ? void 0 : config.discoveredPackages);
|
|
159
199
|
// 6. Register consumer-provided shards and apps
|
|
@@ -44,6 +44,46 @@ beforeEach(() => {
|
|
|
44
44
|
});
|
|
45
45
|
describe('createDocumentPicker', () => {
|
|
46
46
|
const sampleDoc = { shardId: 'my-shard', path: 'readme.md', kind: 'file' };
|
|
47
|
+
describe('options forwarding to browser modal', () => {
|
|
48
|
+
it('threads listFolders/handle/readOnlyShard/initialShardId into the modal props', async () => {
|
|
49
|
+
const listFn = async () => [];
|
|
50
|
+
const listFolders = vi.fn(async () => ['empty']);
|
|
51
|
+
const handle = {
|
|
52
|
+
mkdir: vi.fn(async () => { }),
|
|
53
|
+
rmdir: vi.fn(async () => { }),
|
|
54
|
+
renameFolder: vi.fn(async () => { }),
|
|
55
|
+
rename: vi.fn(async () => { }),
|
|
56
|
+
delete: vi.fn(async () => { }),
|
|
57
|
+
};
|
|
58
|
+
const readOnlyShard = vi.fn(() => false);
|
|
59
|
+
const picker = createDocumentPicker(listFn, {
|
|
60
|
+
listFolders, handle, readOnlyShard,
|
|
61
|
+
initialShardId: 'svg-designer', lockToShard: true,
|
|
62
|
+
});
|
|
63
|
+
mockModal();
|
|
64
|
+
picker.save();
|
|
65
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
66
|
+
const props = mockModalOpen.mock.calls[0][1];
|
|
67
|
+
expect(props.listFolders).toBe(listFolders);
|
|
68
|
+
expect(props.handle).toBe(handle);
|
|
69
|
+
expect(props.readOnlyShard).toBe(readOnlyShard);
|
|
70
|
+
expect(props.initialShardId).toBe('svg-designer');
|
|
71
|
+
expect(props.lockToShard).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('omits options when none provided (backward-compatible single-arg call)', async () => {
|
|
74
|
+
const listFn = async () => [];
|
|
75
|
+
const picker = createDocumentPicker(listFn);
|
|
76
|
+
mockModal();
|
|
77
|
+
picker.open();
|
|
78
|
+
await vi.waitFor(() => expect(mockModalOpen).toHaveBeenCalledOnce());
|
|
79
|
+
const props = mockModalOpen.mock.calls[0][1];
|
|
80
|
+
expect(props.listFolders).toBeUndefined();
|
|
81
|
+
expect(props.handle).toBeUndefined();
|
|
82
|
+
expect(props.readOnlyShard).toBeUndefined();
|
|
83
|
+
expect(props.initialShardId).toBeUndefined();
|
|
84
|
+
expect(props.lockToShard).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
47
87
|
describe('open() — modal (no anchor)', () => {
|
|
48
88
|
it('resolves with OpenerValue when user commits', async () => {
|
|
49
89
|
const listFn = async () => [{ shardId: 'my-shard', path: 'readme.md', size: 100, lastModified: 0 }];
|
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import type { DocumentPickerApi, DocListFn } from './picker-api';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* and baked in at construction time so callers don't pass their own scope.
|
|
6
|
-
*
|
|
7
|
-
* When an `anchor` element is provided the browser opens as a popup
|
|
8
|
-
* (anchored near the element). Without an anchor it opens as a centered
|
|
9
|
-
* modal (the expected default for file-browser dialogs).
|
|
3
|
+
* Folder mutation surface forwarded to the document browser modal.
|
|
4
|
+
* Mirrors the `handle` prop of `_DocumentBrowser.svelte`.
|
|
10
5
|
*/
|
|
11
|
-
export
|
|
6
|
+
export interface PickerHandleOps {
|
|
7
|
+
mkdir: (shardId: string, path: string) => Promise<void>;
|
|
8
|
+
rmdir: (shardId: string, path: string, opts: {
|
|
9
|
+
recursive: boolean;
|
|
10
|
+
}) => Promise<void>;
|
|
11
|
+
renameFolder: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
12
|
+
rename: (shardId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
13
|
+
delete: (shardId: string, path: string) => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Construction-time options for `createDocumentPicker`. All optional —
|
|
17
|
+
* absent values fall back to a read-only listing of `listFn`.
|
|
18
|
+
*/
|
|
19
|
+
export interface DocumentPickerOptions {
|
|
20
|
+
/** Enumerates immediate child folders of `prefix` in `shardId`. When set,
|
|
21
|
+
* empty folders (with no documents underneath) appear in the browser. */
|
|
22
|
+
listFolders?: (shardId: string, prefix: string) => Promise<string[]>;
|
|
23
|
+
/** Folder/file mutation surface. When set, the browser exposes a toolbar
|
|
24
|
+
* with new-folder / rename / delete actions (gated per shard by
|
|
25
|
+
* `readOnlyShard`). */
|
|
26
|
+
handle?: PickerHandleOps;
|
|
27
|
+
/** Returns true for shards the caller cannot mutate. The browser hides
|
|
28
|
+
* the edit toolbar while navigated into such shards. */
|
|
29
|
+
readOnlyShard?: (shardId: string) => boolean;
|
|
30
|
+
/** When set, the browser opens pre-navigated into this shard's namespace
|
|
31
|
+
* instead of the (often empty) tenant-wide shard list. The "SH3" crumb
|
|
32
|
+
* still lets the user back out — useful in browse mode. */
|
|
33
|
+
initialShardId?: string;
|
|
34
|
+
/** When true, the browser is hard-scoped to `initialShardId`: the "SH3"
|
|
35
|
+
* breadcrumb is hidden and Backspace at the shard root is a no-op.
|
|
36
|
+
* Use this for non-browse shards that can only ever work within their
|
|
37
|
+
* own namespace, so the user can't navigate into a dead-end root. */
|
|
38
|
+
lockToShard?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function createDocumentPicker(listFn: DocListFn, options?: DocumentPickerOptions): DocumentPickerApi;
|