@gajae-code/coding-agent 0.3.0 → 0.3.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 (213) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +7 -0
  4. package/dist/types/cli/args.d.ts +3 -1
  5. package/dist/types/commands/deep-interview.d.ts +3 -0
  6. package/dist/types/commands/launch.d.ts +6 -0
  7. package/dist/types/config/keybindings.d.ts +5 -0
  8. package/dist/types/config/model-profile-activation.d.ts +30 -0
  9. package/dist/types/config/model-profiles.d.ts +19 -0
  10. package/dist/types/config/model-registry.d.ts +8 -0
  11. package/dist/types/config/model-resolver.d.ts +1 -1
  12. package/dist/types/config/models-config-schema.d.ts +47 -0
  13. package/dist/types/config/settings-schema.d.ts +14 -4
  14. package/dist/types/debug/crash-diagnostics.d.ts +45 -0
  15. package/dist/types/debug/runtime-gauges.d.ts +6 -0
  16. package/dist/types/deep-interview/render-middleware.d.ts +1 -0
  17. package/dist/types/eval/py/executor.d.ts +2 -0
  18. package/dist/types/eval/py/kernel.d.ts +2 -0
  19. package/dist/types/exec/bash-executor.d.ts +10 -0
  20. package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
  21. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
  22. package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
  23. package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
  24. package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
  25. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
  26. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
  27. package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
  28. package/dist/types/hooks/skill-state.d.ts +21 -0
  29. package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
  30. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  31. package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
  32. package/dist/types/internal-urls/types.d.ts +4 -0
  33. package/dist/types/lsp/index.d.ts +10 -10
  34. package/dist/types/main.d.ts +10 -1
  35. package/dist/types/modes/bridge/auth.d.ts +12 -0
  36. package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
  37. package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
  38. package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
  39. package/dist/types/modes/bridge/event-stream.d.ts +8 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  41. package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
  43. package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
  44. package/dist/types/modes/components/model-selector.d.ts +6 -1
  45. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  46. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  47. package/dist/types/modes/components/status-line.d.ts +2 -0
  48. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  49. package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
  50. package/dist/types/modes/index.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +1 -0
  52. package/dist/types/modes/jobs-observer.d.ts +57 -0
  53. package/dist/types/modes/rpc/host-tools.d.ts +1 -16
  54. package/dist/types/modes/rpc/host-uris.d.ts +1 -38
  55. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
  56. package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
  57. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
  58. package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
  59. package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
  60. package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
  61. package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
  62. package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
  63. package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
  64. package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
  65. package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
  66. package/dist/types/modes/types.d.ts +2 -0
  67. package/dist/types/sdk.d.ts +3 -1
  68. package/dist/types/session/agent-session.d.ts +11 -1
  69. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
  70. package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
  71. package/dist/types/task/executor.d.ts +1 -0
  72. package/dist/types/task/id.d.ts +7 -0
  73. package/dist/types/task/index.d.ts +5 -0
  74. package/dist/types/task/receipt.d.ts +85 -0
  75. package/dist/types/task/spawn-gate.d.ts +38 -0
  76. package/dist/types/task/types.d.ts +143 -11
  77. package/dist/types/tools/cron.d.ts +6 -0
  78. package/dist/types/tools/hindsight-recall.d.ts +0 -2
  79. package/dist/types/tools/hindsight-reflect.d.ts +0 -2
  80. package/dist/types/tools/hindsight-retain.d.ts +0 -2
  81. package/dist/types/tools/index.d.ts +6 -4
  82. package/dist/types/tools/path-utils.d.ts +1 -0
  83. package/dist/types/tools/subagent.d.ts +15 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +7 -0
  86. package/src/async/job-manager.ts +36 -0
  87. package/src/cli/args.ts +19 -2
  88. package/src/commands/deep-interview.ts +1 -0
  89. package/src/commands/harness.ts +289 -19
  90. package/src/commands/launch.ts +10 -2
  91. package/src/commands/state.ts +2 -1
  92. package/src/commands/team.ts +22 -4
  93. package/src/config/keybindings.ts +6 -0
  94. package/src/config/model-profile-activation.ts +157 -0
  95. package/src/config/model-profiles.ts +155 -0
  96. package/src/config/model-registry.ts +19 -0
  97. package/src/config/model-resolver.ts +3 -2
  98. package/src/config/models-config-schema.ts +36 -0
  99. package/src/config/settings-schema.ts +16 -3
  100. package/src/dap/client.ts +17 -3
  101. package/src/debug/crash-diagnostics.ts +223 -0
  102. package/src/debug/runtime-gauges.ts +20 -0
  103. package/src/deep-interview/render-middleware.ts +6 -0
  104. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  105. package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
  106. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
  107. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
  108. package/src/defaults/gjc-defaults.ts +7 -0
  109. package/src/eval/py/executor.ts +21 -1
  110. package/src/eval/py/kernel.ts +15 -0
  111. package/src/exec/bash-executor.ts +41 -0
  112. package/src/gjc-runtime/cli-write-receipt.ts +31 -0
  113. package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
  114. package/src/gjc-runtime/ralplan-runtime.ts +213 -36
  115. package/src/gjc-runtime/state-migrations.ts +54 -7
  116. package/src/gjc-runtime/state-runtime.ts +461 -64
  117. package/src/gjc-runtime/state-schema.ts +192 -0
  118. package/src/gjc-runtime/state-writer.ts +32 -1
  119. package/src/gjc-runtime/team-runtime.ts +177 -105
  120. package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
  121. package/src/gjc-runtime/workflow-command-ref.ts +239 -0
  122. package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
  123. package/src/gjc-runtime/workflow-manifest.ts +3 -1
  124. package/src/harness-control-plane/control-endpoint.ts +19 -8
  125. package/src/harness-control-plane/owner.ts +57 -10
  126. package/src/harness-control-plane/state-machine.ts +2 -1
  127. package/src/hooks/skill-state.ts +176 -26
  128. package/src/internal-urls/agent-protocol.ts +68 -21
  129. package/src/internal-urls/artifact-protocol.ts +12 -17
  130. package/src/internal-urls/docs-index.generated.ts +8 -10
  131. package/src/internal-urls/registry-helpers.ts +19 -16
  132. package/src/internal-urls/types.ts +4 -0
  133. package/src/lsp/client.ts +18 -2
  134. package/src/main.ts +88 -6
  135. package/src/modes/bridge/auth.ts +41 -0
  136. package/src/modes/bridge/bridge-client-bridge.ts +47 -0
  137. package/src/modes/bridge/bridge-mode.ts +520 -0
  138. package/src/modes/bridge/bridge-ui-context.ts +200 -0
  139. package/src/modes/bridge/event-stream.ts +70 -0
  140. package/src/modes/components/custom-editor.ts +101 -0
  141. package/src/modes/components/custom-provider-wizard.ts +318 -0
  142. package/src/modes/components/hook-selector.ts +61 -18
  143. package/src/modes/components/jobs-overlay-model.ts +109 -0
  144. package/src/modes/components/jobs-overlay.ts +172 -0
  145. package/src/modes/components/model-selector.ts +108 -18
  146. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  147. package/src/modes/components/status-line/presets.ts +7 -5
  148. package/src/modes/components/status-line/segments.ts +25 -0
  149. package/src/modes/components/status-line/types.ts +2 -0
  150. package/src/modes/components/status-line.ts +9 -1
  151. package/src/modes/controllers/extension-ui-controller.ts +39 -3
  152. package/src/modes/controllers/input-controller.ts +97 -9
  153. package/src/modes/controllers/selector-controller.ts +86 -1
  154. package/src/modes/index.ts +1 -0
  155. package/src/modes/interactive-mode.ts +27 -0
  156. package/src/modes/jobs-observer.ts +204 -0
  157. package/src/modes/rpc/host-tools.ts +1 -186
  158. package/src/modes/rpc/host-uris.ts +1 -235
  159. package/src/modes/rpc/rpc-client.ts +25 -10
  160. package/src/modes/rpc/rpc-mode.ts +12 -381
  161. package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
  162. package/src/modes/shared/agent-wire/command-validation.ts +131 -0
  163. package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
  164. package/src/modes/shared/agent-wire/handshake.ts +117 -0
  165. package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
  166. package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
  167. package/src/modes/shared/agent-wire/protocol.ts +96 -0
  168. package/src/modes/shared/agent-wire/responses.ts +17 -0
  169. package/src/modes/shared/agent-wire/scopes.ts +89 -0
  170. package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
  171. package/src/modes/shared/agent-wire/ui-result.ts +48 -0
  172. package/src/modes/types.ts +2 -0
  173. package/src/prompts/memories/consolidation.md +1 -1
  174. package/src/prompts/memories/read-path.md +6 -7
  175. package/src/prompts/memories/unavailable.md +2 -2
  176. package/src/prompts/tools/bash.md +1 -1
  177. package/src/prompts/tools/irc.md +1 -1
  178. package/src/prompts/tools/read.md +2 -2
  179. package/src/prompts/tools/recall.md +1 -0
  180. package/src/prompts/tools/reflect.md +1 -0
  181. package/src/prompts/tools/retain.md +1 -0
  182. package/src/prompts/tools/subagent.md +12 -7
  183. package/src/prompts/tools/task-summary.md +3 -9
  184. package/src/prompts/tools/task.md +5 -1
  185. package/src/sdk.ts +5 -1
  186. package/src/session/agent-session.ts +214 -38
  187. package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
  188. package/src/skill-state/workflow-state-contract.ts +7 -4
  189. package/src/skill-state/workflow-state-version.ts +3 -0
  190. package/src/slash-commands/builtin-registry.ts +9 -1
  191. package/src/task/executor.ts +31 -5
  192. package/src/task/id.ts +33 -0
  193. package/src/task/index.ts +259 -67
  194. package/src/task/output-manager.ts +5 -4
  195. package/src/task/receipt.ts +297 -0
  196. package/src/task/render.ts +48 -131
  197. package/src/task/spawn-gate.ts +132 -0
  198. package/src/task/types.ts +48 -7
  199. package/src/tools/ask.ts +73 -33
  200. package/src/tools/ast-edit.ts +1 -0
  201. package/src/tools/ast-grep.ts +1 -0
  202. package/src/tools/bash.ts +1 -1
  203. package/src/tools/cron.ts +48 -0
  204. package/src/tools/find.ts +4 -1
  205. package/src/tools/hindsight-recall.ts +0 -2
  206. package/src/tools/hindsight-reflect.ts +0 -2
  207. package/src/tools/hindsight-retain.ts +0 -2
  208. package/src/tools/index.ts +6 -18
  209. package/src/tools/path-utils.ts +3 -2
  210. package/src/tools/read.ts +4 -3
  211. package/src/tools/search.ts +1 -0
  212. package/src/tools/skill.ts +6 -1
  213. package/src/tools/subagent.ts +237 -84
