sh3-core 0.7.0 → 0.7.3
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/Shell.svelte +3 -2
- package/dist/api.d.ts +2 -0
- package/dist/api.js +1 -0
- package/dist/app/store/storeShard.svelte.js +5 -0
- package/dist/app/store/verbs.d.ts +4 -0
- package/dist/app/store/verbs.js +220 -0
- package/dist/contract.d.ts +10 -0
- package/dist/contract.js +10 -0
- package/dist/layout/inspection.js +58 -7
- package/dist/layout/ops.js +25 -6
- package/dist/layout/ops.test.js +51 -1
- package/dist/layout/slotHostPool.svelte.js +1 -1
- package/dist/layout/store.svelte.js +2 -2
- package/dist/overlays/FloatFrame.svelte +2 -0
- package/dist/shards/activate.svelte.js +9 -2
- package/dist/shards/registry.d.ts +5 -0
- package/dist/shards/registry.js +19 -3
- package/dist/shards/registry.test.d.ts +1 -0
- package/dist/shards/registry.test.js +62 -0
- package/dist/shards/types.d.ts +9 -0
- package/dist/shell-shard/Terminal.svelte +3 -4
- package/dist/shell-shard/registry.d.ts +2 -64
- package/dist/shell-shard/registry.js +9 -17
- package/dist/shell-shard/shellShard.svelte.js +2 -0
- package/dist/shell-shard/verbs/apps.d.ts +1 -1
- package/dist/shell-shard/verbs/clear.d.ts +1 -1
- package/dist/shell-shard/verbs/help.d.ts +2 -2
- package/dist/shell-shard/verbs/help.js +3 -2
- package/dist/shell-shard/verbs/history.d.ts +1 -1
- package/dist/shell-shard/verbs/index.d.ts +2 -2
- package/dist/shell-shard/verbs/index.js +18 -18
- package/dist/shell-shard/verbs/session.d.ts +1 -1
- package/dist/shell-shard/verbs/shards.d.ts +1 -1
- package/dist/shell-shard/verbs/views.d.ts +1 -1
- package/dist/shell-shard/verbs/zones.d.ts +1 -1
- package/dist/verbs/types.d.ts +62 -0
- package/dist/verbs/types.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/Shell.svelte
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import type { OverlayLayer } from './overlays/types';
|
|
21
21
|
import { registerLayerRoot, unregisterLayerRoot } from './overlays/roots';
|
|
22
22
|
import { bindFloatStore, unbindFloatStore, floatManager } from './overlays/float';
|
|
23
|
-
import { returnToHome, isAdmin } from './api';
|
|
23
|
+
import { returnToHome, isAdmin, focusView } from './api';
|
|
24
24
|
import { getActiveRoot, layoutStore } from './layout/store.svelte';
|
|
25
25
|
import { isAuthenticated, isLocalOwner, getUser, logout } from './auth/index';
|
|
26
26
|
import iconsUrl from './assets/icons.svg';
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
) return;
|
|
83
83
|
}
|
|
84
84
|
e.preventDefault();
|
|
85
|
-
|
|
85
|
+
if (!focusView('shell:terminal'))
|
|
86
|
+
floatManager.open('shell:terminal', { title: 'Shell' });
|
|
86
87
|
}
|
|
87
88
|
window.addEventListener('keydown', onKeyDown);
|
|
88
89
|
return () => window.removeEventListener('keydown', onKeyDown);
|
package/dist/api.d.ts
CHANGED
|
@@ -26,5 +26,7 @@ export declare const capabilities: {
|
|
|
26
26
|
readonly hotInstall: boolean;
|
|
27
27
|
};
|
|
28
28
|
export type { ServerShard, ServerShardContext } from './server-shard/types';
|
|
29
|
+
export type { Verb } from './verbs/types';
|
|
30
|
+
export { listVerbs } from './shards/registry';
|
|
29
31
|
export { VERSION } from './version';
|
|
30
32
|
export { setTokenOverrides, clearTokenOverrides, getTokenOverrides, } from './theme';
|
package/dist/api.js
CHANGED
|
@@ -44,6 +44,7 @@ export const capabilities = {
|
|
|
44
44
|
/** Whether this target supports hot-installing packages via dynamic import from blob URL. */
|
|
45
45
|
hotInstall: typeof Blob !== 'undefined' && typeof URL.createObjectURL === 'function',
|
|
46
46
|
};
|
|
47
|
+
export { listVerbs } from './shards/registry';
|
|
47
48
|
// Package version.
|
|
48
49
|
export { VERSION } from './version';
|
|
49
50
|
// Theme token override API (shell-level theming support).
|
|
@@ -19,6 +19,7 @@ import { installPackage, listInstalledPackages } from '../../registry/installer'
|
|
|
19
19
|
import { loadBundle, savePackage } from '../../registry/storage';
|
|
20
20
|
import { serverInstallPackage, fetchServerPackages } from '../../env/client';
|
|
21
21
|
import { VERSION } from '../../version';
|
|
22
|
+
import { installVerb, uninstallVerb, appinfoVerb } from './verbs';
|
|
22
23
|
/**
|
|
23
24
|
* Compare two semver-like version strings.
|
|
24
25
|
* Returns true only if `available` is strictly greater than `installed`.
|
|
@@ -213,6 +214,10 @@ export const storeShard = {
|
|
|
213
214
|
};
|
|
214
215
|
ctx.registerView('sh3-store:browse', browseFactory);
|
|
215
216
|
ctx.registerView('sh3-store:installed', installedFactory);
|
|
217
|
+
// Store verbs — registered as sh3-store:install, sh3-store:uninstall, sh3-store:appinfo
|
|
218
|
+
ctx.registerVerb(installVerb);
|
|
219
|
+
ctx.registerVerb(uninstallVerb);
|
|
220
|
+
ctx.registerVerb(appinfoVerb);
|
|
216
221
|
// refreshInstalled can run immediately (hits server, no env needed).
|
|
217
222
|
refreshInstalled();
|
|
218
223
|
},
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Store verbs — shell-accessible commands for the sh3-store shard.
|
|
3
|
+
*
|
|
4
|
+
* Registered during storeShard.activate() via ctx.registerVerb().
|
|
5
|
+
* Auto-prefixed to sh3-store:install, sh3-store:uninstall, sh3-store:appinfo.
|
|
6
|
+
*/
|
|
7
|
+
import { storeContext } from './storeShard.svelte';
|
|
8
|
+
import { fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
|
|
9
|
+
import { installPackage } from '../../registry/installer';
|
|
10
|
+
import { serverInstallPackage, serverUninstallPackage } from '../../env/client';
|
|
11
|
+
import { uninstallPackage } from '../../registry/installer';
|
|
12
|
+
function findInCatalog(id) {
|
|
13
|
+
return storeContext.state.ephemeral.catalog.find((p) => p.entry.id === id);
|
|
14
|
+
}
|
|
15
|
+
function findInstalled(id) {
|
|
16
|
+
return storeContext.state.ephemeral.installed.find((p) => p.id === id);
|
|
17
|
+
}
|
|
18
|
+
export const installVerb = {
|
|
19
|
+
name: 'install',
|
|
20
|
+
summary: 'Install a package by id from the catalog.',
|
|
21
|
+
async run(ctx, args) {
|
|
22
|
+
var _a;
|
|
23
|
+
const id = args[0];
|
|
24
|
+
if (!id) {
|
|
25
|
+
ctx.scrollback.push({
|
|
26
|
+
kind: 'status',
|
|
27
|
+
text: 'usage: sh3-store:install <package-id>',
|
|
28
|
+
level: 'warn',
|
|
29
|
+
ts: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const existing = findInstalled(id);
|
|
34
|
+
if (existing) {
|
|
35
|
+
ctx.scrollback.push({
|
|
36
|
+
kind: 'status',
|
|
37
|
+
text: `${id} is already installed (v${existing.version})`,
|
|
38
|
+
level: 'warn',
|
|
39
|
+
ts: Date.now(),
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const pkg = findInCatalog(id);
|
|
44
|
+
if (!pkg) {
|
|
45
|
+
ctx.scrollback.push({
|
|
46
|
+
kind: 'status',
|
|
47
|
+
text: `package "${id}" not found in catalog — try refreshing the store`,
|
|
48
|
+
level: 'error',
|
|
49
|
+
ts: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
ctx.scrollback.push({
|
|
54
|
+
kind: 'status',
|
|
55
|
+
text: `installing ${id} v${pkg.latest.version}...`,
|
|
56
|
+
level: 'info',
|
|
57
|
+
ts: Date.now(),
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const bundle = await fetchBundle(pkg.latest, pkg.sourceRegistry);
|
|
61
|
+
const meta = buildPackageMeta(pkg, pkg.latest);
|
|
62
|
+
let serverBundle;
|
|
63
|
+
if (pkg.latest.serverBundleUrl) {
|
|
64
|
+
serverBundle = await fetchServerBundle(pkg.latest, pkg.sourceRegistry);
|
|
65
|
+
}
|
|
66
|
+
const manifest = {
|
|
67
|
+
id: meta.id,
|
|
68
|
+
type: meta.type,
|
|
69
|
+
label: pkg.entry.label,
|
|
70
|
+
version: meta.version,
|
|
71
|
+
contractVersion: meta.contractVersion,
|
|
72
|
+
sourceRegistry: meta.sourceRegistry,
|
|
73
|
+
installedAt: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
|
|
76
|
+
if (!serverResult.ok) {
|
|
77
|
+
ctx.scrollback.push({
|
|
78
|
+
kind: 'status',
|
|
79
|
+
text: `install failed: ${(_a = serverResult.error) !== null && _a !== void 0 ? _a : 'server error'}`,
|
|
80
|
+
level: 'error',
|
|
81
|
+
ts: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const result = await installPackage(bundle, meta);
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
ctx.scrollback.push({
|
|
88
|
+
kind: 'status',
|
|
89
|
+
text: `server ok but local hot-load failed: ${result.error}`,
|
|
90
|
+
level: 'warn',
|
|
91
|
+
ts: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
await storeContext.refreshInstalled();
|
|
95
|
+
ctx.scrollback.push({
|
|
96
|
+
kind: 'status',
|
|
97
|
+
text: `installed ${id} v${pkg.latest.version}`,
|
|
98
|
+
level: 'info',
|
|
99
|
+
ts: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
ctx.scrollback.push({
|
|
104
|
+
kind: 'status',
|
|
105
|
+
text: `install failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
106
|
+
level: 'error',
|
|
107
|
+
ts: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
export const uninstallVerb = {
|
|
113
|
+
name: 'uninstall',
|
|
114
|
+
summary: 'Uninstall an installed package by id.',
|
|
115
|
+
async run(ctx, args) {
|
|
116
|
+
const id = args[0];
|
|
117
|
+
if (!id) {
|
|
118
|
+
ctx.scrollback.push({
|
|
119
|
+
kind: 'status',
|
|
120
|
+
text: 'usage: sh3-store:uninstall <package-id>',
|
|
121
|
+
level: 'warn',
|
|
122
|
+
ts: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const existing = findInstalled(id);
|
|
127
|
+
if (!existing) {
|
|
128
|
+
ctx.scrollback.push({
|
|
129
|
+
kind: 'status',
|
|
130
|
+
text: `package "${id}" is not installed`,
|
|
131
|
+
level: 'error',
|
|
132
|
+
ts: Date.now(),
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
ctx.scrollback.push({
|
|
137
|
+
kind: 'status',
|
|
138
|
+
text: `uninstalling ${id}...`,
|
|
139
|
+
level: 'info',
|
|
140
|
+
ts: Date.now(),
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
await serverUninstallPackage(id);
|
|
144
|
+
await uninstallPackage(id);
|
|
145
|
+
await storeContext.refreshInstalled();
|
|
146
|
+
ctx.scrollback.push({
|
|
147
|
+
kind: 'status',
|
|
148
|
+
text: `uninstalled ${id}`,
|
|
149
|
+
level: 'info',
|
|
150
|
+
ts: Date.now(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
ctx.scrollback.push({
|
|
155
|
+
kind: 'status',
|
|
156
|
+
text: `uninstall failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
157
|
+
level: 'error',
|
|
158
|
+
ts: Date.now(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
export const appinfoVerb = {
|
|
164
|
+
name: 'appinfo',
|
|
165
|
+
summary: 'Show info about a package (installed status, version, catalog details).',
|
|
166
|
+
async run(ctx, args) {
|
|
167
|
+
const id = args[0];
|
|
168
|
+
if (!id) {
|
|
169
|
+
ctx.scrollback.push({
|
|
170
|
+
kind: 'status',
|
|
171
|
+
text: 'usage: sh3-store:appinfo <package-id>',
|
|
172
|
+
level: 'warn',
|
|
173
|
+
ts: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const installed = findInstalled(id);
|
|
178
|
+
const catalogEntry = findInCatalog(id);
|
|
179
|
+
if (!installed && !catalogEntry) {
|
|
180
|
+
ctx.scrollback.push({
|
|
181
|
+
kind: 'status',
|
|
182
|
+
text: `package "${id}" not found in catalog or installed list`,
|
|
183
|
+
level: 'error',
|
|
184
|
+
ts: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const lines = [];
|
|
189
|
+
lines.push(`--- ${id} ---`);
|
|
190
|
+
if (installed) {
|
|
191
|
+
lines.push(` installed: yes`);
|
|
192
|
+
lines.push(` version: ${installed.version}`);
|
|
193
|
+
lines.push(` type: ${installed.type}`);
|
|
194
|
+
lines.push(` contract: v${installed.contractVersion}`);
|
|
195
|
+
lines.push(` source: ${installed.sourceRegistry}`);
|
|
196
|
+
if (installed.installedAt) {
|
|
197
|
+
lines.push(` installed: ${installed.installedAt}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
lines.push(` installed: no`);
|
|
202
|
+
}
|
|
203
|
+
if (catalogEntry) {
|
|
204
|
+
lines.push(` catalog: yes`);
|
|
205
|
+
lines.push(` latest: ${catalogEntry.latest.version}`);
|
|
206
|
+
lines.push(` label: ${catalogEntry.entry.label}`);
|
|
207
|
+
lines.push(` author: ${catalogEntry.entry.author.name}`);
|
|
208
|
+
lines.push(` desc: ${catalogEntry.entry.description}`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
lines.push(` catalog: no (not in any configured registry)`);
|
|
212
|
+
}
|
|
213
|
+
ctx.scrollback.push({
|
|
214
|
+
kind: 'text',
|
|
215
|
+
stream: 'stdout',
|
|
216
|
+
chunks: [lines.join('\n') + '\n'],
|
|
217
|
+
ts: Date.now(),
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
package/dist/contract.d.ts
CHANGED
|
@@ -8,12 +8,22 @@ export declare const contract: {
|
|
|
8
8
|
* listed in shardImports or hostImports is illegal. */
|
|
9
9
|
readonly packagePrefix: "sh3-core";
|
|
10
10
|
readonly shard: {
|
|
11
|
+
/** Fields external authors must declare. */
|
|
12
|
+
readonly sourceRequiredFields: readonly ["id", "label", "views"];
|
|
13
|
+
/** Fields the framework stamps at load time — must NOT appear in source. */
|
|
14
|
+
readonly runtimeRequiredFields: readonly ["version"];
|
|
15
|
+
/** Union of both — the full runtime shape. Kept for backward compat. */
|
|
11
16
|
readonly requiredFields: readonly ["id", "label", "version", "views"];
|
|
12
17
|
readonly views: {
|
|
13
18
|
readonly requiredFields: readonly ["id", "label"];
|
|
14
19
|
};
|
|
15
20
|
};
|
|
16
21
|
readonly app: {
|
|
22
|
+
/** Fields external authors must declare. */
|
|
23
|
+
readonly sourceRequiredFields: readonly ["id", "label", "requiredShards", "layoutVersion"];
|
|
24
|
+
/** Fields the framework stamps at load time — must NOT appear in source. */
|
|
25
|
+
readonly runtimeRequiredFields: readonly ["version"];
|
|
26
|
+
/** Union of both — the full runtime shape. Kept for backward compat. */
|
|
17
27
|
readonly requiredFields: readonly ["id", "label", "version", "requiredShards", "layoutVersion"];
|
|
18
28
|
};
|
|
19
29
|
};
|
package/dist/contract.js
CHANGED
|
@@ -17,12 +17,22 @@ export const contract = {
|
|
|
17
17
|
* listed in shardImports or hostImports is illegal. */
|
|
18
18
|
packagePrefix: 'sh3-core',
|
|
19
19
|
shard: {
|
|
20
|
+
/** Fields external authors must declare. */
|
|
21
|
+
sourceRequiredFields: ['id', 'label', 'views'],
|
|
22
|
+
/** Fields the framework stamps at load time — must NOT appear in source. */
|
|
23
|
+
runtimeRequiredFields: ['version'],
|
|
24
|
+
/** Union of both — the full runtime shape. Kept for backward compat. */
|
|
20
25
|
requiredFields: ['id', 'label', 'version', 'views'],
|
|
21
26
|
views: {
|
|
22
27
|
requiredFields: ['id', 'label'],
|
|
23
28
|
},
|
|
24
29
|
},
|
|
25
30
|
app: {
|
|
31
|
+
/** Fields external authors must declare. */
|
|
32
|
+
sourceRequiredFields: ['id', 'label', 'requiredShards', 'layoutVersion'],
|
|
33
|
+
/** Fields the framework stamps at load time — must NOT appear in source. */
|
|
34
|
+
runtimeRequiredFields: ['version'],
|
|
35
|
+
/** Union of both — the full runtime shape. Kept for backward compat. */
|
|
26
36
|
requiredFields: ['id', 'label', 'version', 'requiredShards', 'layoutVersion'],
|
|
27
37
|
},
|
|
28
38
|
};
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { activeLayout, getActiveRoot } from './store.svelte';
|
|
16
16
|
import { nodeAtPath, findTabBySlotId, removeTabBySlotId, cleanupTree, splitNodeAtPath, } from './ops';
|
|
17
17
|
import { getSlotHandle } from './slotHostPool.svelte';
|
|
18
|
+
import { floatManager } from '../overlays/float';
|
|
18
19
|
/**
|
|
19
20
|
* Read-only snapshot of the currently-rendered layout tree. The return
|
|
20
21
|
* value is the live object — callers MUST NOT mutate it directly;
|
|
@@ -46,16 +47,20 @@ export function spliceIntoActiveLayout(entry) {
|
|
|
46
47
|
* layout. Returns `true` if a matching tab was found and activated.
|
|
47
48
|
*/
|
|
48
49
|
export function focusTab(slotId) {
|
|
49
|
-
const
|
|
50
|
-
|
|
50
|
+
const tree = activeLayout();
|
|
51
|
+
if (focusTabWhere(tree.docked, (entry) => entry.slotId === slotId))
|
|
52
|
+
return true;
|
|
53
|
+
return focusTabInFloats(tree, (entry) => entry.slotId === slotId);
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Activate the first tab whose `viewId` matches in the currently-rendered
|
|
54
57
|
* layout. Returns `true` if a matching tab was found and activated.
|
|
55
58
|
*/
|
|
56
59
|
export function focusView(viewId) {
|
|
57
|
-
const
|
|
58
|
-
|
|
60
|
+
const tree = activeLayout();
|
|
61
|
+
if (focusTabWhere(tree.docked, (entry) => entry.viewId === viewId))
|
|
62
|
+
return true;
|
|
63
|
+
return focusTabInFloats(tree, (entry) => entry.viewId === viewId);
|
|
59
64
|
}
|
|
60
65
|
/** Walk the tree looking for a tab entry that satisfies `pred`, activate it. */
|
|
61
66
|
function focusTabWhere(node, pred) {
|
|
@@ -75,6 +80,16 @@ function focusTabWhere(node, pred) {
|
|
|
75
80
|
}
|
|
76
81
|
return false;
|
|
77
82
|
}
|
|
83
|
+
/** Search floats for a matching tab; activate it and raise the float. */
|
|
84
|
+
function focusTabInFloats(tree, pred) {
|
|
85
|
+
for (const floatEntry of tree.floats) {
|
|
86
|
+
if (focusTabWhere(floatEntry.content, pred)) {
|
|
87
|
+
floatManager.focus(floatEntry.id);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
78
93
|
/**
|
|
79
94
|
* Collapse a child of a split node at the given path. Returns true if
|
|
80
95
|
* the split was found and the child was collapsed.
|
|
@@ -115,10 +130,13 @@ function setCollapsed(splitPath, childIndex, value) {
|
|
|
115
130
|
* the sole authority on tree mutations.
|
|
116
131
|
*/
|
|
117
132
|
export async function closeTab(slotId) {
|
|
118
|
-
const
|
|
133
|
+
const tree = activeLayout();
|
|
134
|
+
const root = tree.docked;
|
|
119
135
|
const located = findTabBySlotId(root, slotId);
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
// Not found in docked tree — check floats.
|
|
137
|
+
if (!located) {
|
|
138
|
+
return closeFloatTab(tree, slotId);
|
|
139
|
+
}
|
|
122
140
|
const handle = getSlotHandle(slotId);
|
|
123
141
|
const closable = handle === null || handle === void 0 ? void 0 : handle.closable;
|
|
124
142
|
// Non-closable: no action.
|
|
@@ -143,6 +161,39 @@ export async function closeTab(slotId) {
|
|
|
143
161
|
cleanupTree(root);
|
|
144
162
|
return true;
|
|
145
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Close a tab that lives inside a float entry. Float tabs are
|
|
166
|
+
* auto-closable (views gain closability when mounted in a float) but
|
|
167
|
+
* guarded canClose() on the view handle is still respected.
|
|
168
|
+
* Closing the last tab in a float removes the entire float.
|
|
169
|
+
*/
|
|
170
|
+
async function closeFloatTab(tree, slotId) {
|
|
171
|
+
for (const entry of tree.floats) {
|
|
172
|
+
const located = findTabBySlotId(entry.content, slotId);
|
|
173
|
+
if (!located)
|
|
174
|
+
continue;
|
|
175
|
+
// Respect guarded canClose() if the view declared one.
|
|
176
|
+
const handle = getSlotHandle(slotId);
|
|
177
|
+
const closable = handle === null || handle === void 0 ? void 0 : handle.closable;
|
|
178
|
+
if (typeof closable === 'object') {
|
|
179
|
+
const allowed = await closable.canClose();
|
|
180
|
+
if (!allowed)
|
|
181
|
+
return false;
|
|
182
|
+
// Re-verify after async gap.
|
|
183
|
+
if (!findTabBySlotId(entry.content, slotId))
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// Remove the tab from the float's content tree.
|
|
187
|
+
removeTabBySlotId(entry.content, slotId);
|
|
188
|
+
// If the float's content is now empty, remove the entire float.
|
|
189
|
+
const tabs = entry.content.type === 'tabs' ? entry.content : null;
|
|
190
|
+
if (!tabs || tabs.tabs.length === 0) {
|
|
191
|
+
floatManager.close(entry.id);
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
146
197
|
function findFirstTabsNode(node) {
|
|
147
198
|
if (node.type === 'tabs')
|
|
148
199
|
return node;
|
package/dist/layout/ops.js
CHANGED
|
@@ -195,6 +195,24 @@ export function makeSplitWithNewTab(existing, entry, side) {
|
|
|
195
195
|
children,
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Shallow-clone a layout node. Used by splitNodeAtPath's root case to
|
|
200
|
+
* break the circular reference that would occur if the root object
|
|
201
|
+
* (mutated in-place) were embedded as its own child.
|
|
202
|
+
*/
|
|
203
|
+
function snapshotNode(node) {
|
|
204
|
+
if (node.type === 'tabs')
|
|
205
|
+
return Object.assign(Object.assign({}, node), { tabs: [...node.tabs] });
|
|
206
|
+
if (node.type === 'split') {
|
|
207
|
+
const clone = Object.assign(Object.assign({}, node), { children: [...node.children], sizes: [...node.sizes] });
|
|
208
|
+
if (node.pinned)
|
|
209
|
+
clone.pinned = [...node.pinned];
|
|
210
|
+
if (node.collapsed)
|
|
211
|
+
clone.collapsed = [...node.collapsed];
|
|
212
|
+
return clone;
|
|
213
|
+
}
|
|
214
|
+
return Object.assign({}, node); // slot — plain shallow copy
|
|
215
|
+
}
|
|
198
216
|
/**
|
|
199
217
|
* Apply a slot-split as a tree mutation: find the target node at the
|
|
200
218
|
* given path and replace it with a new split. Handles the root case
|
|
@@ -205,14 +223,13 @@ export function splitNodeAtPath(root, path, entry, side) {
|
|
|
205
223
|
const target = nodeAtPath(root, path);
|
|
206
224
|
if (!target)
|
|
207
225
|
return;
|
|
208
|
-
const replacement = makeSplitWithNewTab(target, entry, side);
|
|
209
226
|
if (path.length === 0) {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
227
|
+
// Root case: target IS root. Snapshot it so makeSplitWithNewTab
|
|
228
|
+
// embeds the clone, not root itself — avoids a circular reference
|
|
229
|
+
// when we Object.assign the replacement back onto root.
|
|
230
|
+
const snapshot = snapshotNode(target);
|
|
231
|
+
const replacement = makeSplitWithNewTab(snapshot, entry, side);
|
|
214
232
|
const rootAsRecord = root;
|
|
215
|
-
// Clear stale keys first so Object.assign doesn't leave a hybrid.
|
|
216
233
|
delete rootAsRecord.tabs;
|
|
217
234
|
delete rootAsRecord.activeTab;
|
|
218
235
|
delete rootAsRecord.slotId;
|
|
@@ -220,10 +237,12 @@ export function splitNodeAtPath(root, path, entry, side) {
|
|
|
220
237
|
delete rootAsRecord.direction;
|
|
221
238
|
delete rootAsRecord.sizes;
|
|
222
239
|
delete rootAsRecord.pinned;
|
|
240
|
+
delete rootAsRecord.collapsed;
|
|
223
241
|
delete rootAsRecord.children;
|
|
224
242
|
Object.assign(rootAsRecord, replacement);
|
|
225
243
|
return;
|
|
226
244
|
}
|
|
245
|
+
const replacement = makeSplitWithNewTab(target, entry, side);
|
|
227
246
|
const parentPath = path.slice(0, -1);
|
|
228
247
|
const indexInParent = path[path.length - 1];
|
|
229
248
|
const parent = nodeAtPath(root, parentPath);
|
package/dist/layout/ops.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { findTabInTree } from './ops';
|
|
2
|
+
import { findTabInTree, splitNodeAtPath, cleanupTree, findTabBySlotId } from './ops';
|
|
3
3
|
describe('findTabInTree', () => {
|
|
4
4
|
const tree = {
|
|
5
5
|
docked: {
|
|
@@ -34,3 +34,53 @@ describe('findTabInTree', () => {
|
|
|
34
34
|
expect(findTabInTree(tree, 'nonexistent')).toBeNull();
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
|
+
describe('splitNodeAtPath — root case (path = [])', () => {
|
|
38
|
+
it('does not create a circular reference when splitting the root', () => {
|
|
39
|
+
// splitNodeAtPath mutates root in-place (tabs → split). Cast to
|
|
40
|
+
// LayoutNode so TS doesn't narrow from the initializer.
|
|
41
|
+
const root = {
|
|
42
|
+
type: 'tabs',
|
|
43
|
+
tabs: [{ slotId: 's1', viewId: 'v1', label: 'One' }],
|
|
44
|
+
activeTab: 0,
|
|
45
|
+
};
|
|
46
|
+
const entry = { slotId: 's2', viewId: 'v2', label: 'Two' };
|
|
47
|
+
splitNodeAtPath(root, [], entry, 'right');
|
|
48
|
+
// After split, root should be a split node with two children.
|
|
49
|
+
// Neither child should be root itself (no circular ref).
|
|
50
|
+
expect(root.type).toBe('split');
|
|
51
|
+
if (root.type === 'split') {
|
|
52
|
+
for (const child of root.children) {
|
|
53
|
+
expect(child).not.toBe(root);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it('cleanupTree does not infinite-loop after splitting the root', () => {
|
|
58
|
+
const root = {
|
|
59
|
+
type: 'tabs',
|
|
60
|
+
tabs: [{ slotId: 's1', viewId: 'v1', label: 'One' }],
|
|
61
|
+
activeTab: 0,
|
|
62
|
+
};
|
|
63
|
+
const entry = { slotId: 's2', viewId: 'v2', label: 'Two' };
|
|
64
|
+
splitNodeAtPath(root, [], entry, 'right');
|
|
65
|
+
// This must terminate. Before the fix it infinite-loops.
|
|
66
|
+
cleanupTree(root);
|
|
67
|
+
// Both tabs should still be findable.
|
|
68
|
+
expect(findTabBySlotId(root, 's1')).not.toBeNull();
|
|
69
|
+
expect(findTabBySlotId(root, 's2')).not.toBeNull();
|
|
70
|
+
});
|
|
71
|
+
it('works when root is a slot leaf', () => {
|
|
72
|
+
const root = {
|
|
73
|
+
type: 'slot',
|
|
74
|
+
slotId: 'leaf',
|
|
75
|
+
viewId: 'v',
|
|
76
|
+
};
|
|
77
|
+
const entry = { slotId: 's2', viewId: 'v2', label: 'Two' };
|
|
78
|
+
splitNodeAtPath(root, [], entry, 'bottom');
|
|
79
|
+
expect(root.type).toBe('split');
|
|
80
|
+
if (root.type === 'split') {
|
|
81
|
+
for (const child of root.children) {
|
|
82
|
+
expect(child).not.toBe(root);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -113,7 +113,7 @@ function createHost(slotId, viewId, label) {
|
|
|
113
113
|
},
|
|
114
114
|
};
|
|
115
115
|
entry.handle = factory === null || factory === void 0 ? void 0 : factory.mount(host, ctx);
|
|
116
|
-
if ((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) {
|
|
116
|
+
if (((_a = entry.handle) === null || _a === void 0 ? void 0 : _a.closable) || slotId.startsWith('float:')) {
|
|
117
117
|
closableState[slotId] = true;
|
|
118
118
|
}
|
|
119
119
|
// The pool owns the ResizeObserver so its lifetime matches the
|
|
@@ -50,10 +50,10 @@ const HOME_LAYOUT = {
|
|
|
50
50
|
slotId: 'sh3core.home',
|
|
51
51
|
viewId: 'sh3core:home',
|
|
52
52
|
};
|
|
53
|
-
const HOME_TREE = {
|
|
53
|
+
const HOME_TREE = $state({
|
|
54
54
|
docked: HOME_LAYOUT,
|
|
55
55
|
floats: [],
|
|
56
|
-
};
|
|
56
|
+
});
|
|
57
57
|
let appEntry = $state(null);
|
|
58
58
|
let activeRoot = $state('home');
|
|
59
59
|
// ---------- read-side adapter helpers -------------------------------------
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
function onHeaderPointerDown(e: PointerEvent): void {
|
|
32
32
|
if (e.button !== 0) return;
|
|
33
|
+
if ((e.target as HTMLElement).closest('.sh3-float-close')) return;
|
|
33
34
|
const target = e.currentTarget as HTMLElement;
|
|
34
35
|
target.setPointerCapture(e.pointerId);
|
|
35
36
|
dragging = true;
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
</script>
|
|
64
65
|
|
|
65
66
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
67
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
66
68
|
<div
|
|
67
69
|
class="sh3-float-frame"
|
|
68
70
|
style:left="{entry.position.x}px"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* stays in `registeredShards` — it's still known, just not running.
|
|
18
18
|
*/
|
|
19
19
|
import { shell } from '../shellRuntime.svelte';
|
|
20
|
-
import { registerView, unregisterView } from './registry';
|
|
20
|
+
import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb } from './registry';
|
|
21
21
|
import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
|
|
22
22
|
import { fetchEnvState, putEnvState } from '../env/client';
|
|
23
23
|
import { isAdmin as checkIsAdmin } from '../auth/index';
|
|
@@ -75,7 +75,7 @@ export async function activateShard(id) {
|
|
|
75
75
|
// and is now being required by an app). Idempotent — no error.
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
const entry = { shard, ctx: undefined, viewIds: new Set(), cleanupFns: [] };
|
|
78
|
+
const entry = { shard, ctx: undefined, viewIds: new Set(), verbNames: new Set(), cleanupFns: [] };
|
|
79
79
|
// envState holds the reactive env data for this shard.
|
|
80
80
|
// Must be declared with $state at variable declaration time (Svelte 5 rule).
|
|
81
81
|
const envState = $state({
|
|
@@ -88,6 +88,11 @@ export async function activateShard(id) {
|
|
|
88
88
|
registerView(viewId, factory);
|
|
89
89
|
entry.viewIds.add(viewId);
|
|
90
90
|
},
|
|
91
|
+
registerVerb: (verb) => {
|
|
92
|
+
const prefixed = id === 'shell' ? verb.name : `${id}:${verb.name}`;
|
|
93
|
+
fwRegisterVerb(prefixed, Object.assign(Object.assign({}, verb), { name: prefixed }));
|
|
94
|
+
entry.verbNames.add(prefixed);
|
|
95
|
+
},
|
|
91
96
|
documents: (options) => {
|
|
92
97
|
const handle = createDocumentHandle(getTenantId(), id, getDocumentBackend(), options);
|
|
93
98
|
entry.cleanupFns.push(() => handle.dispose());
|
|
@@ -163,6 +168,8 @@ export function deactivateShard(id) {
|
|
|
163
168
|
// Flush and dispose document handles before tearing down views.
|
|
164
169
|
for (const fn of entry.cleanupFns)
|
|
165
170
|
void fn();
|
|
171
|
+
for (const name of entry.verbNames)
|
|
172
|
+
fwUnregisterVerb(name);
|
|
166
173
|
for (const viewId of entry.viewIds)
|
|
167
174
|
unregisterView(viewId);
|
|
168
175
|
active.delete(id);
|
|
@@ -2,3 +2,8 @@ import type { ViewFactory } from './types';
|
|
|
2
2
|
export declare function registerView(viewId: string, factory: ViewFactory): void;
|
|
3
3
|
export declare function getView(viewId: string): ViewFactory | undefined;
|
|
4
4
|
export declare function unregisterView(viewId: string): void;
|
|
5
|
+
import type { Verb } from '../verbs/types';
|
|
6
|
+
export declare function registerVerb(name: string, verb: Verb): void;
|
|
7
|
+
export declare function getVerb(name: string): Verb | undefined;
|
|
8
|
+
export declare function unregisterVerb(name: string): void;
|
|
9
|
+
export declare function listVerbs(): Verb[];
|
package/dist/shards/registry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Contribution registry —
|
|
2
|
+
* Contribution registry — views and verbs.
|
|
3
3
|
*
|
|
4
4
|
* Tracks which ViewFactory answers a given viewId. In this phase the
|
|
5
5
|
* registry is a flat module-level Map with no awareness of shard identity;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* The shape of this registry is deliberately narrow so later phases can
|
|
11
11
|
* expand it without breaking callers:
|
|
12
12
|
* - Resolution by viewId is the only query slots need.
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
13
|
+
* - Verbs are the second contribution kind (SH11). Toolbar items, menus,
|
|
14
|
+
* hotkeys get their own sibling maps when those kinds land.
|
|
15
15
|
*/
|
|
16
16
|
const views = new Map();
|
|
17
17
|
export function registerView(viewId, factory) {
|
|
@@ -26,3 +26,19 @@ export function getView(viewId) {
|
|
|
26
26
|
export function unregisterView(viewId) {
|
|
27
27
|
views.delete(viewId);
|
|
28
28
|
}
|
|
29
|
+
const verbs = new Map();
|
|
30
|
+
export function registerVerb(name, verb) {
|
|
31
|
+
if (verbs.has(name)) {
|
|
32
|
+
throw new Error(`Verb "${name}" is already registered`);
|
|
33
|
+
}
|
|
34
|
+
verbs.set(name, verb);
|
|
35
|
+
}
|
|
36
|
+
export function getVerb(name) {
|
|
37
|
+
return verbs.get(name);
|
|
38
|
+
}
|
|
39
|
+
export function unregisterVerb(name) {
|
|
40
|
+
verbs.delete(name);
|
|
41
|
+
}
|
|
42
|
+
export function listVerbs() {
|
|
43
|
+
return Array.from(verbs.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { registerVerb, getVerb, unregisterVerb, listVerbs, } from './registry';
|
|
3
|
+
function makeStubVerb(name) {
|
|
4
|
+
return { name, summary: `stub ${name}`, run: async () => { } };
|
|
5
|
+
}
|
|
6
|
+
describe('verb registry', () => {
|
|
7
|
+
const registered = [];
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
for (const name of registered)
|
|
10
|
+
unregisterVerb(name);
|
|
11
|
+
registered.length = 0;
|
|
12
|
+
});
|
|
13
|
+
function trackVerb(name, verb) {
|
|
14
|
+
registerVerb(name, verb);
|
|
15
|
+
registered.push(name);
|
|
16
|
+
}
|
|
17
|
+
it('registers and retrieves a verb by name', () => {
|
|
18
|
+
const verb = makeStubVerb('foo');
|
|
19
|
+
trackVerb('foo', verb);
|
|
20
|
+
expect(getVerb('foo')).toBe(verb);
|
|
21
|
+
});
|
|
22
|
+
it('returns undefined for unknown verb', () => {
|
|
23
|
+
expect(getVerb('nope')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('throws on duplicate verb name', () => {
|
|
26
|
+
trackVerb('dup', makeStubVerb('dup'));
|
|
27
|
+
expect(() => trackVerb('dup', makeStubVerb('dup'))).toThrowError('Verb "dup" is already registered');
|
|
28
|
+
});
|
|
29
|
+
it('unregisters a verb', () => {
|
|
30
|
+
trackVerb('gone', makeStubVerb('gone'));
|
|
31
|
+
unregisterVerb('gone');
|
|
32
|
+
registered.pop();
|
|
33
|
+
expect(getVerb('gone')).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it('lists verbs sorted by name', () => {
|
|
36
|
+
trackVerb('zeta', makeStubVerb('zeta'));
|
|
37
|
+
trackVerb('alpha', makeStubVerb('alpha'));
|
|
38
|
+
trackVerb('mid', makeStubVerb('mid'));
|
|
39
|
+
const names = listVerbs().map((v) => v.name);
|
|
40
|
+
expect(names).toEqual(['alpha', 'mid', 'zeta']);
|
|
41
|
+
});
|
|
42
|
+
it('stores prefixed name inside verb object (mirrors activate auto-prefix)', () => {
|
|
43
|
+
// activate.svelte.ts does: { ...verb, name: prefixed }
|
|
44
|
+
const original = makeStubVerb('install');
|
|
45
|
+
const prefixed = Object.assign(Object.assign({}, original), { name: 'registry:install' });
|
|
46
|
+
trackVerb('registry:install', prefixed);
|
|
47
|
+
const found = getVerb('registry:install');
|
|
48
|
+
expect(found === null || found === void 0 ? void 0 : found.name).toBe('registry:install');
|
|
49
|
+
expect(found === null || found === void 0 ? void 0 : found.summary).toBe('stub install');
|
|
50
|
+
});
|
|
51
|
+
it('bulk unregister simulates deactivate cleanup', () => {
|
|
52
|
+
// activate.svelte.ts tracks verbNames and unregisters on deactivate
|
|
53
|
+
const names = ['registry:install', 'registry:search', 'registry:info'];
|
|
54
|
+
for (const name of names)
|
|
55
|
+
trackVerb(name, makeStubVerb(name));
|
|
56
|
+
expect(listVerbs()).toHaveLength(3);
|
|
57
|
+
for (const name of names)
|
|
58
|
+
unregisterVerb(name);
|
|
59
|
+
registered.length = 0; // already cleaned
|
|
60
|
+
expect(listVerbs()).toHaveLength(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import type { EnvState } from '../env/types';
|
|
5
|
+
import type { Verb } from '../verbs/types';
|
|
5
6
|
/**
|
|
6
7
|
* The object returned by `ViewFactory.mount`. The framework calls
|
|
7
8
|
* `unmount()` when the slot goes away, and `onResize(w, h)` whenever the
|
|
@@ -148,6 +149,14 @@ export interface ShardContext {
|
|
|
148
149
|
* @param factory - The adapter that mounts the view into a container element.
|
|
149
150
|
*/
|
|
150
151
|
registerView(viewId: string, factory: ViewFactory): void;
|
|
152
|
+
/**
|
|
153
|
+
* Register a verb that users can invoke from the shell terminal.
|
|
154
|
+
* The verb name is auto-prefixed with `shardId:` for non-shell shards.
|
|
155
|
+
* Automatically unregistered when the shard deactivates.
|
|
156
|
+
*
|
|
157
|
+
* @param verb - The verb definition (name, summary, run function).
|
|
158
|
+
*/
|
|
159
|
+
registerVerb(verb: Verb): void;
|
|
151
160
|
/** Obtain a file-oriented document handle scoped to this shard. */
|
|
152
161
|
documents(options: DocumentHandleOptions): DocumentHandle;
|
|
153
162
|
/**
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import InputLine from './InputLine.svelte';
|
|
6
6
|
import { SessionClient } from './session-client.svelte';
|
|
7
7
|
import { VerbRegistry, type ShellApi } from './registry';
|
|
8
|
-
import { registerV1Verbs } from './verbs';
|
|
9
8
|
import type { ServerMessage } from './protocol';
|
|
10
9
|
|
|
11
10
|
interface Props {
|
|
@@ -18,13 +17,13 @@
|
|
|
18
17
|
// wsUrl is a prop read at construction only. untrack prevents Svelte 5's
|
|
19
18
|
// "referenced outside a closure" warning; the URL never changes at runtime.
|
|
20
19
|
const session = untrack(() => new SessionClient(wsUrl));
|
|
21
|
-
const
|
|
22
|
-
registerV1Verbs(registry);
|
|
20
|
+
const resolver = new VerbRegistry();
|
|
23
21
|
|
|
24
22
|
let locked = $state(false);
|
|
25
23
|
|
|
26
24
|
async function dispatch(line: string): Promise<void> {
|
|
27
|
-
|
|
25
|
+
session.history.push(line);
|
|
26
|
+
const resolution = resolver.resolve(line);
|
|
28
27
|
if (resolution.kind === 'local') {
|
|
29
28
|
// Log locally-dispatched verbs for shared history
|
|
30
29
|
session.send({ t: 'history-log', line });
|
|
@@ -1,68 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export interface ShellApi {
|
|
4
|
-
listApps(): Array<{
|
|
5
|
-
id: string;
|
|
6
|
-
label: string;
|
|
7
|
-
}>;
|
|
8
|
-
getActiveApp(): {
|
|
9
|
-
id: string;
|
|
10
|
-
label: string;
|
|
11
|
-
} | null;
|
|
12
|
-
launchApp(id: string): void;
|
|
13
|
-
listShards(): Array<{
|
|
14
|
-
id: string;
|
|
15
|
-
label: string;
|
|
16
|
-
version: string;
|
|
17
|
-
}>;
|
|
18
|
-
listViewsInCurrentLayout(): Array<{
|
|
19
|
-
slotId: string;
|
|
20
|
-
viewId: string;
|
|
21
|
-
label: string;
|
|
22
|
-
}>;
|
|
23
|
-
openViewInCurrentLayout(viewId: string): {
|
|
24
|
-
ok: boolean;
|
|
25
|
-
error?: string;
|
|
26
|
-
};
|
|
27
|
-
closeSlot(slotId: string): {
|
|
28
|
-
ok: boolean;
|
|
29
|
-
error?: string;
|
|
30
|
-
};
|
|
31
|
-
listZones(shardId?: string): Array<{
|
|
32
|
-
shardId: string;
|
|
33
|
-
zones: string[];
|
|
34
|
-
}>;
|
|
35
|
-
readZone(shardId: string, zoneName: string): unknown;
|
|
36
|
-
whoAmI(): {
|
|
37
|
-
userId: string;
|
|
38
|
-
admin: boolean;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export interface VerbContext {
|
|
42
|
-
shell: ShellApi;
|
|
43
|
-
scrollback: Scrollback;
|
|
44
|
-
session: SessionClient;
|
|
45
|
-
cwd: string;
|
|
46
|
-
/** Invoke another registered verb programmatically (used by rich-entry clicks). */
|
|
47
|
-
dispatch(line: string): Promise<void>;
|
|
48
|
-
}
|
|
49
|
-
export interface Verb {
|
|
50
|
-
name: string;
|
|
51
|
-
summary: string;
|
|
52
|
-
run(ctx: VerbContext, args: string[]): Promise<void>;
|
|
53
|
-
}
|
|
54
|
-
export type Resolution = {
|
|
55
|
-
kind: 'local';
|
|
56
|
-
verb: Verb;
|
|
57
|
-
args: string[];
|
|
58
|
-
line: string;
|
|
59
|
-
} | {
|
|
60
|
-
kind: 'forward';
|
|
61
|
-
line: string;
|
|
62
|
-
};
|
|
1
|
+
import type { Verb, VerbContext, Resolution, ShellApi } from '../verbs/types';
|
|
2
|
+
export type { Verb, VerbContext, Resolution, ShellApi };
|
|
63
3
|
export declare class VerbRegistry {
|
|
64
|
-
private readonly verbs;
|
|
65
|
-
register(verb: Verb): void;
|
|
66
4
|
list(): Verb[];
|
|
67
5
|
get(name: string): Verb | undefined;
|
|
68
6
|
resolve(line: string): Resolution;
|
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Verb
|
|
2
|
+
* Verb resolution for shell-shard.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 3. Otherwise → { kind: 'forward', line } (unknown, ship to server)
|
|
9
|
-
*
|
|
10
|
-
* Future: other shards register verbs via a new contribution kind.
|
|
4
|
+
* The VerbRegistry class owns resolution logic (escape hatch, token
|
|
5
|
+
* lookup, forward-to-server). Verb storage is the framework's job —
|
|
6
|
+
* see shards/registry.ts. This class reads from the framework map
|
|
7
|
+
* via getVerb() and listVerbs().
|
|
11
8
|
*/
|
|
9
|
+
import { getVerb, listVerbs } from '../shards/registry';
|
|
12
10
|
export class VerbRegistry {
|
|
13
|
-
constructor() {
|
|
14
|
-
this.verbs = new Map();
|
|
15
|
-
}
|
|
16
|
-
register(verb) {
|
|
17
|
-
this.verbs.set(verb.name, verb);
|
|
18
|
-
}
|
|
19
11
|
list() {
|
|
20
|
-
return
|
|
12
|
+
return listVerbs();
|
|
21
13
|
}
|
|
22
14
|
get(name) {
|
|
23
|
-
return
|
|
15
|
+
return getVerb(name);
|
|
24
16
|
}
|
|
25
17
|
resolve(line) {
|
|
26
18
|
const trimmed = line.trim();
|
|
@@ -37,7 +29,7 @@ export class VerbRegistry {
|
|
|
37
29
|
const space = trimmed.indexOf(' ');
|
|
38
30
|
const head = space === -1 ? trimmed : trimmed.slice(0, space);
|
|
39
31
|
const rest = space === -1 ? '' : trimmed.slice(space + 1);
|
|
40
|
-
const verb =
|
|
32
|
+
const verb = getVerb(head);
|
|
41
33
|
if (!verb)
|
|
42
34
|
return { kind: 'forward', line };
|
|
43
35
|
// Simple space-split for args — verbs can re-tokenize if they need quoting
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { mount, unmount } from 'svelte';
|
|
16
16
|
import { manifest } from './manifest';
|
|
17
17
|
import Terminal from './Terminal.svelte';
|
|
18
|
+
import { registerV1Verbs } from './verbs';
|
|
18
19
|
import { listRegisteredApps, getActiveApp } from '../apps/registry.svelte';
|
|
19
20
|
import { launchApp } from '../apps/lifecycle';
|
|
20
21
|
import { registeredShards } from '../shards/activate.svelte';
|
|
@@ -114,6 +115,7 @@ export const shellShard = {
|
|
|
114
115
|
// Non-admin: don't expose the view. Nothing to register.
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
118
|
+
registerV1Verbs(ctx);
|
|
117
119
|
const shell = makeShellApi(ctx);
|
|
118
120
|
const factory = {
|
|
119
121
|
mount(container, _context) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Verb } from '
|
|
1
|
+
import type { Verb } from '../../verbs/types';
|
|
2
2
|
export declare const clearVerb: Verb;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Verb
|
|
2
|
-
export declare function makeHelpVerb(
|
|
1
|
+
import type { Verb } from '../../verbs/types';
|
|
2
|
+
export declare function makeHelpVerb(): Verb;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { listVerbs } from '../../shards/registry';
|
|
1
2
|
import HelpTable from '../rich/HelpTable.svelte';
|
|
2
|
-
export function makeHelpVerb(
|
|
3
|
+
export function makeHelpVerb() {
|
|
3
4
|
return {
|
|
4
5
|
name: 'help',
|
|
5
6
|
summary: 'List verbs or show detail for one.',
|
|
6
7
|
async run(ctx) {
|
|
7
|
-
const rows =
|
|
8
|
+
const rows = listVerbs().map((v) => ({ name: v.name, summary: v.summary }));
|
|
8
9
|
ctx.scrollback.push({
|
|
9
10
|
kind: 'rich',
|
|
10
11
|
component: HelpTable,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Verb } from '
|
|
1
|
+
import type { Verb } from '../../verbs/types';
|
|
2
2
|
export declare const historyVerb: Verb;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function registerV1Verbs(
|
|
1
|
+
import type { ShardContext } from '../../shards/types';
|
|
2
|
+
export declare function registerV1Verbs(ctx: ShardContext): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Bundle of v1 verbs for shell-shard. Call registerV1Verbs(
|
|
3
|
-
*
|
|
2
|
+
* Bundle of v1 verbs for shell-shard. Call registerV1Verbs(ctx) once
|
|
3
|
+
* during shell-shard activate() to populate the framework verb registry.
|
|
4
4
|
*/
|
|
5
5
|
import { makeHelpVerb } from './help';
|
|
6
6
|
import { clearVerb } from './clear';
|
|
@@ -10,20 +10,20 @@ import { shardsVerb } from './shards';
|
|
|
10
10
|
import { viewsVerb, openVerb, closeVerb } from './views';
|
|
11
11
|
import { zonesVerb, zoneVerb } from './zones';
|
|
12
12
|
import { pwdVerb, cdVerb, envVerb, whoamiVerb } from './session';
|
|
13
|
-
export function registerV1Verbs(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
export function registerV1Verbs(ctx) {
|
|
14
|
+
ctx.registerVerb(makeHelpVerb());
|
|
15
|
+
ctx.registerVerb(clearVerb);
|
|
16
|
+
ctx.registerVerb(historyVerb);
|
|
17
|
+
ctx.registerVerb(appsVerb);
|
|
18
|
+
ctx.registerVerb(appVerb);
|
|
19
|
+
ctx.registerVerb(shardsVerb);
|
|
20
|
+
ctx.registerVerb(viewsVerb);
|
|
21
|
+
ctx.registerVerb(openVerb);
|
|
22
|
+
ctx.registerVerb(closeVerb);
|
|
23
|
+
ctx.registerVerb(zonesVerb);
|
|
24
|
+
ctx.registerVerb(zoneVerb);
|
|
25
|
+
ctx.registerVerb(pwdVerb);
|
|
26
|
+
ctx.registerVerb(cdVerb);
|
|
27
|
+
ctx.registerVerb(envVerb);
|
|
28
|
+
ctx.registerVerb(whoamiVerb);
|
|
29
29
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { Verb } from '
|
|
1
|
+
import type { Verb } from '../../verbs/types';
|
|
2
2
|
export declare const shardsVerb: Verb;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Scrollback } from '../shell-shard/scrollback.svelte';
|
|
2
|
+
import type { SessionClient } from '../shell-shard/session-client.svelte';
|
|
3
|
+
export interface ShellApi {
|
|
4
|
+
listApps(): Array<{
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}>;
|
|
8
|
+
getActiveApp(): {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
} | null;
|
|
12
|
+
launchApp(id: string): void;
|
|
13
|
+
listShards(): Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
version: string;
|
|
17
|
+
}>;
|
|
18
|
+
listViewsInCurrentLayout(): Array<{
|
|
19
|
+
slotId: string;
|
|
20
|
+
viewId: string;
|
|
21
|
+
label: string;
|
|
22
|
+
}>;
|
|
23
|
+
openViewInCurrentLayout(viewId: string): {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
};
|
|
27
|
+
closeSlot(slotId: string): {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
};
|
|
31
|
+
listZones(shardId?: string): Array<{
|
|
32
|
+
shardId: string;
|
|
33
|
+
zones: string[];
|
|
34
|
+
}>;
|
|
35
|
+
readZone(shardId: string, zoneName: string): unknown;
|
|
36
|
+
whoAmI(): {
|
|
37
|
+
userId: string;
|
|
38
|
+
admin: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface VerbContext {
|
|
42
|
+
shell: ShellApi;
|
|
43
|
+
scrollback: Scrollback;
|
|
44
|
+
session: SessionClient;
|
|
45
|
+
cwd: string;
|
|
46
|
+
/** Invoke another registered verb programmatically (used by rich-entry clicks). */
|
|
47
|
+
dispatch(line: string): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export interface Verb {
|
|
50
|
+
name: string;
|
|
51
|
+
summary: string;
|
|
52
|
+
run(ctx: VerbContext, args: string[]): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export type Resolution = {
|
|
55
|
+
kind: 'local';
|
|
56
|
+
verb: Verb;
|
|
57
|
+
args: string[];
|
|
58
|
+
line: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'forward';
|
|
61
|
+
line: string;
|
|
62
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Verb types — framework-level contribution kind.
|
|
3
|
+
*
|
|
4
|
+
* Shared between the framework verb registry (shards/registry.ts) and
|
|
5
|
+
* shell-shard's resolution logic (shell-shard/registry.ts). ShellApi
|
|
6
|
+
* stays in shell-shard — it's the Terminal's bridge to host actions.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
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.7.
|
|
2
|
+
export declare const VERSION = "0.7.3";
|
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.7.
|
|
2
|
+
export const VERSION = '0.7.3';
|