dsh-opencode 0.1.4 → 0.1.6
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/README.md +9 -5
- package/lib/client.d.ts +5 -1734
- package/lib/client.js +428 -245
- package/lib/commands.js +51 -10
- package/lib/credentials.js +4 -3
- package/lib/language.js +8 -0
- package/lib/shared/language.js +78 -0
- package/lib/transport.js +1 -1
- package/package.json +3 -8
package/lib/client.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({id:'dsh-opencode',factory:(require)=>{var module={exports:{}};var exports=module.exports;
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
let react = require("react");
|
|
4
|
-
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
5
4
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
5
|
//#region src/shared/opencode.ts
|
|
7
6
|
/** Browser-safe identifiers shared by the Host metadata and Client UI. */
|
|
@@ -11,16 +10,137 @@ const ROUTES = {
|
|
|
11
10
|
zen: "opencode-zen-live",
|
|
12
11
|
go: "opencode-go-live"
|
|
13
12
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/shared/language.ts
|
|
15
|
+
/** Explicit DSH preference wins; otherwise use the primary browser/OS locale. */
|
|
16
|
+
function resolveLanguage(preference, fallback) {
|
|
17
|
+
const locale = typeof preference === "string" && preference.trim() ? preference : fallback;
|
|
18
|
+
return /^ja(?:[-_.@]|$)/i.test(locale?.trim() ?? "") ? "ja" : "en";
|
|
19
|
+
}
|
|
20
|
+
function localePreference(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && "preference" in value ? value.preference : void 0;
|
|
22
|
+
}
|
|
23
|
+
[
|
|
24
|
+
"1. Open Settings > Models.",
|
|
25
|
+
"2. Enter your API key in the OpenCode provider you want to use.",
|
|
26
|
+
"3. Click \"Save API key\"."
|
|
27
|
+
].join("\n");
|
|
28
|
+
const setupUsage = "Enter your API key in Settings > Models, not in chat.";
|
|
29
|
+
const setupSaved = "Your OpenCode API key is saved. Choose an OpenCode model from the model picker at the bottom right of the chat.";
|
|
30
|
+
const setupCancelled = "The check was cancelled.";
|
|
31
|
+
/** English source copy is the key. Only our known copy is translated. */
|
|
32
|
+
const japanese = {
|
|
33
|
+
"OpenCode settings": "OpenCode の設定",
|
|
34
|
+
"Close": "閉じる",
|
|
35
|
+
"OpenCode API key": "OpenCode APIキー",
|
|
36
|
+
"Key saved — enter a new key to replace it": "保存済みです。変更する場合は新しいキーを入力してください",
|
|
37
|
+
"Paste your OpenCode API key": "OpenCodeのAPIキーを貼り付けてください",
|
|
38
|
+
"This key is shared by OpenCode Zen and Go. Deleting it removes the key for both providers.": "このキーはOpenCode ZenとGoで共有しています。削除すると両方のキー設定が解除されます。",
|
|
39
|
+
"Checking credential status…": "APIキーの設定を確認しています…",
|
|
40
|
+
"Saving…": "保存中…",
|
|
41
|
+
"Save API key": "APIキーを保存",
|
|
42
|
+
"Deleting…": "削除中…",
|
|
43
|
+
"Delete API key": "APIキーを削除",
|
|
44
|
+
"Clear input": "入力を消去",
|
|
45
|
+
"The credential check was cancelled.": "APIキーの確認をキャンセルしました。",
|
|
46
|
+
"The configured credential reference is unavailable.": "APIキーの保存先を確認できませんでした。",
|
|
47
|
+
"Could not check whether an API key is saved.": "APIキーが保存されているか確認できませんでした。",
|
|
48
|
+
"Could not load settings or credential status.": "設定またはAPIキーの状態を読み込めませんでした。",
|
|
49
|
+
"The settings form was closed.": "設定画面が閉じられました。",
|
|
50
|
+
"The settings form was closed. Open it again and retry.": "設定画面が閉じられました。開き直して再試行してください。",
|
|
51
|
+
"The API key settings changed. Check which providers share the key and retry.": "APIキーの設定が変更されました。キーを共有するプロバイダーを確認して再試行してください。",
|
|
52
|
+
"This credential is read-only. Remove it from the environment that launches DSH.": "このAPIキーは読み取り専用です。DSHを起動する環境の設定から削除してください。",
|
|
53
|
+
"This credential is already being updated.": "このAPIキーは更新中です。完了後に再試行してください。",
|
|
54
|
+
"Could not delete the API key. Try again.": "APIキーを削除できませんでした。再試行してください。",
|
|
55
|
+
"The deletion request succeeded, but the key status could not be confirmed. Reload Settings > Models.": "削除リクエストは成功しましたが、キーの状態を確認できませんでした。Settings > Modelsを開き直してください。",
|
|
56
|
+
"The saved key was removed, but an API key is still configured. Check its source in Settings > Models.": "保存したキーは削除されましたが、別のAPIキー設定が残っています。Settings > Modelsで設定元を確認してください。",
|
|
57
|
+
"API key deleted for OpenCode Zen and Go.": "OpenCode ZenとGoの共有APIキーを削除しました。",
|
|
58
|
+
"API key deleted.": "APIキーを削除しました。",
|
|
59
|
+
"Paste the API key only, without quotes or an environment-variable assignment.": "引用符や環境変数の代入式を含めず、APIキーだけを貼り付けてください。",
|
|
60
|
+
"The credential reference changed. Reload its status and retry.": "APIキーの保存先が変更されました。状態を読み込み直して再試行してください。",
|
|
61
|
+
"This credential is read-only. Update it in the environment that launches DSH.": "このAPIキーは読み取り専用です。DSHを起動する環境の設定で変更してください。",
|
|
62
|
+
"Could not save the API key.": "APIキーを保存できませんでした。",
|
|
63
|
+
"The API key is not configured. Enter it in Settings > Models, click \"Save API key\", then send your message again.": "APIキーが未設定です。Settings > ModelsでAPIキーを入力して「APIキーを保存」を押してから、もう一度送信してください。",
|
|
64
|
+
"API key saved.": "APIキーを保存しました。",
|
|
65
|
+
"The key was saved, but its status could not be confirmed.": "APIキーを保存しましたが、保存後の状態を確認できませんでした。",
|
|
66
|
+
"1. Open Settings > Models.": "1. Settings > Modelsを開きます。",
|
|
67
|
+
"2. Enter your API key in the OpenCode provider you want to use.": "2. 使用するOpenCodeの欄にAPIキーを入力します。",
|
|
68
|
+
"3. Click \"Save API key\".": "3. 「APIキーを保存」を押してください。",
|
|
69
|
+
[setupUsage]: "APIキーはチャットに入力せず、Settings > Modelsから設定してください。",
|
|
70
|
+
[setupSaved]: "OpenCodeのAPIキーは保存されています。チャット右下のモデル選択から、使いたいOpenCodeのモデルを選んでください。",
|
|
71
|
+
[setupCancelled]: "確認をキャンセルしました。"
|
|
72
|
+
};
|
|
73
|
+
for (const label of [
|
|
74
|
+
"OpenCode",
|
|
75
|
+
"OpenCode Zen",
|
|
76
|
+
"OpenCode Go",
|
|
77
|
+
"OpenCode Zen and OpenCode Go"
|
|
78
|
+
]) {
|
|
79
|
+
const jaLabel = label.replace(" and ", "・");
|
|
80
|
+
japanese[`${label} API key is not configured.`] = `${jaLabel}のAPIキーが未設定です。`;
|
|
81
|
+
japanese[`Could not check the API key settings for ${label}.`] = `${jaLabel}のAPIキー設定を確認できませんでした。`;
|
|
82
|
+
}
|
|
83
|
+
const english = new Map(Object.entries(japanese).map(([en, ja]) => [ja, en]));
|
|
84
|
+
function translate(text, language) {
|
|
85
|
+
return text.split("\n").map((line) => {
|
|
86
|
+
const source = english.get(line) ?? line;
|
|
87
|
+
return language === "ja" ? japanese[source] ?? line : source;
|
|
88
|
+
}).join("\n");
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/client/language.ts
|
|
92
|
+
/** Read the durable preference without registering languages or changing DSH settings. */
|
|
93
|
+
var LanguageController = class {
|
|
94
|
+
ctx;
|
|
95
|
+
listeners = /* @__PURE__ */ new Set();
|
|
96
|
+
unsubscribe;
|
|
97
|
+
language;
|
|
98
|
+
disposed = false;
|
|
99
|
+
constructor(ctx) {
|
|
100
|
+
this.ctx = ctx;
|
|
101
|
+
this.language = this.read();
|
|
102
|
+
this.unsubscribe = ctx.settingsScope.describe().subscribe(this.refresh);
|
|
103
|
+
if (typeof window !== "undefined") window.addEventListener?.("languagechange", this.refresh);
|
|
104
|
+
ctx.settingsScope.describe().ensure().then(this.refresh, () => void 0);
|
|
105
|
+
}
|
|
106
|
+
read() {
|
|
107
|
+
const snapshot = this.ctx.settingsScope.describe().getSnapshot();
|
|
108
|
+
const value = snapshot.status === "ready" ? snapshot.view?.namespaces.find((item) => item.ns === "locale")?.value : void 0;
|
|
109
|
+
const browser = typeof navigator === "undefined" ? void 0 : navigator.languages?.[0] || navigator.language;
|
|
110
|
+
return resolveLanguage(localePreference(value), browser);
|
|
111
|
+
}
|
|
112
|
+
refresh = () => {
|
|
113
|
+
if (this.disposed) return;
|
|
114
|
+
const next = this.read();
|
|
115
|
+
if (this.language === next) return;
|
|
116
|
+
this.language = next;
|
|
117
|
+
for (const listener of this.listeners) listener();
|
|
118
|
+
};
|
|
119
|
+
getSnapshot = () => this.language;
|
|
120
|
+
subscribe = (listener) => {
|
|
121
|
+
this.listeners.add(listener);
|
|
122
|
+
return () => {
|
|
123
|
+
this.listeners.delete(listener);
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
dispose() {
|
|
127
|
+
this.disposed = true;
|
|
128
|
+
this.unsubscribe();
|
|
129
|
+
if (typeof window !== "undefined") window.removeEventListener?.("languagechange", this.refresh);
|
|
130
|
+
this.listeners.clear();
|
|
131
|
+
}
|
|
18
132
|
};
|
|
133
|
+
function useTranslation(controller) {
|
|
134
|
+
const language = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
135
|
+
return (text) => translate(text, language);
|
|
136
|
+
}
|
|
19
137
|
//#endregion
|
|
20
138
|
//#region src/client/OpenCodeCredentialForm.tsx
|
|
21
139
|
function OpenCodeCredentialForm(props) {
|
|
140
|
+
const t = useTranslation(props.language);
|
|
22
141
|
const [draft, setDraft] = (0, react.useState)("");
|
|
23
|
-
const [
|
|
142
|
+
const [action, setAction] = (0, react.useState)();
|
|
143
|
+
const saving = action !== void 0;
|
|
24
144
|
const [message, setMessage] = (0, react.useState)("");
|
|
25
145
|
const [error, setError] = (0, react.useState)(false);
|
|
26
146
|
const id = `opencode-api-key-${props.route}`;
|
|
@@ -30,27 +150,56 @@ function OpenCodeCredentialForm(props) {
|
|
|
30
150
|
const submit = (event) => {
|
|
31
151
|
event.preventDefault();
|
|
32
152
|
if (props.state.kind !== "known") return;
|
|
33
|
-
|
|
153
|
+
setAction("save");
|
|
34
154
|
setError(false);
|
|
35
155
|
props.controller.save(props.route, draft, props.state.ref).then((result) => {
|
|
36
|
-
|
|
156
|
+
setAction(void 0);
|
|
157
|
+
setMessage(result.message);
|
|
158
|
+
setError(result.kind === "error");
|
|
159
|
+
if (result.kind !== "error") setDraft("");
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const remove = () => {
|
|
163
|
+
if (saving || props.state.kind !== "known" || !props.state.configured || !props.state.writable) return;
|
|
164
|
+
setAction("delete");
|
|
165
|
+
setError(false);
|
|
166
|
+
setMessage("");
|
|
167
|
+
props.controller.remove(props.route, props.state.ref).then((result) => {
|
|
168
|
+
setAction(void 0);
|
|
37
169
|
setMessage(result.message);
|
|
38
170
|
setError(result.kind === "error");
|
|
39
171
|
if (result.kind !== "error") setDraft("");
|
|
40
172
|
});
|
|
41
173
|
};
|
|
42
174
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
175
|
+
lang: props.language.getSnapshot(),
|
|
43
176
|
onSubmit: submit,
|
|
44
177
|
noValidate: true,
|
|
178
|
+
style: {
|
|
179
|
+
display: "grid",
|
|
180
|
+
gap: 10,
|
|
181
|
+
padding: "16px 0"
|
|
182
|
+
},
|
|
45
183
|
children: [
|
|
46
184
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: props.route === "zen" ? "OpenCode Zen (Live)" : "OpenCode Go (Live)" }),
|
|
47
185
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
48
186
|
htmlFor: id,
|
|
49
|
-
children: "OpenCode API
|
|
187
|
+
children: t("OpenCode API key")
|
|
50
188
|
}),
|
|
51
189
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
52
190
|
id,
|
|
53
191
|
type: "password",
|
|
192
|
+
placeholder: props.state.kind === "known" && props.state.configured ? t("Key saved — enter a new key to replace it") : t("Paste your OpenCode API key"),
|
|
193
|
+
style: {
|
|
194
|
+
width: "100%",
|
|
195
|
+
boxSizing: "border-box",
|
|
196
|
+
padding: "10px 12px",
|
|
197
|
+
border: "1px solid #8886",
|
|
198
|
+
borderRadius: 8,
|
|
199
|
+
background: "transparent",
|
|
200
|
+
color: "inherit",
|
|
201
|
+
font: "inherit"
|
|
202
|
+
},
|
|
54
203
|
value: draft,
|
|
55
204
|
onChange: (event) => setDraft(event.currentTarget.value),
|
|
56
205
|
autoComplete: "new-password",
|
|
@@ -59,28 +208,54 @@ function OpenCodeCredentialForm(props) {
|
|
|
59
208
|
"aria-invalid": error,
|
|
60
209
|
"aria-describedby": `${id}-status`
|
|
61
210
|
}),
|
|
62
|
-
|
|
63
|
-
|
|
211
|
+
props.state.kind === "known" && props.state.sharedWith.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("This key is shared by OpenCode Zen and Go. Deleting it removes the key for both providers.") }),
|
|
212
|
+
unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t(props.state.kind === "unavailable" ? props.state.reason : "Checking credential status…") }),
|
|
213
|
+
readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("This credential is read-only. Update it in the environment that launches DSH.") }),
|
|
64
214
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
65
215
|
id: `${id}-status`,
|
|
66
216
|
role: error ? "alert" : "status",
|
|
67
217
|
"aria-live": "polite",
|
|
68
|
-
children: message
|
|
69
|
-
}),
|
|
70
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
71
|
-
type: "submit",
|
|
72
|
-
variant: "primary",
|
|
73
|
-
disabled,
|
|
74
|
-
children: "保存"
|
|
218
|
+
children: t(message)
|
|
75
219
|
}),
|
|
76
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
props.onCancel();
|
|
220
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
221
|
+
style: {
|
|
222
|
+
display: "flex",
|
|
223
|
+
flexWrap: "wrap",
|
|
224
|
+
gap: 8
|
|
82
225
|
},
|
|
83
|
-
children:
|
|
226
|
+
children: [
|
|
227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
228
|
+
type: "submit",
|
|
229
|
+
disabled: disabled || draft.trim().length === 0,
|
|
230
|
+
style: {
|
|
231
|
+
padding: "8px 16px",
|
|
232
|
+
borderRadius: 8,
|
|
233
|
+
cursor: "pointer"
|
|
234
|
+
},
|
|
235
|
+
children: t(action === "save" ? "Saving…" : "Save API key")
|
|
236
|
+
}),
|
|
237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
238
|
+
type: "button",
|
|
239
|
+
disabled: disabled || props.state.kind !== "known" || !props.state.configured,
|
|
240
|
+
onClick: remove,
|
|
241
|
+
style: {
|
|
242
|
+
padding: "8px 16px",
|
|
243
|
+
borderRadius: 8,
|
|
244
|
+
color: "#b42318"
|
|
245
|
+
},
|
|
246
|
+
children: t(action === "delete" ? "Deleting…" : "Delete API key")
|
|
247
|
+
}),
|
|
248
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
249
|
+
type: "button",
|
|
250
|
+
disabled: saving,
|
|
251
|
+
onClick: () => {
|
|
252
|
+
setDraft("");
|
|
253
|
+
setMessage("");
|
|
254
|
+
setError(false);
|
|
255
|
+
},
|
|
256
|
+
children: t("Clear input")
|
|
257
|
+
})
|
|
258
|
+
]
|
|
84
259
|
})
|
|
85
260
|
]
|
|
86
261
|
});
|
|
@@ -137,6 +312,7 @@ var CredentialController = class {
|
|
|
137
312
|
this.generations.clear();
|
|
138
313
|
this.states.clear();
|
|
139
314
|
this.inFlight.clear();
|
|
315
|
+
this.listeners.clear();
|
|
140
316
|
}
|
|
141
317
|
subscribe(listener) {
|
|
142
318
|
const listeners = this.listeners;
|
|
@@ -144,9 +320,23 @@ var CredentialController = class {
|
|
|
144
320
|
return () => listeners.delete(listener);
|
|
145
321
|
}
|
|
146
322
|
listeners = /* @__PURE__ */ new Set();
|
|
323
|
+
reloadScheduled = false;
|
|
147
324
|
invalidate() {
|
|
148
|
-
|
|
325
|
+
if (this.disposed) return;
|
|
326
|
+
for (const route of this.states.keys()) {
|
|
327
|
+
this.generations.set(route, (this.generations.get(route) ?? 0) + 1);
|
|
328
|
+
this.states.set(route, {
|
|
329
|
+
kind: "loading",
|
|
330
|
+
route
|
|
331
|
+
});
|
|
332
|
+
}
|
|
149
333
|
for (const listener of this.listeners) listener();
|
|
334
|
+
if (this.reloadScheduled) return;
|
|
335
|
+
this.reloadScheduled = true;
|
|
336
|
+
queueMicrotask(() => {
|
|
337
|
+
this.reloadScheduled = false;
|
|
338
|
+
if (!this.disposed) for (const route of this.states.keys()) this.loadRoute(route);
|
|
339
|
+
});
|
|
150
340
|
}
|
|
151
341
|
currentSettings() {
|
|
152
342
|
const face = this.ctx.settingsScope.describe();
|
|
@@ -170,18 +360,19 @@ var CredentialController = class {
|
|
|
170
360
|
kind: "loading",
|
|
171
361
|
route
|
|
172
362
|
});
|
|
173
|
-
if (signal?.aborted || this.disposed) return unavailable(route, "
|
|
363
|
+
if (signal?.aborted || this.disposed) return unavailable(route, "The credential check was cancelled.");
|
|
174
364
|
const settings = this.ctx.settingsScope.describe();
|
|
175
365
|
try {
|
|
176
366
|
await settings.ensure();
|
|
177
367
|
const current = this.currentSettings();
|
|
178
|
-
if (current === void 0) return this.commit(route, generation, unavailable(route, "
|
|
368
|
+
if (current === void 0) return this.commit(route, generation, unavailable(route, "The configured credential reference is unavailable."));
|
|
179
369
|
const ref = this.refFor(current.value, route);
|
|
180
|
-
if (ref === void 0) return this.commit(route, generation, unavailable(route, "
|
|
370
|
+
if (ref === void 0) return this.commit(route, generation, unavailable(route, "The configured credential reference is unavailable."));
|
|
181
371
|
const info = decodeDescribe(await this.ctx.remote.credentials.describe([ref]), ref);
|
|
182
|
-
if (info === void 0) return this.commit(route, generation, unavailable(route, "
|
|
372
|
+
if (info === void 0) return this.commit(route, generation, unavailable(route, "Could not check whether an API key is saved."));
|
|
183
373
|
const sharedWith = [];
|
|
184
374
|
for (const other of ["zen", "go"]) if (other !== route && this.refFor(current.value, other) === ref) sharedWith.push(other);
|
|
375
|
+
if (signal?.aborted) return this.commit(route, generation, unavailable(route, "The credential check was cancelled."));
|
|
185
376
|
return this.commit(route, generation, {
|
|
186
377
|
kind: "known",
|
|
187
378
|
route,
|
|
@@ -190,7 +381,7 @@ var CredentialController = class {
|
|
|
190
381
|
sharedWith
|
|
191
382
|
});
|
|
192
383
|
} catch {
|
|
193
|
-
return this.commit(route, generation, unavailable(route, "
|
|
384
|
+
return this.commit(route, generation, unavailable(route, "Could not load settings or credential status."));
|
|
194
385
|
}
|
|
195
386
|
}
|
|
196
387
|
commit(route, generation, state) {
|
|
@@ -198,7 +389,7 @@ var CredentialController = class {
|
|
|
198
389
|
this.states.set(route, state);
|
|
199
390
|
for (const listener of this.listeners) listener();
|
|
200
391
|
}
|
|
201
|
-
return
|
|
392
|
+
return this.states.get(route) ?? unavailable(route, "The settings form was closed.");
|
|
202
393
|
}
|
|
203
394
|
async loadRoutes(signal) {
|
|
204
395
|
const [zen, go] = await Promise.all([this.loadRoute("zen", signal), this.loadRoute("go", signal)]);
|
|
@@ -210,53 +401,104 @@ var CredentialController = class {
|
|
|
210
401
|
state(route) {
|
|
211
402
|
return this.states.get(route);
|
|
212
403
|
}
|
|
404
|
+
/** Remove the saved credential, preserving the provider and its reference. */
|
|
405
|
+
async remove(route, displayedRef) {
|
|
406
|
+
const displayed = this.states.get(route);
|
|
407
|
+
const fence = this.disposalGeneration;
|
|
408
|
+
const current = await this.loadRoute(route);
|
|
409
|
+
if (this.disposed || this.disposalGeneration !== fence) return {
|
|
410
|
+
kind: "error",
|
|
411
|
+
message: "The settings form was closed. Open it again and retry."
|
|
412
|
+
};
|
|
413
|
+
if (displayed?.kind !== "known" || current.kind !== "known" || displayed.ref !== displayedRef || current.ref !== displayedRef || displayed.sharedWith.join(",") !== current.sharedWith.join(",")) return {
|
|
414
|
+
kind: "error",
|
|
415
|
+
message: "The API key settings changed. Check which providers share the key and retry."
|
|
416
|
+
};
|
|
417
|
+
if (!current.writable) return {
|
|
418
|
+
kind: "error",
|
|
419
|
+
message: "This credential is read-only. Remove it from the environment that launches DSH."
|
|
420
|
+
};
|
|
421
|
+
if (this.inFlight.has(current.ref)) return {
|
|
422
|
+
kind: "error",
|
|
423
|
+
message: "This credential is already being updated."
|
|
424
|
+
};
|
|
425
|
+
this.inFlight.add(current.ref);
|
|
426
|
+
try {
|
|
427
|
+
if (!isSuccessfulWrite(await this.ctx.remote.credentials.unset(current.ref))) return {
|
|
428
|
+
kind: "error",
|
|
429
|
+
message: "Could not delete the API key. Try again."
|
|
430
|
+
};
|
|
431
|
+
await Promise.all([route, ...current.sharedWith].map((other) => this.loadRoute(other)));
|
|
432
|
+
const confirmed = this.state(route);
|
|
433
|
+
if (confirmed?.kind !== "known" || confirmed.ref !== current.ref) return {
|
|
434
|
+
kind: "deleted-unconfirmed",
|
|
435
|
+
message: "The deletion request succeeded, but the key status could not be confirmed. Reload Settings > Models."
|
|
436
|
+
};
|
|
437
|
+
if (confirmed.configured) return {
|
|
438
|
+
kind: "deleted-unconfirmed",
|
|
439
|
+
message: "The saved key was removed, but an API key is still configured. Check its source in Settings > Models."
|
|
440
|
+
};
|
|
441
|
+
return {
|
|
442
|
+
kind: "deleted",
|
|
443
|
+
message: current.sharedWith.length > 0 ? "API key deleted for OpenCode Zen and Go." : "API key deleted."
|
|
444
|
+
};
|
|
445
|
+
} catch {
|
|
446
|
+
return {
|
|
447
|
+
kind: "error",
|
|
448
|
+
message: "Could not delete the API key. Try again."
|
|
449
|
+
};
|
|
450
|
+
} finally {
|
|
451
|
+
this.inFlight.delete(current.ref);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
213
454
|
async save(route, value, displayedRef) {
|
|
214
455
|
const normalized = value.trim();
|
|
215
456
|
if (normalized.length === 0 || /[\r\n]/.test(normalized) || /^['"].*['"]$/.test(normalized) || normalized.includes("=")) return {
|
|
216
457
|
kind: "error",
|
|
217
|
-
message: "API
|
|
458
|
+
message: "Paste the API key only, without quotes or an environment-variable assignment."
|
|
218
459
|
};
|
|
219
460
|
const displayed = this.states.get(route);
|
|
220
461
|
const fence = this.disposalGeneration;
|
|
221
462
|
const current = await this.loadRoute(route);
|
|
222
463
|
if (this.disposed || this.disposalGeneration !== fence) return {
|
|
223
464
|
kind: "error",
|
|
224
|
-
message: "
|
|
465
|
+
message: "The settings form was closed. Open it again and retry."
|
|
225
466
|
};
|
|
226
467
|
if (displayed?.kind !== "known" || current.kind !== "known" || displayed.ref !== displayedRef || current.ref !== displayedRef) return {
|
|
227
468
|
kind: "error",
|
|
228
|
-
message: "
|
|
469
|
+
message: "The credential reference changed. Reload its status and retry."
|
|
229
470
|
};
|
|
230
471
|
if (!current.writable) return {
|
|
231
472
|
kind: "error",
|
|
232
|
-
message: "
|
|
473
|
+
message: "This credential is read-only. Update it in the environment that launches DSH."
|
|
233
474
|
};
|
|
234
475
|
if (this.inFlight.has(current.ref)) return {
|
|
235
476
|
kind: "error",
|
|
236
|
-
message: "
|
|
477
|
+
message: "This credential is already being updated."
|
|
237
478
|
};
|
|
238
479
|
this.inFlight.add(current.ref);
|
|
239
480
|
try {
|
|
240
481
|
if (this.disposed || this.disposalGeneration !== fence) return {
|
|
241
482
|
kind: "error",
|
|
242
|
-
message: "
|
|
483
|
+
message: "The settings form was closed. Open it again and retry."
|
|
243
484
|
};
|
|
244
485
|
if (!isSuccessfulWrite(await this.ctx.remote.credentials.set(current.ref, normalized))) return {
|
|
245
486
|
kind: "error",
|
|
246
|
-
message: "API
|
|
487
|
+
message: "Could not save the API key."
|
|
247
488
|
};
|
|
248
489
|
const confirmed = await this.loadRoute(route);
|
|
490
|
+
for (const other of current.sharedWith) await this.loadRoute(other);
|
|
249
491
|
return confirmed.kind === "known" && confirmed.configured ? {
|
|
250
492
|
kind: "saved",
|
|
251
|
-
message: "
|
|
493
|
+
message: "API key saved."
|
|
252
494
|
} : {
|
|
253
495
|
kind: "saved-unconfirmed",
|
|
254
|
-
message: "
|
|
496
|
+
message: "The key was saved, but its status could not be confirmed."
|
|
255
497
|
};
|
|
256
498
|
} catch {
|
|
257
499
|
return {
|
|
258
500
|
kind: "error",
|
|
259
|
-
message: "API
|
|
501
|
+
message: "Could not save the API key."
|
|
260
502
|
};
|
|
261
503
|
} finally {
|
|
262
504
|
this.inFlight.delete(current.ref);
|
|
@@ -293,244 +535,185 @@ function OpenCodeProviderCard(props) {
|
|
|
293
535
|
route,
|
|
294
536
|
state,
|
|
295
537
|
controller: props.controller,
|
|
296
|
-
|
|
538
|
+
language: props.language
|
|
297
539
|
})
|
|
298
540
|
});
|
|
299
541
|
}
|
|
300
542
|
//#endregion
|
|
301
|
-
//#region src/client/
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
543
|
+
//#region src/client/CommandNotice.tsx
|
|
544
|
+
/** Native modal keeps the full message readable and owns focus and Escape. */
|
|
545
|
+
function CommandNotice({ controller, language }) {
|
|
546
|
+
const t = useTranslation(language);
|
|
547
|
+
const text = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
548
|
+
const dialog = (0, react.useRef)(null);
|
|
549
|
+
const titleId = (0, react.useId)();
|
|
550
|
+
const bodyId = (0, react.useId)();
|
|
551
|
+
(0, react.useEffect)(() => {
|
|
552
|
+
const element = dialog.current;
|
|
553
|
+
if (text === void 0 || element === null) return;
|
|
554
|
+
element.showModal();
|
|
555
|
+
return () => {
|
|
556
|
+
element.close();
|
|
557
|
+
};
|
|
558
|
+
}, [text]);
|
|
559
|
+
if (text === void 0) return null;
|
|
560
|
+
const lines = t(text).split("\n");
|
|
561
|
+
const stepStart = lines.findIndex((line) => /^\d+\. /.test(line));
|
|
562
|
+
const hasSteps = stepStart >= 0 && lines.slice(stepStart).every((line) => /^\d+\. /.test(line));
|
|
563
|
+
const summary = hasSteps ? lines.slice(0, stepStart).join("\n") : t(text);
|
|
564
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dialog", {
|
|
565
|
+
lang: language.getSnapshot(),
|
|
566
|
+
ref: dialog,
|
|
567
|
+
className: "dsh-opencode-notice",
|
|
568
|
+
"aria-labelledby": titleId,
|
|
569
|
+
"aria-describedby": bodyId,
|
|
570
|
+
onCancel: (event) => {
|
|
571
|
+
event.preventDefault();
|
|
572
|
+
controller.close();
|
|
573
|
+
},
|
|
574
|
+
style: {
|
|
575
|
+
width: "min(520px, calc(100vw - 32px))",
|
|
576
|
+
maxHeight: "calc(100dvh - 32px)",
|
|
577
|
+
boxSizing: "border-box",
|
|
578
|
+
padding: 28,
|
|
579
|
+
border: "1px solid #8884",
|
|
580
|
+
borderRadius: 16,
|
|
581
|
+
background: "Canvas",
|
|
582
|
+
color: "CanvasText",
|
|
583
|
+
boxShadow: "0 16px 64px #0003",
|
|
584
|
+
pointerEvents: "auto",
|
|
585
|
+
overflow: "auto"
|
|
586
|
+
},
|
|
320
587
|
children: [
|
|
321
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
322
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: ".dsh-opencode-notice::backdrop { background: rgb(0 0 0 / 35%); }" }),
|
|
589
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
590
|
+
id: titleId,
|
|
591
|
+
style: {
|
|
592
|
+
margin: "0 0 20px",
|
|
593
|
+
fontSize: 22
|
|
594
|
+
},
|
|
595
|
+
children: t("OpenCode settings")
|
|
596
|
+
}),
|
|
597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
598
|
+
id: bodyId,
|
|
599
|
+
style: {
|
|
600
|
+
fontSize: 16,
|
|
601
|
+
lineHeight: 1.8,
|
|
602
|
+
overflowWrap: "anywhere"
|
|
603
|
+
},
|
|
604
|
+
children: [summary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
605
|
+
style: {
|
|
606
|
+
margin: "0 0 16px",
|
|
607
|
+
whiteSpace: "pre-wrap",
|
|
608
|
+
fontWeight: 600
|
|
609
|
+
},
|
|
610
|
+
children: summary
|
|
611
|
+
}), hasSteps && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
612
|
+
style: {
|
|
613
|
+
margin: 0,
|
|
614
|
+
paddingLeft: 24
|
|
615
|
+
},
|
|
616
|
+
children: lines.slice(stepStart).map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
617
|
+
style: {
|
|
618
|
+
paddingLeft: 4,
|
|
619
|
+
marginTop: 10
|
|
620
|
+
},
|
|
621
|
+
children: line.replace(/^\d+\. /, "")
|
|
622
|
+
}, index))
|
|
623
|
+
})]
|
|
624
|
+
}),
|
|
625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
626
|
+
style: {
|
|
627
|
+
display: "flex",
|
|
628
|
+
justifyContent: "flex-end",
|
|
629
|
+
marginTop: 24
|
|
630
|
+
},
|
|
631
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
632
|
+
autoFocus: true,
|
|
633
|
+
type: "button",
|
|
634
|
+
onClick: controller.close,
|
|
635
|
+
style: {
|
|
636
|
+
padding: "10px 24px",
|
|
637
|
+
border: "1px solid #8886",
|
|
638
|
+
borderRadius: 8,
|
|
639
|
+
font: "inherit",
|
|
640
|
+
cursor: "pointer"
|
|
641
|
+
},
|
|
642
|
+
children: t("Close")
|
|
342
643
|
})
|
|
343
|
-
] }),
|
|
344
|
-
body,
|
|
345
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
346
|
-
type: "button",
|
|
347
|
-
variant: "ghost",
|
|
348
|
-
onClick: close,
|
|
349
|
-
children: "キャンセル"
|
|
350
644
|
})
|
|
351
645
|
]
|
|
352
646
|
});
|
|
353
647
|
}
|
|
354
|
-
function CredentialFormForRoute(props) {
|
|
355
|
-
const state = props.controller.state(props.route) ?? {
|
|
356
|
-
kind: "loading",
|
|
357
|
-
route: props.route
|
|
358
|
-
};
|
|
359
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenCodeCredentialForm, {
|
|
360
|
-
route: props.route,
|
|
361
|
-
state,
|
|
362
|
-
controller: props.controller.credentials,
|
|
363
|
-
onCancel: props.onCancel
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
function StatusRows(props) {
|
|
367
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: ["zen", "go"].map((route) => {
|
|
368
|
-
const state = props.controller.state(route);
|
|
369
|
-
const text = state?.kind === "known" ? state.configured ? "API キー保存済み" : "API キー未設定" : state?.kind === "unavailable" ? state.reason : "状態を確認中です";
|
|
370
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
371
|
-
route === "zen" ? "Zen" : "Go",
|
|
372
|
-
": ",
|
|
373
|
-
text
|
|
374
|
-
] }, route);
|
|
375
|
-
}) });
|
|
376
|
-
}
|
|
377
648
|
//#endregion
|
|
378
|
-
//#region src/client/
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
current = { open: false };
|
|
649
|
+
//#region src/client/command-notice.ts
|
|
650
|
+
/** Browser-local notice: only a command submitted here opens it. */
|
|
651
|
+
var CommandNoticeController = class {
|
|
652
|
+
text;
|
|
383
653
|
listeners = /* @__PURE__ */ new Set();
|
|
384
|
-
|
|
385
|
-
this.ctx = ctx;
|
|
386
|
-
this.credentials = new CredentialController(ctx);
|
|
387
|
-
}
|
|
388
|
-
getSnapshot = () => this.current;
|
|
654
|
+
getSnapshot = () => this.text;
|
|
389
655
|
subscribe = (listener) => {
|
|
390
656
|
this.listeners.add(listener);
|
|
391
|
-
const credentialDispose = this.credentials.subscribe(listener);
|
|
392
657
|
return () => {
|
|
393
658
|
this.listeners.delete(listener);
|
|
394
|
-
credentialDispose();
|
|
395
659
|
};
|
|
396
660
|
};
|
|
661
|
+
show(name, result) {
|
|
662
|
+
if (name !== "dsh-opencode" || !result.text?.trim()) return;
|
|
663
|
+
this.text = result.text;
|
|
664
|
+
this.notify();
|
|
665
|
+
}
|
|
666
|
+
close = () => {
|
|
667
|
+
this.text = void 0;
|
|
668
|
+
this.notify();
|
|
669
|
+
};
|
|
397
670
|
dispose() {
|
|
398
|
-
this.
|
|
671
|
+
this.text = void 0;
|
|
399
672
|
this.listeners.clear();
|
|
400
|
-
this.close();
|
|
401
673
|
}
|
|
402
|
-
|
|
403
|
-
this.current = next;
|
|
674
|
+
notify() {
|
|
404
675
|
for (const listener of this.listeners) listener();
|
|
405
676
|
}
|
|
406
|
-
open(route, message) {
|
|
407
|
-
this.update({
|
|
408
|
-
open: true,
|
|
409
|
-
route,
|
|
410
|
-
...message === void 0 ? {} : { message }
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
close() {
|
|
414
|
-
this.update({ open: false });
|
|
415
|
-
}
|
|
416
|
-
sessionId;
|
|
417
|
-
async select(option, sessionId) {
|
|
418
|
-
this.sessionId = sessionId;
|
|
419
|
-
if (option.id === "zen" || option.id === "go") {
|
|
420
|
-
this.open(option.id);
|
|
421
|
-
await this.credentials.loadRoute(option.id);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
if (option.id === "status") {
|
|
425
|
-
this.open("status", "設定状況を確認中…");
|
|
426
|
-
await this.credentials.loadRoutes();
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (option.id === "refresh") {
|
|
430
|
-
this.open("status", "モデル一覧を更新中…");
|
|
431
|
-
const execute = commandsExecute(this.ctx);
|
|
432
|
-
if (execute === void 0 || this.sessionId === void 0) {
|
|
433
|
-
this.open("status", "更新コマンドを利用できません。/opencode-refresh all を実行してください。");
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
try {
|
|
437
|
-
const result = await execute(this.sessionId, "/opencode-refresh all", [], void 0);
|
|
438
|
-
this.open("status", result.ok ? "モデル一覧の更新コマンドを送信しました。" : "モデル一覧を更新できませんでした。");
|
|
439
|
-
} catch {
|
|
440
|
-
this.open("status", "モデル一覧を更新できませんでした。");
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
state(route) {
|
|
445
|
-
return this.credentials.state(route);
|
|
446
|
-
}
|
|
447
677
|
};
|
|
448
|
-
function commandsExecute(ctx) {
|
|
449
|
-
const commands = ctx.remote.commands;
|
|
450
|
-
if (typeof commands !== "object" || commands === null) return void 0;
|
|
451
|
-
const execute = commands.execute;
|
|
452
|
-
return typeof execute === "function" ? execute.bind(commands) : void 0;
|
|
453
|
-
}
|
|
454
678
|
//#endregion
|
|
455
679
|
//#region src/client/index.ts
|
|
456
|
-
/**
|
|
680
|
+
/** Settings must work without a session, slash commands, or a shell overlay. */
|
|
457
681
|
const inject = [
|
|
458
|
-
"
|
|
459
|
-
"@deepseek-ai/dsh-commands",
|
|
460
|
-
"@deepseek-ai/dsh-client-locale",
|
|
461
|
-
"@deepseek-ai/dsh-client-store",
|
|
462
|
-
"@deepseek-ai/dsh-client-ui-commands",
|
|
463
|
-
"@deepseek-ai/dsh-client-ui-layout",
|
|
464
|
-
"@deepseek-ai/dsh-client-ui-primitives",
|
|
465
|
-
"@deepseek-ai/dsh-client-ui-renderer",
|
|
466
|
-
"@deepseek-ai/dsh-client-ui-settings",
|
|
467
|
-
"@deepseek-ai/dsh-client-ui-settings-models",
|
|
468
|
-
"@deepseek-ai/dsh-client-ui-slots"
|
|
469
|
-
];
|
|
470
|
-
const services = [
|
|
471
|
-
"commandUi",
|
|
682
|
+
"remote",
|
|
472
683
|
"remote.credentials",
|
|
473
|
-
"remote.commands",
|
|
474
684
|
"settingsScope",
|
|
475
685
|
"slots"
|
|
476
686
|
];
|
|
477
687
|
function apply(ctx) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
id: "refresh",
|
|
505
|
-
label: "モデル一覧を更新",
|
|
506
|
-
detail: "Host の更新コマンドを実行してください"
|
|
507
|
-
});
|
|
508
|
-
return options;
|
|
509
|
-
},
|
|
510
|
-
onSelect: (option, session) => {
|
|
511
|
-
setup.select(option, session.sessionId);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}));
|
|
515
|
-
disposers.push(client.slots.inject("settings.models.provider-card", () => client.slots.register({
|
|
516
|
-
name: "settings.models.provider-card",
|
|
517
|
-
key: "opencode-live",
|
|
518
|
-
inject: () => ({ controller: setup.credentials })
|
|
519
|
-
}, OpenCodeProviderCard)));
|
|
520
|
-
disposers.push(client.slots.inject("shell.overlay", () => client.slots.register({
|
|
521
|
-
name: "shell.overlay",
|
|
522
|
-
id: "opencode-live-setup",
|
|
523
|
-
inject: () => ({ controller: setup })
|
|
524
|
-
}, OpenCodeSetupDialog)));
|
|
525
|
-
return () => {
|
|
526
|
-
setup.dispose();
|
|
527
|
-
for (const dispose of disposers.splice(0)) dispose();
|
|
528
|
-
};
|
|
529
|
-
}, `${CLIENT_MODULE_ID}: client registrations`);
|
|
688
|
+
const language = new LanguageController(ctx);
|
|
689
|
+
ctx.effect(() => () => language.dispose());
|
|
690
|
+
ctx.effect(function* () {
|
|
691
|
+
const controller = new CredentialController(ctx);
|
|
692
|
+
yield () => controller.dispose();
|
|
693
|
+
yield ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
|
|
694
|
+
name: "settings.models.provider-card",
|
|
695
|
+
key: "opencode-live",
|
|
696
|
+
inject: () => ({
|
|
697
|
+
controller,
|
|
698
|
+
language
|
|
699
|
+
})
|
|
700
|
+
}, OpenCodeProviderCard));
|
|
701
|
+
}, `${CLIENT_MODULE_ID}: provider settings`);
|
|
702
|
+
ctx.slots.inject("shell.overlay", function* () {
|
|
703
|
+
const notice = new CommandNoticeController();
|
|
704
|
+
yield () => notice.dispose();
|
|
705
|
+
yield ctx.on("command/executed", (_sessionId, name, result) => notice.show(name, result));
|
|
706
|
+
yield ctx.slots.register({
|
|
707
|
+
name: "shell.overlay",
|
|
708
|
+
id: "opencode-command-notice",
|
|
709
|
+
inject: () => ({
|
|
710
|
+
controller: notice,
|
|
711
|
+
language
|
|
712
|
+
})
|
|
713
|
+
}, CommandNotice);
|
|
530
714
|
});
|
|
531
715
|
}
|
|
532
716
|
//#endregion
|
|
533
|
-
exports.SetupController = SetupController;
|
|
534
717
|
exports.apply = apply;
|
|
535
718
|
exports.inject = inject;
|
|
536
719
|
|