@gajae-code/coding-agent 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/types/cli/skills-cli.d.ts +9 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/session.d.ts +24 -0
  5. package/dist/types/commands/skills.d.ts +26 -0
  6. package/dist/types/config/model-registry.d.ts +33 -4
  7. package/dist/types/config/models-config-schema.d.ts +52 -5
  8. package/dist/types/config/settings-schema.d.ts +1 -24
  9. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +15 -0
  10. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  11. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  12. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  14. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  15. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  17. package/dist/types/goals/runtime.d.ts +3 -9
  18. package/dist/types/goals/state.d.ts +3 -6
  19. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  20. package/dist/types/modes/components/model-selector.d.ts +21 -1
  21. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  22. package/dist/types/modes/components/status-line.d.ts +0 -3
  23. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -12
  25. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  26. package/dist/types/modes/theme/theme.d.ts +1 -2
  27. package/dist/types/modes/types.d.ts +1 -7
  28. package/dist/types/session/agent-session.d.ts +2 -0
  29. package/dist/types/session/contribution-prep.d.ts +47 -0
  30. package/dist/types/skill-state/active-state.d.ts +4 -0
  31. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  32. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  33. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  34. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  35. package/package.json +7 -7
  36. package/src/cli/args.ts +17 -2
  37. package/src/cli/skills-cli.ts +88 -0
  38. package/src/cli.ts +7 -1
  39. package/src/commands/contribution-prep.ts +41 -0
  40. package/src/commands/deep-interview.ts +6 -22
  41. package/src/commands/launch.ts +10 -1
  42. package/src/commands/ralplan.ts +10 -22
  43. package/src/commands/session.ts +150 -0
  44. package/src/commands/skills.ts +48 -0
  45. package/src/commands/state.ts +14 -4
  46. package/src/commands/team.ts +23 -3
  47. package/src/commit/agentic/index.ts +1 -0
  48. package/src/commit/pipeline.ts +1 -0
  49. package/src/config/model-registry.ts +269 -10
  50. package/src/config/models-config-schema.ts +124 -88
  51. package/src/config/settings-schema.ts +1 -25
  52. package/src/config.ts +1 -1
  53. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -13
  54. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -2
  55. package/src/defaults/gjc/skills/team/SKILL.md +29 -7
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +23 -25
  57. package/src/eval/py/prelude.py +1 -1
  58. package/src/gjc-runtime/deep-interview-runtime.ts +279 -0
  59. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  60. package/src/gjc-runtime/launch-tmux.ts +83 -43
  61. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  62. package/src/gjc-runtime/state-runtime.ts +562 -0
  63. package/src/gjc-runtime/team-runtime.ts +708 -52
  64. package/src/gjc-runtime/tmux-common.ts +119 -0
  65. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  66. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  67. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  68. package/src/goals/runtime.ts +38 -144
  69. package/src/goals/state.ts +36 -7
  70. package/src/goals/tools/goal-tool.ts +15 -172
  71. package/src/hooks/skill-state.ts +31 -12
  72. package/src/internal-urls/docs-index.generated.ts +4 -3
  73. package/src/main.ts +10 -1
  74. package/src/modes/components/model-selector.ts +109 -28
  75. package/src/modes/components/skill-hud/render.ts +4 -0
  76. package/src/modes/components/status-line/segments.ts +5 -16
  77. package/src/modes/components/status-line/types.ts +0 -3
  78. package/src/modes/components/status-line.ts +0 -6
  79. package/src/modes/controllers/command-controller.ts +25 -1
  80. package/src/modes/controllers/input-controller.ts +0 -15
  81. package/src/modes/controllers/selector-controller.ts +42 -2
  82. package/src/modes/interactive-mode.ts +18 -219
  83. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  84. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  85. package/src/modes/theme/theme.ts +0 -6
  86. package/src/modes/types.ts +1 -7
  87. package/src/prompts/goals/goal-continuation.md +1 -4
  88. package/src/prompts/goals/goal-mode-active.md +3 -5
  89. package/src/prompts/system/system-prompt.md +5 -7
  90. package/src/prompts/tools/goal.md +4 -4
  91. package/src/sdk.ts +2 -1
  92. package/src/session/agent-session.ts +18 -0
  93. package/src/session/contribution-prep.ts +320 -0
  94. package/src/setup/provider-onboarding.ts +2 -0
  95. package/src/skill-state/active-state.ts +38 -0
  96. package/src/skill-state/deep-interview-mutation-guard.ts +88 -24
  97. package/src/skill-state/workflow-hud.ts +23 -5
  98. package/src/skill-state/workflow-state-contract.ts +121 -0
  99. package/src/slash-commands/acp-builtins.ts +11 -2
  100. package/src/slash-commands/builtin-registry.ts +40 -13
  101. package/src/task/commands.ts +1 -5
  102. package/src/tools/gh.ts +212 -2
  103. package/src/tools/index.ts +2 -5
  104. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  105. package/dist/types/commands/question.d.ts +0 -7
  106. package/dist/types/modes/loop-limit.d.ts +0 -22
  107. package/src/commands/gjc-runtime-bridge.ts +0 -227
  108. package/src/commands/question.ts +0 -12
  109. package/src/modes/loop-limit.ts +0 -140
  110. package/src/prompts/commands/orchestrate.md +0 -49
  111. package/src/prompts/goals/goal-budget-limit.md +0 -16
  112. package/src/prompts/tools/create-goal.md +0 -3
  113. package/src/prompts/tools/get-goal.md +0 -3
  114. package/src/prompts/tools/update-goal.md +0 -3
