kandown 0.14.0 → 0.15.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/kandown.js CHANGED
@@ -322,18 +322,43 @@ ${c.bold}Examples:${c.reset}
322
322
  `);
323
323
  }
324
324
 
325
- function copyRecursive(src, dest) {
326
- if (!existsSync(src)) return;
327
- mkdirSync(dest, { recursive: true });
328
- for (const entry of readdirSync(src)) {
325
+ /**
326
+ * 📖 Resilient recursive copy used by `kandown init` (t113).
327
+ * Returns the list of per-entry error messages; an empty array means a fully
328
+ * clean copy. A partially-failed init no longer leaves the user with no clue
329
+ * about what went wrong — the caller reports each failure.
330
+ */
331
+ function copyRecursive(src, dest, errors = []) {
332
+ if (!existsSync(src)) return errors;
333
+ try {
334
+ mkdirSync(dest, { recursive: true });
335
+ } catch (e) {
336
+ errors.push(`Failed to create directory ${dest}: ${e.message}`);
337
+ return errors;
338
+ }
339
+ let entries;
340
+ try {
341
+ entries = readdirSync(src);
342
+ } catch (e) {
343
+ errors.push(`Failed to read ${src}: ${e.message}`);
344
+ return errors;
345
+ }
346
+ for (const entry of entries) {
329
347
  const srcPath = join(src, entry);
330
348
  const destPath = join(dest, entry);
331
- if (statSync(srcPath).isDirectory()) {
332
- copyRecursive(srcPath, destPath);
333
- } else {
334
- copyFileSync(srcPath, destPath);
349
+ try {
350
+ if (statSync(srcPath).isDirectory()) {
351
+ copyRecursive(srcPath, destPath, errors);
352
+ } else {
353
+ copyFileSync(srcPath, destPath);
354
+ }
355
+ } catch (e) {
356
+ // 📖 One unreadable / unwritable file shouldn't abort the whole init —
357
+ // record the error and keep copying the rest (t113).
358
+ errors.push(`Failed to copy ${srcPath}: ${e.message}`);
335
359
  }
336
360
  }
361
+ return errors;
337
362
  }
338
363
 
339
364
  function findAgentsFile(cwd) {
@@ -348,7 +373,15 @@ function findAgentsFile(cwd) {
348
373
  function appendAgentReference(cwd, agentsFile, kandownPath) {
349
374
  const filePath = join(cwd, agentsFile);
350
375
  const marker = '<!-- kandown:agent-ref -->';
351
- const existing = readFileSync(filePath, 'utf8');
376
+ let existing;
377
+ try {
378
+ existing = readFileSync(filePath, 'utf8');
379
+ } catch (e) {
380
+ // 📖 TOCTOU: file existed at the check but vanished or became unreadable.
381
+ // Warn and skip rather than crashing init (t113).
382
+ warn(`Could not read ${agentsFile} (${e.message}); skipping agent reference.`);
383
+ return false;
384
+ }
352
385
 
353
386
  if (existing.includes(marker)) {
354
387
  info(`${agentsFile} already references the kandown (skipped)`);
@@ -368,8 +401,13 @@ This project uses a file-based kanban:
368
401
  - **Completion workflow:** set task frontmatter \`status: Done\` + write the completion report
369
402
  `;
370
403
 
371
- writeFileSync(filePath, existing + ref, 'utf8');
372
- return true;
404
+ try {
405
+ writeFileSync(filePath, existing + ref, 'utf8');
406
+ return true;
407
+ } catch (e) {
408
+ warn(`Could not append agent reference to ${agentsFile} (${e.message}).`);
409
+ return false;
410
+ }
373
411
  }
374
412
 
