@junctionpanel/server 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/server/client/daemon-client.d.ts +1 -0
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +3 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/activity-curator.d.ts.map +1 -1
  6. package/dist/server/server/agent/activity-curator.js +4 -0
  7. package/dist/server/server/agent/activity-curator.js.map +1 -1
  8. package/dist/server/server/agent/agent-manager.d.ts +3 -1
  9. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-manager.js +8 -1
  11. package/dist/server/server/agent/agent-manager.js.map +1 -1
  12. package/dist/server/server/agent/agent-sdk-types.d.ts +14 -1
  13. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  14. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +24 -0
  15. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  16. package/dist/server/server/session.d.ts.map +1 -1
  17. package/dist/server/server/session.js +36 -17
  18. package/dist/server/server/session.js.map +1 -1
  19. package/dist/server/server/worktree-bootstrap.d.ts +6 -0
  20. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  21. package/dist/server/server/worktree-bootstrap.js +428 -30
  22. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  23. package/dist/server/shared/bootstrap-setup.d.ts +18 -0
  24. package/dist/server/shared/bootstrap-setup.d.ts.map +1 -0
  25. package/dist/server/shared/bootstrap-setup.js +116 -0
  26. package/dist/server/shared/bootstrap-setup.js.map +1 -0
  27. package/dist/server/shared/messages.d.ts +209 -192
  28. package/dist/server/shared/messages.d.ts.map +1 -1
  29. package/dist/server/shared/messages.js +19 -0
  30. package/dist/server/shared/messages.js.map +1 -1
  31. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  32. package/dist/server/shared/tool-call-display.js +4 -0
  33. package/dist/server/shared/tool-call-display.js.map +1 -1
  34. package/dist/server/terminal/terminal-manager.d.ts +2 -0
  35. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  36. package/dist/server/terminal/terminal-manager.js +8 -1
  37. package/dist/server/terminal/terminal-manager.js.map +1 -1
  38. package/dist/server/terminal/terminal.d.ts +1 -0
  39. package/dist/server/terminal/terminal.d.ts.map +1 -1
  40. package/dist/server/terminal/terminal.js +2 -2
  41. package/dist/server/terminal/terminal.js.map +1 -1
  42. package/dist/server/utils/worktree.d.ts +12 -0
  43. package/dist/server/utils/worktree.d.ts.map +1 -1
  44. package/dist/server/utils/worktree.js +38 -1
  45. package/dist/server/utils/worktree.js.map +1 -1
  46. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import type { Logger } from "pino";
2
+ import type { BootstrapSetupOverride } from "../shared/bootstrap-setup.js";
2
3
  import type { TerminalManager } from "../terminal/terminal-manager.js";
3
4
  import { type WorktreeConfig } from "../utils/worktree.js";
4
5
  import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