@@ -0,0 +1,172 @@
1
+ import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
2
+ import type { JobsSnapshot } from "../jobs-observer";
3
+ import { getSelectListTheme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+ import {
6
+ buildConfirmItems,
7
+ buildJobDetailItems,
8
+ buildJobsListItems,
9
+ type JobRef,
10
+ parseJobRef,
11
+ } from "./jobs-overlay-model";
12
+
13
+ /**
14
+ * Generic single-level selector used by the jobs overlay. The selector
15
+ * controller mounts a fresh instance per navigation level (list -> detail ->
16
+ * confirm); focus is placed on the inner SelectList, matching the existing
17
+ * selector components (e.g. ThemeSelectorComponent).
18
+ */
19
+ export class JobsSelectorComponent extends Container {
20
+ #selectList: SelectList;
21
+
22
+ constructor(items: SelectItem[], onSelect: (item: SelectItem) => void, onCancel: () => void, maxVisible = 12) {
23
+ super();
24
+ this.addChild(new DynamicBorder());
25
+ this.#selectList = new SelectList(items, maxVisible, getSelectListTheme());
26
+ this.#selectList.onSelect = onSelect;
27
+ this.#selectList.onCancel = onCancel;
28
+ this.addChild(this.#selectList);
29
+ this.addChild(new DynamicBorder());
30
+ }
31
+
32
+ getSelectList(): SelectList {
33
+ return this.#selectList;
34
+ }
35
+ }
36
+
37
+ export interface JobsOverlayController {
38
+ acknowledgeFailures(): void;
39
+ getSnapshot(): JobsSnapshot;
40
+ getMonitorOutput(id: string): string;
41
+ cancelMonitor(id: string): boolean;
42
+ deleteCron(id: string): boolean;
43
+ }
44
+
45
+ export interface JobsOverlayCallbacks {
46
+ close(): void;
47
+ requestRender(): void;
48
+ }
49
+
50
+ type JobsOverlayView = "list" | "detail" | "confirm";
51
+ type JobsOverlayAction = "cancel" | "delete";
52
+
53
+ export class JobsOverlayComponent extends Container {
54
+ readonly #controller: JobsOverlayController;
55
+ readonly #callbacks: JobsOverlayCallbacks;
56
+ #view: JobsOverlayView = "list";
57
+ #ref: JobRef | undefined;
58
+ #action: JobsOverlayAction | undefined;
59
+ #selectList: SelectList | undefined;
60
+
61
+ constructor(controller: JobsOverlayController, callbacks: JobsOverlayCallbacks) {
62
+ super();
63
+ this.#controller = controller;
64
+ this.#callbacks = callbacks;
65
+ this.#controller.acknowledgeFailures();
66
+ this.#renderList();
67
+ }
68
+
69
+ getFocus(): SelectList {
70
+ if (!this.#selectList) throw new Error("Jobs overlay has no focusable list");
71
+ return this.#selectList;
72
+ }
73
+
74
+ handleInput(data: string): void {
75
+ if (this.#view === "confirm") {
76
+ const key = data.toLowerCase();
77
+ if (key === "y") {
78
+ this.#confirmYes();
79
+ return;
80
+ }
81
+ if (key === "n") {
82
+ this.#renderDetail();
83
+ return;
84
+ }
85
+ }
86
+ this.#selectList?.handleInput(data);
87
+ }
88
+
89
+ #replaceList(
90
+ items: SelectItem[],
91
+ onSelect: (item: SelectItem) => void,
92
+ onCancel: () => void,
93
+ maxVisible = 12,
94
+ ): void {
95
+ this.clear();
96
+ this.addChild(new DynamicBorder());
97
+ this.#selectList = new SelectList(items, maxVisible, getSelectListTheme());
98
+ this.#selectList.onSelect = onSelect;
99
+ this.#selectList.onCancel = onCancel;
100
+ this.addChild(this.#selectList);
101
+ this.addChild(new DynamicBorder());
102
+ this.#callbacks.requestRender();
103
+ }
104
+
105
+ #renderList(): void {
106
+ this.#view = "list";
107
+ this.#ref = undefined;
108
+ this.#action = undefined;
109
+ const snapshot = this.#controller.getSnapshot();
110
+ const built = buildJobsListItems(snapshot);
111
+ const items = built.length > 0 ? built : [{ value: "close", label: "No active monitor or cron jobs" }];
112
+ this.#replaceList(
113
+ items,
114
+ item => {
115
+ const ref = parseJobRef(item.value);
116
+ if (ref) this.#renderDetail(ref);
117
+ else this.#callbacks.close();
118
+ },
119
+ () => this.#callbacks.close(),
120
+ );
121
+ }
122
+
123
+ #renderDetail(ref = this.#ref): void {
124
+ if (!ref) {
125
+ this.#renderList();
126
+ return;
127
+ }
128
+ this.#view = "detail";
129
+ this.#ref = ref;
130
+ this.#action = undefined;
131
+ const output = ref.kind === "monitor" ? this.#controller.getMonitorOutput(ref.id) : "";
132
+ const items = buildJobDetailItems(this.#controller.getSnapshot(), ref, output);
133
+ this.#replaceList(
134
+ items,
135
+ item => {
136
+ if (item.value === "action:cancel") this.#renderConfirm("cancel");
137
+ else if (item.value === "action:delete") this.#renderConfirm("delete");
138
+ else if (item.value === "back") this.#renderList();
139
+ },
140
+ () => this.#callbacks.close(),
141
+ );
142
+ }
143
+
144
+ #renderConfirm(action: JobsOverlayAction): void {
145
+ if (!this.#ref) {
146
+ this.#renderList();
147
+ return;
148
+ }
149
+ this.#view = "confirm";
150
+ this.#action = action;
151
+ const label = action === "cancel" ? "cancel this monitor" : "delete this cron";
152
+ this.#replaceList(
153
+ buildConfirmItems(label),
154
+ item => {
155
+ if (item.value === "yes") this.#confirmYes();
156
+ else this.#renderDetail();
157
+ },
158
+ () => this.#renderDetail(),
159
+ 4,
160
+ );
161
+ }
162
+
163
+ #confirmYes(): void {
164
+ if (!this.#ref || !this.#action) {
165
+ this.#renderList();
166
+ return;
167
+ }
168
+ if (this.#action === "cancel") this.#controller.cancelMonitor(this.#ref.id);
169
+ else this.#controller.deleteCron(this.#ref.id);
170
+ this.#renderList();
171
+ }
172
+ }
@@ -12,6 +12,7 @@ import {
12
12
  Text,
13
13
  type TUI,
14
14
  } from "@gajae-code/tui";
