@mars-sea/dsh-commandcode-provider 0.10.3 → 0.10.4

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/index.js CHANGED
@@ -220,16 +220,38 @@ var CommandCodeAccountPool = class {
220
220
  until: 0
221
221
  });
222
222
  }
223
- /** One account's key: literal → credential seam → auth file (default slot). */
223
+ /**
224
+ * One account's key: literal → credential seam → auth file (default slot).
225
+ *
226
+ * Every source is normalized here, at the single point where a slot's key
227
+ * enters the pool. The adapter sends the key through the harness's
228
+ * `assertUsableApiKey()`, which trims it — a stored key from the credentials
229
+ * seam, a `.env` line, or a shell export all pick up surrounding whitespace
230
+ * — and reports that trimmed form back to `markRejected()`. Returning the
231
+ * raw value would file every 429/401 mark under a key no later lookup can
232
+ * find: rotation would re-offer the same account, the account card would show
233
+ * no mark, and the usage endpoints would 401 while chat kept working.
234
+ * Normalizing once makes resolution, probing, marking, and the request path
235
+ * agree on one string.
236
+ */
224
237
  async resolveSlotKey(slot) {
225
- if (slot.literal !== void 0 && slot.literal !== "") return slot.literal;
238
+ if (slot.literal !== void 0) {
239
+ const literal = normalizeResolvedKey(slot.literal);
240
+ if (literal !== void 0) return literal;
241
+ }
226
242
  if (slot.ref !== void 0) {
227
243
  const hit = await this.deps.resolveRef(slot.ref);
228
- if (hit !== void 0 && hit !== "") return hit;
244
+ if (hit !== void 0) {
245
+ const resolved = normalizeResolvedKey(hit);
246
+ if (resolved !== void 0) return resolved;
247
+ }
229
248
  }
230
249
  if (slot.allowAuthFile) {
231
250
  const fromFile = this.deps.authFileKey();
232
- if (fromFile !== void 0 && fromFile !== "") return fromFile;
251
+ if (fromFile !== void 0) {
252
+ const fileKey = normalizeResolvedKey(fromFile);
253
+ if (fileKey !== void 0) return fileKey;
254
+ }
233
255
  }
234
256
  }
235
257
  /** Hand out the chosen account's key. */
@@ -240,6 +262,16 @@ var CommandCodeAccountPool = class {
240
262
  };
241
263
  }
242
264
  };
265
+ /**
266
+ * Normalize one resolved credential to the form every consumer sees. Trim
267
+ * only: a blank-after-trim value means "no key" (the slot is omitted and the
268
+ * caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
269
+ * carry are left for `assertUsableApiKey()` to reject with its own message.
270
+ */
271
+ function normalizeResolvedKey(value) {
272
+ const trimmed = value.trim();
273
+ return trimmed === "" ? void 0 : trimmed;
274
+ }
243
275
  //#endregion
244
276
  //#region src/capabilities.ts
