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.
Files changed (42) hide show
  1. package/dist/api.d.ts +2 -1
  2. package/dist/api.js +1 -1
  3. package/dist/app/admin/SystemView.svelte +149 -11
  4. package/dist/documents/backends.d.ts +8 -0
  5. package/dist/documents/backends.js +87 -0
  6. package/dist/documents/backends.test.d.ts +1 -0
  7. package/dist/documents/backends.test.js +33 -0
  8. package/dist/documents/browse.d.ts +12 -0
  9. package/dist/documents/browse.js +19 -0
  10. package/dist/documents/browse.test.d.ts +1 -0
  11. package/dist/documents/browse.test.js +41 -0
  12. package/dist/documents/http-backend.d.ts +4 -0
  13. package/dist/documents/http-backend.js +14 -0
  14. package/dist/documents/sync/index.d.ts +1 -2
  15. package/dist/documents/sync/index.js +0 -2
  16. package/dist/documents/sync/observer.d.ts +3 -0
  17. package/dist/documents/sync/observer.js +45 -0
  18. package/dist/documents/sync/registry.d.ts +3 -0
  19. package/dist/documents/sync/registry.js +8 -1
  20. package/dist/documents/sync/registry.test.js +11 -0
  21. package/dist/documents/types.d.ts +18 -0
  22. package/dist/documents/types.js +6 -1
  23. package/dist/layout/inspection.d.ts +17 -0
  24. package/dist/layout/inspection.js +53 -0
  25. package/dist/shards/activate-browse.test.d.ts +1 -0
  26. package/dist/shards/activate-browse.test.js +36 -0
  27. package/dist/shards/activate-sync-registry.test.d.ts +1 -0
  28. package/dist/shards/activate-sync-registry.test.js +42 -0
  29. package/dist/shards/activate-tenantid.test.d.ts +1 -0
  30. package/dist/shards/activate-tenantid.test.js +21 -0
  31. package/dist/shards/activate.svelte.d.ts +12 -0
  32. package/dist/shards/activate.svelte.js +33 -3
  33. package/dist/shards/types.d.ts +33 -0
  34. package/dist/shell-shard/manifest.js +1 -1
  35. package/dist/shell-shard/shellShard.svelte.js +52 -4
  36. package/dist/shell-shard/verbs/index.js +3 -1
  37. package/dist/shell-shard/verbs/views.d.ts +2 -0
  38. package/dist/shell-shard/verbs/views.js +103 -2
  39. package/dist/verbs/types.d.ts +19 -0
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. 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
- sync: ((_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_SYNC))
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 ((_c = shard.autostart) === null || _c === void 0 ? void 0 : _c.call(shard, ctx));
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)
@@ -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) returns boolean
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
- const found = focusView(viewId);
83
- return found ? { ok: true } : { ok: false, error: `view "${viewId}" not found in current layout` };
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,4 +1,6 @@
1
1
  import type { Verb } from '../../verbs/types';
2
2
  export declare const viewsVerb: Verb;
3
3
  export declare const openVerb: Verb;
4
+ export declare const popoutVerb: Verb;
5
+ export declare const dockVerb: Verb;
4
6
  export declare const closeVerb: Verb;
@@ -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 in the active layout.',
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.',
@@ -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.0";
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.0';
2
+ export const VERSION = '0.8.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"