kandown 0.11.2 → 0.13.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
@@ -54431,6 +54431,7 @@ function taskToBoardTask(task) {
54431
54431
  priority: normalizePriority(frontmatter.priority),
54432
54432
  ownerType: normalizeOwnerType(frontmatter.ownerType),
54433
54433
  progress: total > 0 ? { done, total } : null,
54434
+ dependsOn: Array.isArray(frontmatter.depends_on) ? frontmatter.depends_on.filter((d) => typeof d === "string" && d.trim().length > 0) : [],
54434
54435
  frontmatter: metadata
54435
54436
  };
54436
54437
  }
@@ -54536,8 +54537,11 @@ function serializeTaskFile(frontmatter, body) {
54536
54537
  function getProjectRoot(kandownDir) {
54537
54538
  return dirname(kandownDir);
54538
54539
  }
54540
+ function getTasksDir(kandownDir) {
54541
+ return join2(getProjectRoot(kandownDir), "tasks");
54542
+ }
54539
54543
  function listTaskIds(kandownDir) {
54540
- const tasksDir = join2(kandownDir, "tasks");
54544
+ const tasksDir = getTasksDir(kandownDir);
54541
54545
  if (!existsSync3(tasksDir)) return [];
54542
54546
  return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
54543
54547
  }
@@ -54561,7 +54565,7 @@ function readBoard(kandownDir) {
54561
54565
  };
54562
54566
  }
