kandown 0.7.5 → 0.9.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/bin/tui.js CHANGED
@@ -54438,7 +54438,7 @@ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
54438
54438
  const configured = columnNames.map((name) => ({ name, tasks: [] }));
54439
54439
  for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
54440
54440
  const unknownColumns = [];
54441
- const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
54441
+ const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).filter((task) => !isArchived(task)).sort((a, b) => {
54442
54442
  const byOrder = taskOrder(a) - taskOrder(b);
54443
54443
  if (byOrder !== 0) return byOrder;
54444
54444
  return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
@@ -54455,6 +54455,9 @@ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
54455
54455
  }
54456
54456
  return [...unknownColumns, ...configured];
54457
54457
  }
54458
+ function isArchived(task) {
54459
+ return String(task.frontmatter.archived) === "true";
54460
+ }
54458
54461
  function extractSubtasks(body) {
54459
54462
  const subtasks = [];
54460
54463
  if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
@@ -54488,6 +54491,16 @@ function extractSubtasks(body) {
54488
54491
  subtasks[subtasks.length - 1].report = reportMatch[1];
54489
54492
  continue;
54490
54493
  }
54494
+ const legacyDescMatch = line.match(/^\s+description:\s*(.+)$/);
54495
+ if (legacyDescMatch && inSubtaskSection && subtasks.length > 0) {
54496
+ subtasks[subtasks.length - 1].description = legacyDescMatch[1].trim();
54497
+ continue;
54498
+ }
54499
+ const legacyReportMatch = line.match(/^\s+report:\s*(.+)$/);
54500
+ if (legacyReportMatch && inSubtaskSection && subtasks.length > 0) {
54501
+ subtasks[subtasks.length - 1].report = legacyReportMatch[1].trim();
54502
+ continue;
54503
+ }
54491
54504
  kept.push(line);
54492
54505
  }
54493
54506
  return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
@@ -54504,7 +54517,7 @@ function serializeTaskFile(frontmatter, body) {
54504
54517
  lines.push(`${k}: [${v.join(", ")}]`);
54505
54518
  } else if (typeof v === "string" && v.includes("\n")) {
54506
54519
  lines.push(`${k}: |`);
54507
- lines.push(...v.split("\n").map((line) => ` ${line}`));
54520
+ lines.push(...v.split("\n").map((line) => line === "" ? "" : ` ${line}`));
54508
54521
  } else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
54509
54522
  lines.push(`${k}: ${v}`);
54510
54523
  }
@@ -54589,10 +54602,137 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
54589
54602
  return true;
54590
54603
  }
54591
54604
 
