kandown 0.7.4 → 0.8.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
@@ -27895,7 +27895,7 @@ var require_backend = __commonJS({
27895
27895
  });
27896
27896
  return value;
27897
27897
  },
27898
- useEffect: function useEffect10(create3) {
27898
+ useEffect: function useEffect11(create3) {
27899
27899
  nextHook();
27900
27900
  hookLog.push({
27901
27901
  displayName: null,
@@ -27983,7 +27983,7 @@ var require_backend = __commonJS({
27983
27983
  });
27984
27984
  return initialValue;
27985
27985
  },
27986
- useState: function useState9(initialState) {
27986
+ useState: function useState10(initialState) {
27987
27987
  var hook = nextHook();
27988
27988
  initialState = null !== hook ? hook.memoizedState : "function" === typeof initialState ? initialState() : initialState;
27989
27989
  hookLog.push({
@@ -53951,6 +53951,8 @@ var use_app_default = useApp;
53951
53951
 
53952
53952
  // node_modules/.pnpm/ink@7.0.1_@types+react@19.2.14_react-devtools-core@7.0.1_react@19.2.5/node_modules/ink/build/hooks/use-stdout.js
53953
53953
  var import_react25 = __toESM(require_react(), 1);
53954
+ var useStdout = () => (0, import_react25.useContext)(StdoutContext_default);
53955
+ var use_stdout_default = useStdout;
53954
53956
 
53955
53957
  // node_modules/.pnpm/ink@7.0.1_@types+react@19.2.14_react-devtools-core@7.0.1_react@19.2.5/node_modules/ink/build/hooks/use-stderr.js
53956
53958
  var import_react26 = __toESM(require_react(), 1);
@@ -53976,6 +53978,9 @@ var import_react32 = __toESM(require_react(), 1);
53976
53978
  // node_modules/.pnpm/ink@7.0.1_@types+react@19.2.14_react-devtools-core@7.0.1_react@19.2.5/node_modules/ink/build/hooks/use-box-metrics.js
53977
53979
  var import_react33 = __toESM(require_react(), 1);
53978
53980
 
53981
+ // src/cli/app.tsx
53982
+ var import_react38 = __toESM(require_react(), 1);
53983
+
53979
53984
  // src/cli/screens/settings.tsx
53980
53985
  var import_react34 = __toESM(require_react(), 1);
53981
53986
 
@@ -54584,10 +54589,137 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
54584
54589
  return true;
54585
54590
  }
54586
54591
 
54592
+ // src/cli/lib/daemon.ts
54593
+ import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
54594
+ import { dirname as dirname2, join as join3 } from "path";
54595
+ import { spawn } from "child_process";
54596
+ function metadataPath(kandownDir) {
54597
+ return join3(kandownDir, "daemon.json");
54598
+ }
54599
+ function isRecord(value) {
54600
+ return typeof value === "object" && value !== null;
54601
+ }
54602
+ function parseMetadata(value) {
54603
+ if (!isRecord(value)) return null;
54604
+ const { pid, port, url, kandownDir, startedAt, version } = value;
54605
+ if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
54606
+ if (typeof port !== "number" || !Number.isInteger(port)) return null;
54607
+ if (typeof url !== "string" || typeof kandownDir !== "string") return null;
54608
+ if (typeof startedAt !== "string") return null;
54609
+ if (version !== null && typeof version !== "string" && version !== void 0) return null;
54610
+ return { pid, port, url, kandownDir, startedAt, version: version ?? null };
54611
+ }
54612
+ function readDaemonMetadata(kandownDir) {
54613
+ const path = metadataPath(kandownDir);
54614
+ if (!existsSync4(path)) return null;
54615
+ try {
54616
+ return parseMetadata(JSON.parse(readFileSync4(path, "utf8")));
54617
+ } catch {
54618
+ return null;
54619
+ }
54620
+ }
54621
+ function removeDaemonMetadata(kandownDir) {
54622
+ try {
54623
+ const path = metadataPath(kandownDir);
54624
+ if (existsSync4(path)) unlinkSync(path);
54625
+ } catch {
54626
+ }
54627
+ }
54628
+ function isProcessAlive(pid) {
54629
+ try {
54630
+ process.kill(pid, 0);
54631
+ return true;
54632
+ } catch {
54633
+ return false;
54634
+ }
54635
+ }
54636
+ function parseRemoteDaemonInfo(value) {
54637
+ if (!isRecord(value)) return null;
54638
+ const { ok, pid, kandownDir } = value;
54639
+ if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
54640
+ return { ok, pid, kandownDir };
54641
+ }
54642
+ async function fetchDaemonInfo(port) {
54643
+ try {
54644
+ const response = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
54645
+ signal: AbortSignal.timeout(700)
54646
+ });
54647
+ if (!response.ok) return null;
54648
+ return parseRemoteDaemonInfo(await response.json());
54649
+ } catch {
54650
+ return null;
54651
+ }
54652
+ }
54653
+ async function getDaemonStatus(kandownDir) {
54654
+ const metadata = readDaemonMetadata(kandownDir);
54655
+ if (!metadata) return { running: false, metadata: null };
54656
+ if (!isProcessAlive(metadata.pid)) {
54657
+ removeDaemonMetadata(kandownDir);
54658
+ return { running: false, metadata: null };
54659
+ }
54660
+ const remote = await fetchDaemonInfo(metadata.port);
54661
+ if (!remote || remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
54662
+ removeDaemonMetadata(kandownDir);
54663
+ return { running: false, metadata: null };
54664
+ }
54665
+ return { running: true, metadata };
54666
+ }
54667
+ async function waitForDaemon(kandownDir, timeoutMs = 5e3) {
54668
+ const started = Date.now();
54669
+ while (Date.now() - started < timeoutMs) {
54670
+ const status = await getDaemonStatus(kandownDir);
54671
+ if (status.running) return status;
54672
+ await new Promise((resolve3) => setTimeout(resolve3, 150));
54673
+ }
54674
+ return { running: false, metadata: null };
54675
+ }
54676
+ async function startProjectDaemon(kandownDir, preferredPort) {
54677
+ const current = await getDaemonStatus(kandownDir);
54678
+ if (current.running) return current;
54679
+ const cliPath = process.argv[1];
54680
+ if (!cliPath) throw new Error("Cannot locate kandown CLI entrypoint");
54681
+ const args = [
54682
+ cliPath,
54683
+ "--no-update-check",
54684
+ "daemon",
54685
+ "run",
54686
+ "--path",
54687
+ kandownDir
54688
+ ];
54689
+ if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
54690
+ args.push("--port", String(preferredPort));
54691
+ }
54692
+ const child = spawn(process.execPath, args, {
54693
+ cwd: dirname2(kandownDir),
54694
+ detached: true,
54695
+ stdio: "ignore",
54696
+ env: { ...process.env, KANDOWN_DAEMON: "1" }
54697
+ });
54698
+ child.unref();
54699
+ return waitForDaemon(kandownDir);
54700
+ }
54701
+ async function stopProjectDaemon(kandownDir) {
54702
+ const status = await getDaemonStatus(kandownDir);
54703
+ if (!status.running || !status.metadata) {
54704
+ removeDaemonMetadata(kandownDir);
54705
+ return false;
54706
+ }
54707
+ try {
54708
+ process.kill(status.metadata.pid, "SIGTERM");
54709
+ } catch {
54710
+ }
54711
+ const started = Date.now();
54712
+ while (Date.now() - started < 2500 && isProcessAlive(status.metadata.pid)) {
54713
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
54714
+ }
54715
+ removeDaemonMetadata(kandownDir);
54716
+ return true;
54717
+ }
54718
+
54587
54719
  // src/cli/lib/file-watcher.ts
54588
54720
  import { createReadStream, statSync } from "fs";
54589
54721
  import { createHash } from "crypto";
54590
- import { join as join5 } from "path";
54722
+ import { join as join6 } from "path";
54591
54723
 
54592
54724
  // node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/index.js
54593
54725
  import { stat as statcb } from "fs";
@@ -55318,9 +55450,9 @@ var NodeFsHandler = class {
55318
55450
  if (this.fsw.closed) {
55319
55451
  return;
55320
55452
  }
55321
- const dirname4 = sysPath.dirname(file);
55453
+ const dirname5 = sysPath.dirname(file);
55322
55454
  const basename3 = sysPath.basename(file);
55323
- const parent = this.fsw._getWatchedDir(dirname4);
55455
+ const parent = this.fsw._getWatchedDir(dirname5);
55324
55456
  let prevStats = stats;
55325
55457
  if (parent.has(basename3))
55326
55458
  return;
@@ -55347,7 +55479,7 @@ var NodeFsHandler = class {
55347
55479
  prevStats = newStats2;
55348
55480
  }
55349
55481
  } catch (error) {
55350
- this.fsw._remove(dirname4, basename3);
55482
+ this.fsw._remove(dirname5, basename3);
55351
55483
  }
55352
55484
  } else if (parent.has(basename3)) {
55353
55485
  const at = newStats.atimeMs;
@@ -56309,18 +56441,18 @@ var FileWatcher = class {
56309
56441
  * poll to catch any races or network-mounted file changes.
56310
56442
  */
56311
56443
  start(kandownDir) {
56312
- const tasksDir = join5(kandownDir, "tasks");
56313
- const configPath = join5(kandownDir, "kandown.json");
56444
+ const tasksDir = join6(kandownDir, "tasks");
56445
+ const configPath = join6(kandownDir, "kandown.json");
56314
56446
  const existingIds = listTaskIds(kandownDir);
56315
56447
  for (const id of existingIds) {
56316
56448
  this.knownTaskIds.add(id);
56317
56449
  try {
56318
- const filePath = join5(tasksDir, `${id}.md`);
56450
+ const filePath = join6(tasksDir, `${id}.md`);
56319
56451
  this.taskHashes.set(id, hashFileSync(filePath));
56320
56452
  } catch {
56321
56453
  }
56322
56454
  }
56323
- this.watcher = watch([join5(tasksDir, "*.md"), configPath], {
56455
+ this.watcher = watch([join6(tasksDir, "*.md"), configPath], {
56324
56456
  ignoreInitial: true,
56325
56457
  awaitWriteFinish: { stabilityThreshold: 25, pollInterval: 25 },
56326
56458
  alwaysStat: true
@@ -56368,8 +56500,8 @@ var FileWatcher = class {
56368
56500
  // ─── Private ───────────────────────────────────────────────────────────────
56369
56501
  handleFsEvent(event, filePath, kandownDir) {
56370
56502
  if (this.stopped) return;
56371
- const tasksDir = join5(kandownDir, "tasks");
56372
- const configPath = join5(kandownDir, "kandown.json");
56503
+ const tasksDir = join6(kandownDir, "tasks");
56504
+ const configPath = join6(kandownDir, "kandown.json");
56373
56505
  if (filePath === configPath) {
56374
56506
  const key = `config:${event}`;
56375
56507
  const existing = this.debounceTimers.get(key);
@@ -56421,14 +56553,14 @@ var FileWatcher = class {
56421
56553
  /** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
56422
56554
  async pollHashes(kandownDir) {
56423
56555
  if (this.stopped) return;
56424
- const tasksDir = join5(kandownDir, "tasks");
56425
- const configPath = join5(kandownDir, "kandown.json");
56556
+ const tasksDir = join6(kandownDir, "tasks");
56557
+ const configPath = join6(kandownDir, "kandown.json");
56426
56558
  try {
56427
56559
  const newHash = hashFileSync(configPath);
56428
56560
  } catch {
56429
56561
  }
56430
56562
  for (const taskId of this.knownTaskIds) {
56431
- const filePath = join5(tasksDir, `${taskId}.md`);
56563
+ const filePath = join6(tasksDir, `${taskId}.md`);
56432
56564
  try {
56433
56565
  statSync(filePath);
56434
56566
  const newHash = await hashFile(filePath);
@@ -56445,7 +56577,7 @@ var FileWatcher = class {
56445
56577
  const currentIds = listTaskIds(kandownDir);
56446
56578
  for (const id of currentIds) {
56447
56579
  if (!this.knownTaskIds.has(id)) {
56448
- const filePath = join5(tasksDir, `${id}.md`);
56580
+ const filePath = join6(tasksDir, `${id}.md`);
56449
56581
  try {
56450
56582
  const newHash = await hashFile(filePath);
56451
56583
  this.knownTaskIds.add(id);
@@ -56615,9 +56747,9 @@ function buildPrompt(agentDoc, taskContent, taskId, kandownDir) {
56615
56747
  }
56616
56748
 
56617
56749
  // src/cli/lib/launcher.ts
56618
- import { execSync, spawn } from "child_process";
56750
+ import { execSync, spawn as spawn2 } from "child_process";
56619
56751
  import { writeFileSync as writeFileSync3 } from "fs";
56620
- import { join as join6 } from "path";
56752
+ import { join as join7 } from "path";
56621
56753
  import { tmpdir } from "os";
56622
56754
  function isInTmux() {
56623
56755
  return !!process.env.TMUX;
@@ -56641,7 +56773,7 @@ function launchAgent(opts) {
56641
56773
  ].join("\n");
56642
56774
  const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
56643
56775
  moveTaskToColumn(kandownDir, taskId, "In Progress");
56644
- const contextFile = join6(tmpdir(), `kandown-${taskId}-context.md`);
56776
+ const contextFile = join7(tmpdir(), `kandown-${taskId}-context.md`);
56645
56777
  writeFileSync3(contextFile, `${systemPrompt}
56646
56778
 
56647
56779
  ---
@@ -56659,7 +56791,7 @@ ${taskPrompt}`, "utf8");
56659
56791
  });
56660
56792
  } else {
56661
56793
  onBeforeExec?.();
56662
- const child = spawn(binary, args, {
56794
+ const child = spawn2(binary, args, {
56663
56795
  stdio: "inherit",
56664
56796
  env: {
56665
56797
  ...process.env,
@@ -56757,8 +56889,8 @@ function AgentPicker({ agents, taskId, onSelect, onCancel }) {
56757
56889
 
56758
56890
  // src/cli/hooks/use-mouse.ts
56759
56891
  var import_react36 = __toESM(require_react(), 1);
56760
- var MOUSE_ENABLE = "\x1B[?1000h\x1B[?1006h";
56761
- var MOUSE_DISABLE = "\x1B[?1006l\x1B[?1000l";
56892
+ var MOUSE_ENABLE = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
56893
+ var MOUSE_DISABLE = "\x1B[?1006l\x1B[?1002l\x1B[?1000l";
56762
56894
  var RE_SGR_MOUSE = /^\[<(\d+);(\d+);(\d+)([Mm])/;
56763
56895
  function parseMouseInput(input) {
56764
56896
  const match = input.match(RE_SGR_MOUSE);
@@ -56766,31 +56898,44 @@ function parseMouseInput(input) {
56766
56898
  const cb = parseInt(match[1], 10);
56767
56899
  const cx = parseInt(match[2], 10);
56768
56900
  const cy = parseInt(match[3], 10);
56769
- const isPress = match[4] === "M";
56901
+ const final = match[4];
56902
+ const buttonNumber = cb & 3 | (cb & 192) >> 4;
56903
+ const dragging = (cb & 32) === 32;
56904
+ let action;
56905
+ if (final === "m") action = "release";
56906
+ else if (dragging && buttonNumber <= 2) action = "drag";
56907
+ else if (dragging) action = "move";
56908
+ else if (buttonNumber >= 4 && buttonNumber <= 7) action = "scroll";
56909
+ else action = "press";
56770
56910
  return {
56771
56911
  x: cx,
56772
56912
  y: cy,
56773
- button: cb & 3,
56774
- action: isPress ? "press" : "release"
56913
+ button: buttonNumber <= 2 ? buttonNumber : 0,
56914
+ action
56775
56915
  };
56776
56916
  }
56777
56917
  function isMouseInput(input) {
56778
56918
  return input.startsWith("[<") || input.startsWith("[M");
56779
56919
  }
56920
+ function safeWrite(write, data) {
56921
+ try {
56922
+ write(data);
56923
+ } catch {
56924
+ }
56925
+ }
56780
56926
  function useMouseMode(enabled = true) {
56927
+ const { write } = use_stdout_default();
56781
56928
  (0, import_react36.useEffect)(() => {
56782
56929
  if (!enabled) return;
56783
56930
  if (!process.stdin.isTTY) return;
56784
- process.stdout.write(MOUSE_ENABLE);
56785
- const cleanup = () => {
56786
- process.stdout.write(MOUSE_DISABLE);
56787
- };
56931
+ safeWrite(write, MOUSE_ENABLE);
56932
+ const cleanup = () => safeWrite(write, MOUSE_DISABLE);
56788
56933
  process.on("exit", cleanup);
56789
56934
  return () => {
56790
56935
  process.removeListener("exit", cleanup);
56791
- process.stdout.write(MOUSE_DISABLE);
56936
+ safeWrite(write, MOUSE_DISABLE);
56792
56937
  };
56793
- }, [enabled]);
56938
+ }, [enabled, write]);
56794
56939
  }
56795
56940
 
56796
56941
  // src/cli/components/task-context-menu.tsx
@@ -56846,8 +56991,17 @@ var RE_HEADER = /^#{1,3}\s/;
56846
56991
  var RE_SUBTASK = /^\s*-\s+\[([ xX])\]/;
56847
56992
  var RE_DONE = /^\s*-\s+\[x\]/i;
56848
56993
  var RE_BRACKET_TAG = /^\[([^\]]+)\]\s*/;
56849
- function TaskRow({ task, focused, colWidth }) {
56850
- const cursor = focused ? "\u25B8" : " ";
56994
+ function columnAccentColor(name) {
56995
+ const normalized = name.toLowerCase();
56996
+ if (/backlog/.test(normalized)) return "magenta";
56997
+ if (/todo/.test(normalized)) return "cyan";
56998
+ if (/progress|doing/.test(normalized)) return "yellow";
56999
+ if (/review/.test(normalized)) return "blue";
57000
+ if (/done|archive|closed|complete/.test(normalized)) return "green";
57001
+ return "cyan";
57002
+ }
57003
+ function TaskRow({ task, focused, dragging, colWidth }) {
57004
+ const cursor = dragging ? "\u2195" : focused ? "\u25B8" : " ";
56851
57005
  const check2 = task.checked ? "\u2713" : "\u25CB";
56852
57006
  const idStr = task.id;
56853
57007
  const tagMatch = task.title.match(RE_BRACKET_TAG);
@@ -56857,15 +57011,15 @@ function TaskRow({ task, focused, colWidth }) {
56857
57011
  const tagChars = tag ? tag.length + 1 : 0;
56858
57012
  const titleStr = truncate(titleClean, Math.max(4, colWidth - fixedChars - tagChars));
56859
57013
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { children: [
56860
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: focused ? "cyan" : void 0, bold: focused, children: [
57014
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: dragging ? "yellow" : focused ? "cyan" : void 0, bold: focused || dragging, children: [
56861
57015
  cursor,
56862
57016
  " "
56863
57017
  ] }),
56864
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: task.checked ? "green" : focused ? "white" : "gray", children: [
57018
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: dragging ? "yellow" : task.checked ? "green" : focused ? "white" : "gray", children: [
56865
57019
  check2,
56866
57020
  " "
56867
57021
  ] }),
56868
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: focused ? "cyan" : "yellow", bold: focused, children: idStr }),
57022
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: dragging ? "yellow" : focused ? "cyan" : "yellow", bold: focused || dragging, children: idStr }),
56869
57023
  tag && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: focused ? "white" : "magenta", bold: true, children: [
56870
57024
  " ",
56871
57025
  tag
@@ -56899,10 +57053,12 @@ function KanbanColumn({
56899
57053
  contextMenuRow,
56900
57054
  contextMenuCursor,
56901
57055
  showMoveTarget,
56902
- isMoveFocused
57056
+ isMoveFocused,
57057
+ draggedTaskId
56903
57058
  }) {
56904
- const headerBg = isFocused ? "cyan" : void 0;
56905
- const headerColor = isFocused ? "black" : "cyan";
57059
+ const accent = columnAccentColor(name);
57060
+ const headerBg = isFocused ? accent : void 0;
57061
+ const headerColor = isFocused ? "black" : accent;
56906
57062
  const countStr = tasks.length > 0 ? ` (${tasks.length})` : "";
56907
57063
  const rows = [];
56908
57064
  tasks.forEach((task, idx) => {
@@ -56912,6 +57068,7 @@ function KanbanColumn({
56912
57068
  {
56913
57069
  task,
56914
57070
  focused: isFocused && idx === focusedRow,
57071
+ dragging: task.id === draggedTaskId,
56915
57072
  colWidth
56916
57073
  },
56917
57074
  task.id
@@ -56940,34 +57097,46 @@ function KanbanColumn({
56940
57097
  showMoveTarget && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(MovePlaceholder, { name, focused: !!isMoveFocused, colWidth })
56941
57098
  ] });
56942
57099
  }
56943
- function BoardHeader({ title, inTmux, modeHint, version }) {
57100
+ function BoardHeader({ title, inTmux, modeHint, version, daemonStatus, daemonBusy }) {
56944
57101
  const tmuxHint = inTmux ? " tmux" : "";
56945
57102
  const versionTag = version ? ` v${version}` : "";
56946
- 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";
56947
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, justifyContent: "space-between", children: [
56948
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { bold: true, color: "cyan", children: [
57103
+ const daemonLabel = daemonBusy ? "\u25CC daemon\u2026" : daemonStatus.running ? `\u25CF web ${daemonStatus.metadata?.port ?? ""}` : "\u25CB web off";
57104
+ const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 d daemon \xB7 r reload \xB7 q quit";
57105
+ const width = termWidth();
57106
+ const leftWidth = Math.min(Math.max(34, Math.floor(width * 0.46)), width);
57107
+ const daemonWidth = Math.min(16, Math.max(0, width - leftWidth));
57108
+ const rightWidth = Math.max(0, width - leftWidth - daemonWidth);
57109
+ const left = pad(` \u25C6 KANDOWN${tmuxHint}${versionTag} ${title}`, leftWidth);
57110
+ const daemon = pad(daemonLabel, daemonWidth);
57111
+ const right = truncate(hint, rightWidth).padStart(rightWidth, " ");
57112
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, children: [
57113
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { bold: true, color: "cyan", children: left }),
57114
+ daemonWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: daemonStatus.running ? "green" : "yellow", bold: true, children: daemon }),
57115
+ rightWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", dimColor: true, children: right })
57116
+ ] });
57117
+ }
57118
+ function StatusBar({ message, task, daemonStatus }) {
57119
+ const daemonText = daemonStatus.running && daemonStatus.metadata ? `web daemon ON \xB7 ${daemonStatus.metadata.url}` : "web daemon OFF \xB7 press d to start";
57120
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginTop: 1, flexDirection: "column", children: [
57121
+ message ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", bold: true, children: [
57122
+ " \u2726 ",
57123
+ message
57124
+ ] }) : task ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", children: [
56949
57125
  " ",
56950
- "KANDOWN",
56951
- tmuxHint,
56952
- versionTag,
57126
+ "\u25C6 ",
57127
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: task.id }),
57128
+ task.progress ? ` checklist ${task.progress.done}/${task.progress.total}` : "",
56953
57129
  " ",
56954
- title
56955
- ] }),
56956
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", dimColor: true, children: hint })
57130
+ task.checked ? "\u2713 done" : "\u25CB open"
57131
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " " }),
57132
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: daemonStatus.running ? "green" : "yellow", dimColor: !daemonStatus.running, children: [
57133
+ " ",
57134
+ daemonStatus.running ? "\u25CF" : "\u25CB",
57135
+ " ",
57136
+ daemonText
57137
+ ] })
56957
57138
  ] });
56958
57139
  }
56959
- function StatusBar({ message, task }) {
56960
- if (message) {
56961
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: message }) });
56962
- }
56963
- if (!task) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " " }) });
56964
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "gray", children: [
56965
- task.id,
56966
- task.progress ? ` (${task.progress.done}/${task.progress.total})` : "",
56967
- " ",
56968
- task.checked ? "\u2713 done" : "\u25CB open"
56969
- ] }) });
56970
- }
56971
57140
  function TaskDetail({ task, taskId, scrollOffset }) {
56972
57141
  const fm = task.frontmatter;
56973
57142
  const bodyLines = task.body.split("\n");
@@ -57029,6 +57198,11 @@ function Board({ kandownDir, version }) {
57029
57198
  const [ctxMenuCursor, setCtxMenuCursor] = (0, import_react37.useState)(0);
57030
57199
  const [moveTaskId, setMoveTaskId] = (0, import_react37.useState)(null);
57031
57200
  const [moveTargetCol, setMoveTargetCol] = (0, import_react37.useState)(0);
57201
+ const [mousePress, setMousePress] = (0, import_react37.useState)(null);
57202
+ const [taskDrag, setTaskDrag] = (0, import_react37.useState)(null);
57203
+ const [daemonStatus, setDaemonStatus] = (0, import_react37.useState)({ running: false, metadata: null });
57204
+ const [daemonBusy, setDaemonBusy] = (0, import_react37.useState)(false);
57205
+ const [preferredDaemonPort, setPreferredDaemonPort] = (0, import_react37.useState)(null);
57032
57206
  const [installedAgents, setInstalledAgents] = (0, import_react37.useState)([]);
57033
57207
  const inTmux = isInTmux();
57034
57208
  const layoutRef = (0, import_react37.useRef)({
@@ -57046,6 +57220,25 @@ function Board({ kandownDir, version }) {
57046
57220
  }
57047
57221
  layoutRef.current = { colStarts: starts, colWidth: cw };
57048
57222
  }, []);
57223
+ const columnAtX = (0, import_react37.useCallback)((x) => {
57224
+ const layout = layoutRef.current;
57225
+ for (let c = 0; c < layout.colStarts.length; c++) {
57226
+ const start = layout.colStarts[c];
57227
+ if (x >= start && x < start + layout.colWidth) return c;
57228
+ }
57229
+ return -1;
57230
+ }, []);
57231
+ const taskHitAt = (0, import_react37.useCallback)((x, y) => {
57232
+ if (!board) return null;
57233
+ const clickedCol = columnAtX(x);
57234
+ if (clickedCol < 0) return null;
57235
+ const col = board.columns[clickedCol];
57236
+ if (!col) return null;
57237
+ const taskIdx = y - TASKS_START_Y;
57238
+ const task = taskIdx >= 0 ? col.tasks[taskIdx] : void 0;
57239
+ if (!task) return null;
57240
+ return { taskId: task.id, colIndex: clickedCol, rowIndex: taskIdx, startX: x, startY: y };
57241
+ }, [board, columnAtX]);
57049
57242
  (0, import_react37.useEffect)(() => {
57050
57243
  const loaded = readBoard(kandownDir);
57051
57244
  setBoard(loaded);
@@ -57083,6 +57276,42 @@ function Board({ kandownDir, version }) {
57083
57276
  setStatusMsg("Board reloaded");
57084
57277
  setTimeout(() => setStatusMsg(""), 1500);
57085
57278
  }, [kandownDir, updateLayout]);
57279
+ const refreshDaemonStatus = (0, import_react37.useCallback)(async () => {
57280
+ const next = await getDaemonStatus(kandownDir);
57281
+ setDaemonStatus(next);
57282
+ if (next.running && next.metadata) setPreferredDaemonPort(next.metadata.port);
57283
+ }, [kandownDir]);
57284
+ (0, import_react37.useEffect)(() => {
57285
+ void refreshDaemonStatus();
57286
+ const timer = setInterval(() => {
57287
+ void refreshDaemonStatus();
57288
+ }, 2e3);
57289
+ return () => clearInterval(timer);
57290
+ }, [refreshDaemonStatus]);
57291
+ const toggleDaemon = (0, import_react37.useCallback)(async () => {
57292
+ if (daemonBusy) return;
57293
+ setDaemonBusy(true);
57294
+ try {
57295
+ const current = await getDaemonStatus(kandownDir);
57296
+ if (current.running) {
57297
+ if (current.metadata) setPreferredDaemonPort(current.metadata.port);
57298
+ await stopProjectDaemon(kandownDir);
57299
+ const next = await getDaemonStatus(kandownDir);
57300
+ setDaemonStatus(next);
57301
+ setStatusMsg("Web daemon stopped");
57302
+ } else {
57303
+ const next = await startProjectDaemon(kandownDir, preferredDaemonPort);
57304
+ setDaemonStatus(next);
57305
+ if (next.running && next.metadata) setPreferredDaemonPort(next.metadata.port);
57306
+ setStatusMsg(next.running ? "Web daemon started" : "Web daemon failed to start");
57307
+ }
57308
+ } catch (error) {
57309
+ setStatusMsg(`Daemon error: ${error instanceof Error ? error.message : String(error)}`);
57310
+ } finally {
57311
+ setDaemonBusy(false);
57312
+ setTimeout(() => setStatusMsg(""), 2500);
57313
+ }
57314
+ }, [daemonBusy, kandownDir, preferredDaemonPort]);
57086
57315
  const getFocusedTask = (0, import_react37.useCallback)(() => {
57087
57316
  if (!board) return null;
57088
57317
  const col = board.columns[colIndex];
@@ -57239,12 +57468,87 @@ function Board({ kandownDir, version }) {
57239
57468
  return;
57240
57469
  }
57241
57470
  }, [board, mode, colIndex, rowIndex, ctxMenuRow, moveTaskId, kandownDir, updateLayout, openDetail, closeContextMenu]);
57471
+ const handleMouseEvent = (0, import_react37.useCallback)((mouse) => {
57472
+ if (!board) return;
57473
+ if (mode === "browse") {
57474
+ if (mouse.action === "press" && mouse.button === 0) {
57475
+ const hit = taskHitAt(mouse.x, mouse.y);
57476
+ if (hit) {
57477
+ setMousePress(hit);
57478
+ setColIndex(hit.colIndex);
57479
+ setRowIndex(hit.rowIndex);
57480
+ }
57481
+ return;
57482
+ }
57483
+ if (mouse.action === "drag" && mousePress) {
57484
+ const delta = Math.max(Math.abs(mouse.x - mousePress.startX), Math.abs(mouse.y - mousePress.startY));
57485
+ if (delta < 1) return;
57486
+ const hoverCol = columnAtX(mouse.x);
57487
+ setTaskDrag({ taskId: mousePress.taskId, sourceCol: mousePress.colIndex, hoverCol });
57488
+ setMoveTargetCol(hoverCol >= 0 ? hoverCol : mousePress.colIndex);
57489
+ setMoveTaskId(mousePress.taskId);
57490
+ setMode("dragging");
57491
+ closeContextMenu();
57492
+ return;
57493
+ }
57494
+ if (mouse.action === "release" && mousePress) {
57495
+ const hit = taskHitAt(mouse.x, mouse.y);
57496
+ if (hit && hit.taskId === mousePress.taskId) {
57497
+ setCtxMenuRow(mousePress.rowIndex);
57498
+ setCtxMenuCursor(0);
57499
+ setMode("context-menu");
57500
+ }
57501
+ setMousePress(null);
57502
+ return;
57503
+ }
57504
+ if (mouse.action === "press") handleMouseClick(mouse.x, mouse.y);
57505
+ return;
57506
+ }
57507
+ if (mode === "dragging") {
57508
+ if (!taskDrag) {
57509
+ setMode("browse");
57510
+ setMoveTaskId(null);
57511
+ setMousePress(null);
57512
+ return;
57513
+ }
57514
+ if (mouse.action === "drag") {
57515
+ const hoverCol = columnAtX(mouse.x);
57516
+ setTaskDrag((current) => current ? { ...current, hoverCol } : current);
57517
+ setMoveTargetCol(hoverCol >= 0 ? hoverCol : taskDrag.sourceCol);
57518
+ return;
57519
+ }
57520
+ if (mouse.action === "release") {
57521
+ const targetCol = columnAtX(mouse.x);
57522
+ if (targetCol >= 0 && targetCol !== taskDrag.sourceCol) {
57523
+ const targetColName = board.columns[targetCol]?.name;
57524
+ if (targetColName) {
57525
+ moveTaskToColumn(kandownDir, taskDrag.taskId, targetColName);
57526
+ const loaded = readBoard(kandownDir);
57527
+ setBoard(loaded);
57528
+ updateLayout(loaded);
57529
+ setColIndex(targetCol);
57530
+ const movedRow = loaded.columns[targetCol]?.tasks.findIndex((task) => task.id === taskDrag.taskId) ?? 0;
57531
+ setRowIndex(Math.max(0, movedRow));
57532
+ setStatusMsg(`Dragged ${taskDrag.taskId} \u2192 ${targetColName}`);
57533
+ setTimeout(() => setStatusMsg(""), 2e3);
57534
+ }
57535
+ }
57536
+ setTaskDrag(null);
57537
+ setMousePress(null);
57538
+ setMoveTaskId(null);
57539
+ setMode("browse");
57540
+ return;
57541
+ }
57542
+ return;
57543
+ }
57544
+ if (mouse.action === "press" && mouse.button === 0) {
57545
+ handleMouseClick(mouse.x, mouse.y);
57546
+ }
57547
+ }, [board, mode, mousePress, taskDrag, taskHitAt, columnAtX, closeContextMenu, handleMouseClick, kandownDir, updateLayout]);
57242
57548
  use_input_default((input, key) => {
57243
57549
  if (isMouseInput(input)) {
57244
57550
  const mouse = parseMouseInput(input);
57245
- if (mouse && mouse.action === "press" && mouse.button === 0) {
57246
- handleMouseClick(mouse.x, mouse.y);
57247
- }
57551
+ if (mouse) handleMouseEvent(mouse);
57248
57552
  return;
57249
57553
  }
57250
57554
  if (mode === "browse") {
@@ -57256,6 +57560,10 @@ function Board({ kandownDir, version }) {
57256
57560
  reloadBoard();
57257
57561
  return;
57258
57562
  }
57563
+ if (input === "d") {
57564
+ void toggleDaemon();
57565
+ return;
57566
+ }
57259
57567
  if (input === "l" || key.rightArrow) {
57260
57568
  const maxCol = (board?.columns.length ?? 1) - 1;
57261
57569
  setColIndex((c) => Math.min(c + 1, maxCol));
@@ -57335,6 +57643,16 @@ function Board({ kandownDir, version }) {
57335
57643
  return;
57336
57644
  }
57337
57645
  }
57646
+ if (mode === "dragging") {
57647
+ if (key.escape || input === "q") {
57648
+ setTaskDrag(null);
57649
+ setMousePress(null);
57650
+ setMoveTaskId(null);
57651
+ setMode("browse");
57652
+ return;
57653
+ }
57654
+ return;
57655
+ }
57338
57656
  if (mode === "move-target") {
57339
57657
  if (key.escape || input === "q") {
57340
57658
  setMoveTaskId(null);
@@ -57418,11 +57736,13 @@ function Board({ kandownDir, version }) {
57418
57736
  modeHint = "j/k choose \xB7 Enter confirm \xB7 Esc cancel \xB7 or click";
57419
57737
  } else if (mode === "move-target") {
57420
57738
  modeHint = "\u2190/\u2192 pick column \xB7 Enter confirm \xB7 Esc cancel \xB7 or click \u2193";
57739
+ } else if (mode === "dragging") {
57740
+ modeHint = "drag over target column \xB7 release to drop \xB7 Esc cancel";
57421
57741
  }
57422
57742
  if (mode === "agent-picker") {
57423
57743
  const taskId = detailTaskId || focusedTask?.id || "";
57424
57744
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
57425
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, version }),
57745
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, version, daemonStatus, daemonBusy }),
57426
57746
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
57427
57747
  AgentPicker,
57428
57748
  {
@@ -57448,7 +57768,7 @@ function Board({ kandownDir, version }) {
57448
57768
  ] });
57449
57769
  }
57450
57770
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", children: [
57451
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, modeHint, version }),
57771
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BoardHeader, { title: board.title, inTmux, modeHint, version, daemonStatus, daemonBusy }),
57452
57772
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { flexDirection: "row", children: board.columns.map((col, cIdx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
57453
57773
  KanbanColumn,
57454
57774
  {
@@ -57459,36 +57779,57 @@ function Board({ kandownDir, version }) {
57459
57779
  colWidth,
57460
57780
  contextMenuRow: mode === "context-menu" && cIdx === colIndex ? ctxMenuRow : -1,
57461
57781
  contextMenuCursor: ctxMenuCursor,
57462
- showMoveTarget: mode === "move-target" && cIdx !== colIndex,
57463
- isMoveFocused: mode === "move-target" && cIdx === moveTargetCol
57782
+ showMoveTarget: mode === "move-target" && cIdx !== colIndex || mode === "dragging" && cIdx !== taskDrag?.sourceCol,
57783
+ isMoveFocused: (mode === "move-target" || mode === "dragging") && cIdx === moveTargetCol,
57784
+ draggedTaskId: taskDrag?.taskId ?? null
57464
57785
  },
57465
57786
  col.name
57466
57787
  )) }),
