sh3-core 0.7.5 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +12 -2
- package/dist/api.js +13 -1
- package/dist/app/admin/SystemView.svelte +149 -11
- package/dist/app/store/StoreView.svelte +36 -7
- package/dist/app/store/storeShard.svelte.js +9 -3
- package/dist/app/store/verbs.js +8 -2
- package/dist/apps/lifecycle.d.ts +11 -0
- package/dist/apps/lifecycle.js +21 -1
- package/dist/apps/lifecycle.test.js +50 -1
- package/dist/apps/types.d.ts +7 -2
- package/dist/createShell.d.ts +2 -0
- package/dist/createShell.js +9 -7
- package/dist/documents/backends.d.ts +8 -0
- package/dist/documents/backends.js +87 -0
- package/dist/documents/backends.test.d.ts +1 -0
- package/dist/documents/backends.test.js +33 -0
- package/dist/documents/browse.d.ts +12 -0
- package/dist/documents/browse.js +19 -0
- package/dist/documents/browse.test.d.ts +1 -0
- package/dist/documents/browse.test.js +41 -0
- package/dist/documents/handle.js +5 -0
- package/dist/documents/http-backend.d.ts +4 -0
- package/dist/documents/http-backend.js +14 -0
- package/dist/documents/index.d.ts +1 -0
- package/dist/documents/index.js +1 -0
- package/dist/documents/journal-hook.d.ts +6 -0
- package/dist/documents/journal-hook.js +16 -0
- package/dist/documents/sync/activate-integration.test.d.ts +1 -0
- package/dist/documents/sync/activate-integration.test.js +37 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte +99 -0
- package/dist/documents/sync/components/DocumentSyncExplorer.svelte.d.ts +15 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte +70 -0
- package/dist/documents/sync/components/SyncGrantPicker.svelte.d.ts +12 -0
- package/dist/documents/sync/conflicts.d.ts +30 -0
- package/dist/documents/sync/conflicts.js +77 -0
- package/dist/documents/sync/conflicts.test.d.ts +1 -0
- package/dist/documents/sync/conflicts.test.js +71 -0
- package/dist/documents/sync/engine.d.ts +19 -0
- package/dist/documents/sync/engine.js +188 -0
- package/dist/documents/sync/engine.test.d.ts +1 -0
- package/dist/documents/sync/engine.test.js +169 -0
- package/dist/documents/sync/handle.d.ts +11 -0
- package/dist/documents/sync/handle.js +79 -0
- package/dist/documents/sync/handle.test.d.ts +1 -0
- package/dist/documents/sync/handle.test.js +56 -0
- package/dist/documents/sync/hash.d.ts +1 -0
- package/dist/documents/sync/hash.js +13 -0
- package/dist/documents/sync/hash.test.d.ts +1 -0
- package/dist/documents/sync/hash.test.js +20 -0
- package/dist/documents/sync/index.d.ts +5 -0
- package/dist/documents/sync/index.js +10 -0
- package/dist/documents/sync/journal.d.ts +30 -0
- package/dist/documents/sync/journal.js +179 -0
- package/dist/documents/sync/journal.test.d.ts +1 -0
- package/dist/documents/sync/journal.test.js +87 -0
- package/dist/documents/sync/observer.d.ts +3 -0
- package/dist/documents/sync/observer.js +45 -0
- package/dist/documents/sync/registry.d.ts +13 -0
- package/dist/documents/sync/registry.js +73 -0
- package/dist/documents/sync/registry.test.d.ts +1 -0
- package/dist/documents/sync/registry.test.js +53 -0
- package/dist/documents/sync/serialization.d.ts +5 -0
- package/dist/documents/sync/serialization.js +24 -0
- package/dist/documents/sync/serialization.test.d.ts +1 -0
- package/dist/documents/sync/serialization.test.js +26 -0
- package/dist/documents/sync/singleton.d.ts +11 -0
- package/dist/documents/sync/singleton.js +26 -0
- package/dist/documents/sync/tombstones.d.ts +19 -0
- package/dist/documents/sync/tombstones.js +58 -0
- package/dist/documents/sync/tombstones.test.d.ts +1 -0
- package/dist/documents/sync/tombstones.test.js +37 -0
- package/dist/documents/sync/types.d.ts +116 -0
- package/dist/documents/sync/types.js +27 -0
- package/dist/documents/sync/write-hook.test.d.ts +1 -0
- package/dist/documents/sync/write-hook.test.js +36 -0
- package/dist/documents/types.d.ts +18 -0
- package/dist/documents/types.js +6 -1
- package/dist/env/client.d.ts +10 -5
- package/dist/env/client.js +12 -4
- package/dist/layout/inspection.d.ts +17 -0
- package/dist/layout/inspection.js +53 -0
- package/dist/registry/installer.d.ts +10 -7
- package/dist/registry/installer.js +39 -35
- package/dist/registry/register.d.ts +17 -0
- package/dist/registry/register.js +22 -0
- package/dist/registry/register.test.d.ts +1 -0
- package/dist/registry/register.test.js +28 -0
- package/dist/shards/activate-browse.test.d.ts +1 -0
- package/dist/shards/activate-browse.test.js +36 -0
- package/dist/shards/activate-sync-registry.test.d.ts +1 -0
- package/dist/shards/activate-sync-registry.test.js +42 -0
- package/dist/shards/activate-tenantid.test.d.ts +1 -0
- package/dist/shards/activate-tenantid.test.js +21 -0
- package/dist/shards/activate.svelte.d.ts +12 -0
- package/dist/shards/activate.svelte.js +53 -2
- package/dist/shards/types.d.ts +43 -1
- package/dist/shell-shard/Terminal.svelte +140 -33
- package/dist/shell-shard/Terminal.svelte.d.ts +3 -0
- package/dist/shell-shard/auto-relocate.d.ts +12 -0
- package/dist/shell-shard/auto-relocate.js +20 -0
- package/dist/shell-shard/auto-relocate.test.d.ts +1 -0
- package/dist/shell-shard/auto-relocate.test.js +35 -0
- package/dist/shell-shard/dispatch.d.ts +15 -0
- package/dist/shell-shard/dispatch.js +56 -0
- package/dist/shell-shard/manifest.js +1 -1
- package/dist/shell-shard/modes/builtin.d.ts +5 -0
- package/dist/shell-shard/modes/builtin.js +18 -0
- package/dist/shell-shard/modes/prefs.d.ts +5 -0
- package/dist/shell-shard/modes/prefs.js +31 -0
- package/dist/shell-shard/modes/prefs.test.d.ts +1 -0
- package/dist/shell-shard/modes/prefs.test.js +46 -0
- package/dist/shell-shard/modes/registry.d.ts +7 -0
- package/dist/shell-shard/modes/registry.js +27 -0
- package/dist/shell-shard/modes/registry.test.d.ts +1 -0
- package/dist/shell-shard/modes/registry.test.js +35 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/modes/types.js +1 -0
- package/dist/shell-shard/protocol.d.ts +6 -0
- package/dist/shell-shard/shellShard.svelte.js +57 -5
- package/dist/shell-shard/tenant-fs-client.d.ts +24 -0
- package/dist/shell-shard/tenant-fs-client.js +44 -0
- package/dist/shell-shard/tenant-fs-client.test.d.ts +1 -0
- package/dist/shell-shard/tenant-fs-client.test.js +49 -0
- package/dist/shell-shard/terminal-dispatch.test.d.ts +1 -0
- package/dist/shell-shard/terminal-dispatch.test.js +53 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte +62 -0
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte +28 -0
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte.d.ts +7 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +102 -0
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte.d.ts +11 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte +17 -0
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte.d.ts +6 -0
- package/dist/shell-shard/toolbar/slots.d.ts +17 -0
- package/dist/shell-shard/toolbar/slots.js +26 -0
- package/dist/shell-shard/toolbar/slots.test.d.ts +1 -0
- package/dist/shell-shard/toolbar/slots.test.js +28 -0
- package/dist/shell-shard/verbs/cat.d.ts +2 -0
- package/dist/shell-shard/verbs/cat.js +34 -0
- package/dist/shell-shard/verbs/cd.test.d.ts +1 -0
- package/dist/shell-shard/verbs/cd.test.js +56 -0
- package/dist/shell-shard/verbs/env.d.ts +2 -0
- package/dist/shell-shard/verbs/env.js +14 -0
- package/dist/shell-shard/verbs/index.js +9 -2
- package/dist/shell-shard/verbs/ls.d.ts +2 -0
- package/dist/shell-shard/verbs/ls.js +29 -0
- package/dist/shell-shard/verbs/ls.test.d.ts +1 -0
- package/dist/shell-shard/verbs/ls.test.js +49 -0
- package/dist/shell-shard/verbs/session.d.ts +0 -1
- package/dist/shell-shard/verbs/session.js +58 -26
- package/dist/shell-shard/verbs/views.d.ts +2 -0
- package/dist/shell-shard/verbs/views.js +103 -2
- package/dist/verbs/types.d.ts +21 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified bundle registration — stamps loader-assigned metadata onto every
|
|
3
|
+
* shard/app manifest from a loaded bundle, then registers each. Shared by
|
|
4
|
+
* `installer.installPackage`, `installer.loadInstalledPackages`, and
|
|
5
|
+
* `createShell` discoveredPackages so the three boot paths stay in sync.
|
|
6
|
+
*
|
|
7
|
+
* Per ADR-013, external package authors omit `version` from their source
|
|
8
|
+
* manifests; the authoritative value comes from the persisted/server
|
|
9
|
+
* metadata and must be stamped here before any consumer reads the manifest.
|
|
10
|
+
*/
|
|
11
|
+
import { registerShard } from '../shards/activate.svelte';
|
|
12
|
+
import { registerApp } from '../apps/registry.svelte';
|
|
13
|
+
export function registerLoadedBundle(loaded, meta) {
|
|
14
|
+
for (const shard of loaded.shards) {
|
|
15
|
+
shard.manifest = Object.assign(Object.assign({}, shard.manifest), { version: meta.version });
|
|
16
|
+
registerShard(shard);
|
|
17
|
+
}
|
|
18
|
+
for (const app of loaded.apps) {
|
|
19
|
+
app.manifest = Object.assign(Object.assign({}, app.manifest), { version: meta.version });
|
|
20
|
+
registerApp(app);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { resetFramework } from '../__test__/reset';
|
|
3
|
+
import { makeApp, makeAppManifest, makeShard, makeShardManifest } from '../__test__/fixtures';
|
|
4
|
+
import { registerLoadedBundle } from './register';
|
|
5
|
+
import { registeredApps } from '../apps/registry.svelte';
|
|
6
|
+
import { registeredShards } from '../shards/activate.svelte';
|
|
7
|
+
describe('registerLoadedBundle', () => {
|
|
8
|
+
beforeEach(resetFramework);
|
|
9
|
+
it('stamps meta.version onto every shard manifest before registering', () => {
|
|
10
|
+
var _a;
|
|
11
|
+
const shard = makeShard({ manifest: makeShardManifest({ id: 'shard-1', version: '' }) });
|
|
12
|
+
registerLoadedBundle({ shards: [shard], apps: [] }, { version: '2.3.4', sourceRegistry: 'https://r', contractVersion: '1' });
|
|
13
|
+
expect((_a = registeredShards.get('shard-1')) === null || _a === void 0 ? void 0 : _a.manifest.version).toBe('2.3.4');
|
|
14
|
+
});
|
|
15
|
+
it('stamps meta.version onto every app manifest before registering', () => {
|
|
16
|
+
var _a;
|
|
17
|
+
const app = makeApp({ manifest: makeAppManifest({ id: 'app-1', version: '' }) });
|
|
18
|
+
registerLoadedBundle({ shards: [], apps: [app] }, { version: '2.3.4', sourceRegistry: 'https://r', contractVersion: '1' });
|
|
19
|
+
expect((_a = registeredApps.get('app-1')) === null || _a === void 0 ? void 0 : _a.manifest.version).toBe('2.3.4');
|
|
20
|
+
});
|
|
21
|
+
it('registers combo bundles (both shards and apps)', () => {
|
|
22
|
+
const shard = makeShard({ manifest: makeShardManifest({ id: 'combo-s' }) });
|
|
23
|
+
const app = makeApp({ manifest: makeAppManifest({ id: 'combo-a', requiredShards: ['combo-s'] }) });
|
|
24
|
+
registerLoadedBundle({ shards: [shard], apps: [app] }, { version: '1.0.0', sourceRegistry: '', contractVersion: '1' });
|
|
25
|
+
expect(registeredShards.has('combo-s')).toBe(true);
|
|
26
|
+
expect(registeredApps.has('combo-a')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
+
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
6
|
+
describe('ctx.browse permission gating', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
__resetShardRegistryForTest();
|
|
9
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
10
|
+
__setTenantId('tenant-a');
|
|
11
|
+
});
|
|
12
|
+
it('is undefined when permission is absent', async () => {
|
|
13
|
+
let captured = null;
|
|
14
|
+
registerShard({
|
|
15
|
+
manifest: { id: 'no-browse', label: 'n', version: '0.0.0', views: [] },
|
|
16
|
+
activate(ctx) { captured = ctx; },
|
|
17
|
+
});
|
|
18
|
+
await activateShard('no-browse');
|
|
19
|
+
expect(captured.browse).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
it('is defined when documents:browse is declared', async () => {
|
|
22
|
+
var _a, _b, _c;
|
|
23
|
+
let captured = null;
|
|
24
|
+
registerShard({
|
|
25
|
+
manifest: {
|
|
26
|
+
id: 'has-browse', label: 'b', version: '0.0.0', views: [],
|
|
27
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE],
|
|
28
|
+
},
|
|
29
|
+
activate(ctx) { captured = ctx; },
|
|
30
|
+
});
|
|
31
|
+
await activateShard('has-browse');
|
|
32
|
+
expect(typeof ((_a = captured.browse) === null || _a === void 0 ? void 0 : _a.listDocuments)).toBe('function');
|
|
33
|
+
expect(typeof ((_b = captured.browse) === null || _b === void 0 ? void 0 : _b.watchDocuments)).toBe('function');
|
|
34
|
+
expect(typeof ((_c = captured.browse) === null || _c === void 0 ? void 0 : _c.listShards)).toBe('function');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { __resetSyncBundlesForTest } from '../documents/sync/singleton';
|
|
5
|
+
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
6
|
+
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
7
|
+
describe('ctx.syncRegistry', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
__resetShardRegistryForTest();
|
|
10
|
+
__resetSyncBundlesForTest();
|
|
11
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
12
|
+
__setTenantId('tenant-a');
|
|
13
|
+
});
|
|
14
|
+
it('is undefined without documents:browse', async () => {
|
|
15
|
+
let captured = null;
|
|
16
|
+
registerShard({
|
|
17
|
+
manifest: { id: 'no-obs', label: 'n', version: '0.0.0', views: [] },
|
|
18
|
+
activate(ctx) { captured = ctx; },
|
|
19
|
+
});
|
|
20
|
+
await activateShard('no-obs');
|
|
21
|
+
expect(captured.syncRegistry).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
it('is available under documents:browse', async () => {
|
|
24
|
+
let captured = null;
|
|
25
|
+
registerShard({
|
|
26
|
+
manifest: {
|
|
27
|
+
id: 'obs', label: 'o', version: '0.0.0', views: [],
|
|
28
|
+
permissions: [PERMISSION_DOCUMENTS_BROWSE],
|
|
29
|
+
},
|
|
30
|
+
activate(ctx) { captured = ctx; },
|
|
31
|
+
});
|
|
32
|
+
await activateShard('obs');
|
|
33
|
+
const reg = captured.syncRegistry();
|
|
34
|
+
expect(typeof reg.list).toBe('function');
|
|
35
|
+
expect(typeof reg.listConflicts).toBe('function');
|
|
36
|
+
expect(typeof reg.listAllConnectorIds).toBe('function');
|
|
37
|
+
expect(typeof reg.revoke).toBe('function');
|
|
38
|
+
// Functional smoke: empty registry should return []
|
|
39
|
+
expect(await reg.list()).toEqual([]);
|
|
40
|
+
expect(await reg.listConflicts()).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryDocumentBackend } from '../documents/backends';
|
|
3
|
+
import { __setDocumentBackend, __setTenantId } from '../documents/config';
|
|
4
|
+
import { registerShard, activateShard, __resetShardRegistryForTest } from './activate.svelte';
|
|
5
|
+
describe('ctx.tenantId', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
__resetShardRegistryForTest();
|
|
8
|
+
__setDocumentBackend(new MemoryDocumentBackend());
|
|
9
|
+
__setTenantId('tenant-a');
|
|
10
|
+
});
|
|
11
|
+
it('is present unconditionally on ctx', async () => {
|
|
12
|
+
let captured = null;
|
|
13
|
+
const shard = {
|
|
14
|
+
manifest: { id: 'test-tenantid', label: 't', version: '0.0.0', views: [] },
|
|
15
|
+
activate(ctx) { captured = ctx; },
|
|
16
|
+
};
|
|
17
|
+
registerShard(shard);
|
|
18
|
+
await activateShard('test-tenantid');
|
|
19
|
+
expect(captured.tenantId).toBe('tenant-a');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -50,6 +50,18 @@ export declare function isActive(id: string): boolean;
|
|
|
50
50
|
* Used by lifecycle.ts to pass context to `shard.resume()`.
|
|
51
51
|
*/
|
|
52
52
|
export declare function getShardContext(id: string): ShardContext | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Enumerate every view declared as `standalone` across the currently
|
|
55
|
+
* active shards. Intended for the `views --standalone` verb and any
|
|
56
|
+
* launcher UI that wants to surface "summonable" primitives. Only
|
|
57
|
+
* pulls from `activeShards` — registered-but-inactive shards aren't
|
|
58
|
+
* ready to mount.
|
|
59
|
+
*/
|
|
60
|
+
export declare function listStandaloneViews(): Array<{
|
|
61
|
+
shardId: string;
|
|
62
|
+
viewId: string;
|
|
63
|
+
label: string;
|
|
64
|
+
}>;
|
|
53
65
|
/**
|
|
54
66
|
* Test-only reset. Tears down any active shard entries (without running
|
|
55
67
|
* deactivate hooks — tests should run deactivate explicitly if they care)
|
|
@@ -23,6 +23,12 @@ import { fetchEnvState, putEnvState } from '../env/client';
|
|
|
23
23
|
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
24
24
|
import { createZoneManager } from '../state/manage';
|
|
25
25
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
26
|
+
import { PERMISSION_DOCUMENTS_SYNC } from '../documents/sync/types';
|
|
27
|
+
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
28
|
+
import { createBrowseCapability } from '../documents/browse';
|
|
29
|
+
import { getSyncBundle } from '../documents/sync/singleton';
|
|
30
|
+
import { createSyncHandle } from '../documents/sync/handle';
|
|
31
|
+
import { createSyncRegistryAccessor } from '../documents/sync/observer';
|
|
26
32
|
/**
|
|
27
33
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
28
34
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -65,7 +71,7 @@ export function registerShard(shard) {
|
|
|
65
71
|
* @throws If the shard is not registered, or if a manifest view has no factory after activation.
|
|
66
72
|
*/
|
|
67
73
|
export async function activateShard(id) {
|
|
68
|
-
var _a, _b;
|
|
74
|
+
var _a, _b, _c, _d, _e;
|
|
69
75
|
const shard = registeredShards.get(id);
|
|
70
76
|
if (!shard) {
|
|
71
77
|
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
@@ -125,9 +131,36 @@ export async function activateShard(id) {
|
|
|
125
131
|
get isAdmin() {
|
|
126
132
|
return checkIsAdmin();
|
|
127
133
|
},
|
|
134
|
+
get tenantId() {
|
|
135
|
+
return getTenantId();
|
|
136
|
+
},
|
|
128
137
|
zones: ((_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_STATE_MANAGE))
|
|
129
138
|
? createZoneManager()
|
|
130
139
|
: undefined,
|
|
140
|
+
browse: ((_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_BROWSE))
|
|
141
|
+
? createBrowseCapability(getTenantId(), getDocumentBackend())
|
|
142
|
+
: undefined,
|
|
143
|
+
syncRegistry: ((_c = shard.manifest.permissions) === null || _c === void 0 ? void 0 : _c.includes(PERMISSION_DOCUMENTS_BROWSE))
|
|
144
|
+
? createSyncRegistryAccessor(getDocumentBackend(), getTenantId())
|
|
145
|
+
: undefined,
|
|
146
|
+
sync: ((_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_SYNC))
|
|
147
|
+
? () => {
|
|
148
|
+
const backend = getDocumentBackend();
|
|
149
|
+
const tenantId = getTenantId();
|
|
150
|
+
const bundlePromise = getSyncBundle(backend, tenantId);
|
|
151
|
+
const handlePromise = bundlePromise.then(({ engine, registry }) => createSyncHandle({ tenantId, connectorId: id, engine, registry }));
|
|
152
|
+
return {
|
|
153
|
+
connectorId: id,
|
|
154
|
+
grantedScopes: async () => (await handlePromise).grantedScopes(),
|
|
155
|
+
getManifest: async (scope) => (await handlePromise).getManifest(scope),
|
|
156
|
+
changesSince: async (scope, cursor) => (await handlePromise).changesSince(scope, cursor),
|
|
157
|
+
ack: async (scope, cursor) => (await handlePromise).ack(scope, cursor),
|
|
158
|
+
apply: async (scope, entry, opts) => (await handlePromise).apply(scope, entry, opts),
|
|
159
|
+
applyBatch: async (scope, manifest, opts) => (await handlePromise).applyBatch(scope, manifest, opts),
|
|
160
|
+
forget: async (scope, path) => (await handlePromise).forget(scope, path),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
131
164
|
};
|
|
132
165
|
entry.ctx = ctx;
|
|
133
166
|
active.set(id, entry);
|
|
@@ -149,7 +182,7 @@ export async function activateShard(id) {
|
|
|
149
182
|
console.warn(`[sh3] Failed to hydrate env state for shard "${id}":`, err instanceof Error ? err.message : err);
|
|
150
183
|
}
|
|
151
184
|
}
|
|
152
|
-
void ((
|
|
185
|
+
void ((_e = shard.autostart) === null || _e === void 0 ? void 0 : _e.call(shard, ctx));
|
|
153
186
|
}
|
|
154
187
|
/**
|
|
155
188
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|
|
@@ -191,6 +224,24 @@ export function getShardContext(id) {
|
|
|
191
224
|
var _a;
|
|
192
225
|
return (_a = active.get(id)) === null || _a === void 0 ? void 0 : _a.ctx;
|
|
193
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Enumerate every view declared as `standalone` across the currently
|
|
229
|
+
* active shards. Intended for the `views --standalone` verb and any
|
|
230
|
+
* launcher UI that wants to surface "summonable" primitives. Only
|
|
231
|
+
* pulls from `activeShards` — registered-but-inactive shards aren't
|
|
232
|
+
* ready to mount.
|
|
233
|
+
*/
|
|
234
|
+
export function listStandaloneViews() {
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const shard of activeShards.values()) {
|
|
237
|
+
for (const view of shard.manifest.views) {
|
|
238
|
+
if (view.standalone) {
|
|
239
|
+
out.push({ shardId: shard.manifest.id, viewId: view.id, label: view.label });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
194
245
|
/**
|
|
195
246
|
* Test-only reset. Tears down any active shard entries (without running
|
|
196
247
|
* deactivate hooks — tests should run deactivate explicitly if they care)
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { StateZones } from '../state/zones.svelte';
|
|
2
2
|
import type { ZoneSchema, ZoneManager } from '../state/types';
|
|
3
3
|
import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
|
|
4
|
+
import type { BrowseCapability } from '../documents/browse';
|
|
5
|
+
import type { SyncHandle } from '../documents/sync/types';
|
|
6
|
+
import type { SyncRegistry } from '../documents/sync/registry';
|
|
4
7
|
import type { EnvState } from '../env/types';
|
|
5
8
|
import type { Verb } from '../verbs/types';
|
|
6
9
|
/**
|
|
@@ -82,6 +85,14 @@ export interface ViewDeclaration {
|
|
|
82
85
|
label: string;
|
|
83
86
|
/** Optional icon hint (reserved; not yet rendered in phase 8). */
|
|
84
87
|
icon?: string;
|
|
88
|
+
/**
|
|
89
|
+
* When true, this view is a standalone primitive — summonable from
|
|
90
|
+
* anywhere without requiring an owning app. Standalone views are
|
|
91
|
+
* enumerable via the framework's view directory and can be popped
|
|
92
|
+
* out into a float or docked into the active layout by verbs like
|
|
93
|
+
* `popout` and `dock`. Defaults to false.
|
|
94
|
+
*/
|
|
95
|
+
standalone?: boolean;
|
|
85
96
|
}
|
|
86
97
|
/**
|
|
87
98
|
* Static description of a shard as observed by the framework and consumers
|
|
@@ -116,7 +127,11 @@ export interface ShardManifest {
|
|
|
116
127
|
/**
|
|
117
128
|
* Optional permissions this shard requests beyond the default sandbox.
|
|
118
129
|
* Declared in the manifest and surfaced to the user at install time.
|
|
119
|
-
* Currently recognized:
|
|
130
|
+
* Currently recognized:
|
|
131
|
+
* - 'state:manage' — cross-shard zone access.
|
|
132
|
+
* - 'documents:sync' — cross-shard document sync API.
|
|
133
|
+
* - 'documents:browse' — tenant-wide document observation and sync
|
|
134
|
+
* registry visibility (observer-class shards, e.g. file-explorer).
|
|
120
135
|
*/
|
|
121
136
|
permissions?: string[];
|
|
122
137
|
}
|
|
@@ -185,12 +200,39 @@ export interface ShardContext {
|
|
|
185
200
|
envUpdate<T extends Record<string, unknown>>(patch: Partial<T>): Promise<void>;
|
|
186
201
|
/** Whether the current session has admin privileges. */
|
|
187
202
|
isAdmin: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* The active tenant id. Always present. Exposed for logging / diagnostics.
|
|
205
|
+
* Scopes never carry tenantId; the engine rebinds per-session. Do not
|
|
206
|
+
* serialize into persistent storage.
|
|
207
|
+
*/
|
|
208
|
+
tenantId: string;
|
|
188
209
|
/**
|
|
189
210
|
* Cross-shard zone management API. Only present when the shard's
|
|
190
211
|
* manifest declares the `'state:manage'` permission. Check with
|
|
191
212
|
* `if (ctx.zones)` before use.
|
|
192
213
|
*/
|
|
193
214
|
zones?: ZoneManager;
|
|
215
|
+
/**
|
|
216
|
+
* Cross-shard document sync API. Only present when the shard's
|
|
217
|
+
* manifest declares the `'documents:sync'` permission. Check with
|
|
218
|
+
* `if (ctx.sync)` before use.
|
|
219
|
+
*/
|
|
220
|
+
sync?: () => SyncHandle;
|
|
221
|
+
/**
|
|
222
|
+
* Tenant-wide document browse API. Read-only enumeration and change
|
|
223
|
+
* subscription across every shard's documents for the active tenant.
|
|
224
|
+
* Only present when the shard's manifest declares the
|
|
225
|
+
* `'documents:browse'` permission. Writes still flow through the
|
|
226
|
+
* owning shard's own `ctx.documents()` handle.
|
|
227
|
+
*/
|
|
228
|
+
browse?: BrowseCapability;
|
|
229
|
+
/**
|
|
230
|
+
* Sync registry observer. Read-only list/revoke/conflict enumeration
|
|
231
|
+
* for explorer-class shards. Only present when the shard declares
|
|
232
|
+
* `'documents:browse'`. Granting still happens exclusively via
|
|
233
|
+
* `<SyncGrantPicker />`.
|
|
234
|
+
*/
|
|
235
|
+
syncRegistry?: () => SyncRegistry;
|
|
194
236
|
}
|
|
195
237
|
/**
|
|
196
238
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
@@ -6,55 +6,149 @@
|
|
|
6
6
|
import { SessionClient } from './session-client.svelte';
|
|
7
7
|
import { VerbRegistry, type ShellApi } from './registry';
|
|
8
8
|
import type { ServerMessage } from './protocol';
|
|
9
|
+
import { TenantFsClient } from './tenant-fs-client';
|
|
10
|
+
import { ShellModeRegistry } from './modes/registry';
|
|
11
|
+
import { registerBuiltinModes } from './modes/builtin';
|
|
12
|
+
import { resolveInitialMode, writeLastMode } from './modes/prefs';
|
|
13
|
+
import type { ShellMode, ShellRole } from './modes/types';
|
|
14
|
+
import { makeDispatch } from './dispatch';
|
|
15
|
+
import { computeRelocate } from './auto-relocate';
|
|
16
|
+
import { activeLayout } from '../layout/store.svelte';
|
|
17
|
+
import type { LayoutNode } from '../layout/types';
|
|
18
|
+
import Toolbar from './toolbar/Toolbar.svelte';
|
|
19
|
+
import { ToolbarSlotRegistry } from './toolbar/slots';
|
|
20
|
+
import ModeSlot from './toolbar/slots/ModeSlot.svelte';
|
|
21
|
+
import FocusLockSlot from './toolbar/slots/FocusLockSlot.svelte';
|
|
22
|
+
import TargetShardSlot from './toolbar/slots/TargetShardSlot.svelte';
|
|
9
23
|
|
|
10
24
|
interface Props {
|
|
11
25
|
shell: ShellApi;
|
|
12
26
|
wsUrl: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
role: ShellRole;
|
|
13
29
|
}
|
|
14
|
-
let { shell, wsUrl }: Props = $props();
|
|
30
|
+
let { shell, wsUrl, userId, role }: Props = $props();
|
|
15
31
|
|
|
16
32
|
const scrollback = new Scrollback();
|
|
33
|
+
const resolver = new VerbRegistry();
|
|
34
|
+
const fs = new TenantFsClient();
|
|
35
|
+
|
|
36
|
+
// Mode registry
|
|
37
|
+
const modeRegistry = new ShellModeRegistry();
|
|
38
|
+
registerBuiltinModes(modeRegistry);
|
|
39
|
+
|
|
40
|
+
// Reactive current mode
|
|
41
|
+
let mode = $state<ShellMode>(
|
|
42
|
+
untrack(() => resolveInitialMode(modeRegistry, userId, role)),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
function setMode(id: string): void {
|
|
46
|
+
const next = modeRegistry.get(id);
|
|
47
|
+
if (!next) return;
|
|
48
|
+
if (next.requiresRole && next.requiresRole !== role) return;
|
|
49
|
+
mode = next;
|
|
50
|
+
writeLastMode(userId, id);
|
|
51
|
+
if (next.transport !== 'ws') {
|
|
52
|
+
scrollback.push({ kind: 'status', text: 'mode switch: reload to take effect for server-shell changes', level: 'info', ts: Date.now() });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
17
56
|
// wsUrl is a prop read at construction only. untrack prevents Svelte 5's
|
|
18
57
|
// "referenced outside a closure" warning; the URL never changes at runtime.
|
|
19
58
|
const session = untrack(() => new SessionClient(wsUrl));
|
|
20
|
-
|
|
59
|
+
|
|
60
|
+
const dispatch = untrack(() => makeDispatch({
|
|
61
|
+
mode: () => mode,
|
|
62
|
+
resolver,
|
|
63
|
+
scrollback,
|
|
64
|
+
session,
|
|
65
|
+
shell,
|
|
66
|
+
fs,
|
|
67
|
+
cwd: () => session.cwd,
|
|
68
|
+
}));
|
|
21
69
|
|
|
22
70
|
let locked = $state(false);
|
|
23
71
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Auto-relocate: track the focused shard and update session.cwd when focus
|
|
74
|
+
// changes to a shard whose documents directory exists. focusLocked and
|
|
75
|
+
// targetShard are read by Task 13's toolbar component via props.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
let focusLocked = $state(false);
|
|
79
|
+
let targetShard = $state<string | null>(null);
|
|
80
|
+
|
|
81
|
+
function toggleFocusLock(): void {
|
|
82
|
+
focusLocked = !focusLocked;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Toolbar slot registry
|
|
86
|
+
const toolbarRegistry = new ToolbarSlotRegistry();
|
|
87
|
+
toolbarRegistry.register({ id: 'mode', order: 10, visible: () => true, component: ModeSlot });
|
|
88
|
+
toolbarRegistry.register({ id: 'focus-lock', order: 20, visible: (ctx) => ctx.mode.id === 'user', component: FocusLockSlot });
|
|
89
|
+
toolbarRegistry.register({ id: 'target-shard', order: 30, visible: (ctx) => ctx.mode.id === 'user', component: TargetShardSlot });
|
|
90
|
+
|
|
91
|
+
let toolbarExpanded = $state((() => {
|
|
92
|
+
try { return localStorage.getItem('sh3.shell.toolbarExpanded') !== '0'; } catch { return true; }
|
|
93
|
+
})());
|
|
94
|
+
|
|
95
|
+
function toggleToolbar() {
|
|
96
|
+
toolbarExpanded = !toolbarExpanded;
|
|
97
|
+
try { localStorage.setItem('sh3.shell.toolbarExpanded', toolbarExpanded ? '1' : '0'); } catch {}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Walk the layout tree and return the viewId of the active tab in the first
|
|
101
|
+
* TabsNode found (breadth-first). Returns null if the layout contains no
|
|
102
|
+
* tabs node with a populated active tab. */
|
|
103
|
+
function getActiveViewId(node: LayoutNode): string | null {
|
|
104
|
+
if (node.type === 'tabs') {
|
|
105
|
+
const entry = node.tabs[node.activeTab];
|
|
106
|
+
return entry?.viewId ?? null;
|
|
107
|
+
}
|
|
108
|
+
if (node.type === 'split') {
|
|
109
|
+
for (const child of node.children) {
|
|
110
|
+
const found = getActiveViewId(child);
|
|
111
|
+
if (found !== null) return found;
|
|
51
112
|
}
|
|
52
|
-
} else {
|
|
53
|
-
// Forward to server
|
|
54
|
-
session.send({ t: 'submit', line: resolution.line });
|
|
55
113
|
}
|
|
114
|
+
// slot node
|
|
115
|
+
return node.viewId ?? null;
|
|
56
116
|
}
|
|
57
117
|
|
|
118
|
+
/** Derive the focused shard id from the currently-active layout. The shard
|
|
119
|
+
* id is the prefix before the first ':' in a viewId (e.g. 'shell:terminal'
|
|
120
|
+
* → 'shell'). Returns null when no view is active. */
|
|
121
|
+
function getFocusedShardId(): string | null {
|
|
122
|
+
try {
|
|
123
|
+
const tree = activeLayout();
|
|
124
|
+
const viewId = getActiveViewId(tree.docked);
|
|
125
|
+
if (!viewId) return null;
|
|
126
|
+
const colon = viewId.indexOf(':');
|
|
127
|
+
return colon >= 0 ? viewId.slice(0, colon) : viewId;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$effect(() => {
|
|
134
|
+
const focused = getFocusedShardId();
|
|
135
|
+
const autoRelocate = mode.autoRelocate;
|
|
136
|
+
const isFocusLocked = focusLocked;
|
|
137
|
+
(async () => {
|
|
138
|
+
const r = await computeRelocate({
|
|
139
|
+
modeAutoRelocate: autoRelocate,
|
|
140
|
+
focusLocked: isFocusLocked,
|
|
141
|
+
focusedShardId: focused,
|
|
142
|
+
currentShardId: 'shell',
|
|
143
|
+
}, fs);
|
|
144
|
+
if (r.kind === 'relocate' && r.path !== undefined) {
|
|
145
|
+
session.cwd = r.path;
|
|
146
|
+
}
|
|
147
|
+
if (focused && focused !== 'shell') targetShard = focused;
|
|
148
|
+
else if (!focused) targetShard = null;
|
|
149
|
+
})();
|
|
150
|
+
});
|
|
151
|
+
|
|
58
152
|
function handleServerMessage(msg: ServerMessage) {
|
|
59
153
|
if (msg.t !== 'event') return;
|
|
60
154
|
const e = msg.event;
|
|
@@ -96,7 +190,9 @@
|
|
|
96
190
|
|
|
97
191
|
onMount(() => {
|
|
98
192
|
unsub = session.onMessage(handleServerMessage);
|
|
99
|
-
|
|
193
|
+
if (mode.transport === 'ws') {
|
|
194
|
+
session.connect();
|
|
195
|
+
}
|
|
100
196
|
});
|
|
101
197
|
|
|
102
198
|
onDestroy(() => {
|
|
@@ -106,6 +202,17 @@
|
|
|
106
202
|
</script>
|
|
107
203
|
|
|
108
204
|
<div class="shell-terminal">
|
|
205
|
+
<Toolbar
|
|
206
|
+
registry={toolbarRegistry}
|
|
207
|
+
ctx={{ mode, role }}
|
|
208
|
+
expanded={toolbarExpanded}
|
|
209
|
+
onToggle={toggleToolbar}
|
|
210
|
+
slotProps={{
|
|
211
|
+
mode: { mode, role, registry: modeRegistry, onSelect: setMode },
|
|
212
|
+
'focus-lock': { locked: focusLocked, onToggle: () => (focusLocked = !focusLocked) },
|
|
213
|
+
'target-shard': { target: targetShard },
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
109
216
|
<ScrollbackView {scrollback} />
|
|
110
217
|
<InputLine
|
|
111
218
|
cwd={session.cwd}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type ShellApi } from './registry';
|
|
2
|
+
import type { ShellRole } from './modes/types';
|
|
2
3
|
interface Props {
|
|
3
4
|
shell: ShellApi;
|
|
4
5
|
wsUrl: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
role: ShellRole;
|
|
5
8
|
}
|
|
6
9
|
declare const Terminal: import("svelte").Component<Props, {}, "">;
|
|
7
10
|
type Terminal = ReturnType<typeof Terminal>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TenantFsClient } from './tenant-fs-client';
|
|
2
|
+
export interface RelocateInput {
|
|
3
|
+
modeAutoRelocate: boolean;
|
|
4
|
+
focusLocked: boolean;
|
|
5
|
+
focusedShardId: string | null;
|
|
6
|
+
currentShardId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RelocateEffect {
|
|
9
|
+
kind: 'noop' | 'relocate';
|
|
10
|
+
path?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function computeRelocate(input: RelocateInput, fs: TenantFsClient): Promise<RelocateEffect>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function computeRelocate(input, fs) {
|
|
2
|
+
if (!input.modeAutoRelocate)
|
|
3
|
+
return { kind: 'noop' };
|
|
4
|
+
if (input.focusLocked)
|
|
5
|
+
return { kind: 'noop' };
|
|
6
|
+
if (input.focusedShardId === null)
|
|
7
|
+
return { kind: 'noop' };
|
|
8
|
+
if (input.focusedShardId === input.currentShardId)
|
|
9
|
+
return { kind: 'noop' };
|
|
10
|
+
const path = input.focusedShardId;
|
|
11
|
+
try {
|
|
12
|
+
const s = await fs.stat(path);
|
|
13
|
+
if (s.kind !== 'dir')
|
|
14
|
+
return { kind: 'noop' };
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
return { kind: 'noop' };
|
|
18
|
+
}
|
|
19
|
+
return { kind: 'relocate', path };
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { computeRelocate } from './auto-relocate';
|
|
3
|
+
const statOk = { list: vi.fn(), read: vi.fn(), stat: async () => ({ name: 'notes', kind: 'dir', size: 0, mtime: 0 }) };
|
|
4
|
+
const statMissing = { list: vi.fn(), read: vi.fn(), stat: async () => { throw new Error('not found'); } };
|
|
5
|
+
const statFile = { list: vi.fn(), read: vi.fn(), stat: async () => ({ name: 'notes', kind: 'file', size: 0, mtime: 0 }) };
|
|
6
|
+
describe('computeRelocate', () => {
|
|
7
|
+
it('noop when mode disables auto-relocate', async () => {
|
|
8
|
+
const r = await computeRelocate({ modeAutoRelocate: false, focusLocked: false, focusedShardId: 'notes', currentShardId: 'shell' }, statOk);
|
|
9
|
+
expect(r.kind).toBe('noop');
|
|
10
|
+
});
|
|
11
|
+
it('noop when focus-lock is on', async () => {
|
|
12
|
+
const r = await computeRelocate({ modeAutoRelocate: true, focusLocked: true, focusedShardId: 'notes', currentShardId: 'shell' }, statOk);
|
|
13
|
+
expect(r.kind).toBe('noop');
|
|
14
|
+
});
|
|
15
|
+
it('noop when nothing relevant is focused', async () => {
|
|
16
|
+
const r = await computeRelocate({ modeAutoRelocate: true, focusLocked: false, focusedShardId: null, currentShardId: 'shell' }, statOk);
|
|
17
|
+
expect(r.kind).toBe('noop');
|
|
18
|
+
});
|
|
19
|
+
it('noop when focusing the shell itself', async () => {
|
|
20
|
+
const r = await computeRelocate({ modeAutoRelocate: true, focusLocked: false, focusedShardId: 'shell', currentShardId: 'shell' }, statOk);
|
|
21
|
+
expect(r.kind).toBe('noop');
|
|
22
|
+
});
|
|
23
|
+
it('noop when target documents folder does not exist', async () => {
|
|
24
|
+
const r = await computeRelocate({ modeAutoRelocate: true, focusLocked: false, focusedShardId: 'notes', currentShardId: 'shell' }, statMissing);
|
|
25
|
+
expect(r.kind).toBe('noop');
|
|
26
|
+
});
|
|
27
|
+
it('noop when target is a file, not a directory', async () => {
|
|
28
|
+
const r = await computeRelocate({ modeAutoRelocate: true, focusLocked: false, focusedShardId: 'notes', currentShardId: 'shell' }, statFile);
|
|
29
|
+
expect(r.kind).toBe('noop');
|
|
30
|
+
});
|
|
31
|
+
it('relocates to the shard id path on happy path', async () => {
|
|
32
|
+
const r = await computeRelocate({ modeAutoRelocate: true, focusLocked: false, focusedShardId: 'notes', currentShardId: 'shell' }, statOk);
|
|
33
|
+
expect(r).toEqual({ kind: 'relocate', path: 'notes' });
|
|
34
|
+
});
|
|
35
|
+
});
|