@achasoft/dsh-advanced-sidebar 0.1.0 → 0.3.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 (38) hide show
  1. package/README.md +279 -128
  2. package/cordis.patch.yml +31 -3
  3. package/lib/client.js +2803 -466
  4. package/lib/client.js.map +1 -1
  5. package/lib/host.js +2071 -418
  6. package/lib/index.js +6 -2
  7. package/lib/preview-content-BVUQ5oOR.js +465 -0
  8. package/lib/remote.js +330 -25
  9. package/lib/typert.host.js +330 -25
  10. package/lib/ui-preview.js +352 -0
  11. package/package.json +8 -2
  12. package/types/client/ActionMenu.d.ts +16 -1
  13. package/types/client/LogDownloadDialog.d.ts +24 -0
  14. package/types/client/contract.d.ts +57 -1
  15. package/types/client/index.d.ts +4 -2
  16. package/types/client/locales.d.ts +100 -0
  17. package/types/client/log-download.d.ts +179 -0
  18. package/types/client/panels/PreviewPanel.d.ts +20 -15
  19. package/types/client/panels/preview-file.d.ts +61 -0
  20. package/types/client/panels/preview-mode.d.ts +67 -0
  21. package/types/client/panels/preview-scratchpad.d.ts +53 -0
  22. package/types/client/panels/preview-url.d.ts +17 -0
  23. package/types/client/panels/shared.d.ts +15 -2
  24. package/types/client/preview-driver.d.ts +121 -0
  25. package/types/client/preview-storage.d.ts +43 -0
  26. package/types/client/preview-types.d.ts +21 -0
  27. package/types/client/preview-values.d.ts +43 -0
  28. package/types/host/deletion.d.ts +32 -23
  29. package/types/host/git.d.ts +94 -8
  30. package/types/host/index.d.ts +97 -5
  31. package/types/host/preview-content.d.ts +179 -0
  32. package/types/host/preview-serve.d.ts +242 -0
  33. package/types/host/settings-section.d.ts +49 -0
  34. package/types/host/types.d.ts +341 -0
  35. package/types/host/ui-bridge.d.ts +197 -0
  36. package/types/host/ui-preview-tool.d.ts +60 -0
  37. package/types/index.d.ts +6 -2
  38. package/types/ui-preview.d.ts +11 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Pure decisions the same-origin preview routes are built on: what a file is, what MIME type it