57467
- mode === "move-target" && moveTaskId && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", bold: true, children: [
57788
+ (mode === "move-target" || mode === "dragging") && moveTaskId && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", bold: true, children: [
57468
57789
  "Moving ",
57469
57790
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "cyan", children: moveTaskId }),
57470
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " \u2014 click " }),
57471
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: "\u2193" }),
57472
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " or \u2190/\u2192 + Enter" })
57791
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: " \u2014 " }),
57792
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", bold: true, children: mode === "dragging" ? "release over a column" : "click \u2193" }),
57793
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: mode === "dragging" ? " \xB7 Esc cancel" : " or \u2190/\u2192 + Enter" })
57473
57794
  ] }) }),
57474
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBar, { message: statusMsg, task: focusedTask })
57795
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBar, { message: statusMsg, task: focusedTask, daemonStatus })
57475
57796
  ] });
57476
57797
  }
57477
57798
 
57478
57799
  // src/cli/app.tsx
57479
57800
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
57801
+ function getTerminalRows() {
57802
+ const rows = process.stdout.rows;
57803
+ return typeof rows === "number" && Number.isFinite(rows) && rows > 0 ? rows : 24;
57804
+ }
57805
+ function useTerminalRows() {
57806
+ const [rows, setRows] = (0, import_react38.useState)(getTerminalRows);
57807
+ (0, import_react38.useEffect)(() => {
57808
+ const handleResize = () => setRows(getTerminalRows());
57809
+ process.stdout.on("resize", handleResize);
57810
+ return () => {
57811
+ process.stdout.off("resize", handleResize);
57812
+ };
57813
+ }, []);
57814
+ return rows;
57815
+ }
57480
57816
  function App2({ screen, kandownDir, version }) {
57817
+ const rows = useTerminalRows();
57818
+ let content;
57481
57819
  switch (screen) {
57482
57820
  case "settings":
57483
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Settings, { kandownDir, version });
57821
+ content = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Settings, { kandownDir, version });
57822
+ break;
57484
57823
  case "board":