package/src/main.ts CHANGED
@@ -46,6 +46,7 @@ import type { AgentSession } from "./session/agent-session";
46
46
  import type { AuthStorage } from "./session/auth-storage";
47
47
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
48
48
  import { formatModelOnboardingGuidance } from "./setup/model-onboarding-guidance";
49
+ import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
49
50
  import { resolvePromptInput } from "./system-prompt";
50
51
  import type { LspStartupServerInfo } from "./tools";
51
52
  import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
@@ -295,7 +296,14 @@ async function runInteractiveMode(
295
296
 
296
297
  for (const message of initialMessages) {
297
298
  try {
298
- await session.prompt(message);
299
+ let text = message;
300
+ const slashResult = await executeBuiltinSlashCommand(text, {
301
+ ctx: mode,
302
+ handleBackgroundCommand: () => mode.handleBackgroundCommand(),
303
+ });
304
+ if (slashResult === true) continue;
305
+ if (typeof slashResult === "string") text = slashResult;
306
+ await session.prompt(text);
299
307
  } catch (error: unknown) {
300
308
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
301
309
  mode.showError(errorMessage);
@@ -710,6 +718,7 @@ export async function runRootCommand(
710
718
  if (parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
711
719
  applyRpcDefaultSettingOverrides(settingsInstance);
712
720
  }
721
+ modelRegistry.applyConfiguredModelBindings(settingsInstance);
713
722
  if (parsedArgs.noPty || parsedArgs.mode === "rpc-ui") {
714
723
  Bun.env.PI_NO_PTY = "1";
715
724
  }
@@ -1,5 +1,5 @@
1
1
  import { ThinkingLevel } from "@gajae-code/agent-core";
2
- import { getSupportedEfforts, type Model, modelsAreEqual } from "@gajae-code/ai";
2
+ import { clampThinkingLevelForModel, getSupportedEfforts, type Model, modelsAreEqual } from "@gajae-code/ai";
3
3
  import {
4
4
  Container,
5
5
  fuzzyFilter,
@@ -81,18 +81,36 @@ interface RoleAssignment {
81
81
  thinkingLevel: ThinkingLevel;
82
82
  }
83
83
 
84
+ export interface ModelAssignmentPreset {
85
+ id: "openai-codex";
86
+ label: string;
87
+ description: string;
88
+ assignments: Partial<Record<GjcModelAssignmentTargetId, ThinkingLevel>>;
89
+ }
90
+
91
+ export type ModelSelectorSelection =
92
+ | {
93
+ kind: "assignment";
94
+ model: Model;
95
+ role: GjcModelAssignmentTargetId | null;
96
+ thinkingLevel?: ThinkingLevel;
97
+ selector?: string;
98
+ }
99
+ | {
100
+ kind: "preset";
101
+ model: Model;
102
+ selector: string;
103
+ preset: ModelAssignmentPreset;
104
+ assignments: Record<GjcModelAssignmentTargetId, ThinkingLevel>;
105
+ };
106
+
84
107
  interface PendingThinkingChoice {
85
108
  item: ModelItem | CanonicalModelItem;
86
109
  role: GjcModelAssignmentTargetId | null;
87
110
  levels: ThinkingLevel[];
88
111
  }
89
112
 
90
- type RoleSelectCallback = (
91
- model: Model,
92
- role: GjcModelAssignmentTargetId | null,
93
- thinkingLevel?: ThinkingLevel,
94
- selector?: string,
95
- ) => void;
113
+ type RoleSelectCallback = (selection: ModelSelectorSelection) => void;
96
114
  type CancelCallback = () => void;
97
115
 
98
116
  interface ProviderTabState {
@@ -107,6 +125,18 @@ const STATIC_PROVIDER_TABS: ProviderTabState[] = [
107
125
  { id: ALL_TAB, label: ALL_TAB },
108
126
  { id: CANONICAL_TAB, label: CANONICAL_TAB },
109
127
  ];
128
+ const OPENAI_CODE_PROFILE_PRESET: ModelAssignmentPreset = {
129
+ id: "openai-codex",
130
+ label: "Apply OpenAI Codex role preset",
131
+ description: "Default medium, Executor low, Architect xhigh, Planner medium, Critic high",
132
+ assignments: {
133
+ default: ThinkingLevel.Medium,
134
+ executor: ThinkingLevel.Low,
135
+ architect: ThinkingLevel.XHigh,
136
+ planner: ThinkingLevel.Medium,
137
+ critic: ThinkingLevel.High,
138
+ },
139
+ };
110
140
 
111
141
  function formatProviderTabLabel(providerId: string): string {
112
142
  return providerId.replace(/[-_]+/g, " ").toUpperCase();
@@ -156,12 +186,7 @@ export class ModelSelectorComponent extends Container {
156
186
  settings: Settings,
157
187
  modelRegistry: ModelRegistry,
158
188
  scopedModels: ReadonlyArray<ScopedModelItem>,
159
- onSelect: (
160
- model: Model,
161
- role: GjcModelAssignmentTargetId | null,
162
- thinkingLevel?: ThinkingLevel,
163
- selector?: string,
164
- ) => void,
189
+ onSelect: RoleSelectCallback,
165
190
  onCancel: () => void,
166
191
  options?: { temporaryOnly?: boolean; initialSearchInput?: string },
167
192
  ) {
@@ -750,11 +775,13 @@ export class ModelSelectorComponent extends Container {
750
775
  this.#listContainer.addChild(new Spacer(1));
751
776
  this.#listContainer.addChild(new Text(theme.fg("muted", ` Action for: ${item.model.id}`), 0, 0));
752
777
  this.#listContainer.addChild(new Spacer(1));
753
- for (let i = 0; i < GJC_MODEL_ASSIGNMENT_TARGET_IDS.length; i++) {
754
- const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
755
- const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
778
+ const actionCount = this.#getActionCount(item.model);
779
+ for (let i = 0; i < actionCount; i++) {
756
780
  const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
757
- const label = `Set as ${target.tag ?? role.toUpperCase()} (${target.name})`;
781
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
782
+ const label = role
783
+ ? `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`
784
+ : `${OPENAI_CODE_PROFILE_PRESET.label} (${OPENAI_CODE_PROFILE_PRESET.description})`;
758
785
  this.#listContainer.addChild(
759
786
  new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
760
787
  );
@@ -782,6 +809,9 @@ export class ModelSelectorComponent extends Container {
782
809
  #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
783
810
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
784
811
  }
812
+ #getActionCount(model: Model): number {
813
+ return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + (supportsOpenAICodexPreset(model) ? 1 : 0);
814
+ }
785
815
 
786
816
  #getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
787
817
  return this.#isCanonicalTab()
@@ -853,25 +883,27 @@ export class ModelSelectorComponent extends Container {
853
883
  }
854
884
 
855
885
  #handleActionMenuInput(keyData: string): void {
886
+ const item = this.#pendingActionItem;
887
+ if (!item) return;
888
+ const actionCount = this.#getActionCount(item.model);
856
889
  if (matchesKey(keyData, "up")) {
857
- this.#selectedActionIndex =
858
- this.#selectedActionIndex === 0
859
- ? GJC_MODEL_ASSIGNMENT_TARGET_IDS.length - 1
860
- : this.#selectedActionIndex - 1;
890
+ this.#selectedActionIndex = this.#selectedActionIndex === 0 ? actionCount - 1 : this.#selectedActionIndex - 1;
861
891
  this.#updateList();
862
892
  return;
863
893
  }
864
894
  if (matchesKey(keyData, "down")) {
865
- this.#selectedActionIndex = (this.#selectedActionIndex + 1) % GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
895
+ this.#selectedActionIndex = (this.#selectedActionIndex + 1) % actionCount;
866
896
  this.#updateList();
867
897
  return;
868
898
  }
869
899
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
870
- const item = this.#pendingActionItem;
871
- if (!item) return;
872
- const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
873
900
  this.#pendingActionItem = undefined;
874
- this.#handleSelect(item, role);
901
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
902
+ if (role) {
903
+ this.#handleSelect(item, role);
904
+ } else {
905
+ this.#handlePresetSelect(item, OPENAI_CODE_PROFILE_PRESET);
906
+ }
875
907
  return;
876
908
  }
877
909
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
@@ -910,6 +942,18 @@ export class ModelSelectorComponent extends Container {
910
942
  this.#updateList();
911
943
  }
912
944
  }
945
+ #handlePresetSelect(item: ModelItem | CanonicalModelItem, preset: ModelAssignmentPreset): void {
946
+ const selectorValue = item.selector;
947
+ const assignments = resolvePresetAssignments(item.model, preset);
948
+ for (const [role, thinkingLevel] of Object.entries(assignments) as [
949
+ GjcModelAssignmentTargetId,
950
+ ThinkingLevel,
951
+ ][]) {
952
+ this.#roles[role] = { model: item.model, thinkingLevel };
953
+ }
954
+ this.#onSelectCallback({ kind: "preset", model: item.model, selector: selectorValue, preset, assignments });
955
+ this.#updateList();
956
+ }
913
957
 
914
958
  #handleSelect(
915
959
  item: ModelItem | CanonicalModelItem,
@@ -927,7 +971,13 @@ export class ModelSelectorComponent extends Container {
927
971
 
928
972
  // For temporary role, don't save to settings - just notify caller
929
973
  if (role === null) {
930
- this.#onSelectCallback(item.model, null, itemThinkingLevel, item.selector);
974
+ this.#onSelectCallback({
975
+ kind: "assignment",
976
+ model: item.model,
977
+ role: null,
978
+ thinkingLevel: itemThinkingLevel,
979
+ selector: item.selector,
980
+ });
931
981
  return;
932
982
  }
933
983
 
@@ -939,7 +989,13 @@ export class ModelSelectorComponent extends Container {
939
989
  this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
940
990
 
941
991
  // Notify caller (for updating agent state if needed)
942
- this.#onSelectCallback(item.model, role, selectedThinkingLevel, selectorValue);
992
+ this.#onSelectCallback({
993
+ kind: "assignment",
994
+ model: item.model,
995
+ role,
996
+ thinkingLevel: selectedThinkingLevel,
997
+ selector: selectorValue,
998
+ });
943
999
 
944
1000
  // Update list to show new badges
945
1001
  this.#updateList();
@@ -954,6 +1010,31 @@ function requiresExplicitThinkingChoice(model: Model): boolean {
954
1010
  return model.reasoning === true && (model.provider === "openai" || model.provider === "openai-codex");
955
1011
  }
956
1012
 
1013
+ function supportsOpenAICodexPreset(model: Model): boolean {
1014
+ return model.provider === "openai-codex" && model.reasoning === true;
1015
+ }
1016
+
1017
+ function resolvePresetAssignments(
1018
+ model: Model,
1019
+ preset: ModelAssignmentPreset,
1020
+ ): Record<GjcModelAssignmentTargetId, ThinkingLevel> {
1021
+ const resolved = {} as Record<GjcModelAssignmentTargetId, ThinkingLevel>;
1022
+ for (const [role, requestedLevel] of Object.entries(preset.assignments) as [
1023
+ GjcModelAssignmentTargetId,
1024
+ ThinkingLevel,
1025
+ ][]) {
1026
+ const clampedLevel =
1027
+ requestedLevel === ThinkingLevel.Off || requestedLevel === ThinkingLevel.Inherit
1028
+ ? requestedLevel
1029
+ : clampThinkingLevelForModel(model, requestedLevel);
1030
+ if (!clampedLevel) {
1031
+ throw new Error(`Model ${model.provider}/${model.id} does not support ${requestedLevel} reasoning`);
1032
+ }
1033
+ resolved[role] = clampedLevel;
1034
+ }
1035
+ return resolved;
1036
+ }
1037
+
957
1038
  function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
958
1039
  const levels: ThinkingLevel[] = [ThinkingLevel.Off];
959
1040
  let efforts: readonly string[];
@@ -1,4 +1,5 @@
1
1
  import type { SkillActiveEntry, WorkflowHudChip } from "../../../skill-state/active-state";
2
+ import { workflowReceiptStatus } from "../../../skill-state/workflow-state-contract";
2
3
 
3
4
  const ANSI_RESET_FG = "\x1b[39m";
4
5
  const ANSI_RESET_BOLD = "\x1b[22m";
@@ -60,6 +61,9 @@ function formatEntry(entry: SkillActiveEntry): string {
60
61
  .map(formatChip)
61
62
  .filter((chip): chip is string => Boolean(chip));
62
63
  if (entry.stale === true) chips.unshift("warn:stale");
64
+ const receiptStatus = workflowReceiptStatus(entry.receipt);
65
+ if (receiptStatus === "stale") chips.unshift("warn:receipt=stale");
66
+ if (receiptStatus === "fresh") chips.push("receipt=fresh");
63
67
  const summary = sanitizeHudPart(entry.hud?.summary);
64
68
  return [base, summary, ...chips].filter(Boolean).join(" ");
65
69
  }
@@ -110,10 +110,9 @@ const modelSegment: StatusLineSegment = {
110
110
  },
111
111
  };
112
112
 
113
- function formatGoalBudget(current: number, budget?: number): string {
113
+ function formatGoalUsage(current: number): string {
114
114
  const used = formatNumber(current);
115
- if (budget === undefined) return used;
116
- return `${used}/${formatNumber(budget)}`;
115
+ return used;
117
116
  }
118
117
 
119
118
  function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
@@ -131,10 +130,6 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
131
130
  icon = theme.symbol("status.success");
132
131
  color = "success";
133
132
  break;
134
- case "budget-limited":
135
- icon = theme.symbol("status.warning");
136
- color = "warning";
137
- break;
138
133
  case "dropped":
139
134
  icon = theme.symbol("status.aborted");
140
135
  color = "dim";
@@ -144,9 +139,9 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
144
139
  }
145
140
 
146
141
  const parts: string[] = [withIcon(icon, "Goal")];
147
- const showBudget = ctx.session.settings.get("goal.statusInFooter") === true;
148
- if (showBudget && goal) {
149
- parts.push(formatGoalBudget(goal.tokensUsed, goal.tokenBudget));
142
+ const showUsage = ctx.session.settings.get("goal.statusInFooter") === true;
143
+ if (showUsage && goal) {
144
+ parts.push(formatGoalUsage(goal.tokensUsed));
150
145
  }
151
146
  return { content: theme.fg(color, parts.join(" ")), visible: true };
152
147
  }
@@ -169,12 +164,6 @@ const modeSegment: StatusLineSegment = {
169
164
  return renderGoalMode(ctx, goal);
170
165
  }
171
166
 
172
- const loop = ctx.loopMode;
173
- if (loop?.enabled) {
174
- const content = withIcon(theme.icon.loop, "Loop");
175
- return { content: theme.fg("customMessageLabel", content), visible: true };
176
- }
177
-
178
167
  return { content: "", visible: false };
179
168
  },
180
169
  };
@@ -24,9 +24,6 @@ export interface SegmentContext {
24
24
  enabled: boolean;
25
25
  paused: boolean;
26
26
  } | null;
27
- loopMode: {
28
- enabled: boolean;
29
- } | null;
30
27
  goalMode: {
31
28
  enabled: boolean;
32
29
  paused: boolean;
@@ -155,7 +155,6 @@ export class StatusLineComponent implements Component {
155
155
  #subagentCount: number = 0;
156
156
  #sessionStartTime: number = Date.now();
157
157
  #planModeStatus: { enabled: boolean; paused: boolean } | null = null;
158
- #loopModeStatus: { enabled: boolean } | null = null;
159
158
  #goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
160
159
  #skillHudEntries: SkillActiveEntry[] = [];
161
160
  #skillHudLastFetch = 0;
@@ -229,10 +228,6 @@ export class StatusLineComponent implements Component {
229
228
  this.#planModeStatus = status ?? null;
230
229
  }
231
230
 
232
- setLoopModeStatus(status: { enabled: boolean } | undefined): void {
233
- this.#loopModeStatus = status ?? null;
234
- }
235
-
236
231
  setGoalModeStatus(status: { enabled: boolean; paused: boolean } | undefined): void {
237
232
  this.#goalModeStatus = status ?? null;
238
233
  }
@@ -611,7 +606,6 @@ export class StatusLineComponent implements Component {
611
606
  width,
612
607
  options: this.#resolveSettings().segmentOptions ?? {},
613
608
  planMode: this.#planModeStatus,
614
- loopMode: this.#loopModeStatus,
615
609
  goalMode: this.#goalModeStatus,
616
610
  usageStats,
617
611
  contextPercent,
@@ -1248,7 +1248,31 @@ export class CommandController {
1248
1248
  this.ctx.statusContainer.clear();
1249
1249
  this.ctx.editor.onEscape = originalOnEscape;
1250
1250
  }
1251
- this.ctx.ui.requestRender();
1251
+ }
1252
+
1253
+ async handleContributionPrepCommand(customInstructions?: string): Promise<void> {
1254
+ this.ctx.editor.setText("");
1255
+ try {
1256
+ const result = await this.ctx.session.prepareContributionPrep({ customInstructions, spawnWorker: true });
1257
+ this.ctx.showStatus(
1258
+ [
1259
+ "Contribution prep artifacts written.",
1260
+ `Manifest: ${result.manifestPath}`,
1261
+ `Worker prompt: ${result.workerPromptPath}`,
1262
+ ].join("\n"),
1263
+ );
1264
+ this.ctx.chatContainer.addChild(
1265
+ new Text(
1266
+ `${theme.fg("accent", `${theme.status.success} Contribution prep ready`)}\nManifest: ${result.manifestPath}`,
1267
+ 1,
1268
+ 1,
1269
+ ),
1270
+ );
1271
+ this.ctx.ui.requestRender();
1272
+ } catch (error) {
1273
+ const message = error instanceof Error ? error.message : String(error);
1274
+ this.ctx.showError(`Contribution prep failed: ${message}`);
1275
+ }
1252
1276
  }
1253
1277
  }
1254
1278
 
@@ -51,15 +51,6 @@ export class InputController {
51
51
  this.ctx.retryEscapeHandler,
52
52
  );
53
53
  this.ctx.editor.onEscape = () => {
54
- if (this.ctx.loopModeEnabled) {
55
- this.ctx.pauseLoop();
56
- if (this.ctx.session.isStreaming) {
57
- void this.#abortInteractive();
58
- } else {
59
- this.ctx.cancelPendingSubmission();
60
- }
61
- return;
62
- }
63
54
  if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
64
55
  return;
65
56
  }
@@ -292,12 +283,6 @@ export class InputController {
292
283
  }
293
284
  }
294
285
 
295
- // While loop mode is on, every user-typed prompt becomes the new loop
296
- // prompt that auto-resubmits after each yield.
297
- if (this.ctx.loopModeEnabled) {
298
- this.ctx.loopPrompt = text;
299
- }
300
-
301
286
  // Queue input during compaction
302
287
  if (this.ctx.session.isCompacting) {
303
288
  if (this.ctx.pendingImages.length > 0) {
@@ -37,7 +37,7 @@ import { AgentDashboard } from "../components/agent-dashboard";
37
37
  import { AssistantMessageComponent } from "../components/assistant-message";
38
38
  import { ExtensionDashboard } from "../components/extensions";
39
39
  import { HistorySearchComponent } from "../components/history-search";
40
- import { ModelSelectorComponent } from "../components/model-selector";
40
+ import { ModelSelectorComponent, type ModelSelectorSelection } from "../components/model-selector";
41
41
  import { OAuthSelectorComponent } from "../components/oauth-selector";
42
42
  import { PluginSelectorComponent } from "../components/plugin-selector";
43
43
  import {
@@ -431,8 +431,15 @@ export class SelectorController {
431
431
  this.ctx.settings,
432
432
  this.ctx.session.modelRegistry,
433
433
  this.ctx.session.scopedModels,
434
- async (model, role, thinkingLevel, selector) => {
434
+ async selection => {
435
435
  try {
436
+ if (selection.kind === "preset") {
437
+ await this.#applyModelAssignmentPreset(selection);
438
+ done();
439
+ this.ctx.ui.requestRender();
440
+ return;
441
+ }
442
+ const { model, role, thinkingLevel, selector } = selection;
436
443
  if (role === null) {
437
444
  // Temporary: update agent state but don't persist to settings
438
445
  await this.ctx.session.setModelTemporary(model, thinkingLevel);
@@ -483,6 +490,39 @@ export class SelectorController {
483
490
  });
484
491
  }
485
492
 
493
+ async #applyModelAssignmentPreset(selection: Extract<ModelSelectorSelection, { kind: "preset" }>): Promise<void> {
494
+ const { assignments, model, preset, selector } = selection;
495
+ const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
496
+ if (!apiKey) {
497
+ throw new Error(`No API key for ${model.provider}/${model.id}`);
498
+ }
499
+
500
+ const defaultThinkingLevel = assignments.default;
501
+ await this.ctx.session.setModel(model, "default", {
502
+ selector,
503
+ thinkingLevel: defaultThinkingLevel,
504
+ });
505
+ if (defaultThinkingLevel && defaultThinkingLevel !== ThinkingLevel.Inherit) {
506
+ this.ctx.session.setThinkingLevel(defaultThinkingLevel);
507
+ }
508
+
509
+ const overrides = this.ctx.settings.get("task.agentModelOverrides");
510
+ const nextOverrides = { ...overrides };
511
+ for (const [targetRole, presetThinkingLevel] of Object.entries(assignments) as [
512
+ keyof Extract<ModelSelectorSelection, { kind: "preset" }>["assignments"],
513
+ ThinkingLevel,
514
+ ][]) {
515
+ if (!targetRole || targetRole === "default") continue;
516
+ nextOverrides[targetRole] =
517
+ presetThinkingLevel === ThinkingLevel.Inherit ? selector : `${selector}:${presetThinkingLevel}`;
518
+ }
519
+ this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
520
+ this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
521
+ this.ctx.statusLine.invalidate();
522
+ this.ctx.updateEditorBorderColor();
523
+ this.ctx.showStatus(`${preset.label}: ${selector}`);
524
+ }
525
+
486
526
  async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
487
527
  const mgr = new MarketplaceManager({
488
528
  marketplacesRegistryPath: getMarketplacesRegistryPath(),