54605
+ // src/cli/lib/daemon.ts
54606
+ import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
54607
+ import { dirname as dirname2, join as join3 } from "path";
54608
+ import { spawn } from "child_process";
54609
+ function metadataPath(kandownDir) {
54610
+ return join3(kandownDir, "daemon.json");
54611
+ }
54612
+ function isRecord(value) {
54613
+ return typeof value === "object" && value !== null;
54614
+ }
54615
+ function parseMetadata(value) {
54616
+ if (!isRecord(value)) return null;
54617
+ const { pid, port, url, kandownDir, startedAt, version } = value;
54618
+ if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
54619
+ if (typeof port !== "number" || !Number.isInteger(port)) return null;
54620
+ if (typeof url !== "string" || typeof kandownDir !== "string") return null;
54621
+ if (typeof startedAt !== "string") return null;
54622
+ if (version !== null && typeof version !== "string" && version !== void 0) return null;
54623
+ return { pid, port, url, kandownDir, startedAt, version: version ?? null };
54624
+ }
54625
+ function readDaemonMetadata(kandownDir) {
54626
+ const path = metadataPath(kandownDir);
54627
+ if (!existsSync4(path)) return null;
54628
+ try {
54629
+ return parseMetadata(JSON.parse(readFileSync4(path, "utf8")));
54630
+ } catch {
54631
+ return null;
54632
+ }
54633
+ }
54634
+ function removeDaemonMetadata(kandownDir) {
54635
+ try {
54636
+ const path = metadataPath(kandownDir);
54637
+ if (existsSync4(path)) unlinkSync(path);
54638
+ } catch {
54639
+ }
54640
+ }
54641
+ function isProcessAlive(pid) {
54642
+ try {
54643
+ process.kill(pid, 0);
54644
+ return true;
54645
+ } catch {
54646
+ return false;
54647
+ }
54648
+ }
54649
+ function parseRemoteDaemonInfo(value) {
54650
+ if (!isRecord(value)) return null;
54651
+ const { ok, pid, kandownDir } = value;
54652
+ if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
54653
+ return { ok, pid, kandownDir };
54654
+ }
54655
+ async function fetchDaemonInfo(port) {
54656
+ try {
54657
+ const response = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
54658
+ signal: AbortSignal.timeout(700)
54659
+ });
54660
+ if (!response.ok) return null;
54661
+ return parseRemoteDaemonInfo(await response.json());
54662
+ } catch {
54663
+ return null;
54664
+ }
54665
+ }
54666
+ async function getDaemonStatus(kandownDir) {
54667
+ const metadata = readDaemonMetadata(kandownDir);
54668
+ if (!metadata) return { running: false, metadata: null };
54669
+ if (!isProcessAlive(metadata.pid)) {
54670
+ removeDaemonMetadata(kandownDir);
54671
+ return { running: false, metadata: null };
54672
+ }
54673
+ const remote = await fetchDaemonInfo(metadata.port);
54674
+ if (!remote || remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
54675
+ removeDaemonMetadata(kandownDir);
54676
+ return { running: false, metadata: null };
54677
+ }
54678
+ return { running: true, metadata };
54679
+ }
54680
+ async function waitForDaemon(kandownDir, timeoutMs = 5e3) {
54681
+ const started = Date.now();
54682
+ while (Date.now() - started < timeoutMs) {
54683
+ const status = await getDaemonStatus(kandownDir);
54684
+ if (status.running) return status;
54685
+ await new Promise((resolve3) => setTimeout(resolve3, 150));
54686
+ }
54687
+ return { running: false, metadata: null };
54688
+ }
54689
+ async function startProjectDaemon(kandownDir, preferredPort) {
54690
+ const current = await getDaemonStatus(kandownDir);
54691
+ if (current.running) return current;
54692
+ const cliPath = process.argv[1];
54693
+ if (!cliPath) throw new Error("Cannot locate kandown CLI entrypoint");
54694
+ const args = [
54695
+ cliPath,
54696
+ "--no-update-check",
54697
+ "daemon",
54698
+ "run",
54699
+ "--path",
54700
+ kandownDir
54701
+ ];
54702
+ if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
54703
+ args.push("--port", String(preferredPort));
54704
+ }
54705
+ const child = spawn(process.execPath, args, {
54706
+ cwd: dirname2(kandownDir),
54707
+ detached: true,
54708
+ stdio: "ignore",
54709
+ env: { ...process.env, KANDOWN_DAEMON: "1" }
54710
+ });
54711
+ child.unref();
54712
+ return waitForDaemon(kandownDir);
54713
+ }
54714
+ async function stopProjectDaemon(kandownDir) {
54715
+ const status = await getDaemonStatus(kandownDir);
54716
+ if (!status.running || !status.metadata) {
54717
+ removeDaemonMetadata(kandownDir);
54718
+ return false;
54719
+ }
54720
+ try {
54721
+ process.kill(status.metadata.pid, "SIGTERM");
54722
+ } catch {
54723
+ }
54724
+ const started = Date.now();
54725
+ while (Date.now() - started < 2500 && isProcessAlive(status.metadata.pid)) {
54726
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
54727
+ }
54728
+ removeDaemonMetadata(kandownDir);
54729
+ return true;
54730
+ }
54731
+
54592
54732
  // src/cli/lib/file-watcher.ts
54593
54733
  import { createReadStream, statSync } from "fs";
54594
54734
  import { createHash } from "crypto";
54595
- import { join as join5 } from "path";
54735
+ import { join as join6 } from "path";
54596
54736
 
54597
54737
  // node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/index.js
54598
54738
  import { stat as statcb } from "fs";
