@gajae-code/coding-agent 0.12.11 → 0.12.12
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 +38 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +24 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +23 -5
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +24 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -9
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +206 -7
- package/src/session/session-manager.ts +148 -38
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/index.ts +8 -0
- package/src/utils/npm-registry.ts +660 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { MCPServerConnection, MCPToolCallResult } from "../../runtime-mcp";
|
|
2
|
+
import { callTool, connectToServer, disconnectServer } from "../../runtime-mcp";
|
|
3
|
+
import { createExactPrefixCommandBridge } from "./prefix-command-bridge";
|
|
4
|
+
import type { ExtensionContext, InputEvent, InputEventResult } from "./types";
|
|
5
|
+
|
|
6
|
+
const OUROBOROS_CLI_ENV = "OUROBOROS_CLI";
|
|
7
|
+
const INTERVIEW_COMMAND = "ooo interview";
|
|
8
|
+
const INTERVIEW_TOOL = "ouroboros_interview";
|
|
9
|
+
const INTERVIEW_SESSION_PATTERN = /^interview_[A-Za-z0-9_-]+$/;
|
|
10
|
+
|
|
11
|
+
interface OuroborosOooBridgeOptions {
|
|
12
|
+
connect?: typeof connectToServer;
|
|
13
|
+
callTool?: typeof callTool;
|
|
14
|
+
disconnect?: typeof disconnectServer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface InterviewState {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface OuroborosOooBridgeHandler {
|
|
22
|
+
(event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult>;
|
|
23
|
+
reset(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveOuroborosCommand(): string {
|
|
27
|
+
return process.env[OUROBOROS_CLI_ENV]?.trim() || "ouroboros";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function interviewArgument(text: string): string | undefined {
|
|
31
|
+
if (text === INTERVIEW_COMMAND) return "";
|
|
32
|
+
if (text.startsWith(`${INTERVIEW_COMMAND} `) || text.startsWith(`${INTERVIEW_COMMAND}\t`)) {
|
|
33
|
+
return text.slice(INTERVIEW_COMMAND.length).trim();
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isOooCommand(text: string): boolean {
|
|
39
|
+
return text === "ooo" || text.startsWith("ooo ") || text.startsWith("ooo\t");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isBuiltInControlInput(text: string): boolean {
|
|
43
|
+
return text === "." || text === "c" || text.startsWith("/");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resetsInterviewState(text: string): boolean {
|
|
47
|
+
return /^\/(?:clear|drop|exit|new|quit)(?:\s|$)/.test(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resultText(result: MCPToolCallResult): string {
|
|
51
|
+
return result.content
|
|
52
|
+
.filter(content => content.type === "text")
|
|
53
|
+
.map(content => content.text)
|
|
54
|
+
.join("\n\n")
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resultMeta(result: MCPToolCallResult): Record<string, unknown> {
|
|
59
|
+
return result._meta ?? {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resultSessionId(result: MCPToolCallResult, text: string): string | undefined {
|
|
63
|
+
const metadataSessionId = resultMeta(result).session_id;
|
|
64
|
+
if (typeof metadataSessionId === "string" && INTERVIEW_SESSION_PATTERN.test(metadataSessionId)) {
|
|
65
|
+
return metadataSessionId;
|
|
66
|
+
}
|
|
67
|
+
const textSessionId = /\bSession(?: ID)?:\s*(interview_[A-Za-z0-9_-]+)/.exec(text)?.[1];
|
|
68
|
+
return textSessionId && INTERVIEW_SESSION_PATTERN.test(textSessionId) ? textSessionId : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resultCompleted(result: MCPToolCallResult): boolean {
|
|
72
|
+
const meta = resultMeta(result);
|
|
73
|
+
return meta.completed === true || meta.phase === "complete";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createOuroborosOooBridge(options: OuroborosOooBridgeOptions = {}): OuroborosOooBridgeHandler {
|
|
77
|
+
const connect = options.connect ?? connectToServer;
|
|
78
|
+
const invoke = options.callTool ?? callTool;
|
|
79
|
+
const disconnect = options.disconnect ?? disconnectServer;
|
|
80
|
+
let interview: InterviewState | undefined;
|
|
81
|
+
let interviewCaptureActive = false;
|
|
82
|
+
let activeConnection: MCPServerConnection | undefined;
|
|
83
|
+
let pendingConnection: Promise<MCPServerConnection> | undefined;
|
|
84
|
+
let activeOperationAbort: AbortController | undefined;
|
|
85
|
+
let lifecycleGeneration = 0;
|
|
86
|
+
let operationTail: Promise<void> = Promise.resolve();
|
|
87
|
+
|
|
88
|
+
const commandBridge = createExactPrefixCommandBridge({
|
|
89
|
+
prefix: "ooo",
|
|
90
|
+
command: resolveOuroborosCommand(),
|
|
91
|
+
args: ["dispatch", "--runtime", "gjc"],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function assertCurrent(generation: number, signal: AbortSignal | undefined): void {
|
|
95
|
+
if (generation !== lifecycleGeneration || signal?.aborted) {
|
|
96
|
+
throw signal?.reason instanceof Error ? signal.reason : new Error("Ouroboros interview operation cancelled");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function disconnectSafely(connection: MCPServerConnection | undefined): Promise<void> {
|
|
101
|
+
if (!connection) return;
|
|
102
|
+
try {
|
|
103
|
+
await disconnect(connection);
|
|
104
|
+
} catch {
|
|
105
|
+
// State is already fenced. A dead transport must not keep ordinary input captured.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function resetInterview(): Promise<void> {
|
|
110
|
+
lifecycleGeneration++;
|
|
111
|
+
interviewCaptureActive = false;
|
|
112
|
+
const operationAbort = activeOperationAbort;
|
|
113
|
+
activeOperationAbort = undefined;
|
|
114
|
+
operationAbort?.abort(new Error("Ouroboros interview reset"));
|
|
115
|
+
const connectionToClose = activeConnection;
|
|
116
|
+
interview = undefined;
|
|
117
|
+
activeConnection = undefined;
|
|
118
|
+
pendingConnection = undefined;
|
|
119
|
+
await disconnectSafely(connectionToClose);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function connection(ctx: ExtensionContext, generation: number): Promise<MCPServerConnection> {
|
|
123
|
+
assertCurrent(generation, ctx.signal);
|
|
124
|
+
if (activeConnection) return activeConnection;
|
|
125
|
+
const pending =
|
|
126
|
+
pendingConnection ??
|
|
127
|
+
connect(
|
|
128
|
+
"ouroboros-ooo-bridge",
|
|
129
|
+
{
|
|
130
|
+
type: "stdio",
|
|
131
|
+
command: resolveOuroborosCommand(),
|
|
132
|
+
args: ["mcp", "serve", "--runtime", "gjc"],
|
|
133
|
+
cwd: ctx.cwd,
|
|
134
|
+
},
|
|
135
|
+
{ signal: ctx.signal },
|
|
136
|
+
);
|
|
137
|
+
pendingConnection = pending;
|
|
138
|
+
try {
|
|
139
|
+
const connected = await pending;
|
|
140
|
+
try {
|
|
141
|
+
assertCurrent(generation, ctx.signal);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
await disconnectSafely(connected);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
activeConnection = connected;
|
|
147
|
+
return connected;
|
|
148
|
+
} finally {
|
|
149
|
+
if (pendingConnection === pending) pendingConnection = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runInterview(text: string, ctx: ExtensionContext): Promise<InputEventResult> {
|
|
154
|
+
const operationAbort = new AbortController();
|
|
155
|
+
activeOperationAbort = operationAbort;
|
|
156
|
+
const operationSignal = ctx.signal ? AbortSignal.any([ctx.signal, operationAbort.signal]) : operationAbort.signal;
|
|
157
|
+
const operationContext: ExtensionContext = { ...ctx, signal: operationSignal };
|
|
158
|
+
const generation = lifecycleGeneration;
|
|
159
|
+
const abortHandler = () => {
|
|
160
|
+
void resetInterview();
|
|
161
|
+
};
|
|
162
|
+
ctx.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
163
|
+
try {
|
|
164
|
+
const interviewConnection = await connection(operationContext, generation);
|
|
165
|
+
const commandArgument = interviewArgument(text);
|
|
166
|
+
const args: Record<string, unknown> = { cwd: ctx.cwd };
|
|
167
|
+
if (interview) {
|
|
168
|
+
args.session_id = interview.sessionId;
|
|
169
|
+
const answer = commandArgument === undefined ? text.trim() : commandArgument;
|
|
170
|
+
if (answer) args.answer = answer;
|
|
171
|
+
} else {
|
|
172
|
+
args.initial_context = commandArgument ?? "";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = await invoke(interviewConnection, INTERVIEW_TOOL, args, { signal: operationSignal });
|
|
176
|
+
assertCurrent(generation, operationSignal);
|
|
177
|
+
const output = resultText(result);
|
|
178
|
+
if (result.isError) throw new Error(output || "Ouroboros interview failed");
|
|
179
|
+
|
|
180
|
+
const sessionId = resultSessionId(result, output);
|
|
181
|
+
if (!resultCompleted(result)) {
|
|
182
|
+
if (!sessionId) throw new Error("Ouroboros interview response did not include a session ID");
|
|
183
|
+
interview = { sessionId };
|
|
184
|
+
} else {
|
|
185
|
+
await resetInterview();
|
|
186
|
+
}
|
|
187
|
+
return output ? { handled: true, text: output } : { handled: true };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
await resetInterview();
|
|
190
|
+
if (!ctx.signal?.aborted) {
|
|
191
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
192
|
+
ctx.ui?.notify(message, "error");
|
|
193
|
+
}
|
|
194
|
+
return { handled: true };
|
|
195
|
+
} finally {
|
|
196
|
+
ctx.signal?.removeEventListener("abort", abortHandler);
|
|
197
|
+
if (activeOperationAbort === operationAbort) activeOperationAbort = undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function enqueueInterview(
|
|
202
|
+
text: string,
|
|
203
|
+
ctx: ExtensionContext,
|
|
204
|
+
explicitInterview: boolean,
|
|
205
|
+
): Promise<InputEventResult> {
|
|
206
|
+
const submissionGeneration = lifecycleGeneration;
|
|
207
|
+
const operation = operationTail.then(async () => {
|
|
208
|
+
if (submissionGeneration !== lifecycleGeneration) return { handled: true };
|
|
209
|
+
if (!explicitInterview && !interviewCaptureActive && !interview) return { handled: true };
|
|
210
|
+
return runInterview(text, ctx);
|
|
211
|
+
});
|
|
212
|
+
operationTail = operation.then(
|
|
213
|
+
() => undefined,
|
|
214
|
+
() => undefined,
|
|
215
|
+
);
|
|
216
|
+
return operation;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const handler = async (event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult> => {
|
|
220
|
+
if (event.source !== undefined && event.source !== "interactive") return {};
|
|
221
|
+
if (isBuiltInControlInput(event.text)) {
|
|
222
|
+
if (resetsInterviewState(event.text)) await resetInterview();
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
const argument = interviewArgument(event.text);
|
|
226
|
+
const explicitInterview = argument !== undefined;
|
|
227
|
+
if (explicitInterview) interviewCaptureActive = true;
|
|
228
|
+
if (explicitInterview || ((interviewCaptureActive || interview) && !isOooCommand(event.text))) {
|
|
229
|
+
return enqueueInterview(event.text, ctx, explicitInterview);
|
|
230
|
+
}
|
|
231
|
+
return commandBridge(event, ctx);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return Object.assign(handler, { reset: resetInterview });
|
|
235
|
+
}
|
|
@@ -86,7 +86,10 @@ export function createExactPrefixCommandBridge(options: ExactPrefixCommandBridge
|
|
|
86
86
|
process.env[recursionEnv] = nextRecursionDepth(recursionEnv);
|
|
87
87
|
try {
|
|
88
88
|
const result = await dispatch(options.command, [...args, event.text], ctx, { timeout });
|
|
89
|
-
if (result.code === 0)
|
|
89
|
+
if (result.code === 0) {
|
|
90
|
+
const output = result.stdout.trim() || result.stderr.trim();
|
|
91
|
+
return output ? { handled: true, text: output } : { handled: true };
|
|
92
|
+
}
|
|
90
93
|
if (result.code === continueExitCode) return {};
|
|
91
94
|
|
|
92
95
|
const output =
|
|
@@ -118,11 +121,3 @@ export function createExactPrefixCommandBridge(options: ExactPrefixCommandBridge
|
|
|
118
121
|
}
|
|
119
122
|
};
|
|
120
123
|
}
|
|
121
|
-
|
|
122
|
-
export function createOuroborosOooBridge() {
|
|
123
|
-
return createExactPrefixCommandBridge({
|
|
124
|
-
prefix: "ooo",
|
|
125
|
-
command: "ouroboros",
|
|
126
|
-
args: ["dispatch"],
|
|
127
|
-
});
|
|
128
|
-
}
|
|
@@ -79,6 +79,16 @@ export function testSetExtensionHandlerTimeoutMs(timeoutMs: number): void {
|
|
|
79
79
|
const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
|
|
80
80
|
|
|
81
81
|
const MAX_PENDING_CREDENTIAL_DISABLED = 32;
|
|
82
|
+
function createHandlerContext(ctx: ExtensionContext, signal: AbortSignal): ExtensionContext {
|
|
83
|
+
const descriptors = Object.getOwnPropertyDescriptors(ctx);
|
|
84
|
+
descriptors.signal = {
|
|
85
|
+
configurable: true,
|
|
86
|
+
enumerable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: signal,
|
|
89
|
+
};
|
|
90
|
+
return Object.defineProperties({}, descriptors) as ExtensionContext;
|
|
91
|
+
}
|
|
82
92
|
|
|
83
93
|
/**
|
|
84
94
|
* Events handled by the generic emit() method.
|
|
@@ -688,12 +698,14 @@ export class ExtensionRunner {
|
|
|
688
698
|
ext: Extension,
|
|
689
699
|
timeoutMs: number,
|
|
690
700
|
): Promise<TResult | undefined> {
|
|
691
|
-
let timeout:
|
|
701
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
702
|
+
const abortController = new AbortController();
|
|
703
|
+
const handlerContext = createHandlerContext(ctx, abortController.signal);
|
|
692
704
|
try {
|
|
693
705
|
const timeoutPromise = new Promise<typeof EXTENSION_HANDLER_TIMEOUT>(resolve => {
|
|
694
706
|
timeout = setTimeout(() => resolve(EXTENSION_HANDLER_TIMEOUT), timeoutMs);
|
|
695
707
|
});
|
|
696
|
-
const handlerResult = await Promise.race([Promise.resolve(handler(event,
|
|
708
|
+
const handlerResult = await Promise.race([Promise.resolve(handler(event, handlerContext)), timeoutPromise]);
|
|
697
709
|
if (timeout !== undefined) {
|
|
698
710
|
clearTimeout(timeout);
|
|
699
711
|
timeout = undefined;
|
|
@@ -701,6 +713,7 @@ export class ExtensionRunner {
|
|
|
701
713
|
|
|
702
714
|
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
703
715
|
const error = `handler timed out after ${timeoutMs}ms`;
|
|
716
|
+
abortController.abort(new Error(error));
|
|
704
717
|
logger.warn("Extension handler timed out", {
|
|
705
718
|
extensionPath: ext.path,
|
|
706
719
|
event: event.type,
|
|
@@ -352,6 +352,8 @@ export interface ExtensionContext {
|
|
|
352
352
|
hasUI: boolean;
|
|
353
353
|
/** Current working directory */
|
|
354
354
|
cwd: string;
|
|
355
|
+
/** Aborted when the runner stops waiting for this handler, including handler timeout. */
|
|
356
|
+
signal?: AbortSignal;
|
|
355
357
|
/** Session manager (read-only) */
|
|
356
358
|
sessionManager: ReadonlySessionManager;
|
|
357
359
|
/** Session classification supplied by the SDK for extension policy decisions. */
|
|
@@ -652,6 +652,56 @@ function renameTmuxWindow(
|
|
|
652
652
|
spawnSync(tmuxCommand, buildTmuxRenameWindowArgs(title, target), options);
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
interface TmuxWindowIdentity {
|
|
656
|
+
paneId: string;
|
|
657
|
+
windowId: string;
|
|
658
|
+
windowIndex: string;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function parseTmuxWindowIdentity(value: string): TmuxWindowIdentity | null {
|
|
662
|
+
const [paneId, windowId, windowIndex, extra] = value.trim().split("\t");
|
|
663
|
+
if (
|
|
664
|
+
extra !== undefined ||
|
|
665
|
+
paneId === undefined ||
|
|
666
|
+
windowId === undefined ||
|
|
667
|
+
windowIndex === undefined ||
|
|
668
|
+
!/^%\d+$/.test(paneId) ||
|
|
669
|
+
!/^@\d+$/.test(windowId) ||
|
|
670
|
+
!/^\d+$/.test(windowIndex)
|
|
671
|
+
)
|
|
672
|
+
return null;
|
|
673
|
+
return { paneId, windowId, windowIndex };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function quoteTmuxCommandArgument(value: string): string {
|
|
677
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function renameExistingTmuxWindow(
|
|
681
|
+
tmuxCommand: string,
|
|
682
|
+
paneId: string,
|
|
683
|
+
title: string,
|
|
684
|
+
spawnSync: TmuxSpawnSync,
|
|
685
|
+
options: TmuxSpawnOptions,
|
|
686
|
+
): void {
|
|
687
|
+
if (!/^%\d+$/.test(paneId)) return;
|
|
688
|
+
const observed = spawnSync(
|
|
689
|
+
tmuxCommand,
|
|
690
|
+
["display-message", "-p", "-t", paneId, "#{pane_id}\t#{window_id}\t#{window_index}"],
|
|
691
|
+
options,
|
|
692
|
+
);
|
|
693
|
+
if (observed.exitCode !== 0) return;
|
|
694
|
+
const identity = parseTmuxWindowIdentity(observed.stdout ?? "");
|
|
695
|
+
if (!identity || identity.paneId !== paneId) return;
|
|
696
|
+
|
|
697
|
+
// `if-shell -F` evaluates the pane binding and inserts the rename into the
|
|
698
|
+
// same tmux command queue. The nested command targets the immutable window
|
|
699
|
+
// id, so an active-window switch or index reuse cannot redirect the rename.
|
|
700
|
+
const predicate = `#{&&:#{==:#{pane_id},${identity.paneId}},#{&&:#{==:#{window_id},${identity.windowId}},#{==:#{window_index},${identity.windowIndex}}}}`;
|
|
701
|
+
const command = `rename-window -t ${identity.windowId} -- ${quoteTmuxCommandArgument(title)}`;
|
|
702
|
+
spawnSync(tmuxCommand, ["if-shell", "-t", identity.paneId, "-F", predicate, command], options);
|
|
703
|
+
}
|
|
704
|
+
|
|
655
705
|
function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
|
|
656
706
|
const env = context.env ?? process.env;
|
|
657
707
|
if (!env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return;
|
|
@@ -669,11 +719,13 @@ function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
|
|
|
669
719
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
670
720
|
if (!tmuxAvailable) return;
|
|
671
721
|
|
|
722
|
+
const paneId = env.TMUX_PANE?.trim();
|
|
723
|
+
if (!paneId) return;
|
|
672
724
|
const cwd = context.cwd ?? process.cwd();
|
|
673
725
|
const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
|
|
674
726
|
const title = buildGjcTmuxWindowTitle(context.project ?? cwd, branch);
|
|
675
727
|
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
676
|
-
|
|
728
|
+
renameExistingTmuxWindow(tmuxCommand, paneId, title, spawnSync, {
|
|
677
729
|
cwd,
|
|
678
730
|
env,
|
|
679
731
|
stdin: "pipe",
|