@gajae-code/coding-agent 0.5.2 → 0.5.4
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 +23 -0
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +10 -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/tmux-sessions.d.ts +7 -1
- 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/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 -0
- package/dist/types/modes/types.d.ts +1 -0
- 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 +29 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/streaming-output.d.ts +12 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- 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/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +181 -43
- package/src/config/file-lock.ts +9 -1
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- 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-runtime.ts +14 -13
- package/src/gjc-runtime/ralplan-runtime.ts +10 -0
- package/src/gjc-runtime/state-runtime.ts +73 -0
- package/src/gjc-runtime/tmux-gc.ts +86 -37
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/client.ts +64 -26
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +21 -0
- 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/model-selector.ts +34 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- 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 +19 -0
- package/src/modes/controllers/selector-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +1 -1
- 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 +27 -0
- package/src/session/agent-session.ts +271 -25
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/session-manager.ts +29 -13
- package/src/session/streaming-output.ts +95 -3
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +2 -6
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/web/search/providers/codex.ts +6 -5
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/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;
|
|
@@ -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;
|
|
@@ -626,6 +646,7 @@ export async function runBridgeMode(
|
|
|
626
646
|
hostToolBridge,
|
|
627
647
|
hostUriBridge,
|
|
628
648
|
commandScopes,
|
|
649
|
+
endpointMatrix,
|
|
629
650
|
unattendedControlPlane,
|
|
630
651
|
commandDispatcher: command =>
|
|
631
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
|
|
|
@@ -210,6 +210,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
210
210
|
#tui: TUI;
|
|
211
211
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
212
212
|
#temporaryOnly: boolean;
|
|
213
|
+
#currentModel?: Model;
|
|
214
|
+
#isFastForProvider: (provider?: string) => boolean = () => false;
|
|
215
|
+
#isFastForSubagentProvider: (provider?: string) => boolean = () => false;
|
|
213
216
|
#pendingActionItem?: ModelItem | CanonicalModelItem;
|
|
214
217
|
#selectedActionIndex: number = 0;
|
|
215
218
|
#pendingThinkingChoice?: PendingThinkingChoice;
|
|
@@ -239,7 +242,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
239
242
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
240
243
|
onSelect: RoleSelectCallback,
|
|
241
244
|
onCancel: () => void,
|
|
242
|
-
options?: {
|
|
245
|
+
options?: {
|
|
246
|
+
temporaryOnly?: boolean;
|
|
247
|
+
initialSearchInput?: string;
|
|
248
|
+
sessionId?: string;
|
|
249
|
+
isFastForProvider?: (provider?: string) => boolean;
|
|
250
|
+
isFastForSubagentProvider?: (provider?: string) => boolean;
|
|
251
|
+
},
|
|
243
252
|
) {
|
|
244
253
|
super();
|
|
245
254
|
|
|
@@ -251,6 +260,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
251
260
|
this.#onCancelCallback = onCancel;
|
|
252
261
|
this.#temporaryOnly = options?.temporaryOnly ?? false;
|
|
253
262
|
this.#authSessionId = options?.sessionId;
|
|
263
|
+
this.#currentModel = _currentModel;
|
|
264
|
+
this.#isFastForProvider = options?.isFastForProvider ?? (() => false);
|
|
265
|
+
this.#isFastForSubagentProvider = options?.isFastForSubagentProvider ?? (() => false);
|
|
254
266
|
const initialSearchInput = options?.initialSearchInput;
|
|
255
267
|
this.#viewMode = this.#temporaryOnly || initialSearchInput || scopedModels.length > 0 ? "models" : "presets";
|
|
256
268
|
|
|
@@ -928,15 +940,35 @@ export class ModelSelectorComponent extends Container {
|
|
|
928
940
|
|
|
929
941
|
// Build role badges (inverted: color as background, black text)
|
|
930
942
|
const roleBadgeTokens: string[] = [];
|
|
943
|
+
let roleMatched = false;
|
|
931
944
|
for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
|
|
932
945
|
const roleInfo = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
933
946
|
const assigned = this.#roles[role];
|
|
934
947
|
if (roleInfo.tag && assigned && modelsAreEqual(assigned.model, item.model)) {
|
|
948
|
+
roleMatched = true;
|
|
935
949
|
const badge = makeInvertedBadge(roleInfo.tag, roleInfo.color ?? "muted");
|
|
936
950
|
const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
|
|
937
|
-
|
|
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}`);
|
|
938
959
|
}
|
|
939
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
|
+
}
|
|
940
972
|
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
|
|
941
973
|
|
|
942
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
|
}
|
|
@@ -131,6 +131,11 @@ export class MCPAddWizard extends Container {
|
|
|
131
131
|
| null = null;
|
|
132
132
|
#onTestConnectionCallback: ((config: MCPServerConfig) => Promise<void>) | null = null;
|
|
133
133
|
#onRenderCallback: (() => void) | null = null;
|
|
134
|
+
#disposed = false;
|
|
135
|
+
#transitionTimers = new Set<NodeJS.Timeout>();
|
|
136
|
+
#healthCheckSpinner?: NodeJS.Timeout;
|
|
137
|
+
#healthCheckTimeout?: NodeJS.Timeout;
|
|
138
|
+
#asyncGeneration = 0;
|
|
134
139
|
|
|
135
140
|
constructor(
|
|
136
141
|
onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void,
|
|
@@ -178,6 +183,39 @@ export class MCPAddWizard extends Container {
|
|
|
178
183
|
this.#renderStep();
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
dispose(): void {
|
|
187
|
+
if (this.#disposed) return;
|
|
188
|
+
this.#disposed = true;
|
|
189
|
+
this.#asyncGeneration += 1;
|
|
190
|
+
for (const timer of this.#transitionTimers) {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
}
|
|
193
|
+
this.#transitionTimers.clear();
|
|
194
|
+
this.#clearHealthCheckTimers();
|
|
195
|
+
super.dispose();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#scheduleTransition(callback: () => void, delay: number): void {
|
|
199
|
+
if (this.#disposed) return;
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
this.#transitionTimers.delete(timer);
|
|
202
|
+
if (this.#disposed) return;
|
|
203
|
+
callback();
|
|
204
|
+
}, delay);
|
|
205
|
+
this.#transitionTimers.add(timer);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#clearHealthCheckTimers(): void {
|
|
209
|
+
if (this.#healthCheckSpinner) {
|
|
210
|
+
clearInterval(this.#healthCheckSpinner);
|
|
211
|
+
this.#healthCheckSpinner = undefined;
|
|
212
|
+
}
|
|
213
|
+
if (this.#healthCheckTimeout) {
|
|
214
|
+
clearTimeout(this.#healthCheckTimeout);
|
|
215
|
+
this.#healthCheckTimeout = undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
#requestRender(): void {
|
|
182
220
|
this.#onRenderCallback?.();
|
|
183
221
|
}
|
|
@@ -941,6 +979,8 @@ export class MCPAddWizard extends Container {
|
|
|
941
979
|
* Test connection and automatically detect if auth is needed.
|
|
942
980
|
*/
|
|
943
981
|
async #testConnectionAndDetectAuth(): Promise<void> {
|
|
982
|
+
if (this.#disposed) return;
|
|
983
|
+
const generation = ++this.#asyncGeneration;
|
|
944
984
|
const testConfig = this.#buildServerConfig();
|
|
945
985
|
|
|
946
986
|
if (!this.#onTestConnectionCallback) {
|
|
@@ -954,6 +994,7 @@ export class MCPAddWizard extends Container {
|
|
|
954
994
|
try {
|
|
955
995
|
// Try to connect - timeout is handled by the transport layer (5 seconds)
|
|
956
996
|
await this.#onTestConnectionCallback(testConfig);
|
|
997
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
957
998
|
|
|
958
999
|
// Success! No auth required
|
|
959
1000
|
this.#contentContainer.clear();
|
|
@@ -962,7 +1003,7 @@ export class MCPAddWizard extends Container {
|
|
|
962
1003
|
this.#contentContainer.addChild(new Text("No authentication required", 0, 0));
|
|
963
1004
|
this.#contentContainer.addChild(new Spacer(1));
|
|
964
1005
|
|
|
965
|
-
|
|
1006
|
+
this.#scheduleTransition(() => {
|
|
966
1007
|
this.#state.authMethod = "none";
|
|
967
1008
|
this.#currentStep = "scope";
|
|
968
1009
|
this.#selectedIndex = 0;
|
|
@@ -978,10 +1019,12 @@ export class MCPAddWizard extends Container {
|
|
|
978
1019
|
if (!oauth && this.#state.transport !== "stdio" && this.#state.url) {
|
|
979
1020
|
try {
|
|
980
1021
|
oauth = await discoverOAuthEndpoints(this.#state.url, authResult.authServerUrl);
|
|
1022
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
981
1023
|
} catch {
|
|
982
1024
|
// Ignore discovery failures and fallback to manual auth.
|
|
983
1025
|
}
|
|
984
1026
|
}
|
|
1027
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
985
1028
|
|
|
986
1029
|
if (oauth) {
|
|
987
1030
|
this.#state.oauthAuthUrl = oauth.authorizationUrl;
|
|
@@ -1019,7 +1062,7 @@ export class MCPAddWizard extends Container {
|
|
|
1019
1062
|
this.#contentContainer.addChild(new Spacer(1));
|
|
1020
1063
|
this.#contentContainer.addChild(new Text(theme.fg("muted", "Adding server anyway..."), 0, 0));
|
|
1021
1064
|
|
|
1022
|
-
|
|
1065
|
+
this.#scheduleTransition(() => {
|
|
1023
1066
|
this.#state.authMethod = "none";
|
|
1024
1067
|
this.#currentStep = "scope";
|
|
1025
1068
|
this.#selectedIndex = 0;
|
|
@@ -1107,6 +1150,8 @@ export class MCPAddWizard extends Container {
|
|
|
1107
1150
|
}
|
|
1108
1151
|
|
|
1109
1152
|
async #launchOAuthFlow(): Promise<void> {
|
|
1153
|
+
if (this.#disposed) return;
|
|
1154
|
+
const generation = ++this.#asyncGeneration;
|
|
1110
1155
|
if (!this.#onOAuthCallback) {
|
|
1111
1156
|
this.#contentContainer.clear();
|
|
1112
1157
|
this.#contentContainer.addChild(new Text(theme.fg("error", "OAuth flow not available"), 0, 0));
|
|
@@ -1150,6 +1195,7 @@ export class MCPAddWizard extends Container {
|
|
|
1150
1195
|
this.#state.oauthClientSecret,
|
|
1151
1196
|
this.#state.oauthScopes,
|
|
1152
1197
|
);
|
|
1198
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
1153
1199
|
|
|
1154
1200
|
// Store credential ID + any dynamically-registered client credentials,
|
|
1155
1201
|
// so the final mcp.json entry persists everything needed for refresh.
|
|
@@ -1168,7 +1214,8 @@ export class MCPAddWizard extends Container {
|
|
|
1168
1214
|
this.#contentContainer.addChild(healthText);
|
|
1169
1215
|
|
|
1170
1216
|
let spinnerIndex = 0;
|
|
1171
|
-
|
|
1217
|
+
this.#healthCheckSpinner = setInterval(() => {
|
|
1218
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
1172
1219
|
healthText.setText(
|
|
1173
1220
|
theme.fg("muted", `${spinnerFrames[spinnerIndex % spinnerFrames.length]} Checking server connection...`),
|
|
1174
1221
|
);
|
|
@@ -1181,7 +1228,7 @@ export class MCPAddWizard extends Container {
|
|
|
1181
1228
|
if (this.#onTestConnectionCallback) {
|
|
1182
1229
|
try {
|
|
1183
1230
|
const { promise: timeoutPromise, reject: timeoutReject } = Promise.withResolvers<never>();
|
|
1184
|
-
|
|
1231
|
+
this.#healthCheckTimeout = setTimeout(
|
|
1185
1232
|
() => timeoutReject(new Error("Health check timed out after 10 seconds")),
|
|
1186
1233
|
10_000,
|
|
1187
1234
|
);
|
|
@@ -1191,15 +1238,19 @@ export class MCPAddWizard extends Container {
|
|
|
1191
1238
|
timeoutPromise,
|
|
1192
1239
|
]);
|
|
1193
1240
|
} finally {
|
|
1194
|
-
|
|
1241
|
+
if (this.#healthCheckTimeout) {
|
|
1242
|
+
clearTimeout(this.#healthCheckTimeout);
|
|
1243
|
+
this.#healthCheckTimeout = undefined;
|
|
1244
|
+
}
|
|
1195
1245
|
}
|
|
1196
1246
|
} catch (error) {
|
|
1197
1247
|
healthPassed = false;
|
|
1198
1248
|
healthError = sanitize(error instanceof Error ? error.message : String(error));
|
|
1199
1249
|
}
|
|
1200
1250
|
}
|
|
1251
|
+
if (this.#disposed || generation !== this.#asyncGeneration) return;
|
|
1201
1252
|
|
|
1202
|
-
|
|
1253
|
+
this.#clearHealthCheckTimers();
|
|
1203
1254
|
if (healthPassed) {
|
|
1204
1255
|
healthText.setText(theme.fg("success", "✓ Health check passed"));
|
|
1205
1256
|
} else {
|
|
@@ -1210,7 +1261,7 @@ export class MCPAddWizard extends Container {
|
|
|
1210
1261
|
this.#requestRender();
|
|
1211
1262
|
|
|
1212
1263
|
// Move to scope selection after short delay
|
|
1213
|
-
|
|
1264
|
+
this.#scheduleTransition(
|
|
1214
1265
|
() => {
|
|
1215
1266
|
this.#currentStep = "scope";
|
|
1216
1267
|
this.#selectedIndex = 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TextContent } from "@gajae-code/ai";
|
|
2
2
|
import type { Component } from "@gajae-code/tui";
|
|
3
|
-
import { Box, Container, Markdown, Spacer, Text } from "@gajae-code/tui";
|
|
3
|
+
import { Box, Container, Markdown, Spacer, Text, truncateToWidth } from "@gajae-code/tui";
|
|
4
4
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
|
|
6
6
|
|
|
@@ -39,29 +39,37 @@ export class SkillMessageComponent extends Container {
|
|
|
39
39
|
this.addChild(this.#box);
|
|
40
40
|
this.#box.clear();
|
|
41
41
|
|
|
42
|
-
const label = theme.fg("customMessageLabel", theme.bold("[skill]"));
|
|
43
|
-
this.#box.addChild(new Text(label, 0, 0));
|
|
44
|
-
this.#box.addChild(new Spacer(1));
|
|
45
|
-
|
|
46
42
|
const details = this.message.details;
|
|
43
|
+
const name = details?.name ?? "unknown";
|
|
47
44
|
const args = details?.args?.trim();
|
|
48
|
-
const infoLines = [
|
|
49
|
-
`Skill: ${details?.name ?? "unknown"}`,
|
|
50
|
-
args ? `Args: ${args}` : undefined,
|
|
51
|
-
details?.path ? `Path: ${details.path}` : undefined,
|
|
52
|
-
typeof details?.lineCount === "number" ? `Prompt: ${details.lineCount} lines` : undefined,
|
|
53
|
-
].filter((line): line is string => Boolean(line));
|
|
54
45
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
46
|
+
// Single compact line: `[skill] <name>: <args>`. The summary is the
|
|
47
|
+
// args the user typed; with none, just `[skill] <name>`. Collapsed to
|
|
48
|
+
// one line — path / line-count / full prompt body are debugging detail
|
|
49
|
+
// and only render once expanded.
|
|
50
|
+
const summary = args ? truncateToWidth(args.replace(/\s+/g, " "), 72) : undefined;
|
|
51
|
+
const header = `${theme.fg("customMessageLabel", theme.bold("[skill]"))} ${theme.fg("customMessageText", name)}`;
|
|
52
|
+
const headerText = summary ? `${header}${theme.fg("customMessageText", `: ${summary}`)}` : header;
|
|
53
|
+
this.#box.addChild(new Text(headerText, 0, 0));
|
|
60
54
|
|
|
61
55
|
if (!this.#expanded) {
|
|
62
56
|
return;
|
|
63
57
|
}
|
|
64
58
|
|
|
59
|
+
const detailLines = [
|
|
60
|
+
details?.path ? `Path: ${details.path}` : undefined,
|
|
61
|
+
typeof details?.lineCount === "number" ? `Prompt: ${details.lineCount} lines` : undefined,
|
|
62
|
+
].filter((line): line is string => Boolean(line));
|
|
63
|
+
|
|
64
|
+
if (detailLines.length > 0) {
|
|
65
|
+
this.#box.addChild(new Spacer(1));
|
|
66
|
+
this.#box.addChild(
|
|
67
|
+
new Markdown(detailLines.join("\n"), 0, 0, getMarkdownTheme(), {
|
|
68
|
+
color: (value: string) => theme.fg("customMessageText", value),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
const text = this.#extractText();
|
|
66
74
|
if (!text) {
|
|
67
75
|
return;
|