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.
Files changed (40) hide show
  1. package/dist/BrandSlot.svelte +62 -3
  2. package/dist/BrandSlot.test.js +52 -0
  3. package/dist/Sh3.svelte +4 -4
  4. package/dist/actions/listActive.js +1 -0
  5. package/dist/actions/listActive.test.js +13 -0
  6. package/dist/actions/types.d.ts +12 -0
  7. package/dist/api.d.ts +2 -0
  8. package/dist/api.js +2 -0
  9. package/dist/app/store/StoreView.svelte +1 -1
  10. package/dist/apps/types.d.ts +8 -0
  11. package/dist/chrome/MenuSheet.svelte +19 -6
  12. package/dist/contributions/contextSource.d.ts +48 -0
  13. package/dist/contributions/contextSource.js +21 -0
  14. package/dist/documents/picker-primitive.d.ts +0 -9
  15. package/dist/documents/picker-primitive.js +0 -9
  16. package/dist/layout/store.svelte.js +1 -1
  17. package/dist/overlays/presets.d.ts +17 -2
  18. package/dist/overlays/presets.js +28 -2
  19. package/dist/overlays/presets.test.js +29 -0
  20. package/dist/primitives/widgets/DocumentFilePicker.svelte +9 -7
  21. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +44 -27
  22. package/dist/primitives/widgets/_DocumentBrowser.svelte +4 -4
  23. package/dist/registry/installer.js +50 -10
  24. package/dist/registry/installer.test.d.ts +1 -0
  25. package/dist/registry/installer.test.js +146 -0
  26. package/dist/registry/types.d.ts +19 -0
  27. package/dist/runtime/runVerb.test.js +87 -0
  28. package/dist/sh3core-shard/Sh3Home.svelte +0 -1
  29. package/dist/shards/lifecycle.svelte.d.ts +8 -0
  30. package/dist/shards/lifecycle.svelte.js +17 -0
  31. package/dist/shell-shard/verbs/xfer.js +66 -4
  32. package/dist/shell-shard/verbs/xfer.test.js +74 -0
  33. package/dist/transport/apiFetch.js +21 -3
  34. package/dist/transport/apiFetch.test.js +63 -0
  35. package/dist/verbs/types.d.ts +49 -12
  36. package/dist/verbs/types.test.d.ts +1 -0
  37. package/dist/verbs/types.test.js +43 -0
  38. package/dist/version.d.ts +1 -1
  39. package/dist/version.js +1 -1
  40. package/package.json +1 -1
@@ -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
- type Mode = 'brand' | 'app' | 'breadcrumb' | 'project-home' | 'project-app' | 'project-breadcrumb';
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>
@@ -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
- function onPointerDown(e: PointerEvent): void {
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
- function onPointerEnd(e: PointerEvent): void {
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'),
@@ -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, readManifestFromArchive } from '../../registry/archive';
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';
@@ -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
- const currentItems = $derived.by(() => {
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 as const,
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
- shortcut: item.shortcut,
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: { id: string; isContainer?: boolean; isSubmenu?: boolean }) {
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;
@@ -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
  });