sh3-core 0.15.0 → 0.15.2
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/actions/ctx-actions.svelte.test.js +111 -0
- package/dist/actions/dispatcher.svelte.js +23 -2
- package/dist/actions/dispatcher.test.js +33 -0
- package/dist/actions/listActionsFromEntries.test.js +78 -0
- package/dist/actions/listActive.d.ts +2 -1
- package/dist/actions/listActive.js +43 -17
- package/dist/actions/listeners.d.ts +16 -0
- package/dist/actions/listeners.js +68 -14
- package/dist/actions/programmatic-dispatch.svelte.test.d.ts +1 -0
- package/dist/actions/programmatic-dispatch.svelte.test.js +98 -0
- package/dist/actions/types.d.ts +37 -0
- package/dist/api.d.ts +1 -1
- package/dist/app/store/verbs.js +4 -0
- package/dist/app-appearance/appearanceShard.svelte.js +19 -6
- package/dist/app-appearance/appearanceState.svelte.js +3 -3
- package/dist/host.js +2 -1
- package/dist/layouts-shard/LayoutSaveModal.svelte +145 -0
- package/dist/layouts-shard/LayoutSaveModal.svelte.d.ts +12 -0
- package/dist/layouts-shard/LayoutsSection.svelte +142 -0
- package/dist/layouts-shard/LayoutsSection.svelte.d.ts +3 -0
- package/dist/layouts-shard/filter.d.ts +3 -0
- package/dist/layouts-shard/filter.js +66 -0
- package/dist/layouts-shard/filter.test.d.ts +1 -0
- package/dist/layouts-shard/filter.test.js +123 -0
- package/dist/layouts-shard/index.d.ts +1 -0
- package/dist/layouts-shard/index.js +1 -0
- package/dist/layouts-shard/layoutsApi.d.ts +12 -0
- package/dist/layouts-shard/layoutsApi.js +41 -0
- package/dist/layouts-shard/layoutsApi.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsApi.test.js +74 -0
- package/dist/layouts-shard/layoutsShard.svelte.d.ts +11 -0
- package/dist/layouts-shard/layoutsShard.svelte.js +231 -0
- package/dist/layouts-shard/layoutsShard.svelte.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsShard.svelte.test.js +215 -0
- package/dist/layouts-shard/layoutsState.svelte.d.ts +9 -0
- package/dist/layouts-shard/layoutsState.svelte.js +50 -0
- package/dist/layouts-shard/layoutsState.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsState.test.js +43 -0
- package/dist/layouts-shard/types.d.ts +21 -0
- package/dist/layouts-shard/types.js +6 -0
- package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte} +36 -31
- package/dist/overlays/EntityAppearanceModal.svelte.d.ts +19 -0
- package/dist/overlays/EntityAppearanceModal.test.d.ts +1 -0
- package/dist/overlays/EntityAppearanceModal.test.js +57 -0
- package/dist/overlays/FloatFrame.svelte +149 -8
- package/dist/overlays/FloatFrame.svelte.d.ts +1 -1
- package/dist/overlays/FloatLayer.svelte +2 -2
- package/dist/overlays/float.d.ts +38 -1
- package/dist/overlays/float.js +82 -0
- package/dist/overlays/float.test.js +394 -0
- package/dist/overlays/floatMaximized.svelte.d.ts +4 -0
- package/dist/overlays/floatMaximized.svelte.js +30 -0
- package/dist/runtime/runVerb-shell.test.d.ts +1 -0
- package/dist/runtime/runVerb-shell.test.js +231 -0
- package/dist/sh3core-shard/ShellHome.svelte +3 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +7 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +23 -0
- package/dist/shards/activate-runtime.test.js +24 -2
- package/dist/shards/activate.svelte.js +18 -4
- package/dist/shards/types.d.ts +44 -4
- package/dist/shell-shard/CommandLine.svelte +143 -0
- package/dist/shell-shard/CommandLine.svelte.d.ts +26 -0
- package/dist/shell-shard/CommandLine.svelte.test.d.ts +1 -0
- package/dist/shell-shard/CommandLine.svelte.test.js +43 -0
- package/dist/shell-shard/InputLine.svelte +17 -40
- package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
- package/dist/shell-shard/ScrollbackView.svelte +10 -3
- package/dist/shell-shard/ScrollbackView.svelte.d.ts +1 -0
- package/dist/shell-shard/Terminal.svelte +94 -22
- package/dist/shell-shard/buffer-store.d.ts +15 -0
- package/dist/shell-shard/buffer-store.js +124 -0
- package/dist/shell-shard/buffer-store.svelte.test.d.ts +1 -0
- package/dist/shell-shard/buffer-store.svelte.test.js +107 -0
- package/dist/shell-shard/buffer-zone-state.svelte.d.ts +38 -0
- package/dist/shell-shard/buffer-zone-state.svelte.js +31 -0
- package/dist/shell-shard/contract.d.ts +7 -0
- package/dist/shell-shard/dispatch-custom.test.js +3 -1
- package/dist/shell-shard/dispatch-gating.test.js +6 -2
- package/dist/shell-shard/dispatch-invoke.test.js +10 -8
- package/dist/shell-shard/dispatch.d.ts +7 -2
- package/dist/shell-shard/dispatch.js +23 -27
- package/dist/shell-shard/display-cwd.d.ts +1 -0
- package/dist/shell-shard/display-cwd.js +27 -0
- package/dist/shell-shard/display-cwd.test.d.ts +1 -0
- package/dist/shell-shard/display-cwd.test.js +29 -0
- package/dist/shell-shard/entries/StatusEntry.svelte +2 -0
- package/dist/shell-shard/manifest.js +2 -1
- package/dist/shell-shard/manifest.test.d.ts +1 -0
- package/dist/shell-shard/manifest.test.js +8 -0
- package/dist/shell-shard/mode-buffer.svelte.d.ts +8 -0
- package/dist/shell-shard/mode-buffer.svelte.js +19 -0
- package/dist/shell-shard/mode-buffer.svelte.test.d.ts +1 -0
- package/dist/shell-shard/mode-buffer.svelte.test.js +25 -0
- package/dist/shell-shard/modes/builtin.js +2 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/protocol.d.ts +12 -6
- package/dist/shell-shard/replay.d.ts +3 -0
- package/dist/shell-shard/replay.js +44 -0
- package/dist/shell-shard/replay.svelte.test.d.ts +1 -0
- package/dist/shell-shard/replay.svelte.test.js +47 -0
- package/dist/shell-shard/rich-registry.d.ts +5 -0
- package/dist/shell-shard/rich-registry.js +25 -0
- package/dist/shell-shard/rich-registry.test.d.ts +1 -0
- package/dist/shell-shard/rich-registry.test.js +31 -0
- package/dist/shell-shard/scrollback.svelte.d.ts +2 -0
- package/dist/shell-shard/scrollback.svelte.js +23 -0
- package/dist/shell-shard/scrollback.svelte.test.d.ts +1 -0
- package/dist/shell-shard/scrollback.svelte.test.js +51 -0
- package/dist/shell-shard/session-client.svelte.d.ts +18 -2
- package/dist/shell-shard/session-client.svelte.js +21 -4
- package/dist/shell-shard/shellApi.d.ts +2 -1
- package/dist/shell-shard/shellApi.js +32 -3
- package/dist/shell-shard/shellApi.svelte.test.d.ts +1 -0
- package/dist/shell-shard/shellApi.svelte.test.js +59 -0
- package/dist/shell-shard/shellShard.svelte.js +11 -1
- package/dist/shell-shard/terminal-dispatch.test.js +3 -1
- package/dist/shell-shard/verbs/apps.js +9 -0
- package/dist/shell-shard/verbs/env.js +4 -0
- package/dist/shell-shard/verbs/help.js +9 -1
- package/dist/shell-shard/verbs/help.svelte.test.d.ts +1 -0
- package/dist/shell-shard/verbs/help.svelte.test.js +53 -0
- package/dist/shell-shard/verbs/history.js +8 -1
- package/dist/shell-shard/verbs/index.js +0 -8
- package/dist/shell-shard/verbs/shards.js +5 -0
- package/dist/shell-shard/verbs/views.js +9 -0
- package/dist/shell-shard/verbs/zones.js +9 -0
- package/dist/verbs/types.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +0 -8
- package/dist/shell-shard/verbs/cat.d.ts +0 -2
- package/dist/shell-shard/verbs/cat.js +0 -34
- package/dist/shell-shard/verbs/cd.test.js +0 -56
- package/dist/shell-shard/verbs/ls.d.ts +0 -2
- package/dist/shell-shard/verbs/ls.js +0 -29
- package/dist/shell-shard/verbs/ls.test.js +0 -49
- package/dist/shell-shard/verbs/session.d.ts +0 -4
- package/dist/shell-shard/verbs/session.js +0 -97
- /package/dist/{shell-shard/verbs/cd.test.d.ts → actions/ctx-actions.svelte.test.d.ts} +0 -0
- /package/dist/{shell-shard/verbs/ls.test.d.ts → actions/listActionsFromEntries.test.d.ts} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { VerbRegistry, ShellApi } from './registry';
|
|
2
|
-
import type { Scrollback } from './scrollback.svelte';
|
|
3
2
|
import type { SessionClient } from './session-client.svelte';
|
|
4
3
|
import type { TenantFsClient } from './tenant-fs-client';
|
|
4
|
+
import type { ModeBuffer } from './mode-buffer.svelte';
|
|
5
5
|
import type { ShellMode, ShellRole } from './modes/types';
|
|
6
6
|
import type { ShellModeDescriptor } from './contract';
|
|
7
7
|
export interface DispatchDeps {
|
|
@@ -9,7 +9,12 @@ export interface DispatchDeps {
|
|
|
9
9
|
/** Current shell role — used by invoke() role-gating. */
|
|
10
10
|
role: () => ShellRole;
|
|
11
11
|
resolver: VerbRegistry;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Active-mode buffer getter. Returns the ModeBuffer for the current
|
|
14
|
+
* mode id; reads its `scrollback` and `history` to fold dispatched
|
|
15
|
+
* activity into per-mode state.
|
|
16
|
+
*/
|
|
17
|
+
buffer: () => ModeBuffer;
|
|
13
18
|
session: SessionClient;
|
|
14
19
|
shell: ShellApi;
|
|
15
20
|
fs: TenantFsClient;
|
|
@@ -20,7 +20,6 @@ export function makeDispatch(deps) {
|
|
|
20
20
|
* because the caller is a trusted mode shard, not user input from a
|
|
21
21
|
* different mode.
|
|
22
22
|
*/
|
|
23
|
-
let bashConnected = false;
|
|
24
23
|
async function invoke(modeId, line) {
|
|
25
24
|
var _a, _b, _c, _d;
|
|
26
25
|
const current = deps.mode();
|
|
@@ -35,7 +34,7 @@ export function makeDispatch(deps) {
|
|
|
35
34
|
}
|
|
36
35
|
await resolution.verb.run({
|
|
37
36
|
shell: deps.shell,
|
|
38
|
-
scrollback: deps.scrollback,
|
|
37
|
+
scrollback: deps.buffer().scrollback,
|
|
39
38
|
session: deps.session,
|
|
40
39
|
cwd: deps.cwd(),
|
|
41
40
|
dispatch,
|
|
@@ -47,11 +46,7 @@ export function makeDispatch(deps) {
|
|
|
47
46
|
if (deps.role() !== 'admin') {
|
|
48
47
|
throw new Error("invoke: 'bash' requires admin role");
|
|
49
48
|
}
|
|
50
|
-
|
|
51
|
-
bashConnected = true;
|
|
52
|
-
deps.session.connect();
|
|
53
|
-
}
|
|
54
|
-
deps.session.send({ t: 'submit', line });
|
|
49
|
+
deps.session.send({ t: 'submit', line, mode: 'bash' });
|
|
55
50
|
return;
|
|
56
51
|
}
|
|
57
52
|
// Custom mode
|
|
@@ -66,7 +61,7 @@ export function makeDispatch(deps) {
|
|
|
66
61
|
throw new Error('invoke: server-side modes are not yet supported');
|
|
67
62
|
}
|
|
68
63
|
const subOutput = makeShellModeOutput({
|
|
69
|
-
scrollback: deps.scrollback,
|
|
64
|
+
scrollback: deps.buffer().scrollback,
|
|
70
65
|
busy: deps.busy,
|
|
71
66
|
invoke,
|
|
72
67
|
});
|
|
@@ -79,26 +74,27 @@ export function makeDispatch(deps) {
|
|
|
79
74
|
const controller = new AbortController();
|
|
80
75
|
activeController = controller;
|
|
81
76
|
const mode = deps.mode();
|
|
82
|
-
deps.
|
|
77
|
+
deps.buffer().history.push(line);
|
|
83
78
|
// User-mode $ escape: block server-shell access
|
|
84
79
|
if (mode.transport === 'none' && line.trimStart().startsWith('$ ')) {
|
|
85
|
-
deps.scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line, ts: Date.now() });
|
|
86
|
-
deps.scrollback.push({ kind: 'status', text: 'shell: server shell not available in user mode', level: 'error', ts: Date.now() });
|
|
80
|
+
deps.buffer().scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line, ts: Date.now() });
|
|
81
|
+
deps.buffer().scrollback.push({ kind: 'status', text: 'shell: server shell not available in user mode', level: 'error', ts: Date.now() });
|
|
87
82
|
return;
|
|
88
83
|
}
|
|
89
84
|
const resolution = deps.resolver.resolve(line, {
|
|
90
85
|
globalOnly: mode.id !== 'sh3',
|
|
91
86
|
});
|
|
92
87
|
if (resolution.kind === 'local') {
|
|
93
|
-
// Log locally-dispatched verbs
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
deps.
|
|
88
|
+
// Log locally-dispatched verbs to the server's per-mode history.
|
|
89
|
+
// The session-client queues this message while disconnected and
|
|
90
|
+
// flushes on reconnect; sh3/custom modes don't depend on a live
|
|
91
|
+
// bash WS for history persistence.
|
|
92
|
+
deps.session.send({ t: 'history-log', line, mode: mode.id });
|
|
93
|
+
deps.buffer().scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line, ts: Date.now() });
|
|
98
94
|
try {
|
|
99
95
|
await resolution.verb.run({
|
|
100
96
|
shell: deps.shell,
|
|
101
|
-
scrollback: deps.scrollback,
|
|
97
|
+
scrollback: deps.buffer().scrollback,
|
|
102
98
|
session: deps.session,
|
|
103
99
|
cwd: deps.cwd(),
|
|
104
100
|
dispatch,
|
|
@@ -106,7 +102,7 @@ export function makeDispatch(deps) {
|
|
|
106
102
|
}, resolution.args);
|
|
107
103
|
}
|
|
108
104
|
catch (err) {
|
|
109
|
-
deps.scrollback.push({
|
|
105
|
+
deps.buffer().scrollback.push({
|
|
110
106
|
kind: 'status',
|
|
111
107
|
text: `shell: verb ${resolution.verb.name} threw — ${err.message}`,
|
|
112
108
|
level: 'error',
|
|
@@ -117,14 +113,14 @@ export function makeDispatch(deps) {
|
|
|
117
113
|
}
|
|
118
114
|
// forward path
|
|
119
115
|
if (mode.transport === 'ws') {
|
|
120
|
-
deps.session.send({ t: 'submit', line: resolution.line });
|
|
116
|
+
deps.session.send({ t: 'submit', line: resolution.line, mode: mode.id });
|
|
121
117
|
return;
|
|
122
118
|
}
|
|
123
119
|
if (mode.transport === 'custom') {
|
|
124
|
-
deps.scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line: resolution.line, ts: Date.now() });
|
|
120
|
+
deps.buffer().scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line: resolution.line, ts: Date.now() });
|
|
125
121
|
const desc = (_b = (_a = deps.customMode) === null || _a === void 0 ? void 0 : _a.call(deps, mode.id)) !== null && _b !== void 0 ? _b : null;
|
|
126
122
|
if (!desc) {
|
|
127
|
-
deps.scrollback.push({
|
|
123
|
+
deps.buffer().scrollback.push({
|
|
128
124
|
kind: 'status',
|
|
129
125
|
text: `mode '${mode.id}' is no longer available`,
|
|
130
126
|
level: 'error',
|
|
@@ -133,7 +129,7 @@ export function makeDispatch(deps) {
|
|
|
133
129
|
return;
|
|
134
130
|
}
|
|
135
131
|
if (desc.runsOn === 'server') {
|
|
136
|
-
deps.scrollback.push({
|
|
132
|
+
deps.buffer().scrollback.push({
|
|
137
133
|
kind: 'status',
|
|
138
134
|
text: 'server-side modes are not yet supported (planned for a future release)',
|
|
139
135
|
level: 'error',
|
|
@@ -142,7 +138,7 @@ export function makeDispatch(deps) {
|
|
|
142
138
|
return;
|
|
143
139
|
}
|
|
144
140
|
const output = makeShellModeOutput({
|
|
145
|
-
scrollback: deps.scrollback,
|
|
141
|
+
scrollback: deps.buffer().scrollback,
|
|
146
142
|
busy: deps.busy,
|
|
147
143
|
invoke,
|
|
148
144
|
});
|
|
@@ -152,10 +148,10 @@ export function makeDispatch(deps) {
|
|
|
152
148
|
}
|
|
153
149
|
catch (err) {
|
|
154
150
|
if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
|
|
155
|
-
deps.scrollback.push({ kind: 'status', text: 'mode dispatch aborted', level: 'info', ts: Date.now() });
|
|
151
|
+
deps.buffer().scrollback.push({ kind: 'status', text: 'mode dispatch aborted', level: 'info', ts: Date.now() });
|
|
156
152
|
}
|
|
157
153
|
else {
|
|
158
|
-
deps.scrollback.push({
|
|
154
|
+
deps.buffer().scrollback.push({
|
|
159
155
|
kind: 'status',
|
|
160
156
|
text: `mode '${mode.id}' threw — ${(_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : String(err)}`,
|
|
161
157
|
level: 'error',
|
|
@@ -170,8 +166,8 @@ export function makeDispatch(deps) {
|
|
|
170
166
|
}
|
|
171
167
|
// 'none' transport, unknown verb: print error
|
|
172
168
|
const firstToken = (_d = resolution.line.split(/\s+/)[0]) !== null && _d !== void 0 ? _d : '';
|
|
173
|
-
deps.scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line: resolution.line, ts: Date.now() });
|
|
174
|
-
deps.scrollback.push({ kind: 'status', text: `unknown verb: ${firstToken}`, level: 'error', ts: Date.now() });
|
|
169
|
+
deps.buffer().scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line: resolution.line, ts: Date.now() });
|
|
170
|
+
deps.buffer().scrollback.push({ kind: 'status', text: `unknown verb: ${firstToken}`, level: 'error', ts: Date.now() });
|
|
175
171
|
}
|
|
176
172
|
return {
|
|
177
173
|
dispatch,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function shortenCwd(cwd: string, tenantRoot: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* shortenCwd — render a server cwd relative to the per-user tenant root.
|
|
3
|
+
*
|
|
4
|
+
* - Equal to tenant root → `~`
|
|
5
|
+
* - Inside tenant root → `~/<rest>` (forward-slash separators)
|
|
6
|
+
* - Outside tenant root → cwd unchanged
|
|
7
|
+
*
|
|
8
|
+
* Boundary-aware: a tenant root of `/foo` does not match `/foobar`. Handles
|
|
9
|
+
* both POSIX (`/`) and Windows (`\`) path separators in the input; the
|
|
10
|
+
* resulting display path always uses forward slashes for readability.
|
|
11
|
+
*/
|
|
12
|
+
export function shortenCwd(cwd, tenantRoot) {
|
|
13
|
+
if (!cwd || !tenantRoot)
|
|
14
|
+
return cwd;
|
|
15
|
+
if (cwd === tenantRoot)
|
|
16
|
+
return '~';
|
|
17
|
+
// Boundary check: the next character after the tenant prefix must be a
|
|
18
|
+
// path separator; otherwise we'd shorten /foobar against /foo.
|
|
19
|
+
if (cwd.length > tenantRoot.length && cwd.startsWith(tenantRoot)) {
|
|
20
|
+
const next = cwd.charAt(tenantRoot.length);
|
|
21
|
+
if (next === '/' || next === '\\') {
|
|
22
|
+
const rest = cwd.slice(tenantRoot.length + 1).replace(/\\/g, '/');
|
|
23
|
+
return `~/${rest}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return cwd;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { shortenCwd } from './display-cwd';
|
|
3
|
+
describe('shortenCwd', () => {
|
|
4
|
+
it('returns ~ when cwd equals tenant root', () => {
|
|
5
|
+
expect(shortenCwd('/home/u/data/users/x/documents/shell', '/home/u/data/users/x/documents/shell')).toBe('~');
|
|
6
|
+
});
|
|
7
|
+
it('substitutes ~ for tenant-root prefix (POSIX)', () => {
|
|
8
|
+
expect(shortenCwd('/home/u/data/users/x/documents/shell/notes', '/home/u/data/users/x/documents/shell')).toBe('~/notes');
|
|
9
|
+
});
|
|
10
|
+
it('substitutes ~ for tenant-root prefix (Windows)', () => {
|
|
11
|
+
expect(shortenCwd('C:\\a\\b\\users\\x\\documents\\shell\\notes', 'C:\\a\\b\\users\\x\\documents\\shell')).toBe('~/notes');
|
|
12
|
+
});
|
|
13
|
+
it('normalizes Windows backslashes to forward slashes after the tilde', () => {
|
|
14
|
+
expect(shortenCwd('C:\\root\\a\\b\\c', 'C:\\root')).toBe('~/a/b/c');
|
|
15
|
+
});
|
|
16
|
+
it('returns the absolute cwd when outside the tenant root', () => {
|
|
17
|
+
expect(shortenCwd('/tmp', '/home/u/data/users/x/documents/shell')).toBe('/tmp');
|
|
18
|
+
});
|
|
19
|
+
it('does not match when cwd matches the tenant root prefix without a separator boundary', () => {
|
|
20
|
+
// tenant=/foo, cwd=/foobar → must NOT shorten to ~bar
|
|
21
|
+
expect(shortenCwd('/foobar', '/foo')).toBe('/foobar');
|
|
22
|
+
});
|
|
23
|
+
it('returns cwd unchanged when tenantRoot is empty', () => {
|
|
24
|
+
expect(shortenCwd('/some/path', '')).toBe('/some/path');
|
|
25
|
+
});
|
|
26
|
+
it('returns cwd unchanged when cwd is empty', () => {
|
|
27
|
+
expect(shortenCwd('', '/tenant')).toBe('');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
padding: 2px 8px;
|
|
16
16
|
font-family: var(--shell-font-mono, monospace);
|
|
17
17
|
font-style: italic;
|
|
18
|
+
white-space: pre-wrap;
|
|
19
|
+
word-break: break-word;
|
|
18
20
|
}
|
|
19
21
|
.shell-status.info { color: var(--shell-fg-muted, #888); }
|
|
20
22
|
.shell-status.warn { color: var(--shell-fg-warn, #fc6); }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VERSION } from '../version';
|
|
2
|
+
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
2
3
|
export const manifest = {
|
|
3
4
|
id: 'shell',
|
|
4
5
|
label: 'Shell',
|
|
@@ -8,5 +9,5 @@ export const manifest = {
|
|
|
8
9
|
// and is statically mounted at sh3-server boot. The existing contract in
|
|
9
10
|
// sh3-core/src/shards/types.ts documents that framework-shipped shards do
|
|
10
11
|
// not use this field.
|
|
11
|
-
permissions: [],
|
|
12
|
+
permissions: [PERMISSION_STATE_MANAGE],
|
|
12
13
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { manifest } from './manifest';
|
|
3
|
+
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
4
|
+
describe('shell manifest', () => {
|
|
5
|
+
it('declares state:manage permission for cross-shard zone introspection', () => {
|
|
6
|
+
expect(manifest.permissions).toContain(PERMISSION_STATE_MANAGE);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ModeBuffer — per-mode bundle of scrollback + history + locked flag.
|
|
3
|
+
*
|
|
4
|
+
* Owned by Terminal.svelte's Map<modeId, ModeBuffer>. Switching mode
|
|
5
|
+
* rebinds the visible buffer; each buffer remains in the map so the
|
|
6
|
+
* user can flip back without losing state.
|
|
7
|
+
*
|
|
8
|
+
* `locked` is bash-specific today (set true while a process runs).
|
|
9
|
+
* sh3 / custom modes leave it false.
|
|
10
|
+
*/
|
|
11
|
+
import { Scrollback } from './scrollback.svelte';
|
|
12
|
+
export class ModeBuffer {
|
|
13
|
+
constructor(modeId) {
|
|
14
|
+
this.history = $state([]);
|
|
15
|
+
this.locked = $state(false);
|
|
16
|
+
this.modeId = modeId;
|
|
17
|
+
this.scrollback = new Scrollback();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ModeBuffer } from './mode-buffer.svelte';
|
|
3
|
+
describe('ModeBuffer', () => {
|
|
4
|
+
it('constructs with empty scrollback and history', () => {
|
|
5
|
+
const buf = new ModeBuffer('sh3');
|
|
6
|
+
expect(buf.modeId).toBe('sh3');
|
|
7
|
+
expect(buf.scrollback.entries).toEqual([]);
|
|
8
|
+
expect(buf.history).toEqual([]);
|
|
9
|
+
expect(buf.locked).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
it('history is independently mutable per buffer', () => {
|
|
12
|
+
const a = new ModeBuffer('a');
|
|
13
|
+
const b = new ModeBuffer('b');
|
|
14
|
+
a.history.push('one');
|
|
15
|
+
expect(a.history).toEqual(['one']);
|
|
16
|
+
expect(b.history).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it('locked flag is independently mutable per buffer', () => {
|
|
19
|
+
const a = new ModeBuffer('a');
|
|
20
|
+
const b = new ModeBuffer('b');
|
|
21
|
+
a.locked = true;
|
|
22
|
+
expect(a.locked).toBe(true);
|
|
23
|
+
expect(b.locked).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -5,12 +5,14 @@ export const BASH_MODE = {
|
|
|
5
5
|
requiresRole: 'admin',
|
|
6
6
|
transport: 'ws',
|
|
7
7
|
autoRelocate: false,
|
|
8
|
+
showCwd: true,
|
|
8
9
|
};
|
|
9
10
|
export const SH3_MODE = {
|
|
10
11
|
id: 'sh3',
|
|
11
12
|
label: 'SH3',
|
|
12
13
|
transport: 'none',
|
|
13
14
|
autoRelocate: true,
|
|
15
|
+
showCwd: false,
|
|
14
16
|
};
|
|
15
17
|
export function registerBuiltinModes(reg) {
|
|
16
18
|
reg.register(BASH_MODE);
|
|
@@ -5,4 +5,12 @@ export interface ShellMode {
|
|
|
5
5
|
requiresRole?: 'admin';
|
|
6
6
|
transport: 'ws' | 'none' | 'custom';
|
|
7
7
|
autoRelocate: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the input prompt should display the current working directory.
|
|
10
|
+
* Builtin shell modes (sh3, bash) set this true; custom modes default to
|
|
11
|
+
* false (a `cwd` chip in front of every prompt is rarely meaningful for
|
|
12
|
+
* LLM/agent-style modes). Undefined falls through to the consumer's
|
|
13
|
+
* default — InputLine renders the cwd unless this is explicitly false.
|
|
14
|
+
*/
|
|
15
|
+
showCwd?: boolean;
|
|
8
16
|
}
|
|
@@ -5,20 +5,23 @@ export type ClientMessage =
|
|
|
5
5
|
t: 'hello';
|
|
6
6
|
replayFrom?: number;
|
|
7
7
|
}
|
|
8
|
-
/** A line the client could not resolve locally.
|
|
8
|
+
/** A line the client could not resolve locally. `mode` tags the active
|
|
9
|
+
* mode at submit time; the server records it on the history entry. */
|
|
9
10
|
| {
|
|
10
11
|
t: 'submit';
|
|
11
12
|
line: string;
|
|
13
|
+
mode: string;
|
|
12
14
|
}
|
|
13
15
|
/** Ctrl+C / Ctrl+D while a process runs. */
|
|
14
16
|
| {
|
|
15
17
|
t: 'signal';
|
|
16
18
|
sig: 'SIGINT' | 'EOF';
|
|
17
19
|
}
|
|
18
|
-
/** Local verb dispatch — tell the server to append to
|
|
20
|
+
/** Local verb dispatch — tell the server to append to per-mode history. */
|
|
19
21
|
| {
|
|
20
22
|
t: 'history-log';
|
|
21
23
|
line: string;
|
|
24
|
+
mode: string;
|
|
22
25
|
}
|
|
23
26
|
/** Ask the server for its current cwd. */
|
|
24
27
|
| {
|
|
@@ -31,11 +34,14 @@ export type ClientMessage =
|
|
|
31
34
|
path: string;
|
|
32
35
|
};
|
|
33
36
|
export type ServerMessage =
|
|
34
|
-
/** Sent once on successful attach, immediately after hello.
|
|
37
|
+
/** Sent once on successful attach, immediately after hello.
|
|
38
|
+
* `tenantRoot` is the initial per-user shell cwd; clients use it to
|
|
39
|
+
* shorten displayed paths (e.g., render the tenant root as `~`). */
|
|
35
40
|
{
|
|
36
41
|
t: 'welcome';
|
|
37
42
|
userId: string;
|
|
38
43
|
cwd: string;
|
|
44
|
+
tenantRoot: string;
|
|
39
45
|
env: Record<string, string>;
|
|
40
46
|
seq: number;
|
|
41
47
|
}
|
|
@@ -44,10 +50,10 @@ export type ServerMessage =
|
|
|
44
50
|
t: 'replay';
|
|
45
51
|
events: ServerEvent[];
|
|
46
52
|
}
|
|
47
|
-
/**
|
|
53
|
+
/** Per-mode persisted history for up-arrow navigation. Keyed by mode id. */
|
|
48
54
|
| {
|
|
49
|
-
t: 'history';
|
|
50
|
-
|
|
55
|
+
t: 'history-bundle';
|
|
56
|
+
byMode: Record<string, string[]>;
|
|
51
57
|
}
|
|
52
58
|
/** Live event. */
|
|
53
59
|
| {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pure replay-folder. Takes a list of server events and folds them into
|
|
3
|
+
* a ModeBuffer in the same order, using the same routing rules as the
|
|
4
|
+
* live event-handler in Terminal.svelte. Extracted so it can be unit-
|
|
5
|
+
* tested without DOM mounting and so the live handler and the on-attach
|
|
6
|
+
* replay path share one implementation of the rules.
|
|
7
|
+
*
|
|
8
|
+
* Used by Terminal.svelte's `t: 'replay'` handler to fill the gap between
|
|
9
|
+
* the last persisted bash-buffer snapshot (workspace zone) and the WS
|
|
10
|
+
* reconnect — events the server still has in its ring buffer but the
|
|
11
|
+
* client hasn't seen since the last page load.
|
|
12
|
+
*/
|
|
13
|
+
export function applyReplayEvents(buf, events) {
|
|
14
|
+
for (const e of events) {
|
|
15
|
+
switch (e.kind) {
|
|
16
|
+
case 'prompt':
|
|
17
|
+
buf.scrollback.push({ kind: 'prompt', cwd: e.cwd, line: e.line, ts: e.ts });
|
|
18
|
+
buf.locked = true;
|
|
19
|
+
break;
|
|
20
|
+
case 'stdout':
|
|
21
|
+
buf.scrollback.push({ kind: 'text', stream: 'stdout', chunks: [e.data], ts: e.ts });
|
|
22
|
+
break;
|
|
23
|
+
case 'stderr':
|
|
24
|
+
buf.scrollback.push({ kind: 'text', stream: 'stderr', chunks: [e.data], ts: e.ts });
|
|
25
|
+
break;
|
|
26
|
+
case 'exit':
|
|
27
|
+
if (e.signal || (e.code !== null && e.code !== 0)) {
|
|
28
|
+
buf.scrollback.push({
|
|
29
|
+
kind: 'status',
|
|
30
|
+
text: e.signal
|
|
31
|
+
? `shell: process exited (${e.signal})`
|
|
32
|
+
: `shell: process exited (${e.code})`,
|
|
33
|
+
level: 'error',
|
|
34
|
+
ts: e.ts,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
buf.locked = false;
|
|
38
|
+
break;
|
|
39
|
+
case 'status':
|
|
40
|
+
buf.scrollback.push({ kind: 'status', text: e.text, level: e.level, ts: e.ts });
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { applyReplayEvents } from './replay';
|
|
3
|
+
import { ModeBuffer } from './mode-buffer.svelte';
|
|
4
|
+
describe('applyReplayEvents', () => {
|
|
5
|
+
it('folds events into a fresh bash buffer like live events', () => {
|
|
6
|
+
const buf = new ModeBuffer('bash');
|
|
7
|
+
const events = [
|
|
8
|
+
{ seq: 5, kind: 'prompt', line: 'ls', cwd: '/', ts: 100 },
|
|
9
|
+
{ seq: 6, kind: 'stdout', data: 'a\nb\n', ts: 101 },
|
|
10
|
+
{ seq: 7, kind: 'exit', code: 0, signal: null, ts: 102 },
|
|
11
|
+
];
|
|
12
|
+
applyReplayEvents(buf, events);
|
|
13
|
+
// prompt + coalesced text — exit code 0 is silent (matches Terminal.svelte UX).
|
|
14
|
+
expect(buf.scrollback.entries).toHaveLength(2);
|
|
15
|
+
expect(buf.locked).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
it('locks the buffer if replay ends with a prompt without exit', () => {
|
|
18
|
+
const buf = new ModeBuffer('bash');
|
|
19
|
+
const events = [
|
|
20
|
+
{ seq: 10, kind: 'prompt', line: 'sleep 5', cwd: '/', ts: 200 },
|
|
21
|
+
];
|
|
22
|
+
applyReplayEvents(buf, events);
|
|
23
|
+
expect(buf.locked).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('emits a status entry on non-zero exit', () => {
|
|
26
|
+
const buf = new ModeBuffer('bash');
|
|
27
|
+
const events = [
|
|
28
|
+
{ seq: 1, kind: 'prompt', line: 'false', cwd: '/', ts: 1 },
|
|
29
|
+
{ seq: 2, kind: 'exit', code: 1, signal: null, ts: 2 },
|
|
30
|
+
];
|
|
31
|
+
applyReplayEvents(buf, events);
|
|
32
|
+
const last = buf.scrollback.entries[buf.scrollback.entries.length - 1];
|
|
33
|
+
expect(last.kind).toBe('status');
|
|
34
|
+
if (last.kind === 'status')
|
|
35
|
+
expect(last.text).toMatch(/exited \(1\)/);
|
|
36
|
+
});
|
|
37
|
+
it('routes stderr to a stderr text entry', () => {
|
|
38
|
+
const buf = new ModeBuffer('bash');
|
|
39
|
+
applyReplayEvents(buf, [
|
|
40
|
+
{ seq: 1, kind: 'stderr', data: 'oops\n', ts: 1 },
|
|
41
|
+
]);
|
|
42
|
+
const e = buf.scrollback.entries[0];
|
|
43
|
+
expect(e.kind).toBe('text');
|
|
44
|
+
if (e.kind === 'text')
|
|
45
|
+
expect(e.stream).toBe('stderr');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
export declare function registerRichComponent(key: string, component: Component<any>): void;
|
|
3
|
+
export declare function lookupRichComponent(key: string): Component<any> | null;
|
|
4
|
+
/** Test-only seam. Do not call from production code. */
|
|
5
|
+
export declare function __resetRichRegistryForTests(): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Stable string-keyed registry mapping a componentKey to a Svelte
|
|
3
|
+
* component. Used by ScrollbackView (live render) and BufferStore
|
|
4
|
+
* (rehydrate persisted rich entries).
|
|
5
|
+
*
|
|
6
|
+
* Each rich-emitting verb registers its component once at module load.
|
|
7
|
+
* Keys must be globally unique within the shell-shard namespace.
|
|
8
|
+
*/
|
|
9
|
+
const components = new Map();
|
|
10
|
+
export function registerRichComponent(key, component) {
|
|
11
|
+
const existing = components.get(key);
|
|
12
|
+
if (existing && existing !== component) {
|
|
13
|
+
console.warn(`[shell-shard/rich-registry] duplicate registration for key "${key}" — keeping first`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
components.set(key, component);
|
|
17
|
+
}
|
|
18
|
+
export function lookupRichComponent(key) {
|
|
19
|
+
var _a;
|
|
20
|
+
return (_a = components.get(key)) !== null && _a !== void 0 ? _a : null;
|
|
21
|
+
}
|
|
22
|
+
/** Test-only seam. Do not call from production code. */
|
|
23
|
+
export function __resetRichRegistryForTests() {
|
|
24
|
+
components.clear();
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { registerRichComponent, lookupRichComponent, __resetRichRegistryForTests, } from './rich-registry';
|
|
3
|
+
const FakeComp = {};
|
|
4
|
+
const OtherComp = {};
|
|
5
|
+
describe('rich-registry', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
__resetRichRegistryForTests();
|
|
8
|
+
});
|
|
9
|
+
it('returns null for unknown keys', () => {
|
|
10
|
+
expect(lookupRichComponent('nope')).toBe(null);
|
|
11
|
+
});
|
|
12
|
+
it('roundtrips a registered key', () => {
|
|
13
|
+
registerRichComponent('test', FakeComp);
|
|
14
|
+
expect(lookupRichComponent('test')).toBe(FakeComp);
|
|
15
|
+
});
|
|
16
|
+
it('warns and ignores duplicate registrations of a different component', () => {
|
|
17
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
18
|
+
registerRichComponent('dup', FakeComp);
|
|
19
|
+
registerRichComponent('dup', OtherComp);
|
|
20
|
+
expect(lookupRichComponent('dup')).toBe(FakeComp);
|
|
21
|
+
expect(warn).toHaveBeenCalledOnce();
|
|
22
|
+
warn.mockRestore();
|
|
23
|
+
});
|
|
24
|
+
it('idempotent registration of the same component is silent', () => {
|
|
25
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
26
|
+
registerRichComponent('idem', FakeComp);
|
|
27
|
+
registerRichComponent('idem', FakeComp);
|
|
28
|
+
expect(warn).not.toHaveBeenCalled();
|
|
29
|
+
warn.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -15,6 +15,7 @@ export type ScrollbackEntry = {
|
|
|
15
15
|
} | {
|
|
16
16
|
kind: 'rich';
|
|
17
17
|
id: string;
|
|
18
|
+
componentKey?: string;
|
|
18
19
|
component: RichComponent;
|
|
19
20
|
props: Record<string, unknown>;
|
|
20
21
|
ts: number;
|
|
@@ -32,5 +33,6 @@ export declare class Scrollback {
|
|
|
32
33
|
constructor(cap?: number);
|
|
33
34
|
push(entry: DistributiveOmit<ScrollbackEntry, 'id'>): void;
|
|
34
35
|
clear(): void;
|
|
36
|
+
restore(entries: ScrollbackEntry[]): void;
|
|
35
37
|
}
|
|
36
38
|
export {};
|
|
@@ -16,6 +16,14 @@ let nextId = 0;
|
|
|
16
16
|
function mkId() {
|
|
17
17
|
return `e${++nextId}`;
|
|
18
18
|
}
|
|
19
|
+
// Parses the numeric suffix of an id minted by mkId (`e<n>`). Returns NaN
|
|
20
|
+
// for any other shape so restore() can ignore it without throwing.
|
|
21
|
+
function parseIdNumber(id) {
|
|
22
|
+
if (id.length < 2 || id.charCodeAt(0) !== 101 /* 'e' */)
|
|
23
|
+
return NaN;
|
|
24
|
+
const n = Number(id.slice(1));
|
|
25
|
+
return Number.isInteger(n) ? n : NaN;
|
|
26
|
+
}
|
|
19
27
|
export class Scrollback {
|
|
20
28
|
constructor(cap = DEFAULT_CAP) {
|
|
21
29
|
this.entries = $state([]);
|
|
@@ -40,4 +48,19 @@ export class Scrollback {
|
|
|
40
48
|
clear() {
|
|
41
49
|
this.entries.length = 0;
|
|
42
50
|
}
|
|
51
|
+
// Replace entries from a persisted snapshot. Splices in place to keep the
|
|
52
|
+
// $state proxy identity stable, and bumps the module-scoped id counter
|
|
53
|
+
// past any restored numeric id so subsequent push()es never collide with
|
|
54
|
+
// hydrated keys (which would crash Svelte's keyed {#each}).
|
|
55
|
+
restore(entries) {
|
|
56
|
+
let max = 0;
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
const n = parseIdNumber(e.id);
|
|
59
|
+
if (Number.isFinite(n) && n > max)
|
|
60
|
+
max = n;
|
|
61
|
+
}
|
|
62
|
+
if (max > nextId)
|
|
63
|
+
nextId = max;
|
|
64
|
+
this.entries.splice(0, this.entries.length, ...entries);
|
|
65
|
+
}
|
|
43
66
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|