@@ -55323,9 +55463,9 @@ var NodeFsHandler = class {
55323
55463
  if (this.fsw.closed) {
55324
55464
  return;
55325
55465
  }
55326
- const dirname4 = sysPath.dirname(file);
55466
+ const dirname5 = sysPath.dirname(file);
55327
55467
  const basename3 = sysPath.basename(file);
55328
- const parent = this.fsw._getWatchedDir(dirname4);
55468
+ const parent = this.fsw._getWatchedDir(dirname5);
55329
55469
  let prevStats = stats;
55330
55470
  if (parent.has(basename3))
55331
55471
  return;
@@ -55352,7 +55492,7 @@ var NodeFsHandler = class {
55352
55492
  prevStats = newStats2;
55353
55493
  }
55354
55494
  } catch (error) {
55355
- this.fsw._remove(dirname4, basename3);
55495
+ this.fsw._remove(dirname5, basename3);
55356
55496
  }
55357
55497
  } else if (parent.has(basename3)) {
55358
55498
  const at = newStats.atimeMs;
@@ -56314,18 +56454,18 @@ var FileWatcher = class {
56314
56454
  * poll to catch any races or network-mounted file changes.
56315
56455
  */
56316
56456
  start(kandownDir) {
56317
- const tasksDir = join5(kandownDir, "tasks");
56318
- const configPath = join5(kandownDir, "kandown.json");
56457
+ const tasksDir = join6(kandownDir, "tasks");
56458
+ const configPath = join6(kandownDir, "kandown.json");
56319
56459
  const existingIds = listTaskIds(kandownDir);
56320
56460
  for (const id of existingIds) {
56321
56461
  this.knownTaskIds.add(id);
56322
56462
  try {
56323
- const filePath = join5(tasksDir, `${id}.md`);
56463
+ const filePath = join6(tasksDir, `${id}.md`);
56324
56464
  this.taskHashes.set(id, hashFileSync(filePath));
56325
56465
  } catch {
56326
56466
  }
56327
56467
  }
56328
- this.watcher = watch([join5(tasksDir, "*.md"), configPath], {
56468
+ this.watcher = watch([join6(tasksDir, "*.md"), configPath], {
56329
56469
  ignoreInitial: true,
56330
56470
  awaitWriteFinish: { stabilityThreshold: 25, pollInterval: 25 },
56331
56471
  alwaysStat: true
@@ -56373,8 +56513,8 @@ var FileWatcher = class {
56373
56513
  // ─── Private ───────────────────────────────────────────────────────────────
56374
56514
  handleFsEvent(event, filePath, kandownDir) {
56375
56515
  if (this.stopped) return;
56376
- const tasksDir = join5(kandownDir, "tasks");
56377
- const configPath = join5(kandownDir, "kandown.json");
56516
+ const tasksDir = join6(kandownDir, "tasks");
56517
+ const configPath = join6(kandownDir, "kandown.json");
56378
56518
  if (filePath === configPath) {
56379
56519
  const key = `config:${event}`;
56380
56520
  const existing = this.debounceTimers.get(key);
@@ -56426,14 +56566,14 @@ var FileWatcher = class {
56426
56566
  /** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
56427
56567
  async pollHashes(kandownDir) {
56428
56568
  if (this.stopped) return;
56429
- const tasksDir = join5(kandownDir, "tasks");
56430
- const configPath = join5(kandownDir, "kandown.json");
56569
+ const tasksDir = join6(kandownDir, "tasks");
56570
+ const configPath = join6(kandownDir, "kandown.json");
56431
56571
  try {
56432
56572
  const newHash = hashFileSync(configPath);
56433
56573
  } catch {
56434
56574
  }
56435
56575
  for (const taskId of this.knownTaskIds) {
56436
- const filePath = join5(tasksDir, `${taskId}.md`);
56576
+ const filePath = join6(tasksDir, `${taskId}.md`);
56437
56577
  try {
56438
56578
  statSync(filePath);
56439
56579
  const newHash = await hashFile(filePath);
@@ -56450,7 +56590,7 @@ var FileWatcher = class {
56450
56590
  const currentIds = listTaskIds(kandownDir);
56451
56591
  for (const id of currentIds) {
56452
56592
  if (!this.knownTaskIds.has(id)) {
56453
- const filePath = join5(tasksDir, `${id}.md`);
56593
+ const filePath = join6(tasksDir, `${id}.md`);
56454
56594
  try {
56455
56595
  const newHash = await hashFile(filePath);
56456
56596
  this.knownTaskIds.add(id);
@@ -56620,9 +56760,9 @@ function buildPrompt(agentDoc, taskContent, taskId, kandownDir) {
56620
56760
  }
56621
56761
 
56622
56762
  // src/cli/lib/launcher.ts
56623
- import { execSync, spawn } from "child_process";
56763
+ import { execSync, spawn as spawn2 } from "child_process";
56624
56764
  import { writeFileSync as writeFileSync3 } from "fs";
56625
- import { join as join6 } from "path";
56765
+ import { join as join7 } from "path";
56626
56766
  import { tmpdir } from "os";
56627
56767
  function isInTmux() {
56628
56768
  return !!process.env.TMUX;
@@ -56646,7 +56786,7 @@ function launchAgent(opts) {
56646
56786
  ].join("\n");
56647
56787
  const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
56648
56788
  moveTaskToColumn(kandownDir, taskId, "In Progress");
56649
- const contextFile = join6(tmpdir(), `kandown-${taskId}-context.md`);
56789
+ const contextFile = join7(tmpdir(), `kandown-${taskId}-context.md`);
56650
56790
  writeFileSync3(contextFile, `${systemPrompt}
56651
56791
 
56652
56792
  ---
@@ -56664,7 +56804,7 @@ ${taskPrompt}`, "utf8");
56664
56804
  });
56665
56805
  } else {
56666
56806
  onBeforeExec?.();
56667
- const child = spawn(binary, args, {
56807
+ const child = spawn2(binary, args, {
56668
56808
  stdio: "inherit",
56669
56809
  env: {
56670
56810
  ...process.env,
@@ -56864,6 +57004,15 @@ var RE_HEADER = /^#{1,3}\s/;
56864
57004
  var RE_SUBTASK = /^\s*-\s+\[([ xX])\]/;
56865
57005
  var RE_DONE = /^\s*-\s+\[x\]/i;
56866
57006
  var RE_BRACKET_TAG = /^\[([^\]]+)\]\s*/;
57007
+ function columnAccentColor(name) {
57008
+ const normalized = name.toLowerCase();
57009
+ if (/backlog/.test(normalized)) return "magenta";
57010
+ if (/todo/.test(normalized)) return "cyan";
57011
+ if (/progress|doing/.test(normalized)) return "yellow";
57012
+ if (/review/.test(normalized)) return "blue";
57013
+ if (/done|archive|closed|complete/.test(normalized)) return "green";
57014
+ return "cyan";
57015
+ }
56867
57016
  function TaskRow({ task, focused, dragging, colWidth }) {
56868
57017
  const cursor = dragging ? "\u2195" : focused ? "\u25B8" : " ";
56869
57018
  const check2 = task.checked ? "\u2713" : "\u25CB";
@@ -56920,8 +57069,9 @@ function KanbanColumn({
56920
57069
  isMoveFocused,
56921
57070
  draggedTaskId
56922
57071
  }) {
56923
- const headerBg = isFocused ? "cyan" : void 0;
56924
- const headerColor = isFocused ? "black" : "cyan";
57072
+ const accent = columnAccentColor(name);
57073
+ const headerBg = isFocused ? accent : void 0;
57074
+ const headerColor = isFocused ? "black" : accent;
56925
57075
  const countStr = tasks.length > 0 ? ` (${tasks.length})` : "";
56926
57076
  const rows = [];
56927
57077
  tasks.forEach((task, idx) => {
@@ -56960,31 +57110,45 @@ function KanbanColumn({
56960
57110
  showMoveTarget && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MovePlaceholder, { name, focused: !!isMoveFocused, colWidth })
56961
57111
  ] });
56962
57112
  }
56963
- function BoardHeader({ title, inTmux, modeHint, version }) {
57113
+ function BoardHeader({ title, inTmux, modeHint, version, daemonStatus, daemonBusy }) {
56964
57114
  const tmuxHint = inTmux ? " tmux" : "";
56965
57115
  const versionTag = version ? ` v${version}` : "";
56966
- const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 Enter detail \xB7 m menu \xB7 a agent \xB7 r reload \xB7 q quit";
57116
+ const daemonLabel = daemonBusy ? "\u25CC daemon\u2026" : daemonStatus.running ? `\u25CF web ${daemonStatus.metadata?.port ?? ""}` : "\u25CB web off";
57117
+ const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 d daemon \xB7 r reload \xB7 q quit";
56967
57118
  const width = termWidth();
56968
- const leftWidth = Math.min(Math.max(28, Math.floor(width * 0.42)), width);
56969
- const rightWidth = Math.max(0, width - leftWidth);
56970
- const left = pad(` KANDOWN${tmuxHint}${versionTag} ${title}`, leftWidth);
57119
+ const leftWidth = Math.min(Math.max(34, Math.floor(width * 0.46)), width);
57120
+ const daemonWidth = Math.min(16, Math.max(0, width - leftWidth));
57121
+ const rightWidth = Math.max(0, width - leftWidth - daemonWidth);
57122
+ const left = pad(` \u25C6 KANDOWN${tmuxHint}${versionTag} ${title}`, leftWidth);
57123
+ const daemon = pad(daemonLabel, daemonWidth);
56971
57124
  const right = truncate(hint, rightWidth).padStart(rightWidth, " ");
56972
57125
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, children: [
56973
57126
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { bold: true, color: "cyan", children: left }),
57127
+ daemonWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: daemonStatus.running ? "green" : "yellow", bold: true, children: daemon }),
56974
57128
  rightWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", dimColor: true, children: right })
56975
57129
  ] });
56976
57130
  }
56977
- function StatusBar({ message, task }) {
56978
- if (message) {
56979
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: message }) });
56980
- }
56981
- if (!task) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " " }) });
56982
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", children: [
56983
- task.id,
56984
- task.progress ? ` (${task.progress.done}/${task.progress.total})` : "",
56985
- " ",
56986
- task.checked ? "\u2713 done" : "\u25CB open"
56987
- ] }) });
57131
+ function StatusBar({ message, task, daemonStatus }) {
57132
+ const daemonText = daemonStatus.running && daemonStatus.metadata ? `web daemon ON \xB7 ${daemonStatus.metadata.url}` : "web daemon OFF \xB7 press d to start";
57133
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginTop: 1, flexDirection: "column", children: [
57134
+ message ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", bold: true, children: [
57135
+ " \u2726 ",
57136
+ message
57137
+ ] }) : task ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", children: [
57138
+ " ",
57139
+ "\u25C6 ",
57140
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: task.id }),
57141
+ task.progress ? ` checklist ${task.progress.done}/${task.progress.total}` : "",
57142
+ " ",
57143
+ task.checked ? "\u2713 done" : "\u25CB open"
57144
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " " }),
57145
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: daemonStatus.running ? "green" : "yellow", dimColor: !daemonStatus.running, children: [
57146
+ " ",
57147
+ daemonStatus.running ? "\u25CF" : "\u25CB",
57148
+ " ",
57149
+ daemonText
57150
+ ] })
57151
+ ] });
56988
57152
  }