375
413
  function createAgentsFileIfMissing(cwd, kandownPath) {
@@ -586,7 +624,13 @@ function doInit(args, cwd, kandownPath, kandownDir) {
586
624
  const tasksDest = getTasksDir(kandownDir);
587
625
  if (!existsSync(tasksDest)) {
588
626
  mkdirSync(tasksDest, { recursive: true });
589
- copyRecursive(tasksSrc, tasksDest);
627
+ // 📖 copyRecursive now returns an errors[] array — report any partial
628
+ // failures instead of letting them crash the whole init (t113).
629
+ const copyErrors = copyRecursive(tasksSrc, tasksDest);
630
+ if (copyErrors.length > 0) {
631
+ warn(`Some task template files could not be copied:`);
632
+ for (const msg of copyErrors) warn(` - ${msg}`);
633
+ }
590
634
  success('./tasks/ (with welcome example)');
591
635
  } else {
592
636
  info('./tasks/ already exists (kept)');
@@ -1747,7 +1791,10 @@ function serveApp(res, kandownDir) {
1747
1791
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1748
1792
  res.end(injected);
1749
1793
  } catch (e) {
1750
- writeText(res, 500, `Failed to serve kandown.html: ${e.message}`);
1794
+ // 📖 Don't leak internal paths / error details to HTTP clients — log the
1795
+ // full error server-side, send a generic message to the browser (t113).
1796
+ console.error(`[serve] Error serving ${htmlPath}:`, e);
1797
+ writeText(res, 500, 'Internal server error — check the terminal where kandown is running.');
1751
1798
  }
1752
1799
  }
1753
1800
 
@@ -1837,7 +1884,10 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
1837
1884
  await listen(server, p);
1838
1885
  return { server, port: p };
1839
1886
  } catch (e) {
1840
- if (e.code !== 'EADDRINUSE') throw e;
1887
+ // 📖 EADDRINUSE → try the next port. EACCES (privileged port / blocked
1888
+ // by OS) → also try the next port instead of crashing (t113). Anything
1889
+ // else is unexpected and bubbles up.
1890
+ if (e.code !== 'EADDRINUSE' && e.code !== 'EACCES') throw e;
1841
1891
  }
1842
1892
  }
1843
1893
 
package/bin/tui.js CHANGED
@@ -54017,24 +54017,37 @@ var DEFAULT_CONFIG = {
54017
54017
  function loadConfig(kandownDir) {
54018
54018
  const configPath = join(kandownDir, "kandown.json");
54019
54019
  if (!existsSync2(configPath)) return structuredClone(DEFAULT_CONFIG);
54020
+ let raw;
54020
54021
  try {
54021
- const raw = JSON.parse(readFileSync2(configPath, "utf8"));
54022
- const merged = {
54023
- ui: { ...DEFAULT_CONFIG.ui, ...raw.ui },
54024
- agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
54025
- board: {
54026
- ...DEFAULT_CONFIG.board,
54027
- ...raw.board,
54028
- columns: Array.isArray(raw.board?.columns) && raw.board.columns.length > 0 ? raw.board.columns.filter((name) => typeof name === "string" && name.trim().length > 0) : DEFAULT_CONFIG.board.columns
54029
- },
54030
- fields: { ...DEFAULT_CONFIG.fields, ...raw.fields },
54031
- notifications: { ...DEFAULT_CONFIG.notifications, ...raw.notifications }
54032
- };
54033
- if (raw.agents) merged.agents = raw.agents;
54034
- return merged;
54035
- } catch {
54022
+ raw = JSON.parse(readFileSync2(configPath, "utf8"));
54023
+ } catch (e) {
54024
+ const err = e;
54025
+ if (err.code === "ENOENT") return structuredClone(DEFAULT_CONFIG);
54026
+ console.warn(`[kandown] kandown.json is corrupted, using defaults: ${e.message}`);
54027
+ return structuredClone(DEFAULT_CONFIG);
54028
+ }
54029
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
54030
+ console.warn("[kandown] kandown.json must be a JSON object, using defaults.");
54036
54031
  return structuredClone(DEFAULT_CONFIG);
54037
54032
  }
54033
+ const obj = raw;
54034
+ const safeObj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
54035
+ const boardRaw = safeObj(obj.board);
54036
+ const merged = {
54037
+ ui: { ...DEFAULT_CONFIG.ui, ...safeObj(obj.ui) },
54038
+ agent: { ...DEFAULT_CONFIG.agent, ...safeObj(obj.agent) },
54039
+ board: {
54040
+ ...DEFAULT_CONFIG.board,
54041
+ ...boardRaw,
54042
+ columns: Array.isArray(boardRaw.columns) && boardRaw.columns.length > 0 ? boardRaw.columns.filter((name) => typeof name === "string" && name.trim().length > 0) : DEFAULT_CONFIG.board.columns
54043
+ },
54044
+ fields: { ...DEFAULT_CONFIG.fields, ...safeObj(obj.fields) },
54045
+ notifications: { ...DEFAULT_CONFIG.notifications, ...safeObj(obj.notifications) }
54046
+ };
54047
+ if (obj.agents && typeof obj.agents === "object") {
54048
+ merged.agents = obj.agents;
54049
+ }
54050
+ return merged;
54038
54051
  }
