sh3-core 0.23.2 → 0.25.0
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/BrandSlot.svelte +62 -3
- package/dist/BrandSlot.test.js +52 -0
- package/dist/Sh3.svelte +4 -4
- package/dist/actions/listActive.js +1 -0
- package/dist/actions/listActive.test.js +13 -0
- package/dist/actions/types.d.ts +12 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +2 -0
- package/dist/app/store/StoreView.svelte +1 -1
- package/dist/apps/types.d.ts +8 -0
- package/dist/chrome/MenuSheet.svelte +19 -6
- package/dist/contributions/contextSource.d.ts +48 -0
- package/dist/contributions/contextSource.js +21 -0
- package/dist/documents/picker-primitive.d.ts +0 -9
- package/dist/documents/picker-primitive.js +0 -9
- package/dist/layout/store.svelte.js +1 -1
- package/dist/overlays/presets.d.ts +17 -2
- package/dist/overlays/presets.js +28 -2
- package/dist/overlays/presets.test.js +29 -0
- package/dist/primitives/widgets/DocumentFilePicker.svelte +9 -7
- package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +44 -27
- package/dist/primitives/widgets/_DocumentBrowser.svelte +4 -4
- package/dist/registry/installer.js +50 -10
- package/dist/registry/installer.test.d.ts +1 -0
- package/dist/registry/installer.test.js +146 -0
- package/dist/registry/types.d.ts +19 -0
- package/dist/runtime/runVerb.test.js +87 -0
- package/dist/sh3core-shard/Sh3Home.svelte +0 -1
- package/dist/shards/lifecycle.svelte.d.ts +8 -0
- package/dist/shards/lifecycle.svelte.js +17 -0
- package/dist/shell-shard/verbs/xfer.js +66 -4
- package/dist/shell-shard/verbs/xfer.test.js +74 -0
- package/dist/transport/apiFetch.js +21 -3
- package/dist/transport/apiFetch.test.js +63 -0
- package/dist/verbs/types.d.ts +49 -12
- package/dist/verbs/types.test.d.ts +1 -0
- package/dist/verbs/types.test.js +43 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/BrandSlot.svelte
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
import { getBreadcrumbAppId, getRegisteredApp } from './apps/registry.svelte';
|
|
16
16
|
import { sessionState, switchProjectScope } from './projects/session-state.svelte';
|
|
17
17
|
import { projectsState } from './projects-shard/projectsShard.svelte';
|
|
18
|
+
import { presetManager } from './overlays/presets';
|
|
19
|
+
|
|
20
|
+
function titleCasePresetName(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.split(/[-_]/)
|
|
23
|
+
.filter((part) => part.length > 0)
|
|
24
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
25
|
+
.join(' ');
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
const activeAppId = $derived(getLiveDispatcherState().activeAppId);
|
|
20
29
|
const breadcrumbId = $derived(getBreadcrumbAppId());
|
|
@@ -30,15 +39,44 @@
|
|
|
30
39
|
projectId ? projectsState.projects.find((p) => p.id === projectId)?.name ?? projectId : null,
|
|
31
40
|
);
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
// Preset state is only meaningful while an app is attached. Existing
|
|
43
|
+
// BrandSlot tests set activeAppId without binding a preset blob, so the
|
|
44
|
+
// try/catch here is necessary, not just defensive.
|
|
45
|
+
const offDefault = $derived.by(() => {
|
|
46
|
+
if (activeAppId === null) return false;
|
|
47
|
+
try {
|
|
48
|
+
return presetManager.active() !== presetManager.default();
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const presetLabel = $derived.by(() => {
|
|
55
|
+
if (!offDefault) return null;
|
|
56
|
+
try {
|
|
57
|
+
return titleCasePresetName(presetManager.active());
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
type Mode =
|
|
64
|
+
| 'brand'
|
|
65
|
+
| 'app'
|
|
66
|
+
| 'app-off-default'
|
|
67
|
+
| 'breadcrumb'
|
|
68
|
+
| 'project-home'
|
|
69
|
+
| 'project-app'
|
|
70
|
+
| 'project-app-off-default'
|
|
71
|
+
| 'project-breadcrumb';
|
|
34
72
|
|
|
35
73
|
const mode: Mode = $derived.by(() => {
|
|
36
74
|
if (projectId) {
|
|
37
|
-
if (activeAppId) return 'project-app';
|
|
75
|
+
if (activeAppId) return offDefault ? 'project-app-off-default' : 'project-app';
|
|
38
76
|
if (breadcrumbId) return 'project-breadcrumb';
|
|
39
77
|
return 'project-home';
|
|
40
78
|
}
|
|
41
|
-
if (activeAppId) return 'app';
|
|
79
|
+
if (activeAppId) return offDefault ? 'app-off-default' : 'app';
|
|
42
80
|
if (breadcrumbId) return 'breadcrumb';
|
|
43
81
|
return 'brand';
|
|
44
82
|
});
|
|
@@ -54,6 +92,14 @@
|
|
|
54
92
|
function reenterProjectHome() {
|
|
55
93
|
if (activeAppId) void returnToHome();
|
|
56
94
|
}
|
|
95
|
+
|
|
96
|
+
function backToDefaultPreset() {
|
|
97
|
+
try {
|
|
98
|
+
presetManager.switchToDefault();
|
|
99
|
+
} catch {
|
|
100
|
+
// Blob unbound mid-click — nothing to do.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
57
103
|
</script>
|
|
58
104
|
|
|
59
105
|
<div class="sh3-brand-slot">
|
|
@@ -61,6 +107,10 @@
|
|
|
61
107
|
<span class="sh3-brand">SH3</span>
|
|
62
108
|
{:else if mode === 'app'}
|
|
63
109
|
<span class="sh3-brand sh3-brand-app">{activeLabel}</span>
|
|
110
|
+
{:else if mode === 'app-off-default'}
|
|
111
|
+
<button type="button" class="sh3-brand sh3-brand-app sh3-brand-clickable" onclick={backToDefaultPreset} title="Return to default layout">{activeLabel}</button>
|
|
112
|
+
<span class="sh3-brand-sep" aria-hidden="true">›</span>
|
|
113
|
+
<span class="sh3-brand-preset">{presetLabel}</span>
|
|
64
114
|
{:else if mode === 'breadcrumb'}
|
|
65
115
|
<span class="sh3-brand">SH3</span>
|
|
66
116
|
<span class="sh3-brand-sep" aria-hidden="true">›</span>
|
|
@@ -73,6 +123,12 @@
|
|
|
73
123
|
<span class="sh3-brand sh3-brand-project">{projectLabel}</span>
|
|
74
124
|
<span class="sh3-brand-sep" aria-hidden="true">›</span>
|
|
75
125
|
<span class="sh3-brand sh3-brand-app">{activeLabel}</span>
|
|
126
|
+
{:else if mode === 'project-app-off-default'}
|
|
127
|
+
<span class="sh3-brand sh3-brand-project">{projectLabel}</span>
|
|
128
|
+
<span class="sh3-brand-sep" aria-hidden="true">›</span>
|
|
129
|
+
<button type="button" class="sh3-brand sh3-brand-app sh3-brand-clickable" onclick={backToDefaultPreset} title="Return to default layout">{activeLabel}</button>
|
|
130
|
+
<span class="sh3-brand-sep" aria-hidden="true">›</span>
|
|
131
|
+
<span class="sh3-brand-preset">{presetLabel}</span>
|
|
76
132
|
{:else if mode === 'project-breadcrumb'}
|
|
77
133
|
<button type="button" class="sh3-brand sh3-brand-clickable" onclick={exitProject} title="Exit project">SH3</button>
|
|
78
134
|
<span class="sh3-brand-sep" aria-hidden="true">›</span>
|
|
@@ -126,4 +182,7 @@
|
|
|
126
182
|
.sh3-brand-clickable:hover {
|
|
127
183
|
background: var(--sh3-bg-elevated);
|
|
128
184
|
}
|
|
185
|
+
.sh3-brand-preset {
|
|
186
|
+
color: var(--sh3-fg-muted);
|
|
187
|
+
}
|
|
129
188
|
</style>
|
package/dist/BrandSlot.test.js
CHANGED
|
@@ -11,6 +11,7 @@ import BrandSlot from './BrandSlot.svelte';
|
|
|
11
11
|
import { setActiveApp, __resetDispatcherStateForTest } from './actions/state.svelte';
|
|
12
12
|
import { registerApp, __resetAppRegistryForTest, __resetBreadcrumbForTest, breadcrumbApp, } from './apps/registry.svelte';
|
|
13
13
|
import { launchApp } from './apps/lifecycle';
|
|
14
|
+
import { __bindPresetBlobForTest, __resetPresetManagerForTest, } from './overlays/presets';
|
|
14
15
|
let host;
|
|
15
16
|
let cmp = null;
|
|
16
17
|
function makeApp(id, label) {
|
|
@@ -27,6 +28,7 @@ beforeEach(() => {
|
|
|
27
28
|
document.body.appendChild(host);
|
|
28
29
|
__resetBreadcrumbForTest();
|
|
29
30
|
__resetDispatcherStateForTest();
|
|
31
|
+
__resetPresetManagerForTest();
|
|
30
32
|
});
|
|
31
33
|
afterEach(() => {
|
|
32
34
|
if (cmp) {
|
|
@@ -37,7 +39,20 @@ afterEach(() => {
|
|
|
37
39
|
__resetAppRegistryForTest();
|
|
38
40
|
__resetDispatcherStateForTest();
|
|
39
41
|
vi.clearAllMocks();
|
|
42
|
+
__resetPresetManagerForTest();
|
|
40
43
|
});
|
|
44
|
+
function bindBlobWithPresets(activePreset, names, defaultHint) {
|
|
45
|
+
const blob = {
|
|
46
|
+
layoutVersion: 1,
|
|
47
|
+
activePreset,
|
|
48
|
+
presets: Object.fromEntries(names.map((n) => [
|
|
49
|
+
n,
|
|
50
|
+
{ default: { docked: { type: 'slot', slotId: `${n}-s`, viewId: 'v' }, floats: [] } },
|
|
51
|
+
])),
|
|
52
|
+
};
|
|
53
|
+
__bindPresetBlobForTest(blob, defaultHint);
|
|
54
|
+
return blob;
|
|
55
|
+
}
|
|
41
56
|
describe('BrandSlot', () => {
|
|
42
57
|
it('renders SH3 when no app has launched this session', async () => {
|
|
43
58
|
var _a;
|
|
@@ -68,4 +83,41 @@ describe('BrandSlot', () => {
|
|
|
68
83
|
btn.click();
|
|
69
84
|
expect(launchApp).toHaveBeenCalledWith('app.a');
|
|
70
85
|
});
|
|
86
|
+
it('renders [App Name] (no preset chip) when active app is on default preset', async () => {
|
|
87
|
+
registerApp(makeApp('app.a', 'My App'));
|
|
88
|
+
breadcrumbApp.id = 'app.a';
|
|
89
|
+
setActiveApp('app.a', new Set());
|
|
90
|
+
bindBlobWithPresets('home', ['home', 'editing'], 'home');
|
|
91
|
+
cmp = mount(BrandSlot, { target: host, props: {} });
|
|
92
|
+
await tick();
|
|
93
|
+
expect(host.textContent).toContain('My App');
|
|
94
|
+
expect(host.textContent).not.toContain('Home');
|
|
95
|
+
expect(host.textContent).not.toContain('Editing');
|
|
96
|
+
// App segment is plain text (no button) in the default-preset case.
|
|
97
|
+
expect(host.querySelector('button')).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
it('renders [App Name] › [Preset] with App clickable when off default', async () => {
|
|
100
|
+
registerApp(makeApp('app.a', 'My App'));
|
|
101
|
+
breadcrumbApp.id = 'app.a';
|
|
102
|
+
setActiveApp('app.a', new Set());
|
|
103
|
+
const blob = bindBlobWithPresets('editing', ['home', 'editing'], 'home');
|
|
104
|
+
cmp = mount(BrandSlot, { target: host, props: {} });
|
|
105
|
+
await tick();
|
|
106
|
+
expect(host.textContent).toMatch(/My App.*Editing/);
|
|
107
|
+
const btn = host.querySelector('button');
|
|
108
|
+
expect(btn).not.toBeNull();
|
|
109
|
+
expect(btn.textContent).toContain('My App');
|
|
110
|
+
btn.click();
|
|
111
|
+
await tick();
|
|
112
|
+
expect(blob.activePreset).toBe('home');
|
|
113
|
+
});
|
|
114
|
+
it('titlecases multi-word preset names with hyphens or underscores', async () => {
|
|
115
|
+
registerApp(makeApp('app.a', 'My App'));
|
|
116
|
+
breadcrumbApp.id = 'app.a';
|
|
117
|
+
setActiveApp('app.a', new Set());
|
|
118
|
+
bindBlobWithPresets('my-cool-preset', ['home', 'my-cool-preset'], 'home');
|
|
119
|
+
cmp = mount(BrandSlot, { target: host, props: {} });
|
|
120
|
+
await tick();
|
|
121
|
+
expect(host.textContent).toContain('My Cool Preset');
|
|
122
|
+
});
|
|
71
123
|
});
|
package/dist/Sh3.svelte
CHANGED
|
@@ -79,19 +79,19 @@
|
|
|
79
79
|
|
|
80
80
|
const edgePointers = new Set<number>();
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
83
83
|
const rect = el.getBoundingClientRect();
|
|
84
84
|
const local = e.clientX - rect.left;
|
|
85
85
|
if (local >= EDGE_PX && local <= rect.width - EDGE_PX) return;
|
|
86
86
|
const granted = claim(e.pointerId, { ownerId: 'sh3:edge', axis: 'x', priority: 'edge', depth: 0 });
|
|
87
87
|
if (granted) edgePointers.add(e.pointerId);
|
|
88
|
-
}
|
|
88
|
+
};
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
const onPointerEnd = (e: PointerEvent): void => {
|
|
91
91
|
if (!edgePointers.has(e.pointerId)) return;
|
|
92
92
|
revoke(e.pointerId, 'sh3:edge');
|
|
93
93
|
edgePointers.delete(e.pointerId);
|
|
94
|
-
}
|
|
94
|
+
};
|
|
95
95
|
|
|
96
96
|
el.addEventListener('pointerdown', onPointerDown);
|
|
97
97
|
el.addEventListener('pointerup', onPointerEnd);
|
|
@@ -50,6 +50,7 @@ export function listActionsFromEntries(entries, state) {
|
|
|
50
50
|
ownerShardId: entry.ownerShardId,
|
|
51
51
|
paletteItem: entry.action.paletteItem !== false,
|
|
52
52
|
contextItem: entry.action.contextItem !== false,
|
|
53
|
+
aiInvocable: entry.action.aiInvocable,
|
|
53
54
|
submenu: entry.action.submenu,
|
|
54
55
|
submenuOf: entry.action.submenuOf,
|
|
55
56
|
active,
|
|
@@ -57,6 +57,19 @@ describe('listActiveFromEntries', () => {
|
|
|
57
57
|
expect(out[0].paletteItem).toBe(false);
|
|
58
58
|
expect(out[0].contextItem).toBe(true); // defaults to true
|
|
59
59
|
});
|
|
60
|
+
it('propagates aiInvocable from the registered action, preserving undefined', () => {
|
|
61
|
+
const entries = [
|
|
62
|
+
mkEntry({ id: 'opt-out', scope: 'home', aiInvocable: false }),
|
|
63
|
+
mkEntry({ id: 'opt-in', scope: 'home', aiInvocable: true }),
|
|
64
|
+
mkEntry({ id: 'unset', scope: 'home' }),
|
|
65
|
+
];
|
|
66
|
+
const out = listActiveFromEntries(entries, mkState());
|
|
67
|
+
const byId = Object.fromEntries(out.map((d) => [d.id, d]));
|
|
68
|
+
expect(byId['opt-out'].aiInvocable).toBe(false);
|
|
69
|
+
expect(byId['opt-in'].aiInvocable).toBe(true);
|
|
70
|
+
// `undefined` is significant — consumers filter `=== false`, not falsy.
|
|
71
|
+
expect(byId['unset'].aiInvocable).toBeUndefined();
|
|
72
|
+
});
|
|
60
73
|
it('dedupes by action id', () => {
|
|
61
74
|
const entries = [
|
|
62
75
|
mkEntry({ id: 'dup', scope: 'home' }, 'shard.a'),
|
package/dist/actions/types.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface Action {
|
|
|
14
14
|
scope: ActionScope;
|
|
15
15
|
contextItem?: boolean;
|
|
16
16
|
paletteItem?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Opt-out flag for AI tool catalogs. Set `false` to hide this action
|
|
19
|
+
* from LLM-facing surfaces (e.g. `sh3-ai`'s action→tool adapter) — use
|
|
20
|
+
* for palette-only actions that need a UI picker and are meaningless
|
|
21
|
+
* to invoke programmatically. Defaults to `undefined` (catalog
|
|
22
|
+
* inclusion decided by the consumer).
|
|
23
|
+
*/
|
|
24
|
+
aiInvocable?: boolean;
|
|
17
25
|
/**
|
|
18
26
|
* Optional menu container id. When set and the active app's declared
|
|
19
27
|
* (or canonical fallback) menu list contains this id, the action
|
|
@@ -148,6 +156,8 @@ export interface ActiveActionDescriptor {
|
|
|
148
156
|
ownerShardId: string;
|
|
149
157
|
paletteItem: boolean;
|
|
150
158
|
contextItem: boolean;
|
|
159
|
+
/** Carried through from the registered action; see `Action.aiInvocable`. */
|
|
160
|
+
aiInvocable?: boolean;
|
|
151
161
|
/** True when this action is a submenu parent (children opened by drill). */
|
|
152
162
|
submenu?: true;
|
|
153
163
|
/** Parent action id when this action is a submenu child. */
|
|
@@ -187,6 +197,8 @@ export interface ActionDescriptor {
|
|
|
187
197
|
ownerShardId: string;
|
|
188
198
|
paletteItem: boolean;
|
|
189
199
|
contextItem: boolean;
|
|
200
|
+
/** Carried through from the registered action; see `Action.aiInvocable`. */
|
|
201
|
+
aiInvocable?: boolean;
|
|
190
202
|
/** True when this action is a submenu parent (children opened by drill). */
|
|
191
203
|
submenu?: true;
|
|
192
204
|
/** Parent action id when this action is a submenu child. */
|
package/dist/api.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export type { RunVerbOpts, RunVerbResult } from './runtime';
|
|
|
65
65
|
export { registerShellMode } from './shell-shard/registerShellMode';
|
|
66
66
|
export type { ShellModeDescriptor, ShellModeOutput, ShellModeDispatchHandler, ShellModeDispatchInput, ShellModeRunsOn, RichEntryHandle, StreamHandle, } from './shell-shard/contract';
|
|
67
67
|
export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
|
|
68
|
+
export { CONTEXT_SOURCE_POINT_ID } from './contributions/contextSource';
|
|
69
|
+
export type { ContextSource } from './contributions/contextSource';
|
|
68
70
|
export type { GestureRegistry, GestureHandle } from './gestures';
|
|
69
71
|
export type { GestureType, Axis, ClaimPriority, ClaimEntry, PanEvent, ScrollEvent, ButtonEvent, PanOptions, DragOptions, ButtonOptions, ScrollOptions, } from './gestures/types';
|
|
70
72
|
export { VERSION } from './version';
|
package/dist/api.js
CHANGED
|
@@ -65,6 +65,8 @@ export { runVerbProgrammatic } from './runtime';
|
|
|
65
65
|
// Sh3 mode contributions (external shards extend the sh3 with new modes).
|
|
66
66
|
export { registerShellMode } from './shell-shard/registerShellMode';
|
|
67
67
|
export { SHELL_MODE_CONTRIBUTION_POINT } from './shell-shard/contract';
|
|
68
|
+
// Context-source contributions (publishers register entries; consumers like sh3-ai pick them up).
|
|
69
|
+
export { CONTEXT_SOURCE_POINT_ID } from './contributions/contextSource';
|
|
68
70
|
// Package version.
|
|
69
71
|
export { VERSION } from './version';
|
|
70
72
|
// Framework shard IDs — shards that are always present (built-in to sh3-core).
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { storeContext } from './storeShard.svelte';
|
|
10
10
|
import { fetchArchive, buildPackageMeta } from '../../registry/client';
|
|
11
|
-
import { readFileFromArchive
|
|
11
|
+
import { readFileFromArchive } from '../../registry/archive';
|
|
12
12
|
import { installPackage } from '../../registry/installer';
|
|
13
13
|
import { loadBundleModule, type LoadedBundle } from '../../registry/loader';
|
|
14
14
|
import { extractBundlePermissions } from '../../registry/permission-descriptions';
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -95,6 +95,14 @@ export interface AppManifest {
|
|
|
95
95
|
* Optional default home-card color default to transparent if not set
|
|
96
96
|
*/
|
|
97
97
|
color?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Name of the preset (from `initialLayout: LayoutPreset[]`) that is treated
|
|
100
|
+
* as the entry/home preset. When set, clicking the app segment in the
|
|
101
|
+
* breadcrumb returns the user here; framework helpers also use it as the
|
|
102
|
+
* "back to default" target. When omitted, the first preset in declaration
|
|
103
|
+
* order is used. Ignored when `initialLayout` is not an array of presets.
|
|
104
|
+
*/
|
|
105
|
+
defaultPreset?: string;
|
|
98
106
|
}
|
|
99
107
|
/**
|
|
100
108
|
* Context object passed to `App.activate`. Provides app-scoped state zones
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
resolveMenuContainers,
|
|
16
16
|
resolveMenuItems,
|
|
17
17
|
resolveSubmenuItems,
|
|
18
|
-
type MenuBarItem,
|
|
19
18
|
} from '../actions/menuBarModel';
|
|
20
19
|
import { listActions } from '../actions/registry';
|
|
21
20
|
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
@@ -54,7 +53,16 @@
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
// --- derived items for current nav level ---------------------------
|
|
57
|
-
|
|
56
|
+
interface SheetItem {
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
isContainer: boolean;
|
|
60
|
+
isSubmenu: boolean;
|
|
61
|
+
shortcut: string | null;
|
|
62
|
+
disabled: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const currentItems = $derived.by<SheetItem[]>(() => {
|
|
58
66
|
const entries = listActions();
|
|
59
67
|
const nav = currentNav;
|
|
60
68
|
|
|
@@ -65,7 +73,10 @@
|
|
|
65
73
|
.map((c) => ({
|
|
66
74
|
id: c.id,
|
|
67
75
|
label: c.label,
|
|
68
|
-
isContainer: true
|
|
76
|
+
isContainer: true,
|
|
77
|
+
isSubmenu: false,
|
|
78
|
+
shortcut: null,
|
|
79
|
+
disabled: false,
|
|
69
80
|
}));
|
|
70
81
|
}
|
|
71
82
|
|
|
@@ -74,8 +85,9 @@
|
|
|
74
85
|
return items.map((item) => ({
|
|
75
86
|
id: item.id,
|
|
76
87
|
label: item.label,
|
|
77
|
-
|
|
88
|
+
isContainer: false,
|
|
78
89
|
isSubmenu: item.submenu === true,
|
|
90
|
+
shortcut: item.shortcut,
|
|
79
91
|
disabled: item.disabled,
|
|
80
92
|
}));
|
|
81
93
|
}
|
|
@@ -85,14 +97,15 @@
|
|
|
85
97
|
return items.map((item) => ({
|
|
86
98
|
id: item.id,
|
|
87
99
|
label: item.label,
|
|
100
|
+
isContainer: false,
|
|
101
|
+
isSubmenu: item.submenu === true,
|
|
88
102
|
shortcut: item.shortcut,
|
|
89
103
|
disabled: item.disabled,
|
|
90
|
-
isSubmenu: item.submenu === true,
|
|
91
104
|
}));
|
|
92
105
|
});
|
|
93
106
|
|
|
94
107
|
// --- actions --------------------------------------------------------
|
|
95
|
-
function handleTap(entry:
|
|
108
|
+
function handleTap(entry: SheetItem) {
|
|
96
109
|
if (entry.isContainer) {
|
|
97
110
|
const c = containers.find((x) => x.id === entry.id);
|
|
98
111
|
if (c) push({ kind: 'container', containerId: c.id, label: c.label });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution point id: shards register `ContextSource` descriptors here.
|
|
3
|
+
* Each registration adds one pickable entry in any consuming UI (e.g. the
|
|
4
|
+
* "SOURCES" section of the AI Edit modal when sh3-ai is installed) and may
|
|
5
|
+
* be picked up by future consumers. Lifecycle is publisher-owned — register
|
|
6
|
+
* when content becomes relevant (app activation, project load, selection),
|
|
7
|
+
* dispose when it stops being relevant.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CONTEXT_SOURCE_POINT_ID = "sh3.contextSource";
|
|
10
|
+
/** A single context-source contribution. */
|
|
11
|
+
export interface ContextSource {
|
|
12
|
+
/**
|
|
13
|
+
* Globally unique. Convention: `<shardId>:<slug>`. Used as the picker
|
|
14
|
+
* selection key, so it must be stable across re-renders. Re-registering
|
|
15
|
+
* with an existing id silently replaces — generally dispose the prior
|
|
16
|
+
* registration first when swapping content.
|
|
17
|
+
*/
|
|
18
|
+
id: string;
|
|
19
|
+
/** Short display name shown in the picker row and the chip body. */
|
|
20
|
+
label: string;
|
|
21
|
+
/**
|
|
22
|
+
* Tooltip in any consuming UI. Consumers may also surface this to
|
|
23
|
+
* downstream tools (e.g. as the description sh3-ai exposes when
|
|
24
|
+
* chat-side context tools land).
|
|
25
|
+
*/
|
|
26
|
+
description?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Drives prompt formatting (when consumed by sh3-ai) and the chip kind tag.
|
|
29
|
+
* - `text` (default): value coerced to string, dumped raw.
|
|
30
|
+
* - `markdown`: value coerced to string, wrapped in fenced ```markdown``` block.
|
|
31
|
+
* - `json`: value `JSON.stringify`-ed with 2-space indent, wrapped in fenced ```json``` block.
|
|
32
|
+
*/
|
|
33
|
+
kind?: 'text' | 'markdown' | 'json';
|
|
34
|
+
/**
|
|
35
|
+
* Sub-header under the picker's SOURCES section (e.g. the consuming
|
|
36
|
+
* shard's display name). Entries without a group fall under an "Other"
|
|
37
|
+
* sub-header.
|
|
38
|
+
*/
|
|
39
|
+
group?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Lazy fetcher. Called when the user picks the chip (for the expand
|
|
42
|
+
* preview pane) and again at consume time. May be sync or async.
|
|
43
|
+
* Returning null/undefined signals "no content available right now" —
|
|
44
|
+
* entry is silently omitted but the chip remains. Throwing/rejecting
|
|
45
|
+
* surfaces a toast and skips the entry.
|
|
46
|
+
*/
|
|
47
|
+
get(): unknown | Promise<unknown>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public contract for context-source contributions. Shards register
|
|
3
|
+
* `ContextSource` descriptors at `CONTEXT_SOURCE_POINT_ID` via the
|
|
4
|
+
* standard `ctx.contributions.register` API; consumers (sh3-ai today,
|
|
5
|
+
* potentially inspectors / hover previews / chat-side context tools
|
|
6
|
+
* tomorrow) enumerate them via `ctx.contributions.list`.
|
|
7
|
+
*
|
|
8
|
+
* v1 has a single consumer (sh3-ai). The descriptor shape is hosted
|
|
9
|
+
* here so publisher shards do not need a devDependency on sh3-ai to
|
|
10
|
+
* contribute. Lifecycle is consumer-owned — see the JSDoc on
|
|
11
|
+
* `CONTEXT_SOURCE_POINT_ID` below.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Contribution point id: shards register `ContextSource` descriptors here.
|
|
15
|
+
* Each registration adds one pickable entry in any consuming UI (e.g. the
|
|
16
|
+
* "SOURCES" section of the AI Edit modal when sh3-ai is installed) and may
|
|
17
|
+
* be picked up by future consumers. Lifecycle is publisher-owned — register
|
|
18
|
+
* when content becomes relevant (app activation, project load, selection),
|
|
19
|
+
* dispose when it stops being relevant.
|
|
20
|
+
*/
|
|
21
|
+
export const CONTEXT_SOURCE_POINT_ID = 'sh3.contextSource';
|
|
@@ -37,13 +37,4 @@ export interface DocumentPickerOptions {
|
|
|
37
37
|
* own namespace, so the user can't navigate into a dead-end root. */
|
|
38
38
|
lockToShard?: boolean;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Create a document picker API bound to a document listing function.
|
|
42
|
-
* The listFn is derived from the shard's document zone + browse permission
|
|
43
|
-
* and baked in at construction time so callers don't pass their own scope.
|
|
44
|
-
*
|
|
45
|
-
* When an `anchor` element is provided the browser opens as a popup
|
|
46
|
-
* (anchored near the element). Without an anchor it opens as a centered
|
|
47
|
-
* modal (the expected default for file-browser dialogs).
|
|
48
|
-
*/
|
|
49
40
|
export declare function createDocumentPicker(listFn: DocListFn, options?: DocumentPickerOptions): DocumentPickerApi;
|
|
@@ -2,15 +2,6 @@ import { sh3 } from '../sh3Runtime.svelte';
|
|
|
2
2
|
import DocumentBrowser from '../primitives/widgets/_DocumentBrowser.svelte';
|
|
3
3
|
const BOX_STYLE = 'max-width: min(800px, 95vw);';
|
|
4
4
|
const MODAL_OPTS = { dismissOnBackdrop: true, boxStyle: BOX_STYLE };
|
|
5
|
-
/**
|
|
6
|
-
* Create a document picker API bound to a document listing function.
|
|
7
|
-
* The listFn is derived from the shard's document zone + browse permission
|
|
8
|
-
* and baked in at construction time so callers don't pass their own scope.
|
|
9
|
-
*
|
|
10
|
-
* When an `anchor` element is provided the browser opens as a popup
|
|
11
|
-
* (anchored near the element). Without an anchor it opens as a centered
|
|
12
|
-
* modal (the expected default for file-browser dialogs).
|
|
13
|
-
*/
|
|
14
5
|
export function createDocumentPicker(listFn, options = {}) {
|
|
15
6
|
const { listFolders, handle, readOnlyShard, initialShardId, lockToShard } = options;
|
|
16
7
|
function openBrowser(browserProps, anchor) {
|
|
@@ -142,7 +142,7 @@ export function attachApp(app) {
|
|
|
142
142
|
// their view factories. Binding the preset manager proxy happens here
|
|
143
143
|
// so shards can read/switch presets from their activate() hook.
|
|
144
144
|
appEntry = { appId: app.manifest.id, proxy, heldSlotIds: [] };
|
|
145
|
-
bindPresetBlob(proxy);
|
|
145
|
+
bindPresetBlob(proxy, app.manifest.defaultPreset);
|
|
146
146
|
bindDrawerStoreToBlob(proxy);
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
@@ -6,16 +6,31 @@ export interface PresetManager {
|
|
|
6
6
|
active(): string;
|
|
7
7
|
/** Switch to the named preset. Throws if unknown. */
|
|
8
8
|
switch(name: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Resolved default preset name. Returns the hint passed at bind time
|
|
11
|
+
* when it matches a known preset, otherwise the first preset in
|
|
12
|
+
* declaration order. Throws when no app is attached.
|
|
13
|
+
*/
|
|
14
|
+
default(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Switch to the resolved default. No-op when already on default.
|
|
17
|
+
* Throws when no app is attached.
|
|
18
|
+
*/
|
|
19
|
+
switchToDefault(): void;
|
|
9
20
|
}
|
|
10
21
|
/**
|
|
11
22
|
* Bind the manager to the attached app's `AppLayoutBlob` workspace-zone
|
|
12
23
|
* proxy. Called from `attachApp` in the layout store.
|
|
24
|
+
*
|
|
25
|
+
* @param defaultPresetHint - Optional name from `AppManifest.defaultPreset`.
|
|
26
|
+
* Used by `default()` / `switchToDefault()`. Falls back to first preset
|
|
27
|
+
* when omitted or when the hint doesn't match any preset.
|
|
13
28
|
*/
|
|
14
|
-
export declare function bindPresetBlob(blob: AppLayoutBlob): void;
|
|
29
|
+
export declare function bindPresetBlob(blob: AppLayoutBlob, defaultPresetHint?: string): void;
|
|
15
30
|
/** Unbind on detach. Called from `detachApp`. */
|
|
16
31
|
export declare function unbindPresetBlob(): void;
|
|
17
32
|
/** Test-only bind alias for tests that build a synthetic blob. */
|
|
18
|
-
export declare function __bindPresetBlobForTest(blob: AppLayoutBlob): void;
|
|
33
|
+
export declare function __bindPresetBlobForTest(blob: AppLayoutBlob, defaultPresetHint?: string): void;
|
|
19
34
|
/** Test-only reset. Clears the binding. */
|
|
20
35
|
export declare function __resetPresetManagerForTest(): void;
|
|
21
36
|
export declare const presetManager: PresetManager;
|
package/dist/overlays/presets.js
CHANGED
|
@@ -18,24 +18,33 @@
|
|
|
18
18
|
* all methods throw — there is no pre-boot fallback.
|
|
19
19
|
*/
|
|
20
20
|
let boundBlob = null;
|
|
21
|
+
let boundDefaultHint = null;
|
|
21
22
|
/**
|
|
22
23
|
* Bind the manager to the attached app's `AppLayoutBlob` workspace-zone
|
|
23
24
|
* proxy. Called from `attachApp` in the layout store.
|
|
25
|
+
*
|
|
26
|
+
* @param defaultPresetHint - Optional name from `AppManifest.defaultPreset`.
|
|
27
|
+
* Used by `default()` / `switchToDefault()`. Falls back to first preset
|
|
28
|
+
* when omitted or when the hint doesn't match any preset.
|
|
24
29
|
*/
|
|
25
|
-
export function bindPresetBlob(blob) {
|
|
30
|
+
export function bindPresetBlob(blob, defaultPresetHint) {
|
|
26
31
|
boundBlob = blob;
|
|
32
|
+
boundDefaultHint = defaultPresetHint !== null && defaultPresetHint !== void 0 ? defaultPresetHint : null;
|
|
27
33
|
}
|
|
28
34
|
/** Unbind on detach. Called from `detachApp`. */
|
|
29
35
|
export function unbindPresetBlob() {
|
|
30
36
|
boundBlob = null;
|
|
37
|
+
boundDefaultHint = null;
|
|
31
38
|
}
|
|
32
39
|
/** Test-only bind alias for tests that build a synthetic blob. */
|
|
33
|
-
export function __bindPresetBlobForTest(blob) {
|
|
40
|
+
export function __bindPresetBlobForTest(blob, defaultPresetHint) {
|
|
34
41
|
boundBlob = blob;
|
|
42
|
+
boundDefaultHint = defaultPresetHint !== null && defaultPresetHint !== void 0 ? defaultPresetHint : null;
|
|
35
43
|
}
|
|
36
44
|
/** Test-only reset. Clears the binding. */
|
|
37
45
|
export function __resetPresetManagerForTest() {
|
|
38
46
|
boundBlob = null;
|
|
47
|
+
boundDefaultHint = null;
|
|
39
48
|
}
|
|
40
49
|
function requireBlob() {
|
|
41
50
|
if (!boundBlob) {
|
|
@@ -56,8 +65,25 @@ function switchPreset(name) {
|
|
|
56
65
|
}
|
|
57
66
|
blob.activePreset = name;
|
|
58
67
|
}
|
|
68
|
+
function resolveDefault() {
|
|
69
|
+
const blob = requireBlob();
|
|
70
|
+
const keys = Object.keys(blob.presets);
|
|
71
|
+
if (boundDefaultHint && boundDefaultHint in blob.presets) {
|
|
72
|
+
return boundDefaultHint;
|
|
73
|
+
}
|
|
74
|
+
return keys[0];
|
|
75
|
+
}
|
|
76
|
+
function switchToDefault() {
|
|
77
|
+
const target = resolveDefault();
|
|
78
|
+
const blob = requireBlob();
|
|
79
|
+
if (blob.activePreset === target)
|
|
80
|
+
return;
|
|
81
|
+
blob.activePreset = target;
|
|
82
|
+
}
|
|
59
83
|
export const presetManager = {
|
|
60
84
|
list: listPresets,
|
|
61
85
|
active: activePreset,
|
|
62
86
|
switch: switchPreset,
|
|
87
|
+
default: resolveDefault,
|
|
88
|
+
switchToDefault,
|
|
63
89
|
};
|
|
@@ -37,4 +37,33 @@ describe('presetManager', () => {
|
|
|
37
37
|
it('list() throws when no blob is bound', () => {
|
|
38
38
|
expect(() => presetManager.list()).toThrow(/no app attached/);
|
|
39
39
|
});
|
|
40
|
+
it('default() returns the explicit hint when it matches a known preset', () => {
|
|
41
|
+
__bindPresetBlobForTest(makeBlob('author', ['author', 'review', 'inspect']), 'review');
|
|
42
|
+
expect(presetManager.default()).toBe('review');
|
|
43
|
+
});
|
|
44
|
+
it('default() falls back to first preset when no hint is provided', () => {
|
|
45
|
+
__bindPresetBlobForTest(makeBlob('review', ['author', 'review']));
|
|
46
|
+
expect(presetManager.default()).toBe('author');
|
|
47
|
+
});
|
|
48
|
+
it('default() falls back to first preset when hint is unknown', () => {
|
|
49
|
+
__bindPresetBlobForTest(makeBlob('author', ['author', 'review']), 'nope');
|
|
50
|
+
expect(presetManager.default()).toBe('author');
|
|
51
|
+
});
|
|
52
|
+
it('default() throws when no blob is bound', () => {
|
|
53
|
+
expect(() => presetManager.default()).toThrow(/no app attached/);
|
|
54
|
+
});
|
|
55
|
+
it('switchToDefault() switches to the resolved default preset', () => {
|
|
56
|
+
const blob = makeBlob('review', ['author', 'review']);
|
|
57
|
+
__bindPresetBlobForTest(blob, 'author');
|
|
58
|
+
presetManager.switchToDefault();
|
|
59
|
+
expect(blob.activePreset).toBe('author');
|
|
60
|
+
expect(presetManager.active()).toBe('author');
|
|
61
|
+
});
|
|
62
|
+
it('switchToDefault() is a no-op when already on default', () => {
|
|
63
|
+
const blob = makeBlob('author', ['author', 'review']);
|
|
64
|
+
__bindPresetBlobForTest(blob, 'author');
|
|
65
|
+
const before = blob.activePreset;
|
|
66
|
+
presetManager.switchToDefault();
|
|
67
|
+
expect(blob.activePreset).toBe(before);
|
|
68
|
+
});
|
|
40
69
|
});
|