@gajae-code/coding-agent 0.5.0 → 0.5.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 (194) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +26 -0
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/list-models.d.ts +6 -0
  6. package/dist/types/cli/setup-cli.d.ts +8 -1
  7. package/dist/types/commands/gc.d.ts +26 -0
  8. package/dist/types/commands/setup.d.ts +7 -0
  9. package/dist/types/config/file-lock-gc.d.ts +5 -0
  10. package/dist/types/config/file-lock.d.ts +29 -0
  11. package/dist/types/config/model-registry.d.ts +4 -0
  12. package/dist/types/config/models-config-schema.d.ts +5 -0
  13. package/dist/types/config/settings-schema.d.ts +62 -0
  14. package/dist/types/coordinator/contract.d.ts +1 -1
  15. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  16. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  25. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  26. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  27. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  28. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  29. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  31. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  32. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  33. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  34. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  42. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  43. package/dist/types/harness-control-plane/owner.d.ts +7 -0
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  46. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -1
  49. package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
  50. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  51. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  52. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  53. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  54. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  55. package/dist/types/modes/theme/theme.d.ts +1 -0
  56. package/dist/types/modes/types.d.ts +1 -1
  57. package/dist/types/session/agent-session.d.ts +1 -1
  58. package/dist/types/session/blob-store.d.ts +39 -3
  59. package/dist/types/session/history-storage.d.ts +2 -2
  60. package/dist/types/session/session-manager.d.ts +10 -1
  61. package/dist/types/setup/credential-import.d.ts +79 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/executor.d.ts +1 -0
  64. package/dist/types/task/render.d.ts +1 -1
  65. package/dist/types/tools/ask.d.ts +15 -1
  66. package/dist/types/tools/subagent-render.d.ts +7 -1
  67. package/dist/types/tools/subagent.d.ts +27 -0
  68. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  69. package/dist/types/web/search/index.d.ts +4 -4
  70. package/dist/types/web/search/provider.d.ts +16 -20
  71. package/dist/types/web/search/providers/base.d.ts +2 -1
  72. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  73. package/dist/types/web/search/types.d.ts +14 -2
  74. package/package.json +7 -7
  75. package/scripts/build-binary.ts +7 -0
  76. package/src/async/job-manager.ts +52 -0
  77. package/src/cli/args.ts +5 -0
  78. package/src/cli/auth-broker-cli.ts +1 -0
  79. package/src/cli/fast-help.ts +2 -0
  80. package/src/cli/list-models.ts +13 -1
  81. package/src/cli/setup-cli.ts +138 -3
  82. package/src/cli.ts +1 -0
  83. package/src/commands/gc.ts +22 -0
  84. package/src/commands/harness.ts +7 -3
  85. package/src/commands/setup.ts +5 -1
  86. package/src/commands/ultragoal.ts +3 -1
  87. package/src/config/file-lock-gc.ts +193 -0
  88. package/src/config/file-lock.ts +66 -10
  89. package/src/config/model-profile-activation.ts +15 -3
  90. package/src/config/model-profiles.ts +39 -30
  91. package/src/config/model-registry.ts +21 -1
  92. package/src/config/models-config-schema.ts +1 -0
  93. package/src/config/settings-schema.ts +62 -0
  94. package/src/coordinator/contract.ts +1 -0
  95. package/src/coordinator-mcp/server.ts +459 -3
  96. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  97. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  106. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  107. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  108. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  109. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  110. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  111. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  113. package/src/defaults/gjc-defaults.ts +7 -0
  114. package/src/defaults/gjc-grok-cli.ts +22 -0
  115. package/src/extensibility/extensions/index.ts +1 -0
  116. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  117. package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
  118. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  119. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  120. package/src/gjc-runtime/gc-render.ts +70 -0
  121. package/src/gjc-runtime/gc-runtime.ts +403 -0
  122. package/src/gjc-runtime/launch-tmux.ts +3 -4
  123. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  124. package/src/gjc-runtime/ralplan-runtime.ts +232 -19
  125. package/src/gjc-runtime/state-renderer.ts +12 -3
  126. package/src/gjc-runtime/state-runtime.ts +48 -30
  127. package/src/gjc-runtime/state-writer.ts +254 -7
  128. package/src/gjc-runtime/team-gc.ts +49 -0
  129. package/src/gjc-runtime/team-runtime.ts +179 -2
  130. package/src/gjc-runtime/tmux-common.ts +14 -0
  131. package/src/gjc-runtime/tmux-gc.ts +177 -0
  132. package/src/gjc-runtime/tmux-sessions.ts +49 -1
  133. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  134. package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
  135. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  136. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  137. package/src/harness-control-plane/gc-adapter.ts +184 -0
  138. package/src/harness-control-plane/owner.ts +14 -2
  139. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  140. package/src/harness-control-plane/storage.ts +70 -0
  141. package/src/hooks/skill-state.ts +121 -2
  142. package/src/internal-urls/docs-index.generated.ts +22 -12
  143. package/src/lsp/defaults.json +1 -0
  144. package/src/main.ts +18 -3
  145. package/src/modes/acp/acp-agent.ts +4 -2
  146. package/src/modes/bridge/bridge-mode.ts +2 -1
  147. package/src/modes/components/history-search.ts +5 -2
  148. package/src/modes/components/hook-selector.ts +19 -0
  149. package/src/modes/components/model-selector.ts +51 -8
  150. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  151. package/src/modes/components/status-line/segments.ts +1 -1
  152. package/src/modes/controllers/command-controller.ts +25 -6
  153. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  154. package/src/modes/controllers/selector-controller.ts +81 -1
  155. package/src/modes/interactive-mode.ts +11 -1
  156. package/src/modes/rpc/rpc-mode.ts +266 -34
  157. package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
  158. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  159. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  160. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  161. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  162. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  163. package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
  164. package/src/modes/theme/defaults/claude-code.json +100 -0
  165. package/src/modes/theme/defaults/codex.json +100 -0
  166. package/src/modes/theme/defaults/index.ts +6 -0
  167. package/src/modes/theme/defaults/opencode.json +102 -0
  168. package/src/modes/theme/theme.ts +2 -2
  169. package/src/modes/types.ts +1 -1
  170. package/src/prompts/agents/executor.md +5 -2
  171. package/src/sdk.ts +29 -4
  172. package/src/session/agent-session.ts +99 -19
  173. package/src/session/blob-store.ts +59 -3
  174. package/src/session/history-storage.ts +32 -11
  175. package/src/session/session-manager.ts +72 -20
  176. package/src/setup/credential-import.ts +429 -0
  177. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  178. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  179. package/src/skill-state/workflow-hud.ts +106 -10
  180. package/src/slash-commands/builtin-registry.ts +3 -2
  181. package/src/task/executor.ts +16 -1
  182. package/src/task/render.ts +18 -7
  183. package/src/tools/ask.ts +59 -2
  184. package/src/tools/cron.ts +1 -1
  185. package/src/tools/job.ts +3 -2
  186. package/src/tools/monitor.ts +36 -1
  187. package/src/tools/subagent-render.ts +128 -29
  188. package/src/tools/subagent.ts +173 -9
  189. package/src/tools/ultragoal-ask-guard.ts +39 -0
  190. package/src/web/search/index.ts +25 -25
  191. package/src/web/search/provider.ts +178 -87
  192. package/src/web/search/providers/base.ts +2 -1
  193. package/src/web/search/providers/openai-compatible.ts +151 -0
  194. package/src/web/search/types.ts +47 -22
