sh3-core 0.8.0 → 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 +2 -1
- package/dist/api.js +1 -1
- package/dist/app/admin/SystemView.svelte +149 -11
- 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/http-backend.d.ts +4 -0
- package/dist/documents/http-backend.js +14 -0
- package/dist/documents/sync/index.d.ts +1 -2
- package/dist/documents/sync/index.js +0 -2
- package/dist/documents/sync/observer.d.ts +3 -0
- package/dist/documents/sync/observer.js +45 -0
- package/dist/documents/sync/registry.d.ts +3 -0
- package/dist/documents/sync/registry.js +8 -1
- package/dist/documents/sync/registry.test.js +11 -0
- package/dist/documents/types.d.ts +18 -0
- package/dist/documents/types.js +6 -1
- package/dist/layout/inspection.d.ts +17 -0
- package/dist/layout/inspection.js +53 -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 +33 -3
- package/dist/shards/types.d.ts +33 -0
- package/dist/shell-shard/manifest.js +1 -1
- package/dist/shell-shard/shellShard.svelte.js +52 -4
- package/dist/shell-shard/verbs/index.js +3 -1
- 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 +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -50,6 +50,23 @@ export declare function expandChild(splitPath: number[], childIndex: number): bo
|
|
|
50
50
|
* the sole authority on tree mutations.
|
|
51
51
|
*/
|
|
52
52
|
export declare function closeTab(slotId: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Pop a docked tab out into a new float. Locates the tab by `slotId` in
|
|
55
|
+
* the currently-rendered docked tree, removes it (preserving viewId +
|
|
56
|
+
* meta), and opens a float with the same view. Returns the new floatId
|
|
57
|
+
* on success, or null if the slot wasn't found in the docked tree.
|
|
58
|
+
*
|
|
59
|
+
* Guarded canClose() is NOT consulted — popout is not a close. The slot
|
|
60
|
+
* is recreated in the float with a fresh slotId, so the view is remounted.
|
|
61
|
+
*/
|
|
62
|
+
export declare function popoutView(slotId: string): string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Dock a float back into the currently-rendered layout. The float's
|
|
65
|
+
* active tab is appended to the first tabs group (same policy as
|
|
66
|
+
* `dockIntoActiveLayout`). Returns true on success. The float is closed
|
|
67
|
+
* after its content is transferred.
|
|
68
|
+
*/
|
|
69
|
+
export declare function dockFloat(floatId: string): boolean;
|
|
53
70
|
/**
|
|
54
71
|
* Dock a view into the currently-rendered layout without caring which
|
|
55
72
|
* root it is. Used by the Ctrl+` shell hotkey and other "just put it
|
|
@@ -194,6 +194,59 @@ async function closeFloatTab(tree, slotId) {
|
|
|
194
194
|
}
|
|
195
195
|
return false;
|
|
196
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Pop a docked tab out into a new float. Locates the tab by `slotId` in
|
|
199
|
+
* the currently-rendered docked tree, removes it (preserving viewId +
|
|
200
|
+
* meta), and opens a float with the same view. Returns the new floatId
|
|
201
|
+
* on success, or null if the slot wasn't found in the docked tree.
|
|
202
|
+
*
|
|
203
|
+
* Guarded canClose() is NOT consulted — popout is not a close. The slot
|
|
204
|
+
* is recreated in the float with a fresh slotId, so the view is remounted.
|
|
205
|
+
*/
|
|
206
|
+
export function popoutView(slotId) {
|
|
207
|
+
const tree = activeLayout();
|
|
208
|
+
const located = findTabBySlotId(tree.docked, slotId);
|
|
209
|
+
if (!located)
|
|
210
|
+
return null;
|
|
211
|
+
const entry = located.entry;
|
|
212
|
+
const viewId = entry.viewId;
|
|
213
|
+
if (!viewId)
|
|
214
|
+
return null;
|
|
215
|
+
const title = entry.label;
|
|
216
|
+
const meta = entry.meta;
|
|
217
|
+
removeTabBySlotId(tree.docked, slotId);
|
|
218
|
+
cleanupTree(tree.docked);
|
|
219
|
+
return floatManager.open(viewId, { title, meta });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Dock a float back into the currently-rendered layout. The float's
|
|
223
|
+
* active tab is appended to the first tabs group (same policy as
|
|
224
|
+
* `dockIntoActiveLayout`). Returns true on success. The float is closed
|
|
225
|
+
* after its content is transferred.
|
|
226
|
+
*/
|
|
227
|
+
export function dockFloat(floatId) {
|
|
228
|
+
var _a, _b;
|
|
229
|
+
const tree = activeLayout();
|
|
230
|
+
const floatEntry = tree.floats.find((f) => f.id === floatId);
|
|
231
|
+
if (!floatEntry)
|
|
232
|
+
return false;
|
|
233
|
+
const content = floatEntry.content;
|
|
234
|
+
const tabs = content.type === 'tabs' ? content : null;
|
|
235
|
+
if (!tabs || tabs.tabs.length === 0) {
|
|
236
|
+
floatManager.close(floatId);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const entry = (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0];
|
|
240
|
+
const ok = dockIntoActiveLayout({
|
|
241
|
+
slotId: entry.slotId,
|
|
242
|
+
viewId: entry.viewId,
|
|
243
|
+
label: entry.label,
|
|
244
|
+
meta: entry.meta,
|
|
245
|
+
});
|
|
246
|
+
if (ok)
|
|
247
|
+
floatManager.close(floatId);
|
|
248
|
+
return ok;
|
|
249
|
+
}
|
|
197
250
|
function findFirstTabsNode(node) {
|
|
198
251
|
if (node.type === 'tabs')
|
|
199
252
|
return node;
|
|
@@ -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)
|
|
@@ -24,8 +24,11 @@ import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
|
24
24
|
import { createZoneManager } from '../state/manage';
|
|
25
25
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
26
26
|
import { PERMISSION_DOCUMENTS_SYNC } from '../documents/sync/types';
|
|
27
|
+
import { PERMISSION_DOCUMENTS_BROWSE } from '../documents/types';
|
|
28
|
+
import { createBrowseCapability } from '../documents/browse';
|
|
27
29
|
import { getSyncBundle } from '../documents/sync/singleton';
|
|
28
30
|
import { createSyncHandle } from '../documents/sync/handle';
|
|
31
|
+
import { createSyncRegistryAccessor } from '../documents/sync/observer';
|
|
29
32
|
/**
|
|
30
33
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
31
34
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -68,7 +71,7 @@ export function registerShard(shard) {
|
|
|
68
71
|
* @throws If the shard is not registered, or if a manifest view has no factory after activation.
|
|
69
72
|
*/
|
|
70
73
|
export async function activateShard(id) {
|
|
71
|
-
var _a, _b, _c;
|
|
74
|
+
var _a, _b, _c, _d, _e;
|
|
72
75
|
const shard = registeredShards.get(id);
|
|
73
76
|
if (!shard) {
|
|
74
77
|
throw new Error(`Cannot activate shard "${id}": not registered`);
|
|
@@ -128,10 +131,19 @@ export async function activateShard(id) {
|
|
|
128
131
|
get isAdmin() {
|
|
129
132
|
return checkIsAdmin();
|
|
130
133
|
},
|
|
134
|
+
get tenantId() {
|
|
135
|
+
return getTenantId();
|
|
136
|
+
},
|
|
131
137
|
zones: ((_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_STATE_MANAGE))
|
|
132
138
|
? createZoneManager()
|
|
133
139
|
: undefined,
|
|
134
|
-
|
|
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))
|
|
135
147
|
? () => {
|
|
136
148
|
const backend = getDocumentBackend();
|
|
137
149
|
const tenantId = getTenantId();
|
|
@@ -170,7 +182,7 @@ export async function activateShard(id) {
|
|
|
170
182
|
console.warn(`[sh3] Failed to hydrate env state for shard "${id}":`, err instanceof Error ? err.message : err);
|
|
171
183
|
}
|
|
172
184
|
}
|
|
173
|
-
void ((
|
|
185
|
+
void ((_e = shard.autostart) === null || _e === void 0 ? void 0 : _e.call(shard, ctx));
|
|
174
186
|
}
|
|
175
187
|
/**
|
|
176
188
|
* Deactivate an active shard. Calls `shard.deactivate`, flushes and disposes
|
|
@@ -212,6 +224,24 @@ export function getShardContext(id) {
|
|
|
212
224
|
var _a;
|
|
213
225
|
return (_a = active.get(id)) === null || _a === void 0 ? void 0 : _a.ctx;
|
|
214
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
|
+
}
|
|
215
245
|
/**
|
|
216
246
|
* Test-only reset. Tears down any active shard entries (without running
|
|
217
247
|
* deactivate hooks — tests should run deactivate explicitly if they care)
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -1,7 +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';
|
|
4
5
|
import type { SyncHandle } from '../documents/sync/types';
|
|
6
|
+
import type { SyncRegistry } from '../documents/sync/registry';
|
|
5
7
|
import type { EnvState } from '../env/types';
|
|
6
8
|
import type { Verb } from '../verbs/types';
|
|
7
9
|
/**
|
|
@@ -83,6 +85,14 @@ export interface ViewDeclaration {
|
|
|
83
85
|
label: string;
|
|
84
86
|
/** Optional icon hint (reserved; not yet rendered in phase 8). */
|
|
85
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;
|
|
86
96
|
}
|
|
87
97
|
/**
|
|
88
98
|
* Static description of a shard as observed by the framework and consumers
|
|
@@ -120,6 +130,8 @@ export interface ShardManifest {
|
|
|
120
130
|
* Currently recognized:
|
|
121
131
|
* - 'state:manage' — cross-shard zone access.
|
|
122
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).
|
|
123
135
|
*/
|
|
124
136
|
permissions?: string[];
|
|
125
137
|
}
|
|
@@ -188,6 +200,12 @@ export interface ShardContext {
|
|
|
188
200
|
envUpdate<T extends Record<string, unknown>>(patch: Partial<T>): Promise<void>;
|
|
189
201
|
/** Whether the current session has admin privileges. */
|
|
190
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;
|
|
191
209
|
/**
|
|
192
210
|
* Cross-shard zone management API. Only present when the shard's
|
|
193
211
|
* manifest declares the `'state:manage'` permission. Check with
|
|
@@ -200,6 +218,21 @@ export interface ShardContext {
|
|
|
200
218
|
* `if (ctx.sync)` before use.
|
|
201
219
|
*/
|
|
202
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;
|
|
203
236
|
}
|
|
204
237
|
/**
|
|
205
238
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
@@ -3,7 +3,7 @@ export const manifest = {
|
|
|
3
3
|
id: 'shell',
|
|
4
4
|
label: 'Shell',
|
|
5
5
|
version: VERSION,
|
|
6
|
-
views: [{ id: 'shell:terminal', label: 'Shell' }],
|
|
6
|
+
views: [{ id: 'shell:terminal', label: 'Shell', standalone: true }],
|
|
7
7
|
// serverBundle intentionally omitted — this shard is a framework built-in
|
|
8
8
|
// and is statically mounted at sh3-server boot. The existing contract in
|
|
9
9
|
// sh3-core/src/shards/types.ts documents that framework-shipped shards do
|
|
@@ -19,7 +19,9 @@ import { registerV1Verbs } from './verbs';
|
|
|
19
19
|
import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
|
|
20
20
|
import { launchApp } from '../apps/lifecycle';
|
|
21
21
|
import { registeredShards } from '../shards/activate.svelte';
|
|
22
|
-
import { inspectActiveLayout, focusView, closeTab } from '../layout/inspection';
|
|
22
|
+
import { inspectActiveLayout, focusView, closeTab, popoutView, dockFloat, dockIntoActiveLayout } from '../layout/inspection';
|
|
23
|
+
import { floatManager } from '../overlays/float';
|
|
24
|
+
import { listStandaloneViews } from '../shards/activate.svelte';
|
|
23
25
|
import { getUser, isAdmin } from '../auth/index';
|
|
24
26
|
/** Walk a layout tree and collect all tab entries (slotId + viewId + label). */
|
|
25
27
|
function collectTabEntries(node) {
|
|
@@ -76,16 +78,62 @@ function makeShellApi(_ctx) {
|
|
|
76
78
|
return [];
|
|
77
79
|
}
|
|
78
80
|
},
|
|
79
|
-
// → layout/inspection: focusView(viewId)
|
|
81
|
+
// → layout/inspection: focusView(viewId). Falls back to dockIntoActiveLayout
|
|
82
|
+
// for standalone views that aren't mounted yet — this is the single
|
|
83
|
+
// "summon" entry point wired behind the `open` verb.
|
|
80
84
|
openViewInCurrentLayout(viewId) {
|
|
81
85
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
if (focusView(viewId))
|
|
87
|
+
return { ok: true };
|
|
88
|
+
const standalone = listStandaloneViews().find((v) => v.viewId === viewId);
|
|
89
|
+
if (standalone) {
|
|
90
|
+
const slotId = `standalone:${viewId}:${Date.now()}`;
|
|
91
|
+
const ok = dockIntoActiveLayout({ slotId, viewId, label: standalone.label });
|
|
92
|
+
return ok ? { ok: true } : { ok: false, error: `could not dock "${viewId}" — no available slot` };
|
|
93
|
+
}
|
|
94
|
+
return { ok: false, error: `view "${viewId}" not found in current layout` };
|
|
84
95
|
}
|
|
85
96
|
catch (err) {
|
|
86
97
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
87
98
|
}
|
|
88
99
|
},
|
|
100
|
+
// → shards/activate.svelte: listStandaloneViews() walks activeShards
|
|
101
|
+
listStandaloneViews() {
|
|
102
|
+
return listStandaloneViews();
|
|
103
|
+
},
|
|
104
|
+
// → layout/inspection: popoutView(slotId) returns floatId | null
|
|
105
|
+
popoutSlot(slotId) {
|
|
106
|
+
try {
|
|
107
|
+
const floatId = popoutView(slotId);
|
|
108
|
+
return floatId ? { ok: true, floatId } : { ok: false, error: `slot "${slotId}" not found in docked tree` };
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
// → layout/inspection: dockFloat(floatId) returns boolean
|
|
115
|
+
dockFloat(floatId) {
|
|
116
|
+
try {
|
|
117
|
+
const ok = dockFloat(floatId);
|
|
118
|
+
return ok ? { ok: true } : { ok: false, error: `float "${floatId}" not found or has no dockable content` };
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
// → overlays/float: floatManager.list() returns FloatEntry[]
|
|
125
|
+
listFloats() {
|
|
126
|
+
return floatManager.list().map((f) => {
|
|
127
|
+
var _a, _b, _c, _d, _e;
|
|
128
|
+
const tabs = f.content.type === 'tabs' ? f.content : null;
|
|
129
|
+
const active = tabs ? (_b = tabs.tabs[(_a = tabs.activeTab) !== null && _a !== void 0 ? _a : 0]) !== null && _b !== void 0 ? _b : tabs.tabs[0] : null;
|
|
130
|
+
return {
|
|
131
|
+
floatId: f.id,
|
|
132
|
+
viewId: (_c = active === null || active === void 0 ? void 0 : active.viewId) !== null && _c !== void 0 ? _c : null,
|
|
133
|
+
label: (_e = (_d = f.title) !== null && _d !== void 0 ? _d : active === null || active === void 0 ? void 0 : active.label) !== null && _e !== void 0 ? _e : f.id,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
},
|
|
89
137
|
// → layout/inspection: closeTab(slotId) is async (guarded close).
|
|
90
138
|
// Fire-and-forget; the tab disappears asynchronously. ShellApi stays sync.
|
|
91
139
|
closeSlot(slotId) {
|
|
@@ -7,7 +7,7 @@ import { clearVerb } from './clear';
|
|
|
7
7
|
import { historyVerb } from './history';
|
|
8
8
|
import { appsVerb, appVerb } from './apps';
|
|
9
9
|
import { shardsVerb } from './shards';
|
|
10
|
-
import { viewsVerb, openVerb, closeVerb } from './views';
|
|
10
|
+
import { viewsVerb, openVerb, closeVerb, popoutVerb, dockVerb } from './views';
|
|
11
11
|
import { zonesVerb, zoneVerb } from './zones';
|
|
12
12
|
import { pwdVerb, cdVerb, whoamiVerb } from './session';
|
|
13
13
|
import { envVerb } from './env';
|
|
@@ -23,6 +23,8 @@ export function registerV1Verbs(ctx) {
|
|
|
23
23
|
ctx.registerVerb(viewsVerb);
|
|
24
24
|
ctx.registerVerb(openVerb);
|
|
25
25
|
ctx.registerVerb(closeVerb);
|
|
26
|
+
ctx.registerVerb(popoutVerb);
|
|
27
|
+
ctx.registerVerb(dockVerb);
|
|
26
28
|
ctx.registerVerb(zonesVerb);
|
|
27
29
|
ctx.registerVerb(zoneVerb);
|
|
28
30
|
ctx.registerVerb(pwdVerb);
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import ViewsTable from '../rich/ViewsTable.svelte';
|
|
2
2
|
export const viewsVerb = {
|
|
3
3
|
name: 'views',
|
|
4
|
-
summary: 'List views currently mounted
|
|
5
|
-
async run(ctx) {
|
|
4
|
+
summary: 'List views currently mounted. Pass --standalone to list summonable views instead.',
|
|
5
|
+
async run(ctx, args) {
|
|
6
|
+
if (args.includes('--standalone')) {
|
|
7
|
+
const standalones = ctx.shell.listStandaloneViews();
|
|
8
|
+
if (standalones.length === 0) {
|
|
9
|
+
ctx.scrollback.push({
|
|
10
|
+
kind: 'status',
|
|
11
|
+
text: 'shell: no standalone views are currently available.',
|
|
12
|
+
level: 'info',
|
|
13
|
+
ts: Date.now(),
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
ctx.scrollback.push({
|
|
18
|
+
kind: 'status',
|
|
19
|
+
text: standalones
|
|
20
|
+
.map((v) => ` ${v.viewId.padEnd(32)} ${v.label}`)
|
|
21
|
+
.join('\n'),
|
|
22
|
+
level: 'info',
|
|
23
|
+
ts: Date.now(),
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
6
27
|
const views = ctx.shell.listViewsInCurrentLayout();
|
|
7
28
|
ctx.scrollback.push({
|
|
8
29
|
kind: 'rich',
|
|
@@ -62,6 +83,86 @@ export const openVerb = {
|
|
|
62
83
|
}
|
|
63
84
|
},
|
|
64
85
|
};
|
|
86
|
+
export const popoutVerb = {
|
|
87
|
+
name: 'popout',
|
|
88
|
+
summary: 'Pop a docked view out into a float by slot id.',
|
|
89
|
+
async run(ctx, args) {
|
|
90
|
+
var _a;
|
|
91
|
+
const slotId = args[0];
|
|
92
|
+
if (!slotId) {
|
|
93
|
+
ctx.scrollback.push({
|
|
94
|
+
kind: 'status',
|
|
95
|
+
text: 'usage: popout <slotId>',
|
|
96
|
+
level: 'warn',
|
|
97
|
+
ts: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const result = ctx.shell.popoutSlot(slotId);
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
ctx.scrollback.push({
|
|
104
|
+
kind: 'status',
|
|
105
|
+
text: `shell: popout failed — ${(_a = result.error) !== null && _a !== void 0 ? _a : 'unknown'}`,
|
|
106
|
+
level: 'error',
|
|
107
|
+
ts: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
ctx.scrollback.push({
|
|
112
|
+
kind: 'status',
|
|
113
|
+
text: `shell: popped out ${slotId} → ${result.floatId}`,
|
|
114
|
+
level: 'info',
|
|
115
|
+
ts: Date.now(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
export const dockVerb = {
|
|
121
|
+
name: 'dock',
|
|
122
|
+
summary: 'Dock a float back into the current layout by float id. Run with no args to list floats.',
|
|
123
|
+
async run(ctx, args) {
|
|
124
|
+
var _a;
|
|
125
|
+
const floatId = args[0];
|
|
126
|
+
if (!floatId) {
|
|
127
|
+
const floats = ctx.shell.listFloats();
|
|
128
|
+
if (floats.length === 0) {
|
|
129
|
+
ctx.scrollback.push({
|
|
130
|
+
kind: 'status',
|
|
131
|
+
text: 'shell: no active floats.',
|
|
132
|
+
level: 'info',
|
|
133
|
+
ts: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
ctx.scrollback.push({
|
|
138
|
+
kind: 'status',
|
|
139
|
+
text: floats
|
|
140
|
+
.map((f) => { var _a; return ` ${f.floatId.padEnd(24)} ${(_a = f.viewId) !== null && _a !== void 0 ? _a : '-'}\t${f.label}`; })
|
|
141
|
+
.join('\n'),
|
|
142
|
+
level: 'info',
|
|
143
|
+
ts: Date.now(),
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const result = ctx.shell.dockFloat(floatId);
|
|
148
|
+
if (!result.ok) {
|
|
149
|
+
ctx.scrollback.push({
|
|
150
|
+
kind: 'status',
|
|
151
|
+
text: `shell: dock failed — ${(_a = result.error) !== null && _a !== void 0 ? _a : 'unknown'}`,
|
|
152
|
+
level: 'error',
|
|
153
|
+
ts: Date.now(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
ctx.scrollback.push({
|
|
158
|
+
kind: 'status',
|
|
159
|
+
text: `shell: docked ${floatId}`,
|
|
160
|
+
level: 'info',
|
|
161
|
+
ts: Date.now(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
65
166
|
export const closeVerb = {
|
|
66
167
|
name: 'close',
|
|
67
168
|
summary: 'Close a view by slot id.',
|
package/dist/verbs/types.d.ts
CHANGED
|
@@ -29,6 +29,25 @@ export interface ShellApi {
|
|
|
29
29
|
ok: boolean;
|
|
30
30
|
error?: string;
|
|
31
31
|
};
|
|
32
|
+
listStandaloneViews(): Array<{
|
|
33
|
+
shardId: string;
|
|
34
|
+
viewId: string;
|
|
35
|
+
label: string;
|
|
36
|
+
}>;
|
|
37
|
+
popoutSlot(slotId: string): {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
error?: string;
|
|
40
|
+
floatId?: string;
|
|
41
|
+
};
|
|
42
|
+
dockFloat(floatId: string): {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
error?: string;
|
|
45
|
+
};
|
|
46
|
+
listFloats(): Array<{
|
|
47
|
+
floatId: string;
|
|
48
|
+
viewId: string | null;
|
|
49
|
+
label: string;
|
|
50
|
+
}>;
|
|
32
51
|
listZones(shardId?: string): Array<{
|
|
33
52
|
shardId: string;
|
|
34
53
|
zones: string[];
|
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.8.
|
|
2
|
+
export declare const VERSION = "0.8.1";
|
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.8.
|
|
2
|
+
export const VERSION = '0.8.1';
|