57485
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Board, { kandownDir, version });
57824
+ content = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Board, { kandownDir, version });
57825
+ break;
57486
57826
  default:
57487
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { color: "red", bold: true, children: [
57827
+ content = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { padding: 2, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Text, { color: "red", bold: true, children: [
57488
57828
  "Unknown screen: ",
57489
57829
  screen
57490
57830
  ] }) });
57491
57831
  }
57832
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Box_default, { flexDirection: "column", height: rows, overflow: "hidden", children: content });
57492
57833
  }
57493
57834
 
57494
57835
  // src/cli/tui.tsx
@@ -57499,15 +57840,13 @@ async function run(screen, kandownDir, version) {
57499
57840
  "kandown TUI requires an interactive terminal. Run this command directly in your terminal."
57500
57841
  );
57501
57842
  }
57502
- process.stdout.write("\x1B[?1049h\x1B[H");
57503
57843
  const instance = render_default(/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(App2, { screen, kandownDir, version }), {
57504
- exitOnCtrlC: true
57844
+ exitOnCtrlC: true,
57845
+ interactive: true,
57846
+ alternateScreen: true,
57847
+ maxFps: 30
57505
57848
  });
57506
- try {
57507
- await instance.waitUntilExit();
57508
- } finally {
57509
- process.stdout.write("\x1B[?1049l");
57510
- }
57849
+ await instance.waitUntilExit();
57511
57850
  }
57512
57851
  export {
57513
57852
  run