@sayknow-cli/coding-agent 0.5.25 → 0.5.26
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 +13 -1
- package/dist/types/config/settings-schema.d.ts +26 -0
- package/dist/types/config/task-model-specialties.d.ts +55 -0
- package/dist/types/decisions/task-routing.d.ts +96 -11
- package/dist/types/i18n/messages/en.d.ts +15 -0
- package/dist/types/lsp/index.d.ts +1 -1
- package/dist/types/lsp/types.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +11 -0
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +114 -18
- package/dist/types/tools/subagent.d.ts +2 -2
- package/package.json +7 -7
- package/src/config/settings-schema.ts +37 -7
- package/src/config/task-model-specialties.ts +131 -0
- package/src/decisions/task-routing.ts +382 -66
- package/src/i18n/messages/de.ts +16 -0
- package/src/i18n/messages/en.ts +16 -0
- package/src/i18n/messages/es.ts +16 -0
- package/src/i18n/messages/fr.ts +16 -0
- package/src/i18n/messages/ja.ts +16 -0
- package/src/i18n/messages/ko.ts +16 -0
- package/src/i18n/messages/zh.ts +16 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/components/model-selector.ts +275 -34
- package/src/modes/controllers/selector-controller.ts +50 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +1 -1
- package/src/prompts/tools/task.md +1 -0
- package/src/slash-commands/builtin-registry.ts +11 -9
- package/src/task/index.ts +98 -40
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +44 -0
|
@@ -35,6 +35,7 @@ import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
|
35
35
|
import { getProviderAuthHealth } from "../../config/provider-auth-health";
|
|
36
36
|
import { compareRankedProviders, type ProviderAuthState } from "../../config/provider-ranking";
|
|
37
37
|
import type { Settings } from "../../config/settings";
|
|
38
|
+
import { specialtiesForRole, type TaskModelSpecialty } from "../../config/task-model-specialties";
|
|
38
39
|
import { t } from "../../i18n";
|
|
39
40
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
40
41
|
import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
|
|
@@ -111,6 +112,18 @@ export type ModelSelectorSelection =
|
|
|
111
112
|
profileName: string;
|
|
112
113
|
setDefault: boolean;
|
|
113
114
|
}
|
|
115
|
+
| {
|
|
116
|
+
kind: "specialtyAssignment";
|
|
117
|
+
model: Model;
|
|
118
|
+
role: SkcModelAssignmentTargetId;
|
|
119
|
+
specialty: TaskModelSpecialty;
|
|
120
|
+
thinkingLevel?: ThinkingLevel;
|
|
121
|
+
selector?: string;
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
kind: "specialtyReset";
|
|
125
|
+
role: SkcModelAssignmentTargetId;
|
|
126
|
+
}
|
|
114
127
|
| {
|
|
115
128
|
kind: "createProfile";
|
|
116
129
|
profile: ModelProfileConfig;
|
|
@@ -127,6 +140,34 @@ interface PendingThinkingChoice {
|
|
|
127
140
|
levels: ThinkingLevel[];
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
/** Role agents only — the bulk action that deliberately excludes `default`. */
|
|
144
|
+
const ROLE_AGENT_TARGET_IDS = ["executor", "architect", "planner", "critic"] as const;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A first-level row. Stable descriptors, never positions.
|
|
148
|
+
*
|
|
149
|
+
* `scope` distinguishes the two bulk rows rather than a boolean, because the
|
|
150
|
+
* difference between them is which targets they write, not a degree of anything.
|
|
151
|
+
*/
|
|
152
|
+
type ModelSelectorAction =
|
|
153
|
+
| { kind: "role"; role: SkcModelAssignmentTargetId }
|
|
154
|
+
| {
|
|
155
|
+
kind: "bulk";
|
|
156
|
+
roles: readonly SkcModelAssignmentTargetId[];
|
|
157
|
+
scope: "roleAgents" | "allTargets";
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** A second-level row under one canonical role. */
|
|
161
|
+
type ModelSelectorDetailAction =
|
|
162
|
+
| { kind: "roleGeneral"; role: SkcModelAssignmentTargetId }
|
|
163
|
+
| { kind: "specialty"; role: SkcModelAssignmentTargetId; specialty: TaskModelSpecialty }
|
|
164
|
+
| { kind: "resetSpecialties"; role: SkcModelAssignmentTargetId };
|
|
165
|
+
|
|
166
|
+
interface PendingDetailChoice {
|
|
167
|
+
item: ModelItem | CanonicalModelItem;
|
|
168
|
+
role: SkcModelAssignmentTargetId;
|
|
169
|
+
}
|
|
170
|
+
|
|
130
171
|
type RoleSelectCallback = (selection: ModelSelectorSelection) => void | Promise<void>;
|
|
131
172
|
type CancelCallback = () => void;
|
|
132
173
|
|
|
@@ -315,6 +356,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
315
356
|
#onSelectCallback = (() => {}) as RoleSelectCallback;
|
|
316
357
|
#onCancelCallback = (() => {}) as CancelCallback;
|
|
317
358
|
#errorMessage?: unknown;
|
|
359
|
+
#catalogRefreshGeneration = 0;
|
|
318
360
|
#tui: TUI;
|
|
319
361
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
320
362
|
#temporaryOnly: boolean;
|
|
@@ -327,6 +369,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
327
369
|
#isCurrentModelFastModeActive: () => boolean = () => false;
|
|
328
370
|
#pendingActionItem?: ModelItem | CanonicalModelItem;
|
|
329
371
|
#selectedActionIndex: number = 0;
|
|
372
|
+
#pendingDetailChoice?: PendingDetailChoice;
|
|
373
|
+
#selectedDetailIndex: number = 0;
|
|
330
374
|
#pendingThinkingChoice?: PendingThinkingChoice;
|
|
331
375
|
#selectedThinkingIndex: number = 0;
|
|
332
376
|
|
|
@@ -431,19 +475,42 @@ export class ModelSelectorComponent extends Container {
|
|
|
431
475
|
// Add bottom border
|
|
432
476
|
this.addChild(new DynamicBorder());
|
|
433
477
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
478
|
+
void this.#bootstrapCatalog();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async #bootstrapCatalog(): Promise<void> {
|
|
482
|
+
await this.#loadModels();
|
|
483
|
+
this.#buildProviderTabs();
|
|
484
|
+
if (this.#viewMode === "presets" && (this.#modelRegistry.getModelProfiles?.().size ?? 0) === 0) {
|
|
485
|
+
this.#viewMode = "models";
|
|
486
|
+
}
|
|
487
|
+
if (this.#viewMode === "presets") {
|
|
488
|
+
void this.#refreshProviderAuth();
|
|
489
|
+
this.#renderPresetLanding();
|
|
490
|
+
} else {
|
|
491
|
+
this.#updateTabBar();
|
|
492
|
+
const currentQuery = this.#searchInput.getValue();
|
|
493
|
+
if (currentQuery) {
|
|
494
|
+
this.#filterModels(currentQuery);
|
|
443
495
|
} else {
|
|
496
|
+
this.#updateList();
|
|
497
|
+
}
|
|
498
|
+
void this.#refreshLiveCatalog();
|
|
499
|
+
}
|
|
500
|
+
this.#tui.requestRender();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async #refreshLiveCatalog(): Promise<void> {
|
|
504
|
+
if (this.#scopedModels.length > 0) return;
|
|
505
|
+
const generation = ++this.#catalogRefreshGeneration;
|
|
506
|
+
try {
|
|
507
|
+
await this.#modelRegistry.refresh("online");
|
|
508
|
+
if (generation !== this.#catalogRefreshGeneration) return;
|
|
509
|
+
await this.#loadModels();
|
|
510
|
+
if (generation !== this.#catalogRefreshGeneration) return;
|
|
511
|
+
this.#buildProviderTabs();
|
|
512
|
+
if (this.#viewMode === "models") {
|
|
444
513
|
this.#updateTabBar();
|
|
445
|
-
// Always apply the current search query — the user may have typed
|
|
446
|
-
// while models were loading asynchronously.
|
|
447
514
|
const currentQuery = this.#searchInput.getValue();
|
|
448
515
|
if (currentQuery) {
|
|
449
516
|
this.#filterModels(currentQuery);
|
|
@@ -451,9 +518,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
451
518
|
this.#updateList();
|
|
452
519
|
}
|
|
453
520
|
}
|
|
454
|
-
// Request re-render after models are loaded
|
|
455
521
|
this.#tui.requestRender();
|
|
456
|
-
})
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (generation !== this.#catalogRefreshGeneration) return;
|
|
524
|
+
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
525
|
+
this.#updateList();
|
|
526
|
+
this.#tui.requestRender();
|
|
527
|
+
}
|
|
457
528
|
}
|
|
458
529
|
|
|
459
530
|
#isActiveDefaultFallback(): boolean {
|
|
@@ -1145,6 +1216,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1145
1216
|
this.#setSearchInputValue(seed ?? this.#searchInput.getValue());
|
|
1146
1217
|
this.#updateTabBar();
|
|
1147
1218
|
this.#filterModels(this.#searchInput.getValue());
|
|
1219
|
+
void this.#refreshLiveCatalog();
|
|
1148
1220
|
}
|
|
1149
1221
|
|
|
1150
1222
|
/**
|
|
@@ -1475,33 +1547,114 @@ export class ModelSelectorComponent extends Container {
|
|
|
1475
1547
|
);
|
|
1476
1548
|
if (this.#pendingThinkingChoice) {
|
|
1477
1549
|
this.#renderThinkingMenu(this.#pendingThinkingChoice);
|
|
1550
|
+
} else if (this.#pendingDetailChoice) {
|
|
1551
|
+
this.#renderDetailMenu(this.#pendingDetailChoice);
|
|
1478
1552
|
} else if (this.#pendingActionItem) {
|
|
1479
1553
|
this.#renderActionMenu(this.#pendingActionItem);
|
|
1480
1554
|
}
|
|
1481
1555
|
}
|
|
1482
1556
|
}
|
|
1483
1557
|
|
|
1558
|
+
/**
|
|
1559
|
+
* First-level actions, in a fixed order the tests and muscle memory both rely on.
|
|
1560
|
+
*
|
|
1561
|
+
* Built as descriptors rather than derived from an index so that adding a row
|
|
1562
|
+
* cannot silently renumber the ones below it — the previous index arithmetic
|
|
1563
|
+
* meant every new action was a chance to mis-assign a model to the wrong role.
|
|
1564
|
+
*/
|
|
1565
|
+
#getActions(): ModelSelectorAction[] {
|
|
1566
|
+
const actions: ModelSelectorAction[] = SKC_MODEL_ASSIGNMENT_TARGET_IDS.map(role => ({ kind: "role", role }));
|
|
1567
|
+
actions.push({ kind: "bulk", roles: ROLE_AGENT_TARGET_IDS, scope: "roleAgents" });
|
|
1568
|
+
actions.push({ kind: "bulk", roles: SKC_MODEL_ASSIGNMENT_TARGET_IDS, scope: "allTargets" });
|
|
1569
|
+
return actions;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Second-level rows for one canonical role: General, then its specialties, then reset.
|
|
1574
|
+
*
|
|
1575
|
+
* `default` and the bulk actions never reach here. They write canonical role
|
|
1576
|
+
* models only, because a "set everything" action that also rewrote five
|
|
1577
|
+
* specialty overrides would be impossible to undo from this menu.
|
|
1578
|
+
*/
|
|
1579
|
+
#getDetailActions(role: SkcModelAssignmentTargetId): ModelSelectorDetailAction[] {
|
|
1580
|
+
const rows: ModelSelectorDetailAction[] = [{ kind: "roleGeneral", role }];
|
|
1581
|
+
for (const specialty of specialtiesForRole(role)) rows.push({ kind: "specialty", role, specialty });
|
|
1582
|
+
if (rows.length > 1) rows.push({ kind: "resetSpecialties", role });
|
|
1583
|
+
return rows;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
#actionLabel(action: ModelSelectorAction): string {
|
|
1587
|
+
if (action.kind === "bulk") {
|
|
1588
|
+
return action.scope === "roleAgents"
|
|
1589
|
+
? t("modelSelector.setForAllRoleAgents")
|
|
1590
|
+
: t("modelSelector.setForAllTargets");
|
|
1591
|
+
}
|
|
1592
|
+
const target = SKC_MODEL_ASSIGNMENT_TARGETS[action.role];
|
|
1593
|
+
const label = t("modelSelector.setAsTarget", {
|
|
1594
|
+
tag: target.tag ?? action.role.toUpperCase(),
|
|
1595
|
+
name: target.name,
|
|
1596
|
+
});
|
|
1597
|
+
const count = specialtiesForRole(action.role).length;
|
|
1598
|
+
return count > 0 ? `${label} ${theme.fg("muted", t("modelSelector.hasDetailedUses"))}` : label;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
#detailActionLabel(action: ModelSelectorDetailAction): string {
|
|
1602
|
+
if (action.kind === "roleGeneral") return t("modelSelector.generalRole");
|
|
1603
|
+
if (action.kind === "resetSpecialties") return t("modelSelector.resetSpecialties");
|
|
1604
|
+
// Read through rather than caching: a row that states what it already holds
|
|
1605
|
+
// is worse than useless if it states a stale value.
|
|
1606
|
+
const assigned = this.#settings.get("task.modelRouting.specialtyModels")?.[action.specialty];
|
|
1607
|
+
const current = selectorHead(assigned);
|
|
1608
|
+
const label = t(`modelSelector.specialty.${action.specialty}`);
|
|
1609
|
+
return current ? `${label} ${theme.fg("muted", `— ${current}`)}` : label;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1484
1612
|
#renderActionMenu(item: ModelItem | CanonicalModelItem): void {
|
|
1485
1613
|
this.#listContainer.addChild(new Spacer(1));
|
|
1486
1614
|
this.#listContainer.addChild(
|
|
1487
1615
|
new Text(theme.fg("muted", ` ${t("modelSelector.actionFor", { id: item.model.id })}`), 0, 0),
|
|
1488
1616
|
);
|
|
1489
1617
|
this.#listContainer.addChild(new Spacer(1));
|
|
1490
|
-
const
|
|
1491
|
-
for (let i = 0; i <
|
|
1618
|
+
const actions = this.#getActions();
|
|
1619
|
+
for (let i = 0; i < actions.length; i++) {
|
|
1620
|
+
const action = actions[i];
|
|
1621
|
+
if (!action) continue;
|
|
1492
1622
|
const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
1493
|
-
const
|
|
1494
|
-
const label = role
|
|
1495
|
-
? `Set as ${SKC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${SKC_MODEL_ASSIGNMENT_TARGETS[role].name})`
|
|
1496
|
-
: i === SKC_MODEL_ASSIGNMENT_TARGET_IDS.length
|
|
1497
|
-
? "Set for all role agents"
|
|
1498
|
-
: "Set for all targets";
|
|
1623
|
+
const label = this.#actionLabel(action);
|
|
1499
1624
|
this.#listContainer.addChild(
|
|
1500
1625
|
new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
|
|
1501
1626
|
);
|
|
1502
1627
|
}
|
|
1503
1628
|
}
|
|
1504
1629
|
|
|
1630
|
+
#renderDetailMenu(choice: PendingDetailChoice): void {
|
|
1631
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
1632
|
+
this.#listContainer.addChild(
|
|
1633
|
+
new Text(
|
|
1634
|
+
theme.fg(
|
|
1635
|
+
"muted",
|
|
1636
|
+
` ${t("modelSelector.detailedUseFor", {
|
|
1637
|
+
target: SKC_MODEL_ASSIGNMENT_TARGETS[choice.role].name,
|
|
1638
|
+
id: choice.item.model.id,
|
|
1639
|
+
})}`,
|
|
1640
|
+
),
|
|
1641
|
+
0,
|
|
1642
|
+
0,
|
|
1643
|
+
),
|
|
1644
|
+
);
|
|
1645
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
1646
|
+
const rows = this.#getDetailActions(choice.role);
|
|
1647
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1648
|
+
const action = rows[i];
|
|
1649
|
+
if (!action) continue;
|
|
1650
|
+
const prefix = i === this.#selectedDetailIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
1651
|
+
const label = this.#detailActionLabel(action);
|
|
1652
|
+
this.#listContainer.addChild(
|
|
1653
|
+
new Text(`${prefix}${i === this.#selectedDetailIndex ? theme.fg("accent", label) : label}`, 0, 0),
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1505
1658
|
#renderThinkingMenu(choice: PendingThinkingChoice): void {
|
|
1506
1659
|
const targetLabel = choice.roles
|
|
1507
1660
|
? choice.roles.includes("default")
|
|
@@ -1537,7 +1690,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1537
1690
|
return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
|
|
1538
1691
|
}
|
|
1539
1692
|
#getActionCount(_model: Model): number {
|
|
1540
|
-
return
|
|
1693
|
+
return this.#getActions().length;
|
|
1541
1694
|
}
|
|
1542
1695
|
|
|
1543
1696
|
#getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
|
|
@@ -1551,6 +1704,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
1551
1704
|
this.#handleThinkingMenuInput(keyData);
|
|
1552
1705
|
return;
|
|
1553
1706
|
}
|
|
1707
|
+
if (this.#pendingDetailChoice) {
|
|
1708
|
+
this.#handleDetailMenuInput(keyData);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1554
1711
|
if (this.#pendingActionItem) {
|
|
1555
1712
|
this.#handleActionMenuInput(keyData);
|
|
1556
1713
|
return;
|
|
@@ -1783,17 +1940,24 @@ export class ModelSelectorComponent extends Container {
|
|
|
1783
1940
|
return;
|
|
1784
1941
|
}
|
|
1785
1942
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
if (
|
|
1789
|
-
this.#
|
|
1943
|
+
const action = this.#getActions()[this.#selectedActionIndex];
|
|
1944
|
+
if (!action) return;
|
|
1945
|
+
if (action.kind === "bulk") {
|
|
1946
|
+
this.#pendingActionItem = undefined;
|
|
1947
|
+
this.#handleSelect(item, "default", undefined, action.roles);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
// A role with specialties opens the detail level instead of assigning
|
|
1951
|
+
// straight away; a role without any keeps the old one-keystroke path.
|
|
1952
|
+
if (this.#getDetailActions(action.role).length > 1) {
|
|
1953
|
+
this.#pendingActionItem = undefined;
|
|
1954
|
+
this.#pendingDetailChoice = { item, role: action.role };
|
|
1955
|
+
this.#selectedDetailIndex = 0;
|
|
1956
|
+
this.#updateList();
|
|
1790
1957
|
return;
|
|
1791
1958
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
? (["executor", "architect", "planner", "critic"] as const)
|
|
1795
|
-
: SKC_MODEL_ASSIGNMENT_TARGET_IDS;
|
|
1796
|
-
this.#handleSelect(item, "default", undefined, roles);
|
|
1959
|
+
this.#pendingActionItem = undefined;
|
|
1960
|
+
this.#handleSelect(item, action.role);
|
|
1797
1961
|
return;
|
|
1798
1962
|
}
|
|
1799
1963
|
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
@@ -1801,6 +1965,74 @@ export class ModelSelectorComponent extends Container {
|
|
|
1801
1965
|
this.#updateList();
|
|
1802
1966
|
}
|
|
1803
1967
|
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Second level: General, the role's specialties, and reset.
|
|
1970
|
+
*
|
|
1971
|
+
* Cancel steps back to the first level rather than closing outright, so a
|
|
1972
|
+
* wrong turn costs one keystroke instead of restarting the whole selection.
|
|
1973
|
+
*/
|
|
1974
|
+
#handleDetailMenuInput(keyData: string): void {
|
|
1975
|
+
const choice = this.#pendingDetailChoice;
|
|
1976
|
+
if (!choice) return;
|
|
1977
|
+
const rows = this.#getDetailActions(choice.role);
|
|
1978
|
+
if (matchesKey(keyData, "up")) {
|
|
1979
|
+
this.#selectedDetailIndex = this.#selectedDetailIndex === 0 ? rows.length - 1 : this.#selectedDetailIndex - 1;
|
|
1980
|
+
this.#updateList();
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if (matchesKey(keyData, "down")) {
|
|
1984
|
+
this.#selectedDetailIndex = (this.#selectedDetailIndex + 1) % rows.length;
|
|
1985
|
+
this.#updateList();
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
1989
|
+
const action = rows[this.#selectedDetailIndex];
|
|
1990
|
+
if (!action) return;
|
|
1991
|
+
this.#pendingDetailChoice = undefined;
|
|
1992
|
+
if (action.kind === "roleGeneral") {
|
|
1993
|
+
this.#handleSelect(choice.item, action.role);
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (action.kind === "resetSpecialties") {
|
|
1997
|
+
this.#onSelectCallback({ kind: "specialtyReset", role: action.role });
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
this.#handleSpecialtySelect(choice.item, action.role, action.specialty);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
2004
|
+
this.#pendingDetailChoice = undefined;
|
|
2005
|
+
this.#pendingActionItem = choice.item;
|
|
2006
|
+
this.#selectedActionIndex = Math.max(
|
|
2007
|
+
0,
|
|
2008
|
+
this.#getActions().findIndex(action => action.kind === "role" && action.role === choice.role),
|
|
2009
|
+
);
|
|
2010
|
+
this.#updateList();
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Assign a model to one specialty.
|
|
2016
|
+
*
|
|
2017
|
+
* Reuses the same explicit-thinking gate as a canonical assignment, because a
|
|
2018
|
+
* specialty override is dispatched exactly like a role model and would hit
|
|
2019
|
+
* the same "which effort?" ambiguity if it skipped it.
|
|
2020
|
+
*/
|
|
2021
|
+
#handleSpecialtySelect(
|
|
2022
|
+
item: ModelItem | CanonicalModelItem,
|
|
2023
|
+
role: SkcModelAssignmentTargetId,
|
|
2024
|
+
specialty: TaskModelSpecialty,
|
|
2025
|
+
): void {
|
|
2026
|
+
const thinkingLevel = item.thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
|
|
2027
|
+
this.#onSelectCallback({
|
|
2028
|
+
kind: "specialtyAssignment",
|
|
2029
|
+
model: item.model,
|
|
2030
|
+
role,
|
|
2031
|
+
specialty,
|
|
2032
|
+
thinkingLevel,
|
|
2033
|
+
selector: formatModelSelectorValue(item.selector, thinkingLevel),
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
1804
2036
|
|
|
1805
2037
|
#handleThinkingMenuInput(keyData: string): void {
|
|
1806
2038
|
const choice = this.#pendingThinkingChoice;
|
|
@@ -1826,10 +2058,19 @@ export class ModelSelectorComponent extends Container {
|
|
|
1826
2058
|
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
1827
2059
|
this.#pendingThinkingChoice = undefined;
|
|
1828
2060
|
if (choice.role !== null) {
|
|
2061
|
+
// Step back one level, landing on the row that got us here. Located by
|
|
2062
|
+
// descriptor rather than computed from lengths, so the two bulk rows
|
|
2063
|
+
// cannot drift apart from the list that renders them.
|
|
1829
2064
|
this.#pendingActionItem = choice.item;
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
2065
|
+
const actions = this.#getActions();
|
|
2066
|
+
const bulkRoles = choice.roles;
|
|
2067
|
+
const wanted = bulkRoles
|
|
2068
|
+
? actions.findIndex(
|
|
2069
|
+
action =>
|
|
2070
|
+
action.kind === "bulk" && action.roles.includes("default") === bulkRoles.includes("default"),
|
|
2071
|
+
)
|
|
2072
|
+
: actions.findIndex(action => action.kind === "role" && action.role === choice.role);
|
|
2073
|
+
this.#selectedActionIndex = Math.max(0, wanted);
|
|
1833
2074
|
}
|
|
1834
2075
|
this.#updateList();
|
|
1835
2076
|
}
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
15
15
|
import { SKC_MODEL_ASSIGNMENT_TARGETS, type SkcModelAssignmentTargetId } from "../../config/model-registry";
|
|
16
16
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
17
|
-
import { selectorHead } from "../../config/model-selector-value";
|
|
17
|
+
import { type ModelSelectorValue, selectorHead } from "../../config/model-selector-value";
|
|
18
18
|
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
19
19
|
import { type Settings, settings } from "../../config/settings";
|
|
20
|
+
import { specialtiesForRole } from "../../config/task-model-specialties";
|
|
20
21
|
import { DebugSelectorComponent } from "../../debug";
|
|
21
22
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
22
23
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
MarketplaceManager,
|
|
29
30
|
} from "../../extensibility/plugins/marketplace";
|
|
30
31
|
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
32
|
+
import { t } from "../../i18n";
|
|
31
33
|
import {
|
|
32
34
|
getAvailableThemes,
|
|
33
35
|
getCurrentThemeName,
|
|
@@ -1589,6 +1591,52 @@ export class SelectorController {
|
|
|
1589
1591
|
this.ctx.ui.requestRender();
|
|
1590
1592
|
return;
|
|
1591
1593
|
}
|
|
1594
|
+
if (selection.kind === "specialtyAssignment") {
|
|
1595
|
+
const value =
|
|
1596
|
+
selection.selector ??
|
|
1597
|
+
formatModelSelectorValue(
|
|
1598
|
+
`${selection.model.provider}/${selection.model.id}`,
|
|
1599
|
+
selection.thinkingLevel,
|
|
1600
|
+
);
|
|
1601
|
+
const current = this.ctx.settings.get("task.modelRouting.specialtyModels") ?? {};
|
|
1602
|
+
this.ctx.settings.set("task.modelRouting.specialtyModels", {
|
|
1603
|
+
...current,
|
|
1604
|
+
[selection.specialty]: value,
|
|
1605
|
+
});
|
|
1606
|
+
this.ctx.settings
|
|
1607
|
+
.getStorage()
|
|
1608
|
+
?.recordModelUsage(`${selection.model.provider}/${selection.model.id}`);
|
|
1609
|
+
await this.ctx.notifyConfigChanged?.();
|
|
1610
|
+
// The assignment is live for declared work as soon as it is saved. The
|
|
1611
|
+
// routing switch only governs auto-detection, so say that rather than
|
|
1612
|
+
// auto-enabling a classifier the user did not ask for.
|
|
1613
|
+
const specialtyLabel = t(`modelSelector.specialty.${selection.specialty}`);
|
|
1614
|
+
this.ctx.showStatus(
|
|
1615
|
+
this.ctx.settings.get("task.modelRouting.enabled")
|
|
1616
|
+
? t("modelSelector.specialtySaved", { specialty: specialtyLabel, value })
|
|
1617
|
+
: t("modelSelector.specialtySavedRoutingOff", { specialty: specialtyLabel, value }),
|
|
1618
|
+
);
|
|
1619
|
+
done();
|
|
1620
|
+
this.ctx.ui.requestRender();
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
if (selection.kind === "specialtyReset") {
|
|
1624
|
+
const current = this.ctx.settings.get("task.modelRouting.specialtyModels") ?? {};
|
|
1625
|
+
const remaining: Record<string, ModelSelectorValue> = {};
|
|
1626
|
+
for (const [key, value] of Object.entries(current)) {
|
|
1627
|
+
if (!specialtiesForRole(selection.role).includes(key as never)) remaining[key] = value;
|
|
1628
|
+
}
|
|
1629
|
+
this.ctx.settings.set("task.modelRouting.specialtyModels", remaining);
|
|
1630
|
+
await this.ctx.notifyConfigChanged?.();
|
|
1631
|
+
this.ctx.showStatus(
|
|
1632
|
+
t("modelSelector.specialtyCleared", {
|
|
1633
|
+
target: SKC_MODEL_ASSIGNMENT_TARGETS[selection.role].name,
|
|
1634
|
+
}),
|
|
1635
|
+
);
|
|
1636
|
+
done();
|
|
1637
|
+
this.ctx.ui.requestRender();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1592
1640
|
const { model, role, thinkingLevel, selector: selectedSelector } = selection;
|
|
1593
1641
|
if (role === null) {
|
|
1594
1642
|
// Temporary: update agent state but don't persist to settings
|
|
@@ -2306,7 +2354,7 @@ export class SelectorController {
|
|
|
2306
2354
|
},
|
|
2307
2355
|
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
2308
2356
|
});
|
|
2309
|
-
await this.ctx.session.modelRegistry.refresh();
|
|
2357
|
+
await this.ctx.session.modelRegistry.refresh("online");
|
|
2310
2358
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
2311
2359
|
this.ctx.chatContainer.addChild(
|
|
2312
2360
|
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
|
|
@@ -454,7 +454,7 @@ export async function dispatchRpcCommand(
|
|
|
454
454
|
return new Promise<string>(() => {});
|
|
455
455
|
},
|
|
456
456
|
});
|
|
457
|
-
await session.modelRegistry.refresh();
|
|
457
|
+
await session.modelRegistry.refresh("online");
|
|
458
458
|
return rpcSuccess(id, "login", { providerId: command.providerId });
|
|
459
459
|
} catch (err: unknown) {
|
|
460
460
|
return rpcError(id, "login", err instanceof Error ? err.message : String(err));
|
|
@@ -21,6 +21,7 @@ Subagents have no conversation history. Every fact, file path, and direction the
|
|
|
21
21
|
- `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase
|
|
22
22
|
- `.description`: UI label only — subagent never sees it
|
|
23
23
|
- `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
|
|
24
|
+
- `.specialty` (optional): the kind of work — `backendArchitecture`, `frontendDesign`, `implementation`, `testing`, or `review`. When the user assigned a model to that specialty under `/model`, this child runs on it, deterministically: no classifier, no routing switch. It only leaves that model on a transport error (429, 5xx, auth, quota), falling back to the agent's role model. Declare it whenever the work clearly is one of these — a frontend build is `frontendDesign` even for `executor`. Omit to keep the role model or let auto-detection decide.
|
|
24
25
|
{{#if contextEnabled}}- `context`: shared background prepended to every assignment; session-specific only{{/if}}
|
|
25
26
|
{{#if contextEnabled}}
|
|
26
27
|
- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `"none"` for no copied context. `"receipt"` copies a minimal receipt-sized snapshot, `"last-turn"` copies only the latest exchange, `"bounded"` copies the bounded default snapshot, and `"full"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.
|
|
@@ -360,9 +360,8 @@ async function resolveModelCommandSelection(
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
const providerRef = parseProviderQualifiedSelector(selector);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
await runtime.session.modelRegistry.refreshProvider?.(providerRef.provider, "online");
|
|
363
|
+
if (providerRef && runtime.session.modelRegistry?.refreshProvider) {
|
|
364
|
+
await runtime.session.modelRegistry.refreshProvider(providerRef.provider, "online");
|
|
366
365
|
availableModels = runtime.session.getAvailableModels?.() ?? [];
|
|
367
366
|
const refreshedSelection = resolveModelCommandSelectionFromAvailable(
|
|
368
367
|
runtime,
|
|
@@ -372,12 +371,15 @@ async function resolveModelCommandSelection(
|
|
|
372
371
|
if (refreshedSelection) {
|
|
373
372
|
return { ok: true, selection: refreshedSelection };
|
|
374
373
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
374
|
+
const discoverableProviders = runtime.session.modelRegistry.getDiscoverableProviders?.() ?? [];
|
|
375
|
+
if (discoverableProviders.includes(providerRef.provider)) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
failure: {
|
|
379
|
+
message: formatDiscoverableProviderFailure(selector, providerRef.provider, providerRef.modelId, runtime),
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
381
383
|
}
|
|
382
384
|
|
|
383
385
|
return {
|