kandown 0.13.1 → 0.14.1

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
@@ -53993,7 +53993,8 @@ var DEFAULT_CONFIG = {
53993
53993
  board: {
53994
53994
  columns: ["Backlog", "Todo", "In Progress", "Review", "Done"],
53995
53995
  defaultPriority: "P3",
53996
- defaultOwnerType: "human"
53996
+ defaultOwnerType: "human",
53997
+ stackDefaultState: "collapsed"
53997
53998
  },
53998
53999
  fields: {
53999
54000
  priority: false,
@@ -54016,24 +54017,37 @@ var DEFAULT_CONFIG = {
54016
54017
  function loadConfig(kandownDir) {
54017
54018
  const configPath = join(kandownDir, "kandown.json");
54018
54019
  if (!existsSync2(configPath)) return structuredClone(DEFAULT_CONFIG);
54020
+ let raw;
54019
54021
  try {
54020
- const raw = JSON.parse(readFileSync2(configPath, "utf8"));
54021
- const merged = {
54022
- ui: { ...DEFAULT_CONFIG.ui, ...raw.ui },
54023
- agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
54024
- board: {
54025
- ...DEFAULT_CONFIG.board,
54026
- ...raw.board,
54027
- 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
54028
- },
54029
- fields: { ...DEFAULT_CONFIG.fields, ...raw.fields },
54030
- notifications: { ...DEFAULT_CONFIG.notifications, ...raw.notifications }
54031
- };
54032
- if (raw.agents) merged.agents = raw.agents;
54033
- return merged;
54034
- } 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.");
54035
54031
  return structuredClone(DEFAULT_CONFIG);
54036
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;
54037
54051
  }
54038
54052
  function saveConfig(kandownDir, config) {
54039
54053
  const configPath = join(kandownDir, "kandown.json");
@@ -54107,6 +54121,13 @@ var SETTINGS = [
54107
54121
  max: 5
54108
54122
  },
54109
54123
  // Board
54124
+ {
54125
+ key: "board.stackDefaultState",
54126
+ label: "Task groups",
54127
+ section: "Board",
54128
+ type: "select",
54129
+ options: ["collapsed", "expanded"]
54130
+ },
54110
54131
  {
54111
54132
  key: "board.defaultPriority",
54112
54133
  label: "Default priority",
@@ -54164,8 +54185,12 @@ function Settings({ kandownDir, version }) {
54164
54185
  const persistConfig = (0, import_react34.useCallback)(
54165
54186
  (newConfig) => {
54166
54187
  setConfig(newConfig);
54167
- saveConfig(kandownDir, newConfig);
54168
- 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
+ }
54169
54194
  },
54170
54195
  [kandownDir]
54171
54196
  );
@@ -54547,17 +54572,23 @@ function listTaskIds(kandownDir) {
54547
54572
  }
54548
54573
  function readBoard(kandownDir) {
54549
54574
  const config = loadConfig(kandownDir);
54550
- const tasks = listTaskIds(kandownDir).map((id) => {
54551
- const task = readTask(kandownDir, id);
54552
- return {
54553
- ...task,
54554
- frontmatter: {
54555
- ...task.frontmatter,
54556
- id: task.frontmatter.id || id,
54557
- status: task.frontmatter.status || "Backlog"
54558
- }
54559
- };
54560
- });
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
+ }
54561
54592
  return {
54562
54593
  frontmatter: null,
54563
54594
  title: "Project Kanban",
@@ -54590,8 +54621,11 @@ function readAgentDoc(kandownDir) {
54590
54621
  join2(kandownDir, "AGENT.md")
54591
54622
  ];
54592
54623
  for (const candidate of candidates) {
54593
- if (existsSync3(candidate)) {
54624
+ if (!existsSync3(candidate)) continue;
54625
+ try {
54594
54626
  return readFileSync3(candidate, "utf8");
54627
+ } catch (e) {
54628
+ console.warn(`[kandown] Could not read ${candidate}:`, e.message);
54595
54629
  }
54596
54630
  }
54597
54631
  return "";
@@ -54599,13 +54633,18 @@ function readAgentDoc(kandownDir) {
54599
54633
  function moveTaskToColumn(kandownDir, taskId, targetColumn) {
54600
54634
  const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
54601
54635
  if (!existsSync3(taskPath)) return false;
54602
- const parsed = readTask(kandownDir, taskId);
54603
- writeFileSync2(taskPath, serializeTaskFile({
54604
- ...parsed.frontmatter,
54605
- id: taskId,
54606
- status: targetColumn
54607
- }, parsed.body), "utf8");
54608
- 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
+ }
54609
54648
  }
54610
54649
 
54611
54650
  // src/cli/lib/daemon.ts
@@ -56787,7 +56826,13 @@ function launchAgent(opts) {
56787
56826
  if (!agentDef) {
56788
56827
  throw new Error(`Unknown agent: ${agentId}`);
56789
56828
  }
56790
- 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";
56791
56836
  const agentDoc = readAgentDoc(kandownDir);
56792
56837
  const taskFileContent = [
56793
56838
  `---`,
@@ -56799,16 +56844,24 @@ function launchAgent(opts) {
56799
56844
  task.body.trim()
56800
56845
  ].join("\n");
56801
56846
  const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
56802
- 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
+ }
56803
56851
  const contextFile = join7(tmpdir(), `kandown-${taskId}-context.md`);
56804
- writeFileSync3(contextFile, `${systemPrompt}
56852
+ try {
56853
+ writeFileSync3(contextFile, `${systemPrompt}
56805
56854
 
56806
56855
  ---
56807
56856
 
56808
56857
  ${taskPrompt}`, "utf8");
56858
+ } catch (e) {
56859
+ console.warn(`[kandown] Failed to write context file (${e.message}); launching anyway.`);
56860
+ }
56809
56861
  const launchOpts = { systemPrompt, taskPrompt, kandownDir, taskId };
56810
56862
  const [binary, ...args] = agentDef.buildCommand(launchOpts);
56811
56863
  if (!binary) {
56864
+ rollbackTaskStatus(kandownDir, taskId, originalStatus);
56812
56865
  throw new Error(`Agent ${agentId} returned an empty command`);
56813
56866
  }
56814
56867
  if (isInTmux()) {
@@ -56818,24 +56871,47 @@ ${taskPrompt}`, "utf8");
56818
56871
  `KANDOWN_TASK_ID=${shellescape(taskId)}`,
56819
56872
  `KANDOWN_DIR=${shellescape(kandownDir)}`
56820
56873
  ].join(" ");
56821
- execSync(`tmux split-window -h -p 50 ${shellescape(`env ${envPrefix} ${shellCmd}`)}`, {
56822
- stdio: "inherit"
56823
- });
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
+ }
56824
56882
  } else {
56825
- onBeforeExec?.();
56826
- const child = spawn2(binary, args, {
56827
- stdio: "inherit",
56828
- env: {
56829
- ...process.env,
56830
- // 📖 Expose the context file path so agents that support env vars can use it
56831
- KANDOWN_CONTEXT_FILE: contextFile,
56832
- KANDOWN_TASK_ID: taskId,
56833
- KANDOWN_DIR: kandownDir
56834
- }
56835
- });
56836
- child.on("exit", (code) => {
56837
- process.exit(code ?? 0);
56838
- });
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.`);
56839
56915
  }
56840
56916
  }
56841
56917
  function buildShellCmd(binary, args) {
@@ -56895,6 +56971,7 @@ function AgentPicker({ agents, taskId, onSelect, onCancel }) {
56895
56971
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "yellow", children: taskId })
56896
56972
  ] })
56897
56973
  ] }),
56974
+ agents.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "yellow", children: "No agents installed. Install claude, codex, or opencode." }),
56898
56975
  agents.map((agent, idx) => {
56899
56976
  const isFocused = idx === cursor;
56900
56977
  const numHint = idx < 9 ? `${idx + 1} ` : " ";
@@ -57272,6 +57349,7 @@ function Board({ kandownDir, version }) {
57272
57349
  const [rowIndex, setRowIndex] = (0, import_react37.useState)(0);
57273
57350
  const [mode, setMode] = (0, import_react37.useState)("browse");
57274
57351
  const [statusMsg, setStatusMsg] = (0, import_react37.useState)("");
57352
+ const [boardError, setBoardError] = (0, import_react37.useState)(null);
57275
57353
  const [detailTask, setDetailTask] = (0, import_react37.useState)(null);
57276
57354
  const [detailTaskId, setDetailTaskId] = (0, import_react37.useState)("");
57277
57355
  const [detailScroll, setDetailScroll] = (0, import_react37.useState)(0);
@@ -57320,43 +57398,49 @@ function Board({ kandownDir, version }) {
57320
57398
  if (!task) return null;
57321
57399
  return { taskId: task.id, colIndex: clickedCol, rowIndex: taskIdx, startX: x, startY: y };
57322
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]);
57323
57415
  (0, import_react37.useEffect)(() => {
57324
- const loaded = readBoard(kandownDir);
57325
- setBoard(loaded);
57326
- updateLayout(loaded);
57416
+ loadBoardInto();
57327
57417
  setInstalledAgents(detectInstalledAgents());
57328
- }, [kandownDir, updateLayout]);
57418
+ }, [kandownDir, loadBoardInto]);
57329
57419
  (0, import_react37.useEffect)(() => {
57330
57420
  const watcher = createWatcher();
57331
57421
  watcher.on("taskChanged", () => {
57332
- const loaded = readBoard(kandownDir);
57333
- setBoard(loaded);
57334
- updateLayout(loaded);
57422
+ loadBoardInto();
57335
57423
  });
57336
57424
  watcher.on("newTaskDetected", (taskId) => {
57337
- const loaded = readBoard(kandownDir);
57338
- setBoard(loaded);
57339
- updateLayout(loaded);
57425
+ loadBoardInto();
57340
57426
  setStatusMsg(`New task: ${taskId}`);
57341
57427
  setTimeout(() => setStatusMsg(""), 2e3);
57342
57428
  });
57343
57429
  watcher.on("configChanged", () => {
57344
- const loaded = readBoard(kandownDir);
57345
- setBoard(loaded);
57346
- updateLayout(loaded);
57430
+ loadBoardInto();
57347
57431
  });
57348
57432
  watcher.start(kandownDir);
57349
57433
  return () => {
57350
57434
  watcher.stop();
57351
57435
  };
57352
- }, [kandownDir, updateLayout]);
57436
+ }, [kandownDir, loadBoardInto]);
57353
57437
  const reloadBoard = (0, import_react37.useCallback)(() => {
57354
- const loaded = readBoard(kandownDir);
57355
- setBoard(loaded);
57356
- updateLayout(loaded);
57357
- setStatusMsg("Board reloaded");
57358
- setTimeout(() => setStatusMsg(""), 1500);
57359
- }, [kandownDir, updateLayout]);
57438
+ const loaded = loadBoardInto();
57439
+ if (loaded) {
57440
+ setStatusMsg("Board reloaded");
57441
+ setTimeout(() => setStatusMsg(""), 1500);
57442
+ }
57443
+ }, [loadBoardInto]);
57360
57444
  const refreshDaemonStatus = (0, import_react37.useCallback)(async () => {
57361
57445
  const next = await getDaemonStatus(kandownDir);
57362
57446
  setDaemonStatus(next);
@@ -57455,11 +57539,16 @@ function Board({ kandownDir, version }) {
57455
57539
  return col.tasks[Math.min(rowIndex, col.tasks.length - 1)] ?? null;
57456
57540
  }, [board, colIndex, rowIndex]);
57457
57541
  const openDetail = (0, import_react37.useCallback)((taskId) => {
57458
- const task = readTask(kandownDir, taskId);
57459
- setDetailTask(task);
57460
- setDetailTaskId(taskId);
57461
- setDetailScroll(0);
57462
- 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
+ }
57463
57552
  }, [kandownDir]);
57464
57553
  const closeContextMenu = (0, import_react37.useCallback)(() => {
57465
57554
  setCtxMenuRow(-1);
@@ -57594,9 +57683,7 @@ function Board({ kandownDir, version }) {
57594
57683
  return;
57595
57684
  }
57596
57685
  moveTaskToColumn(kandownDir, moveTaskId, targetColName);
57597
- const loaded = readBoard(kandownDir);
57598
- setBoard(loaded);
57599
- updateLayout(loaded);
57686
+ loadBoardInto();
57600
57687
  setStatusMsg(`Moved ${moveTaskId} \u2192 ${targetColName}`);
57601
57688
  setTimeout(() => setStatusMsg(""), 2e3);
57602
57689
  }
@@ -57608,7 +57695,7 @@ function Board({ kandownDir, version }) {
57608
57695
  setMode("browse");
57609
57696
  return;
57610
57697
  }
57611
- }, [board, mode, colIndex, rowIndex, ctxMenuRow, moveTaskId, kandownDir, updateLayout, openDetail, closeContextMenu]);
57698
+ }, [board, mode, colIndex, rowIndex, ctxMenuRow, moveTaskId, kandownDir, updateLayout, openDetail, closeContextMenu, loadBoardInto]);
57612
57699
  const handleMouseEvent = (0, import_react37.useCallback)((mouse) => {
57613
57700
  if (!board) return;
57614
57701
  if (mode === "browse") {
@@ -57670,11 +57757,9 @@ function Board({ kandownDir, version }) {
57670
57757
  return;
57671
57758
  }
57672
57759
  moveTaskToColumn(kandownDir, taskDrag.taskId, targetColName);
57673
- const loaded = readBoard(kandownDir);
57674
- setBoard(loaded);
57675
- updateLayout(loaded);
57760
+ const loaded = loadBoardInto();
57676
57761
  setColIndex(targetCol);
57677
- 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;
57678
57763
  setRowIndex(Math.max(0, movedRow));
57679
57764
  setStatusMsg(`Dragged ${taskDrag.taskId} \u2192 ${targetColName}`);
57680
57765
  setTimeout(() => setStatusMsg(""), 2e3);
@@ -57836,9 +57921,7 @@ function Board({ kandownDir, version }) {
57836
57921
  return;
57837
57922
  }
57838
57923
  moveTaskToColumn(kandownDir, moveTaskId, name);
57839
- const loaded = readBoard(kandownDir);
57840
- setBoard(loaded);
57841
- updateLayout(loaded);
57924
+ loadBoardInto();
57842
57925
  setStatusMsg(`Moved ${moveTaskId} \u2192 ${name}`);
57843
57926
  setTimeout(() => setStatusMsg(""), 2e3);
57844
57927
  }
@@ -57871,6 +57954,13 @@ function Board({ kandownDir, version }) {
57871
57954
  }
57872
57955
  }
57873
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
+ }
57874
57964
  if (!board) {
57875
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" }) });
57876
57966
  }