3
+ * gets, and whether a URL is one this plugin is willing to fetch or frame.
4
+ *
5
+ * Kept free of `ctx`, `node:http`, and the filesystem so every rule here is a table lookup a test
6
+ * can state in one line. The two rules that matter most — a proxy target must be loopback, and a
7
+ * framed document must be same-origin — are the whole difference between a helpful preview and an
8
+ * open proxy or a cross-origin hole, so they live in functions rather than in a request handler's
9
+ * middle.
10
+ * @module @achasoft/dsh-advanced-sidebar/host/preview-content
11
+ */
12
+ import type { PreviewFileKind } from './types.ts';
13
+ /**
14
+ * Absolute path of the workspace-file route; one route, parameterized by query.
15
+ *
16
+ * Declared here rather than in the module that registers it because the BROWSER half builds URLs
17
+ * from these strings too, and this module is the one both halves already share. Two literals that
18
+ * had to agree would eventually stop agreeing, and the symptom would be a frame that 404s only
19
+ * after a rename nobody thought was load-bearing.
20
+ */
21
+ export declare const FILE_ROUTE = "/advanced-sidebar/preview-file";
22
+ /** Absolute path of the loopback reverse proxy; subpaths are forwarded as-is. */
23
+ export declare const PROXY_ROUTE = "/advanced-sidebar/preview-proxy";
24
+ /** Absolute path of the scratchpad route: renders text the panel posts, with no file behind it. */
25
+ export declare const SCRATCHPAD_ROUTE = "/advanced-sidebar/preview-scratchpad";
26
+ /**
27
+ * The scratchpad route derived from the file route.
28
+ *
29
+ * The two differ only in their last segment, and a deployment that overrides one would have to
30
+ * override the other; deriving it is what makes that impossible to forget.
31
+ * @param fileRoute - the file route, `/…/preview-file`.
32
+ * @returns the scratchpad route.
33
+ */
34
+ export declare function scratchRoute(fileRoute: string): string;
35
+ /**
36
+ * The lower-case extension of a path, without its dot.
37
+ * @param path - a file path or a URL pathname.
38
+ * @returns the extension, or the empty string when there is none.
39
+ */
40
+ export declare function extensionOf(path: string): string;
41
+ /**
42
+ * The MIME type one path is served with.
43
+ * @param path - a file path or a URL pathname.
44
+ * @returns the type; `application/octet-stream` when the extension says nothing.
45
+ */
46
+ export declare function contentTypeOf(path: string): string;
47
+ /**
48
+ * Whether a MIME type is text that needs an explicit charset.
49
+ *
50
+ * `text/*` and the `+json`/`+xml` suffixes are the two families where a browser guessing a charset
51
+ * would be guessing right most of the time and wrong exactly when it matters — a UTF-8 source file
52
+ * rendered as latin-1.
53
+ * @param contentType - the type, without parameters.
54
+ * @returns true when the type should carry `; charset=utf-8`.
55
+ */
56
+ export declare function isTextual(contentType: string): boolean;
57
+ /**
58
+ * How the browser should present one path.
59
+ *
60
+ * `.html` is `iframe` rather than `text`: the frame is the point of the mode, and the same-origin
61
+ * route injects the base URL that makes its relative assets resolve. `.svg` is treated as an
62
+ * `iframe` too, because an SVG document is scriptable — handing it to an `<img>` would silently
63
+ * drop its scripts and hand it to the frame instead keeps one behaviour for "a document".
64
+ * @param path - a file path or a URL pathname.
65
+ * @param contentType - the type it is served with; defaults to the one {@link contentTypeOf} gives.
66
+ * @returns the preview kind.
67
+ */
68
+ export declare function classifyFile(path: string, contentType?: string): PreviewFileKind;
69
+ /**
70
+ * Reject anything that is not plain HTTP(S).
71
+ *
72
+ * A `file:` or `data:` URL handed to the proxy would read the Host's own disk, and a `javascript:`
73
+ * one would be an injection; none of them is something a preview of a dev server needs.
74
+ * @param value - the candidate URL.
75
+ * @returns the parsed URL, or the reason it was refused.
76
+ */
77
+ export declare function parseHttpUrl(value: string): {
78
+ ok: true;
79
+ url: URL;
80
+ } | {
81
+ ok: false;
82
+ message: string;
83
+ };
84
+ /**
85
+ * Whether a parsed URL points at this machine.
86
+ *
87
+ * The proxy exists so a loopback dev server can be framed same-origin. Without this check it would
88
+ * also fetch `http://10.0.0.5/admin` on the operator's behalf, from the operator's network position
89
+ * — an open proxy bolted to the GUI. Only literal loopback names and addresses pass: `localhost`,
90
+ * `127.0.0.0/8`, and `[::1]`.
91
+ *
92
+ * A hostname that merely *resolves* to loopback (a split-horizon DNS entry, a hostfile alias) is
93
+ * refused rather than probed. Deciding this by lookup would make the answer depend on the resolver
94
+ * at request time, and a DNS rebinding attack is exactly the case where the answer changes between
95
+ * the check and the fetch.
96
+ * @param url - a parsed URL.
97
+ * @returns true when the host is a loopback literal.
98
+ */
99
+ export declare function isLoopbackHost(url: URL): boolean;
100
+ /**
101
+ * Refuse a URL this plugin will not fetch.
102
+ * @param value - the candidate URL.
103
+ * @returns the parsed URL, or the reason it was refused.
104
+ */
105
+ export declare function validateProxyTarget(value: string): {
106
+ ok: true;
107
+ url: URL;
108
+ } | {
109
+ ok: false;
110
+ message: string;
111
+ };
112
+ /**
113
+ * Percent-encode a value for a query string, using the one encoder every runtime here has.
114
+ * @param value - the raw value.
115
+ * @returns the encoded value.
116
+ */
117
+ export declare function encodeQuery(value: string): string;
118
+ /**
119
+ * Build the same-origin URL one workspace file is framed from.
120
+ * @param fileRoute - the absolute file route path, no trailing slash.
121
+ * @param workspacePath - absolute Host workspace directory.
122
+ * @param filePath - absolute Host path inside it.
123
+ * @returns the path plus query string.
124
+ */
125
+ export declare function fileUrl(fileRoute: string, workspacePath: string, filePath: string): string;
126
+ /**
127
+ * Build the same-origin URL that proxies one absolute upstream URL.
128
+ * @param proxyRoute - the absolute proxy route path, no trailing slash.
129
+ * @param target - the loopback URL to fetch.
130
+ * @returns the path plus query string.
131
+ */
132
+ export declare function proxyUrlFor(proxyRoute: string, target: string): string;
133
+ /**
134
+ * Turn one absolute upstream URL into a same-origin frame URL when it is a loopback target.
135
+ *
136
+ * A cross-origin dev server is left exactly as it is: pointing the GUI's own proxy at a host it
137
+ * would refuse is not an improvement, and the panel says why the frame is opaque rather than
138
+ * silently refusing to show it.
139
+ * @param proxyRoute - the absolute proxy route path, no trailing slash.
140
+ * @param raw - the URL a person typed.
141
+ * @returns the frame URL, and whether it is same-origin with the GUI.
142
+ */
143
+ export declare function frameUrlFor(proxyRoute: string, raw: string): {
144
+ src: string;
145
+ sameOrigin: boolean;
146
+ };
147
+ /**
148
+ * Insert a `<base>` element into an HTML document's head.
149
+ *
150
+ * String surgery rather than a parser on purpose: this runs on every HTML response, the document is
151
+ * untrusted, and the only structural fact needed — "where does the head begin" — is unambiguous in
152
+ * any HTML a browser will accept. The base goes immediately after the head's own opening tag, or
153
+ * immediately after `<html>` when there is no head, or at the very front when there is neither —
154
+ * which the parser then files into the implied head, where a base belongs.
155
+ *
156
+ * The global regex is what makes "no head" mean it: `indexOf('<head')` alone matches the substring
157
+ * inside `<header>`, and splicing a base into a `<header>` would leave the document resolving every
158
+ * relative URL against the wrong place.
159
+ * @param html - the document text, as bytes decoded by the caller.
160
+ * @param baseHref - the absolute same-origin prefix relative paths resolve against.
161
+ * @returns the document with the base element added.
162
+ */
163
+ export declare function injectBase(html: string, baseHref: string): string;
164
+ /**
165
+ * Whether a byte window looks like an HTML document that should get the base marker.
166
+ * @param contentType - the response's MIME type.
167
+ * @param bytes - the document's leading bytes.
168
+ * @returns true for an HTML response.
169
+ */
170
+ export declare function isHtmlDocument(contentType: string, bytes: Uint8Array): boolean;
171
+ /**
172
+ * Decode a byte window as UTF-8 for the base injection, preserving nothing it cannot decode.
173
+ *
174
+ * Non-fatal on purpose: an HTML file whose tail is cut mid-sequence is still a document worth
175
+ * rendering, and one replacement character is better than a 500.
176
+ * @param bytes - the window.
177
+ * @returns the decoded text.
178
+ */
179
+ export declare function decodeText(bytes: Uint8Array): string;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Same-origin preview serving: the Host routes that make a workspace file and a loopback dev server
3
+ * loadable *from the GUI's own origin*.
4
+ *
5
+ * This is the piece the whole agent-driven debugging story rests on. An `<iframe src="http://127.0.0.1:5173">`
6
+ * is cross-origin, and a cross-origin frame's `document` is unreachable — the panel cannot read its
7
+ * DOM, its console, or its box metrics, and neither can the model. Serving the same bytes from
8
+ * `/advanced-sidebar/preview-file?…` and proxying the dev server through
9
+ * `/advanced-sidebar/preview-proxy?url=…` makes the frame same-origin with the page that hosts it,
10
+ * so the panel's driver can inspect and drive it directly.
11
+ *
12
+ * Four deliberate refusals keep that power from becoming a hole in the GUI:
13
+ *
14
+ * 1. **Every route answers only the authenticated GUI.** Each HTTP route and the websocket upgrade
15
+ * call the Host connection's `requestRejection` before anything else — the same gate `/api`, the
16
+ * Typert websocket and the harness's own open-in-app routes use. It applies the Host/Origin fence
17
+ * (a loopback or configured `trustedHosts` authority, no `Sec-Fetch-Site: cross-site`, an `Origin`
18
+ * that matches the `Host` when present — which is what defeats DNS rebinding) and then verifies the
19
+ * signed, authority-bound `dsh-auth-*` browser cookie. That cookie is `SameSite=Strict`, so the
20
+ * panel's same-origin `<iframe src>`, its `fetch`, and the frame's own subresources all carry it,
21
+ * while a page on any other site cannot. A Host whose connection exposes no such gate gets **no
22
+ * routes at all**: these routes read files and relay to local ports, and an ungated copy of them is
23
+ * a file server for every process and web page that can reach the port.
24
+ * 2. **The proxy only talks to loopback.** `validateProxyTarget` refuses every host that is not a
25
+ * loopback literal, so this cannot fetch an intranet service from the operator's network
26
+ * position. A URL that merely resolves to loopback is refused too — see that function. The GUI's
27
+ * own `dsh-auth-*` cookie is removed from what is forwarded, so a dev server never receives a
28
+ * credential for the harness, and cannot overwrite it either.
29
+ * 3. **A file is only ever read from inside a registered workspace.** The request names a workspace,
30
+ * but that name is a claim: the file must also sit inside one of the workspaces the harness's own
31
+ * `workspaceRegistry` holds, or it is refused. Without that, `?workspace=/` made the whole disk a
32
+ * workspace. A Host with no registry serves no file.
33
+ * 4. **Containment is the filesystem's, not string arithmetic.** Every path goes through
34
+ * `resolveWorkspace`/`resolveInside`, the same containment the Files panel uses, and a symlink
35
+ * that escapes is caught by the filesystem's own canonicalization.
36
+ *
37
+ * Nothing here is cached. A dev server's own asset pipeline already handles its own caching; a
38
+ * workspace file is exactly the thing an operator edits and expects to see change, so the route
39
+ * answers `no-store` plus a weak validator and lets the panel decide when to reload.
40
+ * @module @achasoft/dsh-advanced-sidebar/host/preview-serve
41
+ */
42
+ import type { Context } from '@deepseek-ai/cordis';
43
+ export { FILE_ROUTE, PROXY_ROUTE, SCRATCHPAD_ROUTE } from './preview-content.ts';
44
+ import type { AdvancedSidebarSettings, PreviewFileInfoResult, PreviewSurfaceInfo } from './types.ts';
45
+ /**
46
+ * Largest scratchpad document the Host will echo back.
47
+ *
48
+ * The scratchpad is a person typing HTML into a text area; a megabyte of it is a mistake or an
49
+ * attempt to make the Host hold memory, and neither is worth serving.
50
+ */
51
+ export declare const SCRATCHPAD_MAX_BYTES: number;
52
+ /**
53
+ * Serves workspace files and proxies loopback dev servers on the GUI's own origin.
54
+ *
55
+ * One instance is created by the service and disposed with it, which is what guarantees no route
56
+ * outlives the plugin: every registration returns a disposer, and `dispose()` runs them all.
57
+ */
58
+ export declare class PreviewSurface {
59
+ private readonly ctx;
60
+ private readonly source;
61
+ /**
62
+ * The upstream URL each panel last pointed its frame at, keyed by its client id.
63
+ *
64
+ * Only a fallback: a proxied document's own URLs all carry their target explicitly, because the
65
+ * browser rewrites them from the injected `<base>`. What needs this map is a request the base
66
+ * cannot reach — a `fetch()` from inside the page to a relative path, or a link with no base of
67
+ * its own — and there is exactly one sensible target for those.
68
+ */
69
+ private readonly targets;
70
+ /** Every request currently in flight, so a dispose does not leave sockets open. */
71
+ private readonly live;
72
+ private closed;
73
+ /** Whether the routes are registered; see {@link RouteState}. */
74
+ private routes;
75
+ /**
76
+ * @param ctx - Host context carrying the optional filesystem capability.
77
+ * @param source - reads the current settings section; called per request.
78
+ */
79
+ constructor(ctx: Context, source: () => AdvancedSidebarSettings);
80
+ /**
81
+ * What this surface is, for `describe()`.
82
+ * @returns the route paths and whether the routes are actually mounted.
83
+ */
84
+ info(): PreviewSurfaceInfo;
85
+ /**
86
+ * The URL one workspace file is framed from, or undefined when this Host serves no routes.
87
+ * @param workspacePath - absolute Host workspace directory.
88
+ * @param filePath - absolute Host path inside it.
89
+ * @returns the same-origin path, query included.
90
+ */
91
+ fileUrl(workspacePath: string, filePath: string): string | undefined;
92
+ /**
93
+ * The same-origin URL that proxies one loopback URL.
94
+ * @param target - the loopback URL, already validated by the caller.
95
+ * @returns the same-origin path, query included.
96
+ */
97
+ proxyUrl(target: string): string;
98
+ /**
99
+ * Register every route with the mounted web server, each behind the connection's request gate.
100
+ *
101
+ * Registration goes through `ctx.inject(['connection', 'webServer'], …)` rather than a constructor
102
+ * read: a headless deployment composes neither, and the inject face simply never runs, leaving the
103
+ * rest of the plugin working. The connection is a hard requirement rather than an optional extra —
104
+ * it is the only thing that can tell the GUI's own browser from any other client of the port — so a
105
+ * connection without `requestRejection` mounts nothing and says so through {@link info}.
106
+ */
107
+ install(): void;
108
+ /** Forget every target and abort every in-flight request. Called from the plugin's teardown. */
109
+ dispose(): void;
110
+ /**
111
+ * Describe one workspace file for the panel, or refuse it.
112
+ * @param workspacePath - absolute Host workspace directory.
113
+ * @param path - absolute Host path, or a path relative to the workspace.
114
+ * @param signal - cancellation for the resolution and the metadata read.
115
+ * @returns the file's kind, size, frame URL, and change token.
116
+ */
117
+ info_(workspacePath: string | undefined, path: string, signal?: AbortSignal): Promise<PreviewFileInfoResult>;
118
+ /**
119
+ * Serve one workspace file as the frame's document or as one of its subresources.
120
+ * @param req - the incoming request.
121
+ * @param res - the response the handler owns.
122
+ */
123
+ private handleFile;
124
+ /**
125
+ * Serve text the panel posted, as an HTML document on this origin.
126
+ *
127
+ * The scratchpad renders as `text/html` rather than through `srcdoc` so that the frame's document
128
+ * has a real URL with this origin: `document.baseURI`, relative `fetch`, and `window.location` all
129
+ * then behave the way the page under test expects, and an agent's `eval` sees them.
130
+ * @param req - the incoming request.
131
+ * @param res - the response the handler owns.
132
+ */
133
+ private handleScratchpad;
134
+ /**
135
+ * Forward one request to a loopback upstream and stream the answer back.
136
+ * @param req - the incoming request.
137
+ * @param res - the response the handler owns.
138
+ */
139
+ private handleProxy;
140
+ /**
141
+ * Forward one upgraded connection to a loopback upstream.
142
+ *
143
+ * A dev server's live-reload socket is an optional convenience, not part of the inspection story:
144
+ * a websocket carries no DOM and no console, and a page whose socket never opens still renders and
145
+ * is still drivable. The tunnel is here because it is cheap — a raw `net` pipe with no protocol
146
+ * knowledge — but a server that negotiates on a path other than the route's own root is not
147
+ * tunnelled, because the web server's upgrade seat matches exact paths and claiming a wildcard
148
+ * would collide with the app's own sockets.
149
+ * @param req - the upgrade request.
150
+ * @param socket - the client socket the handler owns.
151
+ * @param head - bytes the parser already read past the request line.
152
+ */
153
+ private handleUpgrade;
154
+ /**
155
+ * One upstream request, with the response streamed straight through.
156
+ * @param req - the client request.
157
+ * @param res - the client response.
158
+ * @param upstreamUrl - the absolute loopback URL to fetch.
159
+ */
160
+ private forward;
161
+ /**
162
+ * Resolve a workspace and one path inside it.
163
+ * @param workspacePath - absolute Host workspace directory, absent to treat the path itself as one.
164
+ * @param path - absolute Host path, or a path relative to the workspace.
165
+ * @param signal - cancellation for the resolution.
166
+ * @returns the workspace, the contained target, and the path as it should be displayed.
167
+ */
168
+ private resolve;
169
+ /**
170
+ * Refuse a target that sits inside none of the harness's registered workspaces.
171
+ *
172
+ * The workspace a request names is only the root its relative paths hang from; it is not evidence
173
+ * that the directory is one the operator opened. `?workspace=/` would otherwise make the whole disk
174
+ * a workspace, so the canonical target is checked against the registry's own canonical paths — the
175
+ * list the sidebar shows, and nothing a request can add to. A subdirectory of a registered
176
+ * workspace passes, because the file is still inside what the operator opened.
177
+ *
178
+ * Each registered path is resolved through the same filesystem as the target, so a symlinked
179
+ * workspace compares as its real directory on both sides. A registered directory that no longer
180
+ * resolves (deleted, unmounted) simply contains nothing.
181
+ * @param target - the canonical target the request resolved to.
182
+ * @param path - the path as asked, for the refusal message.
183
+ * @param signal - cancellation for the resolutions.
184
+ * @returns the refusal, or undefined when a registered workspace contains the target.
185
+ */
186
+ private outsideRegisteredWorkspaces;
187
+ /**
188
+ * Describe one already-resolved file.
189
+ * @param workspace - the resolved workspace.
190
+ * @param target - the resolved target.
191
+ * @param display - the path to report back and to base the content type on.
192
+ * @param signal - cancellation for the metadata and token reads.
193
+ * @returns the description.
194
+ */
195
+ private describeFile;
196
+ /**
197
+ * A weak validator for one file: the backend's version, its size, and a digest of its head.
198
+ * @param target - the resolved target.
199
+ * @param version - the backend's opaque freshness token.
200
+ * @param size - the byte size; `undefined` asks for no probe read.
201
+ * @param reportedSize - the size the backend reported, which may be absent.
202
+ * @param signal - cancellation for the probe read.
203
+ * @returns the ETag body, quotes included.
204
+ */
205
+ private etag;
206
+ /**
207
+ * The target one subresource request should be forwarded to.
208
+ * @param req - the subresource request.
209
+ * @param suffix - the path below the proxy route, without a leading slash.
210
+ * @returns the validated target, or undefined when this Host holds none for the caller.
211
+ */
212
+ private fallbackTarget;
213
+ /**
214
+ * Record the target one panel is framing, so its subresources resolve.
215
+ * @param clientId - the panel's id.
216
+ * @param target - the upstream URL, or undefined when the panel stopped framing one.
217
+ */
218
+ rememberTarget(clientId: string, target: string | undefined): void;
219
+ }
220
+ /**
221
+ * A `Cookie` request header minus the harness's own browser-session cookie.
222
+ * @param header - the raw header; Node joins repeated `Cookie` headers into one string.
223
+ * @returns the remaining pairs, or undefined when nothing remains.
224
+ */
225
+ export declare function withoutHostAuthCookies(header: string | readonly string[]): string | undefined;
226
+ /** One byte range a request asked for. */
227
+ interface Range {
228
+ /** First byte, inclusive. */
229
+ readonly start: number;
230
+ /** Last byte, inclusive. */
231
+ readonly end: number;
232
+ }
233
+ /**
234
+ * Parse one `Range` header against a known size.
235
+ *
236
+ * Only `bytes=` and only a single range are honoured: a multipart range costs a multipart encoder to
237
+ * serve, and the one caller that matters — a `<video>` scrubber — asks for one range at a time.
238
+ * @param header - the raw header value.
239
+ * @param size - the file's size in bytes.
240
+ * @returns the range, undefined for a full response, or `unsatisfiable`.
241
+ */
242
+ export declare function parseRange(header: string | undefined, size: number): Range | undefined | 'unsatisfiable';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Attaching this plugin's settings section to the harness settings provider.
3
+ *
4
+ * Harness 0.1.1-rc.2 and earlier exported two free helpers from `@deepseek-ai/dsh-settings` for
5
+ * this, `installSettingsSection` and `settingsNamespace`. Current harnesses removed both: the same
6
+ * wiring is the provider's own `settings.installSection(owner, ns, schema, entry, hooks)`, reached
7
+ * through `ctx.inject(['settings'], …)`. Importing the old helpers by name fails to link on a
8
+ * current install, so the plugin carries these two small equivalents instead of depending on a
9
+ * compatibility shim that only exists on a development machine.
10
+ *
11
+ * Injecting `settings` rather than requiring it keeps the plugin loadable in a deployment that
12
+ * mounts no settings provider: the section simply never attaches, and the composition entry the
13
+ * plugin was configured with stays the source.
14
+ * @module @achasoft/dsh-advanced-sidebar/host/settings-section
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ import type z from '@deepseek-ai/schemastery';
18
+ /** What a consumer hands the provider, as the installed harness defines it. */
19
+ export interface SettingsSectionHooks<T> {
20
+ /**
21
+ * Receive the active configuration source: the resolved settings scope while one is attached,
22
+ * the composition entry otherwise.
23
+ * @param current - thunk returning the currently authoritative value.
24
+ */
25
+ setSource: (current: () => T) => void;
26
+ /** Re-judge anything derived from the source after an attach, a detach, or a committed change. */
27
+ onChange: () => void;
28
+ /**
29
+ * Reject a resolved section the plugin could not act on, for constraints its schema cannot express.
30
+ * @param value - the resolved section.
31
+ */
32
+ validate?: (value: T) => void;
33
+ }
34
+ /**
35
+ * Check a settings namespace against the provider's grammar, failing at load rather than at attach.
36
+ * @param value - the namespace.
37
+ * @returns the same namespace.
38
+ * @throws TypeError when it is not a lowercase hyphenated identifier.
39
+ */
40
+ export declare function settingsNamespace<const N extends string>(value: N): N;
41
+ /**
42
+ * Attach one settings section whenever a settings provider is present.
43
+ * @param ctx - the owning plugin context; its unload detaches the section.
44
+ * @param ns - the plugin's settings namespace.
45
+ * @param schema - schema resolving the section.
46
+ * @param entry - the composition entry, used as the base layer and as the fallback without a provider.
47
+ * @param hooks - source sink, change notification, and optional validation.
48
+ */
49
+ export declare function installSettingsSection<T>(ctx: Context, ns: string, schema: z<T>, entry: T, hooks: SettingsSectionHooks<T>): void;