@@ -12,9 +13,14 @@ export interface WorktreeBootstrapTerminalResult {
12
13
  export interface RunAsyncWorktreeBootstrapOptions {
13
14
  agentId: string;
14
15
  worktree: WorktreeConfig;
16
+ setupOverride?: BootstrapSetupOverride | null;
15
17
  terminalManager: TerminalManager | null;
16
18
  appendTimelineItem: (item: AgentTimelineItem) => Promise<boolean>;
17
19
  emitLiveTimelineItem?: (item: AgentTimelineItem) => Promise<boolean>;
20
+ onSetupSettled?: (result: {
21
+ setupStatus: "not_required" | "completed" | "failed";
22
+ errorMessage: string | null;
23
+ }) => Promise<void> | void;
18
24
  logger?: Logger;
19
25
  }
20
26
  export interface CreateAgentWorktreeOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/server/worktree-bootstrap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE,OAAO,EAML,KAAK,cAAc,EAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,cAAc,CAAC;IACzB,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,kBAAkB,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiHD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CASzB;AA+SD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,IAAI,CAAC,CAgIf"}
1
+ {"version":3,"file":"worktree-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/server/worktree-bootstrap.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE,OAAO,EAOL,KAAK,cAAc,EAKpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC9C,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,kBAAkB,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE;QACxB,WAAW,EAAE,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC;QACrD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;KAC7B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiJD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CASzB;AA0qBD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,IAAI,CAAC,CAqOf"}
@@ -1,8 +1,37 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { basename, join } from "node:path";
1
3
  import { v4 as uuidv4 } from "uuid";
2
- import { createWorktree, getWorktreeTerminalSpecs, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
4
+ import { createWorktree, getWorktreeTerminalSpecs, resolveWorktreeBootstrapSetup, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
3
5
  const MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES = 64 * 1024;
4
6
  const WORKTREE_SETUP_TRUNCATION_MARKER = "\n...<output truncated in the middle>...\n";
5
7
  const WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS = 1500;
8
+ const SETUP_TERMINAL_NAME = "Setup";
9
+ const SETUP_SENTINEL_PREFIX = "__JUNCTION_SETUP_DONE__";
10
+ const SETUP_COMMAND_START_SENTINEL_PREFIX = "__JUNCTION_SETUP_COMMAND_START__";
11
+ const SETUP_COMMAND_DONE_SENTINEL_PREFIX = "__JUNCTION_SETUP_COMMAND_DONE__";
12
+ const ANSI_ESCAPE_REGEX = /\u001B[\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
13
+ function countCopiedFiles(rootPath) {
14
+ let total = 0;
15
+ const stack = [rootPath];
16
+ while (stack.length > 0) {
17
+ const currentPath = stack.pop();
18
+ if (!currentPath) {
19
+ continue;
20
+ }
21
+ for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
22
+ if (entry.name === ".git") {
23
+ continue;
24
+ }
25
+ const nextPath = join(currentPath, entry.name);
26
+ if (entry.isDirectory()) {
27
+ stack.push(nextPath);
28
+ continue;
29
+ }
30
+ total += 1;
31
+ }
32
+ }
33
+ return total;
34
+ }
6
35
  function byteLength(text) {
7
36
  return Buffer.byteLength(text, "utf8");
8
37
  }
@@ -96,6 +125,44 @@ export async function createAgentWorktree(options) {
96
125
  function formatDurationMs(durationMs) {
97
126
  return `${(durationMs / 1000).toFixed(2)}s`;
98
127
  }
128
+ function escapeRegex(value) {
129
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
130
+ }
131
+ function quoteForShell(value) {
132
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
133
+ }
134
+ function resolveSetupTerminalShells() {
135
+ const interactiveShell = process.env.SHELL || "/bin/sh";
136
+ const interactiveShellName = basename(interactiveShell).toLowerCase();
137
+ const supportsDashLc = interactiveShellName === "sh" || interactiveShellName === "bash" || interactiveShellName === "zsh";
138
+ return {
139
+ runnerShell: supportsDashLc ? interactiveShell : "/bin/sh",
140
+ interactiveShell,
141
+ };
142
+ }
143
+ function normalizeTerminalControlText(value) {
144
+ if (!value) {
145
+ return "";
146
+ }
147
+ const stripped = value.replace(ANSI_ESCAPE_REGEX, "");
148
+ let normalized = "";
149
+ for (const char of stripped) {
150
+ if (char === "\b") {
151
+ normalized = normalized.slice(0, -1);
152
+ continue;
153
+ }
154
+ if (char === "\r") {
155
+ normalized += "\n";
156
+ continue;
157
+ }
158
+ const codePoint = char.codePointAt(0) ?? 0;
159
+ if (codePoint < 0x20 && char !== "\n" && char !== "\t") {
160
+ continue;
161
+ }
162
+ normalized += char;
163
+ }
164
+ return normalized;
165
+ }
99
166
  function commandStatusFromResult(result) {
100
167
  if (result.exitCode === null) {
101
168
  return "running";
@@ -184,6 +251,45 @@ function buildSetupTimelineItem(input) {
184
251
  error: { message: input.errorMessage ?? "Worktree setup failed" },
185
252
  };
186
253
  }
254
+ function buildWorkspaceBootstrapTimelineItem(input) {
255
+ const detail = {
256
+ type: "workspace_bootstrap",
257
+ workspaceName: input.worktree.workspaceName,
258
+ baseBranch: input.worktree.baseBranch,
259
+ branchName: input.worktree.branchName,
260
+ worktreePath: input.worktree.worktreePath,
261
+ copiedFilesCount: input.copiedFilesCount,
262
+ setupStatus: input.setupStatus,
263
+ };
264
+ if (input.status === "running") {
265
+ return {
266
+ type: "tool_call",
267
+ name: "junction_workspace_bootstrap",
268
+ callId: input.callId,
269
+ status: "running",
270
+ detail,
271
+ error: null,
272
+ };
273
+ }
274
+ if (input.status === "completed") {
275
+ return {
276
+ type: "tool_call",
277
+ name: "junction_workspace_bootstrap",
278
+ callId: input.callId,
279
+ status: "completed",
280
+ detail,
281
+ error: null,
282
+ };
283
+ }
284
+ return {
285
+ type: "tool_call",
286
+ name: "junction_workspace_bootstrap",
287
+ callId: input.callId,
288
+ status: "failed",
289
+ detail,
290
+ error: { message: input.errorMessage ?? "Workspace bootstrap failed" },
291
+ };
292
+ }
187
293
  function buildTerminalTimelineItem(input) {
188
294
  const detailInput = {
189
295
  worktreePath: input.worktree.worktreePath,
@@ -268,6 +374,225 @@ async function waitForTerminalBootstrapReadiness(terminal) {
268
374
  timeout = setTimeout(finish, WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS);
269
375
  });
270
376
  }
377
+ function buildSetupTerminalBootstrapCommand(input) {
378
+ const lines = [
379
+ `__junction_setup_exec_shell=${quoteForShell(input.runnerShell)}`,
380
+ `__junction_setup_interactive_shell=${quoteForShell(input.interactiveShell)}`,
381
+ "__junction_setup_status=0",
382
+ "__junction_setup_run_command() {",
383
+ ' __junction_setup_index="$1"',
384
+ ' __junction_setup_command="$2"',
385
+ ` printf '\\n${SETUP_COMMAND_START_SENTINEL_PREFIX}:${input.token}:%s\\n' "$__junction_setup_index"`,
386
+ ' "$__junction_setup_exec_shell" -lc "$__junction_setup_command"',
387
+ " __junction_setup_command_status=$?",
388
+ ` printf '\\n${SETUP_COMMAND_DONE_SENTINEL_PREFIX}:${input.token}:%s:%s\\n' "$__junction_setup_index" "$__junction_setup_command_status"`,
389
+ ' return "$__junction_setup_command_status"',
390
+ "}",
391
+ ];
392
+ for (const [index, command] of input.commands.entries()) {
393
+ lines.push('if [ "$__junction_setup_status" -eq 0 ]; then');
394
+ lines.push(` __junction_setup_run_command ${index + 1} ${quoteForShell(command)} || __junction_setup_status=$?`);
395
+ lines.push("fi");
396
+ }
397
+ lines.push(`printf '\\n${SETUP_SENTINEL_PREFIX}:${input.token}:%s\\n' "$__junction_setup_status"`);
398
+ lines.push('exec "$__junction_setup_interactive_shell" -i || exec /bin/sh -i');
399
+ lines.push("");
400
+ return lines.join("\n");
401
+ }
402
+ function stripSetupProtocolOutput(output, token) {
403
+ if (!output) {
404
+ return "";
405
+ }
406
+ const protocolPattern = new RegExp([
407
+ escapeRegex(SETUP_COMMAND_START_SENTINEL_PREFIX),
408
+ escapeRegex(SETUP_COMMAND_DONE_SENTINEL_PREFIX),
409
+ escapeRegex(SETUP_SENTINEL_PREFIX),
410
+ ]
411
+ .map((prefix) => `\\r?\\n?${prefix}:${escapeRegex(token)}(?::\\d+){1,2}\\r?\\n?`)
412
+ .join("|"), "g");
413
+ return output.replace(protocolPattern, "\n");
414
+ }
415
+ async function runSetupCommandsInTerminal(options) {
416
+ const token = uuidv4();
417
+ const { runnerShell, interactiveShell } = resolveSetupTerminalShells();
418
+ // Ensure Terminal 1 is provisioned before Setup so both tabs always exist together.
419
+ await options.terminalManager.getTerminals(options.worktree.worktreePath);
420
+ const terminal = await options.terminalManager.createTerminal({
421
+ cwd: options.worktree.worktreePath,
422
+ name: SETUP_TERMINAL_NAME,
423
+ env: options.runtimeEnv,
424
+ shell: runnerShell,
425
+ shellArgs: [
426
+ "-lc",
427
+ buildSetupTerminalBootstrapCommand({
428
+ commands: options.setup.commands,
429
+ token,
430
+ runnerShell,
431
+ interactiveShell,
432
+ }),
433
+ ],
434
+ });
435
+ let terminalClosedError = null;
436
+ const commandOutputByIndex = new Map();
437
+ const commandStartedAtByIndex = new Map();
438
+ const resultsByIndex = new Map();
439
+ const startPattern = new RegExp(`^${escapeRegex(SETUP_COMMAND_START_SENTINEL_PREFIX)}:${escapeRegex(token)}:(\\d+)$`);
440
+ const donePattern = new RegExp(`^${escapeRegex(SETUP_COMMAND_DONE_SENTINEL_PREFIX)}:${escapeRegex(token)}:(\\d+):(\\d+)$`);
441
+ const setupDonePattern = new RegExp(`^${escapeRegex(SETUP_SENTINEL_PREFIX)}:${escapeRegex(token)}:(\\d+)$`);
442
+ let activeCommandIndex = null;
443
+ let normalizedBuffer = "";
444
+ const unsubscribeExit = terminal.onExit(() => {
445
+ terminalClosedError = "Setup terminal exited before the setup script completed.";
446
+ });
447
+ const buildOrderedResults = () => Array.from(resultsByIndex.entries())
448
+ .sort((a, b) => a[0] - b[0])
449
+ .map(([, result]) => result);
450
+ try {
451
+ await new Promise((resolve, reject) => {
452
+ let settled = false;
453
+ let rawSubscription = null;
454
+ let exitPoll = null;
455
+ const cleanup = () => {
456
+ if (exitPoll) {
457
+ clearInterval(exitPoll);
458
+ exitPoll = null;
459
+ }
460
+ rawSubscription?.unsubscribe();
461
+ rawSubscription = null;
462
+ };
463
+ const settle = (callback) => {
464
+ if (settled) {
465
+ return;
466
+ }
467
+ settled = true;
468
+ cleanup();
469
+ callback();
470
+ };
471
+ const handleError = (error) => {
472
+ settle(() => reject(error));
473
+ };
474
+ const processLine = (line) => {
475
+ const startMatch = startPattern.exec(line);
476
+ if (startMatch) {
477
+ const commandIndex = Number.parseInt(startMatch[1] ?? "", 10);
478
+ if (!Number.isFinite(commandIndex) || commandIndex < 1 || commandIndex > options.setup.commands.length) {
479
+ return;
480
+ }
481
+ activeCommandIndex = commandIndex;
482
+ commandStartedAtByIndex.set(commandIndex, Date.now());
483
+ commandOutputByIndex.set(commandIndex, "");
484
+ options.onEvent?.({
485
+ type: "command_started",
486
+ index: commandIndex,
487
+ total: options.setup.commands.length,
488
+ command: options.setup.commands[commandIndex - 1] ?? "",
489
+ cwd: options.worktree.worktreePath,
490
+ });
491
+ return;
492
+ }
493
+ const doneMatch = donePattern.exec(line);
494
+ if (doneMatch) {
495
+ const commandIndex = Number.parseInt(doneMatch[1] ?? "", 10);
496
+ const exitCode = Number.parseInt(doneMatch[2] ?? "", 10);
497
+ if (!Number.isFinite(commandIndex) || commandIndex < 1 || commandIndex > options.setup.commands.length) {
498
+ return;
499
+ }
500
+ const stdout = stripSetupProtocolOutput(commandOutputByIndex.get(commandIndex) ?? "", token);
501
+ const result = {
502
+ command: options.setup.commands[commandIndex - 1] ?? "",
503
+ cwd: options.worktree.worktreePath,
504
+ stdout,
505
+ stderr: "",
506
+ exitCode: Number.isFinite(exitCode) ? exitCode : null,
507
+ durationMs: Math.max(0, Date.now() - (commandStartedAtByIndex.get(commandIndex) ?? Date.now())),
508
+ };
509
+ resultsByIndex.set(commandIndex, result);
510
+ if (activeCommandIndex === commandIndex) {
511
+ activeCommandIndex = null;
512
+ }
513
+ options.onEvent?.({
514
+ type: "command_completed",
515
+ index: commandIndex,
516
+ total: options.setup.commands.length,
517
+ command: result.command,
518
+ cwd: result.cwd,
519
+ exitCode: result.exitCode,
520
+ durationMs: result.durationMs,
521
+ stdout: result.stdout,
522
+ stderr: result.stderr,
523
+ });
524
+ if (result.exitCode !== 0) {
525
+ handleError(new WorktreeSetupError(`Worktree setup command failed: ${result.command}`.trim(), buildOrderedResults()));
526
+ }
527
+ return;
528
+ }
529
+ const setupDoneMatch = setupDonePattern.exec(line);
530
+ if (setupDoneMatch) {
531
+ const exitCode = Number.parseInt(setupDoneMatch[1] ?? "", 10);
532
+ if (!Number.isFinite(exitCode) || exitCode !== 0) {
533
+ handleError(new WorktreeSetupError("Worktree setup failed", buildOrderedResults()));
534
+ return;
535
+ }
536
+ if (resultsByIndex.size !== options.setup.commands.length) {
537
+ handleError(new Error("Setup terminal completed without recording all setup commands."));
538
+ return;
539
+ }
540
+ settle(resolve);
541
+ return;
542
+ }
543
+ if (activeCommandIndex === null) {
544
+ return;
545
+ }
546
+ const outputChunk = `${line}\n`;
547
+ commandOutputByIndex.set(activeCommandIndex, `${commandOutputByIndex.get(activeCommandIndex) ?? ""}${outputChunk}`);
548
+ options.onEvent?.({
549
+ type: "output",
550
+ index: activeCommandIndex,
551
+ total: options.setup.commands.length,
552
+ command: options.setup.commands[activeCommandIndex - 1] ?? "",
553
+ cwd: options.worktree.worktreePath,
554
+ stream: "stdout",
555
+ chunk: outputChunk,
556
+ });
557
+ };
558
+ rawSubscription = terminal.subscribeRaw((chunk) => {
559
+ if (settled) {
560
+ return;
561
+ }
562
+ const normalizedChunk = normalizeTerminalControlText(chunk.data);
563
+ if (!normalizedChunk) {
564
+ return;
565
+ }
566
+ normalizedBuffer = `${normalizedBuffer}${normalizedChunk}`;
567
+ let newlineIndex = normalizedBuffer.indexOf("\n");
568
+ while (newlineIndex !== -1) {
569
+ const line = normalizedBuffer.slice(0, newlineIndex);
570
+ normalizedBuffer = normalizedBuffer.slice(newlineIndex + 1);
571
+ processLine(line);
572
+ newlineIndex = normalizedBuffer.indexOf("\n");
573
+ }
574
+ }, { fromOffset: 0 });
575
+ if (terminalClosedError) {
576
+ handleError(new Error(terminalClosedError));
577
+ return;
578
+ }
579
+ exitPoll = setInterval(() => {
580
+ if (settled) {
581
+ return;
582
+ }
583
+ if (terminalClosedError) {
584
+ handleError(new Error(terminalClosedError));
585
+ }
586
+ }, 25);
587
+ });
588
+ }
589
+ finally {
590
+ unsubscribeExit();
591
+ }
592
+ return Array.from(resultsByIndex.entries())
593
+ .sort((a, b) => a[0] - b[0])
594
+ .map(([, result]) => result);
595
+ }
271
596
  async function runWorktreeTerminalBootstrap(options, runtimeEnv) {
272
597
  const terminalSpecs = getWorktreeTerminalSpecs(options.worktree.worktreePath);
273
598
  if (terminalSpecs.length === 0) {
@@ -336,13 +661,44 @@ async function runWorktreeTerminalBootstrap(options, runtimeEnv) {
336
661
  }));
337
662
  }
338
663
  export async function runAsyncWorktreeBootstrap(options) {
664
+ const bootstrapCallId = uuidv4();
339
665
  const setupCallId = uuidv4();
340
666
  let setupResults = [];
341
667
  let runtimeEnv = null;
668
+ const copiedFilesCount = countCopiedFiles(options.worktree.worktreePath);
669
+ const resolvedSetup = resolveWorktreeBootstrapSetup({
670
+ repoRoot: options.worktree.worktreePath,
671
+ override: options.setupOverride,
672
+ });
673
+ const hasSetupCommands = Boolean(resolvedSetup && resolvedSetup.commands.length > 0);
342
674
  const emitLiveTimelineItem = options.emitLiveTimelineItem;
343
675
  const runningResultsByIndex = new Map();
344
676
  const outputAccumulatorsByIndex = new Map();
345
677
  let liveEmitQueue = Promise.resolve();
678
+ const started = await options.appendTimelineItem(buildWorkspaceBootstrapTimelineItem({
679
+ callId: bootstrapCallId,
680
+ status: "running",
681
+ worktree: options.worktree,
682
+ copiedFilesCount,
683
+ setupStatus: hasSetupCommands ? "running" : "not_required",
684
+ errorMessage: null,
685
+ }));
686
+ if (!started) {
687
+ return;
688
+ }
689
+ if (hasSetupCommands) {
690
+ const setupStarted = await options.appendTimelineItem(buildSetupTimelineItem({
691
+ callId: setupCallId,
692
+ status: "running",
693
+ worktree: options.worktree,
694
+ results: [],
695
+ outputAccumulatorsByIndex,
696
+ errorMessage: null,
697
+ }));
698
+ if (!setupStarted) {
699
+ return;
700
+ }
701
+ }
346
702
  const queueLiveRunningEmit = () => {
347
703
  if (!emitLiveTimelineItem) {
348
704
  return;
@@ -375,12 +731,8 @@ export async function runAsyncWorktreeBootstrap(options) {
375
731
  cwd: options.worktree.worktreePath,
376
732
  env: runtimeEnv,
377
733
  });
378
- setupResults = await runWorktreeSetupCommands({
379
- worktreePath: options.worktree.worktreePath,
380
- branchName: options.worktree.branchName,
381
- cleanupOnFailure: false,
382
- runtimeEnv,
383
- onEvent: (event) => {
734
+ if (resolvedSetup) {
735
+ const handleSetupEvent = (event) => {
384
736
  const existing = runningResultsByIndex.get(event.index);
385
737
  const baseResult = existing ?? {
386
738
  command: event.command,
@@ -390,48 +742,80 @@ export async function runAsyncWorktreeBootstrap(options) {
390
742
  exitCode: null,
391
743
  durationMs: 0,
392
744
  };
393
- if (event.type === "output") {
745
+ if (event.type === "output" && event.chunk) {
394
746
  const outputAccumulator = outputAccumulatorsByIndex.get(event.index) ??
395
747
  createMiddleTruncationAccumulator();
396
748
  appendToMiddleTruncationAccumulator(outputAccumulator, event.chunk);
397
749
  outputAccumulatorsByIndex.set(event.index, outputAccumulator);
398
- runningResultsByIndex.set(event.index, {
399
- ...baseResult,
400
- // Keep the timeline command model lightweight; output is carried in
401
- // outputAccumulatorsByIndex.
402
- stdout: baseResult.stdout,
403
- stderr: baseResult.stderr,
404
- });
750
+ runningResultsByIndex.set(event.index, baseResult);
405
751
  queueLiveRunningEmit();
406
752
  return;
407
753
  }
408
754
  if (event.type === "command_completed") {
755
+ const outputAccumulator = outputAccumulatorsByIndex.get(event.index) ??
756
+ createMiddleTruncationAccumulator();
757
+ appendToMiddleTruncationAccumulator(outputAccumulator, `${event.stdout ?? ""}${event.stderr ?? ""}`);
758
+ outputAccumulatorsByIndex.set(event.index, outputAccumulator);
409
759
  runningResultsByIndex.set(event.index, {
410
760
  ...baseResult,
411
- stdout: event.stdout,
412
- stderr: event.stderr,
413
- exitCode: event.exitCode,
414
- durationMs: event.durationMs,
761
+ stdout: event.stdout ?? "",
762
+ stderr: event.stderr ?? "",
763
+ exitCode: event.exitCode ?? null,
764
+ durationMs: event.durationMs ?? 0,
415
765
  });
416
766
  queueLiveRunningEmit();
417
767
  return;
418
768
  }
419
769
  runningResultsByIndex.set(event.index, baseResult);
420
770
  queueLiveRunningEmit();
421
- },
422
- });
771
+ };
772
+ setupResults =
773
+ options.terminalManager
774
+ ? await runSetupCommandsInTerminal({
775
+ terminalManager: options.terminalManager,
776
+ worktree: options.worktree,
777
+ runtimeEnv,
778
+ setup: resolvedSetup,
779
+ onEvent: handleSetupEvent,
780
+ })
781
+ : await runWorktreeSetupCommands({
782
+ worktreePath: options.worktree.worktreePath,
783
+ branchName: options.worktree.branchName,
784
+ cleanupOnFailure: false,
785
+ commands: resolvedSetup.commands,
786
+ runtimeEnv,
787
+ onEvent: handleSetupEvent,
788
+ });
789
+ }
423
790
  await liveEmitQueue;
424
- const completed = await options.appendTimelineItem(buildSetupTimelineItem({
425
- callId: setupCallId,
791
+ if (hasSetupCommands) {
792
+ const completed = await options.appendTimelineItem(buildSetupTimelineItem({
793
+ callId: setupCallId,
794
+ status: "completed",
795
+ worktree: options.worktree,
796
+ results: setupResults,
797
+ outputAccumulatorsByIndex,
798
+ errorMessage: null,
799
+ }));
800
+ if (!completed) {
801
+ return;
802
+ }
803
+ }
804
+ const bootstrapCompleted = await options.appendTimelineItem(buildWorkspaceBootstrapTimelineItem({
805
+ callId: bootstrapCallId,
426
806
  status: "completed",
427
807
  worktree: options.worktree,
428
- results: setupResults,
429
- outputAccumulatorsByIndex,
808
+ copiedFilesCount,
809
+ setupStatus: hasSetupCommands ? "completed" : "not_required",
430
810
  errorMessage: null,
431
811
  }));
432
- if (!completed) {
812
+ if (!bootstrapCompleted) {
433
813
  return;
434
814
  }
815
+ await options.onSetupSettled?.({
816
+ setupStatus: hasSetupCommands ? "completed" : "not_required",
817
+ errorMessage: null,
818
+ });
435
819
  }
436
820
  catch (error) {
437
821
  if (error instanceof WorktreeSetupError) {
@@ -439,14 +823,28 @@ export async function runAsyncWorktreeBootstrap(options) {
439
823
  }
440
824
  await liveEmitQueue;
441
825
  const message = error instanceof Error ? error.message : String(error);
442
- await options.appendTimelineItem(buildSetupTimelineItem({
443
- callId: setupCallId,
826
+ if (hasSetupCommands) {
827
+ await options.appendTimelineItem(buildSetupTimelineItem({
828
+ callId: setupCallId,
829
+ status: "failed",
830
+ worktree: options.worktree,
831
+ results: setupResults,
832
+ outputAccumulatorsByIndex,
833
+ errorMessage: message,
834
+ }));
835
+ }
836
+ await options.appendTimelineItem(buildWorkspaceBootstrapTimelineItem({
837
+ callId: bootstrapCallId,
444
838
  status: "failed",
445
839
  worktree: options.worktree,
446
- results: setupResults,
447
- outputAccumulatorsByIndex,
840
+ copiedFilesCount,
841
+ setupStatus: "failed",
448
842
  errorMessage: message,
449
843
  }));
844
+ await options.onSetupSettled?.({
845
+ setupStatus: "failed",
846
+ errorMessage: message,
847
+ });
450
848
  return;
451
849
  }
452
850
  await runWorktreeTerminalBootstrap(options, runtimeEnv);