@gajae-code/coding-agent 0.7.11 → 0.8.0
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 +33 -0
- package/README.md +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +25 -2
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line.d.ts +8 -0
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +10 -10
- package/src/capability/mcp.ts +2 -0
- package/src/config/mcp-schema.json +5 -1
- package/src/config/settings-schema.ts +18 -2
- package/src/coordinator-mcp/server.ts +6 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/gjc-runtime/session-state-sidecar.ts +167 -24
- package/src/gjc-runtime/team-runtime.ts +56 -51
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/components/agent-dashboard.ts +1 -34
- package/src/modes/components/custom-editor.ts +18 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/model-selector.ts +58 -2
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +13 -0
- package/src/modes/components/status-line.ts +215 -86
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +171 -35
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +151 -30
- package/src/modes/controllers/selector-controller.ts +25 -28
- package/src/modes/interactive-mode.ts +20 -23
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/types.ts +1 -0
- package/src/notifications/index.ts +20 -1
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/prompts/system/system-prompt.md +15 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +21 -1
- package/src/session/session-manager.ts +58 -18
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +15 -6
- package/src/tools/index.ts +8 -1
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/src/discovery/vscode.ts +0 -105
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import * as fsSync from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { AssistantMessage } from "@gajae-code/ai";
|
|
4
|
-
import { logger } from "@gajae-code/utils";
|
|
5
|
+
import { logger, postmortem } from "@gajae-code/utils";
|
|
5
6
|
|
|
6
7
|
export const GJC_COORDINATOR_SESSION_STATE_FILE_ENV = "GJC_COORDINATOR_SESSION_STATE_FILE";
|
|
7
8
|
export const GJC_COORDINATOR_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
|
|
9
|
+
export const GJC_COORDINATOR_SESSION_BRANCH_ENV = "GJC_COORDINATOR_SESSION_BRANCH";
|
|
8
10
|
|
|
9
11
|
export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
|
|
10
12
|
|
|
13
|
+
type FinalResponseSource = "agent_end" | "launch_error";
|
|
14
|
+
const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
|
|
15
|
+
|
|
11
16
|
interface RuntimeStateEvent {
|
|
12
17
|
type: string;
|
|
13
18
|
messages?: unknown[];
|
|
@@ -17,6 +22,7 @@ interface RuntimeStateContext {
|
|
|
17
22
|
sessionId: string;
|
|
18
23
|
cwd: string;
|
|
19
24
|
sessionFile?: string | null;
|
|
25
|
+
branch?: string | null;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
interface RuntimeStateSidecarPayload {
|
|
@@ -26,6 +32,7 @@ interface RuntimeStateSidecarPayload {
|
|
|
26
32
|
ready_for_input?: unknown;
|
|
27
33
|
cwd?: unknown;
|
|
28
34
|
session_file?: unknown;
|
|
35
|
+
final_response?: { source?: unknown };
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
export type TerminalRuntimeStateStatus =
|
|
@@ -103,7 +110,7 @@ function assistantText(assistant: AssistantMessage | undefined): string | null {
|
|
|
103
110
|
function finalResponseForEvent(event: RuntimeStateEvent): {
|
|
104
111
|
text: string | null;
|
|
105
112
|
format: "markdown";
|
|
106
|
-
source:
|
|
113
|
+
source: FinalResponseSource;
|
|
107
114
|
artifact_path: null;
|
|
108
115
|
truncated: false;
|
|
109
116
|
} | null {
|
|
@@ -127,6 +134,118 @@ function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
|
|
|
127
134
|
return null;
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
function readPreviousPayload(stateFile: string): Record<string, unknown> {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(fsSync.readFileSync(stateFile, "utf8")) as Record<string, unknown>;
|
|
140
|
+
} catch {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload): boolean {
|
|
146
|
+
if (previous.state !== "completed" && previous.state !== "errored") return false;
|
|
147
|
+
const source = previous.final_response?.source;
|
|
148
|
+
return source === "agent_end" || source === "launch_error";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function branchForContext(context: RuntimeStateContext): string | null {
|
|
152
|
+
return context.branch ?? (process.env[GJC_COORDINATOR_SESSION_BRANCH_ENV]?.trim() || null);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function basePayload(input: {
|
|
156
|
+
context: RuntimeStateContext;
|
|
157
|
+
previous: Record<string, unknown>;
|
|
158
|
+
state: RuntimeState;
|
|
159
|
+
now: string;
|
|
160
|
+
source: string;
|
|
161
|
+
event: string;
|
|
162
|
+
reason: string | null;
|
|
163
|
+
}): Record<string, unknown> {
|
|
164
|
+
return {
|
|
165
|
+
schema_version: 1,
|
|
166
|
+
session_id: process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || input.context.sessionId,
|
|
167
|
+
state: input.state,
|
|
168
|
+
ready_for_input: input.state === "completed" || input.state === "ready_for_input",
|
|
169
|
+
updated_at: input.now,
|
|
170
|
+
current_turn_id: typeof input.previous.current_turn_id === "string" ? input.previous.current_turn_id : null,
|
|
171
|
+
last_turn_id: typeof input.previous.last_turn_id === "string" ? input.previous.last_turn_id : null,
|
|
172
|
+
live: input.state === "running",
|
|
173
|
+
reason: input.reason,
|
|
174
|
+
source: input.source,
|
|
175
|
+
event: input.event,
|
|
176
|
+
cwd: input.context.cwd,
|
|
177
|
+
workdir: input.context.cwd,
|
|
178
|
+
branch: branchForContext(input.context),
|
|
179
|
+
session_file: input.context.sessionFile ?? null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function publicSafeErrorMessage(message: string): string {
|
|
183
|
+
const normalized = message.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
|
|
184
|
+
if (normalized.length <= MAX_PUBLIC_ERROR_MESSAGE_LENGTH) return normalized;
|
|
185
|
+
return `${normalized.slice(0, MAX_PUBLIC_ERROR_MESSAGE_LENGTH)}…`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function errorMessageForPostmortem(reason: postmortem.Reason): string {
|
|
189
|
+
return publicSafeErrorMessage(`GJC process cleanup ran for ${reason}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function numericProcessExitCode(defaultCode: number | null): number | null {
|
|
193
|
+
return typeof process.exitCode === "number" ? process.exitCode : defaultCode;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function postmortemExitDetails(reason: postmortem.Reason): {
|
|
197
|
+
state: RuntimeState;
|
|
198
|
+
reason: string;
|
|
199
|
+
exitKind: string;
|
|
200
|
+
exitCode: number | null;
|
|
201
|
+
signal: string | null;
|
|
202
|
+
error?: { code: string; message: string; recoverable: true };
|
|
203
|
+
} {
|
|
204
|
+
if (reason === postmortem.Reason.EXIT || reason === postmortem.Reason.MANUAL) {
|
|
205
|
+
const exitCode = numericProcessExitCode(0) ?? 0;
|
|
206
|
+
const state: RuntimeState = exitCode === 0 ? "completed" : "errored";
|
|
207
|
+
return {
|
|
208
|
+
state,
|
|
209
|
+
reason: reason === postmortem.Reason.EXIT ? "process_exit" : "manual_cleanup",
|
|
210
|
+
exitKind: reason,
|
|
211
|
+
exitCode,
|
|
212
|
+
signal: null,
|
|
213
|
+
...(state === "errored"
|
|
214
|
+
? {
|
|
215
|
+
error: {
|
|
216
|
+
code: "process_exit",
|
|
217
|
+
message: publicSafeErrorMessage(`GJC process exited with code ${exitCode}`),
|
|
218
|
+
recoverable: true,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const signalByReason: Partial<Record<postmortem.Reason, string>> = {
|
|
225
|
+
[postmortem.Reason.SIGINT]: "SIGINT",
|
|
226
|
+
[postmortem.Reason.SIGTERM]: "SIGTERM",
|
|
227
|
+
[postmortem.Reason.SIGHUP]: "SIGHUP",
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
state: "errored",
|
|
231
|
+
reason,
|
|
232
|
+
exitKind: reason,
|
|
233
|
+
exitCode: numericProcessExitCode(null),
|
|
234
|
+
signal: signalByReason[reason] ?? null,
|
|
235
|
+
error: { code: reason, message: errorMessageForPostmortem(reason), recoverable: true },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function writeStateFileSync(stateFile: string, payload: Record<string, unknown>): void {
|
|
240
|
+
fsSync.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
241
|
+
fsSync.writeFileSync(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
|
|
245
|
+
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
246
|
+
await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
247
|
+
}
|
|
248
|
+
|
|
130
249
|
export async function persistCoordinatorRuntimeStateFromEvent(
|
|
131
250
|
event: RuntimeStateEvent,
|
|
132
251
|
context: RuntimeStateContext,
|
|
@@ -136,42 +255,66 @@ export async function persistCoordinatorRuntimeStateFromEvent(
|
|
|
136
255
|
const state = stateForEvent(event);
|
|
137
256
|
if (!state) return;
|
|
138
257
|
const now = new Date().toISOString();
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
previous = JSON.parse(await Bun.file(stateFile).text()) as Record<string, unknown>;
|
|
142
|
-
} catch {
|
|
143
|
-
previous = {};
|
|
144
|
-
}
|
|
258
|
+
const previous = readPreviousPayload(stateFile);
|
|
145
259
|
const finalResponse = finalResponseForEvent(event);
|
|
146
260
|
const payload = {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
state,
|
|
150
|
-
ready_for_input: state === "completed" || state === "ready_for_input",
|
|
151
|
-
updated_at: now,
|
|
152
|
-
current_turn_id: typeof previous.current_turn_id === "string" ? previous.current_turn_id : null,
|
|
153
|
-
last_turn_id: typeof previous.last_turn_id === "string" ? previous.last_turn_id : null,
|
|
154
|
-
live: typeof previous.live === "boolean" ? previous.live : null,
|
|
155
|
-
reason: null,
|
|
156
|
-
source: "agent_session_event",
|
|
157
|
-
event: event.type,
|
|
158
|
-
cwd: context.cwd,
|
|
159
|
-
session_file: context.sessionFile ?? null,
|
|
261
|
+
...basePayload({ context, previous, state, now, source: "agent_session_event", event: event.type, reason: null }),
|
|
262
|
+
...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
|
|
160
263
|
...(finalResponse ? { final_response: finalResponse } : {}),
|
|
161
264
|
...(state === "errored"
|
|
162
265
|
? {
|
|
163
266
|
error: {
|
|
164
267
|
code: "agent_error",
|
|
165
|
-
message: lastAssistant(event.messages)?.errorMessage ?? "agent_error",
|
|
268
|
+
message: publicSafeErrorMessage(lastAssistant(event.messages)?.errorMessage ?? "agent_error"),
|
|
166
269
|
recoverable: true,
|
|
167
270
|
},
|
|
168
271
|
}
|
|
169
272
|
: {}),
|
|
170
273
|
};
|
|
171
274
|
try {
|
|
172
|
-
await
|
|
173
|
-
await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
275
|
+
await writeStateFile(stateFile, payload);
|
|
174
276
|
} catch (error) {
|
|
175
277
|
logger.warn("Failed to persist coordinator runtime state", { error: String(error), stateFile });
|
|
176
278
|
}
|
|
177
279
|
}
|
|
280
|
+
|
|
281
|
+
export function persistCoordinatorRuntimeStateFromPostmortem(
|
|
282
|
+
reason: postmortem.Reason,
|
|
283
|
+
context: RuntimeStateContext,
|
|
284
|
+
): void {
|
|
285
|
+
const stateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
286
|
+
if (!stateFile) return;
|
|
287
|
+
const previous = readPreviousPayload(stateFile);
|
|
288
|
+
if (shouldPreserveTerminalPayload(previous as RuntimeStateSidecarPayload)) return;
|
|
289
|
+
const now = new Date().toISOString();
|
|
290
|
+
const details = postmortemExitDetails(reason);
|
|
291
|
+
const payload = {
|
|
292
|
+
...basePayload({
|
|
293
|
+
context,
|
|
294
|
+
previous,
|
|
295
|
+
state: details.state,
|
|
296
|
+
now,
|
|
297
|
+
source: "process_postmortem",
|
|
298
|
+
event: "process_exit",
|
|
299
|
+
reason: details.reason,
|
|
300
|
+
}),
|
|
301
|
+
ended_at: now,
|
|
302
|
+
detected_at: now,
|
|
303
|
+
exit_kind: details.exitKind,
|
|
304
|
+
exit_code: details.exitCode,
|
|
305
|
+
signal: details.signal,
|
|
306
|
+
...(details.error ? { error: details.error } : {}),
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
writeStateFileSync(stateFile, payload);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.warn("Failed to persist coordinator runtime state during postmortem", { error: String(error), stateFile });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function registerCoordinatorRuntimeStateFinalizer(context: RuntimeStateContext): () => void {
|
|
316
|
+
if (!process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim()) return () => {};
|
|
317
|
+
return postmortem.register("coordinator-runtime-state", reason => {
|
|
318
|
+
persistCoordinatorRuntimeStateFromPostmortem(reason, context);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
@@ -1931,16 +1931,14 @@ export function buildWorkerCommand(
|
|
|
1931
1931
|
const workspace = worker.worktree_path
|
|
1932
1932
|
? `Worker worktree: ${worker.worktree_path}.`
|
|
1933
1933
|
: `Worker cwd: ${config.leader.cwd}.`;
|
|
1934
|
-
// The worker prompt body
|
|
1935
|
-
// `tmux send-keys`,
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
// whitespace-only bodies fall back to a one-line placeholder so the
|
|
1943
|
-
// worker never sits idle at an empty prompt.
|
|
1934
|
+
// The worker prompt body must stay single-line because fallback startup on
|
|
1935
|
+
// Windows/psmux dispatches the command through `tmux send-keys`, where
|
|
1936
|
+
// embedded LF characters are treated as Enter keypresses. POSIX tmux starts
|
|
1937
|
+
// workers by passing the command directly to `split-window`, but keeping the
|
|
1938
|
+
// body normalized there too makes fake-tmux logs and shell argv handling
|
|
1939
|
+
// deterministic. Strip a defensive U+FEFF in case a caller managed to inject
|
|
1940
|
+
// a UTF-8 BOM into the task text. Empty / whitespace-only bodies fall back to
|
|
1941
|
+
// a one-line placeholder so the worker never sits idle at an empty prompt.
|
|
1944
1942
|
const normalizePrompt = (raw: string): string =>
|
|
1945
1943
|
raw
|
|
1946
1944
|
.replace(/[\uFEFF\u200B]/g, "")
|
|
@@ -1995,6 +1993,11 @@ export function buildWorkerCommand(
|
|
|
1995
1993
|
}
|
|
1996
1994
|
return `${joined} ${config.worker_command} ${quote(prompt)}`;
|
|
1997
1995
|
}
|
|
1996
|
+
|
|
1997
|
+
function shouldDispatchWorkerWithSendKeys(tmuxCommand: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
1998
|
+
return platform === "win32" || path.basename(tmuxCommand).toLowerCase() === "psmux";
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1998
2001
|
interface GjcTeamInitialLane {
|
|
1999
2002
|
label: string;
|
|
2000
2003
|
title: string;
|
|
@@ -2098,22 +2101,24 @@ async function startTmuxSession(
|
|
|
2098
2101
|
const splitDirection: string = worker.index === 1 ? "-h" : "-v";
|
|
2099
2102
|
const splitTarget: string =
|
|
2100
2103
|
worker.index === 1 ? config.leader.pane_id : (rightStackRootPaneId ?? config.leader.pane_id);
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2104
|
+
const workerCommand = buildWorkerCommand(config, worker);
|
|
2105
|
+
const workerCwd = worker.worktree_path ?? config.leader.cwd;
|
|
2106
|
+
const useSendKeysFallback = shouldDispatchWorkerWithSendKeys(config.tmux_command);
|
|
2107
|
+
const splitArgs = [
|
|
2108
|
+
config.tmux_command,
|
|
2109
|
+
"split-window",
|
|
2110
|
+
splitDirection,
|
|
2111
|
+
"-t",
|
|
2112
|
+
splitTarget,
|
|
2113
|
+
"-d",
|
|
2114
|
+
"-P",
|
|
2115
|
+
"-F",
|
|
2116
|
+
"#{pane_id}",
|
|
2117
|
+
"-c",
|
|
2118
|
+
workerCwd,
|
|
2119
|
+
...(useSendKeysFallback ? [] : [workerCommand]),
|
|
2120
|
+
];
|
|
2121
|
+
const split: Bun.SyncSubprocess<"pipe", "pipe"> = Bun.spawnSync(splitArgs, { stdout: "pipe", stderr: "pipe" });
|
|
2117
2122
|
if (split.exitCode !== 0)
|
|
2118
2123
|
throw new Error(split.stderr.toString().trim() || `tmux_split_failed:${config.tmux_target}:${worker.id}`);
|
|
2119
2124
|
const paneId: string = split.stdout.toString().trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
@@ -2121,31 +2126,31 @@ async function startTmuxSession(
|
|
|
2121
2126
|
rollbackPaneIds.push(paneId);
|
|
2122
2127
|
if (worker.index === 1) rightStackRootPaneId = paneId;
|
|
2123
2128
|
workers.push({ ...worker, pane_id: paneId });
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2129
|
+
if (useSendKeysFallback) {
|
|
2130
|
+
// On psmux/ConPTY (Windows pwsh default-shell) panes, tmux writes the
|
|
2131
|
+
// split-window command argv to the new pane's stdin but pwsh's readline
|
|
2132
|
+
// never receives an Enter, so the worker never starts. Create the pane
|
|
2133
|
+
// without a command, then dispatch the worker command through send-keys +
|
|
2134
|
+
// Enter so it actually executes.
|
|
2135
|
+
// Two-step dispatch because tmux's `-l` (literal mode) flag is global per
|
|
2136
|
+
// send-keys invocation, not per arg: passing `-l <body> Enter` would treat
|
|
2137
|
+
// "Enter" as literal text too. Sending the body in literal mode first and
|
|
2138
|
+
// the Enter keypress second keeps the body verbatim while still submitting
|
|
2139
|
+
// the prompt as a keystroke.
|
|
2140
|
+
Bun.spawnSync([config.tmux_command, "send-keys", "-l", "-t", paneId, workerCommand], {
|
|
2141
|
+
stdout: "ignore",
|
|
2142
|
+
stderr: "ignore",
|
|
2143
|
+
});
|
|
2144
|
+
const sendKeys = Bun.spawnSync([config.tmux_command, "send-keys", "-t", paneId, "Enter"], {
|
|
2145
|
+
stdout: "ignore",
|
|
2146
|
+
stderr: "ignore",
|
|
2147
|
+
});
|
|
2148
|
+
// void-cast the exit code so the linter does not flag an unused expression;
|
|
2149
|
+
// the value is intentionally discarded here because the actual spawn outcome
|
|
2150
|
+
// is recovered by the leader through the worker startup-ack watcher, not via
|
|
2151
|
+
// the spawn exit code.
|
|
2152
|
+
void sendKeys.exitCode;
|
|
2153
|
+
}
|
|
2149
2154
|
}
|
|
2150
2155
|
Bun.spawnSync([config.tmux_command, "select-layout", "-t", config.tmux_target, "main-vertical"], {
|
|
2151
2156
|
stdout: "ignore",
|