@mars-sea/dsh-commandcode-provider 0.10.4 → 0.10.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/CHANGELOG.md +30 -0
- package/README.md +26 -5
- package/README.zh-CN.md +24 -4
- package/lib/client.js +1 -1
- package/lib/index.d.ts +49 -4
- package/lib/index.js +320 -46
- package/lib/index.js.map +1 -1
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -1024,7 +1024,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
1024
1024
|
* and API key or subscription, and Command Code's terms apply.
|
|
1025
1025
|
*
|
|
1026
1026
|
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
|
|
1027
|
-
* re-verified against command-code@1.53.
|
|
1027
|
+
* re-verified against command-code@1.53.1 — endpoints, request shape, and
|
|
1028
1028
|
* stream events unchanged):
|
|
1029
1029
|
* POST {apiBase}/alpha/generate
|
|
1030
1030
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
@@ -1037,7 +1037,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
1037
1037
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
1038
1038
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
1039
1039
|
*/
|
|
1040
|
-
const COMMAND_CODE_CLI_VERSION = "1.53.
|
|
1040
|
+
const COMMAND_CODE_CLI_VERSION = "1.53.1";
|
|
1041
1041
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
1042
1042
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
1043
1043
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -1105,6 +1105,124 @@ function recordOrEmpty(value) {
|
|
|
1105
1105
|
} catch {}
|
|
1106
1106
|
return {};
|
|
1107
1107
|
}
|
|
1108
|
+
/** How deep a root `$ref` / combinator chain is followed while normalizing. */
|
|
1109
|
+
const SCHEMA_NORMALIZE_MAX_DEPTH = 4;
|
|
1110
|
+
/**
|
|
1111
|
+
* Whether a type-less node is object-shaped enough to be one with the type
|
|
1112
|
+
* declared: `properties`/`required`/`additionalProperties` only make sense on
|
|
1113
|
+
* an object, and a schema carrying them was meant to be one.
|
|
1114
|
+
*/
|
|
1115
|
+
function isObjectShaped(node) {
|
|
1116
|
+
return isRecord(node.properties) || isRecord(node.patternProperties) || Array.isArray(node.required) || node.additionalProperties !== void 0;
|
|
1117
|
+
}
|
|
1118
|
+
/** The `required` names of one schema node, ignoring malformed entries. */
|
|
1119
|
+
function requiredNames(node) {
|
|
1120
|
+
return Array.isArray(node.required) ? node.required.filter((name) => typeof name === "string") : [];
|
|
1121
|
+
}
|
|
1122
|
+
/** Resolve a local `#/...` pointer inside the schema that carried it. */
|
|
1123
|
+
function resolveLocalRef(root, ref) {
|
|
1124
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
1125
|
+
let node = root;
|
|
1126
|
+
for (const rawSegment of ref.slice(2).split("/")) {
|
|
1127
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1128
|
+
if (!isRecord(node)) return void 0;
|
|
1129
|
+
node = node[segment];
|
|
1130
|
+
}
|
|
1131
|
+
return isRecord(node) ? node : void 0;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Flatten a type-less combinator node into one object schema, so the model
|
|
1135
|
+
* still sees the branch fields instead of an argument-free tool. `allOf`
|
|
1136
|
+
* branches must all hold, so their `required` entries are all kept; `anyOf` /
|
|
1137
|
+
* `oneOf` branches are alternatives, so only a name required by every branch
|
|
1138
|
+
* survives. Returns undefined when no branch describes an object.
|
|
1139
|
+
*/
|
|
1140
|
+
function mergeCombinatorBranches(node, depth) {
|
|
1141
|
+
if (depth >= SCHEMA_NORMALIZE_MAX_DEPTH) return void 0;
|
|
1142
|
+
const properties = {};
|
|
1143
|
+
const required = /* @__PURE__ */ new Set();
|
|
1144
|
+
const alternatives = [];
|
|
1145
|
+
let sawBranch = false;
|
|
1146
|
+
for (const key of [
|
|
1147
|
+
"allOf",
|
|
1148
|
+
"anyOf",
|
|
1149
|
+
"oneOf"
|
|
1150
|
+
]) {
|
|
1151
|
+
const branches = node[key];
|
|
1152
|
+
if (!Array.isArray(branches)) continue;
|
|
1153
|
+
const objectBranches = [];
|
|
1154
|
+
for (const branch of branches) {
|
|
1155
|
+
if (!isRecord(branch)) continue;
|
|
1156
|
+
objectBranches.push(toolParametersSchema(branch, depth + 1));
|
|
1157
|
+
}
|
|
1158
|
+
if (objectBranches.length === 0) continue;
|
|
1159
|
+
sawBranch = true;
|
|
1160
|
+
const names = objectBranches.map(requiredNames);
|
|
1161
|
+
if (key === "allOf") for (const name of names.flat()) required.add(name);
|
|
1162
|
+
else if (names.length > 0) alternatives.push(names[0].filter((name) => names.every((list) => list.includes(name))));
|
|
1163
|
+
for (const branch of objectBranches) {
|
|
1164
|
+
const branchProperties = isRecord(branch.properties) ? branch.properties : {};
|
|
1165
|
+
for (const [name, schema] of Object.entries(branchProperties)) if (!(name in properties)) properties[name] = schema;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (!sawBranch) return void 0;
|
|
1169
|
+
for (const group of alternatives) for (const name of group) required.add(name);
|
|
1170
|
+
const merged = {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties
|
|
1173
|
+
};
|
|
1174
|
+
if (required.size > 0) merged.required = [...required];
|
|
1175
|
+
if (node.additionalProperties !== void 0) merged.additionalProperties = node.additionalProperties;
|
|
1176
|
+
for (const key of ["description", "title"]) if (node[key] !== void 0) merged[key] = node[key];
|
|
1177
|
+
return merged;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Normalize one tool's parameters schema to an object-rooted JSON Schema.
|
|
1181
|
+
* A schema that already declares an object root is passed through untouched;
|
|
1182
|
+
* a type-less object-shaped one gains the type; a root `$ref` or combinator is
|
|
1183
|
+
* resolved/merged; anything else (an array/scalar root, or no schema at all)
|
|
1184
|
+
* degrades to a permissive free-form object, because a request the provider
|
|
1185
|
+
* refuses helps no one and the tool's own description is still in the prompt.
|
|
1186
|
+
*/
|
|
1187
|
+
function toolParametersSchema(parameters, depth = 0) {
|
|
1188
|
+
if (!isRecord(parameters)) return {
|
|
1189
|
+
type: "object",
|
|
1190
|
+
properties: {},
|
|
1191
|
+
additionalProperties: true
|
|
1192
|
+
};
|
|
1193
|
+
if (parameters.type === "object") return parameters;
|
|
1194
|
+
if (Array.isArray(parameters.type) && parameters.type.includes("object")) return {
|
|
1195
|
+
...parameters,
|
|
1196
|
+
type: "object"
|
|
1197
|
+
};
|
|
1198
|
+
if (parameters.type === void 0 || parameters.type === null) {
|
|
1199
|
+
if (typeof parameters.$ref === "string" && depth < SCHEMA_NORMALIZE_MAX_DEPTH) {
|
|
1200
|
+
const target = resolveLocalRef(parameters, parameters.$ref);
|
|
1201
|
+
if (target !== void 0) {
|
|
1202
|
+
const { $ref: _ref, ...rest } = parameters;
|
|
1203
|
+
return toolParametersSchema({
|
|
1204
|
+
...target,
|
|
1205
|
+
...rest
|
|
1206
|
+
}, depth + 1);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (isObjectShaped(parameters)) return {
|
|
1210
|
+
...parameters,
|
|
1211
|
+
type: "object"
|
|
1212
|
+
};
|
|
1213
|
+
const merged = mergeCombinatorBranches(parameters, depth);
|
|
1214
|
+
if (merged !== void 0) return merged;
|
|
1215
|
+
if (typeof parameters.$ref === "string") return {
|
|
1216
|
+
...parameters,
|
|
1217
|
+
type: "object"
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
type: "object",
|
|
1222
|
+
properties: {},
|
|
1223
|
+
additionalProperties: true
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1108
1226
|
function projectSlugFromPath(pathName) {
|
|
1109
1227
|
return pathName.toLowerCase().replace(/^[a-z]:/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|(?<!-)-+$/g, "") || "project";
|
|
1110
1228
|
}
|
|
@@ -1281,23 +1399,33 @@ function toolResultTextForWire(media) {
|
|
|
1281
1399
|
return media.text || (media.images.length > 0 ? "(image returned; see the attached image)" : "");
|
|
1282
1400
|
}
|
|
1283
1401
|
/** 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
|
|
1402
|
+
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result";
|
|
1285
1403
|
/**
|
|
1286
1404
|
* The model-visible note introducing the images carried out of one tool
|
|
1287
1405
|
* result. Neither transport merges them back into the tool result, so this
|
|
1288
1406
|
* line is what tells the model the image it is about to see belongs to the
|
|
1289
1407
|
* 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.
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1408
|
+
* because an image-first content array is what some gateways reject.
|
|
1409
|
+
*
|
|
1410
|
+
* The note names the tool call the image came out of, because a turn's
|
|
1411
|
+
* carriers are emitted as a group after the whole tool group (issue #33):
|
|
1412
|
+
* position alone would have to carry the association, and every `read_image`
|
|
1413
|
+
* result renders the same envelope text, so two parallel calls would produce
|
|
1414
|
+
* two identical notes. The id is the WIRE id — the one the model saw on the
|
|
1415
|
+
* `tool-call` it issued and on the tool message just above — so an overlong
|
|
1416
|
+
* cross-provider id is named by the alias that replaced it, not by the
|
|
1417
|
+
* harness-side id the model never saw.
|
|
1418
|
+
*
|
|
1419
|
+
* Count and pixel dimensions are appended when the tool's own text does not
|
|
1420
|
+
* already state them (a result that returns the image and nothing else).
|
|
1293
1421
|
*/
|
|
1294
|
-
function toolResultImageNote(media) {
|
|
1422
|
+
function toolResultImageNote(media, toolCallId) {
|
|
1423
|
+
const lead = `${TOOL_RESULT_IMAGE_TEXT} (${toolCallId}):`;
|
|
1295
1424
|
const first = media.images[0];
|
|
1296
|
-
if (first === void 0) return
|
|
1425
|
+
if (first === void 0) return lead;
|
|
1297
1426
|
const dimensions = `${first.width}x${first.height} px`;
|
|
1298
|
-
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return
|
|
1299
|
-
|
|
1300
|
-
return `${TOOL_RESULT_IMAGE_TEXT} ${count}${dimensions}`;
|
|
1427
|
+
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return lead;
|
|
1428
|
+
return `${lead} ${media.images.length > 1 ? `${media.images.length} images, ` : ""}${dimensions}`;
|
|
1301
1429
|
}
|
|
1302
1430
|
function hasImageContent(message) {
|
|
1303
1431
|
const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
|
|
@@ -1324,9 +1452,15 @@ async function messagesToCC(messages, readImage) {
|
|
|
1324
1452
|
const out = [];
|
|
1325
1453
|
const { ids: paired, names: toolNames } = pairedToolCalls(messages);
|
|
1326
1454
|
const wireIds = wireToolCallIds(paired);
|
|
1455
|
+
const pendingImages = [];
|
|
1456
|
+
const flushPendingImages = () => {
|
|
1457
|
+
for (const message of pendingImages) out.push(message);
|
|
1458
|
+
pendingImages.length = 0;
|
|
1459
|
+
};
|
|
1327
1460
|
for (const message of messages) {
|
|
1328
1461
|
if (message.role === "system") continue;
|
|
1329
1462
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
1463
|
+
flushPendingImages();
|
|
1330
1464
|
const parts = [];
|
|
1331
1465
|
for (const block of message.content) {
|
|
1332
1466
|
if (block.type === "text") parts.push({
|
|
@@ -1338,6 +1472,7 @@ async function messagesToCC(messages, readImage) {
|
|
|
1338
1472
|
parts.push(await imageToCommandCode(block.attachment, readImage));
|
|
1339
1473
|
}
|
|
1340
1474
|
}
|
|
1475
|
+
if (parts.length === 0) continue;
|
|
1341
1476
|
out.push({
|
|
1342
1477
|
role: "user",
|
|
1343
1478
|
content: parts
|
|
@@ -1345,11 +1480,16 @@ async function messagesToCC(messages, readImage) {
|
|
|
1345
1480
|
continue;
|
|
1346
1481
|
}
|
|
1347
1482
|
if (message.role === "assistant") {
|
|
1483
|
+
flushPendingImages();
|
|
1348
1484
|
const parts = [];
|
|
1349
1485
|
for (const block of message.content) if (block.type === "text") parts.push({
|
|
1350
1486
|
type: "text",
|
|
1351
1487
|
text: block.text
|
|
1352
1488
|
});
|
|
1489
|
+
else if (block.type === "reasoning") parts.push({
|
|
1490
|
+
type: "reasoning",
|
|
1491
|
+
text: block.text
|
|
1492
|
+
});
|
|
1353
1493
|
else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
|
|
1354
1494
|
type: "tool-call",
|
|
1355
1495
|
toolCallId: wireIds.get(block.id) ?? block.id,
|
|
@@ -1366,11 +1506,12 @@ async function messagesToCC(messages, readImage) {
|
|
|
1366
1506
|
const block = message.content[0];
|
|
1367
1507
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1368
1508
|
const media = toolResultMedia(block);
|
|
1509
|
+
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
1369
1510
|
out.push({
|
|
1370
1511
|
role: "tool",
|
|
1371
1512
|
content: [{
|
|
1372
1513
|
type: "tool-result",
|
|
1373
|
-
toolCallId:
|
|
1514
|
+
toolCallId: wireToolCallId,
|
|
1374
1515
|
toolName: toolNames.get(block.toolCallId) || "unknown",
|
|
1375
1516
|
output: block.isError ? {
|
|
1376
1517
|
type: "error-text",
|
|
@@ -1385,16 +1526,17 @@ async function messagesToCC(messages, readImage) {
|
|
|
1385
1526
|
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1386
1527
|
const carried = [{
|
|
1387
1528
|
type: "text",
|
|
1388
|
-
text: toolResultImageNote(media)
|
|
1529
|
+
text: toolResultImageNote(media, wireToolCallId)
|
|
1389
1530
|
}];
|
|
1390
1531
|
for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
|
|
1391
|
-
|
|
1532
|
+
pendingImages.push({
|
|
1392
1533
|
role: "user",
|
|
1393
1534
|
content: carried
|
|
1394
1535
|
});
|
|
1395
1536
|
}
|
|
1396
1537
|
}
|
|
1397
1538
|
}
|
|
1539
|
+
flushPendingImages();
|
|
1398
1540
|
return out;
|
|
1399
1541
|
}
|
|
1400
1542
|
/**
|
|
@@ -1422,9 +1564,15 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1422
1564
|
const out = [];
|
|
1423
1565
|
const { ids: paired } = pairedToolCalls(messages);
|
|
1424
1566
|
const wireIds = wireToolCallIds(paired);
|
|
1567
|
+
const pendingImages = [];
|
|
1568
|
+
const flushPendingImages = () => {
|
|
1569
|
+
for (const message of pendingImages) out.push(message);
|
|
1570
|
+
pendingImages.length = 0;
|
|
1571
|
+
};
|
|
1425
1572
|
for (const message of messages) {
|
|
1426
1573
|
if (message.role === "system") continue;
|
|
1427
1574
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
1575
|
+
flushPendingImages();
|
|
1428
1576
|
const parts = [];
|
|
1429
1577
|
for (const block of message.content) {
|
|
1430
1578
|
if (block.type === "text") parts.push({
|
|
@@ -1448,6 +1596,7 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1448
1596
|
continue;
|
|
1449
1597
|
}
|
|
1450
1598
|
if (message.role === "assistant") {
|
|
1599
|
+
flushPendingImages();
|
|
1451
1600
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1452
1601
|
const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
1453
1602
|
const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
|
|
@@ -1472,25 +1621,27 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1472
1621
|
const block = message.content[0];
|
|
1473
1622
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1474
1623
|
const media = toolResultMedia(block);
|
|
1624
|
+
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
1475
1625
|
out.push({
|
|
1476
1626
|
role: "tool",
|
|
1477
|
-
tool_call_id:
|
|
1627
|
+
tool_call_id: wireToolCallId,
|
|
1478
1628
|
content: toolResultTextForWire(media)
|
|
1479
1629
|
});
|
|
1480
1630
|
if (media.images.length > 0) {
|
|
1481
1631
|
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1482
1632
|
const carried = [{
|
|
1483
1633
|
type: "text",
|
|
1484
|
-
text: toolResultImageNote(media)
|
|
1634
|
+
text: toolResultImageNote(media, wireToolCallId)
|
|
1485
1635
|
}];
|
|
1486
1636
|
for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
|
|
1487
|
-
|
|
1637
|
+
pendingImages.push({
|
|
1488
1638
|
role: "user",
|
|
1489
1639
|
content: carried
|
|
1490
1640
|
});
|
|
1491
1641
|
}
|
|
1492
1642
|
}
|
|
1493
1643
|
}
|
|
1644
|
+
flushPendingImages();
|
|
1494
1645
|
return out;
|
|
1495
1646
|
}
|
|
1496
1647
|
/** Account endpoints fetched by one `getUsage()` run (see the classification there). */
|
|
@@ -1595,7 +1746,7 @@ async function buildCliBody(options, connection, facts) {
|
|
|
1595
1746
|
type: "function",
|
|
1596
1747
|
name: tool.name,
|
|
1597
1748
|
description: tool.description,
|
|
1598
|
-
input_schema: tool.parameters
|
|
1749
|
+
input_schema: toolParametersSchema(tool.parameters)
|
|
1599
1750
|
})),
|
|
1600
1751
|
system: facts.systemText,
|
|
1601
1752
|
max_tokens: facts.maxTokens,
|
|
@@ -1617,7 +1768,7 @@ async function buildOpenAIBody(options, facts) {
|
|
|
1617
1768
|
function: {
|
|
1618
1769
|
name: tool.name,
|
|
1619
1770
|
description: tool.description,
|
|
1620
|
-
parameters: tool.parameters
|
|
1771
|
+
parameters: toolParametersSchema(tool.parameters)
|
|
1621
1772
|
}
|
|
1622
1773
|
}));
|
|
1623
1774
|
return {
|
|
@@ -1875,7 +2026,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1875
2026
|
const access = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadBillingAccess();
|
|
1876
2027
|
const visible = this.deps.options().visibleModels;
|
|
1877
2028
|
const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
|
|
1878
|
-
|
|
2029
|
+
const overrides = this.deps.options().modelVisibility;
|
|
2030
|
+
return catalog.filter((model) => modelVisibleInPlan(model.id, access)).filter((model) => {
|
|
2031
|
+
const override = overrides?.[model.id];
|
|
2032
|
+
if (typeof override === "boolean") return override;
|
|
2033
|
+
return allow === void 0 || allow.has(model.id);
|
|
2034
|
+
}).map(toInfo).sort(compareByPlan);
|
|
1879
2035
|
}
|
|
1880
2036
|
async resolveModel(provider, model, signal) {
|
|
1881
2037
|
const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
|
|
@@ -3786,6 +3942,57 @@ var CommandCodeSearchProvider = class {
|
|
|
3786
3942
|
const ACTIVE_ACCOUNT_AUTO = "auto";
|
|
3787
3943
|
/** The selector value meaning "no language override — follow the shell locale". */
|
|
3788
3944
|
const LANG_AUTO = "auto";
|
|
3945
|
+
/** Tier display names, mirroring the web dropdown's headings (`model-select.ts`). */
|
|
3946
|
+
const TIER_TITLES = {
|
|
3947
|
+
go: "Go",
|
|
3948
|
+
goat: "GOAT",
|
|
3949
|
+
pro: "Pro",
|
|
3950
|
+
provider: "Provider",
|
|
3951
|
+
max: "Max"
|
|
3952
|
+
};
|
|
3953
|
+
/** Tiers in picker order; a model outside this set joins the "Other" group. */
|
|
3954
|
+
const TIER_ORDER = [
|
|
3955
|
+
"go",
|
|
3956
|
+
"goat",
|
|
3957
|
+
"pro",
|
|
3958
|
+
"provider",
|
|
3959
|
+
"max"
|
|
3960
|
+
];
|
|
3961
|
+
/** The group holding models this build's catalog does not know. */
|
|
3962
|
+
const OTHER_GROUP_ID = "models-other";
|
|
3963
|
+
/**
|
|
3964
|
+
* Every model this build knows, in checkbox order: plan tier (Go first), then
|
|
3965
|
+
* free before paid inside a tier, then by id.
|
|
3966
|
+
*
|
|
3967
|
+
* The list is the static capability snapshot rather than a live catalog read
|
|
3968
|
+
* on purpose — a settings page must draw synchronously, and the snapshot is
|
|
3969
|
+
* synced from the same upstream table the picker's tier headings come from.
|
|
3970
|
+
* A model added upstream after this build still reaches the user: an empty
|
|
3971
|
+
* allowlist shows everything, and a model named in the allowlist but absent
|
|
3972
|
+
* here is rendered by the "Other" group instead of disappearing.
|
|
3973
|
+
*/
|
|
3974
|
+
function commandCodeTuiModelChoices() {
|
|
3975
|
+
return Object.keys(KNOWN_PLANS).map((id) => ({
|
|
3976
|
+
id,
|
|
3977
|
+
tier: KNOWN_PLANS[id] ?? "",
|
|
3978
|
+
free: isFreeModel(id),
|
|
3979
|
+
hint: capabilityDescription(id)
|
|
3980
|
+
})).sort((a, b) => {
|
|
3981
|
+
const tierDelta = tierRank(a.tier) - tierRank(b.tier);
|
|
3982
|
+
if (tierDelta !== 0) return tierDelta;
|
|
3983
|
+
if (a.free !== b.free) return a.free ? -1 : 1;
|
|
3984
|
+
return a.id.localeCompare(b.id);
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
/** Sort rank of a tier key; unknown tiers trail every known one. */
|
|
3988
|
+
function tierRank(tier) {
|
|
3989
|
+
const rank = TIER_ORDER.indexOf(tier);
|
|
3990
|
+
return rank === -1 ? TIER_ORDER.length : rank;
|
|
3991
|
+
}
|
|
3992
|
+
/** The stored allowlist as a clean id list (non-strings and blanks dropped). */
|
|
3993
|
+
function storedIds(value) {
|
|
3994
|
+
return Array.isArray(value) ? value.filter((id) => typeof id === "string" && id !== "") : [];
|
|
3995
|
+
}
|
|
3789
3996
|
/**
|
|
3790
3997
|
* Build the section descriptor. Pure, so tests can pin the exact fields
|
|
3791
3998
|
* without a dsh-TUI host.
|
|
@@ -3795,13 +4002,65 @@ const LANG_AUTO = "auto";
|
|
|
3795
4002
|
* cannot express "unset" — cycling only ever lands on a declared option, so a
|
|
3796
4003
|
* `select` would strand the user on a pinned value with no way back to
|
|
3797
4004
|
* automatic. The `auto` sentinel plus a `parse` that clears the path keeps the
|
|
3798
|
-
* unset state reachable.
|
|
3799
|
-
* (
|
|
3800
|
-
*
|
|
4005
|
+
* unset state reachable. `filterModelsByPlan` formats its EFFECTIVE default
|
|
4006
|
+
* (unset means true at the adapter), so a fresh install reads true instead of
|
|
4007
|
+
* the screen's "(empty)".
|
|
3801
4008
|
*/
|
|
3802
4009
|
function buildCommandCodeTuiSection(deps) {
|
|
3803
4010
|
const ref = deps.apiKeyRef();
|
|
3804
4011
|
const slots = deps.accountSlots();
|
|
4012
|
+
const choices = (deps.modelChoices ?? commandCodeTuiModelChoices)();
|
|
4013
|
+
const catalogIds = choices.map((choice) => choice.id);
|
|
4014
|
+
const known = new Set(catalogIds);
|
|
4015
|
+
const stored = storedIds(deps.visibleModels());
|
|
4016
|
+
const overrides = deps.modelVisibility?.() ?? {};
|
|
4017
|
+
const extras = [.../* @__PURE__ */ new Set([...stored, ...Object.keys(overrides)])].filter((id) => !known.has(id));
|
|
4018
|
+
const tierGroups = TIER_ORDER.filter((tier) => choices.some((choice) => choice.tier === tier)).map((tier) => ({
|
|
4019
|
+
id: `models-${tier}`,
|
|
4020
|
+
title: `${TIER_TITLES[tier] ?? tier} models`,
|
|
4021
|
+
descriptions: { zh: `${TIER_TITLES[tier] ?? tier} 模型` }
|
|
4022
|
+
}));
|
|
4023
|
+
const unranked = choices.filter((choice) => tierRank(choice.tier) === TIER_ORDER.length);
|
|
4024
|
+
const otherGroup = unranked.length > 0 || extras.length > 0 ? [{
|
|
4025
|
+
id: OTHER_GROUP_ID,
|
|
4026
|
+
title: "Other models",
|
|
4027
|
+
descriptions: { zh: "其他模型" }
|
|
4028
|
+
}] : [];
|
|
4029
|
+
/**
|
|
4030
|
+
* One checkbox: a boolean at a path of its OWN (`modelVisibility.<id>`).
|
|
4031
|
+
*
|
|
4032
|
+
* The path must be unique per model. dsh-TUI keys a staged draft by the
|
|
4033
|
+
* field's path (`fieldKey`), so N checkboxes sharing `visibleModels` share
|
|
4034
|
+
* ONE draft: every one of them then parses that same draft on save, all N
|
|
4035
|
+
* write ops address the same path, and only the LAST field's op survives —
|
|
4036
|
+
* which silently rewrote the allowlist from the last catalog model instead
|
|
4037
|
+
* of the one that was toggled. A per-model key is what makes a checkbox
|
|
4038
|
+
* express one model's state.
|
|
4039
|
+
*
|
|
4040
|
+
* An override equal to what the array already says is written as a CLEAR, so
|
|
4041
|
+
* toggling a model back to its inherited state leaves no residue and the
|
|
4042
|
+
* document stays minimal.
|
|
4043
|
+
*/
|
|
4044
|
+
const modelField = (id, hint, group) => ({
|
|
4045
|
+
path: ["modelVisibility", id],
|
|
4046
|
+
group,
|
|
4047
|
+
kind: "boolean",
|
|
4048
|
+
label: id,
|
|
4049
|
+
...hint === "" ? {} : { hint },
|
|
4050
|
+
format: (value) => {
|
|
4051
|
+
if (typeof value === "boolean") return String(value);
|
|
4052
|
+
const listed = storedIds(deps.visibleModels());
|
|
4053
|
+
return String(listed.length === 0 || listed.includes(id));
|
|
4054
|
+
},
|
|
4055
|
+
parse: (text) => {
|
|
4056
|
+
const on = text.trim() === "true";
|
|
4057
|
+
const listed = storedIds(deps.visibleModels());
|
|
4058
|
+
return on === (listed.length === 0 || listed.includes(id)) ? { kind: "clear" } : {
|
|
4059
|
+
kind: "set",
|
|
4060
|
+
value: on
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
});
|
|
3805
4064
|
return {
|
|
3806
4065
|
ns: deps.ns,
|
|
3807
4066
|
title: deps.title ?? "Command Code",
|
|
@@ -3817,6 +4076,8 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
3817
4076
|
title: "Models",
|
|
3818
4077
|
descriptions: { zh: "模型" }
|
|
3819
4078
|
},
|
|
4079
|
+
...tierGroups,
|
|
4080
|
+
...otherGroup,
|
|
3820
4081
|
{
|
|
3821
4082
|
id: "advanced",
|
|
3822
4083
|
title: "Advanced",
|
|
@@ -3865,23 +4126,9 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
3865
4126
|
value: text.trim() === "true"
|
|
3866
4127
|
})
|
|
3867
4128
|
},
|
|
3868
|
-
{
|
|
3869
|
-
|
|
3870
|
-
|
|
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
|
-
},
|
|
4129
|
+
...choices.filter((choice) => tierRank(choice.tier) !== TIER_ORDER.length).map((choice) => modelField(choice.id, choice.hint, `models-${choice.tier}`)),
|
|
4130
|
+
...unranked.map((choice) => modelField(choice.id, choice.hint, OTHER_GROUP_ID)),
|
|
4131
|
+
...extras.map((id) => modelField(id, capabilityDescription(id), OTHER_GROUP_ID)),
|
|
3885
4132
|
{
|
|
3886
4133
|
path: ["activeAccount"],
|
|
3887
4134
|
group: "advanced",
|
|
@@ -3968,15 +4215,18 @@ function tuiSettingsService(ctx) {
|
|
|
3968
4215
|
}
|
|
3969
4216
|
/**
|
|
3970
4217
|
* Identity of everything in the section that can change at runtime: the
|
|
3971
|
-
* credential reference behind the API-key field
|
|
3972
|
-
*
|
|
3973
|
-
*
|
|
4218
|
+
* credential reference behind the API-key field, the account slots behind the
|
|
4219
|
+
* active-account selector, and the stored-but-unknown allowlist entries that
|
|
4220
|
+
* get a checkbox of their own. Re-registration is skipped while this matches,
|
|
4221
|
+
* so ordinary settings writes never churn the screen's section list — the
|
|
4222
|
+
* checkboxes themselves read their state live and need no re-declaration.
|
|
3974
4223
|
*/
|
|
3975
4224
|
function sectionSignature(section) {
|
|
3976
4225
|
const active = section.fields.find((field) => field.path.join(".") === "activeAccount");
|
|
3977
4226
|
return JSON.stringify({
|
|
3978
4227
|
secret: section.fields.find((field) => field.secret !== void 0)?.secret?.ref ?? "",
|
|
3979
|
-
options: active?.options?.map((option) => option.value) ?? []
|
|
4228
|
+
options: active?.options?.map((option) => option.value) ?? [],
|
|
4229
|
+
other: section.fields.filter((field) => field.group === OTHER_GROUP_ID).map((field) => field.label)
|
|
3980
4230
|
});
|
|
3981
4231
|
}
|
|
3982
4232
|
/**
|
|
@@ -4065,6 +4315,15 @@ const Config = z.object({
|
|
|
4065
4315
|
streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
4066
4316
|
filterModelsByPlan: z.boolean(),
|
|
4067
4317
|
visibleModels: z.array(z.string()),
|
|
4318
|
+
/**
|
|
4319
|
+
* Per-model visibility overrides for the terminal settings page's checkbox
|
|
4320
|
+
* list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
|
|
4321
|
+
* PATH, so two checkboxes sharing one path would overwrite each other's
|
|
4322
|
+
* draft and the section's last model would decide every write; a map gives
|
|
4323
|
+
* each checkbox a path of its own. An id listed here wins over
|
|
4324
|
+
* {@link visibleModels}; ids absent here keep following it.
|
|
4325
|
+
*/
|
|
4326
|
+
modelVisibility: z.dict(z.boolean()),
|
|
4068
4327
|
webSearch: z.boolean().default(true),
|
|
4069
4328
|
accounts: z.array(z.object({
|
|
4070
4329
|
label: z.string(),
|
|
@@ -4094,9 +4353,22 @@ function resolveAdapterOptions(config) {
|
|
|
4094
4353
|
requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
|
|
4095
4354
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
4096
4355
|
filterModelsByPlan: config.filterModelsByPlan ?? true,
|
|
4097
|
-
visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0
|
|
4356
|
+
visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
|
|
4357
|
+
modelVisibility: readModelVisibility(config.modelVisibility)
|
|
4098
4358
|
};
|
|
4099
4359
|
}
|
|
4360
|
+
/**
|
|
4361
|
+
* Per-model visibility overrides, cleaned for the adapter. Programmatic
|
|
4362
|
+
* construction may bypass Schemastery normalization, so a non-object or a
|
|
4363
|
+
* non-boolean entry is dropped here instead of reaching the picker filter.
|
|
4364
|
+
* @param raw - The `modelVisibility` value from any config source.
|
|
4365
|
+
* @returns A frozen id → boolean map, or undefined when nothing is set.
|
|
4366
|
+
*/
|
|
4367
|
+
function readModelVisibility(raw) {
|
|
4368
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
4369
|
+
const entries = Object.entries(raw).filter((entry) => entry[0] !== "" && typeof entry[1] === "boolean");
|
|
4370
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
4371
|
+
}
|
|
4100
4372
|
function apply(ctx, config) {
|
|
4101
4373
|
let current = () => config;
|
|
4102
4374
|
let lastRaw;
|
|
@@ -4268,7 +4540,9 @@ function apply(ctx, config) {
|
|
|
4268
4540
|
accountSlots: () => slots().map((slot) => ({
|
|
4269
4541
|
id: slot.id,
|
|
4270
4542
|
label: slot.label
|
|
4271
|
-
}))
|
|
4543
|
+
})),
|
|
4544
|
+
visibleModels: () => options().visibleModels ?? [],
|
|
4545
|
+
modelVisibility: () => options().modelVisibility
|
|
4272
4546
|
});
|
|
4273
4547
|
tuiCtx.effect(() => () => {
|
|
4274
4548
|
refreshTuiSettings = void 0;
|