54039
54052
  function saveConfig(kandownDir, config) {
54040
54053
  const configPath = join(kandownDir, "kandown.json");
@@ -54172,8 +54185,12 @@ function Settings({ kandownDir, version }) {
54172
54185
  const persistConfig = (0, import_react34.useCallback)(
54173
54186
  (newConfig) => {
54174
54187
  setConfig(newConfig);
54175
- saveConfig(kandownDir, newConfig);
54176
- setSavedAt(Date.now());
54188
+ try {
54189
+ saveConfig(kandownDir, newConfig);
54190
+ setSavedAt(Date.now());
54191
+ } catch (e) {
54192
+ console.error("[kandown] Failed to save config:", e);
54193
+ }
54177
54194
  },
54178
54195
  [kandownDir]
54179
54196
  );
@@ -54555,17 +54572,23 @@ function listTaskIds(kandownDir) {
54555
54572
  }
54556
54573
  function readBoard(kandownDir) {
54557
54574
  const config = loadConfig(kandownDir);
54558
- const tasks = listTaskIds(kandownDir).map((id) => {
54559
- const task = readTask(kandownDir, id);
54560
- return {
54561
- ...task,
54562
- frontmatter: {
54563
- ...task.frontmatter,
54564
- id: task.frontmatter.id || id,
54565
- status: task.frontmatter.status || "Backlog"
54566
- }
54567
- };
54568
- });
54575
+ const ids = listTaskIds(kandownDir);
54576
+ const tasks = [];
54577
+ for (const id of ids) {
54578
+ try {
54579
+ const task = readTask(kandownDir, id);
54580
+ tasks.push({
54581
+ ...task,
54582
+ frontmatter: {
54583
+ ...task.frontmatter,
54584
+ id: task.frontmatter.id || id,
54585
+ status: task.frontmatter.status || "Backlog"
54586
+ }
54587
+ });
54588
+ } catch (e) {
54589
+ console.error(`[kandown] Failed to read task ${id}:`, e.message);
54590
+ }
54591
+ }
54569
54592
  return {
54570
54593
  frontmatter: null,
54571
54594
  title: "Project Kanban",
@@ -54598,8 +54621,11 @@ function readAgentDoc(kandownDir) {
54598
54621
  join2(kandownDir, "AGENT.md")
54599
54622
  ];
54600
54623
  for (const candidate of candidates) {
54601
- if (existsSync3(candidate)) {
54624
+ if (!existsSync3(candidate)) continue;
54625
+ try {
54602
54626
  return readFileSync3(candidate, "utf8");
54627
+ } catch (e) {
54628
+ console.warn(`[kandown] Could not read ${candidate}:`, e.message);
54603
54629
  }
54604
54630
  }
54605
54631
  return "";
@@ -54607,13 +54633,18 @@ function readAgentDoc(kandownDir) {
54607
54633
  function moveTaskToColumn(kandownDir, taskId, targetColumn) {
54608
54634
  const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
54609
54635
  if (!existsSync3(taskPath)) return false;
54610
- const parsed = readTask(kandownDir, taskId);
54611
- writeFileSync2(taskPath, serializeTaskFile({
54612
- ...parsed.frontmatter,
54613
- id: taskId,
54614
- status: targetColumn
54615
- }, parsed.body), "utf8");
54616
- return true;
54636
+ try {
54637
+ const parsed = readTask(kandownDir, taskId);
54638
+ writeFileSync2(taskPath, serializeTaskFile({
54639
+ ...parsed.frontmatter,
54640
+ id: taskId,
54641
+ status: targetColumn
54642
+ }, parsed.body), "utf8");
54643
+ return true;
54644
+ } catch (e) {
54645
+ console.error(`[kandown] Failed to move task ${taskId} to ${targetColumn}:`, e.message);
54646
+ return false;
54647
+ }
54617
54648
  }
54618
54649
 
54619
54650
  // src/cli/lib/daemon.ts
@@ -56795,7 +56826,13 @@ function launchAgent(opts) {
56795
56826
  if (!agentDef) {
56796
56827
  throw new Error(`Unknown agent: ${agentId}`);
56797
56828
  }
56798
- const task = readTask(kandownDir, taskId);
56829
+ let task;
56830
+ try {
56831
+ task = readTask(kandownDir, taskId);
56832
+ } catch (e) {
56833
+ throw new Error(`Failed to read task ${taskId}: ${e.message}`);
56834
+ }
56835
+ const originalStatus = task.frontmatter.status || "Backlog";
56799
56836
  const agentDoc = readAgentDoc(kandownDir);
56800
56837
  const taskFileContent = [
56801
56838
  `---`,
@@ -56807,16 +56844,24 @@ function launchAgent(opts) {
56807
56844
  task.body.trim()
56808
56845
  ].join("\n");
56809
56846
  const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
56810
- moveTaskToColumn(kandownDir, taskId, "In Progress");
56847
+ const taskMoved = moveTaskToColumn(kandownDir, taskId, "In Progress");
56848
+ if (!taskMoved) {
56849
+ throw new Error(`Could not move task ${taskId} to In Progress \u2014 task file missing or unwritable.`);
56850
+ }
56811
56851
  const contextFile = join7(tmpdir(), `kandown-${taskId}-context.md`);
56812
- writeFileSync3(contextFile, `${systemPrompt}
56852
+ try {
56853
+ writeFileSync3(contextFile, `${systemPrompt}
56813
56854
 
56814
56855
  ---
56815
56856
 
56816
56857
  ${taskPrompt}`, "utf8");
56858
+ } catch (e) {
56859
+ console.warn(`[kandown] Failed to write context file (${e.message}); launching anyway.`);
56860
+ }
56817
56861
  const launchOpts = { systemPrompt, taskPrompt, kandownDir, taskId };
56818
56862
  const [binary, ...args] = agentDef.buildCommand(launchOpts);
56819
56863
  if (!binary) {
56864
+ rollbackTaskStatus(kandownDir, taskId, originalStatus);
56820
56865
  throw new Error(`Agent ${agentId} returned an empty command`);
56821
56866
  }
56822
56867
  if (isInTmux()) {
@@ -56826,24 +56871,47 @@ ${taskPrompt}`, "utf8");
56826
56871
  `KANDOWN_TASK_ID=${shellescape(taskId)}`,
56827
56872
  `KANDOWN_DIR=${shellescape(kandownDir)}`
56828
56873
  ].join(" ");
56829
- execSync(`tmux split-window -h -p 50 ${shellescape(`env ${envPrefix} ${shellCmd}`)}`, {
56830
- stdio: "inherit"
56831
- });
56874
+ try {
56875
+ execSync(`tmux split-window -h -p 50 ${shellescape(`env ${envPrefix} ${shellCmd}`)}`, {
56876
+ stdio: "inherit"
56877
+ });
56878
+ } catch (e) {
56879
+ rollbackTaskStatus(kandownDir, taskId, originalStatus);
56880
+ throw new Error(`Failed to open agent pane in tmux: ${e.message}. Is tmux installed and the session valid?`);
56881
+ }
56832
56882
  } else {
56833
- onBeforeExec?.();
56834
- const child = spawn2(binary, args, {
56835
- stdio: "inherit",
56836
- env: {
56837
- ...process.env,
56838
- // 📖 Expose the context file path so agents that support env vars can use it
56839
- KANDOWN_CONTEXT_FILE: contextFile,
56840
- KANDOWN_TASK_ID: taskId,
56841
- KANDOWN_DIR: kandownDir
56842
- }
56843
- });
56844
- child.on("exit", (code) => {
56845
- process.exit(code ?? 0);
56846
- });
56883
+ try {
56884
+ onBeforeExec?.();
56885
+ const child = spawn2(binary, args, {
56886
+ stdio: "inherit",
56887
+ env: {
56888
+ ...process.env,
56889
+ // 📖 Expose the context file path so agents that support env vars can use it
56890
+ KANDOWN_CONTEXT_FILE: contextFile,
56891
+ KANDOWN_TASK_ID: taskId,
56892
+ KANDOWN_DIR: kandownDir
56893
+ }
56894
+ });
56895
+ child.on("error", (e) => {
56896
+ rollbackTaskStatus(kandownDir, taskId, originalStatus);
56897
+ console.error(`[kandown] Failed to launch ${agentDef.name}: ${e.message}`);
56898
+ process.exit(1);
56899
+ });
56900
+ child.on("exit", (code) => {
56901
+ if (code === 0) process.exit(0);
56902
+ if (code === null) return;
56903
+ process.exit(code);
56904
+ });
56905
+ } catch (e) {
56906
+ rollbackTaskStatus(kandownDir, taskId, originalStatus);
56907
+ throw new Error(`Failed to launch ${agentDef.name}: ${e.message}`);
56908
+ }
56909
+ }
56910
+ }
56911
+ function rollbackTaskStatus(kandownDir, taskId, originalStatus) {
56912
+ const ok = moveTaskToColumn(kandownDir, taskId, originalStatus);
56913
+ if (!ok) {
56914
+ console.warn(`[kandown] Could not roll back task ${taskId} to ${originalStatus} \u2014 update it manually.`);
56847
56915
  }
56848
56916
  }
56849
56917
  function buildShellCmd(binary, args) {
@@ -56903,6 +56971,7 @@ function AgentPicker({ agents, taskId, onSelect, onCancel }) {
56903
56971
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "yellow", children: taskId })
56904
56972
  ] })
56905
56973
  ] }),
