@gajae-code/coding-agent 0.5.1 → 0.5.3

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 (165) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +6 -0
  4. package/dist/types/cli/setup-cli.d.ts +8 -1
  5. package/dist/types/commands/setup.d.ts +7 -0
  6. package/dist/types/config/file-lock.d.ts +24 -2
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/models-config-schema.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +62 -0
  10. package/dist/types/dap/client.d.ts +2 -1
  11. package/dist/types/edit/read-file.d.ts +6 -0
  12. package/dist/types/eval/js/context-manager.d.ts +3 -0
  13. package/dist/types/eval/js/executor.d.ts +1 -0
  14. package/dist/types/exec/bash-executor.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  19. package/dist/types/lsp/types.d.ts +2 -0
  20. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +2 -0
  22. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  23. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  24. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  25. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
  28. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  29. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  30. package/dist/types/modes/theme/theme.d.ts +1 -0
  31. package/dist/types/modes/types.d.ts +1 -1
  32. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  33. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  34. package/dist/types/runtime-mcp/types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +17 -1
  36. package/dist/types/session/artifacts.d.ts +4 -1
  37. package/dist/types/session/history-storage.d.ts +2 -2
  38. package/dist/types/session/session-manager.d.ts +10 -1
  39. package/dist/types/session/streaming-output.d.ts +5 -0
  40. package/dist/types/setup/credential-import.d.ts +79 -0
  41. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  42. package/dist/types/task/executor.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +1 -1
  44. package/dist/types/tools/bash.d.ts +1 -0
  45. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  46. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  47. package/dist/types/tools/subagent-render.d.ts +7 -1
  48. package/dist/types/tools/subagent.d.ts +21 -0
  49. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  50. package/dist/types/web/search/index.d.ts +4 -4
  51. package/dist/types/web/search/provider.d.ts +16 -20
  52. package/dist/types/web/search/providers/base.d.ts +2 -1
  53. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  54. package/dist/types/web/search/types.d.ts +14 -2
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +7 -0
  57. package/src/async/job-manager.ts +153 -39
  58. package/src/cli/args.ts +2 -0
  59. package/src/cli/fast-help.ts +2 -0
  60. package/src/cli/setup-cli.ts +138 -3
  61. package/src/commands/setup.ts +5 -1
  62. package/src/commands/ultragoal.ts +3 -1
  63. package/src/config/file-lock-gc.ts +14 -2
  64. package/src/config/file-lock.ts +63 -13
  65. package/src/config/model-profile-activation.ts +15 -3
  66. package/src/config/model-profiles.ts +15 -15
  67. package/src/config/model-registry.ts +21 -1
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +62 -0
  70. package/src/dap/client.ts +105 -64
  71. package/src/dap/session.ts +44 -7
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  73. package/src/edit/read-file.ts +19 -1
  74. package/src/eval/js/context-manager.ts +228 -65
  75. package/src/eval/js/executor.ts +2 -0
  76. package/src/eval/js/index.ts +1 -0
  77. package/src/eval/js/worker-core.ts +10 -6
  78. package/src/eval/py/executor.ts +68 -19
  79. package/src/eval/py/kernel.ts +46 -22
  80. package/src/eval/py/runner.py +68 -14
  81. package/src/exec/bash-executor.ts +49 -13
  82. package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
  83. package/src/gjc-runtime/launch-tmux.ts +3 -4
  84. package/src/gjc-runtime/ralplan-runtime.ts +174 -12
  85. package/src/gjc-runtime/state-runtime.ts +2 -1
  86. package/src/gjc-runtime/state-writer.ts +254 -7
  87. package/src/gjc-runtime/tmux-gc.ts +88 -38
  88. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  89. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  90. package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
  91. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  92. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  93. package/src/harness-control-plane/owner.ts +3 -2
  94. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  95. package/src/hooks/skill-state.ts +121 -2
  96. package/src/internal-urls/artifact-protocol.ts +10 -1
  97. package/src/internal-urls/docs-index.generated.ts +14 -10
  98. package/src/lsp/client.ts +64 -26
  99. package/src/lsp/defaults.json +1 -0
  100. package/src/lsp/index.ts +2 -1
  101. package/src/lsp/lspmux.ts +33 -9
  102. package/src/lsp/types.ts +2 -0
  103. package/src/main.ts +14 -4
  104. package/src/modes/acp/acp-agent.ts +4 -2
  105. package/src/modes/bridge/bridge-mode.ts +23 -1
  106. package/src/modes/components/assistant-message.ts +10 -2
  107. package/src/modes/components/bash-execution.ts +5 -1
  108. package/src/modes/components/eval-execution.ts +5 -1
  109. package/src/modes/components/history-search.ts +5 -2
  110. package/src/modes/components/model-selector.ts +60 -2
  111. package/src/modes/components/oauth-selector.ts +5 -0
  112. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  113. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  114. package/src/modes/components/skill-message.ts +24 -16
  115. package/src/modes/components/tool-execution.ts +6 -0
  116. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  117. package/src/modes/controllers/input-controller.ts +5 -0
  118. package/src/modes/controllers/selector-controller.ts +86 -2
  119. package/src/modes/interactive-mode.ts +11 -1
  120. package/src/modes/rpc/rpc-mode.ts +132 -18
  121. package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
  122. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  123. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  124. package/src/modes/theme/defaults/claude-code.json +100 -0
  125. package/src/modes/theme/defaults/codex.json +100 -0
  126. package/src/modes/theme/defaults/index.ts +6 -0
  127. package/src/modes/theme/defaults/opencode.json +102 -0
  128. package/src/modes/theme/theme.ts +2 -2
  129. package/src/modes/types.ts +1 -1
  130. package/src/modes/utils/ui-helpers.ts +5 -2
  131. package/src/prompts/agents/executor.md +5 -2
  132. package/src/runtime/process-lifecycle.ts +400 -0
  133. package/src/runtime-mcp/manager.ts +164 -50
  134. package/src/runtime-mcp/transports/http.ts +12 -11
  135. package/src/runtime-mcp/transports/stdio.ts +64 -38
  136. package/src/runtime-mcp/types.ts +3 -0
  137. package/src/sdk.ts +39 -1
  138. package/src/session/agent-session.ts +190 -33
  139. package/src/session/artifacts.ts +17 -2
  140. package/src/session/blob-store.ts +36 -2
  141. package/src/session/history-storage.ts +32 -11
  142. package/src/session/session-manager.ts +99 -31
  143. package/src/session/streaming-output.ts +54 -3
  144. package/src/setup/credential-import.ts +429 -0
  145. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  146. package/src/slash-commands/builtin-registry.ts +30 -3
  147. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  148. package/src/task/executor.ts +7 -1
  149. package/src/task/render.ts +18 -7
  150. package/src/tools/archive-reader.ts +10 -1
  151. package/src/tools/ask.ts +4 -2
  152. package/src/tools/bash.ts +11 -4
  153. package/src/tools/browser/tab-supervisor.ts +22 -0
  154. package/src/tools/browser.ts +38 -4
  155. package/src/tools/cron.ts +1 -1
  156. package/src/tools/read.ts +11 -12
  157. package/src/tools/sqlite-reader.ts +19 -5
  158. package/src/tools/subagent-render.ts +119 -29
  159. package/src/tools/subagent.ts +147 -7
  160. package/src/tools/ultragoal-ask-guard.ts +39 -0
  161. package/src/web/search/index.ts +25 -25
  162. package/src/web/search/provider.ts +178 -87
  163. package/src/web/search/providers/base.ts +2 -1
  164. package/src/web/search/providers/openai-compatible.ts +151 -0
  165. package/src/web/search/types.ts +47 -22
