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 +64 -14
- package/bin/tui.js +183 -93
- package/dist/index.html +146 -143
- package/package.json +1 -1
- package/templates/kandown.json +2 -1
package/bin/kandown.js
CHANGED
|
@@ -322,18 +322,43 @@ ${c.bold}Examples:${c.reset}
|
|
|
322
322
|
`);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54021
|
-
|
|
54022
|
-
|
|
54023
|
-
|
|
54024
|
-
|
|
54025
|
-
|
|
54026
|
-
|
|
54027
|
-
|
|
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
|
-
|
|
54168
|
-
|
|
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
|
|
54551
|
-
|
|
54552
|
-
|
|
54553
|
-
|
|
54554
|
-
|
|
54555
|
-
|
|
54556
|
-
|
|
54557
|
-
|
|
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
|
-
|
|
54603
|
-
|
|
54604
|
-
|
|
54605
|
-
|
|
54606
|
-
|
|
54607
|
-
|
|
54608
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56822
|
-
|
|
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
|
-
|
|
56826
|
-
|
|
56827
|
-
|
|
56828
|
-
|
|
56829
|
-
|
|
56830
|
-
|
|
56831
|
-
|
|
56832
|
-
|
|
56833
|
-
|
|
56834
|
-
|
|
56835
|
-
|
|
56836
|
-
|
|
56837
|
-
|
|
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
|
-
|
|
57325
|
-
setBoard(loaded);
|
|
57326
|
-
updateLayout(loaded);
|
|
57416
|
+
loadBoardInto();
|
|
57327
57417
|
setInstalledAgents(detectInstalledAgents());
|
|
57328
|
-
}, [kandownDir,
|
|
57418
|
+
}, [kandownDir, loadBoardInto]);
|
|
57329
57419
|
(0, import_react37.useEffect)(() => {
|
|
57330
57420
|
const watcher = createWatcher();
|
|
57331
57421
|
watcher.on("taskChanged", () => {
|
|
57332
|
-
|
|
57333
|
-
setBoard(loaded);
|
|
57334
|
-
updateLayout(loaded);
|
|
57422
|
+
loadBoardInto();
|
|
57335
57423
|
});
|
|
57336
57424
|
watcher.on("newTaskDetected", (taskId) => {
|
|
57337
|
-
|
|
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
|
-
|
|
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,
|
|
57436
|
+
}, [kandownDir, loadBoardInto]);
|
|
57353
57437
|
const reloadBoard = (0, import_react37.useCallback)(() => {
|
|
57354
|
-
const loaded =
|
|
57355
|
-
|
|
57356
|
-
|
|
57357
|
-
|
|
57358
|
-
|
|
57359
|
-
}, [
|
|
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
|
-
|
|
57459
|
-
|
|
57460
|
-
|
|
57461
|
-
|
|
57462
|
-
|
|
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
|
-
|
|
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 =
|
|
57674
|
-
setBoard(loaded);
|
|
57675
|
-
updateLayout(loaded);
|
|
57760
|
+
const loaded = loadBoardInto();
|
|
57676
57761
|
setColIndex(targetCol);
|
|
57677
|
-
const movedRow = loaded
|
|
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
|
-
|
|
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
|
}
|