@silo-code/sdk 0.6.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/dist/context-keys.d.ts +19 -0
  4. package/dist/context-keys.d.ts.map +1 -0
  5. package/dist/context-keys.js +2 -0
  6. package/dist/context-keys.js.map +1 -0
  7. package/dist/dnd-service.d.ts +140 -0
  8. package/dist/dnd-service.d.ts.map +1 -0
  9. package/dist/dnd-service.js +17 -0
  10. package/dist/dnd-service.js.map +1 -0
  11. package/dist/domain-types.d.ts +237 -0
  12. package/dist/domain-types.d.ts.map +1 -0
  13. package/dist/domain-types.js +11 -0
  14. package/dist/domain-types.js.map +1 -0
  15. package/dist/editor-service.d.ts +175 -0
  16. package/dist/editor-service.d.ts.map +1 -0
  17. package/dist/editor-service.js +2 -0
  18. package/dist/editor-service.js.map +1 -0
  19. package/dist/extension-storage.d.ts +26 -0
  20. package/dist/extension-storage.d.ts.map +1 -0
  21. package/dist/extension-storage.js +2 -0
  22. package/dist/extension-storage.js.map +1 -0
  23. package/dist/file-service.d.ts +84 -0
  24. package/dist/file-service.d.ts.map +1 -0
  25. package/dist/file-service.js +2 -0
  26. package/dist/file-service.js.map +1 -0
  27. package/dist/index.d.ts +32 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +22 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/layout-service.d.ts +46 -0
  32. package/dist/layout-service.d.ts.map +1 -0
  33. package/dist/layout-service.js +2 -0
  34. package/dist/layout-service.js.map +1 -0
  35. package/dist/permissions.d.ts +41 -0
  36. package/dist/permissions.d.ts.map +1 -0
  37. package/dist/permissions.js +40 -0
  38. package/dist/permissions.js.map +1 -0
  39. package/dist/process-service.d.ts +132 -0
  40. package/dist/process-service.d.ts.map +1 -0
  41. package/dist/process-service.js +2 -0
  42. package/dist/process-service.js.map +1 -0
  43. package/dist/terminal-service.d.ts +38 -0
  44. package/dist/terminal-service.d.ts.map +1 -0
  45. package/dist/terminal-service.js +2 -0
  46. package/dist/terminal-service.js.map +1 -0
  47. package/dist/theme-service.d.ts +87 -0
  48. package/dist/theme-service.d.ts.map +1 -0
  49. package/dist/theme-service.js +2 -0
  50. package/dist/theme-service.js.map +1 -0
  51. package/dist/types.d.ts +495 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/ui-service.d.ts +469 -0
  56. package/dist/ui-service.d.ts.map +1 -0
  57. package/dist/ui-service.js +2 -0
  58. package/dist/ui-service.js.map +1 -0
  59. package/dist/use-focus-group.d.ts +202 -0
  60. package/dist/use-focus-group.d.ts.map +1 -0
  61. package/dist/use-focus-group.js +236 -0
  62. package/dist/use-focus-group.js.map +1 -0
  63. package/dist/use-service-state.d.ts +36 -0
  64. package/dist/use-service-state.d.ts.map +1 -0
  65. package/dist/use-service-state.js +25 -0
  66. package/dist/use-service-state.js.map +1 -0
  67. package/dist/workspace-service.d.ts +72 -0
  68. package/dist/workspace-service.d.ts.map +1 -0
  69. package/dist/workspace-service.js +2 -0
  70. package/dist/workspace-service.js.map +1 -0
  71. package/package.json +54 -0
  72. package/src/context-keys.ts +18 -0
  73. package/src/dnd-service.ts +151 -0
  74. package/src/domain-types.ts +252 -0
  75. package/src/editor-service.ts +196 -0
  76. package/src/extension-storage.ts +25 -0
  77. package/src/file-service.ts +90 -0
  78. package/src/index.ts +151 -0
  79. package/src/layout-service.ts +49 -0
  80. package/src/permissions.ts +55 -0
  81. package/src/process-service.ts +143 -0
  82. package/src/terminal-service.ts +41 -0
  83. package/src/theme-service.ts +102 -0
  84. package/src/types.ts +513 -0
  85. package/src/ui-service.ts +487 -0
  86. package/src/use-focus-group.test.ts +168 -0
  87. package/src/use-focus-group.ts +382 -0
  88. package/src/use-service-state.ts +43 -0
  89. package/src/workspace-service.ts +76 -0
