@mars-sea/dsh-commandcode-provider 0.2.0 → 0.2.2

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
@@ -4,7 +4,10 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- //#region src/client/index.ts
7
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/client/sessions.ts
8
11
  /** Whether a selectModel rejection is the harness's image-session gate. */
9
12
  function isImageSessionRejection(result) {
10
13
  return !result.result.ok && result.result.error.code === "model-unavailable" && result.result.error.message.includes("does not accept image input");
@@ -31,20 +34,673 @@ window.__ModuleLoader__.load({
31
34
  }
32
35
  };
33
36
  }
37
+ /** Install the wrapper on a connection's shared sessions face. */
38
+ function installFriendlyImageError(connection) {
39
+ connection.api.sessions = withFriendlyImageError(connection.api.sessions);
40
+ }
41
+ //#endregion
42
+ //#region src/client/settings.ts
43
+ /** The settings namespace the plugin registers (host half, src/index.ts). */
44
+ const COMMANDCODE_NS = "llm-commandcode";
45
+ /** Default credential reference the plugin resolves when none is named. */
46
+ const DEFAULT_API_KEY_REF = "COMMANDCODE_API_KEY";
47
+ /** A free-text field; an empty draft clears it. */
48
+ function textField(field) {
49
+ return {
50
+ field,
51
+ format: (value) => typeof value === "string" ? value : "",
52
+ parse: (text) => {
53
+ const trimmed = text.trim();
54
+ return trimmed === "" ? { kind: "clear" } : {
55
+ kind: "set",
56
+ value: trimmed
57
+ };
58
+ }
59
+ };
60
+ }
61
+ /** A whole-number field; an empty draft clears it, anything non-numeric blocks save. */
62
+ function numberField(field) {
63
+ return {
64
+ field,
65
+ format: (value) => typeof value === "number" ? String(value) : "",
66
+ parse: (text) => {
67
+ const trimmed = text.trim();
68
+ if (trimmed === "") return { kind: "clear" };
69
+ const parsed = Number(trimmed);
70
+ return Number.isFinite(parsed) ? {
71
+ kind: "set",
72
+ value: parsed
73
+ } : { kind: "invalid" };
74
+ }
75
+ };
76
+ }
77
+ /** The fields this page edits inside the `llm-commandcode` namespace. */
78
+ const SECTION_FIELDS = [
79
+ textField("apiBase"),
80
+ textField("workingDir"),
81
+ numberField("requestTimeoutMs"),
82
+ numberField("streamIdleTimeoutMs")
83
+ ];
84
+ /**
85
+ * Controller bridging the `llm-commandcode` scope and the credentials domain
86
+ * onto the page. Public API mirrors the harness's CardForm actions, so the
87
+ * component stays thin.
88
+ */
89
+ var CommandCodeSettingsController = class {
90
+ scope;
91
+ api;
92
+ specs = new Map(SECTION_FIELDS.map((spec) => [spec.field, spec]));
93
+ staged = /* @__PURE__ */ new Map();
94
+ listeners = /* @__PURE__ */ new Set();
95
+ disposers = [];
96
+ disposed = false;
97
+ defaultWorkingDir;
98
+ credential = {
99
+ ref: DEFAULT_API_KEY_REF,
100
+ configured: false,
101
+ writable: true
102
+ };
103
+ saving = false;
104
+ failed = false;
105
+ /**
106
+ * @param scope - bound scope for the `llm-commandcode` namespace.
107
+ * @param api - credentials wire face.
108
+ * @param hostDescription - the Host-description observable whose `cwd` is
109
+ * shown as the placeholder a blank `workingDir` field resolves to.
110
+ */
111
+ constructor(scope, api, hostDescription) {
112
+ this.scope = scope;
113
+ this.api = api;
114
+ this.disposers.push(scope.subscribe(() => {
115
+ this.recomputeCredentialRef();
116
+ this.publish();
117
+ }));
118
+ if (hostDescription !== void 0) {
119
+ this.defaultWorkingDir = hostDescription.getSnapshot()?.cwd;
120
+ this.disposers.push(hostDescription.subscribe(() => {
121
+ if (this.disposed) return;
122
+ const cwd = hostDescription.getSnapshot()?.cwd;
123
+ if (cwd !== this.defaultWorkingDir) {
124
+ this.defaultWorkingDir = cwd;
125
+ this.publish();
126
+ }
127
+ }));
128
+ }
129
+ this.recomputeCredentialRef();
130
+ this.readCredential();
131
+ }
132
+ /** Release every subscription held on external sources. Idempotent. */
133
+ dispose() {
134
+ if (this.disposed) return;
135
+ this.disposed = true;
136
+ for (const dispose of this.disposers) dispose();
137
+ this.disposers.length = 0;
138
+ this.listeners.clear();
139
+ }
140
+ /**
141
+ * The credential reference the section names, or the provider default. A
142
+ * user who renamed `apiKeyEnv` in `settings.yaml` (or the composition
143
+ * config) gets a page that addresses the renamed ref instead of silently
144
+ * writing the default — mirroring the Models page's `refFor()`.
145
+ */
146
+ recomputeCredentialRef() {
147
+ const snapshot = this.scope.getSnapshot();
148
+ const named = typeof snapshot.value?.apiKeyEnv === "string" && snapshot.value.apiKeyEnv.length > 0 ? snapshot.value.apiKeyEnv : DEFAULT_API_KEY_REF;
149
+ if (named === this.credential.ref) return;
150
+ this.credential = {
151
+ ref: named,
152
+ configured: false,
153
+ writable: true
154
+ };
155
+ this.readCredential();
156
+ }
157
+ /** Subscribe to state projections. @returns the disposer. */
158
+ subscribe(listener) {
159
+ this.listeners.add(listener);
160
+ return () => this.listeners.delete(listener);
161
+ }
162
+ /** Build the current page state face. */
163
+ state() {
164
+ const snapshot = this.scope.getSnapshot();
165
+ const plan = this.plan();
166
+ return {
167
+ available: snapshot.status === "ready",
168
+ writable: snapshot.writable,
169
+ apiKeyConfigured: this.credential.configured,
170
+ apiKeyWritable: this.credential.writable,
171
+ apiKey: {
172
+ text: this.staged.get("apiKey")?.text ?? "",
173
+ clear: false,
174
+ overridden: false,
175
+ invalid: false
176
+ },
177
+ apiBase: this.field("apiBase"),
178
+ workingDir: this.field("workingDir"),
179
+ defaultWorkingDir: this.defaultWorkingDir,
180
+ requestTimeoutMs: this.field("requestTimeoutMs"),
181
+ streamIdleTimeoutMs: this.field("streamIdleTimeoutMs"),
182
+ dirty: plan.length > 0,
183
+ invalid: plan.some((item) => item.run === void 0),
184
+ saving: this.saving,
185
+ failed: this.failed
186
+ };
187
+ }
188
+ /** Stage one field's draft text. */
189
+ edit(field, text) {
190
+ this.staged.set(field, {
191
+ text,
192
+ clear: false
193
+ });
194
+ this.failed = false;
195
+ this.publish();
196
+ }
197
+ /** Reset one section field to its inherited (composition) value. */
198
+ resetField(field) {
199
+ if (field === "apiKey") {
200
+ this.staged.delete("apiKey");
201
+ this.failed = false;
202
+ this.publish();
203
+ return;
204
+ }
205
+ const spec = this.spec(field);
206
+ this.staged.set(field, {
207
+ text: spec.format(this.baseValue(field)),
208
+ clear: true
209
+ });
210
+ this.failed = false;
211
+ this.publish();
212
+ }
213
+ /** Discard every staged edit. */
214
+ discard() {
215
+ if (this.staged.size === 0 && !this.failed) return;
216
+ this.staged.clear();
217
+ this.failed = false;
218
+ this.publish();
219
+ }
220
+ /** Write every staged edit, then re-read the Host's accepted state. */
221
+ async save() {
222
+ const plan = this.plan();
223
+ if (plan.length === 0 || this.saving) return;
224
+ const runs = [];
225
+ for (const item of plan) {
226
+ if (item.run === void 0) return;
227
+ runs.push(item.run);
228
+ }
229
+ this.saving = true;
230
+ this.failed = false;
231
+ this.publish();
232
+ let landed = true;
233
+ for (const run of runs) landed = await run() && landed;
234
+ this.saving = false;
235
+ this.failed = !landed;
236
+ if (landed) this.staged.clear();
237
+ this.publish();
238
+ }
239
+ spec(field) {
240
+ const spec = this.specs.get(field);
241
+ if (spec === void 0) throw new Error(`commandcode settings page has no field ${field}`);
242
+ return spec;
243
+ }
244
+ /** One field's rendered state: draft text, whether it is user-overridden, invalid. */
245
+ field(field) {
246
+ const spec = this.spec(field);
247
+ const staged = this.staged.get(field);
248
+ if (staged === void 0) return {
249
+ text: spec.format(this.sectionValue(field)),
250
+ clear: false,
251
+ overridden: this.stored(field),
252
+ invalid: false
253
+ };
254
+ const parsed = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
255
+ return {
256
+ text: staged.text,
257
+ clear: staged.clear,
258
+ overridden: parsed.kind === "set",
259
+ invalid: parsed.kind === "invalid"
260
+ };
261
+ }
262
+ sectionValue(field) {
263
+ return this.scope.getSnapshot().value?.[field];
264
+ }
265
+ baseValue(field) {
266
+ const base = this.scope.getSnapshot().base;
267
+ return typeof base === "object" && base !== null && !Array.isArray(base) ? base[field] : void 0;
268
+ }
269
+ userLayer() {
270
+ const user = this.scope.getSnapshot().user;
271
+ return typeof user === "object" && user !== null && !Array.isArray(user) ? user : void 0;
272
+ }
273
+ stored(field) {
274
+ const user = this.userLayer();
275
+ return user !== void 0 && Object.prototype.hasOwnProperty.call(user, field);
276
+ }
277
+ /**
278
+ * The writes a save would perform, in staged order. A field whose draft is
279
+ * not a value its spec accepts carries no write (the save refuses).
280
+ */
281
+ plan() {
282
+ const plan = [];
283
+ for (const [field, staged] of this.staged) {
284
+ if (field === "apiKey") {
285
+ const value = staged.text.trim();
286
+ if (value !== "") plan.push({
287
+ field,
288
+ run: () => this.writeKey(value)
289
+ });
290
+ continue;
291
+ }
292
+ const spec = this.spec(field);
293
+ if (staged.clear) {
294
+ if (this.stored(field)) plan.push({
295
+ field,
296
+ run: () => this.clear(field)
297
+ });
298
+ continue;
299
+ }
300
+ if (staged.text === spec.format(this.sectionValue(field))) continue;
301
+ const parsed = spec.parse(staged.text);
302
+ if (parsed.kind === "invalid") plan.push({
303
+ field,
304
+ run: void 0
305
+ });
306
+ else if (parsed.kind === "clear") plan.push({
307
+ field,
308
+ run: () => this.clear(field)
309
+ });
310
+ else plan.push({
311
+ field,
312
+ run: () => this.store(field, parsed.value)
313
+ });
314
+ }
315
+ return plan;
316
+ }
317
+ async clear(field) {
318
+ await this.scope.unset(field);
319
+ return !this.stored(field);
320
+ }
321
+ async store(field, value) {
322
+ await this.scope.set(field, value);
323
+ return this.userLayer()?.[field] === value;
324
+ }
325
+ /** Write the staged key, then re-read whether the Host now holds one. */
326
+ async writeKey(value) {
327
+ try {
328
+ if (!(await this.api.credentials.set({
329
+ ref: this.credential.ref,
330
+ value
331
+ })).result.ok) return false;
332
+ } catch {
333
+ return false;
334
+ }
335
+ await this.readCredential();
336
+ return this.credential.configured;
337
+ }
338
+ /** Ask the credentials domain about the reference this page writes. */
339
+ async readCredential() {
340
+ const ref = this.credential.ref;
341
+ let response;
342
+ try {
343
+ response = await this.api.credentials.describe({ refs: [ref] });
344
+ } catch {
345
+ return;
346
+ }
347
+ if (!response.result.ok) return;
348
+ const view = response.result.value.credentials[ref];
349
+ const next = {
350
+ ref,
351
+ configured: view?.configured ?? false,
352
+ writable: view?.writable ?? true
353
+ };
354
+ if (next.configured === this.credential.configured && next.writable === this.credential.writable) return;
355
+ this.credential = next;
356
+ this.publish();
357
+ }
358
+ publish() {
359
+ if (this.disposed) return;
360
+ for (const listener of this.listeners) listener();
361
+ }
362
+ };
363
+ //#endregion
364
+ //#region src/client/section.tsx
365
+ /**
366
+ * React component for the "Command Code" settings page (browser half).
367
+ *
368
+ * Renders as a `settings.section` entry — a page at the same settings-nav
369
+ * level as General / Models / Plugins. The shell supplies the nav row and
370
+ * renders this body inside the content column. All copy comes from the
371
+ * `settings.commandcode` locale namespace; all state comes from the
372
+ * `CommandCodeSettingsController` injected by the slot registration.
373
+ *
374
+ * The layout mirrors the harness's settings pages: a max-width content
375
+ * column, labelled fields with hints, a reset affordance, and a
376
+ * save/discard footer. Styles are injected once by the client entry
377
+ * (see src/client/index.ts) and class-prefixed `cc-` to stay local.
378
+ */
379
+ /** One labelled field row in the page body. */
380
+ function Field({ id, label, hint, state, disabled, numeric, placeholder, onEdit, onReset, t }) {
381
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
+ className: "cc-field",
383
+ children: [
384
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
385
+ className: "cc-fieldHead",
386
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
387
+ className: "cc-label",
388
+ htmlFor: id,
389
+ children: label
390
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
391
+ className: "cc-badges",
392
+ children: [state.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
393
+ className: "cc-badge",
394
+ children: t("overridden")
395
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
396
+ type: "button",
397
+ className: "cc-reset",
398
+ disabled,
399
+ onClick: onReset,
400
+ children: t("reset")
401
+ })]
402
+ })]
403
+ }),
404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
405
+ id,
406
+ className: state.invalid ? "cc-input cc-inputInvalid" : "cc-input",
407
+ type: "text",
408
+ inputMode: numeric ? "numeric" : void 0,
409
+ value: state.text,
410
+ placeholder,
411
+ disabled,
412
+ onChange: (event) => onEdit(event.target.value)
413
+ }),
414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
415
+ className: state.invalid ? "cc-invalid" : "cc-hint",
416
+ children: state.invalid ? t("invalidNumber") : hint
417
+ })
418
+ ]
419
+ });
420
+ }
421
+ /** The API-key control: write-only, reports configured state, never echoes the key. */
422
+ function SecretKeyField({ label, hint, state, disabled, configured, configuredLabel, unconfiguredLabel, onEdit }) {
423
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
424
+ className: "cc-field",
425
+ children: [
426
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
427
+ className: "cc-fieldHead",
428
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
429
+ className: "cc-label",
430
+ htmlFor: "cc-api-key",
431
+ children: label
432
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
433
+ className: "cc-badges",
434
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
435
+ className: configured ? "cc-badge" : "cc-badgeMuted",
436
+ children: configured ? configuredLabel : unconfiguredLabel
437
+ })
438
+ })]
439
+ }),
440
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
441
+ id: "cc-api-key",
442
+ className: "cc-input",
443
+ type: "password",
444
+ autoComplete: "off",
445
+ value: state.text,
446
+ disabled,
447
+ onChange: (event) => onEdit(event.target.value)
448
+ }),
449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
450
+ className: "cc-hint",
451
+ children: hint
452
+ })
453
+ ]
454
+ });
455
+ }
456
+ /** The settings page body: connection facts for the Command Code provider. */
457
+ function CommandCodeSettingsPage(props) {
458
+ const { t } = props;
459
+ const state = props.useCommandCodeSettings((snapshot) => snapshot);
460
+ const disabled = !state.writable;
461
+ const keyLocked = !state.apiKeyWritable;
462
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
463
+ className: "cc-section",
464
+ "aria-label": t("title"),
465
+ children: [
466
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
467
+ className: "cc-title",
468
+ children: t("title")
469
+ }),
470
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
471
+ className: "cc-intro",
472
+ children: t("intro")
473
+ }),
474
+ !state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
475
+ className: "cc-readOnly",
476
+ role: "status",
477
+ children: t("readOnly")
478
+ }) : null,
479
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
480
+ className: "cc-card",
481
+ children: [
482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretKeyField, {
483
+ label: t("apiKey"),
484
+ hint: keyLocked ? t("apiKeyLocked") : t("apiKeyHint"),
485
+ state: state.apiKey,
486
+ disabled: disabled || keyLocked,
487
+ configured: state.apiKeyConfigured,
488
+ configuredLabel: t("apiKeySet"),
489
+ unconfiguredLabel: t("apiKeyUnset"),
490
+ onEdit: (text) => props.edit("apiKey", text)
491
+ }),
492
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
493
+ id: "cc-api-base",
494
+ label: t("apiBase"),
495
+ hint: t("apiBaseHint"),
496
+ state: state.apiBase,
497
+ disabled,
498
+ onEdit: (text) => props.edit("apiBase", text),
499
+ onReset: () => props.resetField("apiBase"),
500
+ t
501
+ }),
502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
503
+ id: "cc-working-dir",
504
+ label: t("workingDir"),
505
+ hint: t("workingDirHint"),
506
+ state: state.workingDir,
507
+ disabled,
508
+ placeholder: state.defaultWorkingDir,
509
+ onEdit: (text) => props.edit("workingDir", text),
510
+ onReset: () => props.resetField("workingDir"),
511
+ t
512
+ }),
513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
514
+ id: "cc-request-timeout",
515
+ label: t("requestTimeoutMs"),
516
+ hint: t("requestTimeoutMsHint"),
517
+ state: state.requestTimeoutMs,
518
+ disabled,
519
+ numeric: true,
520
+ onEdit: (text) => props.edit("requestTimeoutMs", text),
521
+ onReset: () => props.resetField("requestTimeoutMs"),
522
+ t
523
+ }),
524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
525
+ id: "cc-stream-idle-timeout",
526
+ label: t("streamIdleTimeoutMs"),
527
+ hint: t("streamIdleTimeoutMsHint"),
528
+ state: state.streamIdleTimeoutMs,
529
+ disabled,
530
+ numeric: true,
531
+ onEdit: (text) => props.edit("streamIdleTimeoutMs", text),
532
+ onReset: () => props.resetField("streamIdleTimeoutMs"),
533
+ t
534
+ })
535
+ ]
536
+ }),
537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
538
+ className: "cc-footer",
539
+ children: [
540
+ state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
541
+ className: "cc-failed",
542
+ role: "status",
543
+ children: t("saveFailed")
544
+ }) : null,
545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
546
+ variant: "ghost",
547
+ size: "sm",
548
+ disabled: !state.dirty || state.saving,
549
+ onClick: props.discard,
550
+ children: t("discard")
551
+ }),
552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
553
+ variant: "primary",
554
+ size: "sm",
555
+ disabled: !state.dirty || state.invalid || state.saving,
556
+ onClick: props.save,
557
+ children: t(state.saving ? "saving" : "save")
558
+ })
559
+ ]
560
+ })
561
+ ]
562
+ });
563
+ }
564
+ //#endregion
565
+ //#region src/client/locales.ts
566
+ const zh = {
567
+ nav: "Command Code",
568
+ title: "Command Code",
569
+ intro: "配置 Command Code Provider 连接。API 密钥仅保存在本机凭据服务中,不会回显;其他字段写入用户设置,下次请求即生效。",
570
+ apiKey: "API 密钥",
571
+ apiKeyHint: "在 commandcode.ai 控制台创建。留空保存不会覆盖已存储的密钥。",
572
+ apiKeySet: "已配置",
573
+ apiKeyUnset: "未配置",
574
+ apiKeyLocked: "密钥由只读来源提供",
575
+ apiBase: "API 地址",
576
+ apiBaseHint: "默认 https://api.commandcode.ai,一般无需修改。",
577
+ workingDir: "工作目录",
578
+ workingDirHint: "可选。留空时使用占位符显示的进程工作目录;仅在需要固定路径时填写。",
579
+ requestTimeoutMs: "请求超时(毫秒)",
580
+ requestTimeoutMsHint: "等待响应首个字节的超时;默认 60000。",
581
+ streamIdleTimeoutMs: "流空闲超时(毫秒)",
582
+ streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 120000。",
583
+ overridden: "已覆盖",
584
+ reset: "重置",
585
+ invalidNumber: "无效数字",
586
+ readOnly: "当前配置为只读。",
587
+ unsaved: "未保存",
588
+ save: "保存",
589
+ saving: "保存中",
590
+ saveFailed: "保存失败,请重试。",
591
+ discard: "放弃",
592
+ cancel: "取消"
593
+ };
594
+ const en = {
595
+ nav: "Command Code",
596
+ title: "Command Code",
597
+ intro: "Configure the Command Code Provider connection. The API key is stored only in the local credential service and never echoed; other fields are written to user settings and take effect on the next request.",
598
+ apiKey: "API key",
599
+ apiKeyHint: "Create one in the commandcode.ai console. Saving with this field blank keeps the stored key.",
600
+ apiKeySet: "Configured",
601
+ apiKeyUnset: "Not configured",
602
+ apiKeyLocked: "Key provided by a read-only source",
603
+ apiBase: "API base URL",
604
+ apiBaseHint: "Defaults to https://api.commandcode.ai; usually leave as-is.",
605
+ workingDir: "Working directory",
606
+ workingDirHint: "Optional. Leave blank to use the process cwd shown as the placeholder; fill in only to pin a specific path.",
607
+ requestTimeoutMs: "Request timeout (ms)",
608
+ requestTimeoutMsHint: "Time to wait for the first response byte; default 60000.",
609
+ streamIdleTimeoutMs: "Stream idle timeout (ms)",
610
+ streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 120000.",
611
+ overridden: "Overridden",
612
+ reset: "Reset",
613
+ invalidNumber: "Invalid number",
614
+ readOnly: "Settings are read-only.",
615
+ unsaved: "Unsaved",
616
+ save: "Save",
617
+ saving: "Saving",
618
+ saveFailed: "Save failed, please retry.",
619
+ discard: "Discard",
620
+ cancel: "Cancel"
621
+ };
622
+ //#endregion
623
+ //#region src/client/index.ts
624
+ /** CSS for the settings page, injected once (harness bundle convention). */
625
+ const PAGE_CSS = `
626
+ .cc-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}
627
+ .cc-title{margin:0;font-size:18px;font-weight:600}
628
+ .cc-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:1.5}
629
+ .cc-readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
630
+ .cc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:4px 16px}
631
+ .cc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
632
+ .cc-field+.cc-field{border-top:1px solid var(--dsw-alias-border-l2)}
633
+ .cc-fieldHead{align-items:center;gap:8px;display:flex}
634
+ .cc-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
635
+ .cc-badges{align-items:center;gap:8px;display:inline-flex}
636
+ .cc-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
637
+ .cc-badgeMuted{white-space:nowrap;color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px}
638
+ .cc-reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}
639
+ .cc-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
640
+ .cc-reset:disabled{cursor:default;opacity:.5}
641
+ .cc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}
642
+ .cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
643
+ .cc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
644
+ .cc-inputInvalid{border-color:var(--dsw-alias-label-error)}
645
+ .cc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
646
+ .cc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
647
+ .cc-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}
648
+ .cc-failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}
649
+ `;
650
+ /** Inject the page stylesheet once (idempotent per tag). */
651
+ function injectPageCss() {
652
+ if (typeof document === "undefined") return;
653
+ const id = "@mars-sea/dsh-commandcode-provider/CommandCodeSettingsPage.module.css";
654
+ if (document.querySelector(`style[data-plugin-css="${id}"]`) !== null) return;
655
+ const tag = document.createElement("style");
656
+ tag.dataset.plugin = "@mars-sea/dsh-commandcode-provider";
657
+ tag.dataset.pluginCss = id;
658
+ tag.textContent = PAGE_CSS;
659
+ document.head.appendChild(tag);
660
+ }
34
661
  /**
35
- * Client plugin body: install the selectModel wrapper on the connection's
36
- * shared api. `inject: ['connection']` gates activation until the connection
37
- * service is provided (the same pattern the harness's own client plugins
38
- * use), and `connection.api.sessions` is a stable object the model-selection
39
- * UI reads fresh on every call — so wrapping it once covers both the /model
40
- * popup and the composer seat, across reconnects.
662
+ * Client plugin body. Gates on the services the settings page needs
663
+ * (`slots`, `locale`, `connection`, `remote`, `settingsScope`) plus the
664
+ * `connection` used by the friendly-error wrapper the same inject list the
665
+ * harness's own settings-surface plugins declare.
41
666
  */