@@ -4,6 +4,7 @@
4
4
  "args": [],
5
5
  "fileTypes": [".rs"],
6
6
  "rootMarkers": ["Cargo.toml", "rust-analyzer.toml"],
7
+ "warmupTimeoutMs": 30000,
7
8
  "initOptions": {},
8
9
  "settings": {
9
10
  "rust-analyzer": {
package/src/main.ts CHANGED
@@ -30,6 +30,7 @@ import { activateModelProfile } from "./config/model-profile-activation";
30
30
  import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
31
31
  import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
32
32
  import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
33
+ import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
33
34
  import { initializeWithSettings } from "./discovery";
34
35
  import { exportFromFile } from "./export/html";
35
36
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
@@ -742,7 +743,9 @@ export async function runRootCommand(
742
743
  await runListModelsCommand({
743
744
  modelRegistry,
744
745
  cwd: getProjectDir(),
745
- additionalExtensionPaths: [],
746
+ extensionFactories: [
747
+ { factory: getBundledGrokBuildExtensionFactory(), name: BUNDLED_GROK_BUILD_EXTENSION_ID },
748
+ ],
746
749
  settingsExtensions: [],
747
750
  disabledExtensionIds: [],
748
751
  disableExtensionDiscovery: true,
@@ -974,8 +977,20 @@ export async function runRootCommand(
974
977
  }
975
978
 
976
979
  if (mode === "rpc" || mode === "rpc-ui") {
977
- const { runRpcMode } = await import("./modes/rpc/rpc-mode");
978
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
980
+ const { RpcListenRefusedError, runRpcMode } = await import("./modes/rpc/rpc-mode");
981
+ try {
982
+ await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, {
983
+ listen: parsedArgs.rpcListen,
984
+ });
985
+ } catch (error) {
986
+ if (!(error instanceof RpcListenRefusedError)) throw error;
987
+ logger.setTransports({ console: true, file: true });
988
+ logger.error(error.message);
989
+ await session.dispose();
990
+ stopThemeWatcher();
991
+ await postmortem.quit(1);
992
+ process.exit(1);
993
+ }
979
994
  } else if (mode === "bridge") {
980
995
  const { runBridgeMode } = await import("./modes/bridge/bridge-mode");
981
996
  await runBridgeMode(session, setToolUIContext);
@@ -43,7 +43,8 @@ import {
43
43
  type Usage,
44
44
  } from "@agentclientprotocol/sdk";
45
45
  import type { AssistantMessage, Model } from "@gajae-code/ai";
46
- import { logger, VERSION } from "@gajae-code/utils";
46
+ import { logger } from "@gajae-code/utils";
47
+ import packageJson from "../../../package.json" with { type: "json" };
47
48
  import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
48
49
  import { Settings } from "../../config/settings";
49
50
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -98,6 +99,7 @@ const SESSION_PAGE_SIZE = 50;
98
99
  * wait past this guard without hard-coding the literal.
99
100
  */
100
101
  export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
102
+ const CODING_AGENT_VERSION: string = packageJson.version;
101
103
  const ACP_CANCEL_CLEANUP_TIMEOUT_MS = 5_000;
102
104
  const ACP_ASYNC_DELIVERY_DRAIN_TIMEOUT_MS = 250;
103
105
  const ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES = 3;
@@ -414,7 +416,7 @@ export class AcpAgent implements Agent {
414
416
  agentInfo: {
415
417
  name: "gajae-code",
416
418
  title: "Gajae Code",
417
- version: VERSION,
419
+ version: CODING_AGENT_VERSION,
418
420
  },
419
421
  authMethods,
420
422
  agentCapabilities: {
@@ -29,7 +29,7 @@ import {
29
29
  import { UiRequestBroker } from "../shared/agent-wire/ui-request-broker";
30
30
  import type { BridgeUiResult } from "../shared/agent-wire/ui-result";
31
31
  import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
32
- import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
32
+ import { modelSupportsTokenCostMetrics, UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
33
33
  import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
34
34
  import { assertSafeBridgeBind, isBridgeTokenAuthorized } from "./auth";
35
35
  import { type BridgePermissionRequestPayload, createBridgeClientBridge } from "./bridge-client-bridge";
@@ -599,6 +599,7 @@ export async function runBridgeMode(
599
599
  emitFrame: gate => eventStream.publish(toBridgeWorkflowGateFrame(gate, sequencer)),
600
600
  store: gateStore,
601
601
  audit: recordAudit,
602
+ providerSupportsTokenCostMetrics: modelSupportsTokenCostMetrics(session.model),
602
603
  getUsageSnapshot: () => {
603
604
  const stats = session.getSessionStats();
604
605
  return { tokens: stats.tokens.total, costUsd: stats.cost };
@@ -10,6 +10,7 @@ import {
10
10
  truncateToWidth,
11
11
  visibleWidth,
12
12
  } from "@gajae-code/tui";
13
+ import { getProjectDir } from "@gajae-code/utils";
13
14
  import { theme } from "../../modes/theme/theme";
14
15
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
15
16
  import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
@@ -72,6 +73,7 @@ class HistoryResultsList implements Component {
72
73
 
73
74
  export class HistorySearchComponent extends Container {
74
75
  #historyStorage: HistoryStorage;
76
+ #cwd: string;
75
77
  #searchInput: Input;
76
78
  #results: HistoryEntry[] = [];
77
79
  #selectedIndex = 0;
@@ -83,6 +85,7 @@ export class HistorySearchComponent extends Container {
83
85
  constructor(historyStorage: HistoryStorage, onSelect: (prompt: string) => void, onCancel: () => void) {
84
86
  super();
85
87
  this.#historyStorage = historyStorage;
88
+ this.#cwd = getProjectDir();
86
89
  this.#onSelect = onSelect;
87
90
  this.#onCancel = onCancel;
88
91
 
@@ -150,8 +153,8 @@ export class HistorySearchComponent extends Container {
150
153
  #updateResults(): void {
151
154
  const query = this.#searchInput.getValue().trim();
152
155
  this.#results = query
153
- ? this.#historyStorage.search(query, this.#resultLimit)
154
- : this.#historyStorage.getRecent(this.#resultLimit);
156
+ ? this.#historyStorage.search(query, this.#resultLimit, this.#cwd)
157
+ : this.#historyStorage.getRecent(this.#resultLimit, this.#cwd);
155
158
  this.#selectedIndex = 0;
156
159
  this.#resultsList.setResults(this.#results, this.#selectedIndex);
157
160
  }
@@ -3,6 +3,7 @@
3
3
  * Displays a list of string options with keyboard navigation.
4
4
  */
5
5
  import {
6
+ type AutocompleteProvider,
6
7
  Container,
7
8
  Editor,
8
9
  Markdown,
@@ -73,6 +74,12 @@ export interface HookSelectorOptions {
73
74
  optionLabel: string;
74
75
  onSubmit: (text: string) => void;
75
76
  };
77
+ /**
78
+ * Autocomplete provider for the inline custom-input editor. When present,
79
+ * the "Other (type your own)" editor gains the same `@` file-link and `/`
80
+ * completion behavior as the main prompt editor.
81
+ */
82
+ autocompleteProvider?: AutocompleteProvider;
76
83
  }
77
84
 
78
85
  class OutlinedList extends Container {
@@ -320,6 +327,7 @@ export class HookSelectorComponent extends Container {
320
327
  #helpTextComponent: Text;
321
328
  #baseHelpText: string;
322
329
  #tui: TUI | undefined;
330
+ #autocompleteProvider: AutocompleteProvider | undefined;
323
331
  constructor(
324
332
  title: string,
325
333
  options: string[],
@@ -342,6 +350,7 @@ export class HookSelectorComponent extends Container {
342
350
  this.#outline = opts?.outline === true;
343
351
  this.#customInput = opts?.customInput;
344
352
  this.#tui = opts?.tui;
353
+ this.#autocompleteProvider = opts?.autocompleteProvider;
345
354
 
346
355
  this.addChild(new DynamicBorder());
347
356
  this.addChild(new Spacer(1));
@@ -491,6 +500,13 @@ export class HookSelectorComponent extends Container {
491
500
 
492
501
  /** Keys while the inline custom-input editor is open below the option list. */
493
502
  #handleInputModeKey(keyData: string, editor: Editor): void {
503
+ // While the autocomplete dropdown is open, every key belongs to the
504
+ // editor (navigate/apply/cancel the suggestion) instead of submitting
505
+ // or backing out of input mode.
506
+ if (editor.isAutocompleteOpen()) {
507
+ editor.handleInput(keyData);
508
+ return;
509
+ }
494
510
  // Escape backs out to option selection instead of cancelling the dialog,
495
511
  // so a stray Esc never throws away the question context.
496
512
  if (matchesKey(keyData, "escape") || matchesAppInterrupt(keyData)) {
@@ -521,6 +537,9 @@ export class HookSelectorComponent extends Container {
521
537
  editor.setBorderVisible(false);
522
538
  editor.setPromptGutter("> ");
523
539
  editor.disableSubmit = true;
540
+ if (this.#autocompleteProvider) {
541
+ editor.setAutocompleteProvider(this.#autocompleteProvider);
542
+ }
524
543
  this.#inlineEditor = editor;
525
544
  this.#inputArea.addChild(new Spacer(1));
526
545
  this.#inputArea.addChild(editor);
@@ -154,6 +154,20 @@ interface PresetBrowseRow {
154
154
 
155
155
  type PresetLandingRow = PresetGroupRow | PresetProfileRow | PresetBrowseRow;
156
156
 
157
+ // Stable logical identity for a preset landing row, independent of its current
158
+ // list position. Used to relocate the cursor after the expanded group changes so
159
+ // navigation does not silently overshoot the destination group header/profiles.
160
+ function presetRowIdentity(row: PresetLandingRow): string {
161
+ switch (row.kind) {
162
+ case "group":
163
+ return `group:${row.groupId}`;
164
+ case "profile":
165
+ return `profile:${row.groupId}:${row.profile.name}`;
166
+ case "browse":
167
+ return "browse";
168
+ }
169
+ }
170
+
157
171
  const PROFILE_ROLE_PREVIEW_ORDER: GjcModelAssignmentTargetId[] = [
158
172
  "default",
159
173
  "executor",
@@ -705,6 +719,15 @@ export class ModelSelectorComponent extends Container {
705
719
  return this.#getMissingProviders(profileOrProfiles).length === 0;
706
720
  }
707
721
 
722
+ /**
723
+ * A preset group is a list of alternative presets, not an all-or-nothing
724
+ * bundle. Treat the group as usable when at least one member preset has all
725
+ * of its required providers authenticated.
726
+ */
727
+ #isPresetGroupUsable(profiles: ModelProfileDefinition[]): boolean {
728
+ return profiles.some(profile => this.#isPresetAuthenticated(profile));
729
+ }
730
+
708
731
  async #refreshProviderAuth(): Promise<void> {
709
732
  const providers = new Set<string>();
710
733
  for (const profiles of this.#getPresetGroups().values()) {
@@ -730,6 +753,18 @@ export class ModelSelectorComponent extends Container {
730
753
  if (selected?.kind === "group") this.#expandedPresetProviderId = selected.groupId;
731
754
  if (selected?.kind === "profile") this.#expandedPresetProviderId = selected.groupId;
732
755
  const rows = this.#getPresetRows();
756
+ // Expanding/collapsing a group shifts row positions. Relocate the cursor by
757
+ // the selected row's logical identity so crossing a provider group boundary
758
+ // keeps it on the same logical row instead of overshooting into the
759
+ // destination group's profiles (or off the end of the list).
760
+ if (selected) {
761
+ const targetIdentity = presetRowIdentity(selected);
762
+ const relocated = rows.findIndex(row => presetRowIdentity(row) === targetIdentity);
763
+ if (relocated >= 0) {
764
+ this.#presetCursor = relocated;
765
+ return;
766
+ }
767
+ }
733
768
  this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, rows.length - 1));
734
769
  }
735
770
 
@@ -763,7 +798,7 @@ export class ModelSelectorComponent extends Container {
763
798
  continue;
764
799
  }
765
800
  if (row.kind === "group") {
766
- const authenticated = this.#isPresetAuthenticated(row.profiles);
801
+ const authenticated = this.#isPresetGroupUsable(row.profiles);
767
802
  const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
768
803
  const label = `${mark} ${row.groupId}`;
769
804
  const renderedLabel = selected ? theme.fg("accent", label) : authenticated ? label : theme.fg("dim", label);
@@ -1158,18 +1193,26 @@ export class ModelSelectorComponent extends Container {
1158
1193
  this.#switchToModelMode();
1159
1194
  return;
1160
1195
  }
1161
- const missing =
1162
- row.kind === "group" ? this.#getMissingProviders(row.profiles) : this.#getMissingProviders(row.profile);
1196
+ if (row.kind === "group") {
1197
+ // A group is a list of alternative presets; only surface a login hint
1198
+ // when none of its members are usable. A partially-usable group stays
1199
+ // navigable so the user can drill in and pick a usable member.
1200
+ if (!this.#isPresetGroupUsable(row.profiles)) {
1201
+ const missing = this.#getMissingProviders(row.profiles);
1202
+ this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1203
+ this.#renderPresetLanding();
1204
+ }
1205
+ return;
1206
+ }
1207
+ const missing = this.#getMissingProviders(row.profile);
1163
1208
  if (missing.length > 0) {
1164
1209
  this.#presetLoginHint = `Run ${missing.map(provider => `/login ${provider}`).join(", ")}`;
1165
1210
  this.#renderPresetLanding();
1166
1211
  return;
1167
1212
  }
1168
- if (row.kind === "profile") {
1169
- this.#previewProfileName = row.profile.name;
1170
- this.#presetLoginHint = undefined;
1171
- this.#renderPresetLanding();
1172
- }
1213
+ this.#previewProfileName = row.profile.name;
1214
+ this.#presetLoginHint = undefined;
1215
+ this.#renderPresetLanding();
1173
1216
  }
1174
1217
 
1175
1218
  #beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
@@ -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 = "custom-provider-wizard" | "oauth-login" | "api-guide";
7
+ export type ProviderOnboardingAction = "custom-provider-wizard" | "oauth-login" | "import-credentials" | "api-guide";
8
8
 
9
9
  interface ProviderOnboardingOption {
10
10
  label: string;
@@ -28,6 +28,11 @@ const PROVIDER_ONBOARDING_OPTIONS: ProviderOnboardingOption[] = [
28
28
  description: "Show the /provider add and gjc setup provider commands.",
29
29
  action: "api-guide",
30
30
  },
31
+ {
32
+ label: "Import existing credentials",
33
+ description: "Detect and import Claude Code / Codex CLI logins already on this machine.",
34
+ action: "import-credentials",
35
+ },
31
36
  ];
32
37
 
33
38
  export class ProviderOnboardingSelectorComponent extends Container {
@@ -66,7 +66,7 @@ function classifyProjectDir(pwd: string): { scratch: boolean; relative: string |
66
66
  const gajaeSegment: StatusLineSegment = {
67
67
  id: "gajae",
68
68
  render(_ctx) {
69
- return { content: theme.fg("accent", "GJC"), visible: true };
69
+ return { content: theme.fg("accent", "🦞"), visible: true };
70
70
  },
71
71
  };
72
72
 
@@ -13,6 +13,7 @@ import {
13
13
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
14
14
  import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
15
15
  import { $ } from "bun";
16
+ import { jobElapsedMs } from "../../async";
16
17
  import { reset as resetCapabilities } from "../../capability";
17
18
  import { clearClaudePluginRootsCache } from "../../discovery/helpers";
18
19
  import { loadCustomShare } from "../../export/custom-share";
@@ -1279,7 +1280,7 @@ const BAR_WIDTH_MAX = 24;
1279
1280
  const BAR_WIDTH_MIN = 4;
1280
1281
 
1281
1282
  function renderJobLine(job: AsyncJobSnapshotItem, now: number): string {
1282
- const duration = formatDuration(Math.max(0, now - job.startTime));
1283
+ const duration = formatDuration(jobElapsedMs(job, now));
1283
1284
  const status = formatJobStatus(job.status);
1284
1285
  return `${theme.fg("dim", job.id)} ${theme.fg("dim", `[${job.type}]`)} ${status} ${theme.fg("dim", `(${duration})`)}`;
1285
1286
  }
@@ -1538,7 +1539,7 @@ function resolveColumnWidth(count: number, available: number, trailing: number):
1538
1539
  return ideal;
1539
1540
  }
1540
1541
 
1541
- function renderUsageReports(
1542
+ export function renderUsageReports(
1542
1543
  reports: UsageReport[],
1543
1544
  uiTheme: typeof theme,
1544
1545
  nowMs: number,
@@ -1592,17 +1593,35 @@ function renderUsageReports(
1592
1593
 
1593
1594
  lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
1594
1595
 
1596
+ // Stable account column order shared across every window group, so each
1597
+ // account keeps the same column in the 5h / 7d / ... rows. Rank accounts
1598
+ // by total usage across their windows (a per-account value identical for
1599
+ // every group), tie-broken by label for determinism.
1600
+ const rankedAccounts = providerReports
1601
+ .map(report => {
1602
+ const [firstLimit] = report.limits;
1603
+ return {
1604
+ report,
1605
+ total: report.limits.reduce((sum, limit) => sum + (resolveFraction(limit) ?? 0), 0),
1606
+ label: firstLimit ? formatAccountLabel(firstLimit, report, 0) : formatUnlimitedReportLabel(report, 0),
1607
+ };
1608
+ })
1609
+ .sort((a, b) => (a.total !== b.total ? b.total - a.total : a.label.localeCompare(b.label)));
1610
+ const accountRank = new Map<UsageReport, number>();
1611
+ rankedAccounts.forEach((entry, rank) => {
1612
+ accountRank.set(entry.report, rank);
1613
+ });
1614
+
1595
1615
  const renderableGroups = Array.from(limitGroups.values()).map(group => {
1596
1616
  const entries = group.limits.map((limit, index) => ({
1597
1617
  limit,
1598
1618
  report: group.reports[index],
1599
- fraction: resolveFraction(limit),
1600
1619
  index,
1601
1620
  }));
1602
1621
  entries.sort((a, b) => {
1603
- const aFraction = a.fraction ?? -1;
1604
- const bFraction = b.fraction ?? -1;
1605
- if (aFraction !== bFraction) return bFraction - aFraction;
1622
+ const aRank = accountRank.get(a.report) ?? Number.MAX_SAFE_INTEGER;
1623
+ const bRank = accountRank.get(b.report) ?? Number.MAX_SAFE_INTEGER;
1624
+ if (aRank !== bRank) return aRank - bRank;
1606
1625
  return a.index - b.index;
1607
1626
  });
1608
1627
  const sortedLimits = entries.map(entry => entry.limit);
@@ -645,6 +645,9 @@ export class ExtensionUiController {
645
645
  timeout: dialogOptions?.timeout,
646
646
  onTimeout: dialogOptions?.onTimeout,
647
647
  tui: this.ctx.ui,
648
+ // Share the main prompt editor's autocomplete provider so the
649
+ // inline "Other (type your own)" editor supports `@` file links.
650
+ autocompleteProvider: dialogOptions?.customInput ? this.ctx.editor.getAutocompleteProvider() : undefined,
648
651
  outline: dialogOptions?.outline,
649
652
  wrapFocused: dialogOptions?.wrapFocused,
650
653
  scrollTitleRows,
@@ -32,13 +32,20 @@ import {
32
32
  import type { InteractiveModeContext } from "../../modes/types";
33
33
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
34
34
  import { FileSessionStorage } from "../../session/session-storage";
35
+ import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../../setup/credential-import";
35
36
  import {
36
37
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
37
38
  MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
38
39
  MODEL_ONBOARDING_SETUP_COMMAND,
39
40
  } from "../../setup/model-onboarding-guidance";
40
41
  import { addApiCompatibleProvider, formatProviderSetupResult } from "../../setup/provider-onboarding";
41
- import { isSearchProviderPreference, setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
42
+ import {
43
+ isConfigurableSearchProviderId,
44
+ isSearchProviderPreference,
45
+ setPreferredImageProvider,
46
+ setPreferredSearchProvider,
47
+ setSearchFallbackProviders,
48
+ } from "../../tools";
42
49
  import { setSessionTerminalTitle } from "../../utils/title-generator";
43
50
  import { AgentDashboard } from "../components/agent-dashboard";
44
51
  import { AssistantMessageComponent } from "../components/assistant-message";
@@ -70,6 +77,7 @@ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
70
77
  "google-gemini-cli",
71
78
  "google-antigravity",
72
79
  "xai",
80
+ "grok-build",
73
81
  ]);
74
82
 
75
83
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
@@ -127,6 +135,8 @@ export class SelectorController {
127
135
  this.showCustomProviderWizard();
128
136
  } else if (action === "oauth-login") {
129
137
  void this.showOAuthSelector("login");
138
+ } else if (action === "import-credentials") {
139
+ void this.#handleCredentialImport();
130
140
  } else {
131
141
  this.ctx.showStatus(formatProviderOnboardingCommandGuide());
132
142
  }
@@ -140,6 +150,69 @@ export class SelectorController {
140
150
  });
141
151
  }
142
152
 
153
+ async #handleCredentialImport(): Promise<void> {
154
+ this.ctx.showStatus("Scanning for existing Claude Code / Codex CLI credentials…");
155
+ const result = await discoverExternalCredentials();
156
+ const summaryLines = formatDiscoverySummary(result);
157
+
158
+ if (result.importable.length === 0) {
159
+ this.ctx.chatContainer.addChild(new Spacer(1));
160
+ for (const line of summaryLines) {
161
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
162
+ }
163
+ this.ctx.chatContainer.addChild(
164
+ new Text(
165
+ theme.fg(
166
+ "warning",
167
+ "No importable Claude/Codex credentials found. Use /login or add a custom provider.",
168
+ ),
169
+ 1,
170
+ 0,
171
+ ),
172
+ );
173
+ this.ctx.ui.requestRender();
174
+ return;
175
+ }
176
+
177
+ const confirmed = await this.ctx.showHookConfirm(
178
+ `Import ${result.importable.length} credential(s)?`,
179
+ summaryLines.join("\n"),
180
+ );
181
+ if (!confirmed) {
182
+ this.ctx.showStatus("Credential import cancelled.");
183
+ return;
184
+ }
185
+
186
+ const summary = await importCredentials(result.importable, (provider, credential) =>
187
+ this.ctx.session.modelRegistry.authStorage.upsertCredential(provider, credential),
188
+ );
189
+ await this.ctx.session.modelRegistry.refresh();
190
+
191
+ this.ctx.chatContainer.addChild(new Spacer(1));
192
+ for (const credential of summary.imported) {
193
+ this.ctx.chatContainer.addChild(
194
+ new Text(
195
+ theme.fg("success", `${theme.status.success} Imported ${credential.provider} (${credential.source})`),
196
+ 1,
197
+ 0,
198
+ ),
199
+ );
200
+ }
201
+ for (const failure of summary.failed) {
202
+ this.ctx.chatContainer.addChild(
203
+ new Text(
204
+ theme.fg("error", `${theme.status.error} Failed ${failure.credential.provider}: ${failure.error}`),
205
+ 1,
206
+ 0,
207
+ ),
208
+ );
209
+ }
210
+ if (summary.imported.length > 0) {
211
+ this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
212
+ }
213
+ this.ctx.ui.requestRender();
214
+ }
215
+
143
216
  showCustomProviderWizard(): void {
144
217
  this.showSelector(done => {
145
218
  let wizard: CustomProviderWizardComponent;
@@ -506,6 +579,13 @@ export class SelectorController {
506
579
  setPreferredSearchProvider(value);
507
580
  }
508
581
  break;
582
+ case "web_search.fallback":
583
+ if (Array.isArray(value)) {
584
+ setSearchFallbackProviders(
585
+ value.filter(item => typeof item === "string" && isConfigurableSearchProviderId(item)),
586
+ );
587
+ }
588
+ break;
509
589
  case "providers.image":
510
590
  if (
511
591
  value === "auto" ||
@@ -283,7 +283,7 @@ export class InteractiveMode implements InteractiveModeContext {
283
283
  }
284
284
  autoCompactionEscapeHandler?: () => void;
285
285
  retryEscapeHandler?: () => void;
286
- retryCountdownTimer?: ReturnType<typeof setInterval>;
286
+ retryCountdownTimer?: NodeJS.Timeout;
287
287
  unsubscribe?: () => void;
288
288
  onInputCallback?: (input: SubmittedUserInput) => void;
289
289
  optimisticUserMessageSignature: string | undefined = undefined;
@@ -709,6 +709,16 @@ export class InteractiveMode implements InteractiveModeContext {
709
709
  if (this.#pendingSubmittedInput) return;
710
710
  if (this.editor.getText().trim().length > 0) return;
711
711
  if ((this.pendingImages?.length ?? 0) > 0) return;
712
+ // Never fire an autonomous continuation prompt() while the session is
713
+ // busy. A wedged/orphaned subagent turn can leave isStreaming stuck true;
714
+ // firing prompt() here throws AgentBusyError, which submitInteractiveInput
715
+ // surfaces as a red "Error: Agent is already processing…" and then loops
716
+ // back to getUserInput(), re-arming this timer — an infinite error spam.
717
+ // Re-arm and only fire once the session returns to idle.
718
+ if (this.session.isStreaming || this.session.isCompacting) {
719
+ this.#scheduleGoalContinuation();
720
+ return;
721
+ }
712
722
  const latestState = this.session.getGoalModeState();
713
723
  if (!latestState?.enabled || latestState.goal.status !== "active") return;
714
724
  this.#goalContinuationTurnInFlight = true;