56974
+ agents.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "yellow", children: "No agents installed. Install claude, codex, or opencode." }),
56906
56975
  agents.map((agent, idx) => {
56907
56976
  const isFocused = idx === cursor;
56908
56977
  const numHint = idx < 9 ? `${idx + 1} ` : " ";
@@ -57280,6 +57349,7 @@ function Board({ kandownDir, version }) {
57280
57349
  const [rowIndex, setRowIndex] = (0, import_react37.useState)(0);
57281
57350
  const [mode, setMode] = (0, import_react37.useState)("browse");
57282
57351
  const [statusMsg, setStatusMsg] = (0, import_react37.useState)("");
57352
+ const [boardError, setBoardError] = (0, import_react37.useState)(null);
57283
57353
  const [detailTask, setDetailTask] = (0, import_react37.useState)(null);
57284
57354
  const [detailTaskId, setDetailTaskId] = (0, import_react37.useState)("");
57285
57355
  const [detailScroll, setDetailScroll] = (0, import_react37.useState)(0);
@@ -57328,43 +57398,49 @@ function Board({ kandownDir, version }) {
57328
57398
  if (!task) return null;
57329
57399
  return { taskId: task.id, colIndex: clickedCol, rowIndex: taskIdx, startX: x, startY: y };
57330
57400
  }, [board, columnAtX]);
57401
+ const loadBoardInto = (0, import_react37.useCallback)(() => {
57402
+ try {
57403
+ const loaded = readBoard(kandownDir);
57404
+ setBoard(loaded);
57405
+ updateLayout(loaded);
57406
+ setBoardError(null);
57407
+ return loaded;
57408
+ } catch (e) {
57409
+ const msg = e instanceof Error ? e.message : String(e);
57410
+ setBoardError(`Failed to load board: ${msg}`);
57411
+ setStatusMsg("");
57412
+ return null;
57413
+ }
57414
+ }, [kandownDir, updateLayout]);
57331
57415
  (0, import_react37.useEffect)(() => {
57332
- const loaded = readBoard(kandownDir);
57333
- setBoard(loaded);
57334
- updateLayout(loaded);
57416
+ loadBoardInto();
57335
57417
  setInstalledAgents(detectInstalledAgents());
57336
- }, [kandownDir, updateLayout]);
57418
+ }, [kandownDir, loadBoardInto]);
57337
57419
  (0, import_react37.useEffect)(() => {
57338
57420
  const watcher = createWatcher();
57339
57421
  watcher.on("taskChanged", () => {
57340
- const loaded = readBoard(kandownDir);
57341
- setBoard(loaded);
57342
- updateLayout(loaded);
57422
+ loadBoardInto();
57343
57423
  });
57344
57424
  watcher.on("newTaskDetected", (taskId) => {
57345
- const loaded = readBoard(kandownDir);
57346
- setBoard(loaded);
57347
- updateLayout(loaded);
57425
+ loadBoardInto();
57348
57426
  setStatusMsg(`New task: ${taskId}`);
57349
57427
  setTimeout(() => setStatusMsg(""), 2e3);
57350
57428
  });
57351
57429
  watcher.on("configChanged", () => {
57352
- const loaded = readBoard(kandownDir);
57353
- setBoard(loaded);
57354
- updateLayout(loaded);
57430
+ loadBoardInto();
57355
57431
  });
57356
57432
  watcher.start(kandownDir);
57357
57433
  return () => {
57358
57434
  watcher.stop();
57359
57435
  };
57360
- }, [kandownDir, updateLayout]);
57436
+ }, [kandownDir, loadBoardInto]);
57361
57437
  const reloadBoard = (0, import_react37.useCallback)(() => {
57362
- const loaded = readBoard(kandownDir);
57363
- setBoard(loaded);
57364
- updateLayout(loaded);
57365
- setStatusMsg("Board reloaded");
57366
- setTimeout(() => setStatusMsg(""), 1500);
57367
- }, [kandownDir, updateLayout]);
57438
+ const loaded = loadBoardInto();
57439
+ if (loaded) {
57440
+ setStatusMsg("Board reloaded");
57441
+ setTimeout(() => setStatusMsg(""), 1500);
57442
+ }
57443
+ }, [loadBoardInto]);
57368
57444
  const refreshDaemonStatus = (0, import_react37.useCallback)(async () => {
57369
57445
  const next = await getDaemonStatus(kandownDir);
57370
57446
  setDaemonStatus(next);
@@ -57463,11 +57539,16 @@ function Board({ kandownDir, version }) {
57463
57539
  return col.tasks[Math.min(rowIndex, col.tasks.length - 1)] ?? null;
57464
57540
  }, [board, colIndex, rowIndex]);
57465
57541
  const openDetail = (0, import_react37.useCallback)((taskId) => {
57466
- const task = readTask(kandownDir, taskId);
57467
- setDetailTask(task);
57468
- setDetailTaskId(taskId);
57469
- setDetailScroll(0);
57470
- setMode("detail");
57542
+ try {
57543
+ const task = readTask(kandownDir, taskId);
57544
+ setDetailTask(task);
57545
+ setDetailTaskId(taskId);
57546
+ setDetailScroll(0);
57547
+ setMode("detail");
57548
+ } catch (e) {
57549
+ setStatusMsg(`Error opening task: ${e instanceof Error ? e.message : String(e)}`);
57550
+ setTimeout(() => setStatusMsg(""), 4e3);
57551
+ }
57471
57552
  }, [kandownDir]);
57472
57553
  const closeContextMenu = (0, import_react37.useCallback)(() => {
57473
57554
  setCtxMenuRow(-1);
@@ -57602,9 +57683,7 @@ function Board({ kandownDir, version }) {
57602
57683
  return;
57603
57684
  }
57604
57685
  moveTaskToColumn(kandownDir, moveTaskId, targetColName);
57605
- const loaded = readBoard(kandownDir);
57606
- setBoard(loaded);
57607
- updateLayout(loaded);
57686
+ loadBoardInto();
57608
57687
  setStatusMsg(`Moved ${moveTaskId} \u2192 ${targetColName}`);
57609
57688
  setTimeout(() => setStatusMsg(""), 2e3);
57610
57689
  }
@@ -57616,7 +57695,7 @@ function Board({ kandownDir, version }) {
57616
57695
  setMode("browse");
57617
57696
  return;
57618
57697
  }
57619
- }, [board, mode, colIndex, rowIndex, ctxMenuRow, moveTaskId, kandownDir, updateLayout, openDetail, closeContextMenu]);
57698
+ }, [board, mode, colIndex, rowIndex, ctxMenuRow, moveTaskId, kandownDir, updateLayout, openDetail, closeContextMenu, loadBoardInto]);
57620
57699
  const handleMouseEvent = (0, import_react37.useCallback)((mouse) => {
57621
57700
  if (!board) return;
57622
57701
  if (mode === "browse") {
@@ -57678,11 +57757,9 @@ function Board({ kandownDir, version }) {
57678
57757
  return;
57679
57758
  }
57680
57759
  moveTaskToColumn(kandownDir, taskDrag.taskId, targetColName);
57681
- const loaded = readBoard(kandownDir);
57682
- setBoard(loaded);
57683
- updateLayout(loaded);
57760
+ const loaded = loadBoardInto();
57684
57761
  setColIndex(targetCol);
57685
- const movedRow = loaded.columns[targetCol]?.tasks.findIndex((task) => task.id === taskDrag.taskId) ?? 0;
57762
+ const movedRow = loaded?.columns[targetCol]?.tasks.findIndex((task) => task.id === taskDrag.taskId) ?? 0;
57686
57763
  setRowIndex(Math.max(0, movedRow));
57687
57764
  setStatusMsg(`Dragged ${taskDrag.taskId} \u2192 ${targetColName}`);
57688
57765
  setTimeout(() => setStatusMsg(""), 2e3);
@@ -57844,9 +57921,7 @@ function Board({ kandownDir, version }) {
57844
57921
  return;
57845
57922
  }
57846
57923
  moveTaskToColumn(kandownDir, moveTaskId, name);
57847
- const loaded = readBoard(kandownDir);
57848
- setBoard(loaded);
57849
- updateLayout(loaded);
57924
+ loadBoardInto();
57850
57925
  setStatusMsg(`Moved ${moveTaskId} \u2192 ${name}`);
57851
57926
  setTimeout(() => setStatusMsg(""), 2e3);
57852
57927
  }
@@ -57879,6 +57954,13 @@ function Board({ kandownDir, version }) {
57879
57954
  }
57880
57955
  }
57881
57956
  });
57957
+ if (boardError) {
57958
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", padding: 2, children: [
57959
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "red", bold: true, children: "Error loading board" }),
57960
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "red", children: boardError }),
57961
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "Press 'r' to retry or 'q' to quit." })
57962
+ ] });
57963
+ }
57882
57964
  if (!board) {
57883
57965
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "Loading board\u2026" }) });
57884
57966
  }