package/src/lsp/client.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { isEnoent, logger, ptree, untilAborted } from "@gajae-code/utils";
1
+ import { isEnoent, logger, untilAborted } from "@gajae-code/utils";
2
2
  import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
3
+ import { registerResourceOwner, spawnOwnedProcess } from "../runtime/process-lifecycle";
3
4
  import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
4
5
  import { applyWorkspaceEdit } from "./edits";
5
6
  import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
@@ -19,8 +20,10 @@ import { detectLanguageId, fileToUri } from "./utils";
19
20
  // =============================================================================
20
21
 
21
22
  const clients = new Map<string, LspClient>();
23
+ const killedClients = new WeakSet<LspClient>();
22
24
  const clientLocks = new Map<string, Promise<LspClient>>();
23
25
  const fileOperationLocks = new Map<string, Promise<void>>();
26
+ const lspCleanupOwner = registerResourceOwner("lsp:clients", shutdownAll);
24
27
 
25
28
  // Idle timeout configuration (disabled by default)
26
29
  let idleTimeoutMs: number | null = null;
@@ -65,6 +68,32 @@ export function isIdleCheckerActiveForTests(): boolean {
65
68
  return idleCheckInterval !== null;
66
69
  }
67
70
 
71
+ function rejectPendingRequests(client: LspClient, error: Error): void {
72
+ for (const pending of client.pendingRequests.values()) {
73
+ pending.reject(error);
74
+ }
75
+ client.pendingRequests.clear();
76
+ }
77
+
78
+ function deleteCachedClient(key: string, client: LspClient): void {
79
+ if (clients.get(key) === client) {
80
+ clients.delete(key);
81
+ }
82
+ }
83
+
84
+ function deleteClientLock(key: string, clientPromise: Promise<LspClient>): void {
85
+ if (clientLocks.get(key) === clientPromise) {
86
+ clientLocks.delete(key);
87
+ }
88
+ }
89
+
90
+ function evictDeadCachedClient(key: string, client: LspClient): void {
91
+ if (client.proc.exitCode === null && !client.proc.killed && !client.owner?.disposed && !killedClients.has(client))
92
+ return;
93
+ deleteCachedClient(key, client);
94
+ client.resolveProjectLoaded();
95
+ rejectPendingRequests(client, new Error("LSP server exited"));
96
+ }
68
97
  // =============================================================================
