@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.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
package/src/lsp/client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { isEnoent, logger,
|
|
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
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
608
|
+
deleteCachedClient(key, client);
|
|
609
|
+
deleteClientLock(key, clientPromise);
|
|
610
|
+
await shutdownClientInstance(client);
|
|
570
611
|
throw err;
|
|
571
612
|
} finally {
|
|
572
|
-
|
|
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
|
|
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
|
-
|
|
797
|
-
pending.reject(err);
|
|
798
|
-
}
|
|
799
|
-
client.pendingRequests.clear();
|
|
832
|
+
rejectPendingRequests(client, err);
|
|
800
833
|
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
await
|
|
804
|
-
client.
|
|
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();
|
package/src/lsp/defaults.json
CHANGED
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.
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
982
|
-
|
|
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
|
|
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:
|
|
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
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?: {
|
|
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
|
-
|
|
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 {
|