56989
57153
  function TaskDetail({ task, taskId, scrollOffset }) {
56990
57154
  const fm = task.frontmatter;
@@ -57049,6 +57213,9 @@ function Board({ kandownDir, version }) {
57049
57213
  const [moveTargetCol, setMoveTargetCol] = (0, import_react37.useState)(0);
57050
57214
  const [mousePress, setMousePress] = (0, import_react37.useState)(null);
57051
57215
  const [taskDrag, setTaskDrag] = (0, import_react37.useState)(null);
57216
+ const [daemonStatus, setDaemonStatus] = (0, import_react37.useState)({ running: false, metadata: null });
57217
+ const [daemonBusy, setDaemonBusy] = (0, import_react37.useState)(false);
57218
+ const [preferredDaemonPort, setPreferredDaemonPort] = (0, import_react37.useState)(null);
57052
57219
  const [installedAgents, setInstalledAgents] = (0, import_react37.useState)([]);
57053
57220
  const inTmux = isInTmux();
57054
57221
  const layoutRef = (0, import_react37.useRef)({
@@ -57122,6 +57289,42 @@ function Board({ kandownDir, version }) {
57122
57289
  setStatusMsg("Board reloaded");
57123
57290
  setTimeout(() => setStatusMsg(""), 1500);
57124
57291
  }, [kandownDir, updateLayout]);
57292
+ const refreshDaemonStatus = (0, import_react37.useCallback)(async () => {
57293
+ const next = await getDaemonStatus(kandownDir);
57294
+ setDaemonStatus(next);
57295
+ if (next.running && next.metadata) setPreferredDaemonPort(next.metadata.port);
57296
+ }, [kandownDir]);
57297
+ (0, import_react37.useEffect)(() => {
57298
+ void refreshDaemonStatus();
57299
+ const timer = setInterval(() => {
57300
+ void refreshDaemonStatus();
57301
+ }, 2e3);
57302
+ return () => clearInterval(timer);
57303
+ }, [refreshDaemonStatus]);
57304
+ const toggleDaemon = (0, import_react37.useCallback)(async () => {
57305
+ if (daemonBusy) return;
57306
+ setDaemonBusy(true);
57307
+ try {
57308
+ const current = await getDaemonStatus(kandownDir);
57309
+ if (current.running) {
57310
+ if (current.metadata) setPreferredDaemonPort(current.metadata.port);
57311
+ await stopProjectDaemon(kandownDir);
57312
+ const next = await getDaemonStatus(kandownDir);
57313
+ setDaemonStatus(next);
57314
+ setStatusMsg("Web daemon stopped");
57315
+ } else {
57316
+ const next = await startProjectDaemon(kandownDir, preferredDaemonPort);
57317
+ setDaemonStatus(next);
57318
+ if (next.running && next.metadata) setPreferredDaemonPort(next.metadata.port);
57319
+ setStatusMsg(next.running ? "Web daemon started" : "Web daemon failed to start");
57320
+ }
57321
+ } catch (error) {
57322
+ setStatusMsg(`Daemon error: ${error instanceof Error ? error.message : String(error)}`);
57323
+ } finally {
57324
+ setDaemonBusy(false);
57325
+ setTimeout(() => setStatusMsg(""), 2500);
57326
+ }
57327
+ }, [daemonBusy, kandownDir, preferredDaemonPort]);
57125
57328
  const getFocusedTask = (0, import_react37.useCallback)(() => {
57126
57329
  if (!board) return null;
57127
57330
  const col = board.columns[colIndex];
@@ -57370,6 +57573,10 @@ function Board({ kandownDir, version }) {
57370
57573
  reloadBoard();
57371
57574
  return;
57372
57575
  }
57576
+ if (input === "d") {
57577
+ void toggleDaemon();
57578
+ return;
57579
+ }
57373
57580
  if (input === "l" || key.rightArrow) {
57374
57581
  const maxCol = (board?.columns.length ?? 1) - 1;
57375
57582
  setColIndex((c) => Math.min(c + 1, maxCol));
@@ -57548,7 +57755,7 @@ function Board({ kandownDir, version }) {
57548
57755
  if (mode === "agent-picker") {
57549
57756
  const taskId = detailTaskId || focusedTask?.id || "";
57550
57757
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
57551
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, version }),
57758
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, version, daemonStatus, daemonBusy }),
57552
57759
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
57553
57760
  AgentPicker,
57554
57761
  {
@@ -57574,7 +57781,7 @@ function Board({ kandownDir, version }) {
57574
57781
  ] });
57575
57782
  }
57576
57783
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
57577
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, modeHint, version }),
57784
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, modeHint, version, daemonStatus, daemonBusy }),
57578
57785
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { flexDirection: "row", children: board.columns.map((col, cIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
57579
57786
  KanbanColumn,
57580
57787
  {
@@ -57598,7 +57805,7 @@ function Board({ kandownDir, version }) {
57598
57805
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: mode === "dragging" ? "release over a column" : "click \u2193" }),
57599
57806
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: mode === "dragging" ? " \xB7 Esc cancel" : " or \u2190/\u2192 + Enter" })
57600
57807
  ] }) }),
57601
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBar, { message: statusMsg, task: focusedTask })
57808
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBar, { message: statusMsg, task: focusedTask, daemonStatus })
57602
57809
  ] });
57603
57810
  }
57604
57811