sh3-core 0.15.1 → 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-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 +17 -0
- package/dist/overlays/float.d.ts +17 -1
- package/dist/overlays/float.js +16 -0
- package/dist/overlays/float.test.js +35 -0
- package/dist/sh3core-shard/ShellHome.svelte +3 -0
- package/dist/shards/activate.svelte.js +11 -2
- package/dist/shards/types.d.ts +33 -1
- 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 +93 -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 +31 -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 +7 -0
- package/dist/shell-shard/verbs/env.js +4 -0
- package/dist/shell-shard/verbs/help.js +4 -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 +4 -0
- package/dist/shell-shard/verbs/views.js +4 -0
- package/dist/shell-shard/verbs/zones.js +7 -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 -35
- 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 -30
- 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 -99
- /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
package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte}
RENAMED
|
@@ -1,58 +1,65 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* EntityAppearanceModal — generalization of the per-entity Customize
|
|
4
|
+
* modal. Used by __app-appearance__ for apps and __layouts__ for saved
|
|
5
|
+
* layouts. Purely presentational: callers pass an
|
|
6
|
+
* onSave({ label, icon, color }) callback and decide what an empty
|
|
7
|
+
* label means (apps treat empty as "use manifest"; layouts mark the
|
|
8
|
+
* input required via requireLabel: true).
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { untrack } from 'svelte';
|
|
10
12
|
import IconPicker from '../primitives/widgets/IconPicker.svelte';
|
|
11
13
|
import ColorSwatch from '../primitives/widgets/ColorSwatch.svelte';
|
|
12
14
|
import iconsUrl from '../assets/icons.svg';
|
|
13
|
-
import { listRegisteredApps } from '../api';
|
|
14
|
-
import { getAppearance, setAppearance } from './appearanceState.svelte';
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
entityLabel: string;
|
|
18
|
+
initialAppearance?: { icon?: string; color?: string };
|
|
19
|
+
defaultIcon: string;
|
|
20
|
+
requireLabel: boolean;
|
|
21
|
+
onSave(next: { label: string; icon?: string; color?: string }): void;
|
|
22
|
+
onReset(): void;
|
|
19
23
|
close: () => void;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
let {
|
|
26
|
+
let {
|
|
27
|
+
entityLabel,
|
|
28
|
+
initialAppearance,
|
|
29
|
+
defaultIcon,
|
|
30
|
+
requireLabel,
|
|
31
|
+
onSave,
|
|
32
|
+
onReset,
|
|
33
|
+
close,
|
|
34
|
+
}: Props = $props();
|
|
23
35
|
|
|
24
|
-
const initial = untrack(() =>
|
|
25
|
-
const manifestIcon = untrack(
|
|
26
|
-
() => listRegisteredApps().find((m) => m.id === appId)?.icon,
|
|
27
|
-
);
|
|
36
|
+
const initial = untrack(() => initialAppearance);
|
|
28
37
|
|
|
29
38
|
let icon = $state<string | undefined>(initial?.icon);
|
|
30
39
|
let color = $state<string | undefined>(initial?.color);
|
|
31
|
-
let label = $state<string>(
|
|
40
|
+
let label = $state<string>(untrack(() => entityLabel));
|
|
32
41
|
let pickerOpen = $state<boolean>(initial?.icon !== undefined);
|
|
33
42
|
|
|
43
|
+
const trimmed = $derived(label.trim());
|
|
44
|
+
const saveDisabled = $derived(requireLabel && trimmed === '');
|
|
45
|
+
const effectiveLabel = $derived(trimmed === '' ? entityLabel : trimmed);
|
|
46
|
+
const effectiveIcon = $derived(icon ?? defaultIcon);
|
|
34
47
|
const hasOverride = $derived(initial !== undefined);
|
|
35
|
-
const effectiveLabel = $derived(label.trim() === '' ? appLabel : label.trim());
|
|
36
|
-
const effectiveIcon = $derived(icon ?? manifestIcon ?? 'box');
|
|
37
48
|
|
|
38
49
|
function save() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
icon,
|
|
42
|
-
color,
|
|
43
|
-
label: trimmed === '' ? undefined : trimmed,
|
|
44
|
-
});
|
|
50
|
+
if (saveDisabled) return;
|
|
51
|
+
onSave({ label: trimmed, icon, color });
|
|
45
52
|
close();
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
function reset() {
|
|
49
|
-
|
|
56
|
+
onReset();
|
|
50
57
|
close();
|
|
51
58
|
}
|
|
52
59
|
</script>
|
|
53
60
|
|
|
54
61
|
<div class="app-appearance">
|
|
55
|
-
<h2>Customize {
|
|
62
|
+
<h2>Customize {entityLabel}</h2>
|
|
56
63
|
|
|
57
64
|
<div class="preview">
|
|
58
65
|
<div
|
|
@@ -67,11 +74,11 @@
|
|
|
67
74
|
</div>
|
|
68
75
|
</div>
|
|
69
76
|
|
|
70
|
-
<label class="row"><span>Name <em>(empty = default)</em
|
|
77
|
+
<label class="row"><span>Name {#if !requireLabel}<em>(empty = default)</em>{/if}</span>
|
|
71
78
|
<input
|
|
72
79
|
type="text"
|
|
73
80
|
bind:value={label}
|
|
74
|
-
placeholder={
|
|
81
|
+
placeholder={entityLabel}
|
|
75
82
|
class="name-input"
|
|
76
83
|
/>
|
|
77
84
|
</label>
|
|
@@ -95,9 +102,9 @@
|
|
|
95
102
|
</label>
|
|
96
103
|
|
|
97
104
|
<div class="actions">
|
|
98
|
-
<button type="button" class="primary" onclick={save}>Save</button>
|
|
99
|
-
<button type="button" onclick={reset} disabled={!hasOverride}>Reset</button>
|
|
100
|
-
<button type="button" onclick={close}>Cancel</button>
|
|
105
|
+
<button type="button" class="primary" onclick={save} disabled={saveDisabled}>Save</button>
|
|
106
|
+
<button type="button" data-role="reset" onclick={reset} disabled={!hasOverride}>Reset</button>
|
|
107
|
+
<button type="button" onclick={() => close()}>Cancel</button>
|
|
101
108
|
</div>
|
|
102
109
|
</div>
|
|
103
110
|
|
|
@@ -158,8 +165,6 @@
|
|
|
158
165
|
-webkit-line-clamp: 2;
|
|
159
166
|
line-clamp: 2;
|
|
160
167
|
}
|
|
161
|
-
.preview-card-icon { width: 24px; height: 24px; color: var(--shell-fg); }
|
|
162
|
-
.preview-card-label { font-weight: 600; padding: 0 4px; line-height: 1.2; }
|
|
163
168
|
.actions { display: flex; gap: 8px; margin-top: 16px; }
|
|
164
169
|
.actions button {
|
|
165
170
|
background: var(--shell-bg-elevated);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
entityLabel: string;
|
|
3
|
+
initialAppearance?: {
|
|
4
|
+
icon?: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
};
|
|
7
|
+
defaultIcon: string;
|
|
8
|
+
requireLabel: boolean;
|
|
9
|
+
onSave(next: {
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
}): void;
|
|
14
|
+
onReset(): void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
}
|
|
17
|
+
declare const EntityAppearanceModal: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type EntityAppearanceModal = ReturnType<typeof EntityAppearanceModal>;
|
|
19
|
+
export default EntityAppearanceModal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { mount, flushSync } from 'svelte';
|
|
3
|
+
import EntityAppearanceModal from './EntityAppearanceModal.svelte';
|
|
4
|
+
function mountModal(props = {}) {
|
|
5
|
+
const target = document.createElement('div');
|
|
6
|
+
document.body.appendChild(target);
|
|
7
|
+
const onSave = vi.fn();
|
|
8
|
+
const onReset = vi.fn();
|
|
9
|
+
const close = vi.fn();
|
|
10
|
+
const instance = mount(EntityAppearanceModal, {
|
|
11
|
+
target,
|
|
12
|
+
props: Object.assign({ entityLabel: 'My App', defaultIcon: 'box', requireLabel: false, onSave,
|
|
13
|
+
onReset,
|
|
14
|
+
close }, props),
|
|
15
|
+
});
|
|
16
|
+
return { target, onSave, onReset, close, instance };
|
|
17
|
+
}
|
|
18
|
+
describe('EntityAppearanceModal', () => {
|
|
19
|
+
it('renders the entityLabel as title placeholder', () => {
|
|
20
|
+
const { target } = mountModal();
|
|
21
|
+
const input = target.querySelector('input.name-input');
|
|
22
|
+
expect(input.placeholder).toBe('My App');
|
|
23
|
+
});
|
|
24
|
+
it('shows the "(empty = default)" hint when requireLabel is false', () => {
|
|
25
|
+
const { target } = mountModal({ requireLabel: false });
|
|
26
|
+
expect(target.textContent).toContain('(empty = default)');
|
|
27
|
+
});
|
|
28
|
+
it('hides the "(empty = default)" hint when requireLabel is true', () => {
|
|
29
|
+
const { target } = mountModal({ requireLabel: true });
|
|
30
|
+
expect(target.textContent).not.toContain('(empty = default)');
|
|
31
|
+
});
|
|
32
|
+
it('disables Save when requireLabel is true and the input is empty', async () => {
|
|
33
|
+
const { target } = mountModal({ requireLabel: true, entityLabel: '' });
|
|
34
|
+
flushSync();
|
|
35
|
+
const save = target.querySelector('button.primary');
|
|
36
|
+
expect(save.disabled).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('calls onSave with the trimmed label, icon, color', async () => {
|
|
39
|
+
const { target, onSave } = mountModal({
|
|
40
|
+
initialAppearance: { icon: 'cog', color: '#ff0000' },
|
|
41
|
+
});
|
|
42
|
+
const input = target.querySelector('input.name-input');
|
|
43
|
+
input.value = ' Renamed ';
|
|
44
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
45
|
+
flushSync();
|
|
46
|
+
target.querySelector('button.primary').click();
|
|
47
|
+
expect(onSave).toHaveBeenCalledWith({ label: 'Renamed', icon: 'cog', color: '#ff0000' });
|
|
48
|
+
});
|
|
49
|
+
it('calls onReset and close when Reset is clicked', async () => {
|
|
50
|
+
const { target, onReset, close } = mountModal({
|
|
51
|
+
initialAppearance: { icon: 'cog' },
|
|
52
|
+
});
|
|
53
|
+
target.querySelector('button[data-role="reset"]').click();
|
|
54
|
+
expect(onReset).toHaveBeenCalled();
|
|
55
|
+
expect(close).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -31,6 +31,20 @@
|
|
|
31
31
|
import { registerDismissableFrame, unregisterDismissableFrame } from './floatDismiss';
|
|
32
32
|
import { computeMinSize } from '../layout/floats';
|
|
33
33
|
import type { FloatEntry } from '../layout/types';
|
|
34
|
+
import { shell } from '../shellRuntime.svelte';
|
|
35
|
+
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
36
|
+
|
|
37
|
+
const floatHeaderSelection = makeSelectionApi('__layouts__');
|
|
38
|
+
|
|
39
|
+
function openHeaderContextMenu(e: MouseEvent): void {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
floatHeaderSelection.set({ type: 'float-header', ref: { floatId: entry.id } });
|
|
42
|
+
shell.actions.openContextMenu({
|
|
43
|
+
x: e.clientX,
|
|
44
|
+
y: e.clientY,
|
|
45
|
+
scope: { element: 'float-header' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
interface Props {
|
|
36
50
|
entry: FloatEntry;
|
|
@@ -183,11 +197,14 @@
|
|
|
183
197
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
184
198
|
<header
|
|
185
199
|
class="sh3-float-header"
|
|
200
|
+
data-sh3-scope="element:float-header"
|
|
201
|
+
data-float-id={entry.id}
|
|
186
202
|
onpointerdown={onHeaderPointerDown}
|
|
187
203
|
onpointermove={onHeaderPointerMove}
|
|
188
204
|
onpointerup={onHeaderPointerUp}
|
|
189
205
|
onpointercancel={onHeaderPointerUp}
|
|
190
206
|
ondblclick={onHeaderDblClick}
|
|
207
|
+
oncontextmenu={openHeaderContextMenu}
|
|
191
208
|
>
|
|
192
209
|
<span class="sh3-float-title">{entry.title}</span>
|
|
193
210
|
<span class="sh3-float-header-actions">
|
package/dist/overlays/float.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FloatEntry } from '../layout/types';
|
|
1
|
+
import type { LayoutNode, FloatEntry } from '../layout/types';
|
|
2
2
|
import type { Size } from '../layout/floats';
|
|
3
3
|
export interface FloatOptions {
|
|
4
4
|
title?: string;
|
|
@@ -29,6 +29,22 @@ export interface FloatOptions {
|
|
|
29
29
|
}
|
|
30
30
|
export interface FloatManager {
|
|
31
31
|
open(viewId: string, options?: FloatOptions): string;
|
|
32
|
+
/**
|
|
33
|
+
* Open a float with a pre-built content tree. Used by save/restore flows
|
|
34
|
+
* (e.g. saved layouts) where the caller has already arranged the
|
|
35
|
+
* structure rather than starting from a single view id. The `content`
|
|
36
|
+
* tree is stored verbatim — the caller is responsible for cloning if it
|
|
37
|
+
* shouldn't be aliased to a source.
|
|
38
|
+
*/
|
|
39
|
+
openWithContent(options: {
|
|
40
|
+
content: LayoutNode;
|
|
41
|
+
size: Size;
|
|
42
|
+
title?: string;
|
|
43
|
+
position?: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
};
|
|
47
|
+
}): string;
|
|
32
48
|
close(floatId: string): void;
|
|
33
49
|
list(): FloatEntry[];
|
|
34
50
|
focus(floatId: string): void;
|
package/dist/overlays/float.js
CHANGED
|
@@ -137,6 +137,21 @@ function openFloat(viewId, options = {}) {
|
|
|
137
137
|
store.push(entry);
|
|
138
138
|
return id;
|
|
139
139
|
}
|
|
140
|
+
function openFloatWithContent(options) {
|
|
141
|
+
var _a;
|
|
142
|
+
const store = activeStore();
|
|
143
|
+
const id = generateFloatId();
|
|
144
|
+
const position = (_a = options.position) !== null && _a !== void 0 ? _a : cascadePosition(store, getTreeBounds());
|
|
145
|
+
const entry = {
|
|
146
|
+
id,
|
|
147
|
+
content: options.content,
|
|
148
|
+
position,
|
|
149
|
+
size: options.size,
|
|
150
|
+
title: options.title,
|
|
151
|
+
};
|
|
152
|
+
store.push(entry);
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
140
155
|
function closeFloat(floatId) {
|
|
141
156
|
const store = activeStore();
|
|
142
157
|
const idx = store.findIndex((f) => f.id === floatId);
|
|
@@ -211,6 +226,7 @@ function isMaximizedFloat(id) {
|
|
|
211
226
|
}
|
|
212
227
|
export const floatManager = {
|
|
213
228
|
open: openFloat,
|
|
229
|
+
openWithContent: openFloatWithContent,
|
|
214
230
|
close: closeFloat,
|
|
215
231
|
list: listFloats,
|
|
216
232
|
focus: focusFloat,
|
|
@@ -836,3 +836,38 @@ describe('floats — F.20 dismissable + grip', () => {
|
|
|
836
836
|
expect(floatManager.list().some((f) => f.id === id)).toBe(true);
|
|
837
837
|
});
|
|
838
838
|
});
|
|
839
|
+
describe('floatManager.openWithContent', () => {
|
|
840
|
+
beforeEach(() => __resetFloatManagerForTest());
|
|
841
|
+
it('opens a float whose entry.content equals the supplied tree', () => {
|
|
842
|
+
const content = {
|
|
843
|
+
type: 'tabs',
|
|
844
|
+
activeTab: 0,
|
|
845
|
+
tabs: [{ slotId: 'restored:1', viewId: 'shell:terminal', label: 'Shell' }],
|
|
846
|
+
};
|
|
847
|
+
const id = floatManager.openWithContent({
|
|
848
|
+
content,
|
|
849
|
+
size: { w: 700, h: 500 },
|
|
850
|
+
title: 'My Layout',
|
|
851
|
+
});
|
|
852
|
+
const list = floatManager.list();
|
|
853
|
+
expect(list).toHaveLength(1);
|
|
854
|
+
expect(list[0].id).toBe(id);
|
|
855
|
+
expect(list[0].content).toEqual(content);
|
|
856
|
+
expect(list[0].size).toEqual({ w: 700, h: 500 });
|
|
857
|
+
expect(list[0].title).toBe('My Layout');
|
|
858
|
+
});
|
|
859
|
+
it('uses the cascade default position when not given one', () => {
|
|
860
|
+
const content = {
|
|
861
|
+
type: 'slot',
|
|
862
|
+
slotId: 'restored:2',
|
|
863
|
+
viewId: 'shell:terminal',
|
|
864
|
+
};
|
|
865
|
+
const id = floatManager.openWithContent({
|
|
866
|
+
content,
|
|
867
|
+
size: { w: 600, h: 400 },
|
|
868
|
+
});
|
|
869
|
+
const entry = floatManager.list().find((f) => f.id === id);
|
|
870
|
+
expect(typeof entry.position.x).toBe('number');
|
|
871
|
+
expect(typeof entry.position.y).toBe('number');
|
|
872
|
+
});
|
|
873
|
+
});
|
|
@@ -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>
|
|
@@ -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
|
|
@@ -217,6 +219,13 @@ export async function activateShard(id, opts) {
|
|
|
217
219
|
runVerb(shardId, name, args, opts) {
|
|
218
220
|
return runVerbProgrammatic(shardId, name, args, opts);
|
|
219
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
|
+
},
|
|
220
229
|
};
|
|
221
230
|
entry.ctx = ctx;
|
|
222
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
|
/**
|
|
@@ -296,6 +296,38 @@ export interface ShardContext {
|
|
|
296
296
|
result: unknown;
|
|
297
297
|
scrollback: ScrollbackEntry[];
|
|
298
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>;
|
|
299
331
|
}
|
|
300
332
|
/**
|
|
301
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
|
+
});
|