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/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
- const HELP_URLS = {
15
- auth: "https://opencode.ai/auth",
16
- zen: "https://opencode.ai/docs/zen/",
17
- go: "https://opencode.ai/docs/go/"
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 [saving, setSaving] = (0, react.useState)(false);
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
- setSaving(true);
153
+ setAction("save");
34
154
  setError(false);
35
155
  props.controller.save(props.route, draft, props.state.ref).then((result) => {
36
- setSaving(false);
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
- unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: props.state.kind === "unavailable" ? props.state.reason : "状態を確認中です。" }),
63
- readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "この認証参照は読み取り専用です。" }),
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.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
77
- type: "button",
78
- variant: "ghost",
79
- onClick: () => {
80
- setDraft("");
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
- for (const route of ["zen", "go"]) this.generations.set(route, (this.generations.get(route) ?? 0) + 1);
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, "設定の credential reference を確認できません。"));
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, "設定の credential reference を確認できません。"));
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 state;
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
- onCancel: () => void 0
538
+ language: props.language
297
539
  })
298
540
  });
299
541
  }
300
542
  //#endregion
301
- //#region src/client/OpenCodeSetupDialog.tsx
302
- function OpenCodeSetupDialog(props) {
303
- const snapshot = (0, react.useSyncExternalStore)(props.controller.subscribe, props.controller.getSnapshot, props.controller.getSnapshot);
304
- if (!snapshot.open) return null;
305
- const close = () => props.controller.close();
306
- const body = snapshot.route === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
307
- role: "status",
308
- children: [snapshot.message ?? "設定状況を確認しました。", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusRows, { controller: props.controller })]
309
- }) : snapshot.route === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialFormForRoute, {
310
- route: snapshot.route,
311
- controller: props.controller,
312
- onCancel: close
313
- });
314
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
315
- open: true,
316
- onClose: close,
317
- title: "OpenCode API キー設定",
318
- closeLabel: "閉じる",
319
- description: "キーの値は表示しません。",
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)("p", { children: "OpenCode にサインインし、Zen または Go API キーを作成して保存してください。" }),
322
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
323
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
324
- href: HELP_URLS.auth,
325
- target: "_blank",
326
- rel: "noopener noreferrer",
327
- children: "サインイン"
328
- }),
329
- " | ",
330
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
331
- href: HELP_URLS.zen,
332
- target: "_blank",
333
- rel: "noopener noreferrer",
334
- children: "Zen の手順"
335
- }),
336
- " | ",
337
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
338
- href: HELP_URLS.go,
339
- target: "_blank",
340
- rel: "noopener noreferrer",
341
- children: "Go の手順"
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/setup-controller.ts
379
- var SetupController = class {
380
- ctx;
381
- credentials;
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
- constructor(ctx) {
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.credentials.dispose();
671
+ this.text = void 0;
399
672
  this.listeners.clear();
400
- this.close();
401
673
  }
402
- update(next) {
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
- /** Client packages required by this entry's injected services and UI modules. */
680
+ /** Settings must work without a session, slash commands, or a shell overlay. */
457
681
  const inject = [
458
- "@deepseek-ai/dsh-api-remotes",
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
- ctx.inject(services, (injected) => {
479
- const client = injected;
480
- client.effect(() => {
481
- const setup = new SetupController(client);
482
- const disposers = [];
483
- disposers.push(client.commandUi.decorate({
484
- name: "dsh-opencode",
485
- available: () => true,
486
- ui: {
487
- kind: "popupSelect",
488
- options: async (_session, signal) => {
489
- const states = await setup.credentials.loadRoutes(signal);
490
- const options = ["zen", "go"].map((route) => {
491
- const state = states[route];
492
- return {
493
- id: route,
494
- label: route === "zen" ? "OpenCode Zen を設定" : "OpenCode Go を設定",
495
- detail: state.kind === "known" ? state.configured ? "API キー保存済み" : "API キー未設定" : state.kind === "unavailable" ? state.reason : "状態を確認中です",
496
- active: state.kind === "known" && state.configured
497
- };
498
- });
499
- options.push({
500
- id: "status",
501
- label: "設定状況を確認",
502
- detail: "キーの値は表示しません"
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