dsh-opencode-go 0.1.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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The OpenCode Go adapter: one route, one catalog, per-request routing header.
3
+ *
4
+ * Every request to the gateway carries two Harness-owned headers: the
5
+ * attribution User-Agent (`deepseek-harness/<version>`), which pi-ai's client
6
+ * lets request headers override, and `x-opencode-session`, which the gateway
7
+ * requires and uses to route a conversation and share its prompt cache. The
8
+ * header value is the request's session id — stable per conversation, so
9
+ * caching and billing attribution stay correct; a request arriving with no
10
+ * session id gets a fresh random value rather than a shared constant, because
11
+ * a constant would merge unrelated traffic into one cache bucket.
12
+ *
13
+ * Multi-turn correctness rides on the shared pi-ai conversion machinery
14
+ * (`toPiContext` reconstructs provider-native replay state from the session
15
+ * log; `toStreamChunks` maps events to seam chunks), so assistant history,
16
+ * tool calls, and usage land in the session log exactly as the generic pi-ai
17
+ * adapter records them. Image content rides the same machinery: models whose
18
+ * catalog entry declares the image modality convert attachments through the
19
+ * durable attachment service, and every other model refuses image content
20
+ * before any provider I/O.
21
+ *
22
+ * @module dsh-llm-opencode-go/adapter
23
+ */
24
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
25
+ import type { GenerateOptions, ImageAttachmentAccess, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
26
+ import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
27
+ import { OpencodeGoCatalog } from './catalog.ts';
28
+ import type { OpencodeGoConfig } from './config.ts';
29
+ /**
30
+ * The attachment-service bridges one image request reads. Construction-time
31
+ * (context-dependent); the config-dependent policy numbers are merged per
32
+ * request from the current configuration.
33
+ */
34
+ export interface OpencodeGoImageAccess {
35
+ /** Resolve the optional durable attachment service at request time. */
36
+ resolveAttachments: () => AttachmentStore | undefined;
37
+ /** Bridge one attachment reference into the current model-tool execution world. */
38
+ resolveImageAccess: (attachments: AttachmentStore, ref: ImageAttachmentRef) => ImageAttachmentAccess | undefined;
39
+ }
40
+ /** Constructor inputs for {@link OpencodeGoAdapter}. */
41
+ export interface OpencodeGoAdapterOptions {
42
+ /**
43
+ * The current configuration, re-read at every operation: a settings write
44
+ * reaches the next request without a restart, and one operation never mixes
45
+ * two configuration generations.
46
+ */
47
+ config: () => OpencodeGoConfig;
48
+ /** Resolve the route's credential per call; missing must fail loud. */
49
+ resolveApiKey: () => Promise<string | undefined>;
50
+ /**
51
+ * Image input machinery; absent refuses image content, which is the posture
52
+ * for direct construction without a durable attachment service behind it.
53
+ */
54
+ imageAccess?: OpencodeGoImageAccess;
55
+ /** Observe the catalog falling back to the curated table. */
56
+ onFallback?: (detail: {
57
+ url: string;
58
+ error: unknown;
59
+ kept: number;
60
+ }) => void;
61
+ /** Observe live ids the curated table cannot route. */
62
+ onOmitted?: (ids: readonly string[]) => void;
63
+ /** Observe assistant history degrading to provider-neutral conversion. */
64
+ onReplayDegrade?: (reason: string) => void;
65
+ }
66
+ /**
67
+ * The single route's adapter. The catalog snapshot freezes at each operation,
68
+ * so a refresh between two requests never mixes model generations inside one
69
+ * call.
70
+ */
71
+ export declare class OpencodeGoAdapter extends LlmAdapter {
72
+ private readonly options;
73
+ /**
74
+ * One catalog instance per endpoint/refresh pair. A settings write that
75
+ * changes either gets a fresh resolver (and a fresh live-listing fetch) on
76
+ * the next operation; an unchanged configuration keeps its cached snapshot
77
+ * for the whole refresh interval.
78
+ */
79
+ private catalogCache;
80
+ constructor(options: OpencodeGoAdapterOptions);
81
+ /**
82
+ * The catalog resolver for one configuration, rebuilding on the facts it
83
+ * owns. Public for the plugin's discovery registration, which resolves the
84
+ * current configuration the same way the adapter does.
85
+ * @param config - the configuration whose endpoint and refresh interval the
86
+ * resolver serves; a change to either yields a fresh resolver.
87
+ * @returns the resolver caching one snapshot per endpoint/refresh pair.
88
+ */
89
+ catalogOf(config: OpencodeGoConfig): OpencodeGoCatalog;
90
+ providerInfo(provider: string): {
91
+ id: string;
92
+ name: string;
93
+ };
94
+ listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
95
+ resolveModel(_provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
96
+ /** Describe one model: capacities plus the reasoning levels it actually offers. */
97
+ private modelInfo;
98
+ /** Validate an explicit effort against the model's own levels, without clamping. */
99
+ private resolveReasoningLevel;
100
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
101
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The OpenCode Go model catalog: a curated table intersected with the gateway's
3
+ * live model list.
4
+ *
5
+ * The curated table is seeded from pi-ai's installed `opencode-go` catalog —
6
+ * its entries carry the wire protocol, compat quirks, thinking-level spellings,
7
+ * and capacities that the gateway's own listing endpoint does not disclose —
8
+ * plus this package's additions for models the installed catalog has not
9
+ * caught up with. The gateway rotates its list faster than either pi-ai or
10
+ * this package releases, so the served catalog is the curated table **minus**
11
+ * models the live listing no longer advertises: a model the gateway retired
12
+ * must disappear rather than error mid-session. Live ids the curated table
13
+ * does not describe are omitted too, because without a protocol mapping they
14
+ * cannot be routed correctly.
15
+ *
16
+ * The live listing at `{baseURL}/models` is public and answers ids only. A
17
+ * fetch failure never takes the route down: the curated table is served as-is
18
+ * and the failure is reported, on the grounds that a network blip should not
19
+ * deny service, while model discovery — whose whole purpose is the live
20
+ * answer — fails loud instead.
21
+ *
22
+ * @module dsh-llm-opencode-go/catalog
23
+ */
24
+ import type { Api, Model, Provider } from '@earendil-works/pi-ai';
25
+ import type { LlmDiscoveredModel } from '@deepseek-ai/dsh-llm';
26
+ /** Provider route key this plugin registers and every materialized model carries. */
27
+ export declare const PROVIDER_ID = "opencode-go";
28
+ /** Display name for selectors and status labels. */
29
+ export declare const DISPLAY_NAME = "OpenCode Go";
30
+ /** Endpoint serving both model requests and the model listing. */
31
+ export declare const DEFAULT_BASE_URL = "https://opencode.ai/zen/go/v1";
32
+ /** One immutable resolution of the served catalog. */
33
+ export interface CatalogSnapshot {
34
+ /** Served models by id: the curated table minus models absent from the live listing. */
35
+ readonly models: ReadonlyMap<string, Model<Api>>;
36
+ /** The provider serving exactly these models. */
37
+ readonly provider: Provider;
38
+ /** Whether the live listing answered this resolution; `false` on the fallback path. */
39
+ readonly live: boolean;
40
+ /** When this resolution completed, for the refresh interval. */
41
+ readonly fetchedAtMs: number;
42
+ }
43
+ /**
44
+ * Read one live listing reply. The endpoint answers the standard OpenAI
45
+ * `data` array with entries carrying ids only; anything else is a fault the
46
+ * caller treats as "live unavailable" and falls back. An honest empty array
47
+ * is a valid answer: the gateway says it serves nothing.
48
+ * @param body - the decoded JSON reply.
49
+ * @returns the non-empty ids it advertises, in reply order.
50
+ * @throws {Error} when the reply carries no `data` array.
51
+ */
52
+ export declare function readLiveModelIds(body: unknown): readonly string[];
53
+ /**
54
+ * TTL-cached resolution of the served catalog. Concurrent callers share one
55
+ * in-flight fetch; a completed snapshot serves unchanged until the refresh
56
+ * interval elapses, so one stream call never observes two catalog generations.
57
+ */
58
+ export declare class OpencodeGoCatalog {
59
+ private readonly baseURL;
60
+ private readonly refreshMs;
61
+ private readonly onFallback;
62
+ private readonly onOmitted;
63
+ private served;
64
+ private pending;
65
+ /**
66
+ * @param baseURL - The endpoint the gateway serves; also the listing base.
67
+ * @param refreshMs - How long one live resolution stays authoritative.
68
+ * @param onFallback - Observes a failed live listing and how many curated
69
+ * models kept serving because of it.
70
+ * @param onOmitted - Observes live ids the curated table cannot route.
71
+ */
72
+ constructor(baseURL: string, refreshMs: number, onFallback: (detail: {
73
+ url: string;
74
+ error: unknown;
75
+ kept: number;
76
+ }) => void, onOmitted: (ids: readonly string[]) => void);
77
+ /**
78
+ * The current catalog, fetching when expired or never fetched.
79
+ * @returns the snapshot now serving, shared by concurrent callers.
80
+ */
81
+ snapshot(): Promise<CatalogSnapshot>;
82
+ /** One resolution: intersect the curated table with the live listing. */
83
+ private build;
84
+ }
85
+ /**
86
+ * Candidate models for the configuration surface's "fetch available models"
87
+ * action: the same live intersection the route would serve, with the curated
88
+ * capacities the listing endpoint does not disclose. Unlike route resolution,
89
+ * discovery fails when the live listing is unreachable: its whole purpose is
90
+ * the live answer, and a stale answer would offer models the gateway retired.
91
+ * @param catalog - The route's catalog resolver.
92
+ * @returns the served models in curated order.
93
+ * @throws LlmError `DISCOVERY_FAILED` when the live listing is unreachable.
94
+ */
95
+ export declare function discoverCatalogModels(catalog: OpencodeGoCatalog): Promise<readonly LlmDiscoveredModel[]>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The OpenCode Go settings section. It leads with the one value a user has to
3
+ * supply — the API key, stored write-only through the credentials domain — and
4
+ * the models the gateway currently serves, then keeps the credential
5
+ * reference, the endpoint, and the adapter tuning fields in the
6
+ * `llm-opencode-go` namespace behind a collapsed disclosure.
7
+ */
8
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
9
+ import type { OpencodeGoSectionFace } from './section-controller.ts';
10
+ import type { en } from './locales.ts';
11
+ export type { OpencodeGoSectionState } from './section-controller.ts';
12
+ /** Section copy lookup, including the optional `{name}` template params. */
13
+ type SectionTranslate = (key: keyof typeof en, params?: Record<string, unknown>) => string;
14
+ /** Injected dependencies of {@link OpencodeGoSection} (slot `inject`). */
15
+ export interface OpencodeGoSectionInjected extends OpencodeGoSectionFace {
16
+ /** Section copy. */
17
+ t: SectionTranslate;
18
+ }
19
+ /** Props delivered by the slot outlet: the inject face spread flat. */
20
+ export type OpencodeGoSectionProps = Partial<InjectFace<OpencodeGoSectionInjected>>;
21
+ /**
22
+ * Render the OpenCode Go settings page.
23
+ * @param props - locale copy, the page snapshot, and its form actions.
24
+ * @returns the section.
25
+ */
26
+ export declare function OpencodeGoSection(props: OpencodeGoSectionProps): import("react").JSX.Element | null;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * OpenCode Go settings plugin, browser half. Registers the "OpenCode Go"
3
+ * settings page over the `llm-opencode-go` namespace: the API key (stored
4
+ * write-only through the credentials domain), the gateway's current model
5
+ * listing, and the adapter knobs behind the page's advanced disclosure. The
6
+ * Host settings and credential contracts stay behind their existing wire APIs.
7
+ */
8
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
9
+ import { en } from './locales.ts';
10
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
11
+ interface LocaleNamespaceMap {
12
+ /** The OpenCode Go settings page copy. */
13
+ 'settings.opencode-go': keyof typeof en;
14
+ }
15
+ }
16
+ export type { OpencodeGoSectionProps } from './Section.tsx';
17
+ export type { OpencodeGoSectionState, OpencodeGoSettings } from './section-controller.ts';
18
+ export { OPENCODE_GO_NS } from './section-controller.ts';
19
+ export type { OpencodeGoKey } from './locales.ts';
20
+ /**
21
+ * Required services (cordis fiber inject). The target slot is declared by
22
+ * ui-settings' apply, whose activation order relative to this one is NOT
23
+ * constrained; registration depends on each slot through `slots.inject()`.
24
+ * `remote.llm` is the discovery namespace the page reads the model listing
25
+ * through.
26
+ */
27
+ export declare const inject: string[];
28
+ /**
29
+ * Register the section once the `settings.section` declaration is on the
30
+ * ledger, and keep the credential badge fresh on Host-reported key changes
31
+ * written from any surface.
32
+ * @param ctx - client root context.
33
+ */
34
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,50 @@
1
+ /** Copy dictionaries for the OpenCode Go settings page. English is the key-set source of truth. */
2
+ export declare const en: {
3
+ nav: string;
4
+ title: string;
5
+ intro: string;
6
+ enabledLabel: string;
7
+ enabledHint: string;
8
+ enabledOff: string;
9
+ enabledUnavailable: string;
10
+ keyLabel: string;
11
+ keyHint: string;
12
+ keyConfigured: string;
13
+ keyMissing: string;
14
+ keyNotWritable: string;
15
+ modelsLabel: string;
16
+ modelsRefresh: string;
17
+ modelsLoading: string;
18
+ modelsFailed: string;
19
+ modelsEmpty: string;
20
+ modelsCount: string;
21
+ advancedLabel: string;
22
+ advancedHint: string;
23
+ apiKeyEnvLabel: string;
24
+ apiKeyEnvHint: string;
25
+ baseURLLabel: string;
26
+ baseURLHint: string;
27
+ refreshMinutesLabel: string;
28
+ refreshMinutesHint: string;
29
+ streamIdleTimeoutMsLabel: string;
30
+ streamIdleTimeoutMsHint: string;
31
+ maxRequestImageBytesLabel: string;
32
+ maxRequestImageBytesHint: string;
33
+ requestImagePixelBudgetLabel: string;
34
+ requestImagePixelBudgetHint: string;
35
+ requestImageMaxBytesLabel: string;
36
+ requestImageMaxBytesHint: string;
37
+ overridden: string;
38
+ reset: string;
39
+ invalidValue: string;
40
+ save: string;
41
+ discard: string;
42
+ saving: string;
43
+ savedFailed: string;
44
+ readOnly: string;
45
+ unavailable: string;
46
+ };
47
+ /** Chinese copy; the key set mirrors {@link en}. */
48
+ export declare const zh: typeof en;
49
+ /** Every copy key this page's dictionaries must supply. */
50
+ export type OpencodeGoKey = keyof typeof en;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The OpenCode Go settings page's staged form over the `llm-opencode-go`
3
+ * settings namespace, plus the gateway model listing the page reports.
4
+ *
5
+ * The key is the one control that does not live in the section: its literal
6
+ * never rides a response, so the page learns only whether one is configured
7
+ * and writes it through the credentials domain, addressed by the reference the
8
+ * section names. It is still staged with the rest of the form, so one save
9
+ * covers everything the page shows.
10
+ */
11
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
12
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
13
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
14
+ import { type FieldState, type FormActions, type FormShell } from './staged-form.ts';
15
+ /** Namespace of the OpenCode Go adapter. Spelled here rather than imported: a client package must not depend on a Host package. */
16
+ export declare const OPENCODE_GO_NS = "llm-opencode-go";
17
+ /** The adapter fields this page edits. */
18
+ export interface OpencodeGoSettings {
19
+ /** Whether the adapter serves its route; false withdraws it from every picker. */
20
+ enabled?: boolean;
21
+ /** Credential reference naming the environment key. */
22
+ apiKeyEnv?: string;
23
+ /** The gateway endpoint; also the live listing base. */
24
+ baseURL?: string;
25
+ /** Live catalog re-resolution interval, in minutes. */
26
+ refreshMinutes?: number;
27
+ /** Largest idle gap between stream events, in milliseconds. */
28
+ streamIdleTimeoutMs?: number;
29
+ /** Accumulated base64 image payload bound for one request. */
30
+ maxRequestImageBytes?: number;
31
+ /** Total-pixel budget for one request image. */
32
+ requestImagePixelBudget?: number;
33
+ /** Raw encoded-byte target for one request image. */
34
+ requestImageMaxBytes?: number;
35
+ }
36
+ /** The gateway's model listing as the page reports it. */
37
+ export type OpencodeGoModels =
38
+ /** Not asked for yet; the page asks once it mounts. */
39
+ {
40
+ readonly status: 'idle';
41
+ }
42
+ /** A listing request is outstanding. */
43
+ | {
44
+ readonly status: 'loading';
45
+ }
46
+ /** The gateway answered: how many models it serves, and a preview of their names. */
47
+ | {
48
+ readonly status: 'ready';
49
+ readonly count: number;
50
+ readonly preview: readonly string[];
51
+ }
52
+ /** The listing could not be read; `message` is the Host's own diagnostic. */
53
+ | {
54
+ readonly status: 'failed';
55
+ readonly message: string;
56
+ };
57
+ /** What the settings page renders. */
58
+ export interface OpencodeGoSectionState extends FormShell {
59
+ /**
60
+ * Whether the adapter currently serves its route. Resolved from the section
61
+ * rather than staged: the switch writes on the click that flips it, because
62
+ * a withdrawn route is what the user is trying to observe.
63
+ */
64
+ enabled: boolean;
65
+ /** Credential reference naming the environment key. */
66
+ apiKeyEnv: FieldState;
67
+ /** The gateway endpoint. */
68
+ baseURL: FieldState;
69
+ /** Live catalog re-resolution interval, in minutes. */
70
+ refreshMinutes: FieldState;
71
+ /** Largest idle gap between stream events, in milliseconds. */
72
+ streamIdleTimeoutMs: FieldState;
73
+ /** Accumulated base64 image payload bound for one request. */
74
+ maxRequestImageBytes: FieldState;
75
+ /** Total-pixel budget for one request image. */
76
+ requestImagePixelBudget: FieldState;
77
+ /** Raw encoded-byte target for one request image. */
78
+ requestImageMaxBytes: FieldState;
79
+ /** The staged credential, which starts blank on every load. */
80
+ apiKey: FieldState;
81
+ /** Whether the Host reports a credential configured for the referenced key. */
82
+ apiKeyConfigured: boolean;
83
+ /** Whether the credentials domain accepts a write for it; false disables the control. */
84
+ apiKeyWritable: boolean;
85
+ /** The gateway's current model listing. */
86
+ models: OpencodeGoModels;
87
+ }
88
+ /** The registration-side face the page's slot entry injects. */
89
+ export interface OpencodeGoSectionFace extends FormActions {
90
+ hooks: {
91
+ /** Page snapshot bound by the UI renderer as useOpencodeGo. */
92
+ opencodeGo: SnapshotStore<OpencodeGoSectionState>;
93
+ };
94
+ /** Read the gateway's model listing, now or again after a failure. */
95
+ loadModels: () => void;
96
+ /**
97
+ * Turn the adapter's route on or off, writing immediately.
98
+ * @param next - the state the switch asks for.
99
+ */
100
+ setEnabled: (next: boolean) => void;
101
+ }
102
+ /** Bridges the `llm-opencode-go` scope and the credentials domain onto the page. */
103
+ export declare class OpencodeGoSectionController {
104
+ private readonly scope;
105
+ private readonly ctx;
106
+ private readonly form;
107
+ private readonly store;
108
+ private credential;
109
+ private models;
110
+ private modelsRequest;
111
+ private face;
112
+ /**
113
+ * @param scope - the bound settings scope for the `llm-opencode-go` namespace.
114
+ * @param ctx - the page plugin's context, whose `remote.credentials` namespace
115
+ * answers for the credential the section references.
116
+ */
117
+ constructor(scope: SettingsScope<OpencodeGoSettings>, ctx: ClientContext);
118
+ private projection;
119
+ /**
120
+ * The adapter's effective switch state: the resolved section's value, over
121
+ * the Host's own default when the section carries none.
122
+ * @returns whether the route is currently served.
123
+ */
124
+ private enabled;
125
+ /**
126
+ * Flip the switch by writing the field on the click itself.
127
+ *
128
+ * This is the one control on the page that does not wait for Save: the point
129
+ * of turning it off is to watch the models leave the pickers, and the point
130
+ * of turning it back on is to use the route again — staging either behind a
131
+ * second gesture would report a state the Host does not hold. The write is
132
+ * revision-fenced by the scope like every other, and a refusal surfaces as a
133
+ * failed save through the shared shell rather than a silent revert.
134
+ * @param next - the state the switch asks for.
135
+ */
136
+ setEnabled(next: boolean): void;
137
+ /**
138
+ * Read the gateway's model listing through the Host's discovery for this
139
+ * adapter. Called when the page mounts and again from its refresh control.
140
+ * A rejection settles as a failure too: the refresh control is disabled
141
+ * while loading and the next read starts only from `idle`, so leaving the
142
+ * state loading would strand the page with no way to ask the gateway again.
143
+ */
144
+ loadModels(): void;
145
+ /**
146
+ * Ask the credentials domain about the reference the section currently names.
147
+ *
148
+ * The answer is stored with the reference it describes: `apiKeyEnv` can
149
+ * change between the request and its response, and two reads can settle out
150
+ * of order, so a response is published only while it still answers for the
151
+ * reference in force.
152
+ */
153
+ private readCredential;
154
+ /**
155
+ * Re-read after the Host reports a change to the reference this page watches.
156
+ *
157
+ * A key can be written from somewhere else — the Models page addresses the
158
+ * same reference — and the settings section does not change when it is, so
159
+ * without this the badge keeps reporting a state the Host already replaced.
160
+ * @param ref - the reference the Host reports as changed.
161
+ */
162
+ refreshCredential(ref: string): void;
163
+ /**
164
+ * Build the face the page's slot registration injects. Built once: the store
165
+ * is what changes, and the renderer binds the same callbacks across renders.
166
+ * @returns the page snapshot and its form actions.
167
+ */
168
+ inject(): OpencodeGoSectionFace;
169
+ /**
170
+ * Write the staged key, then re-read whether the Host now holds one.
171
+ * @param value - the staged credential literal.
172
+ * @returns whether the Host reports a configured credential afterwards.
173
+ */
174
+ private writeKey;
175
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The staged form behind the OpenCode Go settings page.
3
+ *
4
+ * The page stages what the user types and writes it only when they save. Each
5
+ * settings write is a durable, revision-fenced document mutation, so a control
6
+ * that committed as it settled turned one edit into a write the user never
7
+ * asked for and could not preview; staged text makes what is on screen exactly
8
+ * what a save would store.
9
+ *
10
+ * A field shows its effective value — the user layer over the composition
11
+ * layer over the schema default — and whether the user layer carries it. That
12
+ * presence, not a value comparison, is what marks a field overridden: an
13
+ * override equal to the composition default is still an override.
14
+ *
15
+ * Copied from the plugin-configuration card form (`ui-settings-plugins`): the
16
+ * client bundle purity gate forbids importing it across packages, and the two
17
+ * forms evolve independently anyway.
18
+ */
19
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
20
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
21
+ /** The write one field's staged text performs when the page is saved. */
22
+ export type FieldWrite = {
23
+ kind: 'set';
24
+ value: unknown;
25
+ } | {
26
+ kind: 'clear';
27
+ };
28
+ /** How one section field converts between its stored value and its draft text. */
29
+ export interface FieldSpec {
30
+ /** Field name inside the namespace section. */
31
+ field: string;
32
+ /** Render a stored value as draft text; the empty string when the section carries none. */
33
+ format: (value: unknown) => string;
34
+ /**
35
+ * The write this draft text stages, or undefined when the text is not a
36
+ * value this field accepts — which blocks the save rather than discarding it.
37
+ */
38
+ parse: (text: string) => FieldWrite | undefined;
39
+ }
40
+ /**
41
+ * A control whose value is written outside the settings section. A credential
42
+ * literal never rides a response, so its draft has nothing to seed from: it is
43
+ * blank until typed, and a blank draft writes nothing.
44
+ */
45
+ export interface SecretSpec {
46
+ /** Field name addressing this control inside the page's form. */
47
+ field: string;
48
+ /** Write the staged text; resolves to whether the Host accepted it. */
49
+ write: (text: string) => Promise<boolean>;
50
+ }
51
+ /** One field as the page's control renders it. */
52
+ export interface FieldState {
53
+ /** Draft text the control renders. */
54
+ text: string;
55
+ /**
56
+ * Whether saving would leave a user-layer entry for this field. A staged
57
+ * edit answers for itself, so the badge previews the save rather than
58
+ * reporting a state the pending edit already contradicts.
59
+ */
60
+ overridden: boolean;
61
+ /** Whether the draft is not a value this field accepts, which blocks saving. */
62
+ invalid: boolean;
63
+ }
64
+ /** Form state the page shell shares across its controls. */
65
+ export interface FormShell {
66
+ /** False while the namespace is not served to this client; the page renders nothing. */
67
+ available: boolean;
68
+ /** Whether the Host document accepts writes. */
69
+ writable: boolean;
70
+ /** Whether the form holds edits that a save would write. */
71
+ dirty: boolean;
72
+ /** Whether any staged draft is invalid, which blocks the save. */
73
+ invalid: boolean;
74
+ /** Whether a save is crossing the wire. */
75
+ saving: boolean;
76
+ /** Whether the last save did not land as staged; cleared by the next edit or save. */
77
+ failed: boolean;
78
+ }
79
+ /** The write actions the page's slot entry injects. */
80
+ export interface FormActions {
81
+ /** Stage draft text for one field. */
82
+ edit: (field: string, text: string) => void;
83
+ /** Stage a clear, so saving lets the field re-inherit the composition layer. */
84
+ resetField: (field: string) => void;
85
+ /** Write every staged edit, then re-seed from what the Host accepted. */
86
+ save: () => void;
87
+ /** Drop every staged edit. */
88
+ discard: () => void;
89
+ }
90
+ /**
91
+ * A whole-number field. An empty draft clears the field; any other draft that
92
+ * is not a finite number blocks the save.
93
+ * @param field - field name inside the namespace section.
94
+ * @returns the field's conversion spec.
95
+ */
96
+ export declare function numberField(field: string): FieldSpec;
97
+ /**
98
+ * A two-state field. Unlike the text fields, the draft is the boolean itself
99
+ * rather than text: the control that renders it reports the state the user
100
+ * asked for, and an unset section resolves to the composition default the
101
+ * schema supplied. Its draft text is the state's string form so the shared
102
+ * override and invalid tracking needs no second branch.
103
+ * @param field - field name inside the namespace section.
104
+ * @returns the field's conversion spec.
105
+ */
106
+ export declare function booleanField(field: string): FieldSpec;
107
+ /**
108
+ * A free-text field. An empty draft clears the field, so emptying the control
109
+ * and saving is the same gesture as resetting it.
110
+ * @param field - field name inside the namespace section.
111
+ * @returns the field's conversion spec.
112
+ */
113
+ export declare function textField(field: string): FieldSpec;
114
+ /**
115
+ * Stages the page's edits over the `llm-opencode-go` namespace and writes them
116
+ * on save.
117
+ *
118
+ * The form publishes through a snapshot store because the slot component reads
119
+ * through a snapshot selector, while both the scope and the local drafts
120
+ * change underneath; every projection is rebuilt from the two together.
121
+ */
122
+ export declare class StagedForm {
123
+ private readonly scope;
124
+ private readonly specs;
125
+ private readonly secretSpecs;
126
+ private readonly staged;
127
+ private readonly listeners;
128
+ private saving;
129
+ private failed;
130
+ /**
131
+ * @param scope - the bound settings scope for this page's namespace.
132
+ * @param specs - the section fields this page edits.
133
+ * @param secrets - the page's write-only controls, written outside the section.
134
+ */
135
+ constructor(scope: SettingsScope<Record<string, unknown>>, specs: FieldSpec[], secrets?: SecretSpec[]);
136
+ /**
137
+ * Publish a projection of this form, rebuilt whenever the scope or a draft changes.
138
+ * @param project - build the page state from the form's current reads.
139
+ * @returns the store the component reads through its bound selector.
140
+ */
141
+ bind<S>(project: () => S): SnapshotStore<S>;
142
+ /**
143
+ * Read the page-level state: what the Host serves, and what a save would do.
144
+ * @returns the form state the page shell shares.
145
+ */
146
+ shell(): FormShell;
147
+ /**
148
+ * Read one control's state.
149
+ * @param field - field name of a section field or of a write-only control.
150
+ * @returns the draft text, whether a save would leave an override, and whether it is invalid.
151
+ */
152
+ field(field: string): FieldState;
153
+ /**
154
+ * Build the edit, reset, save, and discard actions bound to this form.
155
+ * @returns the actions the page's slot entry injects.
156
+ */
157
+ actions(): FormActions;
158
+ /**
159
+ * Write every staged edit, then re-seed from what the Host accepted.
160
+ *
161
+ * The Host is the only authority on whether a value was accepted — its
162
+ * validators own the constraints no schema can express — so the outcome is
163
+ * read back from the section rather than predicted here. A save that did not
164
+ * land keeps its drafts, so the user can correct them instead of retyping.
165
+ */
166
+ save(): Promise<void>;
167
+ /**
168
+ * Every staged edit a save would write. An entry whose draft is not a value
169
+ * its field accepts carries no write: the form is still dirty, and the save
170
+ * refuses rather than dropping the edit.
171
+ * @returns the planned writes, in the order the fields were staged.
172
+ */
173
+ private plan;
174
+ private clear;
175
+ private store;
176
+ private stage;
177
+ private spec;
178
+ private snapshotOf;
179
+ private sectionValue;
180
+ private baseValue;
181
+ private userLayer;
182
+ private stored;
183
+ private publish;
184
+ }