69
98
  // Client Capabilities
70
99
  // =============================================================================
@@ -432,8 +461,11 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
432
461
  // Check if client already exists
433
462
  const existingClient = clients.get(key);
434
463
  if (existingClient) {
435
- existingClient.lastActivity = Date.now();
436
- return existingClient;
464
+ evictDeadCachedClient(key, existingClient);
465
+ if (clients.has(key)) {
466
+ existingClient.lastActivity = Date.now();
467
+ return existingClient;
468
+ }
437
469
  }
438
470
 
439
471
  // Check if another coroutine is already creating this client
@@ -443,7 +475,8 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
443
475
  }
444
476
 
445
477
  // Create new client with lock
446
- const clientPromise = (async () => {
478
+ let clientPromise!: Promise<LspClient>;
479
+ clientPromise = (async () => {
447
480
  const baseCommand = config.resolvedCommand ?? config.command;
448
481
  const baseArgs = config.args ?? [];
449
482
 
@@ -452,11 +485,13 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
452
485
  ? await getLspmuxCommand(baseCommand, baseArgs)
453
486
  : { command: baseCommand, args: baseArgs };
454
487
 
455
- const proc = ptree.spawn([command, ...args], {
488
+ const owner = spawnOwnedProcess([command, ...args], {
456
489
  cwd,
457
490
  stdin: "pipe",
458
491
  env: env ? { ...Bun.env, ...env } : undefined,
492
+ name: `lsp:${config.command}`,
459
493
  });
494
+ const proc = owner.child;
460
495
 
461
496
  let resolveProjectLoaded!: () => void;
462
497
  const projectLoaded = new Promise<void>(resolve => {
@@ -474,6 +509,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
474
509
  name: key,
475
510
  cwd,
476
511
  proc,
512
+ owner,
477
513
  config,
478
514
  requestId: 0,
479
515
  diagnostics: new Map(),
@@ -488,12 +524,17 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
488
524
  projectLoaded,
489
525
  resolveProjectLoaded,
490
526
  };
527
+ const originalKill = proc.kill.bind(proc);
528
+ proc.kill = (...args: Parameters<typeof proc.kill>) => {
529
+ killedClients.add(client);
530
+ return originalKill(...args);
531
+ };
491
532
  clients.set(key, client);
492
533
 
493
534
  // Register crash recovery - remove client on process exit
494
535
  proc.exited.then(async () => {
495
- clients.delete(key);
496
- clientLocks.delete(key);
536
+ deleteCachedClient(key, client);
537
+ deleteClientLock(key, clientPromise);
497
538
  client.resolveProjectLoaded();
498
539
 
499
540
  // Reject any pending requests — the server is gone, they will never complete.
@@ -564,12 +605,12 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
564
605
  return client;
565
606
  } catch (err) {
566
607
  // Clean up on initialization failure
567
- clients.delete(key);
568
- clientLocks.delete(key);
569
- proc.kill();
608
+ deleteCachedClient(key, client);
609
+ deleteClientLock(key, clientPromise);
610
+ await shutdownClientInstance(client);
570
611
  throw err;
571
612
  } finally {
572
- clientLocks.delete(key);
613
+ deleteClientLock(key, clientPromise);
573
614
  }
574
615
  })();
575
616
 
@@ -645,12 +686,7 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
645
686
  */
646
687
  export async function waitForProjectLoaded(client: LspClient, signal?: AbortSignal): Promise<void> {
647
688
  if (signal?.aborted) return;
648
- await Promise.race([
649
- client.projectLoaded,
650
- ...(signal
651
- ? [new Promise<void>(resolve => signal.addEventListener("abort", () => resolve(), { once: true }))]
652
- : []),
653
- ]);
689
+ await untilAborted(signal, client.projectLoaded);
654
690
  }
655
691
 
656
692
  /**
@@ -793,16 +829,12 @@ export async function refreshFile(client: LspClient, filePath: string, signal?:
793
829
  */
794
830
  async function shutdownClientInstance(client: LspClient): Promise<void> {
795
831
  const err = new Error("LSP client shutdown");
796
- for (const pending of Array.from(client.pendingRequests.values())) {
797
- pending.reject(err);
798
- }
799
- client.pendingRequests.clear();
832
+ rejectPendingRequests(client, err);
800
833
 
801
- const timeout = Bun.sleep(5_000);
802
- const shutdown = sendRequest(client, "shutdown", null).catch(() => {});
803
- await Promise.race([shutdown, timeout]);
804
- client.proc.kill();
805
- await Promise.race([client.proc.exited.catch(() => {}), Bun.sleep(1_000)]);
834
+ const shutdown = sendRequest(client, "shutdown", null, undefined, 5_000).catch(() => {});
835
+ await Promise.race([shutdown, Bun.sleep(5_000)]);
836
+ await client.owner?.dispose();
837
+ await client.owner?.awaitExit({ timeoutMs: 1_000 });
806
838
  }
807
839
 
808
840
  export async function shutdownClient(key: string): Promise<void> {
@@ -959,6 +991,12 @@ if (typeof process !== "undefined") {
959
991
  process.on("beforeExit", () => {
960
992
  void shutdownAll();
961
993
  });
994
+ process.on("exit", () => {
995
+ lspCleanupOwner();
996
+ for (const client of clients.values()) {
997
+ client.proc.kill();
998
+ }
999
+ });
962
1000
  process.on("SIGINT", () => {
963
1001
  void (async () => {
964
1002
  await shutdownAll();
@@ -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/lsp/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  sendNotification,
20
20
  sendRequest,
21
21
  setIdleTimeout,
22
+ shutdownClient,
22
23
  syncContent,
23
24
  WARMUP_TIMEOUT_MS,
24
25
  waitForProjectLoaded,
@@ -400,7 +401,7 @@ async function reloadServer(client: LspClient, serverName: string, signal?: Abor
400
401
  }
401
402
  }
402
403
  if (output.startsWith("Restarted")) {
403
- client.proc.kill();
404
+ await shutdownClient(client.name);
404
405
  }
405
406
  return output;
406
407
  }
package/src/lsp/lspmux.ts CHANGED
@@ -2,6 +2,7 @@ import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import { $flag, $which, logger } from "@gajae-code/utils";
4
4
  import { TOML } from "bun";
5
+ import { spawnOwnedProcess } from "../runtime/process-lifecycle";
5
6
 
6
7
  /**
7
8
  * lspmux integration for LSP server multiplexing.
@@ -101,19 +102,24 @@ async function parseConfig(): Promise<LspmuxConfig | null> {
101
102
  */
102
103
  async function checkServerRunning(binaryPath: string): Promise<boolean> {
103
104
  try {
104
- const proc = Bun.spawn([binaryPath, "status"], {
105
- stdout: "pipe",
106
- stderr: "pipe",
107
- windowsHide: true,
105
+ const owner = spawnOwnedProcess([binaryPath, "status"], {
106
+ stdin: "ignore",
107
+ name: "lspmux:status",
108
108
  });
109
+ const proc = owner.child;
110
+ drainStream(proc.stdout);
111
+ drainStream(proc.stderr);
109
112
 
110
- const exited = await Promise.race([
111
- proc.exited,
112
- new Promise<null>(resolve => setTimeout(() => resolve(null), LIVENESS_TIMEOUT_MS)),
113
- ]);
113
+ let timer: ReturnType<typeof setTimeout> | undefined;
114
+ const timeout = new Promise<null>(resolve => {
115
+ timer = setTimeout(() => resolve(null), LIVENESS_TIMEOUT_MS);
116
+ });
117
+ const exited = await Promise.race([proc.exited, timeout]);
118
+ if (timer) clearTimeout(timer);
114
119
 
115
120
  if (exited === null) {
116
- proc.kill();
121
+ await owner.dispose();
122
+ await owner.awaitExit({ timeoutMs: 1_000 });
117
123
  return false;
118
124
  }
119
125
 
@@ -123,6 +129,24 @@ async function checkServerRunning(binaryPath: string): Promise<boolean> {
123
129
  }
124
130
  }
125
131
 
132
+ function drainStream(stream: ReadableStream<Uint8Array> | null | undefined): void {
133
+ if (!stream) return;
134
+ void (async () => {
135
+ try {
136
+ const reader = stream.getReader();
137
+ try {
138
+ while (!(await reader.read()).done) {
139
+ // drain only
140
+ }
141
+ } finally {
142
+ reader.releaseLock();
143
+ }
144
+ } catch {
145
+ // Process stream closed or already consumed.
146
+ }
147
+ })();
148
+ }
149
+
126
150
  /**
127
151
  * Detect lspmux availability and state.
128
152
  * Results are cached for STATE_CACHE_TTL_MS.
package/src/lsp/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ptree } from "@gajae-code/utils";
2
2
  import * as z from "zod/v4";
3
+ import type { OwnedProcess } from "../runtime/process-lifecycle";
3
4
 
4
5
  // =============================================================================
5
6
  // Tool Schema
@@ -399,6 +400,7 @@ export interface LspClient {
399
400
  cwd: string;
400
401
  config: ServerConfig;
401
402
  proc: ptree.ChildProcess<"pipe">;
403
+ owner?: OwnedProcess;
402
404
  requestId: number;
403
405
  diagnostics: Map<string, PublishedDiagnostics>;
404
406
  diagnosticsVersion: number;
package/src/main.ts CHANGED
@@ -977,10 +977,20 @@ export async function runRootCommand(
977
977
  }
978
978
 
979
979
  if (mode === "rpc" || mode === "rpc-ui") {
980
- const { runRpcMode } = await import("./modes/rpc/rpc-mode");
981
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, {
982
- listen: parsedArgs.rpcListen,
983
- });
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
+ }
984
994
  } else if (mode === "bridge") {
985
995
  const { runBridgeMode } = await import("./modes/bridge/bridge-mode");
986
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";
@@ -166,6 +166,25 @@ function parseBridgeScopes(value: string | undefined): readonly BridgeCommandSco
166
166
  return [...scopes];
167
167
  }
168
168
 
169
+ // Opt-in endpoint enablement via GJC_BRIDGE_ENDPOINTS (default undefined -> fail closed, backward compatible).
170
+ // Accepts "all" or a comma list of matrix keys.
171
+ export function parseBridgeEndpoints(value: string | undefined): Partial<BridgeEndpointMatrix> | undefined {
172
+ if (!value?.trim()) return undefined;
173
+ const allowed = new Set<string>(Object.keys(FAIL_CLOSED_BRIDGE_ENDPOINTS));
174
+ const matrix: Partial<BridgeEndpointMatrix> = {};
175
+ if (value.trim().toLowerCase() === "all") {
176
+ for (const key of allowed) matrix[key as keyof BridgeEndpointMatrix] = true;
177
+ return matrix;
178
+ }
179
+ for (const raw of value.split(",")) {
180
+ const key = raw.trim();
181
+ if (!key) continue;
182
+ if (!allowed.has(key)) throw new Error(`Invalid GJC_BRIDGE_ENDPOINTS entry: ${key}`);
183
+ matrix[key as keyof BridgeEndpointMatrix] = true;
184
+ }
185
+ return matrix;
186
+ }
187
+
169
188
  function hasScope(scopes: readonly BridgeCommandScope[] | undefined, scope: BridgeCommandScope): boolean {
170
189
  return new Set(scopes ?? DEFAULT_BRIDGE_SCOPES).has(scope);
171
190
  }
@@ -521,6 +540,7 @@ export async function runBridgeMode(
521
540
  throw new Error(`Invalid GJC_BRIDGE_PORT: ${Bun.env.GJC_BRIDGE_PORT}`);
522
541
  }
523
542
  const commandScopes = parseBridgeScopes(Bun.env.GJC_BRIDGE_SCOPES);
543
+ const endpointMatrix = parseBridgeEndpoints(Bun.env.GJC_BRIDGE_ENDPOINTS);
524
544
 
525
545
  const certPath = Bun.env.GJC_BRIDGE_TLS_CERT;
526
546
  const keyPath = Bun.env.GJC_BRIDGE_TLS_KEY;
@@ -599,6 +619,7 @@ export async function runBridgeMode(
599
619
  emitFrame: gate => eventStream.publish(toBridgeWorkflowGateFrame(gate, sequencer)),
600
620
  store: gateStore,
601
621
  audit: recordAudit,
622
+ providerSupportsTokenCostMetrics: modelSupportsTokenCostMetrics(session.model),
602
623
  getUsageSnapshot: () => {
603
624
  const stats = session.getSessionStats();
604
625
  return { tokens: stats.tokens.total, costUsd: stats.cost };
@@ -625,6 +646,7 @@ export async function runBridgeMode(
625
646
  hostToolBridge,
626
647
  hostUriBridge,
627
648
  commandScopes,
649
+ endpointMatrix,
628
650
  unattendedControlPlane,
629
651
  commandDispatcher: command =>
630
652
  dispatchRpcCommand(command, {
@@ -111,8 +111,16 @@ export class AssistantMessageComponent extends Container {
111
111
  const cached = this.#contentBlocksCache.get(content);
112
112
  if (cached?.source === content.text) return cached.component;
113
113
  const trimmed = content.text.trim();
114
- const component =
115
- renderDeepInterviewAssistantText(trimmed, theme) ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
114
+ const deepInterview = renderDeepInterviewAssistantText(trimmed, theme);
115
+ // Reuse the same Markdown instance across streaming chunks (update text in place)
116
+ // instead of constructing a new one each chunk; combined with the markdown
117
+ // per-code-block highlight cache, appends no longer re-highlight the whole prefix.
118
+ if (!deepInterview && cached && cached.component instanceof Markdown) {
119
+ cached.component.setText(trimmed);
120
+ cached.source = content.text;
121
+ return cached.component;
122
+ }
123
+ const component = deepInterview ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
116
124
  this.#contentBlocksCache.set(content, { source: content.text, component });
117
125
  return component;
118
126
  }
@@ -155,7 +155,11 @@ export class BashExecutionComponent extends Container {
155
155
  const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
156
156
 
157
157
  // Rebuild content container
158
- this.#contentContainer.clear();
158
+ // Detach (not dispose): #headerText and the running #loader are persistent,
159
+ // reused instances re-added below. A disposing clear() would stop the loader's
160
+ // animation timer mid-run. Final teardown still stops the loader via the
161
+ // component's recursive dispose().
162
+ this.#contentContainer.detachAll();
159
163
 
160
164
  // Command header
161
165
  this.#contentContainer.addChild(this.#headerText);
@@ -114,7 +114,11 @@ export class EvalExecutionComponent extends Container {
114
114
  const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
115
115
  const hiddenLineCount = availableLines.length - previewLogicalLines.length;
116
116
 
117
- this.#contentContainer.clear();
117
+ // Detach (not dispose): #headerText and the running #loader are persistent,
118
+ // reused instances re-added below. A disposing clear() would stop the loader's
119
+ // animation timer mid-run. Final teardown still stops the loader via the
120
+ // component's recursive dispose().
121
+ this.#contentContainer.detachAll();
118
122
 
119
123
  this.#contentContainer.addChild(this.#headerText);
120
124
 
@@ -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
  }
@@ -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",
@@ -196,6 +210,9 @@ export class ModelSelectorComponent extends Container {
196
210
  #tui: TUI;
197
211
  #scopedModels: ReadonlyArray<ScopedModelItem>;
198
212
  #temporaryOnly: boolean;
213
+ #currentModel?: Model;
214
+ #isFastForProvider: (provider?: string) => boolean = () => false;
215
+ #isFastForSubagentProvider: (provider?: string) => boolean = () => false;
199
216
  #pendingActionItem?: ModelItem | CanonicalModelItem;
200
217
  #selectedActionIndex: number = 0;
201
218
  #pendingThinkingChoice?: PendingThinkingChoice;
@@ -225,7 +242,13 @@ export class ModelSelectorComponent extends Container {
225
242
  scopedModels: ReadonlyArray<ScopedModelItem>,
226
243
  onSelect: RoleSelectCallback,
227
244
  onCancel: () => void,
228
- options?: { temporaryOnly?: boolean; initialSearchInput?: string; sessionId?: string },
245
+ options?: {
246
+ temporaryOnly?: boolean;
247
+ initialSearchInput?: string;
248
+ sessionId?: string;
249
+ isFastForProvider?: (provider?: string) => boolean;
250
+ isFastForSubagentProvider?: (provider?: string) => boolean;
251
+ },
229
252
  ) {
230
253
  super();
231
254
 
@@ -237,6 +260,9 @@ export class ModelSelectorComponent extends Container {
237
260
  this.#onCancelCallback = onCancel;
238
261
  this.#temporaryOnly = options?.temporaryOnly ?? false;
239
262
  this.#authSessionId = options?.sessionId;
263
+ this.#currentModel = _currentModel;
264
+ this.#isFastForProvider = options?.isFastForProvider ?? (() => false);
265
+ this.#isFastForSubagentProvider = options?.isFastForSubagentProvider ?? (() => false);
240
266
  const initialSearchInput = options?.initialSearchInput;
241
267
  this.#viewMode = this.#temporaryOnly || initialSearchInput || scopedModels.length > 0 ? "models" : "presets";
242
268
 
@@ -739,6 +765,18 @@ export class ModelSelectorComponent extends Container {
739
765
  if (selected?.kind === "group") this.#expandedPresetProviderId = selected.groupId;
740
766
  if (selected?.kind === "profile") this.#expandedPresetProviderId = selected.groupId;
741
767
  const rows = this.#getPresetRows();
768
+ // Expanding/collapsing a group shifts row positions. Relocate the cursor by
769
+ // the selected row's logical identity so crossing a provider group boundary
770
+ // keeps it on the same logical row instead of overshooting into the
771
+ // destination group's profiles (or off the end of the list).
772
+ if (selected) {
773
+ const targetIdentity = presetRowIdentity(selected);
774
+ const relocated = rows.findIndex(row => presetRowIdentity(row) === targetIdentity);
775
+ if (relocated >= 0) {
776
+ this.#presetCursor = relocated;
777
+ return;
778
+ }
779
+ }
742
780
  this.#presetCursor = Math.min(this.#presetCursor, Math.max(0, rows.length - 1));
743
781
  }
744
782
 
@@ -902,15 +940,35 @@ export class ModelSelectorComponent extends Container {
902
940
 
903
941
  // Build role badges (inverted: color as background, black text)
904
942
  const roleBadgeTokens: string[] = [];
943
+ let roleMatched = false;
905
944
  for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
906
945
  const roleInfo = GJC_MODEL_ASSIGNMENT_TARGETS[role];
907
946
  const assigned = this.#roles[role];
908
947
  if (roleInfo.tag && assigned && modelsAreEqual(assigned.model, item.model)) {
948
+ roleMatched = true;
909
949
  const badge = makeInvertedBadge(roleInfo.tag, roleInfo.color ?? "muted");
910
950
  const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
911
- roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
951
+ // Subagent roles (task.agentModelOverrides) run under task.serviceTier, so
952
+ // their ⚡ must reflect the effective subagent tier, not the main session tier.
953
+ const roleFast =
954
+ roleInfo.settingsPath === "task.agentModelOverrides"
955
+ ? this.#isFastForSubagentProvider(assigned.model.provider)
956
+ : this.#isFastForProvider(assigned.model.provider);
957
+ const fastSuffix = roleFast ? ` ${theme.icon.fast}` : "";
958
+ roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}${fastSuffix}`);
912
959
  }
913
960
  }
961
+ // Active/current non-role row: show the fast glyph on the session's current
962
+ // model row even when it carries no role badge. Skip when a role token for
963
+ // this row already rendered the glyph (duplicate-glyph guard).
964
+ if (
965
+ !roleMatched &&
966
+ this.#currentModel !== undefined &&
967
+ modelsAreEqual(this.#currentModel, item.model) &&
968
+ this.#isFastForProvider(item.model.provider)
969
+ ) {
970
+ roleBadgeTokens.push(theme.icon.fast);
971
+ }
914
972
  const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
915
973
 
916
974
  let line = "";
@@ -65,6 +65,11 @@ export class OAuthSelectorComponent extends Container {
65
65
  this.#validationGeneration += 1;
66
66
  this.#stopSpinner();
67
67
  }
68
+
69
+ dispose(): void {
70
+ this.stopValidation();
71
+ super.dispose();
72
+ }
68
73
  #loadProviders(): void {
69
74
  this.#allProviders = getOAuthProviders();
70
75
  }
@@ -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 {