@kw-yeh/vibeflow 4.2.0 → 4.4.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/vibeflow.mjs CHANGED
@@ -280,7 +280,7 @@ var init_session_backend = __esm({
280
280
  });
281
281
 
282
282
  // packages/core/src/tmux-backend.ts
283
- import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
283
+ import { execFile as execFile3, execFileSync as execFileSync2 } from "child_process";
284
284
  import fs3 from "fs";
285
285
  import path7 from "path";
286
286
  import * as nodePty2 from "node-pty";
@@ -289,7 +289,7 @@ function q(value) {
289
289
  }
290
290
  function defaultRun(env) {
291
291
  return (args) => new Promise((resolve) => {
292
- execFile2("tmux", args, { env }, (err, stdout) => {
292
+ execFile3("tmux", args, { env }, (err, stdout) => {
293
293
  const code2 = err ? typeof err.code === "number" ? err.code : 1 : 0;
294
294
  resolve({ code: code2, stdout: String(stdout ?? "") });
295
295
  });
@@ -555,14 +555,17 @@ ${entry}
555
555
  }
556
556
  }
557
557
  function writeAttachments(worktreePath, inputs) {
558
+ if (inputs.length === 0) return [];
559
+ ensureAttachmentExclude(worktreePath);
560
+ return writeAttachmentsTo(path10.join(worktreePath, ATTACHMENTS_DIR), inputs);
561
+ }
562
+ function writeAttachmentsTo(attachmentDir, inputs) {
558
563
  if (inputs.length === 0) return [];
559
564
  const decodedInputs = inputs.map((input) => ({
560
565
  input,
561
566
  name: safeAttachmentName(input.name),
562
567
  bytes: decodeAttachment(input)
563
568
  }));
564
- ensureAttachmentExclude(worktreePath);
565
- const attachmentDir = path10.join(worktreePath, ATTACHMENTS_DIR);
566
569
  fs6.mkdirSync(attachmentDir, { recursive: true });
567
570
  return decodedInputs.map(({ input, name, bytes }) => {
568
571
  const id = randomUUID().slice(0, 8);
@@ -607,7 +610,6 @@ function newFormState(defaultAutoMode, agents) {
607
610
  const agentCli = agents && agents.length > 0 && !agents.some((a) => a.id === "claude") ? agents[0].id : "claude";
608
611
  return {
609
612
  mode: "new",
610
- launched: false,
611
613
  title: "",
612
614
  description: "",
613
615
  projectPath: null,
@@ -627,11 +629,11 @@ function editFormState(task, defaultAutoMode) {
627
629
  return {
628
630
  mode: "edit",
629
631
  taskId: task.id,
630
- launched: Boolean(task.launchedAt),
631
632
  title: task.title,
632
633
  description: task.description ?? "",
633
634
  projectPath: task.projectPath ?? null,
634
635
  originalProjectPath: task.projectPath ?? null,
636
+ worktreePath: task.worktreePath,
635
637
  gitInfo: null,
636
638
  gitInfoPath: null,
637
639
  baseBranch: task.baseBranch ?? "",
@@ -662,8 +664,8 @@ function visibleFields(s, ctx) {
662
664
  return fields2;
663
665
  }
664
666
  const fields = ["title", "description", "effort", "autoMode", "project"];
665
- if (projectChanged(s) && isRepo && hasRemote) fields.push("baseBranch");
666
- fields.push("agent", "model", "submit");
667
+ if (isRepo && hasRemote) fields.push("baseBranch");
668
+ fields.push("branch", "agent", "model", "submit");
667
669
  return fields;
668
670
  }
669
671
  function modelOptions(ctx, agentCli, current) {
@@ -712,7 +714,8 @@ function updatePayload(s) {
712
714
  model: s.model || void 0,
713
715
  effort: s.effort,
714
716
  autoMode: s.autoMode,
715
- ...!s.launched && s.projectPath ? { projectPath: s.projectPath, baseBranch: s.baseBranch || null } : {}
717
+ ...s.projectPath ? { projectPath: s.projectPath, baseBranch: s.baseBranch || null } : {},
718
+ branch: s.branch.trim()
716
719
  };
717
720
  }
718
721
  function editorCommand(env, platform) {
@@ -754,16 +757,20 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
754
757
  const lookup = useRef(0);
755
758
  const fields = visibleFields(s, ctx);
756
759
  const field = fields[Math.min(focus, fields.length - 1)];
757
- const projectLocked = s.mode === "edit" && s.launched;
758
760
  const missing = isProjectMissing(ctx.recent, s.projectPath);
759
761
  useEffect(() => {
760
- if (!s.projectPath || missing || s.gitInfoPath === s.projectPath || s.mode === "edit" && !projectChanged(s)) return;
762
+ if (!s.projectPath || missing || s.gitInfoPath === s.projectPath) return;
761
763
  const target = s.projectPath;
762
764
  const id = ++lookup.current;
763
765
  setLoadingGit(true);
764
766
  void api.getGitInfo(target).then((info) => {
765
767
  if (id !== lookup.current) return;
766
- setS((prev) => ({ ...prev, gitInfo: info, gitInfoPath: target, baseBranch: info.defaultBase ?? "" }));
768
+ setS((prev) => ({
769
+ ...prev,
770
+ gitInfo: info,
771
+ gitInfoPath: target,
772
+ baseBranch: prev.mode === "edit" && target === prev.originalProjectPath && prev.baseBranch ? prev.baseBranch : info.defaultBase ?? ""
773
+ }));
767
774
  }).catch((err) => id === lookup.current && setMessage(`\u5075\u6E2C Git \u5931\u6557\uFF1A${err.message}`)).finally(() => id === lookup.current && setLoadingGit(false));
768
775
  }, [s.projectPath]);
769
776
  const leave = (result) => {
@@ -771,7 +778,6 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
771
778
  exit();
772
779
  };
773
780
  const setProject = (projectPath) => {
774
- if (projectLocked) return;
775
781
  setS((prev) => ({ ...prev, projectPath, gitInfo: null, gitInfoPath: null, baseBranch: "" }));
776
782
  };
777
783
  const submit = async () => {
@@ -833,7 +839,7 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
833
839
  if (key.downArrow || key.tab) return setFocus((f) => Math.min(fields.length - 1, f + 1));
834
840
  const step = key.leftArrow ? -1 : key.rightArrow ? 1 : 0;
835
841
  if (step) {
836
- if (field === "project" && !projectLocked) {
842
+ if (field === "project") {
837
843
  const paths = ctx.recent.map((p) => p.path);
838
844
  if (paths.length) setProject(cycle(paths, s.projectPath ?? paths[paths.length - 1], step));
839
845
  } else if (field === "baseBranch") {
@@ -859,7 +865,7 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
859
865
  void api.initRepository(s.projectPath).then((info) => setS((prev) => ({ ...prev, gitInfo: info, gitInfoPath: prev.projectPath, baseBranch: info.defaultBase ?? "" }))).catch((err) => setMessage(err.message)).finally(() => setBusy(null));
860
866
  return;
861
867
  }
862
- if (TEXT_FIELDS.includes(field) && !(field === "project" && projectLocked)) {
868
+ if (TEXT_FIELDS.includes(field)) {
863
869
  setEditing(field);
864
870
  setDraft(field === "title" ? s.title : field === "branch" ? s.branch : field === "project" ? s.projectPath ?? "" : "");
865
871
  }
@@ -870,7 +876,6 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
870
876
  case "title":
871
877
  return s.title || "\uFF08\u4F8B\u5982\uFF1A\u5BE6\u4F5C\u767B\u5165\u9801\u9762\uFF09";
872
878
  case "project": {
873
- if (projectLocked) return `${s.projectPath ? path24.basename(s.projectPath) : "\u2014"}\uFF08\u4EFB\u52D9\u5DF2\u958B\u59CB\uFF0C\u9396\u5B9A\uFF09`;
874
879
  if (!s.projectPath) return ctx.recent.length ? "\u2190/\u2192 \u9078\u64C7\u4F7F\u7528\u904E\u7684\u5C08\u6848\uFF0C\u6216 Enter \u8F38\u5165\u8DEF\u5F91" : "Enter \u8F38\u5165\u5C08\u6848\u8CC7\u6599\u593E\u7684\u7D55\u5C0D\u8DEF\u5F91";
875
880
  return `${s.projectPath}${missing ? "\uFF08\u8DEF\u5F91\u907A\u5931\uFF09" : ""}`;
876
881
  }
@@ -906,15 +911,18 @@ function FormView({ api, ctx, initial, initialFocus, initialMessage, pristine, o
906
911
  };
907
912
  const note = () => {
908
913
  const info = s.gitInfo;
909
- if (field === "project" && !projectLocked) {
914
+ if (field === "project") {
910
915
  if (loadingGit) return "\u5075\u6E2C Git \u72C0\u614B\u4E2D\u2026";
911
916
  if (missing) return `\u627E\u4E0D\u5230 ${s.projectPath}\uFF0C\u5C08\u6848\u53EF\u80FD\u5DF2\u88AB\u79FB\u52D5\u6216\u522A\u9664\u3002`;
912
917
  if (info && !info.isRepo) return "\u9019\u500B\u8CC7\u6599\u593E\u4E0D\u662F Git repository\u3002\u8ACB\u6539\u9078\u4E00\u500B Git \u5C08\u6848\uFF0C\u6216\u5728\u4E0B\u65B9\u521D\u59CB\u5316\u3002";
913
- if (s.mode === "edit" && projectChanged(s)) return "\u66F4\u63DB\u5C08\u6848\u6703\u5728\u65B0\u5C08\u6848\u91CD\u5EFA worktree\uFF08\u6B64\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\uFF0C\u7121\u8B8A\u66F4\u6703\u907A\u5931\uFF09\u3002";
918
+ if (s.mode === "edit" && projectChanged(s)) return "\u5206\u652F\u6703\u5728\u65B0\u5C08\u6848\u3001\u5361\u7247\u958B\u59CB\u57F7\u884C\u6642\u624D\u5EFA\u7ACB\u3002";
914
919
  if (info?.isRepo && !info.hasRemote) return `\u6B64 repository \u6C92\u6709 remote\uFF0C\u5C07\u4EE5\u76EE\u524D\u5206\u652F (${info.currentBranch ?? "HEAD"}) \u70BA\u57FA\u6E96\u5EFA\u7ACB\u672C\u5730 worktree\u3002`;
915
920
  if (info?.isRepo && s.projectPath) return `worktree \u6703\u5EFA\u5728 ${ctx.workstationPath || "~/Desktop"}/${path24.basename(s.projectPath)}`;
916
921
  }
917
- if (field === "branch") return info?.hasRemote ? "\u586B\u5BEB\u5F8C\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\uFF1B\u82E5 remote \u5DF2\u6709\u9019\u500B\u5206\u652F\uFF0C\u5247\u76F4\u63A5\u53D6\u56DE\u4E26\u63A5\u7E8C\u4E0A\u9762\u7684\u5DE5\u4F5C\u3002" : "\u586B\u5BEB\u5F8C\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\u3002";
922
+ if (field === "branch") {
923
+ const when = info?.hasRemote ? "\u958B\u59CB\u57F7\u884C\u6642\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\u4E26\u63A8\u5230 origin\uFF1B\u82E5 remote \u5DF2\u6709\u9019\u500B\u5206\u652F\uFF0C\u5247\u76F4\u63A5\u53D6\u56DE\u4E26\u63A5\u7E8C\u4E0A\u9762\u7684\u5DE5\u4F5C\u3002" : "\u958B\u59CB\u57F7\u884C\u6642\u6703\u5EFA\u7ACB\u540C\u540D\u5206\u652F\u3002";
924
+ return s.mode === "edit" && s.worktreePath ? `${when} \u9019\u5F35\u5361\u5DF2\u6709 worktree\uFF1A\u66F4\u6539\u5206\u652F\u3001\u5C08\u6848\u6216\u57FA\u6E96\u5206\u652F\u6703\u79FB\u9664\u5B83\uFF08\u6709\u672A\u5B8C\u6210\u8B8A\u66F4\u6642\u7121\u6CD5\u5132\u5B58\uFF09\u3002` : when;
925
+ }
918
926
  if (field === "effort") return `${EFFORTS.find((x) => x.value === s.effort).description} Claude Code \u8207 Codex \u6703\u5728\u555F\u52D5\u6B64\u4EFB\u52D9\u6642\u5957\u7528\uFF1B\u5BE6\u969B\u53EF\u7528\u7D1A\u8DDD\u4F9D model \u800C\u5B9A\u3002`;
919
927
  if (field === "autoMode") return s.autoMode ? "\u958B\u555F\uFF1AAgent \u6703\u76F4\u63A5\u4FEE\u6539\u6A94\u6848\u3001\u57F7\u884C\u6307\u4EE4\uFF0C\u4E0D\u6703\u9010\u6B21\u5411\u4F60\u78BA\u8A8D\u3002" : "\u95DC\u9589\uFF1AAgent \u6BCF\u6B21\u8981\u52D5\u4F5C\u524D\uFF0C\u90FD\u6703\u5728\u7D42\u7AEF\u6A5F\u88E1\u7B49\u4F60\u5141\u8A31\u3002";
920
928
  if (field === "submit") return blockReason(s, ctx, loadingGit);
@@ -1130,6 +1138,9 @@ function BoardView({ opts, initial, initialMessage, onAction }) {
1130
1138
  }
1131
1139
  setSel({ column: sel.column + direction, index: 0 });
1132
1140
  if (move.effect === "launch") {
1141
+ if (!move.task.worktreePath) {
1142
+ setMessage(`\u6B63\u5728\u70BA\u300C${move.task.title}\u300D\u5F9E\u6700\u65B0\u7684 ${move.task.baseBranch ?? "\u57FA\u6E96\u5206\u652F"} \u5EFA\u7ACB\u5206\u652F ${move.task.branch} \u8207 worktree\uFF0C\u9700\u8981\u5E7E\u79D2\u9418\u2026`);
1143
+ }
1133
1144
  await api.term.start({ taskId: move.task.id, launch: {} });
1134
1145
  setMessage(`\u5DF2\u555F\u52D5\u300C${move.task.title}\u300D\u7684 agent\u3002Enter \u53EF\u9032\u5165\u7D42\u7AEF\u3002`);
1135
1146
  } else if (move.effect === "cleanup") {
@@ -1189,8 +1200,14 @@ function BoardView({ opts, initial, initialMessage, onAction }) {
1189
1200
  return;
1190
1201
  }
1191
1202
  if (!selected) return;
1192
- if (input === "e") return leave({ type: "edit", task: selected });
1203
+ if (input === "e") {
1204
+ if (COLUMNS2[sel.column] !== "backlog") return setMessage("\u57F7\u884C\u4E2D\u8207\u5DF2\u5B8C\u6210\u7684\u5361\u7247\u53EA\u80FD\u67E5\u770B\uFF0C\u7121\u6CD5\u7DE8\u8F2F");
1205
+ return leave({ type: "edit", task: selected });
1206
+ }
1193
1207
  if (key.return) {
1208
+ if (!selected.worktreePath && COLUMNS2[sel.column] === "backlog") {
1209
+ return setMessage("\u9019\u5F35\u5361\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u5206\u652F\u8207 worktree \u6703\u5728\u958B\u59CB\u6642\u5EFA\u7ACB");
1210
+ }
1194
1211
  if (!selected.worktreePath && !selected.projectPath) return setMessage("\u9019\u5F35\u5361\u6C92\u6709\u5DE5\u4F5C\u76EE\u9304");
1195
1212
  if (COLUMNS2[sel.column] === "done") return setMessage("\u5DF2\u5B8C\u6210\u7684\u5361\u7247\u6C92\u6709\u7D42\u7AEF");
1196
1213
  return leave({ type: "attach", task: selected });
@@ -1486,7 +1503,103 @@ function releaseLock(userDataDir) {
1486
1503
  }
1487
1504
 
1488
1505
  // packages/core/src/platform.ts
1506
+ import { execFile as execFile2 } from "child_process";
1507
+
1508
+ // packages/core/src/folder-dialog.ts
1489
1509
  import { execFile } from "child_process";
1510
+ var MAC_SCRIPT = [
1511
+ "on run argv",
1512
+ "activate",
1513
+ "if (count of argv) > 1 then",
1514
+ "return POSIX path of (choose folder with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))",
1515
+ "end if",
1516
+ "return POSIX path of (choose folder with prompt (item 1 of argv))",
1517
+ "end run"
1518
+ ];
1519
+ var WINDOWS_SCRIPT = [
1520
+ "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
1521
+ "Add-Type -AssemblyName System.Windows.Forms",
1522
+ "$owner = New-Object System.Windows.Forms.Form -Property @{ TopMost = $true }",
1523
+ "$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
1524
+ "$dialog.Description = $env:VIBEFLOW_DIALOG_TITLE",
1525
+ "$dialog.ShowNewFolderButton = $true",
1526
+ "if ($env:VIBEFLOW_DIALOG_PATH) { $dialog.SelectedPath = $env:VIBEFLOW_DIALOG_PATH }",
1527
+ "if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.SelectedPath }"
1528
+ ].join("; ");
1529
+ function folderDialogCommands(platform, { title = "\u9078\u64C7\u8CC7\u6599\u593E", defaultPath } = {}) {
1530
+ if (platform === "darwin") {
1531
+ return [
1532
+ {
1533
+ cmd: "osascript",
1534
+ args: [...MAC_SCRIPT.flatMap((line) => ["-e", line]), title, ...defaultPath ? [defaultPath] : []],
1535
+ // osascript exits 1 on any script error; only -128 is "User canceled".
1536
+ cancelCodes: [1],
1537
+ cancelStderr: /-128/
1538
+ }
1539
+ ];
1540
+ }
1541
+ if (platform === "win32") {
1542
+ return [
1543
+ {
1544
+ cmd: "powershell.exe",
1545
+ args: ["-NoProfile", "-NonInteractive", "-STA", "-Command", WINDOWS_SCRIPT],
1546
+ env: { VIBEFLOW_DIALOG_TITLE: title, VIBEFLOW_DIALOG_PATH: defaultPath ?? "" },
1547
+ cancelCodes: []
1548
+ }
1549
+ ];
1550
+ }
1551
+ const start = defaultPath ? `${defaultPath.replace(/\/+$/, "")}/` : void 0;
1552
+ return [
1553
+ {
1554
+ cmd: "zenity",
1555
+ args: ["--file-selection", "--directory", `--title=${title}`, ...start ? [`--filename=${start}`] : []],
1556
+ cancelCodes: [1]
1557
+ },
1558
+ {
1559
+ cmd: "kdialog",
1560
+ args: ["--getexistingdirectory", defaultPath ?? ".", "--title", title],
1561
+ cancelCodes: [1]
1562
+ }
1563
+ ];
1564
+ }
1565
+ function parseFolderDialogOutput(stdout) {
1566
+ const line = stdout.trim();
1567
+ if (!line) return null;
1568
+ if (line === "/" || /^[A-Za-z]:[\\/]$/.test(line)) return line;
1569
+ return line.replace(/[\\/]+$/, "");
1570
+ }
1571
+ function run(command) {
1572
+ return new Promise((resolve, reject) => {
1573
+ execFile(
1574
+ command.cmd,
1575
+ command.args,
1576
+ { windowsHide: false, encoding: "utf8", env: { ...process.env, ...command.env } },
1577
+ (err, stdout, stderr) => {
1578
+ if (err) {
1579
+ const code2 = err.code;
1580
+ if (code2 === "ENOENT") return resolve("missing");
1581
+ const canceled = typeof code2 === "number" && command.cancelCodes.includes(code2) && (!command.cancelStderr || command.cancelStderr.test(stderr));
1582
+ if (canceled) return resolve({ canceled: true });
1583
+ return reject(new Error(`\u7121\u6CD5\u958B\u555F\u8CC7\u6599\u593E\u9078\u64C7\u8996\u7A97\uFF1A${stderr.trim() || err.message}`));
1584
+ }
1585
+ const picked = parseFolderDialogOutput(stdout);
1586
+ resolve(picked ? { path: picked } : { canceled: true });
1587
+ }
1588
+ );
1589
+ });
1590
+ }
1591
+ async function pickFolderWithOs(options = {}) {
1592
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
1593
+ return { unsupported: true };
1594
+ }
1595
+ for (const command of folderDialogCommands(process.platform, options)) {
1596
+ const result = await run(command);
1597
+ if (result !== "missing") return result;
1598
+ }
1599
+ return { unsupported: true };
1600
+ }
1601
+
1602
+ // packages/core/src/platform.ts
1490
1603
  import { homedir } from "os";
1491
1604
  import path2 from "path";
1492
1605
  var _platform = null;
@@ -1516,7 +1629,7 @@ function openCommand(target) {
1516
1629
  function openWithOs(target) {
1517
1630
  const [cmd, args] = openCommand(target);
1518
1631
  return new Promise((resolve) => {
1519
- execFile(cmd, args, { windowsHide: true }, (err) => resolve(err ? err.message : ""));
1632
+ execFile2(cmd, args, { windowsHide: true }, (err) => resolve(err ? err.message : ""));
1520
1633
  });
1521
1634
  }
1522
1635
  function createNodePlatform(options = {}) {
@@ -1536,7 +1649,8 @@ function createNodePlatform(options = {}) {
1536
1649
  const err = await openWithOs(url);
1537
1650
  if (err) throw new Error(err);
1538
1651
  },
1539
- openPath: (target) => openWithOs(target)
1652
+ openPath: (target) => openWithOs(target),
1653
+ pickFolder: (opts) => pickFolderWithOs(opts)
1540
1654
  };
1541
1655
  }
1542
1656
 
@@ -1840,6 +1954,8 @@ function createBridge(t) {
1840
1954
  getGithubCompareUrl: (taskId) => t.invoke("git:getGithubCompareUrl", taskId),
1841
1955
  /** Open a URL in the system default browser. */
1842
1956
  openExternal: (url) => t.invoke("shell:openExternal", url),
1957
+ /** The OS folder dialog, shown on the machine core runs on. */
1958
+ pickFolder: (options) => t.invoke("dialog:pickFolder", options),
1843
1959
  cleanupTask: (taskId) => t.invoke("vibeflow:cleanupTask", taskId),
1844
1960
  /** Live sub-agent updates pushed from main while a session runs. */
1845
1961
  onSubAgentsUpdate: (callback) => {
@@ -2079,7 +2195,7 @@ async function generateBranchName(title, description) {
2079
2195
  }
2080
2196
 
2081
2197
  // packages/core/src/git.ts
2082
- import { execFile as execFile3 } from "child_process";
2198
+ import { execFile as execFile4 } from "child_process";
2083
2199
  import { promisify } from "util";
2084
2200
  import path12 from "path";
2085
2201
  import fs8 from "fs/promises";
@@ -2400,7 +2516,7 @@ function deleteArtifacts(baseDir, worktreePath) {
2400
2516
  }
2401
2517
 
2402
2518
  // packages/core/src/git.ts
2403
- var pexec = promisify(execFile3);
2519
+ var pexec = promisify(execFile4);
2404
2520
  async function git(cwd, args) {
2405
2521
  const { stdout } = await pexec("git", args, {
2406
2522
  cwd,
@@ -3200,12 +3316,15 @@ import { join as join2 } from "path";
3200
3316
  function projectWorkstationPath(workstationRoot, projectName) {
3201
3317
  return join2(workstationRoot, projectName);
3202
3318
  }
3319
+ function taskAttachmentStagingPath(workspacePath, taskId) {
3320
+ return join2(workspacePath, ".attachments", taskId);
3321
+ }
3203
3322
 
3204
3323
  // packages/core/src/agents.ts
3205
3324
  init_env();
3206
- import { execFile as execFile4 } from "child_process";
3325
+ import { execFile as execFile5 } from "child_process";
3207
3326
  import { promisify as promisify2 } from "util";
3208
- var pexec2 = promisify2(execFile4);
3327
+ var pexec2 = promisify2(execFile5);
3209
3328
  var AGENT_EFFORTS = ["low", "medium", "high", "xhigh"];
3210
3329
  var DEFAULT_TASK_EFFORT = "medium";
3211
3330
  var AGENT_CLIS = [
@@ -3278,25 +3397,21 @@ async function createTaskFromInput(input) {
3278
3397
  if (explicitBranch) {
3279
3398
  await assertBranchAvailable(projectPath, workspacePath, explicitBranch);
3280
3399
  }
3281
- const preferredBranch = explicitBranch ?? await generateBranchName(input.title, input.description);
3282
- let provisionResult;
3283
- try {
3284
- provisionResult = await provisionWorktree(
3285
- projectPath,
3286
- workspacePath,
3287
- taskId,
3288
- input.baseBranch ?? null,
3289
- preferredBranch,
3290
- { explicitBranch: Boolean(explicitBranch) }
3291
- );
3292
- } catch (err) {
3293
- throw Object.assign(
3294
- new Error(`Worktree \u5EFA\u7ACB\u5931\u6557\uFF1A${err.message}`),
3295
- { code: "WORKTREE_CREATE_FAILED" }
3296
- );
3297
- }
3298
- const attachments = writeAttachments(
3299
- provisionResult.worktreePath,
3400
+ const branch = explicitBranch ?? await generateBranchName(input.title, input.description);
3401
+ const draft = {
3402
+ id: taskId,
3403
+ title: input.title.trim() || `Task ${taskId}`,
3404
+ branch: branch ?? fallbackBranchName(taskId),
3405
+ branchExplicit: explicitBranch ? true : void 0,
3406
+ projectPath,
3407
+ projectName,
3408
+ workspacePath,
3409
+ baseBranch: input.baseBranch || info.defaultBase || info.currentBranch || void 0
3410
+ };
3411
+ const col = input.status ?? "backlog";
3412
+ const provisioned = col === "backlog" ? null : await provisionTaskWorktree(draft);
3413
+ const attachments = provisioned?.worktreePath ? writeAttachments(provisioned.worktreePath, input.attachments ?? []) : writeAttachmentsTo(
3414
+ taskAttachmentStagingPath(workspacePath, taskId),
3300
3415
  input.attachments ?? []
3301
3416
  );
3302
3417
  const attachmentLines = attachments.map(
@@ -3305,16 +3420,9 @@ async function createTaskFromInput(input) {
3305
3420
  const baseDescription = input.description?.trim();
3306
3421
  const description = attachmentLines.length ? [baseDescription, attachmentLines.join("\n")].filter(Boolean).join("\n\n") : baseDescription || void 0;
3307
3422
  const task = {
3308
- id: taskId,
3309
- title: input.title.trim() || `Task ${taskId}`,
3423
+ ...draft,
3424
+ ...provisioned,
3310
3425
  description,
3311
- branch: provisionResult.branch,
3312
- projectPath,
3313
- projectName,
3314
- worktreePath: provisionResult.worktreePath,
3315
- workspacePath,
3316
- baseBranch: provisionResult.baseBranch,
3317
- pushed: provisionResult.pushed,
3318
3426
  createdAt: Date.now(),
3319
3427
  agentCli: input.agentCli ?? "claude",
3320
3428
  model: input.model || void 0,
@@ -3323,7 +3431,6 @@ async function createTaskFromInput(input) {
3323
3431
  };
3324
3432
  try {
3325
3433
  const board = store.get("board");
3326
- const col = input.status ?? "backlog";
3327
3434
  board[col] = [task, ...board[col]];
3328
3435
  store.set("board", board);
3329
3436
  recordRecentProject(store, projectPath);
@@ -3335,6 +3442,38 @@ async function createTaskFromInput(input) {
3335
3442
  }
3336
3443
  return { task, storePath: store.path };
3337
3444
  }
3445
+ async function provisionTaskWorktree(task) {
3446
+ if (!task.projectPath || !task.workspacePath) {
3447
+ throw Object.assign(new Error("\u4EFB\u52D9\u6C92\u6709\u5C08\u6848\u8CC7\u6599\u593E\uFF0C\u7121\u6CD5\u5EFA\u7ACB worktree"), {
3448
+ code: "WORKTREE_CREATE_FAILED"
3449
+ });
3450
+ }
3451
+ await fs9.mkdir(task.workspacePath, { recursive: true });
3452
+ if (task.branchExplicit) {
3453
+ await assertBranchAvailable(task.projectPath, task.workspacePath, task.branch);
3454
+ }
3455
+ try {
3456
+ const result = await provisionWorktree(
3457
+ task.projectPath,
3458
+ task.workspacePath,
3459
+ task.id,
3460
+ task.baseBranch ?? null,
3461
+ task.branch,
3462
+ { explicitBranch: task.branchExplicit === true }
3463
+ );
3464
+ return {
3465
+ branch: result.branch,
3466
+ worktreePath: result.worktreePath,
3467
+ baseBranch: result.baseBranch,
3468
+ pushed: result.pushed
3469
+ };
3470
+ } catch (err) {
3471
+ throw Object.assign(
3472
+ new Error(`Worktree \u5EFA\u7ACB\u5931\u6557\uFF1A${err.message}`),
3473
+ { code: "WORKTREE_CREATE_FAILED" }
3474
+ );
3475
+ }
3476
+ }
3338
3477
  function updateTaskFromInput(input) {
3339
3478
  const store = input.storePath ? getStoreAtPath(input.storePath) : getStore();
3340
3479
  const board = store.get("board");
@@ -3521,7 +3660,7 @@ async function taskCommand(argv) {
3521
3660
  title: task2.title,
3522
3661
  description: task2.description,
3523
3662
  branch: task2.branch,
3524
- worktreePath: task2.worktreePath
3663
+ worktreePath: task2.worktreePath ?? null
3525
3664
  }
3526
3665
  };
3527
3666
  }
@@ -3563,7 +3702,7 @@ async function taskCommand(argv) {
3563
3702
  title: task.title,
3564
3703
  projectPath: task.projectPath,
3565
3704
  branch: task.branch,
3566
- worktreePath: task.worktreePath,
3705
+ worktreePath: task.worktreePath ?? null,
3567
3706
  baseBranch: task.baseBranch,
3568
3707
  agentCli: task.agentCli,
3569
3708
  model: task.model,
@@ -4119,13 +4258,13 @@ import path20 from "path";
4119
4258
  import { randomUUID as randomUUID4 } from "crypto";
4120
4259
 
4121
4260
  // packages/core/src/agent-models.ts
4122
- import { execFile as execFile5 } from "child_process";
4261
+ import { execFile as execFile6 } from "child_process";
4123
4262
  import fs12 from "fs";
4124
4263
  import os4 from "os";
4125
4264
  import path16 from "path";
4126
4265
  import { promisify as promisify3 } from "util";
4127
4266
  init_env();
4128
- var pexec3 = promisify3(execFile5);
4267
+ var pexec3 = promisify3(execFile6);
4129
4268
  var CODEX_MODELS_TIMEOUT_MS = 2e4;
4130
4269
  function builtinModels(id) {
4131
4270
  return (AGENT_CLIS.find((a) => a.id === id) ?? AGENT_CLIS[0]).models;
@@ -4192,10 +4331,10 @@ async function listAgentModels(id, opts = {}) {
4192
4331
 
4193
4332
  // packages/core/src/github-auth.ts
4194
4333
  init_env();
4195
- import { execFile as execFile6 } from "child_process";
4334
+ import { execFile as execFile7 } from "child_process";
4196
4335
  import { promisify as promisify4 } from "util";
4197
4336
  import * as nodePty3 from "node-pty";
4198
- var pexec4 = promisify4(execFile6);
4337
+ var pexec4 = promisify4(execFile7);
4199
4338
  var GITHUB_HOST = "github.com";
4200
4339
  var DEVICE_URL = "https://github.com/login/device";
4201
4340
  var activeLogin = null;
@@ -5000,6 +5139,46 @@ function createCore({ sessions, bus, version }) {
5000
5139
  deleteArtifacts(task.workspacePath ?? path20.dirname(task.worktreePath), task.worktreePath);
5001
5140
  await deleteBranch(task.projectPath, branch);
5002
5141
  }
5142
+ function columnOf(taskId) {
5143
+ const board = getState().board;
5144
+ return COLUMNS.find((col) => board[col].some((t) => t.id === taskId)) ?? null;
5145
+ }
5146
+ const provisioning = /* @__PURE__ */ new Map();
5147
+ function ensureProvisioned(task) {
5148
+ if (task.worktreePath) return Promise.resolve(task);
5149
+ const pending = provisioning.get(task.id);
5150
+ if (pending) return pending;
5151
+ const run3 = (async () => {
5152
+ try {
5153
+ updateTask(task.id, { ...await provisionTaskWorktree(task), launchError: void 0 });
5154
+ bus.emit("state:changed", getState());
5155
+ } catch (err) {
5156
+ returnUnstarted(task.id, err.message);
5157
+ throw err;
5158
+ }
5159
+ const provisioned = findTask(task.id);
5160
+ if (!provisioned) throw new Error("\u627E\u4E0D\u5230\u8981\u57F7\u884C\u7684\u4EFB\u52D9");
5161
+ return provisioned;
5162
+ })().finally(() => provisioning.delete(task.id));
5163
+ provisioning.set(task.id, run3);
5164
+ return run3;
5165
+ }
5166
+ function returnUnstarted(taskId, launchError) {
5167
+ const current = findTask(taskId);
5168
+ if (!current) return;
5169
+ const board = getState().board;
5170
+ const next = {
5171
+ backlog: [
5172
+ { ...current, launchedAt: void 0, launchError },
5173
+ ...board.backlog.filter((t) => t.id !== taskId)
5174
+ ],
5175
+ in_progress: board.in_progress.filter((t) => t.id !== taskId),
5176
+ done: board.done.filter((t) => t.id !== taskId)
5177
+ };
5178
+ setBoard(next);
5179
+ bus.emit("state:changed", getState());
5180
+ }
5181
+ let folderDialog = null;
5003
5182
  const handlers = {
5004
5183
  "vibeflow:getState": () => getState(),
5005
5184
  "app:getVersion": () => version,
@@ -5073,7 +5252,13 @@ function createCore({ sessions, bus, version }) {
5073
5252
  // were new. Launching again is the caller's decision.
5074
5253
  "vibeflow:resetTaskRun": async (taskId) => {
5075
5254
  const task = requireTask(taskId);
5076
- if (!task.worktreePath) throw new Error("\u4EFB\u52D9\u6C92\u6709\u53EF\u7528\u7684 worktree");
5255
+ if (!task.worktreePath) {
5256
+ await teardownTask(task.id);
5257
+ updateTask(task.id, { launchedAt: Date.now(), runId: randomUUID4() });
5258
+ const reset2 = findTask(task.id);
5259
+ if (!reset2) throw new Error("\u627E\u4E0D\u5230\u8981\u91CD\u7F6E\u7684\u4EFB\u52D9");
5260
+ return { state: getState(), task: reset2 };
5261
+ }
5077
5262
  if (!task.baseBranch) throw new Error("\u4EFB\u52D9\u6C92\u6709\u8A18\u9304 base branch\uFF0C\u7121\u6CD5\u9084\u539F worktree");
5078
5263
  await teardownTask(task.id);
5079
5264
  await resetWorktreeToBase(task.worktreePath, task.baseBranch);
@@ -5087,21 +5272,25 @@ function createCore({ sessions, bus, version }) {
5087
5272
  bus.emit("subagents:update", { taskId: task.id, subAgents: [] });
5088
5273
  return { state: getState(), task: reset };
5089
5274
  },
5090
- // Edit an existing card's fields. Most are plain metadata read at launch time
5091
- // (title / description / agent / model / workspace). The project folder
5092
- // and base branch are git-bound: re-selecting them rebuilds the worktree, which
5093
- // is only allowed for not-yet-launched tasks (no work to lose).
5275
+ // Edit an existing card's fields. Only a Backlog card is editable: once it
5276
+ // runs, what it was asked to do and where it does it are history. The
5277
+ // git-bound fields (project, base, branch) are just stored — the worktree is
5278
+ // built when the card starts. A Backlog card that already has one (made
5279
+ // before provisioning moved to launch, or returned from a run) gives it up,
5280
+ // but only while it holds no changes.
5094
5281
  "vibeflow:updateTask": async (payload) => {
5095
5282
  const p = obj(payload, "payload");
5096
5283
  const existing = requireTask(p.taskId);
5284
+ if (columnOf(existing.id) !== "backlog") {
5285
+ throw new Error("\u53EA\u6709 Backlog \u7684\u4EFB\u52D9\u53EF\u4EE5\u7DE8\u8F2F");
5286
+ }
5097
5287
  let gitPatch = {};
5098
5288
  const nextProject = p.projectPath || existing.projectPath;
5289
+ const nextBranch = p.branch === void 0 ? "" : str(p.branch, "branch").trim();
5099
5290
  const projectChanged2 = Boolean(p.projectPath && p.projectPath !== existing.projectPath);
5100
5291
  const baseChanged = p.baseBranch != null && p.baseBranch !== (existing.baseBranch ?? null);
5101
- if (nextProject && (projectChanged2 || baseChanged)) {
5102
- if (existing.launchedAt) {
5103
- throw new Error("\u4EFB\u52D9\u5DF2\u958B\u59CB\u57F7\u884C\uFF0C\u7121\u6CD5\u66F4\u63DB\u5C08\u6848\u8CC7\u6599\u593E\u6216\u57FA\u6E96\u5206\u652F");
5104
- }
5292
+ const branchChanged = Boolean(nextBranch && nextBranch !== existing.branch);
5293
+ if (nextProject && (projectChanged2 || baseChanged || branchChanged)) {
5105
5294
  if (existing.worktreePath) {
5106
5295
  const diff = await getWorktreeDiffEntries(
5107
5296
  existing.worktreePath,
@@ -5109,35 +5298,30 @@ function createCore({ sessions, bus, version }) {
5109
5298
  { fetch: false }
5110
5299
  );
5111
5300
  if (diff.length > 0) {
5112
- throw new Error("\u76EE\u524D worktree \u5DF2\u6709\u8B8A\u66F4\uFF0C\u7121\u6CD5\u66F4\u63DB\u5C08\u6848\u8CC7\u6599\u593E\u6216\u57FA\u6E96\u5206\u652F");
5301
+ throw new Error("\u76EE\u524D worktree \u5DF2\u6709\u8B8A\u66F4\uFF0C\u7121\u6CD5\u66F4\u63DB\u5C08\u6848\u8CC7\u6599\u593E\u3001\u57FA\u6E96\u5206\u652F\u6216\u5206\u652F\u540D\u7A31");
5113
5302
  }
5114
5303
  }
5115
5304
  const info = await getGitInfo(nextProject);
5116
5305
  if (!info.isRepo) throw new Error("\u76EE\u6A19\u8CC7\u6599\u593E\u4E0D\u662F git repository");
5117
- await teardownTask(existing.id);
5118
- await removeTaskWorktree(existing);
5119
5306
  const projectName = path20.basename(nextProject);
5120
5307
  const workspacePath = projectWorkstationPath(resolveWorkstationPath(getSettings()), projectName);
5121
- await fs16.promises.mkdir(workspacePath, { recursive: true });
5122
- const result = await provisionWorktree(
5123
- nextProject,
5124
- workspacePath,
5125
- existing.id,
5126
- p.baseBranch ?? existing.baseBranch ?? null,
5127
- existing.branch
5128
- );
5308
+ await teardownTask(existing.id);
5309
+ await removeTaskWorktree(existing);
5310
+ if (branchChanged) await assertBranchAvailable(nextProject, workspacePath, nextBranch);
5129
5311
  recordRecentProject(getStore(), nextProject);
5130
5312
  gitPatch = {
5131
5313
  projectPath: nextProject,
5132
5314
  projectName,
5133
- branch: result.branch,
5134
- worktreePath: result.worktreePath,
5135
5315
  workspacePath,
5136
- baseBranch: result.baseBranch,
5137
- pushed: result.pushed
5316
+ baseBranch: (p.baseBranch ?? (projectChanged2 ? null : existing.baseBranch)) || info.defaultBase || info.currentBranch || void 0,
5317
+ ...branchChanged ? { branch: nextBranch, branchExplicit: true } : {},
5318
+ worktreePath: void 0,
5319
+ pushed: void 0,
5320
+ launchedAt: void 0
5138
5321
  };
5139
5322
  }
5140
5323
  updateTask(existing.id, {
5324
+ launchError: void 0,
5141
5325
  title: (p.title ?? "").trim() || `Task ${existing.id}`,
5142
5326
  description: p.description?.trim() || void 0,
5143
5327
  agentCli: p.agentCli,
@@ -5153,10 +5337,12 @@ function createCore({ sessions, bus, version }) {
5153
5337
  // string or a cwd, so a connected client cannot run anything else.
5154
5338
  "pty:start": async (payload) => {
5155
5339
  const p = obj(payload, "payload");
5156
- const task = requireTask(p.taskId);
5157
- const key = sessionKeyFor(task.id, p.sessionKey);
5158
- const cwd = task.worktreePath ?? task.projectPath;
5159
- if (!cwd) throw new Error("\u4EFB\u52D9\u6C92\u6709\u53EF\u7528\u7684\u5DE5\u4F5C\u76EE\u9304");
5340
+ const requested = requireTask(p.taskId);
5341
+ const key = sessionKeyFor(requested.id, p.sessionKey);
5342
+ const pending = provisioning.get(requested.id);
5343
+ const task = p.launch ? await ensureProvisioned(requested) : pending ? await pending : requested;
5344
+ const cwd = task.worktreePath;
5345
+ if (!cwd) throw new Error("\u4EFB\u52D9\u5C1A\u672A\u958B\u59CB\u57F7\u884C\uFF0C\u9084\u6C92\u6709 worktree");
5160
5346
  let command;
5161
5347
  let fresh = p.fresh === true;
5162
5348
  if (p.launch) {
@@ -5260,7 +5446,7 @@ function createCore({ sessions, bus, version }) {
5260
5446
  const task = requireTask(taskId);
5261
5447
  const base = task.baseBranch ?? "main";
5262
5448
  if (task.worktreePath) return readWorktreeSpecs(task.worktreePath, base);
5263
- if (!task.projectPath) return [];
5449
+ if (!task.projectPath || !task.launchedAt) return [];
5264
5450
  const changed = task.outcome?.files.filter((f) => f.status !== "D").map((f) => f.path);
5265
5451
  return readBranchSpecs(task.projectPath, task.branch, base, changed);
5266
5452
  },
@@ -5333,6 +5519,19 @@ function createCore({ sessions, bus, version }) {
5333
5519
  if (!task.worktreePath) return null;
5334
5520
  return getGithubCompareUrl(task.worktreePath, task.baseBranch ?? "main");
5335
5521
  },
5522
+ // One dialog at a time: a second click while it is open joins it instead
5523
+ // of stacking another window on the host.
5524
+ "dialog:pickFolder": (options) => {
5525
+ const o = options === void 0 || options === null ? {} : obj(options, "options");
5526
+ const title = o.title === void 0 ? void 0 : str(o.title, "title");
5527
+ const defaultPath = o.defaultPath === void 0 ? void 0 : str(o.defaultPath, "defaultPath");
5528
+ const pick = getPlatform().pickFolder;
5529
+ if (!pick) return { unsupported: true };
5530
+ folderDialog ??= pick({ title, defaultPath: defaultPath || void 0 }).finally(() => {
5531
+ folderDialog = null;
5532
+ });
5533
+ return folderDialog;
5534
+ },
5336
5535
  "shell:openExternal": (url) => {
5337
5536
  const u = str(url, "url");
5338
5537
  if (!/^https?:\/\//i.test(u)) invalid("only http(s) URLs can be opened");
@@ -5365,6 +5564,9 @@ function createCore({ sessions, bus, version }) {
5365
5564
  deleteDecisions(task.workspacePath, decisionsKey(task.worktreePath, task.branch));
5366
5565
  }
5367
5566
  await removeTaskWorktree(task);
5567
+ if (task.workspacePath) {
5568
+ fs16.rmSync(taskAttachmentStagingPath(task.workspacePath, task.id), { recursive: true, force: true });
5569
+ }
5368
5570
  clearConversation(task.id);
5369
5571
  removeTask(task.id);
5370
5572
  return getState();
@@ -5372,9 +5574,11 @@ function createCore({ sessions, bus, version }) {
5372
5574
  "attachments:write": (payload) => {
5373
5575
  const p = obj(payload, "payload");
5374
5576
  const task = requireTask(p.taskId);
5375
- if (!task.worktreePath) throw new Error("\u627E\u4E0D\u5230\u4EFB\u52D9 worktree");
5376
5577
  if (!Array.isArray(p.attachments)) invalid("attachments must be an array");
5377
- return writeAttachments(task.worktreePath, p.attachments);
5578
+ const inputs = p.attachments;
5579
+ if (task.worktreePath) return writeAttachments(task.worktreePath, inputs);
5580
+ if (!task.workspacePath) throw new Error("\u627E\u4E0D\u5230\u4EFB\u52D9 worktree");
5581
+ return writeAttachmentsTo(taskAttachmentStagingPath(task.workspacePath, task.id), inputs);
5378
5582
  },
5379
5583
  "chat:load": (taskId) => loadConversation(requireTask(taskId).id),
5380
5584
  "chat:cancel": (taskId) => {
@@ -5875,12 +6079,12 @@ function connectToHost(lock) {
5875
6079
  // packages/cli/src/doctor.ts
5876
6080
  import fs19 from "fs";
5877
6081
  import path23 from "path";
5878
- import { execFile as execFile7 } from "child_process";
6082
+ import { execFile as execFile8 } from "child_process";
5879
6083
  import { promisify as promisify5 } from "util";
5880
6084
  init_env();
5881
6085
  init_git_bash();
5882
6086
  init_tmux_backend();
5883
- var pexec5 = promisify5(execFile7);
6087
+ var pexec5 = promisify5(execFile8);
5884
6088
  function parseVersion(text) {
5885
6089
  const m = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(text);
5886
6090
  return m ? [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)] : [];
@@ -5892,7 +6096,7 @@ function versionAtLeast(have, want) {
5892
6096
  }
5893
6097
  return true;
5894
6098
  }
5895
- async function run(cmd, args) {
6099
+ async function run2(cmd, args) {
5896
6100
  try {
5897
6101
  const { stdout } = await pexec5(cmd, args, { env: execEnv(), timeout: 1e4, windowsHide: true });
5898
6102
  return String(stdout).trim();
@@ -5952,7 +6156,7 @@ async function runDoctor(options) {
5952
6156
  checks.push(
5953
6157
  versionAtLeast(nodeVersion, [22, 0]) ? { id: "node", name: "Node.js", level: "ok", detail: `v${process.versions.node}` } : { id: "node", name: "Node.js", level: "fail", detail: `v${process.versions.node}\uFF0C\u9700\u8981 22 \u4EE5\u4E0A`, fix: installHint("node") }
5954
6158
  );
5955
- const git2 = await run("git", ["--version"]);
6159
+ const git2 = await run2("git", ["--version"]);
5956
6160
  if (!git2) {
5957
6161
  checks.push({ id: "git", name: "Git", level: "fail", detail: "PATH \u4E2D\u627E\u4E0D\u5230 git", fix: installHint("git") });
5958
6162
  } else {
@@ -5980,7 +6184,7 @@ async function runDoctor(options) {
5980
6184
  fix: "\u8981\u8B93\u4EFB\u52D9\u5728\u80CC\u666F\u6301\u7E8C\u57F7\u884C\uFF0C\u8ACB\u5728 WSL \u5167\u4F7F\u7528 vibeflow"
5981
6185
  });
5982
6186
  } else {
5983
- const tmux = await run("tmux", ["-V"]);
6187
+ const tmux = await run2("tmux", ["-V"]);
5984
6188
  checks.push(
5985
6189
  tmux && hasTmux() ? { id: "tmux", name: "tmux", level: "ok", detail: `${tmux}\uFF08\u4EFB\u52D9\u53EF\u5728\u80CC\u666F\u6301\u7E8C\u57F7\u884C\uFF09` } : {
5986
6190
  id: "tmux",