thoth-agents 0.2.7 → 0.2.9

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.
@@ -1,44 +1,16 @@
1
1
  import {
2
- CLAUDE_CODE_ROLE_NAMES,
3
- CODEX_ROLE_NAMES,
4
- applyClaudeCodePlan,
5
- applyCodexPlan,
6
- applyOpenCodePlan,
7
- buildClaudeCodeInstallPlan,
8
- buildClaudeCodeModelPlan,
9
- buildClaudeCodeSetupPlan,
10
- buildClaudeCodeSyncPlan,
11
- buildClaudeCodeUpdatePlan,
12
- buildCodexInstallPlan,
13
- buildCodexModelPlan,
14
- buildCodexSetupPlan,
15
- buildCodexSyncPlan,
16
- buildCodexUpdatePlan,
17
- buildOpenCodeInstallPlan,
18
- buildOpenCodeModelPlan,
19
- buildOpenCodeSyncPlan,
20
- buildOpenCodeUpdatePlan,
21
- defaultClaudeCodeModelRoles,
22
- getClaudeCodeStatus,
23
- getCodexStatus,
24
- getOpenCodeStatus,
25
- listOperationHarnesses,
26
- parseRoleTomlModel,
27
- parseSubagentModel
28
- } from "../../chunk-32672NLJ.js";
29
- import {
30
- ALL_AGENT_NAMES,
31
- DEFAULT_MODELS,
32
- getExistingLiteConfigPath,
33
- parseConfig
34
- } from "../../chunk-N7GFXGFJ.js";
2
+ defaultTuiOperations,
3
+ effortChoicesForModel,
4
+ listOperationHarnesses
5
+ } from "../../chunk-6NMPKYWY.js";
6
+ import "../../chunk-YLRPDTU7.js";
35
7
 
36
8
  // src/cli/tui/index.tsx
37
9
  import { render } from "ink";
38
10
 
39
11
  // src/cli/tui/App.tsx
40
12
  import { Box as Box8, Text as Text8, useApp, useInput } from "ink";
41
- import { useMemo, useState } from "react";
13
+ import { useMemo, useRef, useState } from "react";
42
14
 
43
15
  // src/cli/tui/components/Header.tsx
44
16
  import { Box, Text } from "ink";
@@ -168,6 +140,41 @@ function ModelChoiceScreen({
168
140
  ] }) : null
169
141
  ] });
170
142
  }
143
+ function EffortChoiceScreen({
144
+ model,
145
+ currentEffort,
146
+ draftEffort,
147
+ choices,
148
+ selected
149
+ }) {
150
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
151
+ /* @__PURE__ */ jsxs3(Text3, { children: [
152
+ "Model: ",
153
+ /* @__PURE__ */ jsx3(Text3, { color: theme.accent, children: model })
154
+ ] }),
155
+ /* @__PURE__ */ jsxs3(Text3, { children: [
156
+ "Current effort: ",
157
+ /* @__PURE__ */ jsx3(Text3, { color: theme.accent, children: currentEffort })
158
+ ] }),
159
+ /* @__PURE__ */ jsxs3(Text3, { children: [
160
+ "New effort: ",
161
+ /* @__PURE__ */ jsx3(Text3, { color: theme.warning, children: draftEffort })
162
+ ] }),
163
+ choices.length === 1 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "No explicit effort options are available for this model." }) : null,
164
+ choices.map((effort, index) => /* @__PURE__ */ jsxs3(
165
+ Text3,
166
+ {
167
+ color: index === selected ? theme.accent : void 0,
168
+ children: [
169
+ index === selected ? ">" : " ",
170
+ " ",
171
+ effort
172
+ ]
173
+ },
174
+ effort
175
+ ))
176
+ ] });
177
+ }
171
178
 
172
179
  // src/cli/tui/components/ModelScreen.tsx
173
180
  import { Box as Box4, Text as Text4 } from "ink";