245
277
  /**
@@ -1207,8 +1239,65 @@ function wireToolCallIds(paired) {
1207
1239
  function blockText(block) {
1208
1240
  return block.type === "text" || block.type === "reasoning" ? block.text : "";
1209
1241
  }
1210
- function toolResultText(block) {
1211
- return block.content.map(blockText).filter(Boolean).join("\n");
1242
+ /**
1243
+ * Collect text and nested images of one tool result. The harness `read_image`
1244
+ * tool returns both, so dropping the image half is what made a tool-read image
1245
+ * invisible to a Vision model (issue #30). Deduplication keeps a result that
1246
+ * repeats one attachment from paying for the same pixels twice.
1247
+ */
1248
+ function toolResultMedia(block) {
1249
+ const chunks = [];
1250
+ const images = [];
1251
+ const seen = /* @__PURE__ */ new Set();
1252
+ const walk = (blocks) => {
1253
+ for (const nested of blocks) {
1254
+ if (nested.type === "text" || nested.type === "reasoning") {
1255
+ if (nested.text) chunks.push(nested.text);
1256
+ continue;
1257
+ }
1258
+ if (nested.type === "image") {
1259
+ if (!seen.has(nested.attachment.attachmentId)) {
1260
+ seen.add(nested.attachment.attachmentId);
1261
+ images.push(nested.attachment);
1262
+ }
1263
+ continue;
1264
+ }
1265
+ if (nested.type === "tool-result") walk(nested.content);
1266
+ }
1267
+ };
1268
+ walk(block.content);
1269
+ return {
1270
+ text: chunks.join("\n"),
1271
+ images
1272
+ };
1273
+ }
1274
+ /**
1275
+ * Result text for the wire's `role: 'tool'` message. A tool that returned only
1276
+ * an image (and possibly a nested image-only tool result) has no text at all,
1277
+ * and neither transport accepts an empty tool content string, so it gets a
1278
+ * descriptor pointing at the user message that follows with the pixels.
1279
+ */
1280
+ function toolResultTextForWire(media) {
1281
+ return media.text || (media.images.length > 0 ? "(image returned; see the attached image)" : "");
1282
+ }
1283
+ /** Leading line of the user message that carries a tool result's images. */
1284
+ const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result:";
1285
+ /**
1286
+ * The model-visible note introducing the images carried out of one tool
1287
+ * result. Neither transport merges them back into the tool result, so this
1288
+ * line is what tells the model the image it is about to see belongs to the
1289
+ * tool it just ran rather than to the user; it also leads the part list,
1290
+ * because an image-first content array is what some gateways reject. Count and
1291
+ * pixel dimensions are appended when the tool's own text does not already
1292
+ * state them (a result that returns the image and nothing else).
1293
+ */
1294
+ function toolResultImageNote(media) {
1295
+ const first = media.images[0];
1296
+ if (first === void 0) return TOOL_RESULT_IMAGE_TEXT;
1297
+ const dimensions = `${first.width}x${first.height} px`;
1298
+ if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return TOOL_RESULT_IMAGE_TEXT;
1299
+ const count = media.images.length > 1 ? `${media.images.length} images, ` : "";
1300
+ return `${TOOL_RESULT_IMAGE_TEXT} ${count}${dimensions}`;
1212
1301
  }
