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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
|
|
11
11
|
import ShellTitle from './ShellTitle.svelte';
|
|
12
12
|
import ProjectsSection from '../projects-shard/ProjectsSection.svelte';
|
|
13
|
+
import LayoutsSection from '../layouts-shard/LayoutsSection.svelte';
|
|
13
14
|
import { sessionState } from '../projects/session-state.svelte';
|
|
14
15
|
import { projectsState } from '../projects-shard/projectsShard.svelte';
|
|
15
16
|
import { shell } from '../shellRuntime.svelte';
|
|
@@ -82,6 +83,8 @@
|
|
|
82
83
|
|
|
83
84
|
<ProjectsSection />
|
|
84
85
|
|
|
86
|
+
<LayoutsSection />
|
|
87
|
+
|
|
85
88
|
{#if userApps.length > 0}
|
|
86
89
|
<section class="shell-home-section">
|
|
87
90
|
<h2 class="shell-home-section-title">Apps</h2>
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { Shard } from '../api';
|
|
2
|
+
import type { Action } from '../actions/types';
|
|
3
|
+
/**
|
|
4
|
+
* Build the palette-only float-maximize toggle action. Targets the topmost
|
|
5
|
+
* float (last entry in `floatManager.list()`); disabled when no floats are
|
|
6
|
+
* open. See spec docs/superpowers/specs/2026-05-07-float-resize-maximize-design.md.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildToggleMaximizeAction(): Action;
|
|
2
9
|
export declare const sh3coreShard: Shard;
|
|
@@ -31,7 +31,29 @@ import { registeredApps } from '../apps/registry.svelte';
|
|
|
31
31
|
import { launchApp } from '../apps/lifecycle';
|
|
32
32
|
import { resetActivePresetToDefault } from '../layout/store.svelte';
|
|
33
33
|
import { modalManager } from '../overlays/modal';
|
|
34
|
+
import { floatManager } from '../overlays/float';
|
|
34
35
|
import { registerAppActions } from './appActions';
|
|
36
|
+
/**
|
|
37
|
+
* Build the palette-only float-maximize toggle action. Targets the topmost
|
|
38
|
+
* float (last entry in `floatManager.list()`); disabled when no floats are
|
|
39
|
+
* open. See spec docs/superpowers/specs/2026-05-07-float-resize-maximize-design.md.
|
|
40
|
+
*/
|
|
41
|
+
export function buildToggleMaximizeAction() {
|
|
42
|
+
return {
|
|
43
|
+
id: 'sh3.float.toggleMaximize',
|
|
44
|
+
label: 'Toggle Float Maximize',
|
|
45
|
+
scope: ['home', 'app'],
|
|
46
|
+
paletteItem: true,
|
|
47
|
+
contextItem: false,
|
|
48
|
+
defaultShortcut: 'Ctrl+Shift+M',
|
|
49
|
+
disabled: () => floatManager.list().length === 0,
|
|
50
|
+
run() {
|
|
51
|
+
const top = floatManager.list().at(-1);
|
|
52
|
+
if (top)
|
|
53
|
+
floatManager.toggleMaximize(top.id);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
35
57
|
export const sh3coreShard = {
|
|
36
58
|
manifest: {
|
|
37
59
|
id: '__sh3core__',
|
|
@@ -61,6 +83,7 @@ export const sh3coreShard = {
|
|
|
61
83
|
import('../actions/listeners').then(({ openPalette }) => openPalette());
|
|
62
84
|
},
|
|
63
85
|
});
|
|
86
|
+
ctx.actions.register(buildToggleMaximizeAction());
|
|
64
87
|
ctx.actions.register({
|
|
65
88
|
id: 'sh3.app.reset-layout',
|
|
66
89
|
label: 'Reset Current Layout',
|
|
@@ -44,8 +44,30 @@ describe('ctx.listVerbs / ctx.runVerb (integration)', () => {
|
|
|
44
44
|
const list = consumerCtx.listVerbs();
|
|
45
45
|
const a = list.find((v) => v.name === 'host:a');
|
|
46
46
|
const b = list.find((v) => v.name === 'host:b');
|
|
47
|
-
expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', schema: undefined });
|
|
48
|
-
expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', schema: undefined });
|
|
47
|
+
expect(a).toEqual({ shardId: 'host', name: 'host:a', summary: 'first verb', programmatic: true, schema: undefined });
|
|
48
|
+
expect(b).toEqual({ shardId: 'host', name: 'host:b', summary: 'second verb', programmatic: undefined, schema: undefined });
|
|
49
|
+
});
|
|
50
|
+
it('listVerbs({ programmaticOnly: true }) returns only verbs that opted in', async () => {
|
|
51
|
+
registerShard({
|
|
52
|
+
manifest: { id: 'host', label: 'Host', version: '0.0.0', views: [] },
|
|
53
|
+
activate(ctx) {
|
|
54
|
+
ctx.registerVerb(programmaticVerb('a', 'first verb'));
|
|
55
|
+
ctx.registerVerb(plainVerb('b', 'second verb'));
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
let consumerCtx = null;
|
|
59
|
+
registerShard({
|
|
60
|
+
manifest: { id: 'consumer', label: 'C', version: '0.0.0', views: [] },
|
|
61
|
+
activate(ctx) {
|
|
62
|
+
consumerCtx = ctx;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
await activateShard('host');
|
|
66
|
+
await activateShard('consumer');
|
|
67
|
+
const list = consumerCtx.listVerbs({ programmaticOnly: true });
|
|
68
|
+
expect(list.find((v) => v.name === 'host:a')).toBeDefined();
|
|
69
|
+
expect(list.find((v) => v.name === 'host:b')).toBeUndefined();
|
|
70
|
+
expect(list.every((v) => v.programmatic === true)).toBe(true);
|
|
49
71
|
});
|
|
50
72
|
it('runVerb dispatches a programmatic verb and returns captured scrollback', async () => {
|
|
51
73
|
registerShard({
|
|
@@ -30,9 +30,11 @@ import { createShardKeysApi } from '../keys/client';
|
|
|
30
30
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
31
31
|
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
32
32
|
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
|
|
33
|
-
import { registerAction } from '../actions/registry';
|
|
33
|
+
import { registerAction, listActions as listActionsFromRegistry } from '../actions/registry';
|
|
34
34
|
import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
|
|
35
|
-
import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette } from '../actions/listeners';
|
|
35
|
+
import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette, dispatchActionProgrammatic, } from '../actions/listeners';
|
|
36
|
+
import { listActionsFromEntries } from '../actions/listActive';
|
|
37
|
+
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
36
38
|
/**
|
|
37
39
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
38
40
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -201,17 +203,29 @@ export async function activateShard(id, opts) {
|
|
|
201
203
|
openContextMenu(opts) { shellOpenContextMenu(opts); },
|
|
202
204
|
openPalette(opts) { shellOpenPalette(opts); },
|
|
203
205
|
},
|
|
204
|
-
listVerbs() {
|
|
205
|
-
|
|
206
|
+
listVerbs(opts) {
|
|
207
|
+
const all = listVerbsWithShard();
|
|
208
|
+
const filtered = (opts === null || opts === void 0 ? void 0 : opts.programmaticOnly)
|
|
209
|
+
? all.filter(({ verb }) => verb.programmatic === true)
|
|
210
|
+
: all;
|
|
211
|
+
return filtered.map(({ verb, shardId }) => ({
|
|
206
212
|
shardId,
|
|
207
213
|
name: verb.name,
|
|
208
214
|
summary: verb.summary,
|
|
215
|
+
programmatic: verb.programmatic,
|
|
209
216
|
schema: verb.schema,
|
|
210
217
|
}));
|
|
211
218
|
},
|
|
212
219
|
runVerb(shardId, name, args, opts) {
|
|
213
220
|
return runVerbProgrammatic(shardId, name, args, opts);
|
|
214
221
|
},
|
|
222
|
+
listActions(opts) {
|
|
223
|
+
const all = listActionsFromEntries(listActionsFromRegistry(), getLiveDispatcherState());
|
|
224
|
+
return (opts === null || opts === void 0 ? void 0 : opts.activeOnly) ? all.filter((a) => a.active) : all;
|
|
225
|
+
},
|
|
226
|
+
runAction(actionId, opts) {
|
|
227
|
+
return dispatchActionProgrammatic(actionId, opts);
|
|
228
|
+
},
|
|
215
229
|
};
|
|
216
230
|
entry.ctx = ctx;
|
|
217
231
|
// Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Verb, VerbSchema } from '../verbs/types';
|
|
|
7
7
|
import type { ScrollbackEntry } from '../shell-shard/scrollback.svelte';
|
|
8
8
|
import type { ShardContextKeys } from '../keys/types';
|
|
9
9
|
import type { ContributionsApi } from '../contributions/types';
|
|
10
|
-
import type { ActionsApi } from '../actions/types';
|
|
10
|
+
import type { ActionsApi, ActionDescriptor } from '../actions/types';
|
|
11
11
|
import type { TreeRootRef } from '../layout/types';
|
|
12
12
|
export { PERMISSION_KEYS_MINT, type ShardContextKeys, type ApiKeyPublic, type MintOpts, ScopeEscalationError, ConsentDeniedError } from '../keys/types';
|
|
13
13
|
/**
|
|
@@ -255,17 +255,25 @@ export interface ShardContext {
|
|
|
255
255
|
/**
|
|
256
256
|
* Read-only snapshot of every verb registered across every active shard.
|
|
257
257
|
* Returned entries include the contributing `shardId`, the prefixed
|
|
258
|
-
* `name`, the verb's `summary`, and (when
|
|
259
|
-
* Order is undefined.
|
|
258
|
+
* `name`, the verb's `summary`, the `programmatic` flag, and (when
|
|
259
|
+
* present) its `schema`. Order is undefined.
|
|
260
|
+
*
|
|
261
|
+
* Pass `{ programmaticOnly: true }` to restrict the result to verbs that
|
|
262
|
+
* have opted in via `programmatic: true` — i.e. the verbs that are
|
|
263
|
+
* actually invocable through `ctx.runVerb(...)`. AI-class shards
|
|
264
|
+
* typically want this filter so they only surface what they can call.
|
|
260
265
|
*
|
|
261
266
|
* No permission gate — verb names + summaries are already visible via
|
|
262
267
|
* the `help` verb. Diagnostic and AI-class shards (sh3-ai, sh3-diagnostic)
|
|
263
268
|
* use this to enumerate the host's action surface.
|
|
264
269
|
*/
|
|
265
|
-
listVerbs(
|
|
270
|
+
listVerbs(opts?: {
|
|
271
|
+
programmaticOnly?: boolean;
|
|
272
|
+
}): Array<{
|
|
266
273
|
shardId: string;
|
|
267
274
|
name: string;
|
|
268
275
|
summary: string;
|
|
276
|
+
programmatic?: boolean;
|
|
269
277
|
schema?: VerbSchema;
|
|
270
278
|
}>;
|
|
271
279
|
/**
|
|
@@ -288,6 +296,38 @@ export interface ShardContext {
|
|
|
288
296
|
result: unknown;
|
|
289
297
|
scrollback: ScrollbackEntry[];
|
|
290
298
|
}>;
|
|
299
|
+
/**
|
|
300
|
+
* Read-only snapshot of every action registered across every shard.
|
|
301
|
+
* Returns one descriptor per action id; the `active` flag indicates
|
|
302
|
+
* whether `runAction(id)` would dispatch right now (scope live, not
|
|
303
|
+
* disabled, has a run handler).
|
|
304
|
+
*
|
|
305
|
+
* Pass `{ activeOnly: true }` to filter to currently-dispatchable
|
|
306
|
+
* actions. AI-class shards typically want this filter.
|
|
307
|
+
*
|
|
308
|
+
* No permission gate — actions are already enumerable through the
|
|
309
|
+
* keyboard / palette / context-menu surfaces.
|
|
310
|
+
*/
|
|
311
|
+
listActions(opts?: {
|
|
312
|
+
activeOnly?: boolean;
|
|
313
|
+
}): ActionDescriptor[];
|
|
314
|
+
/**
|
|
315
|
+
* Programmatically dispatch a registered action by id. Synthesizes the
|
|
316
|
+
* same `ActionDispatchContext` the keyboard/palette/context-menu paths
|
|
317
|
+
* use, with `invokedVia: 'programmatic'` and `appId / viewId / selection`
|
|
318
|
+
* sourced from current live state. Resolves after the action's `run`
|
|
319
|
+
* settles. Rejects on:
|
|
320
|
+
* - unknown action id,
|
|
321
|
+
* - action exists but is inactive (out-of-scope, disabled, submenu
|
|
322
|
+
* parent without `run`),
|
|
323
|
+
* - any error thrown by the action's `run`.
|
|
324
|
+
*
|
|
325
|
+
* `opts.signal` is stored on the dispatch context for v1 parity with
|
|
326
|
+
* `runVerb`; today's actions don't read it.
|
|
327
|
+
*/
|
|
328
|
+
runAction(id: string, opts?: {
|
|
329
|
+
signal?: AbortSignal;
|
|
330
|
+
}): Promise<void>;
|
|
291
331
|
}
|
|
292
332
|
/**
|
|
293
333
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single-line command entry widget. The chrome (focus ring, prefix slot,
|
|
6
|
+
* monospace font, anti-credential-fill attrs) is owned here; consumers
|
|
7
|
+
* supply the keybinding semantics via `onkeydown`.
|
|
8
|
+
*
|
|
9
|
+
* Two behaviors are baked in so every consumer gets them for free:
|
|
10
|
+
* - Selection-aware Ctrl+C: when the input has a non-empty selection,
|
|
11
|
+
* the keystroke is NOT forwarded to onkeydown — the browser does its
|
|
12
|
+
* native copy. This avoids the consumer mapping Ctrl+C to "clear
|
|
13
|
+
* draft" or "send SIGINT" eating a copy operation.
|
|
14
|
+
* - data-sh3-passthrough-modifiers on the row: the dispatcher's
|
|
15
|
+
* "block shortcuts in inputs" rule lets Ctrl/Alt/Meta-bearing
|
|
16
|
+
* shortcuts through this widget, so global bindings like Ctrl+K
|
|
17
|
+
* fire while the user is typing.
|
|
18
|
+
*/
|
|
19
|
+
interface Props {
|
|
20
|
+
value: string;
|
|
21
|
+
prefix?: Snippet;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
name?: string;
|
|
24
|
+
onkeydown?: (e: KeyboardEvent) => void;
|
|
25
|
+
}
|
|
26
|
+
let {
|
|
27
|
+
value = $bindable(''),
|
|
28
|
+
prefix,
|
|
29
|
+
disabled = false,
|
|
30
|
+
name,
|
|
31
|
+
onkeydown,
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
let input: HTMLInputElement | null = $state(null);
|
|
35
|
+
|
|
36
|
+
function hasSelection(el: HTMLInputElement): boolean {
|
|
37
|
+
return el.selectionStart !== null
|
|
38
|
+
&& el.selectionEnd !== null
|
|
39
|
+
&& el.selectionStart !== el.selectionEnd;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleKeyDown(e: KeyboardEvent): void {
|
|
43
|
+
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key === 'c'
|
|
44
|
+
&& input && hasSelection(input)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
onkeydown?.(e);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (!disabled && input) input.focus();
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<div
|
|
56
|
+
class="sh3-cmdline"
|
|
57
|
+
class:sh3-cmdline--disabled={disabled}
|
|
58
|
+
data-sh3-passthrough-modifiers
|
|
59
|
+
>
|
|
60
|
+
{#if prefix}<span class="sh3-cmdline__prefix">{@render prefix()}</span>{/if}
|
|
61
|
+
<!--
|
|
62
|
+
Two hidden inputs sit before the real one so Firefox doesn't try to
|
|
63
|
+
autofill saved credentials into a single visible text field. Source:
|
|
64
|
+
https://stackoverflow.com/a/29852908 (CC BY-SA 3.0, Bob The Janitor).
|
|
65
|
+
-->
|
|
66
|
+
<input type="text" class="sh3-cmdline__decoy" tabindex="-1" aria-hidden="true" />
|
|
67
|
+
<input type="password" class="sh3-cmdline__decoy" tabindex="-1" aria-hidden="true" />
|
|
68
|
+
<input
|
|
69
|
+
bind:this={input}
|
|
70
|
+
bind:value
|
|
71
|
+
type="search"
|
|
72
|
+
{name}
|
|
73
|
+
{disabled}
|
|
74
|
+
onkeydown={handleKeyDown}
|
|
75
|
+
spellcheck="false"
|
|
76
|
+
autocomplete="off"
|
|
77
|
+
autocapitalize="off"
|
|
78
|
+
aria-autocomplete="none"
|
|
79
|
+
data-1p-ignore
|
|
80
|
+
data-lpignore="true"
|
|
81
|
+
class="sh3-cmdline__input"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.sh3-cmdline {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: baseline;
|
|
89
|
+
gap: 6px;
|
|
90
|
+
padding: 4px 8px;
|
|
91
|
+
border: 1px solid transparent;
|
|
92
|
+
border-radius: var(--shell-radius);
|
|
93
|
+
font-family: var(--shell-font-mono, monospace);
|
|
94
|
+
font-size: var(--shell-font-size, 13px);
|
|
95
|
+
line-height: 1.4;
|
|
96
|
+
transition: border-color var(--shell-motion-fast) var(--shell-ease-standard),
|
|
97
|
+
box-shadow var(--shell-motion-fast) var(--shell-ease-standard);
|
|
98
|
+
}
|
|
99
|
+
.sh3-cmdline:focus-within {
|
|
100
|
+
border-color: var(--shell-input-border-focus);
|
|
101
|
+
box-shadow: var(--shell-focus-ring);
|
|
102
|
+
}
|
|
103
|
+
.sh3-cmdline__prefix {
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
font: inherit;
|
|
106
|
+
line-height: inherit;
|
|
107
|
+
margin: 0;
|
|
108
|
+
}
|
|
109
|
+
.sh3-cmdline__input {
|
|
110
|
+
flex: 1 1 auto;
|
|
111
|
+
padding: 0;
|
|
112
|
+
margin: 0;
|
|
113
|
+
background: transparent;
|
|
114
|
+
border: 0;
|
|
115
|
+
outline: 0;
|
|
116
|
+
color: inherit;
|
|
117
|
+
font: inherit;
|
|
118
|
+
line-height: inherit;
|
|
119
|
+
-webkit-appearance: none;
|
|
120
|
+
appearance: none;
|
|
121
|
+
}
|
|
122
|
+
.sh3-cmdline__input::-webkit-search-cancel-button,
|
|
123
|
+
.sh3-cmdline__input::-webkit-search-decoration,
|
|
124
|
+
.sh3-cmdline__input::-webkit-search-results-button,
|
|
125
|
+
.sh3-cmdline__input::-webkit-search-results-decoration {
|
|
126
|
+
display: none;
|
|
127
|
+
}
|
|
128
|
+
/* The .sh3-cmdline row owns the focus ring; suppress base.css's global
|
|
129
|
+
input:focus-visible box-shadow so the rings don't double up. */
|
|
130
|
+
.sh3-cmdline__input:focus,
|
|
131
|
+
.sh3-cmdline__input:focus-visible {
|
|
132
|
+
outline: none;
|
|
133
|
+
box-shadow: none;
|
|
134
|
+
border: none;
|
|
135
|
+
}
|
|
136
|
+
.sh3-cmdline__decoy {
|
|
137
|
+
display: none;
|
|
138
|
+
}
|
|
139
|
+
.sh3-cmdline--disabled .sh3-cmdline__input {
|
|
140
|
+
opacity: 0.5;
|
|
141
|
+
cursor: default;
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Single-line command entry widget. The chrome (focus ring, prefix slot,
|
|
4
|
+
* monospace font, anti-credential-fill attrs) is owned here; consumers
|
|
5
|
+
* supply the keybinding semantics via `onkeydown`.
|
|
6
|
+
*
|
|
7
|
+
* Two behaviors are baked in so every consumer gets them for free:
|
|
8
|
+
* - Selection-aware Ctrl+C: when the input has a non-empty selection,
|
|
9
|
+
* the keystroke is NOT forwarded to onkeydown — the browser does its
|
|
10
|
+
* native copy. This avoids the consumer mapping Ctrl+C to "clear
|
|
11
|
+
* draft" or "send SIGINT" eating a copy operation.
|
|
12
|
+
* - data-sh3-passthrough-modifiers on the row: the dispatcher's
|
|
13
|
+
* "block shortcuts in inputs" rule lets Ctrl/Alt/Meta-bearing
|
|
14
|
+
* shortcuts through this widget, so global bindings like Ctrl+K
|
|
15
|
+
* fire while the user is typing.
|
|
16
|
+
*/
|
|
17
|
+
interface Props {
|
|
18
|
+
value: string;
|
|
19
|
+
prefix?: Snippet;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
name?: string;
|
|
22
|
+
onkeydown?: (e: KeyboardEvent) => void;
|
|
23
|
+
}
|
|
24
|
+
declare const CommandLine: import("svelte").Component<Props, {}, "value">;
|
|
25
|
+
type CommandLine = ReturnType<typeof CommandLine>;
|
|
26
|
+
export default CommandLine;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/svelte';
|
|
3
|
+
import CommandLine from './CommandLine.svelte';
|
|
4
|
+
function realInput(container) {
|
|
5
|
+
// The widget renders two FF-credential decoy inputs ahead of the real one.
|
|
6
|
+
return container.querySelector('input.sh3-cmdline__input');
|
|
7
|
+
}
|
|
8
|
+
describe('CommandLine selection-aware Ctrl+C', () => {
|
|
9
|
+
it('does NOT forward Ctrl+C to onkeydown when input has a non-empty selection', async () => {
|
|
10
|
+
const onkeydown = vi.fn();
|
|
11
|
+
const { container } = render(CommandLine, { props: { value: 'hello world', onkeydown } });
|
|
12
|
+
const inp = realInput(container);
|
|
13
|
+
inp.focus();
|
|
14
|
+
inp.setSelectionRange(0, 5); // select "hello"
|
|
15
|
+
await fireEvent.keyDown(inp, { key: 'c', ctrlKey: true });
|
|
16
|
+
expect(onkeydown).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
it('forwards Ctrl+C to onkeydown when there is no selection', async () => {
|
|
19
|
+
const onkeydown = vi.fn();
|
|
20
|
+
const { container } = render(CommandLine, { props: { value: 'hello', onkeydown } });
|
|
21
|
+
const inp = realInput(container);
|
|
22
|
+
inp.focus();
|
|
23
|
+
inp.setSelectionRange(3, 3); // caret only, no selection
|
|
24
|
+
await fireEvent.keyDown(inp, { key: 'c', ctrlKey: true });
|
|
25
|
+
expect(onkeydown).toHaveBeenCalledTimes(1);
|
|
26
|
+
});
|
|
27
|
+
it('forwards non-Ctrl+C keys regardless of selection', async () => {
|
|
28
|
+
const onkeydown = vi.fn();
|
|
29
|
+
const { container } = render(CommandLine, { props: { value: 'hello', onkeydown } });
|
|
30
|
+
const inp = realInput(container);
|
|
31
|
+
inp.focus();
|
|
32
|
+
inp.setSelectionRange(0, 5);
|
|
33
|
+
await fireEvent.keyDown(inp, { key: 'k', ctrlKey: true });
|
|
34
|
+
expect(onkeydown).toHaveBeenCalledTimes(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('CommandLine modifier passthrough attribute', () => {
|
|
38
|
+
it('stamps data-sh3-passthrough-modifiers so the dispatcher lets modifier shortcuts through', () => {
|
|
39
|
+
const { container } = render(CommandLine, { props: { value: '' } });
|
|
40
|
+
const row = container.querySelector('.sh3-cmdline');
|
|
41
|
+
expect(row.hasAttribute('data-sh3-passthrough-modifiers')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SessionClient } from './session-client.svelte';
|
|
3
|
+
import CommandLine from './CommandLine.svelte';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
cwd: string;
|
|
7
|
+
/** When false, hide the cwd chip in front of the prompt. */
|
|
8
|
+
showCwd?: boolean;
|
|
6
9
|
locked: boolean; // true while a process is running
|
|
7
10
|
history: string[]; // persisted history, newest last
|
|
8
11
|
session: SessionClient;
|
|
9
12
|
onSubmit: (line: string) => void; // called with the raw entered line
|
|
10
13
|
}
|
|
11
|
-
let { cwd, locked, history, session, onSubmit }: Props = $props();
|
|
14
|
+
let { cwd, showCwd = true, locked, history, session, onSubmit }: Props = $props();
|
|
12
15
|
|
|
13
16
|
let draft = $state('');
|
|
14
17
|
let historyIndex = $state<number | null>(null); // null = live draft
|
|
15
18
|
let savedDraft = $state(''); // restored when user returns from history
|
|
16
19
|
|
|
17
|
-
let input: HTMLInputElement | null = $state(null);
|
|
18
|
-
|
|
19
20
|
function submit() {
|
|
20
21
|
if (locked) return;
|
|
21
22
|
const line = draft;
|
|
@@ -74,6 +75,8 @@
|
|
|
74
75
|
e.preventDefault();
|
|
75
76
|
navHistoryDown();
|
|
76
77
|
} else if (e.ctrlKey && e.key === 'c') {
|
|
78
|
+
// CommandLine has already filtered this case to no-selection, so the
|
|
79
|
+
// user is asking to clear the draft, not copy.
|
|
77
80
|
e.preventDefault();
|
|
78
81
|
draft = '';
|
|
79
82
|
historyIndex = null;
|
|
@@ -83,52 +86,26 @@
|
|
|
83
86
|
submit();
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
|
-
|
|
87
|
-
// Re-focus the input when the locked state flips to false
|
|
88
|
-
$effect(() => {
|
|
89
|
-
if (!locked && input) {
|
|
90
|
-
input.focus();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
89
|
</script>
|
|
94
90
|
|
|
95
|
-
<div class="shell-input"
|
|
96
|
-
<
|
|
97
|
-
<span class="shell-input-arrow">❯</span>
|
|
98
|
-
<input
|
|
99
|
-
bind:this={input}
|
|
91
|
+
<div class="shell-input-row">
|
|
92
|
+
<CommandLine
|
|
100
93
|
bind:value={draft}
|
|
101
|
-
type="text"
|
|
102
94
|
disabled={locked}
|
|
95
|
+
name="shell-cmdline"
|
|
103
96
|
onkeydown={onKeyDown}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
>
|
|
98
|
+
{#snippet prefix()}
|
|
99
|
+
{#if showCwd}<span class="shell-input-cwd">{cwd}</span>{/if}
|
|
100
|
+
<span class="shell-input-arrow">❯</span>
|
|
101
|
+
{/snippet}
|
|
102
|
+
</CommandLine>
|
|
109
103
|
</div>
|
|
110
104
|
|
|
111
105
|
<style>
|
|
112
|
-
.shell-input {
|
|
113
|
-
display: flex;
|
|
114
|
-
gap: 8px;
|
|
115
|
-
padding: 4px 8px;
|
|
106
|
+
.shell-input-row {
|
|
116
107
|
border-top: 1px solid var(--shell-border, #333);
|
|
117
|
-
font-family: var(--shell-font-mono, monospace);
|
|
118
108
|
}
|
|
119
109
|
.shell-input-cwd { color: var(--shell-fg-muted, #888); }
|
|
120
|
-
.shell-input-arrow { color: var(--shell-accent, #6cf); }
|
|
121
|
-
.shell-input-field {
|
|
122
|
-
padding: 0;
|
|
123
|
-
flex: 1 1 auto;
|
|
124
|
-
background: transparent;
|
|
125
|
-
border: 0;
|
|
126
|
-
outline: 0;
|
|
127
|
-
color: var(--shell-fg, #ddd);
|
|
128
|
-
font: inherit;
|
|
129
|
-
}
|
|
130
|
-
.shell-input.locked .shell-input-field {
|
|
131
|
-
opacity: 0.5;
|
|
132
|
-
cursor: default;
|
|
133
|
-
}
|
|
110
|
+
.shell-input-arrow { color: var(--shell-accent, #6cf); margin-left: 6px; }
|
|
134
111
|
</style>
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
import PromptEntry from './entries/PromptEntry.svelte';
|
|
5
5
|
import StatusEntry from './entries/StatusEntry.svelte';
|
|
6
6
|
import RichEntry from './entries/RichEntry.svelte';
|
|
7
|
+
import { lookupRichComponent } from './rich-registry';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
scrollback: Scrollback;
|
|
11
|
+
showPromptCwd: boolean;
|
|
10
12
|
}
|
|
11
|
-
let { scrollback }: Props = $props();
|
|
13
|
+
let { scrollback, showPromptCwd }: Props = $props();
|
|
12
14
|
|
|
13
15
|
let container: HTMLDivElement | null = $state(null);
|
|
14
16
|
let content: HTMLDivElement | null = $state(null);
|
|
@@ -48,11 +50,16 @@
|
|
|
48
50
|
{#if entry.kind === 'text'}
|
|
49
51
|
<TextEntry stream={entry.stream} chunks={entry.chunks} />
|
|
50
52
|
{:else if entry.kind === 'prompt'}
|
|
51
|
-
<PromptEntry cwd={entry.cwd} line={entry.line} />
|
|
53
|
+
<PromptEntry cwd={showPromptCwd ? entry.cwd : ''} line={entry.line} />
|
|
52
54
|
{:else if entry.kind === 'status'}
|
|
53
55
|
<StatusEntry text={entry.text} level={entry.level} />
|
|
54
56
|
{:else if entry.kind === 'rich'}
|
|
55
|
-
|
|
57
|
+
{@const comp = entry.component ?? (entry.componentKey ? lookupRichComponent(entry.componentKey) : null)}
|
|
58
|
+
{#if comp}
|
|
59
|
+
<RichEntry component={comp} componentProps={entry.props} />
|
|
60
|
+
{:else}
|
|
61
|
+
<StatusEntry text={`<rich entry: ${entry.componentKey ?? 'unknown'} not registered>`} level="info" />
|
|
62
|
+
{/if}
|
|
56
63
|
{/if}
|
|
57
64
|
{/each}
|
|
58
65
|
</div>
|