package/src/index.ts ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The public Silo extension API surface — the single curated entry point an
3
+ * extension author imports from. This is the seed of the future `@silo-code/sdk`
4
+ * package: it re-exports **only** the blessed, permanently supported types.
5
+ * Anything not re-exported here is host-internal and may change without notice.
6
+ *
7
+ * It is also the entry point the API-reference generator (TypeDoc) reads, so
8
+ * the published reference is exactly this surface — no more, no less.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+
13
+ // Core contract + contribution-point types.
14
+ export type {
15
+ Disposable,
16
+ DockPanelApi,
17
+ EditorProps,
18
+ EditorCapabilities,
19
+ Editor,
20
+ NewFileTemplate,
21
+ FileType,
22
+ Command,
23
+ MenuId,
24
+ MenuItemContribution,
25
+ Keybinding,
26
+ SidePanelProps,
27
+ SidePanel,
28
+ DockPanelKind,
29
+ StatusItem,
30
+ SettingsPage,
31
+ ExtensionContext,
32
+ Extension,
33
+ ExtensionHandle,
34
+ } from "./types";
35
+
36
+ // Public domain types (the persisted shapes surfaced through the services).
37
+ // TerminalKind/TerminalRecord and the theme types are re-exported via their
38
+ // service modules below; these are the remaining ones consumers can name.
39
+ export type {
40
+ Workspace,
41
+ EditorMode,
42
+ EditorRecord,
43
+ SidePanelSlot,
44
+ } from "./domain-types";
45
+
46
+ // Consumer services exposed on the ExtensionContext.
47
+ export type {
48
+ WorkspaceService,
49
+ WorkspaceState,
50
+ CreateWorkspaceInput,
51
+ } from "./workspace-service";
52
+ export type {
53
+ EditorService,
54
+ EditorSaveHandlers,
55
+ OpenFileOptions,
56
+ EditorViewInfo,
57
+ OpenDiffSpec,
58
+ DiffContent,
59
+ DiffContentRequest,
60
+ DiffContentProvider,
61
+ } from "./editor-service";
62
+ export type {
63
+ LayoutService,
64
+ LayoutState,
65
+ SidePanelColumnState,
66
+ SideLocation,
67
+ } from "./layout-service";
68
+ export type {
69
+ ProcessService,
70
+ ProcessSession,
71
+ ProcessSpawnOptions,
72
+ ProcessExecOptions,
73
+ ProcessExecResult,
74
+ } from "./process-service";
75
+ export type {
76
+ TerminalService,
77
+ CreateTerminalInput,
78
+ TerminalKind,
79
+ TerminalRecord,
80
+ } from "./terminal-service";
81
+ export type { FileService, FileMeta, FileChangeEvent } from "./file-service";
82
+ // The permission surface: the capability vocabulary an extension declares, and
83
+ // the error the host throws when an extension reaches outside the workspace
84
+ // without the matching grant. `PathDeniedError` is a class (a runtime value).
85
+ export type { Permission } from "./permissions";
86
+ export { PathDeniedError } from "./permissions";
87
+ // The theme domain: the consumer service + its state/resolve shapes, the
88
+ // `ThemePreset` contribution type (registered via ctx.registerThemePreset), and
89
+ // the underlying theme types (re-exported from ./domain-types via theme-service).
90
+ export type {
91
+ ThemeService,
92
+ ThemeState,
93
+ ThemePreset,
94
+ ResolvedTheme,
95
+ ThemeBase,
96
+ ThemeVars,
97
+ CustomTheme,
98
+ ThemeExport,
99
+ } from "./theme-service";
100
+ export type { ExtensionStorage } from "./extension-storage";
101
+ // The drag-and-drop domain: the consumer service + its payload/handler shapes.
102
+ // DND_MIME is a value (the well-known MIME vocabulary), so it's a runtime export.
103
+ export type {
104
+ DndService,
105
+ DndItem,
106
+ DndMime,
107
+ DragInit,
108
+ DndMode,
109
+ DropContext,
110
+ DropTargetHandlers,
111
+ } from "./dnd-service";
112
+ export { DND_MIME } from "./dnd-service";
113
+ // The user-interaction domain: native pickers + toast notifications + menus,
114
+ // plus the file-filter shape the pickers accept, the menu item/entry shapes
115
+ // `showMenu` accepts, and the modal-chrome options `showModal` accepts.
116
+ export type {
117
+ UiService,
118
+ FileFilter,
119
+ MenuItem,
120
+ MenuItemTrailing,
121
+ MenuSeparator,
122
+ MenuHeader,
123
+ MenuEntry,
124
+ ShowMenuOptions,
125
+ ConfirmOptions,
126
+ PromptOptions,
127
+ ModalOptions,
128
+ NotifyAction,
129
+ NotifyOptions,
130
+ } from "./ui-service";
131
+
132
+ // Context keys referenced by `when` predicates on menu items / keybindings.
133
+ export type { ContextKeys } from "./context-keys";
134
+
135
+ // Runtime helpers. The one blessed way for an extension to read a `ctx`
136
+ // service's reactive state in React — replaces hand-rolled useSyncExternalStore.
137
+ export { useServiceState } from "./use-service-state";
138
+ export type { ReactiveService } from "./use-service-state";
139
+ // Headless keyboard navigation for a focus group (list / menu / toolbar / …):
140
+ // one tab stop, arrow/Home/End movement, the WebKit-safe keyboard ring. The
141
+ // `focusGroupNextIndex` helper is the pure roving-index core for widgets that
142
+ // can't use the focus-driven hook (e.g. menus driven by a document listener).
143
+ export { useFocusGroup, focusGroupNextIndex } from "./use-focus-group";
144
+ export type {
145
+ FocusGroup,
146
+ FocusGroupOptions,
147
+ FocusGroupContainerProps,
148
+ FocusGroupItemProps,
149
+ FocusGroupOrientation,
150
+ FocusGroupNavQuery,
151
+ } from "./use-focus-group";
@@ -0,0 +1,49 @@
1
+ import type { Disposable } from "./types";
2
+
3
+ /**
4
+ * Which side column a layout operation targets.
5
+ *
6
+ * @category Consumer Services
7
+ * @public
8
+ */
9
+ export type SideLocation = "left" | "right";
10
+
11
+ /**
12
+ * Collapse state for one side column.
13
+ *
14
+ * @category Consumer Services
15
+ * @public
16
+ */
17
+ export interface SidePanelColumnState {
18
+ /** True when the column is collapsed (hidden). */
19
+ collapsed: boolean;
20
+ }
21
+
22
+ /**
23
+ * An immutable view of side-column layout state.
24
+ *
25
+ * @category Consumer Services
26
+ * @public
27
+ */
28
+ export interface LayoutState {
29
+ left: SidePanelColumnState;
30
+ right: SidePanelColumnState;
31
+ }
32
+
33
+ /**
34
+ * Consumer API for app layout, exposed as {@link ExtensionContext.layout}.
35
+ * Read side-panel collapse state and drive it.
36
+ *
37
+ * @category Consumer Services
38
+ * @public
39
+ */
40
+ export interface LayoutService {
41
+ /** Current frozen layout state. */
42
+ getState(): LayoutState;
43
+ /** Subscribe to layout changes; dispose to stop. */
44
+ subscribe(listener: (s: LayoutState) => void): Disposable;
45
+ /** Toggle a side column between collapsed and expanded. */
46
+ toggleSidePanel(location: SideLocation): void;
47
+ /** Set a side column's collapsed state explicitly. */
48
+ setSidePanelCollapsed(location: SideLocation, collapsed: boolean): void;
49
+ }
@@ -0,0 +1,55 @@
1
+ // The extension permission surface — what an extension may declare it needs
2
+ // (`silo.permissions` in its manifest) and the error the host throws when an
3
+ // extension reaches outside the workspace without the matching grant. See the
4
+ // "Permissions & access" guide and ADR 0015 (phased security model).
5
+
6
+ /**
7
+ * A capability an extension declares in its manifest (`silo.permissions`) to
8
+ * request access **beyond the open workspace**. With none declared, an
9
+ * extension's {@link FileService} / {@link ProcessService} access is confined to
10
+ * the workspace folder(s); each permission lifts one part of that confinement,
11
+ * and the user consents to the set at install.
12
+ *
13
+ * - `fs:read` — read files outside the workspace.
14
+ * - `fs:write` — write files outside the workspace.
15
+ * - `process` — run commands with a working directory outside the workspace.
16
+ * - `network` — make outbound network requests. Declarative consent only until
17
+ * sandboxed execution lands (in-process code can reach the network directly);
18
+ * declare it so the capability is reviewable and shown at install.
19
+ *
20
+ * @category Extension Contract
21
+ * @public
22
+ */
23
+ export type Permission = "fs:read" | "fs:write" | "process" | "network";
24
+
25
+ /**
26
+ * Thrown by {@link FileService} and {@link ProcessService} when an extension
27
+ * touches a path — or runs a process with a working directory — outside the open
28
+ * workspace without the matching {@link Permission}. Catch it and degrade
29
+ * gracefully, the same way you'd handle a missing file:
30
+ *
31
+ * ```ts
32
+ * try {
33
+ * const text = await ctx.files.readText(path);
34
+ * } catch (err) {
35
+ * if (err instanceof PathDeniedError) showMessage("That file is outside the workspace.");
36
+ * }
37
+ * ```
38
+ *
39
+ * @category Core Types
40
+ * @public
41
+ */
42
+ export class PathDeniedError extends Error {
43
+ /** The offending path, exactly as the extension passed it. */
44
+ readonly path: string;
45
+
46
+ constructor(path: string, message?: string) {
47
+ super(message ?? `Path is outside the workspace: ${path}`);
48
+ this.name = "PathDeniedError";
49
+ this.path = path;
50
+ // Restore the prototype chain so `instanceof` works across the down-leveled
51
+ // class output the SDK ships (and across the host↔extension boundary, where
52
+ // there's a single shared SDK instance).
53
+ Object.setPrototypeOf(this, PathDeniedError.prototype);
54
+ }
55
+ }
@@ -0,0 +1,143 @@
1
+ import type { Disposable } from "./types";
2
+
3
+ // `ctx.process` — persistent process / PTY sessions that survive app restarts.
4
+ // The core primitive under the terminal (and future task runners / REPLs).
5
+
6
+ /**
7
+ * Options for spawning a process session. Today sessions are shell PTYs, so
8
+ * `cwd` is required (the webview has no ambient working directory).
9
+ *
10
+ * @category Consumer Services
11
+ * @public
12
+ */
13
+ export interface ProcessSpawnOptions {
14
+ /**
15
+ * Working directory the session starts in. Must resolve inside the open
16
+ * workspace unless the extension declared the `process` {@link Permission}
17
+ * (first-party extensions are unscoped); otherwise throws
18
+ * {@link PathDeniedError}.
19
+ */
20
+ cwd: string;
21
+ /** Initial column count. */
22
+ cols?: number;
23
+ /** Initial row count. */
24
+ rows?: number;
25
+ }
26
+
27
+ /**
28
+ * A live handle to one persistent process session, returned by
29
+ * {@link ProcessService.spawn} / {@link ProcessService.attach}. The underlying
30
+ * session **survives app restarts** — re-`attach` by {@link ProcessSession.id}
31
+ * to reconnect to a still-running session.
32
+ *
33
+ * @category Consumer Services
34
+ * @public
35
+ */
36
+ export interface ProcessSession {
37
+ /** Stable session id; pass to {@link ProcessService.attach} to reconnect. */
38
+ readonly id: string;
39
+ /** Write input to the session (e.g. keystrokes). */
40
+ write(data: string): void;
41
+ /** Notify the session its viewport size changed. */
42
+ resize(cols: number, rows: number): void;
43
+ /** Terminate the session and release it. */
44
+ kill(): Promise<void>;
45
+ /** Fetch the persisted output buffer (to restore a view after re-attach). */
46
+ getBuffer(): Promise<string>;
47
+ /** Persist an output buffer for later restore. */
48
+ saveBuffer(data: string): Promise<void>;
49
+ /** Subscribe to output data. Dispose to stop listening. */
50
+ onData(listener: (data: string) => void): Disposable;
51
+ /** Subscribe to session exit. Dispose to stop listening. */
52
+ onExit(listener: (exitCode: number) => void): Disposable;
53
+ }
54
+
55
+ /**
56
+ * Options for {@link ProcessService.exec}.
57
+ *
58
+ * @category Consumer Services
59
+ * @public
60
+ */
61
+ export interface ProcessExecOptions {
62
+ /**
63
+ * Working directory to run the command in. Defaults to the open **workspace
64
+ * folder** when omitted — the right cwd for CLI tools (git, formatters,
65
+ * linters) that operate on a repo. A `cwd` outside the workspace throws
66
+ * {@link PathDeniedError} unless the extension declared the `process`
67
+ * {@link Permission}. First-party (bundled) extensions are unscoped.
68
+ */
69
+ cwd?: string;
70
+ }
71
+
72
+ /**
73
+ * The captured result of a one-shot subprocess, returned by
74
+ * {@link ProcessService.exec}.
75
+ *
76
+ * @category Consumer Services
77
+ * @public
78
+ */
79
+ export interface ProcessExecResult {
80
+ /** Everything the command wrote to standard output. */
81
+ stdout: string;
82
+ /** Everything the command wrote to standard error. */
83
+ stderr: string;
84
+ /**
85
+ * The process exit code (`0` conventionally means success), or `-1` if the
86
+ * process was terminated by a signal. A non-zero `code` is **not** an error —
87
+ * `exec` resolves regardless; inspect `code`/`stderr` to decide.
88
+ */
89
+ code: number;
90
+ }
91
+
92
+ /**
93
+ * Persistent process / PTY sessions that **survive app restarts** — the core
94
+ * primitive under the terminal (and future task runners, REPLs) — plus one-shot
95
+ * {@link ProcessService.exec | exec} for fire-and-forget subprocess execution.
96
+ * Exposed as {@link ExtensionContext.process}.
97
+ *
98
+ * @category Consumer Services
99
+ * @public
100
+ */
101
+ export interface ProcessService {
102
+ /** Spawn a new session in `opts.cwd`. */
103
+ spawn(opts: ProcessSpawnOptions): Promise<ProcessSession>;
104
+ /**
105
+ * Re-attach to an existing session by id (e.g. after an app restart). Rejects
106
+ * with a 404-style error if the session no longer exists.
107
+ */
108
+ attach(
109
+ id: string,
110
+ opts?: { cols?: number; rows?: number },
111
+ ): Promise<ProcessSession>;
112
+ /**
113
+ * Run a one-shot command and resolve with its captured output — for
114
+ * extensions that wrap a CLI (git, formatters, linters) rather than drive an
115
+ * interactive shell. Use {@link ProcessService.spawn | spawn} for long-lived
116
+ * interactive sessions instead.
117
+ *
118
+ * Runs **off the UI thread**, so a slow or network-bound command never
119
+ * stutters the app. The returned promise rejects only if the process could
120
+ * not be spawned (e.g. the command was not found); a command that runs but
121
+ * exits non-zero **resolves** — check {@link ProcessExecResult.code} and
122
+ * {@link ProcessExecResult.stderr}.
123
+ *
124
+ * @param command - Executable to run (resolved via `PATH`), e.g. `"git"`.
125
+ * @param args - Arguments passed verbatim — not shell-interpreted, so no
126
+ * quoting/escaping concerns and no shell-injection surface.
127
+ * @param options - Optional {@link ProcessExecOptions} (e.g. `cwd`).
128
+ * @example
129
+ * ```ts
130
+ * const { stdout, code } = await ctx.process.exec(
131
+ * "git",
132
+ * ["status", "--porcelain=v2"],
133
+ * { cwd: workspaceFolder },
134
+ * );
135
+ * if (code === 0) parseStatus(stdout);
136
+ * ```
137
+ */
138
+ exec(
139
+ command: string,
140
+ args: string[],
141
+ options?: ProcessExecOptions,
142
+ ): Promise<ProcessExecResult>;
143
+ }
@@ -0,0 +1,41 @@
1
+ import type { TerminalKind, TerminalRecord } from "./domain-types";
2
+
3
+ // Re-export the terminal domain types so consumers can name them from the SDK.
4
+ export type { TerminalKind, TerminalRecord } from "./domain-types";
5
+
6
+ /**
7
+ * Input for {@link TerminalService.create}.
8
+ *
9
+ * @category Consumer Services
10
+ * @public
11
+ */
12
+ export interface CreateTerminalInput {
13
+ /** Terminal kind — `"shell"` (default), `"claude"`, or `"pi"`. */
14
+ kind?: TerminalKind;
15
+ /** Working directory; falls back to the workspace folder when absent. */
16
+ cwd?: string;
17
+ /** Target workspace; defaults to the active workspace. */
18
+ workspaceId?: string;
19
+ }
20
+
21
+ /**
22
+ * Consumer API for the terminal domain, exposed as
23
+ * {@link ExtensionContext.terminals}. The terminal is a core feature — a
24
+ * built-in DockKind like the editor — so this mirrors {@link EditorService}:
25
+ * `create` opens a terminal tab in a workspace, and `closeWorkspace` reaps a
26
+ * workspace's terminals (used when a workspace is deleted). The tab itself is
27
+ * rendered by the core dock from the workspace's terminal records.
28
+ *
29
+ * @category Consumer Services
30
+ * @public
31
+ */
32
+ export interface TerminalService {
33
+ /**
34
+ * Open a new terminal in a workspace (defaults to the active one). Returns the
35
+ * created {@link TerminalRecord}; the PTY session spawns lazily when its tab
36
+ * mounts.
37
+ */
38
+ create(input?: CreateTerminalInput): TerminalRecord | undefined;
39
+ /** Close and kill every terminal in a workspace (e.g. on workspace delete). */
40
+ closeWorkspace(workspaceId: string): void;
41
+ }
@@ -0,0 +1,102 @@
1
+ import type { Disposable } from "./types";
2
+ import type {
3
+ ThemeBase,
4
+ ThemeVars,
5
+ CustomTheme,
6
+ ThemeExport,
7
+ } from "./domain-types";
8
+
9
+ // Re-export the theme domain types so consumers can name them from the SDK.
10
+ export type {
11
+ ThemeBase,
12
+ ThemeVars,
13
+ CustomTheme,
14
+ ThemeExport,
15
+ } from "./domain-types";
16
+
17
+ /**
18
+ * A selectable theme contributed via
19
+ * {@link ExtensionContext.registerThemePreset}. Built-in presets (Tokyo Night,
20
+ * Solarized Light, Gruvbox Dark, …) are registered by the `theme-presets`
21
+ * extension; core ships only Dark and Light. A preset's {@link ThemePreset.vars}
22
+ * are injected as CSS custom properties when it is the active theme.
23
+ *
24
+ * @category Registration
25
+ * @public
26
+ */
27
+ export interface ThemePreset {
28
+ /** Unique id (also the persisted `activeThemeId` when selected). */
29
+ id: string;
30
+ /** Display name shown in the theme picker. */
31
+ name: string;
32
+ /** Dark or light — drives the Monaco/xterm base and the `data-theme` attribute. */
33
+ base: ThemeBase;
34
+ /** The CSS `color-scheme` for native controls. */
35
+ colorScheme: "dark" | "light";
36
+ /** CSS variable overrides applied on top of the base palette in `theme.css`. */
37
+ vars: Partial<ThemeVars>;
38
+ }
39
+
40
+ /**
41
+ * A theme id resolved to its base + effective variables — what the theme picker
42
+ * renders swatches from, returned by {@link ThemeService.resolve}.
43
+ *
44
+ * @category Consumer Services
45
+ * @public
46
+ */
47
+ export interface ResolvedTheme {
48
+ base: ThemeBase;
49
+ colorScheme: "dark" | "light";
50
+ vars: Partial<ThemeVars>;
51
+ }
52
+
53
+ /**
54
+ * An immutable, frozen view of theme state, returned by
55
+ * {@link ThemeService.getState} and delivered to subscribers — read access
56
+ * without a Valtio dependency. `presets` is part of the state (not a static
57
+ * read) because presets are dynamic: extensions register and unregister them at
58
+ * runtime.
59
+ *
60
+ * @category Consumer Services
61
+ * @public
62
+ */
63
+ export interface ThemeState {
64
+ /** The active theme's id (a preset id or a custom theme id). */
65
+ activeId: string;
66
+ /** Core Dark/Light followed by every registered preset, in registration order. */
67
+ presets: readonly ThemePreset[];
68
+ /** The user's custom themes, loaded from disk. */
69
+ customThemes: readonly CustomTheme[];
70
+ }
71
+
72
+ /**
73
+ * Consumer API for the theme domain, exposed as {@link ExtensionContext.theme}.
74
+ * Read via {@link ThemeService.getState | getState} /
75
+ * {@link ThemeService.subscribe | subscribe} (e.g. with `useSyncExternalStore`);
76
+ * drive via {@link ThemeService.setActive | setActive} and the custom-theme
77
+ * methods. Contributing a *new* preset is a separate concern —
78
+ * {@link ExtensionContext.registerThemePreset}.
79
+ *
80
+ * @category Consumer Services
81
+ * @public
82
+ */
83
+ export interface ThemeService {
84
+ /** Current frozen view of theme state. */
85
+ getState(): ThemeState;
86
+ /** Subscribe to theme-state changes (active theme, custom themes, or presets). */
87
+ subscribe(listener: (s: ThemeState) => void): Disposable;
88
+ /** Set the active theme by id (a built-in preset, registered preset, or custom). */
89
+ setActive(id: string): void;
90
+ /** Resolve a theme id to its base + effective vars (for previews/swatches). */
91
+ resolve(id: string): ResolvedTheme;
92
+ /** Persist a custom theme to disk and refresh the in-memory list. */
93
+ saveCustom(theme: CustomTheme): Promise<void>;
94
+ /** Delete a custom theme from disk and refresh the in-memory list. */
95
+ deleteCustom(id: string): Promise<void>;
96
+ /** Reload custom themes from disk into the store. */
97
+ reloadCustom(): Promise<void>;
98
+ /** Strip the id from a custom theme for sharing/serialization. */
99
+ exportTheme(theme: CustomTheme): ThemeExport;
100
+ /** Validate/parse imported JSON into a custom theme (assigns a fresh id). */
101
+ importTheme(data: unknown): CustomTheme;
102
+ }