1213
1302
  function hasImageContent(message) {
1214
1303
  const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
@@ -1276,6 +1365,7 @@ async function messagesToCC(messages, readImage) {
1276
1365
  if (message.role === "user" && message.source.kind === "tool") {
1277
1366
  const block = message.content[0];
1278
1367
  if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
1368
+ const media = toolResultMedia(block);
1279
1369
  out.push({
1280
1370
  role: "tool",
1281
1371
  content: [{
@@ -1284,13 +1374,25 @@ async function messagesToCC(messages, readImage) {
1284
1374
  toolName: toolNames.get(block.toolCallId) || "unknown",
1285
1375
  output: block.isError ? {
1286
1376
  type: "error-text",
1287
- value: toolResultText(block)
1377
+ value: toolResultTextForWire(media)
1288
1378
  } : {
1289
1379
  type: "text",
1290
- value: toolResultText(block)
1380
+ value: toolResultTextForWire(media)
1291
1381
  }
1292
1382
  }]
1293
1383
  });
1384
+ if (media.images.length > 0) {
1385
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
1386
+ const carried = [{
1387
+ type: "text",
1388
+ text: toolResultImageNote(media)
1389
+ }];
1390
+ for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
1391
+ out.push({
1392
+ role: "user",
1393
+ content: carried
1394
+ });
1395
+ }
1294
1396
  }
1295
1397
  }
1296
1398
  return out;
@@ -1369,11 +1471,24 @@ async function messagesToOpenAI(messages, readImage) {
1369
1471
  if (message.role === "user" && message.source.kind === "tool") {
1370
1472
  const block = message.content[0];
1371
1473
  if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
1474
+ const media = toolResultMedia(block);
1372
1475
  out.push({
1373
1476
  role: "tool",
1374
1477
  tool_call_id: wireIds.get(block.toolCallId) ?? block.toolCallId,
1375
- content: toolResultText(block)
1478
+ content: toolResultTextForWire(media)
1376
1479
  });
1480
+ if (media.images.length > 0) {
1481
+ if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
1482
+ const carried = [{
1483
+ type: "text",
1484
+ text: toolResultImageNote(media)
1485
+ }];
1486
+ for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
1487
+ out.push({
1488
+ role: "user",
1489
+ content: carried
1490
+ });
1491
+ }
1377
1492
  }
1378
1493
  }
1379
1494
  return out;
@@ -3122,6 +3237,14 @@ var CommandCodeLoginFlow = class {
3122
3237
  timer;
3123
3238
  /** Settle hooks of the live attempt's callback promise. */
3124
3239
  settle;
3240
+ /**
3241
+ * Attempt generation. A delivered callback keeps validating the key
3242
+ * asynchronously (`complete()`), and that window is open to a cancel or a
3243
+ * fresh `begin()`; the generation lets a late completion recognize that it
3244
+ * no longer owns the status face and stop instead of storing a credential
3245
+ * the user cancelled and flipping the page back to success.
3246
+ */
3247
+ attemptSeq = 0;
3125
3248
  disposed = false;
3126
3249
  constructor(deps) {
3127
3250
  this.deps = deps;
@@ -3142,8 +3265,9 @@ var CommandCodeLoginFlow = class {
3142
3265
  */
3143
3266
  async begin() {
3144
3267
  if (this.disposed) throw new Error("login flow has been disposed");
3145
- if (this.statusValue.state === "waiting") return this.statusValue;
3268
+ if (this.statusValue.state === "waiting" && this.server !== void 0) return this.statusValue;
3146
3269
  this.teardown();
3270
+ const attempt = ++this.attemptSeq;
3147
3271
  const port = await this.findPort();
3148
3272
  const expectedState = this.deps.randomToken?.(32) ?? randomBytes(32).toString("base64url");
3149
3273
  const settled = new Promise((resolve, reject) => {
@@ -3171,7 +3295,7 @@ var CommandCodeLoginFlow = class {
3171
3295
  });
3172
3296
  }, this.deps.timeoutMs ?? 12e4);
3173
3297
  this.timer.unref?.();
3174
- settled.then((credentials) => this.complete(credentials), (failure) => this.failFrom(failure));
3298
+ settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
3175
3299
  return this.statusValue;
3176
3300
  }
3177
3301
  /** Cancel a waiting attempt; terminal states are untouched. */
@@ -3288,6 +3412,13 @@ var CommandCodeLoginFlow = class {
3288
3412
  }
3289
3413
  if (typeof payload === "object" && payload !== null && "error" in payload) {
3290
3414
  const denial = payload;
3415
+ if (denial.state !== expectedState) {
3416
+ json(403, {
3417
+ success: false,
3418
+ error: "Invalid state token"
3419
+ });
3420
+ return;
3421
+ }
3291
3422
  const description = denial.error_description ?? denial.error;
3292
3423
  this.settleAttempt(json, 200, { success: true }, new LoginSettleError(denial.error === "access_denied" ? "denied" : "error", typeof description === "string" && description !== "" ? description : "Authorization failed"));
3293
3424
  return;
@@ -3319,10 +3450,20 @@ var CommandCodeLoginFlow = class {
3319
3450
  if (failure !== void 0) settle.reject(failure);
3320
3451
  else if (credentials !== void 0) settle.resolve(credentials);
3321
3452
  }
3322
- /** Post-validation completion: whoami check, then hand-off to storage. */
3323
- async complete(credentials) {
3324
- if (this.disposed || this.statusValue.state !== "waiting") return;
3453
+ /**
3454
+ * Post-validation completion: whoami check, then hand-off to storage.
3455
+ *
3456
+ * Every step re-checks {@link ownsAttempt} first: the whoami round-trip and
3457
+ * the credential write are awaits, and the user may cancel (or start another
3458
+ * attempt) while one is in flight. A completion that no longer owns the
3459
+ * attempt must not write the key or publish a status — otherwise cancel
3460
+ * would report "cancelled" while the credential landed anyway, and the page
3461
+ * would silently flip to success.
3462
+ */
3463
+ async complete(attempt, credentials) {
3464
+ if (!this.ownsAttempt(attempt)) return;
3325
3465
  const validation = await validateCommandApiKey(this.deps.fetchImpl ?? fetch, this.readApiBase(), credentials.apiKey);
3466
+ if (!this.ownsAttempt(attempt)) return;
3326
3467
  if (!validation.valid) {
3327
3468
  const reason = validation.error === "invalid_key" ? "invalid-key" : validation.error === "network_error" ? "network" : "error";
3328
3469
  this.setStatus({
@@ -3332,9 +3473,11 @@ var CommandCodeLoginFlow = class {
3332
3473
  });
3333
3474
  return;
3334
3475
  }
3476
+ if (!this.ownsAttempt(attempt)) return;
3335
3477
  try {
3336
3478
  await this.deps.storeKey(credentials);
3337
3479
  } catch (error) {
3480
+ if (!this.ownsAttempt(attempt)) return;
3338
3481
  this.setStatus({
3339
3482
  state: "failed",
3340
3483
  reason: "unavailable",
@@ -3342,7 +3485,7 @@ var CommandCodeLoginFlow = class {
3342
3485
  });
3343
3486
  return;
3344
3487
  }
3345
- if (this.disposed) return;
3488
+ if (!this.ownsAttempt(attempt)) return;
3346
3489
  this.clearTimer();
3347
3490
  this.setStatus({
3348
3491
  state: "success",
@@ -3350,10 +3493,14 @@ var CommandCodeLoginFlow = class {
3350
3493
  keyName: credentials.keyName
3351
3494
  });
3352
3495
  }
3496
+ /** Whether one attempt still owns the status face (not cancelled, replaced, or disposed). */
3497
+ ownsAttempt(attempt) {
3498
+ return !this.disposed && this.attemptSeq === attempt && this.statusValue.state === "waiting";
3499
+ }
3353
3500
  /** Map a tagged settle rejection onto the status face. */
3354
- failFrom(failure) {
3501
+ failFrom(attempt, failure) {
3355
3502
  if (!(failure instanceof LoginSettleError)) return;
3356
- if (this.disposed || this.statusValue.state !== "waiting") return;
3503
+ if (!this.ownsAttempt(attempt)) return;
3357
3504
  this.setStatus({
3358
3505
  state: "failed",
3359
3506
  reason: failure.reason,
@@ -3634,6 +3781,244 @@ var CommandCodeSearchProvider = class {
3634
3781
  }
3635
3782
  };
3636
3783
  //#endregion
3784
+ //#region src/tui-settings.ts
3785
+ /** The selector value meaning "no pinned account — follow rotation order". */
3786
+ const ACTIVE_ACCOUNT_AUTO = "auto";
3787
+ /** The selector value meaning "no language override — follow the shell locale". */
3788
+ const LANG_AUTO = "auto";
3789
+ /**
3790
+ * Build the section descriptor. Pure, so tests can pin the exact fields
3791
+ * without a dsh-TUI host.
3792
+ *
3793
+ * Field choices worth keeping: the two option-bearing fields (`activeAccount`,
3794
+ * `lang`) are `text` + `options` rather than `select`, because a `select`
3795
+ * cannot express "unset" — cycling only ever lands on a declared option, so a
3796
+ * `select` would strand the user on a pinned value with no way back to
3797
+ * automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
3798
+ * unset state reachable. The two booleans format their EFFECTIVE default
3799
+ * (`filterModelsByPlan` unset means true at the adapter), so a fresh install
3800
+ * reads true instead of the screen's "(empty)".
3801
+ */
3802
+ function buildCommandCodeTuiSection(deps) {
3803
+ const ref = deps.apiKeyRef();
3804
+ const slots = deps.accountSlots();
3805
+ return {
3806
+ ns: deps.ns,
3807
+ title: deps.title ?? "Command Code",
3808
+ descriptions: { zh: "Command Code(非官方)" },
3809
+ groups: [
3810
+ {
3811
+ id: "connection",
3812
+ title: "Connection",
3813
+ descriptions: { zh: "连接" }
3814
+ },
3815
+ {
3816
+ id: "models",
3817
+ title: "Models",
3818
+ descriptions: { zh: "模型" }
3819
+ },
3820
+ {
3821
+ id: "advanced",
3822
+ title: "Advanced",
3823
+ descriptions: { zh: "高级" }
3824
+ }
3825
+ ],
3826
+ fields: [
3827
+ {
3828
+ path: ["apiKey"],
3829
+ group: "connection",
3830
+ kind: "text",
3831
+ label: "API key",
3832
+ descriptions: { zh: "API 密钥" },
3833
+ secret: { ref },
3834
+ hint: `Stored in the credential store as ${ref}, never in settings.yaml.`,
3835
+ hintDescriptions: { zh: `保存在凭据库(${ref}),不会写入 settings.yaml。` }
3836
+ },
3837
+ {
3838
+ path: ["apiBase"],
3839
+ group: "connection",
3840
+ kind: "text",
3841
+ label: "API base",
3842
+ descriptions: { zh: "API 地址" },
3843
+ placeholder: "https://api.commandcode.ai",
3844
+ hint: "Leave empty for the public Command Code Provider API.",
3845
+ hintDescriptions: { zh: "留空即使用官方 Command Code Provider API。" },
3846
+ parse: (text) => {
3847
+ const trimmed = text.trim();
3848
+ return trimmed === "" ? { kind: "clear" } : {
3849
+ kind: "set",
3850
+ value: trimmed
3851
+ };
3852
+ }
3853
+ },
3854
+ {
3855
+ path: ["filterModelsByPlan"],
3856
+ group: "models",
3857
+ kind: "boolean",
3858
+ label: "Hide out-of-plan models",
3859
+ descriptions: { zh: "隐藏套餐外的模型" },
3860
+ hint: "Keeps models above your subscription tier out of the picker. Fails open.",
3861
+ hintDescriptions: { zh: "在选择器里隐藏超出当前订阅档位的模型;判断不出来时全部显示。" },
3862
+ format: (value) => value === false ? "false" : "true",
3863
+ parse: (text) => ({
3864
+ kind: "set",
3865
+ value: text.trim() === "true"
3866
+ })
3867
+ },
3868
+ {
3869
+ path: ["visibleModels"],
3870
+ group: "models",
3871
+ kind: "text",
3872
+ label: "Visible models",
3873
+ descriptions: { zh: "显示的模型" },
3874
+ hint: "Comma-separated catalog ids; empty shows every model.",
3875
+ hintDescriptions: { zh: "以逗号分隔的模型 id;留空表示显示全部模型。" },
3876
+ format: (value) => (Array.isArray(value) ? value.filter((id) => typeof id === "string" && id !== "") : []).join(", "),
3877
+ parse: (text) => {
3878
+ const ids = text.split(",").map((id) => id.trim()).filter((id) => id !== "");
3879
+ return ids.length === 0 ? { kind: "clear" } : {
3880
+ kind: "set",
3881
+ value: ids
3882
+ };
3883
+ }
3884
+ },
3885
+ {
3886
+ path: ["activeAccount"],
3887
+ group: "advanced",
3888
+ kind: "text",
3889
+ label: "Active account",
3890
+ descriptions: { zh: "当前账号" },
3891
+ hint: "A pinned account id, or auto to follow the rotation order.",
3892
+ hintDescriptions: { zh: "固定使用某个账号的 id;auto 表示按轮换顺序自动选择。" },
3893
+ options: [{
3894
+ value: ACTIVE_ACCOUNT_AUTO,
3895
+ label: "Automatic (rotation order)",
3896
+ descriptions: { zh: "自动(按轮换顺序)" }
3897
+ }, ...slots.map((slot) => ({
3898
+ value: slot.id,
3899
+ label: slot.label,
3900
+ descriptions: { zh: slot.label }
3901
+ }))],
3902
+ format: (value) => typeof value === "string" && value.trim() !== "" ? value : ACTIVE_ACCOUNT_AUTO,
3903
+ parse: (text) => {
3904
+ const trimmed = text.trim();
3905
+ return trimmed === "" || trimmed === "auto" ? { kind: "clear" } : {
3906
+ kind: "set",
3907
+ value: trimmed
3908
+ };
3909
+ }
3910
+ },
3911
+ {
3912
+ path: ["lang"],
3913
+ group: "advanced",
3914
+ kind: "text",
3915
+ label: "Command language",
3916
+ descriptions: { zh: "命令语言" },
3917
+ hint: "Language of the /commandcode dashboard; auto follows the shell locale.",
3918
+ hintDescriptions: { zh: "/commandcode 用量面板的语言;auto 跟随终端 locale。" },
3919
+ options: [
3920
+ {
3921
+ value: LANG_AUTO,
3922
+ label: "Automatic (shell locale)",
3923
+ descriptions: { zh: "自动(跟随终端 locale)" }
3924
+ },
3925
+ {
3926
+ value: "zh",
3927
+ label: "中文"
3928
+ },
3929
+ {
3930
+ value: "en",
3931
+ label: "English"
3932
+ }
3933
+ ],
3934
+ format: (value) => value === "zh" || value === "en" ? value : LANG_AUTO,
3935
+ parse: (text) => {
3936
+ const trimmed = text.trim();
3937
+ return trimmed === "" || trimmed === "auto" ? { kind: "clear" } : {
3938
+ kind: "set",
3939
+ value: trimmed
3940
+ };
3941
+ }
3942
+ }
3943
+ ]
3944
+ };
3945
+ }
3946
+ /**
3947
+ * The `tuiSettingsSections` service, read defensively.
3948
+ *
3949
+ * The service name is declared by dsh-TUI's own module augmentation, which
3950
+ * this package deliberately does not import, so the typed `Context` has no
3951
+ * such property. The read goes through the REFLECTIVE `ctx.get` rather than a
3952
+ * bare property access: cordis refuses a property read for a service the fiber
3953
+ * never declared in `inject` (`cannot get property … without inject`), and an
3954
+ * unmeet seam must degrade to "no section" instead of throwing out of the
3955
+ * plugin's boot.
3956
+ */
3957
+ function tuiSettingsService(ctx) {
3958
+ let candidate;
3959
+ try {
3960
+ candidate = ctx.get("tuiSettingsSections");
3961
+ } catch {
3962
+ return;
3963
+ }
3964
+ if (typeof candidate !== "object" || candidate === null) return void 0;
3965
+ const register = candidate.register;
3966
+ if (typeof register !== "function") return void 0;
3967
+ return { register: register.bind(candidate) };
3968
+ }
3969
+ /**
3970
+ * Identity of everything in the section that can change at runtime: the
3971
+ * credential reference behind the API-key field and the account slots behind
3972
+ * the active-account selector. Re-registration is skipped while this matches,
3973
+ * so ordinary settings writes never churn the screen's section list.
3974
+ */
3975
+ function sectionSignature(section) {
3976
+ const active = section.fields.find((field) => field.path.join(".") === "activeAccount");
3977
+ return JSON.stringify({
3978
+ secret: section.fields.find((field) => field.secret !== void 0)?.secret?.ref ?? "",
3979
+ options: active?.options?.map((option) => option.value) ?? []
3980
+ });
3981
+ }
3982
+ /**
3983
+ * Register the Command Code section on a dsh-TUI host.
3984
+ *
3985
+ * @param ctx - the context of an activated `tuiSettingsSections` injection.
3986
+ * @param deps - plugin-owned facts the section reads.
3987
+ * @returns a refresh function that re-registers the section when a fact it
3988
+ * renders changed (the plugin entry calls it from its settings `onChange`
3989
+ * hook), or `undefined` when the seam is unusable. The returned function is
3990
+ * inert after the fiber is torn down.
3991
+ */
3992
+ function applyCommandCodeTuiSettings(ctx, deps) {
3993
+ const service = tuiSettingsService(ctx);
3994
+ if (service === void 0) return void 0;
3995
+ let disposed = false;
3996
+ let current;
3997
+ const refresh = () => {
3998
+ if (disposed) return;
3999
+ const section = buildCommandCodeTuiSection(deps);
4000
+ const signature = sectionSignature(section);
4001
+ if (current?.signature === signature) return;
4002
+ current?.dispose();
4003
+ current = void 0;
4004
+ try {
4005
+ current = {
4006
+ signature,
4007
+ dispose: service.register(section)
4008
+ };
4009
+ } catch (error) {
4010
+ ctx.logger?.warn(`llm-commandcode: could not register the dsh-TUI settings section: ${error instanceof Error ? error.message : String(error)}`);
4011
+ }
4012
+ };
4013
+ refresh();
4014
+ ctx.effect(() => () => {
4015
+ disposed = true;
4016
+ current?.dispose();
4017
+ current = void 0;
4018
+ }, "dsh-commandcode-provider: tui settings section");
4019
+ return refresh;
4020
+ }
4021
+ //#endregion
3637
4022
  //#region src/index.ts
3638
4023
  /**
3639
4024
  * dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
@@ -3672,7 +4057,7 @@ const PROVIDER = "commandcode";
3672
4057
  const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
3673
4058
  const Config = z.object({
3674
4059
  apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
3675
- apiKey: z.string(),
4060
+ apiKey: z.string().role("secret"),
3676
4061
  apiBase: z.string(),
3677
4062
  workingDir: z.string(),
3678
4063
  modelsCachePath: z.string(),
@@ -3684,7 +4069,8 @@ const Config = z.object({
3684
4069
  accounts: z.array(z.object({
3685
4070
  label: z.string(),
3686
4071
  apiKeyEnv: z.string().role("credential-ref"),
3687
- apiKey: z.string()
4072
+ /** Literal key for one extra slot; see the top-level `apiKey` secret note. */
4073
+ apiKey: z.string().role("secret")
3688
4074
  })),
3689
4075
  activeAccount: z.string(),
3690
4076
  modelAccountRules: z.array(z.object({
@@ -3874,6 +4260,20 @@ function apply(ctx, config) {
3874
4260
  applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
3875
4261
  }, "dsh-commandcode-provider: web search selection");
3876
4262
  });
4263
+ let refreshTuiSettings;
4264
+ ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
4265
+ refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
4266
+ ns: NS,
4267
+ apiKeyRef: () => current().apiKeyEnv ?? DEFAULT_API_KEY_ENV,
4268
+ accountSlots: () => slots().map((slot) => ({
4269
+ id: slot.id,
4270
+ label: slot.label
4271
+ }))
4272
+ });
4273
+ tuiCtx.effect(() => () => {
4274
+ refreshTuiSettings = void 0;
4275
+ }, "dsh-commandcode-provider: tui settings handle");
4276
+ });
3877
4277
  ctx.inject(["settings"], (settingsCtx) => {
3878
4278
  settingsCtx.settings.installSection(ctx, NS, Config, config, {
3879
4279
  setSource: (source) => {
@@ -3881,11 +4281,12 @@ function apply(ctx, config) {
3881
4281
  },
3882
4282
  onChange: () => {
3883
4283
  applySearchSelection(current().webSearch ?? true);
4284
+ refreshTuiSettings?.();
3884
4285
  }
3885
4286
  });
3886
4287
  });
3887
4288
  }
3888
4289
  //#endregion
3889
- export { BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommands, applyUsageRemote, buildCommandAuthUrl, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
4290
+ export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
3890
4291
 
3891
4292
  //# sourceMappingURL=index.js.map