54563
54567
  function readTask(kandownDir, taskId) {
54564
- const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
54568
+ const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
54565
54569
  if (!existsSync3(taskPath)) {
54566
54570
  return {
54567
54571
  frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
@@ -54593,7 +54597,7 @@ function readAgentDoc(kandownDir) {
54593
54597
  return "";
54594
54598
  }
54595
54599
  function moveTaskToColumn(kandownDir, taskId, targetColumn) {
54596
- const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
54600
+ const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
54597
54601
  if (!existsSync3(taskPath)) return false;
54598
54602
  const parsed = readTask(kandownDir, taskId);
54599
54603
  writeFileSync2(taskPath, serializeTaskFile({
@@ -56454,9 +56458,12 @@ var FileWatcher = class {
56454
56458
  * 📖 Start watching task files and kandown.json.
56455
56459
  * Uses chokidar for immediate FS event detection, plus a fallback 500ms
56456
56460
  * poll to catch any races or network-mounted file changes.
56461
+ *
56462
+ * Tasks live at the project root (`./tasks/`, sibling of `.kandown/`);
56463
+ * kandown.json lives inside `.kandown/`.
56457
56464
  */
56458
56465
  start(kandownDir) {
56459
- const tasksDir = join6(kandownDir, "tasks");
56466
+ const tasksDir = getTasksDir(kandownDir);
56460
56467
  const configPath = join6(kandownDir, "kandown.json");
56461
56468
  const existingIds = listTaskIds(kandownDir);
56462
56469
  for (const id of existingIds) {
@@ -56515,7 +56522,7 @@ var FileWatcher = class {
56515
56522
  // ─── Private ───────────────────────────────────────────────────────────────
56516
56523
  handleFsEvent(event, filePath, kandownDir) {
56517
56524
  if (this.stopped) return;
56518
- const tasksDir = join6(kandownDir, "tasks");
56525
+ const tasksDir = getTasksDir(kandownDir);
56519
56526
  const configPath = join6(kandownDir, "kandown.json");
56520
56527
  if (filePath === configPath) {
56521
56528
  const key = `config:${event}`;
@@ -56568,7 +56575,7 @@ var FileWatcher = class {
56568
56575
  /** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
56569
56576
  async pollHashes(kandownDir) {
56570
56577
  if (this.stopped) return;
56571
- const tasksDir = join6(kandownDir, "tasks");
56578
+ const tasksDir = getTasksDir(kandownDir);
56572
56579
  const configPath = join6(kandownDir, "kandown.json");
56573
56580
  try {
56574
56581
  const newHash = hashFileSync(configPath);
@@ -56678,7 +56685,7 @@ ${taskPrompt}`;
56678
56685
  ---
56679
56686
 
56680
56687
  ${taskPrompt}`;
56681
- return ["gemini", "-p", combined];
56688
+ return ["gemini", "--prompt-interactive", combined];
56682
56689
  }
56683
56690
  },
56684
56691
  {
@@ -56717,8 +56724,13 @@ ${taskPrompt}`;
56717
56724
  bin: "opencode",
56718
56725
  description: "SST AI coding TUI",
56719
56726
  interactive: true,
56720
- buildCommand: ({ taskPrompt }) => {
56721
- return ["opencode"];
56727
+ buildCommand: ({ systemPrompt, taskPrompt }) => {
56728
+ const combined = `${systemPrompt}
56729
+
56730
+ ---
56731
+
56732
+ ${taskPrompt}`;
56733
+ return ["opencode", "--prompt", combined];
56722
56734
  }
56723
56735
  }
56724
56736
  ];
@@ -56801,7 +56813,12 @@ ${taskPrompt}`, "utf8");
56801
56813
  }
56802
56814
  if (isInTmux()) {
56803
56815
  const shellCmd = buildShellCmd(binary, args);
56804
- execSync(`tmux split-window -h -p 50 ${shellescape(shellCmd)}`, {
56816
+ const envPrefix = [
56817
+ `KANDOWN_CONTEXT_FILE=${shellescape(contextFile)}`,
56818
+ `KANDOWN_TASK_ID=${shellescape(taskId)}`,
56819
+ `KANDOWN_DIR=${shellescape(kandownDir)}`
56820
+ ].join(" ");
56821
+ execSync(`tmux split-window -h -p 50 ${shellescape(`env ${envPrefix} ${shellCmd}`)}`, {
56805
56822
  stdio: "inherit"
56806
56823
  });
56807
56824
  } else {
@@ -57035,6 +57052,14 @@ function SingleTaskRow({ task, focused, dragging, colWidth }) {
57035
57052
  " "
57036
57053
  ] }),
57037
57054
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: dragging ? "yellow" : focused ? "cyan" : "yellow", bold: focused || dragging, children: idStr }),
57055
+ task.dependsOn && task.dependsOn.length > 0 && // 📖 TUI equivalent of the web card's `↪N` chip. Surfaces blocked
57056
+ // work inline so the user doesn't try to move a task they can't
57057
+ // unblock without a different upstream task first.
57058
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", children: [
57059
+ " ",
57060
+ "\u21AA",
57061
+ task.dependsOn.length
57062
+ ] }),
57038
57063
  tag && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: focused ? "white" : "magenta", bold: true, children: [
57039
57064
  " ",
57040
57065
  tag
@@ -57149,7 +57174,7 @@ function BoardHeader({ title, inTmux, modeHint, version, daemonStatus, daemonBus
57149
57174
  const tmuxHint = inTmux ? " tmux" : "";
57150
57175
  const versionTag = version ? ` v${version}` : "";
57151
57176
  const daemonLabel = daemonBusy ? "\u25CC daemon\u2026" : daemonStatus.running ? `\u25CF web ${daemonStatus.metadata?.port ?? ""}` : "\u25CB web off";
57152
- const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 d daemon \xB7 r reload \xB7 q quit";
57177
+ const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 a agent \xB7 g send-hook \xB7 d daemon \xB7 r reload \xB7 q quit";
57153
57178
  const width = termWidth();
57154
57179
  const leftWidth = Math.min(Math.max(34, Math.floor(width * 0.46)), width);
57155
57180
  const daemonWidth = Math.min(16, Math.max(0, width - leftWidth));
@@ -57205,6 +57230,14 @@ function TaskDetail({ task, taskId, scrollOffset }) {
57205
57230
  fm.assignee ? ` assignee: ${fm.assignee}` : "",
57206
57231
  fm.due ? ` due: ${fm.due}` : ""
57207
57232
  ] }) }),
57233
+ Array.isArray(fm.depends_on) && fm.depends_on.length > 0 && // 📖 TUI equivalent of the web Drawer's dependency chips. Lists raw
57234
+ // ids — the user can `cat tasks/<id>.md` to inspect any one. We don't
57235
+ // resolve live status here (read-only context) but the move gate in
57236
+ // the TUI blocks the user from moving into Done until they're clear.
57237
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, children: [
57238
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "depends on: " }),
57239
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: fm.depends_on.join(", ") })
57240
+ ] }),
57208
57241
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "\u2500".repeat(termWidth() - 4) }),
57209
57242
  visibleLines.map((line, idx) => {
57210
57243
  const isH = RE_HEADER.test(line);
@@ -57360,6 +57393,61 @@ function Board({ kandownDir, version }) {
57360
57393
  setTimeout(() => setStatusMsg(""), 2500);
57361
57394
  }
57362
57395
  }, [daemonBusy, kandownDir, preferredDaemonPort]);
57396
+ const tryMoveWithGate = (0, import_react37.useCallback)((taskId, targetCol) => {
57397
+ if (!board) return false;
57398
+ const cfg = loadConfig(kandownDir);
57399
+ const cols = cfg.board.columns;
57400
+ const terminalLower = (cols[cols.length - 1] || "Done").toLowerCase();
57401
+ const isTerminal = targetCol.toLowerCase() === terminalLower;
57402
+ if (!isTerminal) return true;
57403
+ const resolved = /* @__PURE__ */ new Map();
57404
+ for (const col of board.columns) {
57405
+ for (const t of col.tasks) {
57406
+ const isArch = t.frontmatter && (t.frontmatter.archived === true || t.frontmatter.archived === "true");
57407
+ resolved.set(t.id, isArch || col.name.toLowerCase() === terminalLower);
57408
+ }
57409
+ }
57410
+ const movingTask = board.columns.flatMap((c) => c.tasks).find((t) => t.id === taskId);
57411
+ if (!movingTask) return true;
57412
+ const deps = Array.isArray(movingTask.dependsOn) ? movingTask.dependsOn : [];
57413
+ const blocked = [];
57414
+ for (const dep of deps) {
57415
+ if (typeof dep !== "string" || !dep.trim() || dep === taskId) continue;
57416
+ const r = resolved.get(dep);
57417
+ if (!r) blocked.push(dep);
57418
+ }
57419
+ if (blocked.length > 0) {
57420
+ const list = blocked.length === 1 ? blocked[0] : `${blocked.slice(0, -1).join(", ")} and ${blocked[blocked.length - 1]}`;
57421
+ setStatusMsg(`Blocked: ${taskId} \u2190 ${list}`);
57422
+ setTimeout(() => setStatusMsg(""), 3500);
57423
+ return false;
57424
+ }
57425
+ return true;
57426
+ }, [board, kandownDir]);
57427
+ const sendTaskToAgentHook = (0, import_react37.useCallback)(async (taskId) => {
57428
+ const status = await getDaemonStatus(kandownDir);
57429
+ if (!status.running || !status.metadata) {
57430
+ setStatusMsg("Web daemon not running (press d to start)");
57431
+ setTimeout(() => setStatusMsg(""), 2500);
57432
+ return;
57433
+ }
57434
+ try {
57435
+ const res = await fetch(`http://127.0.0.1:${status.metadata.port}/api/tasks/${encodeURIComponent(taskId)}/agent`, {
57436
+ method: "POST",
57437
+ signal: AbortSignal.timeout(8e3)
57438
+ });
57439
+ if (res.ok) {
57440
+ setStatusMsg(`Sent ${taskId} to agent hook`);
57441
+ } else {
57442
+ const body = await res.text().catch(() => "");
57443
+ setStatusMsg(`Agent hook: ${res.status}${body ? " \u2014 " + body.slice(0, 60) : ""}`);
57444
+ }
57445
+ } catch (error) {
57446
+ setStatusMsg(`Agent hook failed: ${error instanceof Error ? error.message : String(error)}`);
57447
+ } finally {
57448
+ setTimeout(() => setStatusMsg(""), 3e3);
57449
+ }
57450
+ }, [kandownDir]);
57363
57451
  const getFocusedTask = (0, import_react37.useCallback)(() => {
57364
57452
  if (!board) return null;
57365
57453
  const col = board.columns[colIndex];
@@ -57500,6 +57588,11 @@ function Board({ kandownDir, version }) {
57500
57588
  if (y === placeholderY) {
57501
57589
  const targetColName = col.name;
57502
57590
  if (moveTaskId) {
57591
+ if (!tryMoveWithGate(moveTaskId, targetColName)) {
57592
+ setMoveTaskId(null);
57593
+ setMode("browse");
57594
+ return;
57595
+ }
57503
57596
  moveTaskToColumn(kandownDir, moveTaskId, targetColName);
57504
57597
  const loaded = readBoard(kandownDir);
57505
57598
  setBoard(loaded);
@@ -57570,6 +57663,12 @@ function Board({ kandownDir, version }) {
57570
57663
  if (targetCol >= 0 && targetCol !== taskDrag.sourceCol) {
57571
57664
  const targetColName = board.columns[targetCol]?.name;
57572
57665
  if (targetColName) {
57666
+ if (!tryMoveWithGate(taskDrag.taskId, targetColName)) {
57667
+ setTaskDrag(null);
57668
+ setMousePress(null);
57669
+ setMode("browse");
57670
+ return;
57671
+ }
57573
57672
  moveTaskToColumn(kandownDir, taskDrag.taskId, targetColName);
57574
57673
  const loaded = readBoard(kandownDir);
57575
57674
  setBoard(loaded);
@@ -57658,6 +57757,12 @@ function Board({ kandownDir, version }) {
57658
57757
  setMode("agent-picker");
57659
57758
  return;
57660
57759
  }
57760
+ if (input === "g") {
57761
+ const task = getFocusedTask();
57762
+ if (!task) return;
57763
+ void sendTaskToAgentHook(task.id);
57764
+ return;
57765
+ }
57661
57766
  }
57662
57767
  if (mode === "context-menu") {
57663
57768
  if (key.escape || input === "q") {
@@ -57725,6 +57830,11 @@ function Board({ kandownDir, version }) {
57725
57830
  if (!board || !moveTaskId) return;
57726
57831
  const name = board.columns[moveTargetCol]?.name;
57727
57832
  if (name) {
57833
+ if (!tryMoveWithGate(moveTaskId, name)) {
57834
+ setMoveTaskId(null);
57835
+ setMode("browse");
57836
+ return;
57837
+ }
57728
57838
  moveTaskToColumn(kandownDir, moveTaskId, name);
57729
57839
  const loaded = readBoard(kandownDir);
57730
57840
  setBoard(loaded);
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/favicon.svg CHANGED
@@ -1,7 +1,25 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150">
2
- <!-- SVG created with Arrow, by QuiverAI (https://quiver.ai) -->
3
- <style type="text/css">.cls-0 {fill:#161616;}</style>
4
- <path class="cls-0" d="m57.6 64.6 0.1-0.1v-31.3c-0.1-3.5-2.7-5.6-5.7-5.6h-16.3v59.6c0.2-1.3 0.9-2.6 1.7-3.2l20.2-19.4z"/>
5
- <path class="cls-0" d="m87.6 43.8c-3.4 0.1-7.1 1.3-9.9 3.7l-38.5 38.7c-2.1 2.1-3.4 4.8-3.5 7.5v26.7l77.5-76.4v-0.2h-25.6z"/>
6
- <path class="cls-0" d="m108.1 96.4-6 5-22.4-20.8-14.6 14.2 21.1 21.4-5 4.1c-0.6 0.5-0.3 0.9 0.2 0.9h27.6c0.4 0 0.7-0.4 0.7-0.6v-24.8c0-0.7-1.1-0.3-1.6 0.3v0.3z"/>
7
- </svg>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ @file public/favicon.svg
4
+ @description Kandown brand mark vector favicon.
5
+ 📖 Hand-traced from kandownlogo.png. Optimised for 16–256px rendering.
6
+ Colors: dark surface #0a0a0a, K strokes #ffffff, lime accent #cef867.
7
+ The lime slash is a parallelogram that slices from top-right to bottom-left,
8
+ crossing the K. Z-order is: surface → K strokes → lime slash.
9
+ -->
10
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Kandown">
11
+ <title>Kandown</title>
12
+ <rect width="256" height="256" rx="48" ry="48" fill="#0a0a0a"/>
13
+
14
+ <!-- K vertical bar (left stem) -->
15
+ <rect x="50" y="36" width="34" height="184" rx="3" ry="3" fill="#ffffff"/>
16
+
17
+ <!-- K upper diagonal: middle-left → top-right -->
18
+ <path d="M 84 128 L 200 36 L 222 36 L 84 152 Z" fill="#ffffff"/>
19
+
20
+ <!-- K lower diagonal: middle-left → bottom-right (with the small flag/notch on the right) -->
21
+ <path d="M 84 128 L 200 220 L 222 220 L 188 186 L 200 174 L 84 132 Z" fill="#ffffff"/>
22
+
23
+ <!-- Lime diagonal slash: thick parallelogram from top-right to bottom-left -->
24
+ <path d="M 232 22 L 174 22 L 24 172 L 24 230 Z" fill="#cef867"/>
25
+ </svg>