whipped 0.6.0 → 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/dist/cli.js CHANGED
@@ -3529,7 +3529,7 @@ function isResumableSessionState(state) {
3529
3529
  function normalizeTag(raw2) {
3530
3530
  return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3531
3531
  }
3532
- var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3532
+ var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
3533
3533
  var init_api_contract = __esm({
3534
3534
  "src/core/api-contract.ts"() {
3535
3535
  "use strict";
@@ -3890,8 +3890,13 @@ Do NOT include:
3890
3890
  runtimeGithubConfigSchema = z.object({
3891
3891
  token: z.string()
3892
3892
  });
3893
+ runtimeWorktreeCopyEntrySchema = z.object({
3894
+ path: z.string().min(1),
3895
+ symlink: z.boolean().default(false)
3896
+ });
3893
3897
  runtimeWorktreeSetupSchema = z.object({
3894
- filesToCopy: z.array(z.string()).default([]),
3898
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
3899
+ filesToCopy: z.array(z.union([z.string().transform((path2) => ({ path: path2, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
3895
3900
  installCommand: z.string().default("")
3896
3901
  });
3897
3902
  runtimeProjectSecretSchema = z.object({
@@ -13557,11 +13562,15 @@ async function runLogs(options) {
13557
13562
  process.exit(1);
13558
13563
  }
13559
13564
  if (options.follow) {
13560
- const child = spawn2("tail", ["-f", "-n", String(options.lines), path2], {
13565
+ const [cmd, args] = process.platform === "win32" ? [
13566
+ "powershell",
13567
+ ["-NoProfile", "-Command", `Get-Content -Path '${path2}' -Tail ${options.lines} -Wait`]
13568
+ ] : ["tail", ["-f", "-n", String(options.lines), path2]];
13569
+ const child = spawn2(cmd, args, {
13561
13570
  stdio: "inherit"
13562
13571
  });
13563
13572
  child.on("error", (err) => {
13564
- console.error(`Failed to spawn tail: ${err.message}`);
13573
+ console.error(`Failed to follow log file: ${err.message}`);
13565
13574
  process.exit(1);
13566
13575
  });
13567
13576
  child.on("exit", (code) => process.exit(code ?? 0));
@@ -13618,7 +13627,7 @@ import { existsSync as existsSync14, readFileSync as readFileSync8 } from "node:
13618
13627
  import { createServer } from "node:http";
13619
13628
  import { join as join22 } from "node:path";
13620
13629
  import { fileURLToPath as fileURLToPath5 } from "node:url";
13621
- import * as nodePty2 from "node-pty";
13630
+ import * as nodePty3 from "node-pty";
13622
13631
 
13623
13632
  // node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs
13624
13633
  var import_stream = __toESM(require_stream(), 1);
@@ -14397,6 +14406,16 @@ var slackNotifier = new SlackNotifier();
14397
14406
  // src/server/runtime-server.ts
14398
14407
  init_runtime_config();
14399
14408
  init_logger();
14409
+
14410
+ // src/core/shell.ts
14411
+ function getShellInvocation(command) {
14412
+ if (process.platform === "win32") {
14413
+ return [process.env.ComSpec ?? "cmd.exe", ["/c", command]];
14414
+ }
14415
+ return [process.env.SHELL ?? "/bin/sh", ["-c", command]];
14416
+ }
14417
+
14418
+ // src/server/runtime-server.ts
14400
14419
  init_task_id();
14401
14420
 
14402
14421
  // src/notifications/sound-player.ts
@@ -14424,7 +14443,11 @@ async function playNotificationSound(event) {
14424
14443
  }
14425
14444
  }
14426
14445
  function playOnHost(file) {
14427
- const [command, args] = process.platform === "darwin" ? ["afplay", [file]] : process.platform === "linux" ? ["paplay", [file]] : [null, null];
14446
+ const [command, args] = process.platform === "darwin" ? ["afplay", [file]] : process.platform === "win32" ? (
14447
+ // PowerShell's SoundPlayer plays the WAV chimes synchronously in the
14448
+ // detached child (it exits once done); no extra binary needed.
14449
+ ["powershell", ["-NoProfile", "-Command", `(New-Object Media.SoundPlayer '${file}').PlaySync()`]]
14450
+ ) : process.platform === "linux" ? ["paplay", [file]] : [null, null];
14428
14451
  if (!command) return;
14429
14452
  const child = spawn4(command, [...args], { stdio: "ignore", detached: true });
14430
14453
  child.on("error", () => {
@@ -18047,7 +18070,7 @@ async function commitIfDirty(worktreePath, message) {
18047
18070
  function attemptMerge(repoPath, effectiveWorktreeId, taskBranch) {
18048
18071
  const worktreePath = join12(WORKTREES_DIR, effectiveWorktreeId);
18049
18072
  if (existsSync5(worktreePath)) {
18050
- rmSync2(worktreePath, { recursive: true, force: true });
18073
+ rmSync2(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
18051
18074
  git(["worktree", "prune"], repoPath);
18052
18075
  }
18053
18076
  const statusResult = git(["status", "--porcelain"], repoPath);
@@ -18415,7 +18438,7 @@ async function removeWorktreeAsync(taskId, repoPath, branchName) {
18415
18438
  const t0 = Date.now();
18416
18439
  logger.info(`[cleanup:${taskId}] starting worktree removal`);
18417
18440
  try {
18418
- await rm2(worktreePath, { recursive: true, force: true });
18441
+ await rm2(worktreePath, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
18419
18442
  logger.info(`[cleanup:${taskId}] rm worktree dir done (${Date.now() - t0}ms)`);
18420
18443
  } catch (err) {
18421
18444
  logger.error({ err }, `[cleanup:${taskId}] rm worktree dir failed:`);
@@ -19149,12 +19172,34 @@ function getAvailableAgents() {
19149
19172
  }
19150
19173
  });
19151
19174
  }
19175
+ var resolvedCommandCache = /* @__PURE__ */ new Map();
19176
+ function resolveCommandPath(command) {
19177
+ if (process.platform !== "win32") return command;
19178
+ const cached2 = resolvedCommandCache.get(command);
19179
+ if (cached2) return cached2;
19180
+ try {
19181
+ const result = spawnSync4("where.exe", [command], {
19182
+ stdio: ["ignore", "pipe", "ignore"],
19183
+ timeout: 5e3,
19184
+ encoding: "utf-8"
19185
+ });
19186
+ if (result.status === 0 && result.stdout) {
19187
+ const first = result.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)[0];
19188
+ if (first) {
19189
+ resolvedCommandCache.set(command, first);
19190
+ return first;
19191
+ }
19192
+ }
19193
+ } catch {
19194
+ }
19195
+ return command;
19196
+ }
19152
19197
  function getAgentCommand(agentId) {
19153
19198
  const agent = AGENT_DEFINITIONS.find((a) => a.id === agentId);
19154
19199
  if (!agent) {
19155
19200
  throw new Error(`Unknown agent: ${agentId}`);
19156
19201
  }
19157
- return agent.command;
19202
+ return resolveCommandPath(agent.command);
19158
19203
  }
19159
19204
  var READONLY_DISALLOWED_TOOLS = ["Edit", "Write", "NotebookEdit", "Bash"];
19160
19205
  function buildAgentArgs(agentId, prompt, ctx = {}) {
@@ -20436,11 +20481,12 @@ init_workspace_state();
20436
20481
  init_workspace_state();
20437
20482
 
20438
20483
  // src/daemon/scheduler.ts
20439
- import { spawn as spawn6 } from "node:child_process";
20440
- import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
20441
- import { unlink as unlink2 } from "node:fs/promises";
20484
+ var import_tree_kill2 = __toESM(require_tree_kill(), 1);
20485
+ import { existsSync as existsSync10 } from "node:fs";
20486
+ import { cp, link, mkdir as mkdir3, stat as stat2, symlink, unlink as unlink2 } from "node:fs/promises";
20442
20487
  import { dirname as dirname6, join as join17, resolve as resolve2 } from "node:path";
20443
20488
  import { fileURLToPath as fileURLToPath4 } from "node:url";
20489
+ import * as nodePty2 from "node-pty";
20444
20490
  init_api_contract();
20445
20491
  init_logger();
20446
20492
 
@@ -20626,7 +20672,7 @@ async function runReviewSlot(slot, card, streamId, options, customPrompt) {
20626
20672
  useIncremental,
20627
20673
  diffRef: priorReviewedSha ?? card.baseRef
20628
20674
  };
20629
- const stat2 = getGitStat(worktreePath, card.baseRef);
20675
+ const stat3 = getGitStat(worktreePath, card.baseRef);
20630
20676
  const fullDiff = getGitFullDiff(worktreePath, useIncremental ? priorReviewedSha : card.baseRef);
20631
20677
  const context = formatPriorComments(card);
20632
20678
  let subtaskCards = [];
@@ -20636,7 +20682,7 @@ async function runReviewSlot(slot, card, streamId, options, customPrompt) {
20636
20682
  const rawSystemPrompt = buildReviewSlotSystemPrompt(
20637
20683
  slot,
20638
20684
  card,
20639
- stat2,
20685
+ stat3,
20640
20686
  fullDiff,
20641
20687
  customPrompt,
20642
20688
  context.text,
@@ -21218,16 +21264,16 @@ ${fullDiff}`;
21218
21264
  return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
21219
21265
  }
21220
21266
  var FOLLOWUP_REVIEW_FOCUS = `**This is a follow-up review.** An earlier version of this work already passed review in a prior round \u2014 only the \`## New Feedback\` / \`## Current Iteration\` items triggered this run. Re-review ONLY: (1) whether those items were addressed in the changes below, and (2) whether the new changes introduced a regression or broke a caller (grep callers of anything touched to confirm). Do NOT re-litigate previously-approved code or raise issues unrelated to this round's feedback.`;
21221
- function renderReviewDiff(stat2, fullDiff, baseRef, scope) {
21267
+ function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
21222
21268
  if (!scope.useIncremental) {
21223
21269
  return `## Changed files
21224
- ${stat2}
21270
+ ${stat3}
21225
21271
 
21226
21272
  ## Diff
21227
21273
  ${formatDiffBlock(fullDiff, baseRef)}`;
21228
21274
  }
21229
21275
  return `## Changed files (full changeset vs \`${baseRef}\`, for context)
21230
- ${stat2}
21276
+ ${stat3}
21231
21277
 
21232
21278
  ## Changes since your last review (review THIS)
21233
21279
  _The diff below is only what changed since you last reviewed this card. The full changeset is summarised above; run \`git diff ${baseRef}...HEAD\` only if you need it to chase a regression._
@@ -21267,11 +21313,11 @@ Revise these values rather than rewriting from scratch, unless they no longer re
21267
21313
  let statSection = "";
21268
21314
  if (worktreePath) {
21269
21315
  if (fullDiff) {
21270
- const stat2 = getGitStat(worktreePath, statBase);
21316
+ const stat3 = getGitStat(worktreePath, statBase);
21271
21317
  statSection = `
21272
21318
 
21273
21319
  ## Current worktree state (vs ${statBase})
21274
- ${stat2}
21320
+ ${stat3}
21275
21321
 
21276
21322
  ## Diff (vs ${statBase})
21277
21323
  ${formatDiffBlock(fullDiff, statBase, "Git diff")}`;
@@ -21282,11 +21328,11 @@ ${formatDiffBlock(fullDiff, statBase, "Git diff")}`;
21282
21328
 
21283
21329
  This is the initial dev run on this card. The worktree is clean and branched from \`${statBase}\` \u2014 there is no prior diff to inspect. Skip \`git diff\` and start implementing.`;
21284
21330
  } else {
21285
- const stat2 = getGitStat(worktreePath, statBase);
21331
+ const stat3 = getGitStat(worktreePath, statBase);
21286
21332
  statSection = `
21287
21333
 
21288
21334
  ## Current worktree state (vs ${statBase})
21289
- ${stat2}`;
21335
+ ${stat3}`;
21290
21336
  }
21291
21337
  }
21292
21338
  const descAttachNote = (card.descriptionAttachments?.length ?? 0) > 0 ? `
@@ -21395,12 +21441,12 @@ ${systemPrompt.trim()}`);
21395
21441
  ${effectiveGitInstructions}`);
21396
21442
  return { text: parts.join("\n\n"), files: context.files };
21397
21443
  }
21398
- function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets = [], systemPrompt, autoCommit = true, subtaskCards = [], browserEnabled = false) {
21444
+ function buildReviewSlotSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets = [], systemPrompt, autoCommit = true, subtaskCards = [], browserEnabled = false) {
21399
21445
  if (slot.type === "orch") {
21400
21446
  return buildOrchSystemPrompt(
21401
21447
  slot,
21402
21448
  card,
21403
- stat2,
21449
+ stat3,
21404
21450
  fullDiff,
21405
21451
  customPrompt,
21406
21452
  priorContext,
@@ -21414,7 +21460,7 @@ function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt,
21414
21460
  return buildMergedReviewSystemPrompt(
21415
21461
  slot,
21416
21462
  card,
21417
- stat2,
21463
+ stat3,
21418
21464
  fullDiff,
21419
21465
  customPrompt,
21420
21466
  priorContext,
@@ -21424,7 +21470,7 @@ function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt,
21424
21470
  browserEnabled
21425
21471
  );
21426
21472
  }
21427
- function buildMergedReviewSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, browserEnabled = false) {
21473
+ function buildMergedReviewSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, browserEnabled = false) {
21428
21474
  const custom = customPrompt.trim() ? `
21429
21475
 
21430
21476
  ## Project-specific instructions
@@ -21460,7 +21506,7 @@ When you set status "fail" (reopening for rework), you can right-size the **tier
21460
21506
  ## Task to review
21461
21507
  ${card.description ?? ""}${descAttachSection}${priorContext}
21462
21508
 
21463
- ${renderReviewDiff(stat2, fullDiff, card.baseRef, scope)}
21509
+ ${renderReviewDiff(stat3, fullDiff, card.baseRef, scope)}
21464
21510
 
21465
21511
  ${ITERATION_SCOPING_NOTE}
21466
21512
 
@@ -21488,7 +21534,7 @@ ${secretsSection}` : ""}${projectContext}${runningAppSection}${levelAdjustSectio
21488
21534
 
21489
21535
  This MCP call is required \u2014 without it the pipeline has no record of your verdict.`;
21490
21536
  }
21491
- function buildOrchSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, autoCommit = true, subtaskCards = []) {
21537
+ function buildOrchSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, autoCommit = true, subtaskCards = []) {
21492
21538
  const commentType = slotCommentType(slot);
21493
21539
  const custom = customPrompt.trim() ? `
21494
21540
 
@@ -21534,7 +21580,7 @@ ${card.description}
21534
21580
 
21535
21581
  ${subtasksSection}${priorContext}
21536
21582
 
21537
- ${renderReviewDiff(stat2, fullDiff, card.baseRef, scope)}
21583
+ ${renderReviewDiff(stat3, fullDiff, card.baseRef, scope)}
21538
21584
 
21539
21585
  ${ITERATION_SCOPING_NOTE}
21540
21586
 
@@ -21747,6 +21793,12 @@ var TaskScheduler = class {
21747
21793
  planPhaseManuallyStopped = /* @__PURE__ */ new Set();
21748
21794
  // Individual review stream IDs stopped by a manual stopTask() call.
21749
21795
  manuallyStoppedStreams = /* @__PURE__ */ new Set();
21796
+ // Worktree-setup install commands currently running, keyed by taskId — so stopTask()
21797
+ // can kill the install PTY before the dev agent has even started.
21798
+ runningInstalls = /* @__PURE__ */ new Map();
21799
+ // Tasks whose install was manually stopped — signals the install runner to reset the
21800
+ // task to its initial state instead of proceeding to the agent.
21801
+ manuallyStoppedInstalls = /* @__PURE__ */ new Set();
21750
21802
  // Shared worktree IDs currently in use by a dev agent — prevents sibling cards from
21751
21803
  // running concurrently in the same worktree directory.
21752
21804
  runningSharedWorktrees = /* @__PURE__ */ new Set();
@@ -22066,44 +22118,112 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22066
22118
  const { filesToCopy, installCommand } = projectConfig.worktreeSetup;
22067
22119
  if (filesToCopy.length > 0) {
22068
22120
  const copied = [];
22069
- for (const relPath of filesToCopy) {
22070
- const src = join17(repoPath, relPath);
22121
+ const linked = [];
22122
+ for (const entry of filesToCopy) {
22123
+ const src = join17(repoPath, entry.path);
22071
22124
  if (!existsSync10(src)) continue;
22072
- const dst = join17(worktree.path, relPath);
22073
- mkdirSync9(dirname6(dst), { recursive: true });
22125
+ const dst = join17(worktree.path, entry.path);
22126
+ await mkdir3(dirname6(dst), { recursive: true });
22074
22127
  try {
22075
- cpSync(src, dst, { recursive: true });
22076
- copied.push(relPath);
22077
- } catch {
22128
+ if (entry.symlink) {
22129
+ await shareIntoWorktree(src, dst);
22130
+ linked.push(entry.path);
22131
+ } else {
22132
+ await cp(src, dst, { recursive: true, dereference: true });
22133
+ copied.push(entry.path);
22134
+ }
22135
+ } catch (err) {
22136
+ logger.warn(
22137
+ { err },
22138
+ `[scheduler] worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
22139
+ );
22078
22140
  }
22079
22141
  }
22080
- if (copied.length > 0) {
22081
- await appendActivityLog(workspaceId, taskId, `Copied to worktree: ${copied.join(", ")}`);
22142
+ const summary = [
22143
+ copied.length ? `Copied: ${copied.join(", ")}` : "",
22144
+ linked.length ? `Linked: ${linked.join(", ")}` : ""
22145
+ ].filter(Boolean).join(" \xB7 ");
22146
+ if (summary) {
22147
+ await appendActivityLog(workspaceId, taskId, `Worktree setup \u2014 ${summary}`);
22082
22148
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22083
22149
  }
22084
22150
  }
22085
22151
  if (installCommand.trim()) {
22086
- await appendActivityLog(workspaceId, taskId, `Running: ${installCommand.trim()}`);
22152
+ const installCmd = installCommand.trim();
22153
+ await appendActivityLog(workspaceId, taskId, `Running: ${installCmd}`);
22154
+ const installStartedAt = Date.now();
22155
+ const installStreamId = `${taskId}-install-${installStartedAt}`;
22156
+ await appendTerminalSession(workspaceId, taskId, {
22157
+ streamId: installStreamId,
22158
+ type: "install",
22159
+ startedAt: installStartedAt,
22160
+ state: "running"
22161
+ });
22087
22162
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22088
- await new Promise((resolve5) => {
22089
- const proc = spawn6("sh", ["-c", installCommand.trim()], {
22163
+ let installBuffer = "";
22164
+ const emitInstall = (data) => {
22165
+ installBuffer += data;
22166
+ stateHub.broadcastTerminalOutput(workspaceId, installStreamId, data);
22167
+ };
22168
+ emitInstall(`\x1B[1;36m$ ${installCmd}\x1B[0m\r
22169
+ `);
22170
+ const exitCode = await new Promise((resolveExit) => {
22171
+ const [shell, shellArgs] = getShellInvocation(installCmd);
22172
+ const proc = nodePty2.spawn(shell, shellArgs, {
22173
+ name: "xterm-256color",
22174
+ cols: 220,
22175
+ rows: 50,
22090
22176
  cwd: worktree.path,
22091
- stdio: "ignore",
22092
- env: { ...process.env, REPO_PATH: repoPath }
22093
- });
22094
- proc.on("close", (code) => {
22095
- if (code !== 0) {
22096
- logger.error(`[scheduler] Install command failed (code ${code}) for task ${taskId}`);
22097
- void appendActivityLog(
22098
- workspaceId,
22099
- taskId,
22100
- `Install command failed (code ${code}) \u2014 proceeding anyway`
22101
- );
22102
- }
22103
- resolve5();
22177
+ env: { ...process.env, REPO_PATH: repoPath, TERM: "xterm-256color" }
22104
22178
  });
22179
+ this.runningInstalls.set(taskId, proc);
22180
+ proc.onData(emitInstall);
22181
+ proc.onExit(({ exitCode: exitCode2 }) => resolveExit(exitCode2 ?? 0));
22105
22182
  });
22106
- await appendActivityLog(workspaceId, taskId, "Install complete");
22183
+ this.runningInstalls.delete(taskId);
22184
+ this.setRecentBuffer(installStreamId, installBuffer);
22185
+ if (this.isShuttingDown) {
22186
+ await saveTerminalBuffer(workspaceId, installStreamId, installBuffer);
22187
+ return;
22188
+ }
22189
+ if (this.manuallyStoppedInstalls.delete(taskId)) {
22190
+ emitInstall("\r\n\x1B[1;33mInstall stopped \u2014 task reset\x1B[0m\r\n");
22191
+ await saveTerminalBuffer(workspaceId, installStreamId, installBuffer);
22192
+ await endTerminalSession(workspaceId, taskId, installStreamId, Date.now(), "stopped");
22193
+ if (hasSharedWorktree) this.runningSharedWorktrees.delete(effectiveWorktreeId);
22194
+ await removeWorktreeAsync(effectiveWorktreeId, repoPath, worktree.branch);
22195
+ await clearCardSession(workspaceId, taskId);
22196
+ await updateCard(workspaceId, taskId, { readyForDev: false });
22197
+ const stoppedBoard = await loadBoard(workspaceId);
22198
+ if (stoppedBoard.cards[taskId]?.columnId === "in_progress") {
22199
+ await moveCard(workspaceId, taskId, "todo");
22200
+ await appendActivityLog(workspaceId, taskId, "Moved back to Todo");
22201
+ }
22202
+ stateHub.broadcastWorkspaceUpdate(workspaceId);
22203
+ return;
22204
+ }
22205
+ if (exitCode !== 0) {
22206
+ logger.error(`[scheduler] Install command failed (code ${exitCode}) for task ${taskId}`);
22207
+ emitInstall(`\r
22208
+ \x1B[1;31mInstall command failed (code ${exitCode}) \u2014 proceeding anyway\x1B[0m\r
22209
+ `);
22210
+ await appendActivityLog(
22211
+ workspaceId,
22212
+ taskId,
22213
+ `Install command failed (code ${exitCode}) \u2014 proceeding anyway`
22214
+ );
22215
+ } else {
22216
+ emitInstall("\r\n\x1B[1;32mInstall complete\x1B[0m\r\n");
22217
+ await appendActivityLog(workspaceId, taskId, "Install complete");
22218
+ }
22219
+ await saveTerminalBuffer(workspaceId, installStreamId, installBuffer);
22220
+ await endTerminalSession(
22221
+ workspaceId,
22222
+ taskId,
22223
+ installStreamId,
22224
+ Date.now(),
22225
+ exitCode === 0 ? "completed" : "failed"
22226
+ );
22107
22227
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22108
22228
  }
22109
22229
  }
@@ -22355,6 +22475,15 @@ ${devSystemPromptResult.text}`;
22355
22475
  }
22356
22476
  }
22357
22477
  stopTask(taskId) {
22478
+ const installProc = this.runningInstalls.get(taskId);
22479
+ if (installProc) {
22480
+ logger.info(`[scheduler] stopTask: install running \u2014 stopping task ${taskId}`);
22481
+ this.manuallyStoppedInstalls.add(taskId);
22482
+ this.runningInstalls.delete(taskId);
22483
+ killInstallProcess(installProc);
22484
+ void appendActivityLog(this.options.workspaceId, taskId, "Install stopped manually");
22485
+ return;
22486
+ }
22358
22487
  const task = this.running.get(taskId);
22359
22488
  if (task) {
22360
22489
  logger.info(`[scheduler] stopTask: dev agent running \u2014 stopping task ${taskId}`);
@@ -22610,6 +22739,10 @@ ${devSystemPromptResult.text}`;
22610
22739
  for (const [taskId] of this.running) {
22611
22740
  this.stopTask(taskId);
22612
22741
  }
22742
+ for (const proc of this.runningInstalls.values()) {
22743
+ killInstallProcess(proc);
22744
+ }
22745
+ this.runningInstalls.clear();
22613
22746
  this.stopAssistantAgent();
22614
22747
  }
22615
22748
  // Call before stopAll() during graceful shutdown so onExit handlers bail out
@@ -22618,6 +22751,28 @@ ${devSystemPromptResult.text}`;
22618
22751
  this.isShuttingDown = true;
22619
22752
  }
22620
22753
  };
22754
+ function killInstallProcess(proc) {
22755
+ try {
22756
+ (0, import_tree_kill2.default)(proc.pid, "SIGKILL");
22757
+ } catch {
22758
+ }
22759
+ }
22760
+ async function shareIntoWorktree(src, dst) {
22761
+ const isDir = (await stat2(src)).isDirectory();
22762
+ if (process.platform !== "win32") {
22763
+ await symlink(src, dst, isDir ? "dir" : "file");
22764
+ return;
22765
+ }
22766
+ if (isDir) {
22767
+ await symlink(src, dst, "junction");
22768
+ return;
22769
+ }
22770
+ try {
22771
+ await link(src, dst);
22772
+ } catch {
22773
+ await cp(src, dst, { recursive: true, dereference: true });
22774
+ }
22775
+ }
22621
22776
  function getMcpServerPath() {
22622
22777
  const thisFile = fileURLToPath4(import.meta.url);
22623
22778
  const thisDir = dirname6(thisFile);
@@ -26500,7 +26655,8 @@ function appExists(bundle) {
26500
26655
  return paths.some((p2) => existsSync11(p2));
26501
26656
  }
26502
26657
  function binaryExists(bin) {
26503
- const r = spawnSync7("which", [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26658
+ const finder = process.platform === "win32" ? "where" : "which";
26659
+ const r = spawnSync7(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26504
26660
  return r.status === 0 && r.stdout.trim().length > 0;
26505
26661
  }
26506
26662
  function listTerminalApps() {
@@ -26941,8 +27097,8 @@ var checkProjectPath = async (repoPath) => {
26941
27097
  return { valid: false, isGitRepo: false, error: null, name: null, branch: null, remote: null, branches: [] };
26942
27098
  const { statSync: statSync2 } = await import("node:fs");
26943
27099
  try {
26944
- const stat2 = statSync2(repoPath);
26945
- if (!stat2.isDirectory())
27100
+ const stat3 = statSync2(repoPath);
27101
+ if (!stat3.isDirectory())
26946
27102
  return {
26947
27103
  valid: false,
26948
27104
  isGitRepo: false,
@@ -26998,8 +27154,8 @@ var checkProjectPath = async (repoPath) => {
26998
27154
  var addProject = async (repoPath, initialConfig) => {
26999
27155
  const { statSync: statSync2 } = await import("node:fs");
27000
27156
  try {
27001
- const stat2 = statSync2(repoPath);
27002
- if (!stat2.isDirectory()) throw new Error("Not a directory");
27157
+ const stat3 = statSync2(repoPath);
27158
+ if (!stat3.isDirectory()) throw new Error("Not a directory");
27003
27159
  } catch {
27004
27160
  throw BadRequestError(`Path does not exist: ${repoPath}`);
27005
27161
  }
@@ -27336,13 +27492,13 @@ import { z as z14 } from "zod";
27336
27492
  init_runtime_config();
27337
27493
 
27338
27494
  // src/tunnel/cloudflare-setup.ts
27339
- init_runtime_config();
27340
- init_logger();
27341
27495
  import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
27342
- import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27496
+ import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27343
27497
  import { homedir as homedir5 } from "node:os";
27344
27498
  import { join as join20 } from "node:path";
27345
27499
  import { promisify as promisify9 } from "node:util";
27500
+ init_runtime_config();
27501
+ init_logger();
27346
27502
  var execFileAsync7 = promisify9(execFile9);
27347
27503
  var CLOUDFLARED_DIR = join20(homedir5(), ".cloudflared");
27348
27504
  async function checkCloudflaredInstalled() {
@@ -27385,7 +27541,8 @@ async function openCloudflaredLogin(force = false) {
27385
27541
  resolved = true;
27386
27542
  const loginUrl = match2[1].trim();
27387
27543
  logger.info(`[cloudflared-login] Auth URL: ${loginUrl}`);
27388
- spawn7("open", [loginUrl], { stdio: "ignore" });
27544
+ open_default(loginUrl).catch(() => {
27545
+ });
27389
27546
  proc.unref();
27390
27547
  resolve5({ alreadyLoggedIn: false, loginUrl });
27391
27548
  }
@@ -27445,7 +27602,7 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27445
27602
  ` service: http://127.0.0.1:${DEFAULT_PORT}`,
27446
27603
  ` - service: http_status:404`
27447
27604
  ].join("\n");
27448
- await mkdir3(CLOUDFLARED_DIR, { recursive: true });
27605
+ await mkdir4(CLOUDFLARED_DIR, { recursive: true });
27449
27606
  await writeFile3(join20(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
27450
27607
  logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
27451
27608
  }
@@ -27534,7 +27691,7 @@ import { z as z15 } from "zod";
27534
27691
  // src/api/services/workflows-service.ts
27535
27692
  init_workspace_state();
27536
27693
  import { existsSync as existsSync13 } from "node:fs";
27537
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27694
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27538
27695
  import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
27539
27696
  var resolvePromptPath = async (workspaceId, requestedPath) => {
27540
27697
  const workspaces = await listWorkspaces();
@@ -27572,7 +27729,7 @@ var deleteWorkflow = async (workspaceId, workflowId) => {
27572
27729
  };
27573
27730
  var writePromptFile = async (workspaceId, path2, content) => {
27574
27731
  const targetPath = await resolvePromptPath(workspaceId, path2);
27575
- await mkdir4(dirname8(targetPath), { recursive: true });
27732
+ await mkdir5(dirname8(targetPath), { recursive: true });
27576
27733
  await writeFile4(targetPath, content, "utf-8");
27577
27734
  return { path: path2 };
27578
27735
  };
@@ -27902,8 +28059,8 @@ async function createRuntimeServer(options) {
27902
28059
  const runTerminalListeners = /* @__PURE__ */ new Map();
27903
28060
  function startRun(workspaceId, cardId, command, cwd) {
27904
28061
  stopRun(workspaceId);
27905
- const shell = process.env.SHELL ?? "/bin/bash";
27906
- const pty = nodePty2.spawn(shell, ["-c", command], {
28062
+ const [shell, shellArgs] = getShellInvocation(command);
28063
+ const pty = nodePty3.spawn(shell, shellArgs, {
27907
28064
  name: "xterm-256color",
27908
28065
  cols: 120,
27909
28066
  rows: 40,
@@ -28613,7 +28770,7 @@ process.on("uncaughtException", (err) => {
28613
28770
  if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
28614
28771
  throw err;
28615
28772
  });
28616
- var VERSION9 = true ? "0.6.0" : "0.0.0-dev";
28773
+ var VERSION9 = true ? "0.8.0" : "0.0.0-dev";
28617
28774
  async function isPortAvailable(port, host) {
28618
28775
  return new Promise((resolve5) => {
28619
28776
  const probe = createServer2();
@@ -33,7 +33,7 @@ function highestWorkflowLevel(workflow) {
33
33
  }
34
34
  return LEVEL_ORDER[bestIdx] ?? "medium";
35
35
  }
36
- var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
36
+ var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
37
37
  var init_api_contract = __esm({
38
38
  "src/core/api-contract.ts"() {
39
39
  "use strict";
@@ -394,8 +394,13 @@ Do NOT include:
394
394
  runtimeGithubConfigSchema = z.object({
395
395
  token: z.string()
396
396
  });
397
+ runtimeWorktreeCopyEntrySchema = z.object({
398
+ path: z.string().min(1),
399
+ symlink: z.boolean().default(false)
400
+ });
397
401
  runtimeWorktreeSetupSchema = z.object({
398
- filesToCopy: z.array(z.string()).default([]),
402
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
403
+ filesToCopy: z.array(z.union([z.string().transform((path) => ({ path, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
399
404
  installCommand: z.string().default("")
400
405
  });
401
406
  runtimeProjectSecretSchema = z.object({
@@ -6460,6 +6460,10 @@
6460
6460
  color: #7a7a40;
6461
6461
  }
6462
6462
 
6463
+ .text-\[\#7aa2f7\] {
6464
+ color: #7aa2f7;
6465
+ }
6466
+
6463
6467
  .text-\[\#7c6aff\] {
6464
6468
  color: #7c6aff;
6465
6469
  }
@@ -6472,6 +6476,10 @@
6472
6476
  color: #8888a0;
6473
6477
  }
6474
6478
 
6479
+ .text-\[\#45455a\] {
6480
+ color: #45455a;
6481
+ }
6482
+
6475
6483
  .text-\[\#60607a\] {
6476
6484
  color: #60607a;
6477
6485
  }
@@ -7054,6 +7062,10 @@
7054
7062
  background-color: #15151b;
7055
7063
  }
7056
7064
 
7065
+ .hover\:bg-\[\#17171f\]:hover {
7066
+ background-color: #17171f;
7067
+ }
7068
+
7057
7069
  .hover\:bg-\[\#252510\]:hover {
7058
7070
  background-color: #252510;
7059
7071
  }
@@ -7140,6 +7152,10 @@
7140
7152
  color: #7c6aff;
7141
7153
  }
7142
7154
 
7155
+ .hover\:text-\[\#9a9ab0\]:hover {
7156
+ color: #9a9ab0;
7157
+ }
7158
+
7143
7159
  .hover\:text-\[\#22c55e\]:hover {
7144
7160
  color: #22c55e;
7145
7161
  }
@@ -27603,8 +27603,13 @@ const runtimeGlobalConfigSchema = object({
27603
27603
  const runtimeGithubConfigSchema = object({
27604
27604
  token: string$2()
27605
27605
  });
27606
+ const runtimeWorktreeCopyEntrySchema = object({
27607
+ path: string$2().min(1),
27608
+ symlink: boolean$1().default(false)
27609
+ });
27606
27610
  const runtimeWorktreeSetupSchema = object({
27607
- filesToCopy: array(string$2()).default([]),
27611
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
27612
+ filesToCopy: array(union([string$2().transform((path2) => ({ path: path2, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
27608
27613
  installCommand: string$2().default("")
27609
27614
  });
27610
27615
  const runtimeProjectSecretSchema = object({
@@ -50176,6 +50181,9 @@ const AGENT_DISPLAY = {
50176
50181
  dotColor: "bg-[#fb8147]"
50177
50182
  }
50178
50183
  };
50184
+ const SESSION_TYPE_LABELS = {
50185
+ install: "Install"
50186
+ };
50179
50187
  function formatElapsed(sec) {
50180
50188
  return `${Math.floor(sec / 60)}m ${(sec % 60).toString().padStart(2, "0")}s`;
50181
50189
  }
@@ -50506,7 +50514,7 @@ function WorkflowPipeline({
50506
50514
  /* Collapsed: icon-only timeline centered */
50507
50515
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col items-center pb-4 gap-0", children: sessions.length > 0 ? sessions.map((session, idx) => {
50508
50516
  var _a3;
50509
- const slotName = ((_a3 = workflowSlots == null ? void 0 : workflowSlots.find((s16) => s16.id === session.type)) == null ? void 0 : _a3.name) ?? session.type;
50517
+ const slotName = ((_a3 = workflowSlots == null ? void 0 : workflowSlots.find((s16) => s16.id === session.type)) == null ? void 0 : _a3.name) ?? SESSION_TYPE_LABELS[session.type] ?? session.type;
50510
50518
  const status = sessionStatus(session);
50511
50519
  const isFocused = activeStreamId === session.streamId;
50512
50520
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [
@@ -50541,7 +50549,7 @@ function WorkflowPipeline({
50541
50549
  /* Expanded: full rows */
50542
50550
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col px-[18px] pb-4 max-h-72 overflow-y-auto", children: sessions.length > 0 ? sessions.map((session, idx) => {
50543
50551
  var _a3;
50544
- const slotName = ((_a3 = workflowSlots == null ? void 0 : workflowSlots.find((s16) => s16.id === session.type)) == null ? void 0 : _a3.name) ?? session.type;
50552
+ const slotName = ((_a3 = workflowSlots == null ? void 0 : workflowSlots.find((s16) => s16.id === session.type)) == null ? void 0 : _a3.name) ?? SESSION_TYPE_LABELS[session.type] ?? session.type;
50545
50553
  const status = sessionStatus(session);
50546
50554
  const duration2 = slotDuration(session.startedAt, session.endedAt);
50547
50555
  const isFocused = activeStreamId === session.streamId;
@@ -78188,6 +78196,9 @@ const globalConfigFormSchema = runtimeGlobalConfigSchema.extend({
78188
78196
  });
78189
78197
  const notificationSoundsFormSchema = notificationSoundsConfigSchema;
78190
78198
  const environmentFormSchema = runtimeWorktreeSetupSchema.extend({
78199
+ // The form always works with normalized copy entries (legacy-string tolerance
78200
+ // lives in the persistence schema), so the RHF value type stays clean.
78201
+ filesToCopy: array(runtimeWorktreeCopyEntrySchema).default([]),
78191
78202
  startCommand: string$2().default("")
78192
78203
  });
78193
78204
  object({
@@ -79867,39 +79878,70 @@ function FilesBox({
79867
79878
  }, [filesError]);
79868
79879
  const rootFiles = (data == null ? void 0 : data.files) ?? null;
79869
79880
  const discoveredSet = new Set(rootFiles ?? []);
79870
- const allFiles = [.../* @__PURE__ */ new Set([...rootFiles ?? [], ...filesToCopy])].sort();
79881
+ const byPath = new Map(filesToCopy.map((e) => [e.path, e]));
79882
+ const allFiles = [.../* @__PURE__ */ new Set([...rootFiles ?? [], ...filesToCopy.map((e) => e.path)])].sort();
79871
79883
  const toggle = (file, checked) => {
79872
- onChange(checked ? [.../* @__PURE__ */ new Set([...filesToCopy, file])] : filesToCopy.filter((f) => f !== file));
79884
+ if (checked) {
79885
+ if (!byPath.has(file)) onChange([...filesToCopy, { path: file, symlink: false }]);
79886
+ } else {
79887
+ onChange(filesToCopy.filter((e) => e.path !== file));
79888
+ }
79889
+ };
79890
+ const setSymlink = (file, symlink) => {
79891
+ onChange(filesToCopy.map((e) => e.path === file ? { ...e, symlink } : e));
79873
79892
  };
79874
79893
  const addManual = () => {
79875
79894
  const val = addInput.trim();
79876
79895
  if (!val) return;
79877
- onChange([.../* @__PURE__ */ new Set([...filesToCopy, val])]);
79896
+ if (!byPath.has(val)) onChange([...filesToCopy, { path: val, symlink: false }]);
79878
79897
  setAddInput("");
79879
79898
  };
79880
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5 bg-[#0c0c0f] border border-[#2a2a35] rounded-md px-3 py-2 flex-1", children: [
79899
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-0.5 bg-[#0c0c0f] border border-[#2a2a35] rounded-md px-3 py-2 flex-1", children: [
79881
79900
  rootFiles === null && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] py-1 text-[#4a4a5a]", children: "Scanning..." }),
79882
79901
  rootFiles !== null && allFiles.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] py-1 text-[#4a4a5a]", children: "No gitignored files found in repo root" }),
79883
79902
  allFiles.map((file) => {
79884
- const checked = filesToCopy.includes(file);
79903
+ const entry = byPath.get(file);
79904
+ const checked = !!entry;
79885
79905
  const isManual = !discoveredSet.has(file);
79886
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer group", children: [
79887
- /* @__PURE__ */ jsxRuntimeExports.jsx(CustomCheckbox, { checked, onChange: (v2) => toggle(file, v2) }),
79888
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-[12px] font-mono text-[#c0c0d0]", children: file }),
79889
- isManual && /* @__PURE__ */ jsxRuntimeExports.jsx(
79890
- "button",
79891
- {
79892
- onClick: (e) => {
79893
- e.preventDefault();
79894
- onChange(filesToCopy.filter((f) => f !== file));
79895
- },
79896
- className: "opacity-0 group-hover:opacity-100 transition-opacity text-[#60607a]",
79897
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(X$1, { size: 11 })
79898
- }
79899
- )
79900
- ] }, file);
79906
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
79907
+ "label",
79908
+ {
79909
+ className: "flex items-center gap-2 cursor-pointer group -mx-2 px-2 py-1 rounded transition-colors hover:bg-[#17171f]",
79910
+ children: [
79911
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 w-4 flex justify-center", children: entry && /* @__PURE__ */ jsxRuntimeExports.jsx(
79912
+ "button",
79913
+ {
79914
+ type: "button",
79915
+ title: entry.symlink ? "Symlinked (shared from repo). Click to copy instead." : "Copied into worktree. Click to symlink (share from repo, e.g. node_modules).",
79916
+ onClick: (e) => {
79917
+ e.preventDefault();
79918
+ setSymlink(file, !entry.symlink);
79919
+ },
79920
+ className: `transition-colors ${entry.symlink ? "text-[#7aa2f7]" : "text-[#45455a] hover:text-[#9a9ab0]"}`,
79921
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Link2, { size: 12 })
79922
+ }
79923
+ ) }),
79924
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CustomCheckbox, { checked, onChange: (v2) => toggle(file, v2) }),
79925
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-[12px] font-mono text-[#c0c0d0]", children: file }),
79926
+ isManual && /* @__PURE__ */ jsxRuntimeExports.jsx(
79927
+ "button",
79928
+ {
79929
+ type: "button",
79930
+ onClick: (e) => {
79931
+ e.preventDefault();
79932
+ onChange(filesToCopy.filter((f) => f.path !== file));
79933
+ },
79934
+ className: "opacity-0 group-hover:opacity-100 transition-opacity text-[#60607a]",
79935
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(X$1, { size: 11 })
79936
+ }
79937
+ )
79938
+ ]
79939
+ },
79940
+ file
79941
+ );
79901
79942
  }),
79902
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 pt-1", children: [
79943
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 pt-1.5", children: [
79944
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 w-4" }),
79903
79945
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 w-4 h-4 border border-[#2a2a35] rounded-[3px]" }),
79904
79946
  /* @__PURE__ */ jsxRuntimeExports.jsx(
79905
79947
  "input",
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>whipped</title>
8
- <script type="module" crossorigin src="/assets/index-peNzhkq8.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Do7b5IJu.css">
8
+ <script type="module" crossorigin src="/assets/index-CuGz83Sg.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CRXPsGTP.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whipped",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Autonomous AI agent kanban board for Claude, Codex, Opencode, and Cursor.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/nxnom/whipped#readme",
@@ -22,6 +22,7 @@
22
22
  "engines": {
23
23
  "node": ">=22"
24
24
  },
25
+ "packageManager": "pnpm@11.8.0",
25
26
  "scripts": {
26
27
  "clean": "rm -rf dist",
27
28
  "build": "pnpm clean && pnpm web:build && node scripts/build.mjs",
@@ -67,15 +68,5 @@
67
68
  "esbuild": "^0.27.4",
68
69
  "tsx": "^4.20.3",
69
70
  "typescript": "^5.9.3"
70
- },
71
- "pnpm": {
72
- "onlyBuiltDependencies": [
73
- "better-sqlite3",
74
- "esbuild",
75
- "node-pty"
76
- ],
77
- "overrides": {
78
- "hono": "4.12.23"
79
- }
80
71
  }
81
- }
72
+ }