42
667
  function apply(ctx) {
668
+ injectPageCss();
43
669
  const connection = ctx.get("connection");
44
- if (connection === void 0) return;
45
- connection.api.sessions = withFriendlyImageError(connection.api.sessions);
670
+ if (connection !== void 0) installFriendlyImageError(connection);
671
+ ctx.effect(() => ctx.locale.register("settings.commandcode", {
672
+ zh,
673
+ en
674
+ }), "dsh-commandcode-provider: page copy");
675
+ const api = ctx.get("connection").api;
676
+ const hostDescription = ctx.get("connection").hostDescription;
677
+ const controller = new CommandCodeSettingsController(ctx.settingsScope.bind({ namespace: COMMANDCODE_NS }), { credentials: api.credentials }, hostDescription);
678
+ ctx.effect(() => () => controller.dispose(), "dsh-commandcode-provider: settings controller");
679
+ const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(controller.state());
680
+ controller.subscribe(() => store.set(controller.state()));
681
+ const injected = () => ({
682
+ hooks: { commandCodeSettings: store },
683
+ edit: (field, text) => controller.edit(field, text),
684
+ resetField: (field) => controller.resetField(field),
685
+ save: () => void controller.save(),
686
+ discard: () => controller.discard()
687
+ });
688
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
689
+ name: "settings.section",
690
+ id: "commandcode",
691
+ order: 12,
692
+ label: () => ctx.locale.bind("settings.commandcode")("nav"),
693
+ locale: "settings.commandcode",
694
+ inject: injected
695
+ }, CommandCodeSettingsPage));
46
696
  }
47
- const inject = ["connection"];
697
+ const inject = [
698
+ "slots",
699
+ "locale",
700
+ "connection",
701
+ "remote",
702
+ "settingsScope"
703
+ ];
48
704
  //#endregion
49
705
  exports.apply = apply;
50
706
  exports.inject = inject;