@huanlin/dsh-plugin-interpreters 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +975 -964
- package/lib/settings.js +82 -83
- package/lib/types/client/bindSnapshotSelector.js +23 -23
- package/lib/types/client/index.d.ts +39 -39
- package/lib/types/client/index.js +94 -94
- package/lib/types/client/store.d.ts +88 -88
- package/lib/types/client/store.js +196 -196
- package/lib/types/gateway.d.ts +63 -63
- package/lib/types/settings.d.ts +43 -43
- package/lib/types/settings.js +82 -83
- package/package.json +25 -25
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* store.ts — the interpreters card's staged form over the
|
|
3
|
-
* `/interpreters/api/get|set` HTTP route.
|
|
4
|
-
*
|
|
5
|
-
* The DSH settings RPC domain only serves allowlisted namespaces to
|
|
6
|
-
* configuration clients, so this store reads and writes the `interpreters`
|
|
7
|
-
* namespace through the plugin's self-hosted HTTP route
|
|
8
|
-
* (`fetch('/interpreters/api/get'|'set')`) instead of the host's typertRemote
|
|
9
|
-
* dispatch. State publishes through a `SnapshotStore` so the card binds a
|
|
10
|
-
* selector hook via `bindSnapshotSelector`; the store tracks load status,
|
|
11
|
-
* the staged draft, and the apply lifecycle (idle/saving/saved/error).
|
|
12
|
-
*
|
|
13
|
-
* @module dsh-interpreters/client/store
|
|
14
|
-
*/
|
|
15
|
-
import { type SnapshotStore } from '@deepseek-ai/dsh-client-
|
|
16
|
-
/** The persisted shape of the `interpreters` namespace. */
|
|
17
|
-
export interface InterpretersSettings {
|
|
18
|
-
pythonPath?: string;
|
|
19
|
-
nodePath?: string;
|
|
20
|
-
timeoutMs?: number;
|
|
21
|
-
}
|
|
22
|
-
/** Apply lifecycle states (mirrors advisor-store's ApplyState shape). */
|
|
23
|
-
export type ApplyState = {
|
|
24
|
-
kind: 'idle';
|
|
25
|
-
} | {
|
|
26
|
-
kind: 'saving';
|
|
27
|
-
} | {
|
|
28
|
-
kind: 'saved';
|
|
29
|
-
} | {
|
|
30
|
-
kind: 'error';
|
|
31
|
-
message: string;
|
|
32
|
-
};
|
|
33
|
-
/** Card state published through the snapshot store. */
|
|
34
|
-
export interface InterpretersCardState {
|
|
35
|
-
/** 'idle' before the first load fires; 'loading' while in flight; 'ready' once seeded. */
|
|
36
|
-
status: 'idle' | 'loading' | 'ready';
|
|
37
|
-
/** False until the first successful load gates `connection/reset` refreshes. */
|
|
38
|
-
loaded: boolean;
|
|
39
|
-
/** False while the namespace is not served to this client; the card renders the unavailable notice. */
|
|
40
|
-
available: boolean;
|
|
41
|
-
/** Whether the Host document accepts writes. */
|
|
42
|
-
writable: boolean;
|
|
43
|
-
/** Staged draft (last-known host config + local edits). */
|
|
44
|
-
draft: InterpretersSettings;
|
|
45
|
-
/** Whether the form holds edits that a save would write. */
|
|
46
|
-
dirty: boolean;
|
|
47
|
-
/** Apply lifecycle. */
|
|
48
|
-
applyState: ApplyState;
|
|
49
|
-
}
|
|
50
|
-
/** A number field renders empty when the section carries none. */
|
|
51
|
-
declare function formatNumber(value: unknown): string;
|
|
52
|
-
/** A text field renders the empty string when absent. */
|
|
53
|
-
declare function formatText(value: unknown): string;
|
|
54
|
-
/**
|
|
55
|
-
* The card's staged form over the interpreters settings.
|
|
56
|
-
*
|
|
57
|
-
* The store publishes through a `SnapshotStore` because slot components read
|
|
58
|
-
* through a snapshot selector; both the HTTP read and the local drafts
|
|
59
|
-
* change underneath, and every projection is rebuilt from the two together.
|
|
60
|
-
*/
|
|
61
|
-
export declare class InterpretersCardController {
|
|
62
|
-
readonly store: SnapshotStore<InterpretersCardState>;
|
|
63
|
-
/** True after the first successful load; gates `connection/reset` refreshes. */
|
|
64
|
-
loaded: boolean;
|
|
65
|
-
private generation;
|
|
66
|
-
private staged;
|
|
67
|
-
constructor();
|
|
68
|
-
/**
|
|
69
|
-
* Read the resolved config from the Host HTTP route and publish it.
|
|
70
|
-
* @returns settlement after the read.
|
|
71
|
-
*/
|
|
72
|
-
load(): Promise<void>;
|
|
73
|
-
/** Stage draft text for one field. */
|
|
74
|
-
edit(field: keyof InterpretersSettings, text: string): void;
|
|
75
|
-
/** Drop every staged edit. */
|
|
76
|
-
discard(): void;
|
|
77
|
-
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
78
|
-
save(): void;
|
|
79
|
-
private doSave;
|
|
80
|
-
/** The staged edits as one patch (only changed fields). */
|
|
81
|
-
private patchOf;
|
|
82
|
-
}
|
|
83
|
-
/** Refresh the store only after its first load (background invalidation gate). */
|
|
84
|
-
export declare function refreshIfLoaded(controller: InterpretersCardController): void;
|
|
85
|
-
/** Format helpers exposed for the card component. */
|
|
86
|
-
export declare const formatFieldText: typeof formatText;
|
|
87
|
-
export declare const formatFieldNumber: typeof formatNumber;
|
|
88
|
-
export {};
|
|
1
|
+
/**
|
|
2
|
+
* store.ts — the interpreters card's staged form over the
|
|
3
|
+
* `/interpreters/api/get|set` HTTP route.
|
|
4
|
+
*
|
|
5
|
+
* The DSH settings RPC domain only serves allowlisted namespaces to
|
|
6
|
+
* configuration clients, so this store reads and writes the `interpreters`
|
|
7
|
+
* namespace through the plugin's self-hosted HTTP route
|
|
8
|
+
* (`fetch('/interpreters/api/get'|'set')`) instead of the host's typertRemote
|
|
9
|
+
* dispatch. State publishes through a `SnapshotStore` so the card binds a
|
|
10
|
+
* selector hook via `bindSnapshotSelector`; the store tracks load status,
|
|
11
|
+
* the staged draft, and the apply lifecycle (idle/saving/saved/error).
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-interpreters/client/store
|
|
14
|
+
*/
|
|
15
|
+
import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
16
|
+
/** The persisted shape of the `interpreters` namespace. */
|
|
17
|
+
export interface InterpretersSettings {
|
|
18
|
+
pythonPath?: string;
|
|
19
|
+
nodePath?: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Apply lifecycle states (mirrors advisor-store's ApplyState shape). */
|
|
23
|
+
export type ApplyState = {
|
|
24
|
+
kind: 'idle';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'saving';
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'saved';
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'error';
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
/** Card state published through the snapshot store. */
|
|
34
|
+
export interface InterpretersCardState {
|
|
35
|
+
/** 'idle' before the first load fires; 'loading' while in flight; 'ready' once seeded. */
|
|
36
|
+
status: 'idle' | 'loading' | 'ready';
|
|
37
|
+
/** False until the first successful load gates `connection/reset` refreshes. */
|
|
38
|
+
loaded: boolean;
|
|
39
|
+
/** False while the namespace is not served to this client; the card renders the unavailable notice. */
|
|
40
|
+
available: boolean;
|
|
41
|
+
/** Whether the Host document accepts writes. */
|
|
42
|
+
writable: boolean;
|
|
43
|
+
/** Staged draft (last-known host config + local edits). */
|
|
44
|
+
draft: InterpretersSettings;
|
|
45
|
+
/** Whether the form holds edits that a save would write. */
|
|
46
|
+
dirty: boolean;
|
|
47
|
+
/** Apply lifecycle. */
|
|
48
|
+
applyState: ApplyState;
|
|
49
|
+
}
|
|
50
|
+
/** A number field renders empty when the section carries none. */
|
|
51
|
+
declare function formatNumber(value: unknown): string;
|
|
52
|
+
/** A text field renders the empty string when absent. */
|
|
53
|
+
declare function formatText(value: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* The card's staged form over the interpreters settings.
|
|
56
|
+
*
|
|
57
|
+
* The store publishes through a `SnapshotStore` because slot components read
|
|
58
|
+
* through a snapshot selector; both the HTTP read and the local drafts
|
|
59
|
+
* change underneath, and every projection is rebuilt from the two together.
|
|
60
|
+
*/
|
|
61
|
+
export declare class InterpretersCardController {
|
|
62
|
+
readonly store: SnapshotStore<InterpretersCardState>;
|
|
63
|
+
/** True after the first successful load; gates `connection/reset` refreshes. */
|
|
64
|
+
loaded: boolean;
|
|
65
|
+
private generation;
|
|
66
|
+
private staged;
|
|
67
|
+
constructor();
|
|
68
|
+
/**
|
|
69
|
+
* Read the resolved config from the Host HTTP route and publish it.
|
|
70
|
+
* @returns settlement after the read.
|
|
71
|
+
*/
|
|
72
|
+
load(): Promise<void>;
|
|
73
|
+
/** Stage draft text for one field. */
|
|
74
|
+
edit(field: keyof InterpretersSettings, text: string): void;
|
|
75
|
+
/** Drop every staged edit. */
|
|
76
|
+
discard(): void;
|
|
77
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
78
|
+
save(): void;
|
|
79
|
+
private doSave;
|
|
80
|
+
/** The staged edits as one patch (only changed fields). */
|
|
81
|
+
private patchOf;
|
|
82
|
+
}
|
|
83
|
+
/** Refresh the store only after its first load (background invalidation gate). */
|
|
84
|
+
export declare function refreshIfLoaded(controller: InterpretersCardController): void;
|
|
85
|
+
/** Format helpers exposed for the card component. */
|
|
86
|
+
export declare const formatFieldText: typeof formatText;
|
|
87
|
+
export declare const formatFieldNumber: typeof formatNumber;
|
|
88
|
+
export {};
|
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* store.ts — the interpreters card's staged form over the
|
|
3
|
-
* `/interpreters/api/get|set` HTTP route.
|
|
4
|
-
*
|
|
5
|
-
* The DSH settings RPC domain only serves allowlisted namespaces to
|
|
6
|
-
* configuration clients, so this store reads and writes the `interpreters`
|
|
7
|
-
* namespace through the plugin's self-hosted HTTP route
|
|
8
|
-
* (`fetch('/interpreters/api/get'|'set')`) instead of the host's typertRemote
|
|
9
|
-
* dispatch. State publishes through a `SnapshotStore` so the card binds a
|
|
10
|
-
* selector hook via `bindSnapshotSelector`; the store tracks load status,
|
|
11
|
-
* the staged draft, and the apply lifecycle (idle/saving/saved/error).
|
|
12
|
-
*
|
|
13
|
-
* @module dsh-interpreters/client/store
|
|
14
|
-
*/
|
|
15
|
-
import { createSnapshotStore } from '@deepseek-ai/dsh-client-
|
|
16
|
-
/** Initial empty state. */
|
|
17
|
-
function initialState() {
|
|
18
|
-
return {
|
|
19
|
-
status: 'idle',
|
|
20
|
-
loaded: false,
|
|
21
|
-
available: false,
|
|
22
|
-
writable: false,
|
|
23
|
-
draft: {},
|
|
24
|
-
dirty: false,
|
|
25
|
-
applyState: { kind: 'idle' },
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/** A number field renders empty when the section carries none. */
|
|
29
|
-
function formatNumber(value) {
|
|
30
|
-
return typeof value === 'number' ? String(value) : '';
|
|
31
|
-
}
|
|
32
|
-
/** A text field renders the empty string when absent. */
|
|
33
|
-
function formatText(value) {
|
|
34
|
-
return typeof value === 'string' ? value : '';
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* The card's staged form over the interpreters settings.
|
|
38
|
-
*
|
|
39
|
-
* The store publishes through a `SnapshotStore` because slot components read
|
|
40
|
-
* through a snapshot selector; both the HTTP read and the local drafts
|
|
41
|
-
* change underneath, and every projection is rebuilt from the two together.
|
|
42
|
-
*/
|
|
43
|
-
export class InterpretersCardController {
|
|
44
|
-
store;
|
|
45
|
-
/** True after the first successful load; gates `connection/reset` refreshes. */
|
|
46
|
-
loaded = false;
|
|
47
|
-
generation = 0;
|
|
48
|
-
staged = new Map();
|
|
49
|
-
constructor() {
|
|
50
|
-
this.store = createSnapshotStore(initialState());
|
|
51
|
-
void this.load();
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Read the resolved config from the Host HTTP route and publish it.
|
|
55
|
-
* @returns settlement after the read.
|
|
56
|
-
*/
|
|
57
|
-
async load() {
|
|
58
|
-
const gen = ++this.generation;
|
|
59
|
-
this.store.update((s) => { s.status = 'loading'; });
|
|
60
|
-
let config;
|
|
61
|
-
try {
|
|
62
|
-
const response = await fetch('/interpreters/api/get', {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: { 'content-type': 'application/json' },
|
|
65
|
-
body: '{}',
|
|
66
|
-
});
|
|
67
|
-
if (response.ok) {
|
|
68
|
-
const parsed = await response.json().catch(() => null);
|
|
69
|
-
if (parsed?.ok === true && parsed.value !== undefined) {
|
|
70
|
-
config = parsed.value.config;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// Channel unreachable: leave the card unavailable; not a hard error.
|
|
76
|
-
}
|
|
77
|
-
if (gen !== this.generation)
|
|
78
|
-
return;
|
|
79
|
-
if (config === undefined) {
|
|
80
|
-
this.store.update((s) => {
|
|
81
|
-
s.status = 'ready';
|
|
82
|
-
s.available = false;
|
|
83
|
-
s.writable = false;
|
|
84
|
-
});
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
this.loaded = true;
|
|
88
|
-
this.staged.clear();
|
|
89
|
-
this.store.update((s) => {
|
|
90
|
-
s.status = 'ready';
|
|
91
|
-
s.available = true;
|
|
92
|
-
s.writable = true;
|
|
93
|
-
s.draft = { ...config };
|
|
94
|
-
s.dirty = false;
|
|
95
|
-
s.applyState = { kind: 'idle' };
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
/** Stage draft text for one field. */
|
|
99
|
-
edit(field, text) {
|
|
100
|
-
this.staged.set(field, text);
|
|
101
|
-
this.store.update((s) => {
|
|
102
|
-
s.draft = { ...s.draft, [field]: text };
|
|
103
|
-
s.dirty = true;
|
|
104
|
-
s.applyState = { kind: 'idle' };
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
/** Drop every staged edit. */
|
|
108
|
-
discard() {
|
|
109
|
-
if (this.staged.size === 0) {
|
|
110
|
-
this.store.update((s) => { s.applyState = { kind: 'idle' }; });
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
this.staged.clear();
|
|
114
|
-
// Re-seed draft from the last-known host config (drop local edits).
|
|
115
|
-
void this.load();
|
|
116
|
-
}
|
|
117
|
-
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
118
|
-
save() {
|
|
119
|
-
void this.doSave();
|
|
120
|
-
}
|
|
121
|
-
async doSave() {
|
|
122
|
-
const gen = ++this.generation;
|
|
123
|
-
const patch = this.patchOf();
|
|
124
|
-
if (Object.keys(patch).length === 0) {
|
|
125
|
-
this.staged.clear();
|
|
126
|
-
this.store.update((s) => { s.dirty = false; s.applyState = { kind: 'idle' }; });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
this.store.update((s) => { s.applyState = { kind: 'saving' }; });
|
|
130
|
-
try {
|
|
131
|
-
const response = await fetch('/interpreters/api/set', {
|
|
132
|
-
method: 'POST',
|
|
133
|
-
headers: { 'content-type': 'application/json' },
|
|
134
|
-
body: JSON.stringify({ patch }),
|
|
135
|
-
});
|
|
136
|
-
if (gen !== this.generation)
|
|
137
|
-
return;
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
const parsed = await response.json().catch(() => null);
|
|
140
|
-
const message = parsed?.error?.message ?? `HTTP ${response.status}`;
|
|
141
|
-
this.store.update((s) => { s.applyState = { kind: 'error', message }; });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const parsed = await response.json().catch(() => null);
|
|
145
|
-
if (parsed?.ok !== true || parsed.value === undefined) {
|
|
146
|
-
const message = parsed?.error?.message ?? 'unknown error';
|
|
147
|
-
this.store.update((s) => { s.applyState = { kind: 'error', message }; });
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const next = parsed.value.config;
|
|
151
|
-
this.staged.clear();
|
|
152
|
-
this.store.update((s) => {
|
|
153
|
-
s.draft = { ...next };
|
|
154
|
-
s.dirty = false;
|
|
155
|
-
s.applyState = { kind: 'saved' };
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
if (gen !== this.generation)
|
|
160
|
-
return;
|
|
161
|
-
this.store.update((s) => {
|
|
162
|
-
s.applyState = { kind: 'error', message: error instanceof Error ? error.message : String(error) };
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
/** The staged edits as one patch (only changed fields). */
|
|
167
|
-
patchOf() {
|
|
168
|
-
const patch = {};
|
|
169
|
-
for (const [field, text] of this.staged) {
|
|
170
|
-
const value = parseField(field, text);
|
|
171
|
-
if (value === undefined)
|
|
172
|
-
continue;
|
|
173
|
-
patch[field] = value;
|
|
174
|
-
}
|
|
175
|
-
return patch;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/** Parse one field's draft text into a stored value; the empty string clears. */
|
|
179
|
-
function parseField(field, text) {
|
|
180
|
-
const trimmed = text.trim();
|
|
181
|
-
if (trimmed === '')
|
|
182
|
-
return '';
|
|
183
|
-
if (field === 'timeoutMs') {
|
|
184
|
-
const parsed = Number(trimmed);
|
|
185
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
186
|
-
}
|
|
187
|
-
return trimmed;
|
|
188
|
-
}
|
|
189
|
-
/** Refresh the store only after its first load (background invalidation gate). */
|
|
190
|
-
export function refreshIfLoaded(controller) {
|
|
191
|
-
if (controller.loaded)
|
|
192
|
-
void controller.load();
|
|
193
|
-
}
|
|
194
|
-
/** Format helpers exposed for the card component. */
|
|
195
|
-
export const formatFieldText = formatText;
|
|
196
|
-
export const formatFieldNumber = formatNumber;
|
|
1
|
+
/**
|
|
2
|
+
* store.ts — the interpreters card's staged form over the
|
|
3
|
+
* `/interpreters/api/get|set` HTTP route.
|
|
4
|
+
*
|
|
5
|
+
* The DSH settings RPC domain only serves allowlisted namespaces to
|
|
6
|
+
* configuration clients, so this store reads and writes the `interpreters`
|
|
7
|
+
* namespace through the plugin's self-hosted HTTP route
|
|
8
|
+
* (`fetch('/interpreters/api/get'|'set')`) instead of the host's typertRemote
|
|
9
|
+
* dispatch. State publishes through a `SnapshotStore` so the card binds a
|
|
10
|
+
* selector hook via `bindSnapshotSelector`; the store tracks load status,
|
|
11
|
+
* the staged draft, and the apply lifecycle (idle/saving/saved/error).
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-interpreters/client/store
|
|
14
|
+
*/
|
|
15
|
+
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
16
|
+
/** Initial empty state. */
|
|
17
|
+
function initialState() {
|
|
18
|
+
return {
|
|
19
|
+
status: 'idle',
|
|
20
|
+
loaded: false,
|
|
21
|
+
available: false,
|
|
22
|
+
writable: false,
|
|
23
|
+
draft: {},
|
|
24
|
+
dirty: false,
|
|
25
|
+
applyState: { kind: 'idle' },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** A number field renders empty when the section carries none. */
|
|
29
|
+
function formatNumber(value) {
|
|
30
|
+
return typeof value === 'number' ? String(value) : '';
|
|
31
|
+
}
|
|
32
|
+
/** A text field renders the empty string when absent. */
|
|
33
|
+
function formatText(value) {
|
|
34
|
+
return typeof value === 'string' ? value : '';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The card's staged form over the interpreters settings.
|
|
38
|
+
*
|
|
39
|
+
* The store publishes through a `SnapshotStore` because slot components read
|
|
40
|
+
* through a snapshot selector; both the HTTP read and the local drafts
|
|
41
|
+
* change underneath, and every projection is rebuilt from the two together.
|
|
42
|
+
*/
|
|
43
|
+
export class InterpretersCardController {
|
|
44
|
+
store;
|
|
45
|
+
/** True after the first successful load; gates `connection/reset` refreshes. */
|
|
46
|
+
loaded = false;
|
|
47
|
+
generation = 0;
|
|
48
|
+
staged = new Map();
|
|
49
|
+
constructor() {
|
|
50
|
+
this.store = createSnapshotStore(initialState());
|
|
51
|
+
void this.load();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the resolved config from the Host HTTP route and publish it.
|
|
55
|
+
* @returns settlement after the read.
|
|
56
|
+
*/
|
|
57
|
+
async load() {
|
|
58
|
+
const gen = ++this.generation;
|
|
59
|
+
this.store.update((s) => { s.status = 'loading'; });
|
|
60
|
+
let config;
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch('/interpreters/api/get', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'content-type': 'application/json' },
|
|
65
|
+
body: '{}',
|
|
66
|
+
});
|
|
67
|
+
if (response.ok) {
|
|
68
|
+
const parsed = await response.json().catch(() => null);
|
|
69
|
+
if (parsed?.ok === true && parsed.value !== undefined) {
|
|
70
|
+
config = parsed.value.config;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Channel unreachable: leave the card unavailable; not a hard error.
|
|
76
|
+
}
|
|
77
|
+
if (gen !== this.generation)
|
|
78
|
+
return;
|
|
79
|
+
if (config === undefined) {
|
|
80
|
+
this.store.update((s) => {
|
|
81
|
+
s.status = 'ready';
|
|
82
|
+
s.available = false;
|
|
83
|
+
s.writable = false;
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.loaded = true;
|
|
88
|
+
this.staged.clear();
|
|
89
|
+
this.store.update((s) => {
|
|
90
|
+
s.status = 'ready';
|
|
91
|
+
s.available = true;
|
|
92
|
+
s.writable = true;
|
|
93
|
+
s.draft = { ...config };
|
|
94
|
+
s.dirty = false;
|
|
95
|
+
s.applyState = { kind: 'idle' };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Stage draft text for one field. */
|
|
99
|
+
edit(field, text) {
|
|
100
|
+
this.staged.set(field, text);
|
|
101
|
+
this.store.update((s) => {
|
|
102
|
+
s.draft = { ...s.draft, [field]: text };
|
|
103
|
+
s.dirty = true;
|
|
104
|
+
s.applyState = { kind: 'idle' };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Drop every staged edit. */
|
|
108
|
+
discard() {
|
|
109
|
+
if (this.staged.size === 0) {
|
|
110
|
+
this.store.update((s) => { s.applyState = { kind: 'idle' }; });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.staged.clear();
|
|
114
|
+
// Re-seed draft from the last-known host config (drop local edits).
|
|
115
|
+
void this.load();
|
|
116
|
+
}
|
|
117
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
118
|
+
save() {
|
|
119
|
+
void this.doSave();
|
|
120
|
+
}
|
|
121
|
+
async doSave() {
|
|
122
|
+
const gen = ++this.generation;
|
|
123
|
+
const patch = this.patchOf();
|
|
124
|
+
if (Object.keys(patch).length === 0) {
|
|
125
|
+
this.staged.clear();
|
|
126
|
+
this.store.update((s) => { s.dirty = false; s.applyState = { kind: 'idle' }; });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.store.update((s) => { s.applyState = { kind: 'saving' }; });
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch('/interpreters/api/set', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'content-type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ patch }),
|
|
135
|
+
});
|
|
136
|
+
if (gen !== this.generation)
|
|
137
|
+
return;
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const parsed = await response.json().catch(() => null);
|
|
140
|
+
const message = parsed?.error?.message ?? `HTTP ${response.status}`;
|
|
141
|
+
this.store.update((s) => { s.applyState = { kind: 'error', message }; });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const parsed = await response.json().catch(() => null);
|
|
145
|
+
if (parsed?.ok !== true || parsed.value === undefined) {
|
|
146
|
+
const message = parsed?.error?.message ?? 'unknown error';
|
|
147
|
+
this.store.update((s) => { s.applyState = { kind: 'error', message }; });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const next = parsed.value.config;
|
|
151
|
+
this.staged.clear();
|
|
152
|
+
this.store.update((s) => {
|
|
153
|
+
s.draft = { ...next };
|
|
154
|
+
s.dirty = false;
|
|
155
|
+
s.applyState = { kind: 'saved' };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (gen !== this.generation)
|
|
160
|
+
return;
|
|
161
|
+
this.store.update((s) => {
|
|
162
|
+
s.applyState = { kind: 'error', message: error instanceof Error ? error.message : String(error) };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** The staged edits as one patch (only changed fields). */
|
|
167
|
+
patchOf() {
|
|
168
|
+
const patch = {};
|
|
169
|
+
for (const [field, text] of this.staged) {
|
|
170
|
+
const value = parseField(field, text);
|
|
171
|
+
if (value === undefined)
|
|
172
|
+
continue;
|
|
173
|
+
patch[field] = value;
|
|
174
|
+
}
|
|
175
|
+
return patch;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Parse one field's draft text into a stored value; the empty string clears. */
|
|
179
|
+
function parseField(field, text) {
|
|
180
|
+
const trimmed = text.trim();
|
|
181
|
+
if (trimmed === '')
|
|
182
|
+
return '';
|
|
183
|
+
if (field === 'timeoutMs') {
|
|
184
|
+
const parsed = Number(trimmed);
|
|
185
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
186
|
+
}
|
|
187
|
+
return trimmed;
|
|
188
|
+
}
|
|
189
|
+
/** Refresh the store only after its first load (background invalidation gate). */
|
|
190
|
+
export function refreshIfLoaded(controller) {
|
|
191
|
+
if (controller.loaded)
|
|
192
|
+
void controller.load();
|
|
193
|
+
}
|
|
194
|
+
/** Format helpers exposed for the card component. */
|
|
195
|
+
export const formatFieldText = formatText;
|
|
196
|
+
export const formatFieldNumber = formatNumber;
|