15
+ import type { ModelProfileDefinition } from "../../config/model-profiles";
15
16
  import type { GjcModelAssignmentTargetId, ModelRegistry } from "../../config/model-registry";
16
17
  import { GJC_MODEL_ASSIGNMENT_TARGET_IDS, GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
17
18
  import {
@@ -74,6 +75,12 @@ interface CanonicalModelItem {
74
75
  explicitThinkingLevel?: boolean;
75
76
  }
76
77
 
78
+ interface ProfileItem {
79
+ kind: "profile";
80
+ name: string;
81
+ profile: ModelProfileDefinition;
82
+ }
83
+
77
84
  type ScopedModelItem = ScopedModelSelection;
78
85
 
79
86
  interface RoleAssignment {
@@ -102,6 +109,11 @@ export type ModelSelectorSelection =
102
109
  selector: string;
103
110
  preset: ModelAssignmentPreset;
104
111
  assignments: Record<GjcModelAssignmentTargetId, ThinkingLevel>;
112
+ }
113
+ | {
114
+ kind: "profile";
115
+ profileName: string;
116
+ setDefault: boolean;
105
117
  };
106
118
 
107
119
  interface PendingThinkingChoice {
@@ -110,7 +122,7 @@ interface PendingThinkingChoice {
110
122
  levels: ThinkingLevel[];
111
123
  }
112
124
 
113
- type RoleSelectCallback = (selection: ModelSelectorSelection) => void;
125
+ type RoleSelectCallback = (selection: ModelSelectorSelection) => void | Promise<void>;
114
126
  type CancelCallback = () => void;
115
127
 
116
128
  interface ProviderTabState {
@@ -161,6 +173,7 @@ export class ModelSelectorComponent extends Container {
161
173
  #filteredModels: ModelItem[] = [];
162
174
  #canonicalModels: CanonicalModelItem[] = [];
163
175
  #filteredCanonicalModels: CanonicalModelItem[] = [];
176
+ #profileItems: ProfileItem[] = [];
164
177
  #selectedIndex: number = 0;
165
178
  #roles = {} as Record<string, RoleAssignment | undefined>;
166
179
  #settings = null as unknown as Settings;
@@ -228,7 +241,11 @@ export class ModelSelectorComponent extends Container {
228
241
  this.#searchInput.onSubmit = () => {
229
242
  const selectedItem = this.#getSelectedItem();
230
243
  if (selectedItem) {
231
- this.#beginActionMenuOrSelect(selectedItem);
244
+ if (selectedItem.kind === "profile") {
245
+ this.#beginProfileActionMenu(selectedItem);
246
+ } else {
247
+ this.#beginActionMenuOrSelect(selectedItem);
248
+ }
232
249
  }
233
250
  };
234
251
  this.addChild(this.#searchInput);
@@ -465,11 +482,18 @@ export class ModelSelectorComponent extends Container {
465
482
 
466
483
  this.#sortModels(models);
467
484
  this.#sortCanonicalModels(canonicalModels);
485
+ const profiles = this.#modelRegistry.getModelProfiles?.() ?? new Map();
486
+ const profileItems = this.#temporaryOnly
487
+ ? []
488
+ : [...profiles.values()]
489
+ .sort((a, b) => a.name.localeCompare(b.name))
490
+ .map(profile => ({ kind: "profile" as const, name: profile.name, profile }));
468
491
 
469
492
  this.#allModels = models;
470
493
  this.#filteredModels = models;
471
494
  this.#canonicalModels = canonicalModels;
472
495
  this.#filteredCanonicalModels = canonicalModels;
496
+ this.#profileItems = profileItems;
473
497
  this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, models.length - 1));
474
498
  }
475
499
 
@@ -664,20 +688,40 @@ export class ModelSelectorComponent extends Container {
664
688
  }
665
689
  }
666
690
 
691
+ #getVisibleProfiles(): ProfileItem[] {
692
+ return !this.#temporaryOnly && !this.#isCanonicalTab() && this.#getActiveTabId() === ALL_TAB
693
+ ? this.#profileItems
694
+ : [];
695
+ }
696
+
667
697
  #updateList(): void {
668
698
  this.#listContainer.clear();
669
699
  const isCanonicalTab = this.#isCanonicalTab();
700
+ const visibleProfiles = this.#getVisibleProfiles();
701
+ const modelSelectedIndex = Math.max(0, this.#selectedIndex - visibleProfiles.length);
670
702
  const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
671
703
 
672
704
  const maxVisible = 10;
673
705
  const startIndex = Math.max(
674
706
  0,
675
- Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), visibleItems.length - maxVisible),
707
+ Math.min(modelSelectedIndex - Math.floor(maxVisible / 2), visibleItems.length - maxVisible),
676
708
  );
677
709
  const endIndex = Math.min(startIndex + maxVisible, visibleItems.length);
678
710
 
679
711
  const showProvider = this.#getActiveTabId() === ALL_TAB;
680
712
 
713
+ if (visibleProfiles.length > 0) {
714
+ this.#listContainer.addChild(new Text(theme.fg("muted", "Profiles"), 0, 0));
715
+ for (let i = 0; i < visibleProfiles.length; i++) {
716
+ const profile = visibleProfiles[i];
717
+ if (!profile) continue;
718
+ const isSelected = i === this.#selectedIndex;
719
+ const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
720
+ const label = isSelected ? theme.fg("accent", profile.name) : profile.name;
721
+ this.#listContainer.addChild(new Text(`${prefix}${label}`, 0, 0));
722
+ }
723
+ this.#listContainer.addChild(new Spacer(1));
724
+ }
681
725
  // Show visible slice of filtered models
682
726
  for (let i = startIndex; i < endIndex; i++) {
683
727
  const item = visibleItems[i];
@@ -685,7 +729,7 @@ export class ModelSelectorComponent extends Container {
685
729
  const canonicalItem = isCanonicalTab ? (item as CanonicalModelItem) : undefined;
686
730
  const providerItem = isCanonicalTab ? undefined : (item as ModelItem);
687
731
 
688
- const isSelected = i === this.#selectedIndex;
732
+ const isSelected = i + visibleProfiles.length === this.#selectedIndex;
689
733
 
690
734
  // Build role badges (inverted: color as background, black text)
691
735
  const roleBadgeTokens: string[] = [];
@@ -742,7 +786,7 @@ export class ModelSelectorComponent extends Container {
742
786
  for (const line of errorLines) {
743
787
  this.#listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
744
788
  }
745
- } else if (visibleItems.length === 0) {
789
+ } else if (visibleItems.length === 0 && visibleProfiles.length === 0) {
746
790
  const statusMessage = this.#getProviderEmptyStateMessage();
747
791
  this.#listContainer.addChild(
748
792
  new Text(
@@ -751,8 +795,13 @@ export class ModelSelectorComponent extends Container {
751
795
  0,
752
796
  ),
753
797
  );
798
+ } else if (this.#selectedIndex < visibleProfiles.length) {
799
+ const selectedProfile = visibleProfiles[this.#selectedIndex];
800
+ if (selectedProfile && this.#pendingActionItem) {
801
+ this.#renderProfileActionMenu(selectedProfile);
802
+ }
754
803
  } else {
755
- const selected = visibleItems[this.#selectedIndex];
804
+ const selected = visibleItems[modelSelectedIndex];
756
805
  if (!selected) {
757
806
  return;
758
807
  }
@@ -806,6 +855,20 @@ export class ModelSelectorComponent extends Container {
806
855
  }
807
856
  }
808
857
 
858
+ #renderProfileActionMenu(profile: ProfileItem): void {
859
+ this.#listContainer.addChild(new Spacer(1));
860
+ this.#listContainer.addChild(new Text(theme.fg("muted", ` Action for profile: ${profile.name}`), 0, 0));
861
+ this.#listContainer.addChild(new Spacer(1));
862
+ const labels = ["Apply for this session", "Set as default"];
863
+ for (let i = 0; i < labels.length; i++) {
864
+ const label = labels[i] ?? "";
865
+ const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
866
+ this.#listContainer.addChild(
867
+ new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
868
+ );
869
+ }
870
+ }
871
+
809
872
  #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
810
873
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
811
874
  }
@@ -813,10 +876,11 @@ export class ModelSelectorComponent extends Container {
813
876
  return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + (supportsOpenAICodexPreset(model) ? 1 : 0);
814
877
  }
815
878
 
816
- #getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
817
- return this.#isCanonicalTab()
818
- ? this.#filteredCanonicalModels[this.#selectedIndex]
819
- : this.#filteredModels[this.#selectedIndex];
879
+ #getSelectedItem(): ModelItem | CanonicalModelItem | ProfileItem | undefined {
880
+ const visibleProfiles = this.#getVisibleProfiles();
881
+ if (this.#selectedIndex < visibleProfiles.length) return visibleProfiles[this.#selectedIndex];
882
+ const modelIndex = this.#selectedIndex - visibleProfiles.length;
883
+ return this.#isCanonicalTab() ? this.#filteredCanonicalModels[modelIndex] : this.#filteredModels[modelIndex];
820
884
  }
821
885
 
822
886
  handleInput(keyData: string): void {
@@ -836,7 +900,9 @@ export class ModelSelectorComponent extends Container {
836
900
 
837
901
  // Up arrow - navigate list (wrap to bottom when at top)
838
902
  if (matchesKey(keyData, "up")) {
839
- const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
903
+ const itemCount =
904
+ this.#getVisibleProfiles().length +
905
+ (this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length);
840
906
  if (itemCount === 0) return;
841
907
  this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
842
908
  this.#updateList();
@@ -845,7 +911,9 @@ export class ModelSelectorComponent extends Container {
845
911
 
846
912
  // Down arrow - navigate list (wrap to top when at bottom)
847
913
  if (matchesKey(keyData, "down")) {
848
- const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
914
+ const itemCount =
915
+ this.#getVisibleProfiles().length +
916
+ (this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length);
849
917
  if (itemCount === 0) return;
850
918
  this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
851
919
  this.#updateList();
@@ -857,7 +925,11 @@ export class ModelSelectorComponent extends Container {
857
925
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
858
926
  const selectedItem = this.#getSelectedItem();
859
927
  if (selectedItem) {
860
- this.#beginActionMenuOrSelect(selectedItem);
928
+ if (selectedItem.kind === "profile") {
929
+ this.#beginProfileActionMenu(selectedItem);
930
+ } else {
931
+ this.#beginActionMenuOrSelect(selectedItem);
932
+ }
861
933
  }
862
934
  return;
863
935
  }
@@ -872,6 +944,12 @@ export class ModelSelectorComponent extends Container {
872
944
  this.#searchInput.handleInput(keyData);
873
945
  this.#filterModels(this.#searchInput.getValue());
874
946
  }
947
+ #beginProfileActionMenu(profile: ProfileItem): void {
948
+ this.#pendingActionItem = profile as unknown as ModelItem;
949
+ this.#selectedActionIndex = 0;
950
+ this.#updateList();
951
+ }
952
+
875
953
  #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
876
954
  if (this.#temporaryOnly) {
877
955
  this.#handleSelect(item, null);
@@ -885,7 +963,7 @@ export class ModelSelectorComponent extends Container {
885
963
  #handleActionMenuInput(keyData: string): void {
886
964
  const item = this.#pendingActionItem;
887
965
  if (!item) return;
888
- const actionCount = this.#getActionCount(item.model);
966
+ const actionCount = (item as unknown as ProfileItem).kind === "profile" ? 2 : this.#getActionCount(item.model);
889
967
  if (matchesKey(keyData, "up")) {
890
968
  this.#selectedActionIndex = this.#selectedActionIndex === 0 ? actionCount - 1 : this.#selectedActionIndex - 1;
891
969
  this.#updateList();
@@ -898,11 +976,20 @@ export class ModelSelectorComponent extends Container {
898
976
  }
899
977
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
900
978
  this.#pendingActionItem = undefined;
901
- const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
902
- if (role) {
903
- this.#handleSelect(item, role);
979
+ if ((item as unknown as ProfileItem).kind === "profile") {
980
+ const profile = item as unknown as ProfileItem;
981
+ this.#onSelectCallback({
982
+ kind: "profile",
983
+ profileName: profile.name,
984
+ setDefault: this.#selectedActionIndex === 1,
985
+ });
904
986
  } else {
905
- this.#handlePresetSelect(item, OPENAI_CODE_PROFILE_PRESET);
987
+ const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
988
+ if (role) {
989
+ this.#handleSelect(item, role);
990
+ } else {
991
+ this.#handlePresetSelect(item, OPENAI_CODE_PROFILE_PRESET);
992
+ }
906
993
  }
907
994
  return;
908
995
  }
@@ -1004,6 +1091,9 @@ export class ModelSelectorComponent extends Container {
1004
1091
  getSearchInput(): Input {
1005
1092
  return this.#searchInput;
1006
1093
  }
1094
+ async __testSelectProfile(profileName: string, setDefault: boolean): Promise<void> {
1095
+ await this.#onSelectCallback({ kind: "profile", profileName, setDefault });
1096
+ }
1007
1097
  }
1008
1098
 
1009
1099
  function requiresExplicitThinkingChoice(model: Model): boolean {
@@ -4,7 +4,7 @@ import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
4
4
  import { formatModelOnboardingGuidance } from "../../setup/model-onboarding-guidance";
5
5
  import { DynamicBorder } from "./dynamic-border";
6
6
 
7
- export type ProviderOnboardingAction = "oauth-login" | "api-guide";
7
+ export type ProviderOnboardingAction = "custom-provider-wizard" | "oauth-login" | "api-guide";
8
8
 
9
9
  interface ProviderOnboardingOption {
10
10
  label: string;
@@ -13,6 +13,11 @@ interface ProviderOnboardingOption {
13
13
  }
14
14
 
15
15
  const PROVIDER_ONBOARDING_OPTIONS: ProviderOnboardingOption[] = [
16
+ {
17
+ label: "Add custom provider",
18
+ description: "Configure an OpenAI- or Anthropic-compatible API provider interactively.",
19
+ action: "custom-provider-wizard",
20
+ },
16
21
  {
17
22
  label: "Login with OAuth/subscription",
18
23
  description: "Open the interactive OAuth provider selector.",
@@ -3,7 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
3
3
  export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
4
4
  default: {
5
5
  leftSegments: ["model", "mode", "git", "pr", "path"],
6
- rightSegments: ["session_name", "token_rate", "context_pct", "cost"],
6
+ rightSegments: ["session_name", "jobs", "token_rate", "context_pct", "cost"],
7
7
  separator: "slash",
8
8
  segmentOptions: {
9
9
  model: { showThinkingLevel: true },
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
14
14
 
15
15
  minimal: {
16
16
  leftSegments: ["path", "git"],
17
- rightSegments: ["session_name", "mode", "context_pct"],
17
+ rightSegments: ["session_name", "jobs", "mode", "context_pct"],
18
18
  separator: "slash",
19
19
  segmentOptions: {
20
20
  path: { abbreviate: true, maxLength: 30 },
@@ -24,7 +24,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
24
24
 
25
25
  compact: {
26
26
  leftSegments: ["model", "mode", "git", "pr"],
27
- rightSegments: ["session_name", "cost", "context_pct"],
27
+ rightSegments: ["session_name", "jobs", "cost", "context_pct"],
28
28
  separator: "slash",
29
29
  segmentOptions: {
30
30
  model: { showThinkingLevel: false },
@@ -36,6 +36,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
36
36
  leftSegments: ["gajae", "hostname", "model", "mode", "path", "git", "pr", "subagents"],
37
37
  rightSegments: [
38
38
  "session_name",
39
+ "jobs",
39
40
  "token_in",
40
41
  "token_out",
41
42
  "token_rate",
@@ -59,6 +60,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
59
60
  leftSegments: ["gajae", "hostname", "model", "mode", "path", "git", "pr", "session", "subagents"],
60
61
  rightSegments: [
61
62
  "session_name",
63
+ "jobs",
62
64
  "token_in",
63
65
  "token_out",
64
66
  "cache_read",
@@ -82,7 +84,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
82
84
  ascii: {
83
85
  // No Nerd Font dependencies
84
86
  leftSegments: ["model", "mode", "path", "git", "pr"],
85
- rightSegments: ["session_name", "token_total", "cost", "context_pct"],
87
+ rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
86
88
  separator: "ascii",
87
89
  segmentOptions: {
88
90
  model: { showThinkingLevel: true },
@@ -94,7 +96,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
94
96
  custom: {
95
97
  // User-defined - these are just defaults that get overridden
96
98
  leftSegments: ["model", "mode", "path", "git", "pr"],
97
- rightSegments: ["session_name", "token_total", "cost", "context_pct"],
99
+ rightSegments: ["session_name", "jobs", "token_total", "cost", "context_pct"],
98
100
  separator: "slash",
99
101
  segmentOptions: {},
100
102
  },
@@ -270,6 +270,30 @@ const subagentsSegment: StatusLineSegment = {
270
270
  },
271
271
  };
272
272
 
273
+ const jobsSegment: StatusLineSegment = {
274
+ id: "jobs",
275
+ render(ctx) {
276
+ const { jobs } = ctx;
277
+ const visible = jobs.activeMonitorCount > 0 || jobs.activeCronCount > 0 || jobs.worstState === "failed";
278
+ if (!visible) {
279
+ return { content: "", visible: false };
280
+ }
281
+ const parts: string[] = [];
282
+ if (jobs.activeMonitorCount > 0) {
283
+ parts.push(withIcon(theme.icon.agents, `${jobs.activeMonitorCount}`));
284
+ }
285
+ if (jobs.activeCronCount > 0) {
286
+ parts.push(withIcon(theme.icon.time, `${jobs.activeCronCount}`));
287
+ }
288
+ if (parts.length === 0) {
289
+ // Nothing active but a failure is unacknowledged — keep a drill-in marker.
290
+ parts.push(withIcon(theme.icon.warning, "jobs"));
291
+ }
292
+ const color: ThemeColor = jobs.worstState === "failed" ? "error" : "statusLineSubagents";
293
+ return { content: theme.fg(color, parts.join(" ")), visible: true };
294
+ },
295
+ };
296
+
273
297
  const tokenInSegment: StatusLineSegment = {
274
298
  id: "token_in",
275
299
  render(ctx) {
@@ -521,6 +545,7 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
521
545
  git: gitSegment,
522
546
  pr: prSegment,
523
547
  subagents: subagentsSegment,
548
+ jobs: jobsSegment,
524
549
  token_in: tokenInSegment,
525
550
  token_out: tokenOutSegment,
526
551
  token_total: tokenTotalSegment,
@@ -1,5 +1,6 @@
1
1
  import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
2
2
  import type { AgentSession } from "../../../session/agent-session";
3
+ import type { JobsSnapshot } from "../../jobs-observer";
3
4
  import type { StatusLineSegmentOptions, StatusLineSettings } from "../status-line";
4
5
 
5
6
  export type {
@@ -42,6 +43,7 @@ export interface SegmentContext {
42
43
  contextWindow: number;
43
44
  autoCompactEnabled: boolean;
44
45
  subagentCount: number;
46
+ jobs: JobsSnapshot;
45
47
  sessionStartTime: number;
46
48
  git: {
47
49
  branch: string | null;
@@ -11,6 +11,7 @@ import type { AgentSession } from "../../session/agent-session";
11
11
  import { readVisibleSkillActiveState, type SkillActiveEntry } from "../../skill-state/active-state";
12
12
  import * as git from "../../utils/git";
13
13
  import { getSessionAccentAnsi, getSessionAccentHex } from "../../utils/session-color";
14
+ import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
14
15
  import { sanitizeStatusText } from "../shared";
15
16
  import { computeNonMessageTokens } from "../utils/context-usage";
16
17
  import { renderSkillHudBar } from "./skill-hud/render";
@@ -153,6 +154,7 @@ export class StatusLineComponent implements Component {
153
154
  #autoCompactEnabled: boolean = true;
154
155
  #hookStatuses: Map<string, string> = new Map();
155
156
  #subagentCount: number = 0;
157
+ #jobs: JobsSnapshot = EMPTY_JOBS_SNAPSHOT;
156
158
  #sessionStartTime: number = Date.now();
157
159
  #planModeStatus: { enabled: boolean; paused: boolean } | null = null;
158
160
  #goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
@@ -220,6 +222,10 @@ export class StatusLineComponent implements Component {
220
222
  this.#subagentCount = count;
221
223
  }
222
224
 
225
+ setJobs(jobs: JobsSnapshot): void {
226
+ this.#jobs = jobs;
227
+ }
228
+
223
229
  setSessionStartTime(time: number): void {
224
230
  this.#sessionStartTime = time;
225
231
  }
@@ -612,6 +618,7 @@ export class StatusLineComponent implements Component {
612
618
  contextWindow,
613
619
  autoCompactEnabled: this.#autoCompactEnabled,
614
620
  subagentCount: this.#subagentCount,
621
+ jobs: this.#jobs,
615
622
  sessionStartTime: this.#sessionStartTime,
616
623
  git: {
617
624
  branch: this.#getCurrentBranch(),
@@ -687,7 +694,8 @@ export class StatusLineComponent implements Component {
687
694
  }
688
695
  }
689
696
 
690
- const runningBackgroundJobs = this.session.getAsyncJobSnapshot()?.running.length ?? 0;
697
+ const runningBackgroundJobs =
698
+ this.session.getAsyncJobSnapshot()?.running.filter(job => job.metadata?.monitor !== true).length ?? 0;
691
699
  if (runningBackgroundJobs > 0) {
692
700
  const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
693
701
  const label = `${formatCount("job", runningBackgroundJobs)} running`;