@@ -196,7 +203,13 @@ function ModelScreen({
196
203
  role.role,
197
204
  ": ",
198
205
  role.model,
199
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: role.dirty ? ` (was ${role.currentModel})` : "" })
206
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
207
+ " ",
208
+ "\xB7 effort",
209
+ " ",
210
+ role.effort?.kind === "effort" ? role.effort.value : "inherit"
211
+ ] }),
212
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: role.dirty ? ` (was ${role.currentModel} \xB7 effort ${role.currentEffort.kind === "effort" ? role.currentEffort.value : "inherit"})` : "" })
200
213
  ]
201
214
  },
202
215
  role.role
@@ -498,277 +511,6 @@ function StatusView({ report }) {
498
511
  ] });
499
512
  }
500
513
 
501
- // src/cli/tui/model-catalog.ts
502
- import { execFileSync } from "child_process";
503
- var MODEL_CATALOG_TIMEOUT_MS = 5e3;
504
- var MODELS_DEV_MAX_BUFFER = 8 * 1024 * 1024;
505
- function parseOpenCodeModels(output) {
506
- const seen = /* @__PURE__ */ new Set();
507
- const options = [];
508
- for (const line of output.split(/\r?\n/)) {
509
- const match = line.trim().match(/^([a-z0-9_-]+)\/([^\s{]+)/i);
510
- if (!match) continue;
511
- const id = `${match[1]}/${match[2]}`;
512
- if (seen.has(id)) continue;
513
- seen.add(id);
514
- options.push({ id, label: id, provider: match[1] ?? "unknown" });
515
- }
516
- return options;
517
- }
518
- function parseModelsDevOpenAi(output) {
519
- const catalog = JSON.parse(output);
520
- return Object.entries(catalog.openai?.models ?? {}).filter(([id]) => isCodexOpenAiModelId(id)).map(([id, model]) => ({
521
- id,
522
- label: model.name ?? id,
523
- provider: "openai"
524
- }));
525
- }
526
- function isCodexOpenAiModelId(id) {
527
- const match = id.match(/^gpt-(\d+)(?:[.-]|$)/);
528
- return match?.[1] !== void 0 && Number(match[1]) >= 5;
529
- }
530
- function getOpenCodeModelsInvocation(platform = process.platform) {
531
- const options = {
532
- encoding: "utf8",
533
- stdio: ["ignore", "pipe", "ignore"],
534
- timeout: MODEL_CATALOG_TIMEOUT_MS
535
- };
536
- if (platform === "win32") {
537
- return {
538
- command: "opencode models",
539
- args: [],
540
- options: { ...options, shell: true }
541
- };
542
- }
543
- return {
544
- command: "opencode",
545
- args: ["models"],
546
- options
547
- };
548
- }
549
- function getModelsDevCatalog() {
550
- try {
551
- const output = execFileSync(
552
- process.execPath,
553
- [
554
- "-e",
555
- [
556
- "const controller = new AbortController();",
557
- `const timeout = setTimeout(() => controller.abort(), ${MODEL_CATALOG_TIMEOUT_MS});`,
558
- "if (typeof fetch !== 'function') {",
559
- " console.error('fetch unavailable');",
560
- " process.exit(1);",
561
- "}",
562
- "fetch('https://models.dev/api.json', { signal: controller.signal })",
563
- " .then(async (response) => {",
564
- " if (!response.ok) throw new Error(String(response.status));",
565
- " const catalog = await response.json();",
566
- " const models = catalog?.openai?.models ?? {};",
567
- " return JSON.stringify({ openai: { models } });",
568
- " })",
569
- " .then((body) => {",
570
- " clearTimeout(timeout);",
571
- " process.stdout.write(body);",
572
- " })",
573
- " .catch((error) => { clearTimeout(timeout); console.error(error.message); process.exit(1); });"
574
- ].join("\n")
575
- ],
576
- {
577
- encoding: "utf8",
578
- maxBuffer: MODELS_DEV_MAX_BUFFER,
579
- stdio: ["ignore", "pipe", "ignore"],
580
- timeout: MODEL_CATALOG_TIMEOUT_MS
581
- }
582
- );
583
- return parseModelsDevOpenAi(output);
584
- } catch {
585
- return [];
586
- }
587
- }
588
- var CLAUDE_CODE_MODEL_OPTIONS = [
589
- { id: "sonnet", label: "sonnet", provider: "anthropic" },
590
- { id: "opus", label: "opus", provider: "anthropic" },
591
- { id: "haiku", label: "haiku", provider: "anthropic" },
592
- {
593
- id: "inherit",
594
- label: "inherit (main session model)",
595
- provider: "anthropic"
596
- }
597
- ];
598
- function getModelOptions(harness) {
599
- if (harness === "codex") return getModelsDevCatalog();
600
- if (harness === "claude") return CLAUDE_CODE_MODEL_OPTIONS;
601
- try {
602
- const invocation = getOpenCodeModelsInvocation();
603
- const output = execFileSync(
604
- invocation.command,
605
- invocation.args,
606
- invocation.options
607
- );
608
- return parseOpenCodeModels(output);
609
- } catch {
610
- return [];
611
- }
612
- }
613
-
614
- // src/cli/tui/operations.ts
615
- var context = { cwd: process.cwd() };
616
- var codexContext = { cwd: process.cwd() };
617
- var claudeCodeContext = {
618
- cwd: process.cwd(),
619
- scope: "user"
620
- };
621
- var opencodeModelRoles = ALL_AGENT_NAMES.map(
622
- (role) => ({
623
- role,
624
- model: DEFAULT_MODELS[role] ?? "openai/gpt-5.4"
625
- })
626
- );
627
- var codexModelRoles = CODEX_ROLE_NAMES.map(
628
- (role) => ({
629
- role,
630
- model: "gpt-5.4-mini"
631
- })
632
- );
633
- var codexDefaultModels = new Map(
634
- codexModelRoles.map((role) => [role.role, role.model])
635
- );
636
- function codexInstallConfig(source, dryRun) {
637
- return {
638
- dryRun,
639
- reset: false,
640
- scope: source.scope ?? "user",
641
- projectRoot: source.cwd,
642
- homeDir: source.homeDir,
643
- codexHome: source.codexHome,
644
- packageRoot: source.packageRoot,
645
- pluginId: source.pluginId
646
- };
647
- }
648
- function getCodexModelRoles(source = codexContext) {
649
- try {
650
- const plan = buildCodexSetupPlan(codexInstallConfig(source, true));
651
- return CODEX_ROLE_NAMES.map((role) => {
652
- const item = plan.items.find(
653
- (candidate) => candidate.action === "write-role-toml" && candidate.role === role
654
- );
655
- return {
656
- role,
657
- model: (item?.content ? parseRoleTomlModel(item.content) : void 0) ?? codexDefaultModels.get(role) ?? "gpt-5.4-mini"
658
- };
659
- });
660
- } catch {
661
- return codexModelRoles.map((role) => ({ ...role }));
662
- }
663
- }
664
- function getClaudeCodeModelRoles(source = claudeCodeContext) {
665
- try {
666
- const plan = buildClaudeCodeSetupPlan({
667
- dryRun: true,
668
- reset: false,
669
- scope: source.scope ?? "user",
670
- projectRoot: source.cwd,
671
- homeDir: source.homeDir,
672
- packageRoot: source.packageRoot
673
- });
674
- return CLAUDE_CODE_ROLE_NAMES.map((role) => {
675
- const item = plan.items.find(
676
- (candidate) => candidate.kind === "subagent" && candidate.role === role
677
- );
678
- const model = item?.content ? parseSubagentModel(item.content) : void 0;
679
- return { role, model: model ?? "inherit" };
680
- });
681
- } catch {
682
- return defaultClaudeCodeModelRoles();
683
- }
684
- }
685
- function readRoleModel(config, role) {
686
- if (!config || typeof config !== "object" || Array.isArray(config)) {
687
- return void 0;
688
- }
689
- const record = config;
690
- const value = record[role];
691
- if (!value || typeof value !== "object" || Array.isArray(value)) {
692
- return void 0;
693
- }
694
- const model = value.model;
695
- return typeof model === "string" && model.length > 0 ? model : void 0;
696
- }
697
- function getOpenCodeModelRoles() {
698
- const parsed = parseConfig(getExistingLiteConfigPath());
699
- const agents = parsed.config?.agents && typeof parsed.config.agents === "object" ? parsed.config.agents : void 0;
700
- const presets = parsed.config?.presets && typeof parsed.config.presets === "object" ? parsed.config.presets : {};
701
- const openaiPreset = presets.openai && typeof presets.openai === "object" ? presets.openai : void 0;
702
- return ALL_AGENT_NAMES.map((role) => ({
703
- role,
704
- model: readRoleModel(agents, role) ?? readRoleModel(openaiPreset, role) ?? DEFAULT_MODELS[role] ?? "openai/gpt-5.4"
705
- }));
706
- }
707
- function buildTuiModelPlan(harness, roles) {
708
- if (harness === "opencode") {
709
- return buildOpenCodeModelPlan({ harness, dryRun: true, roles }, context);
710
- }
711
- if (harness === "claude") {
712
- return buildClaudeCodeModelPlan(
713
- { harness, dryRun: true, roles },
714
- claudeCodeContext
715
- );
716
- }
717
- return buildCodexModelPlan({ harness, dryRun: true, roles }, codexContext);
718
- }
719
- var defaultTuiOperations = {
720
- status(harness) {
721
- if (harness === "opencode") return getOpenCodeStatus(context);
722
- if (harness === "claude") {
723
- return getClaudeCodeStatus(claudeCodeContext);
724
- }
725
- return getCodexStatus(codexContext);
726
- },
727
- modelRoles(harness) {
728
- if (harness === "opencode") return getOpenCodeModelRoles();
729
- if (harness === "claude") {
730
- return getClaudeCodeModelRoles(claudeCodeContext);
731
- }
732
- return getCodexModelRoles(codexContext);
733
- },
734
- modelOptions(harness) {
735
- return getModelOptions(harness);
736
- },
737
- plan(harness, action) {
738
- if (harness === "opencode") {
739
- if (action === "install") return buildOpenCodeInstallPlan(context);
740
- if (action === "update") return buildOpenCodeUpdatePlan(context);
741
- if (action === "sync") return buildOpenCodeSyncPlan(context);
742
- return buildTuiModelPlan(harness, getOpenCodeModelRoles());
743
- }
744
- if (harness === "claude") {
745
- if (action === "install") {
746
- return buildClaudeCodeInstallPlan(claudeCodeContext);
747
- }
748
- if (action === "update") {
749
- return buildClaudeCodeUpdatePlan(claudeCodeContext);
750
- }
751
- if (action === "sync") return buildClaudeCodeSyncPlan(claudeCodeContext);
752
- return buildTuiModelPlan(
753
- harness,
754
- getClaudeCodeModelRoles(claudeCodeContext)
755
- );
756
- }
757
- if (action === "install") return buildCodexInstallPlan(codexContext);
758
- if (action === "update") return buildCodexUpdatePlan(codexContext);
759
- if (action === "sync") return buildCodexSyncPlan(codexContext);
760
- return buildTuiModelPlan(harness, getCodexModelRoles(codexContext));
761
- },
762
- modelPlan(harness, roles) {
763
- return buildTuiModelPlan(harness, roles);
764
- },
765
- apply(plan) {
766
- if (plan.harness === "opencode") return applyOpenCodePlan(plan);
767
- if (plan.harness === "claude") return applyClaudeCodePlan(plan);
768
- return applyCodexPlan(plan);
769
- }
770
- };
771
-
772
514
  // src/cli/tui/App.tsx
773
515
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
774
516
  var rootItems = [
@@ -873,7 +615,46 @@ function normalizeSelection(current, items) {
873
615
  return Math.min(current, items.length - 1);
874
616
  }
875
617
  function changedRoles(rows) {
876
- return rows.filter((role) => role.dirty).map((role) => ({ role: role.role, model: role.model }));
618
+ return rows.filter((role) => role.dirty).map((role) => {
619
+ const effortChanged = role.effort?.kind !== role.currentEffort.kind || role.effort?.kind === "effort" && role.currentEffort.kind === "effort" && role.effort.value !== role.currentEffort.value;
620
+ const includeEffort = effortChanged || role.effort?.kind === "effort";
621
+ return {
622
+ role: role.role,
623
+ model: role.model,
624
+ ...includeEffort ? { effort: role.effort } : {},
625
+ ...includeEffort && role.provider ? { provider: role.provider } : {},
626
+ ...includeEffort && role.catalogId ? { catalogId: role.catalogId } : {},
627
+ ...includeEffort && role.availableEfforts ? { availableEfforts: role.availableEfforts } : {}
628
+ };
629
+ });
630
+ }
631
+ function metadataForModel(role, model, option) {
632
+ if (option) {
633
+ return {
634
+ provider: option.provider,
635
+ ...option.catalogId ? { catalogId: option.catalogId } : {},
636
+ availableEfforts: option.efforts
637
+ };
638
+ }
639
+ if (model !== role.model) return {};
640
+ return {
641
+ ...role.provider ? { provider: role.provider } : {},
642
+ ...role.catalogId ? { catalogId: role.catalogId } : {},
643
+ ...role.availableEfforts ? { availableEfforts: role.availableEfforts } : {}
644
+ };
645
+ }
646
+ function choicesForRole(role, options, draftModel) {
647
+ const exact = options.find((option) => option.id === draftModel);
648
+ const sameModel = draftModel === role.model;
649
+ const current = exact ?? {
650
+ id: draftModel,
651
+ ...sameModel && role.catalogId ? { catalogId: role.catalogId } : {},
652
+ label: draftModel,
653
+ provider: sameModel ? role.provider ?? "current" : "manual",
654
+ efforts: sameModel ? role.availableEfforts ?? [] : [],
655
+ source: "manual"
656
+ };
657
+ return [current, ...options.filter((option) => option.id !== draftModel)];
877
658
  }
878
659
  function App({
879
660
  operations = defaultTuiOperations,
@@ -899,8 +680,14 @@ function App({
899
680
  const [modelHarness, setModelHarness] = useState("codex");
900
681
  const [modelRoles, setModelRoles] = useState([]);
901
682
  const [modelOptions, setModelOptions] = useState([]);
902
- const [choiceSelected, setChoiceSelected] = useState(0);
683
+ const [modelCatalogLoading, setModelCatalogLoading] = useState(false);
684
+ const [modelCatalogError, setModelCatalogError] = useState();
685
+ const modelCatalogRequest = useRef(0);
686
+ const [modelChoiceSelection, setModelChoiceSelection] = useState({ kind: "manual" });
903
687
  const [editedModels, setEditedModels] = useState({});
688
+ const [editedEfforts, setEditedEfforts] = useState({});
689
+ const [draftModelOption, setDraftModelOption] = useState();
690
+ const [effortSelected, setEffortSelected] = useState(0);
904
691
  const [modelSelected, setModelSelected] = useState(0);
905
692
  const [editingRole, setEditingRole] = useState();
906
693
  const [editDraft, setEditDraft] = useState("");
@@ -909,20 +696,37 @@ function App({
909
696
  const currentManageItems = report?.state === "missing" ? installManageItems : manageItems;
910
697
  const modelRows = modelRoles.map((role) => {
911
698
  const model = editedModels[role.role] ?? role.model;
699
+ const option = modelOptions.find((candidate) => candidate.id === model);
700
+ const metadata = metadataForModel(role, model, option);
701
+ const effort = editedEfforts[role.role] ?? role.effort ?? { kind: "inherit" };
702
+ const currentEffort = role.effort ?? { kind: "inherit" };
703
+ const effortDirty = effort.kind !== currentEffort.kind || effort.kind === "effort" && currentEffort.kind === "effort" && effort.value !== currentEffort.value;
912
704
  return {
913
- ...role,
705
+ role: role.role,
914
706
  model,
707
+ effort,
708
+ ...metadata,
915
709
  currentModel: role.model,
916
- dirty: model !== role.model
710
+ currentEffort,
711
+ dirty: model !== role.model || effortDirty
917
712
  };
918
713
  });
714
+ const modelChoiceOptions = editingRole ? choicesForRole(
715
+ editingRole,
716
+ modelOptions,
717
+ editedModels[editingRole.role] ?? editingRole.model
718
+ ) : modelOptions;
719
+ const selectedModelIndex = modelChoiceSelection.kind === "manual" ? modelChoiceOptions.length : modelChoiceOptions.findIndex(
720
+ (option) => option.id === modelChoiceSelection.id
721
+ );
722
+ const choiceSelected = selectedModelIndex < 0 ? 0 : selectedModelIndex;
919
723
  const dirtyRoles = changedRoles(modelRows);
920
724
  const modelActions = dirtyRoles.length > 0 ? ["Preview changes", "Apply changes", "Back"] : ["Back"];
921
725
  const modelMenuItems = [
922
726
  ...modelRows.map((role) => ({
923
727
  id: role.role,
924
728
  label: `${role.dirty ? "* " : ""}${role.role}`,
925
- detail: role.model
729
+ detail: `${role.model} \xB7 ${role.effort?.kind === "effort" ? role.effort.value : "inherit"}`
926
730
  })),
927
731
  ...modelActions.map((action) => ({
928
732
  id: action,
@@ -946,6 +750,8 @@ function App({
946
750
  setView("manageHarness");
947
751
  } else if (view === "modelChoice") {
948
752
  setView("modelRoles");
753
+ } else if (view === "effortChoice") {
754
+ setView("modelChoice");
949
755
  } else if (view === "modelEdit") {
950
756
  setView("modelChoice");
951
757
  }
@@ -964,13 +770,44 @@ function App({
964
770
  setView("preview");
965
771
  }
966
772
  function openModelRoles(harness) {
773
+ const request = modelCatalogRequest.current + 1;
774
+ modelCatalogRequest.current = request;
967
775
  setModelHarness(harness);
968
776
  setModelRoles(operations.modelRoles(harness));
969
- setModelOptions(operations.modelOptions(harness));
777
+ setModelOptions([]);
778
+ setModelCatalogLoading(true);
779
+ setModelCatalogError(void 0);
970
780
  setEditedModels({});
781
+ setEditedEfforts({});
971
782
  setModelResult(void 0);
972
783
  setModelSelected(0);
973
784
  setView("modelRoles");
785
+ void operations.modelOptions(harness).then((options) => {
786
+ if (modelCatalogRequest.current !== request) return;
787
+ setModelOptions(options);
788
+ setModelCatalogLoading(false);
789
+ }).catch((error) => {
790
+ if (modelCatalogRequest.current !== request) return;
791
+ setModelCatalogError(
792
+ error instanceof Error ? error.message : String(error)
793
+ );
794
+ setModelCatalogLoading(false);
795
+ });
796
+ }
797
+ function beginEffortSelection(role, model, option) {
798
+ const choices = effortChoicesForModel(option);
799
+ const pending = editedEfforts[role.role] ?? role.effort ?? { kind: "inherit" };
800
+ const nextEffort = pending.kind === "effort" && !option?.efforts.includes(pending.value) ? { kind: "inherit" } : pending;
801
+ setEditedModels((models) => ({ ...models, [role.role]: model }));
802
+ setModelChoiceSelection({ kind: "model", id: model });
803
+ setEditedEfforts((efforts) => ({
804
+ ...efforts,
805
+ [role.role]: nextEffort
806
+ }));
807
+ setDraftModelOption(option);
808
+ const nextIndex = nextEffort.kind === "effort" ? choices.indexOf(nextEffort.value) : 0;
809
+ setEffortSelected(nextIndex < 0 ? 0 : nextIndex);
810
+ setView("effortChoice");
974
811
  }
975
812
  function previewModelChanges(applyImmediately) {
976
813
  const nextPlan = operations.modelPlan(modelHarness, dirtyRoles);
@@ -982,12 +819,21 @@ function App({
982
819
  setModelResult(applyResult);
983
820
  if (applyResult.applied) {
984
821
  setModelRoles(
985
- (roles) => roles.map((role) => ({
986
- ...role,
987
- model: editedModels[role.role] ?? role.model
988
- }))
822
+ (roles) => roles.map((role) => {
823
+ const model = editedModels[role.role] ?? role.model;
824
+ const option = modelOptions.find(
825
+ (candidate) => candidate.id === model
826
+ );
827
+ return {
828
+ role: role.role,
829
+ model,
830
+ effort: editedEfforts[role.role] ?? role.effort ?? { kind: "inherit" },
831
+ ...metadataForModel(role, model, option)
832
+ };
833
+ })
989
834
  );
990
835
  setEditedModels({});
836
+ setEditedEfforts({});
991
837
  }
992
838
  return;
993
839
  }
@@ -1001,11 +847,7 @@ function App({
1001
847
  return;
1002
848
  }
1003
849
  if (key.return && editingRole) {
1004
- setEditedModels((models) => ({
1005
- ...models,
1006
- [editingRole.role]: editDraft
1007
- }));
1008
- setView("modelRoles");
850
+ beginEffortSelection(editingRole, editDraft, void 0);
1009
851
  return;
1010
852
  }
1011
853
  if (key.tab) {
@@ -1022,34 +864,67 @@ function App({
1022
864
  }
1023
865
  return;
1024
866
  }
867
+ if (view === "effortChoice") {
868
+ if (key.escape) {
869
+ goBack();
870
+ return;
871
+ }
872
+ const choices = effortChoicesForModel(draftModelOption);
873
+ const choiceItems = choices.map((effort) => ({
874
+ id: effort,
875
+ label: effort,
876
+ detail: effort === "inherit" ? "Use the harness default" : "Set an explicit reasoning effort"
877
+ }));
878
+ if (key.upArrow || input === "k") {
879
+ setEffortSelected((current) => moveSelection(current, -1, choiceItems));
880
+ }
881
+ if (key.downArrow || input === "j") {
882
+ setEffortSelected((current) => moveSelection(current, 1, choiceItems));
883
+ }
884
+ if (key.return && editingRole) {
885
+ const selectedEffort = choices[effortSelected] ?? "inherit";
886
+ setEditedEfforts((efforts) => ({
887
+ ...efforts,
888
+ [editingRole.role]: selectedEffort === "inherit" ? { kind: "inherit" } : { kind: "effort", value: selectedEffort }
889
+ }));
890
+ setView("modelRoles");
891
+ }
892
+ return;
893
+ }
1025
894
  if (view === "modelChoice") {
1026
895
  if (key.escape) {
1027
896
  goBack();
1028
897
  return;
1029
898
  }
1030
899
  const choiceItems = [
1031
- ...modelOptions.map((option) => ({
900
+ ...modelChoiceOptions.map((option) => ({
1032
901
  id: option.id,
1033
902
  label: option.id,
1034
903
  detail: option.provider
1035
904
  })),
1036
905
  { id: "manual", label: "Manual entry", detail: "Type a model ID" }
1037
906
  ];
907
+ const selectChoiceAt = (index) => {
908
+ const option = modelChoiceOptions[index];
909
+ setModelChoiceSelection(
910
+ option ? { kind: "model", id: option.id } : { kind: "manual" }
911
+ );
912
+ };
1038
913
  if (key.upArrow || input === "k") {
1039
- setChoiceSelected((current) => moveSelection(current, -1, choiceItems));
914
+ selectChoiceAt(moveSelection(choiceSelected, -1, choiceItems));
1040
915
  }
1041
916
  if (key.downArrow || input === "j") {
1042
- setChoiceSelected((current) => moveSelection(current, 1, choiceItems));
917
+ selectChoiceAt(moveSelection(choiceSelected, 1, choiceItems));
1043
918
  }
1044
919
  if (key.return && editingRole) {
1045
- const option = modelOptions[choiceSelected];
1046
- if (option) {
1047
- setEditedModels((models) => ({
1048
- ...models,
1049
- [editingRole.role]: option.id
1050
- }));
1051
- setView("modelRoles");
1052
- return;
920
+ if (modelChoiceSelection.kind === "model") {
921
+ const option = modelChoiceOptions.find(
922
+ (candidate) => candidate.id === modelChoiceSelection.id
923
+ );
924
+ if (option) {
925
+ beginEffortSelection(editingRole, option.id, option);
926
+ return;
927
+ }
1053
928
  }
1054
929
  setEditDraft(editingRole.model);
1055
930
  setView("modelEdit");
@@ -1172,7 +1047,11 @@ function App({
1172
1047
  const role = modelRows[modelSelected];
1173
1048
  setEditingRole(role);
1174
1049
  setEditDraft(role?.model ?? "");
1175
- setChoiceSelected(0);
1050
+ setDraftModelOption(void 0);
1051
+ setModelChoiceSelection({
1052
+ kind: "model",
1053
+ id: role?.model ?? ""
1054
+ });
1176
1055
  setView("modelChoice");
1177
1056
  return;
1178
1057
  }
@@ -1276,6 +1155,12 @@ function App({
1276
1155
  actions: modelActions
1277
1156
  }
1278
1157
  ),
1158
+ modelCatalogLoading ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Loading model catalog\u2026" }) : null,
1159
+ modelCatalogError ? /* @__PURE__ */ jsxs7(Text8, { color: theme.warning, children: [
1160
+ "Could not load model catalog: ",
1161
+ modelCatalogError,
1162
+ ". Manual entry is still available."
1163
+ ] }) : null,
1279
1164
  modelResult ? /* @__PURE__ */ jsx8(Text8, { color: modelResult.applied ? theme.ok : theme.warning, children: modelResult.summary }) : null
1280
1165
  ] });
1281
1166
  }
@@ -1304,7 +1189,7 @@ function App({
1304
1189
  Header,
1305
1190
  {
1306
1191
  title: `Choose ${editingRole.role} model`,
1307
- subtitle: "Select a catalog option or Manual entry."
1192
+ subtitle: "Current model is selected first. Press Enter to continue to effort, or choose Manual entry."
1308
1193
  }
1309
1194
  ),
1310
1195
  /* @__PURE__ */ jsx8(
@@ -1312,9 +1197,39 @@ function App({
1312
1197
  {
1313
1198
  currentModel: editingRole.currentModel,
1314
1199
  draftModel: editedModels[editingRole.role] ?? editingRole.model,
1315
- options: modelOptions,
1200
+ options: modelChoiceOptions,
1316
1201
  selected: choiceSelected
1317
1202
  }
1203
+ ),
1204
+ modelCatalogLoading ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "Loading model catalog\u2026" }) : null,
1205
+ modelCatalogError ? /* @__PURE__ */ jsxs7(Text8, { color: theme.warning, children: [
1206
+ "Could not load model catalog: ",
1207
+ modelCatalogError
1208
+ ] }) : null
1209
+ ] });
1210
+ }
1211
+ if (view === "effortChoice" && editingRole) {
1212
+ const draftModel = editedModels[editingRole.role] ?? editingRole.model;
1213
+ const effort = editedEfforts[editingRole.role] ?? {
1214
+ kind: "inherit"
1215
+ };
1216
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
1217
+ /* @__PURE__ */ jsx8(
1218
+ Header,
1219
+ {
1220
+ title: `Choose ${editingRole.role} effort`,
1221
+ subtitle: "Select inherit or an effort supported by this model."
1222
+ }
1223
+ ),
1224
+ /* @__PURE__ */ jsx8(
1225
+ EffortChoiceScreen,
1226
+ {
1227
+ model: draftModel,
1228
+ currentEffort: editingRole.currentEffort.kind === "effort" ? editingRole.currentEffort.value : "inherit",
1229
+ draftEffort: effort.kind === "effort" ? effort.value : "inherit",
1230
+ choices: effortChoicesForModel(draftModelOption),
1231
+ selected: effortSelected
1232
+ }
1318
1233
  )
1319
1234
  ] });
1320
1235
  }