brainfile 0.19.0 → 0.20.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.
Files changed (82) hide show
  1. package/README.md +18 -24
  2. package/dist/__tests__/helpers/mcp-wire.d.ts +45 -0
  3. package/dist/__tests__/helpers/mcp-wire.d.ts.map +1 -0
  4. package/dist/__tests__/helpers/mcp-wire.js +133 -0
  5. package/dist/__tests__/helpers/mcp-wire.js.map +1 -0
  6. package/dist/{chunk-YTFLC673.mjs → chunk-UOCQBEEW.mjs} +1486 -992
  7. package/dist/cli.mjs +916 -776
  8. package/dist/commands/adr.d.ts +1 -9
  9. package/dist/commands/adr.d.ts.map +1 -1
  10. package/dist/commands/adr.js +11 -88
  11. package/dist/commands/adr.js.map +1 -1
  12. package/dist/commands/archive.d.ts +2 -5
  13. package/dist/commands/archive.d.ts.map +1 -1
  14. package/dist/commands/archive.js +43 -11
  15. package/dist/commands/archive.js.map +1 -1
  16. package/dist/commands/brief.d.ts +31 -0
  17. package/dist/commands/brief.d.ts.map +1 -0
  18. package/dist/commands/brief.js +151 -0
  19. package/dist/commands/brief.js.map +1 -0
  20. package/dist/commands/complete.js +1 -1
  21. package/dist/commands/complete.js.map +1 -1
  22. package/dist/commands/log.js +31 -18
  23. package/dist/commands/log.js.map +1 -1
  24. package/dist/commands/mcp.d.ts +9 -0
  25. package/dist/commands/mcp.d.ts.map +1 -1
  26. package/dist/commands/mcp.js +28 -17
  27. package/dist/commands/mcp.js.map +1 -1
  28. package/dist/commands/migrate.js +10 -12
  29. package/dist/commands/migrate.js.map +1 -1
  30. package/dist/mcp/schemas.d.ts +297 -0
  31. package/dist/mcp/schemas.d.ts.map +1 -0
  32. package/dist/mcp/schemas.js +328 -0
  33. package/dist/mcp/schemas.js.map +1 -0
  34. package/dist/mcp/tools/brief_tool.d.ts +14 -0
  35. package/dist/mcp/tools/brief_tool.d.ts.map +1 -0
  36. package/dist/mcp/tools/brief_tool.js +58 -0
  37. package/dist/mcp/tools/brief_tool.js.map +1 -0
  38. package/dist/mcp/tools/contract_tool.d.ts.map +1 -1
  39. package/dist/mcp/tools/contract_tool.js +37 -13
  40. package/dist/mcp/tools/contract_tool.js.map +1 -1
  41. package/dist/mcp/tools/get_task_tool.d.ts.map +1 -1
  42. package/dist/mcp/tools/get_task_tool.js +7 -2
  43. package/dist/mcp/tools/get_task_tool.js.map +1 -1
  44. package/dist/mcp/tools/list_tasks_tool.d.ts.map +1 -1
  45. package/dist/mcp/tools/list_tasks_tool.js +9 -3
  46. package/dist/mcp/tools/list_tasks_tool.js.map +1 -1
  47. package/dist/mcp/tools/search_tool.d.ts.map +1 -1
  48. package/dist/mcp/tools/search_tool.js +17 -4
  49. package/dist/mcp/tools/search_tool.js.map +1 -1
  50. package/dist/mcp/tools/subtask_tool.d.ts.map +1 -1
  51. package/dist/mcp/tools/subtask_tool.js +47 -13
  52. package/dist/mcp/tools/subtask_tool.js.map +1 -1
  53. package/dist/mcp/tools/task_add_tool.d.ts.map +1 -1
  54. package/dist/mcp/tools/task_add_tool.js +7 -2
  55. package/dist/mcp/tools/task_add_tool.js.map +1 -1
  56. package/dist/mcp/tools/task_complete_tool.d.ts.map +1 -1
  57. package/dist/mcp/tools/task_complete_tool.js +34 -6
  58. package/dist/mcp/tools/task_complete_tool.js.map +1 -1
  59. package/dist/mcp/tools/task_delete_tool.d.ts.map +1 -1
  60. package/dist/mcp/tools/task_delete_tool.js +7 -2
  61. package/dist/mcp/tools/task_delete_tool.js.map +1 -1
  62. package/dist/mcp/tools/task_move_tool.d.ts.map +1 -1
  63. package/dist/mcp/tools/task_move_tool.js +15 -6
  64. package/dist/mcp/tools/task_move_tool.js.map +1 -1
  65. package/dist/mcp/tools/task_patch_tool.d.ts.map +1 -1
  66. package/dist/mcp/tools/task_patch_tool.js +18 -6
  67. package/dist/mcp/tools/task_patch_tool.js.map +1 -1
  68. package/dist/schemas/board.json +1 -1
  69. package/dist/tui/theme.d.ts +98 -0
  70. package/dist/tui/theme.d.ts.map +1 -0
  71. package/dist/tui/theme.js +141 -0
  72. package/dist/tui/theme.js.map +1 -0
  73. package/dist/{tui-P56H4X7N.mjs → tui-H3RI22S4.mjs} +1593 -1934
  74. package/dist/utils/cli-error.d.ts +20 -0
  75. package/dist/utils/cli-error.d.ts.map +1 -1
  76. package/dist/utils/cli-error.js +36 -0
  77. package/dist/utils/cli-error.js.map +1 -1
  78. package/package.json +3 -3
  79. package/dist/commands/rules.d.ts +0 -68
  80. package/dist/commands/rules.d.ts.map +0 -1
  81. package/dist/commands/rules.js +0 -322
  82. package/dist/commands/rules.js.map +0 -1
@@ -5,11 +5,16 @@ const require = __brainfileCreateRequire(import.meta.url);
5
5
  const __filename = __brainfileFileURLToPath(import.meta.url);
6
6
  const __dirname = __brainfileDirname(__filename);
7
7
  import {
8
+ GLYPHS,
9
+ PALETTE,
10
+ RULE,
8
11
  createGitHubIssue,
9
12
  createLinearIssue,
10
13
  getArchiveConfig,
14
+ getContractStateColor,
11
15
  getEffectiveDestination,
12
16
  getLinearTeams,
17
+ getTypeGlyph,
13
18
  import_core,
14
19
  isGitHubAuthenticated,
15
20
  isLinearAuthenticated,
@@ -18,7 +23,7 @@ import {
18
23
  require_source,
19
24
  resolveCliBrainfilePath,
20
25
  shouldSuggestV2Migration
21
- } from "./chunk-YTFLC673.mjs";
26
+ } from "./chunk-UOCQBEEW.mjs";
22
27
  import {
23
28
  __commonJS,
24
29
  __export,
@@ -406,8 +411,8 @@ var require_react_production = __commonJS({
406
411
  exports.use = function(usable) {
407
412
  return ReactSharedInternals.H.use(usable);
408
413
  };
409
- exports.useActionState = function(action, initialState2, permalink) {
410
- return ReactSharedInternals.H.useActionState(action, initialState2, permalink);
414
+ exports.useActionState = function(action, initialState, permalink) {
415
+ return ReactSharedInternals.H.useActionState(action, initialState, permalink);
411
416
  };
412
417
  exports.useCallback = function(callback, deps) {
413
418
  return ReactSharedInternals.H.useCallback(callback, deps);
@@ -450,8 +455,8 @@ var require_react_production = __commonJS({
450
455
  exports.useRef = function(initialValue) {
451
456
  return ReactSharedInternals.H.useRef(initialValue);
452
457
  };
453
- exports.useState = function(initialState2) {
454
- return ReactSharedInternals.H.useState(initialState2);
458
+ exports.useState = function(initialState) {
459
+ return ReactSharedInternals.H.useState(initialState);
455
460
  };
456
461
  exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
457
462
  return ReactSharedInternals.H.useSyncExternalStore(
@@ -3227,11 +3232,11 @@ var require_react_reconciler_production = __commonJS({
3227
3232
  var root = enqueueConcurrentRenderForLane(fiber, 2);
3228
3233
  null !== root && scheduleUpdateOnFiber(root, fiber, 2);
3229
3234
  }
3230
- function mountStateImpl(initialState2) {
3235
+ function mountStateImpl(initialState) {
3231
3236
  var hook = mountWorkInProgressHook();
3232
- if ("function" === typeof initialState2) {
3233
- var initialStateInitializer = initialState2;
3234
- initialState2 = initialStateInitializer();
3237
+ if ("function" === typeof initialState) {
3238
+ var initialStateInitializer = initialState;
3239
+ initialState = initialStateInitializer();
3235
3240
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
3236
3241
  setIsStrictModeForDevtools(true);
3237
3242
  try {
@@ -3241,13 +3246,13 @@ var require_react_reconciler_production = __commonJS({
3241
3246
  }
3242
3247
  }
3243
3248
  }
3244
- hook.memoizedState = hook.baseState = initialState2;
3249
+ hook.memoizedState = hook.baseState = initialState;
3245
3250
  hook.queue = {
3246
3251
  pending: null,
3247
3252
  lanes: 0,
3248
3253
  dispatch: null,
3249
3254
  lastRenderedReducer: basicStateReducer,
3250
- lastRenderedState: initialState2
3255
+ lastRenderedState: initialState
3251
3256
  };
3252
3257
  return hook;
3253
3258
  }
@@ -8378,14 +8383,14 @@ var require_react_reconciler_production = __commonJS({
8378
8383
  }
8379
8384
  var exports2 = {};
8380
8385
  "use strict";
8381
- var React22 = require_react(), Scheduler2 = require_scheduler(), assign = Object.assign, REACT_LEGACY_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.element"), REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy");
8386
+ var React20 = require_react(), Scheduler2 = require_scheduler(), assign = Object.assign, REACT_LEGACY_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.element"), REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy");
8382
8387
  /* @__PURE__ */ Symbol.for("react.scope");
8383
8388
  var REACT_ACTIVITY_TYPE = /* @__PURE__ */ Symbol.for("react.activity");
8384
8389
  /* @__PURE__ */ Symbol.for("react.legacy_hidden");
8385
8390
  /* @__PURE__ */ Symbol.for("react.tracing_marker");
8386
8391
  var REACT_MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel");
8387
8392
  /* @__PURE__ */ Symbol.for("react.view_transition");
8388
- var MAYBE_ITERATOR_SYMBOL = Symbol.iterator, REACT_CLIENT_REFERENCE = /* @__PURE__ */ Symbol.for("react.client.reference"), isArrayImpl = Array.isArray, ReactSharedInternals = React22.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, rendererVersion = $$$config.rendererVersion, rendererPackageName = $$$config.rendererPackageName, extraDevToolsConfig = $$$config.extraDevToolsConfig, getPublicInstance = $$$config.getPublicInstance, getRootHostContext = $$$config.getRootHostContext, getChildHostContext = $$$config.getChildHostContext, prepareForCommit = $$$config.prepareForCommit, resetAfterCommit = $$$config.resetAfterCommit, createInstance = $$$config.createInstance;
8393
+ var MAYBE_ITERATOR_SYMBOL = Symbol.iterator, REACT_CLIENT_REFERENCE = /* @__PURE__ */ Symbol.for("react.client.reference"), isArrayImpl = Array.isArray, ReactSharedInternals = React20.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, rendererVersion = $$$config.rendererVersion, rendererPackageName = $$$config.rendererPackageName, extraDevToolsConfig = $$$config.extraDevToolsConfig, getPublicInstance = $$$config.getPublicInstance, getRootHostContext = $$$config.getRootHostContext, getChildHostContext = $$$config.getChildHostContext, prepareForCommit = $$$config.prepareForCommit, resetAfterCommit = $$$config.resetAfterCommit, createInstance = $$$config.createInstance;
8389
8394
  $$$config.cloneMutableInstance;
8390
8395
  var appendInitialChild = $$$config.appendInitialChild, finalizeInitialChildren = $$$config.finalizeInitialChildren, shouldSetTextContent = $$$config.shouldSetTextContent, createTextInstance = $$$config.createTextInstance;
8391
8396
  $$$config.cloneMutableTextInstance;
@@ -8547,7 +8552,7 @@ var require_react_reconciler_production = __commonJS({
8547
8552
  useReducer: function(reducer, initialArg, init) {
8548
8553
  var hook = mountWorkInProgressHook();
8549
8554
  if (void 0 !== init) {
8550
- var initialState2 = init(initialArg);
8555
+ var initialState = init(initialArg);
8551
8556
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
8552
8557
  setIsStrictModeForDevtools(true);
8553
8558
  try {
@@ -8556,14 +8561,14 @@ var require_react_reconciler_production = __commonJS({
8556
8561
  setIsStrictModeForDevtools(false);
8557
8562
  }
8558
8563
  }
8559
- } else initialState2 = initialArg;
8560
- hook.memoizedState = hook.baseState = initialState2;
8564
+ } else initialState = initialArg;
8565
+ hook.memoizedState = hook.baseState = initialState;
8561
8566
  reducer = {
8562
8567
  pending: null,
8563
8568
  lanes: 0,
8564
8569
  dispatch: null,
8565
8570
  lastRenderedReducer: reducer,
8566
- lastRenderedState: initialState2
8571
+ lastRenderedState: initialState
8567
8572
  };
8568
8573
  hook.queue = reducer;
8569
8574
  reducer = reducer.dispatch = dispatchReducerAction.bind(
@@ -8578,15 +8583,15 @@ var require_react_reconciler_production = __commonJS({
8578
8583
  initialValue = { current: initialValue };
8579
8584
  return hook.memoizedState = initialValue;
8580
8585
  },
8581
- useState: function(initialState2) {
8582
- initialState2 = mountStateImpl(initialState2);
8583
- var queue = initialState2.queue, dispatch = dispatchSetState.bind(
8586
+ useState: function(initialState) {
8587
+ initialState = mountStateImpl(initialState);
8588
+ var queue = initialState.queue, dispatch = dispatchSetState.bind(
8584
8589
  null,
8585
8590
  currentlyRenderingFiber,
8586
8591
  queue
8587
8592
  );
8588
8593
  queue.dispatch = dispatch;
8589
- return [initialState2.memoizedState, dispatch];
8594
+ return [initialState.memoizedState, dispatch];
8590
8595
  },
8591
8596
  useDebugValue: mountDebugValue,
8592
8597
  useDeferredValue: function(value, initialValue) {
@@ -9498,7 +9503,7 @@ var require_stack_utils = __commonJS({
9498
9503
  });
9499
9504
 
9500
9505
  // src/commands/tui.tsx
9501
- var import_react46 = __toESM(require_react());
9506
+ var import_react44 = __toESM(require_react());
9502
9507
 
9503
9508
  // node_modules/ink/build/render.js
9504
9509
  import { Stream } from "node:stream";
@@ -14501,8 +14506,8 @@ var cleanupYogaNode = (node) => {
14501
14506
  var currentUpdatePriority = import_constants.NoEventPriority;
14502
14507
  var currentRootNode;
14503
14508
  async function loadPackageJson() {
14504
- const fs6 = await import("node:fs");
14505
- const content = fs6.readFileSync(new URL("../package.json", import.meta.url), "utf8");
14509
+ const fs7 = await import("node:fs");
14510
+ const content = fs7.readFileSync(new URL("../package.json", import.meta.url), "utf8");
14506
14511
  const parsedContent = JSON.parse(content);
14507
14512
  return {
14508
14513
  name: parsedContent?.name,
@@ -17092,8 +17097,8 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
17092
17097
  }
17093
17098
 
17094
17099
  // node_modules/ink/build/components/ErrorOverview.js
17095
- var cleanupPath = (path3) => {
17096
- return path3?.replace(`file://${cwd()}/`, "");
17100
+ var cleanupPath = (path4) => {
17101
+ return path4?.replace(`file://${cwd()}/`, "");
17097
17102
  };
17098
17103
  var stackUtils = new import_stack_utils.default({
17099
17104
  cwd: cwd(),
@@ -19061,7 +19066,9 @@ var parse_keypress_default = parseKeypress;
19061
19066
 
19062
19067
  // node_modules/ink/build/hooks/use-stdin.js
19063
19068
  var import_react21 = __toESM(require_react(), 1);
19069
+ var useStdin = () => (0, import_react21.useContext)(StdinContext_default);
19064
19070
  var useStdinContext = () => (0, import_react21.useContext)(StdinContext_default);
19071
+ var use_stdin_default = useStdin;
19065
19072
 
19066
19073
  // node_modules/ink/build/hooks/use-input.js
19067
19074
  var useInput = (inputHandler, options = {}) => {
@@ -19182,86 +19189,71 @@ var import_react33 = __toESM(require_react(), 1);
19182
19189
 
19183
19190
  // src/commands/tui.tsx
19184
19191
  var import_chalk4 = __toESM(require_source());
19185
- import * as fs5 from "fs";
19192
+ import * as fs6 from "fs";
19186
19193
 
19187
19194
  // src/tui/BrainfileTUI.tsx
19188
- var import_react45 = __toESM(require_react());
19189
- var import_core5 = __toESM(require_dist());
19195
+ var import_react43 = __toESM(require_react());
19196
+ var import_core6 = __toESM(require_dist());
19190
19197
 
19191
- // src/tui/theme.ts
19192
- var PALETTE = {
19193
- // Text hierarchy
19194
- text: "#ffffff",
19195
- textSecondary: "#a0a0a0",
19196
- textMuted: "#606060",
19197
- textDim: "#404040",
19198
- // Priority signal
19199
- critical: "#ef4444",
19200
- high: "#f59e0b",
19201
- medium: "#3b82f6",
19202
- low: "#6b7280",
19203
- giga: "#a855f7",
19204
- // Contract-state signal (design §3: draft dim · ready cyan · in_progress
19205
- // yellow · delivered magenta · done green · failed/blocked red)
19206
- contractDraft: "#606060",
19207
- contractReady: "#06b6d4",
19208
- contractInProgress: "#eab308",
19209
- contractDelivered: "#c026d3",
19210
- contractDone: "#22c55e",
19211
- contractFailed: "#ef4444",
19212
- // Feedback
19213
- success: "#22c55e",
19214
- warning: "#eab308",
19215
- error: "#ef4444",
19216
- info: "#3b82f6",
19217
- accent: "#7c3aed",
19218
- // Carried over for the rules and logs panels, which keep their v2
19219
- // presentation this round. The v3 board and detail views use neither: they
19220
- // have no borders, and selection is an inverse bar rather than a background.
19221
- border: "#333333",
19222
- bgHighlight: "#2a2a2a"
19198
+ // src/tui/copy.ts
19199
+ var STATUS = {
19200
+ completed: (id) => `Completed ${id}`,
19201
+ deleted: (id) => `Deleted ${id}`,
19202
+ moved: (column) => `Moved to ${column}`,
19203
+ added: (id) => `Added ${id}`,
19204
+ addedGeneric: "Added",
19205
+ titleRequired: "Title required",
19206
+ completeCancelled: "Complete cancelled",
19207
+ deleteCancelled: "Delete cancelled",
19208
+ readOnly: "Completed documents are read-only here",
19209
+ alreadyCompleted: "Already completed",
19210
+ cannotComplete: (id) => `${id} cannot be completed`,
19211
+ noDocument: "No document selected",
19212
+ noParent: "No parent",
19213
+ noSubtasks: "No subtasks",
19214
+ reloaded: "Reloaded",
19215
+ toggled: (id) => `Toggled ${id}`,
19216
+ copied: (id) => `Copied ${id}`,
19217
+ copyFailed: "Copy failed",
19218
+ editorNeedsTty: "$EDITOR needs an interactive terminal",
19219
+ editorFailed: (msg) => `Editor failed: ${msg}`,
19220
+ completeFailed: "Could not complete",
19221
+ deleteFailed: "Could not delete",
19222
+ moveFailed: "Could not move",
19223
+ addFailed: "Could not add",
19224
+ toggleFailed: "Could not toggle",
19225
+ priorityFailed: "Could not update priority",
19226
+ priorityUpdated: (priority) => `Priority: ${priority}`,
19227
+ archived: (id) => `Archived ${id}`,
19228
+ archiving: "Archiving\u2026",
19229
+ archiveFailed: "Could not archive"
19223
19230
  };
19224
- var RULE = "\u2500";
19225
- var TYPE_GLYPHS = {
19226
- epic: "\u25B8",
19227
- spec: "\u25C6",
19228
- adr: "\u25CF",
19229
- plan: "\u2398",
19230
- task: ""
19231
+ var EMPTY = {
19232
+ noMatches: "No matches \xB7 esc to clear",
19233
+ nothingCompleted: "Nothing completed yet \xB7 L board",
19234
+ emptyColumn: "No documents in this column \xB7 a add",
19235
+ noType: (type) => `No ${type} documents`,
19236
+ noDocuments: "No documents"
19231
19237
  };
19232
- function getTypeGlyph(type) {
19233
- if (!type) return "";
19234
- return TYPE_GLYPHS[type.toLowerCase()] ?? "";
19235
- }
19236
- var GLYPHS = {
19237
- pointer: "\u25B8",
19238
- cursor: "\u258C",
19239
- collapsed: "\u25B6",
19240
- success: "\u2713",
19241
- error: "\u2717",
19242
- warning: "\u26A0",
19243
- live: "\u25CF",
19244
- subtaskOpen: "\u25FB",
19245
- subtaskDone: "\u2611",
19246
- orphanParent: "\u2190"
19238
+ var CHROME = {
19239
+ filterAffordance: "/ filter ? help",
19240
+ helpTitle: "help",
19241
+ helpDismiss: "any key dismisses \xB7 q quit",
19242
+ tooSmall: "Terminal too small",
19243
+ loading: "Loading\u2026",
19244
+ errorTitle: "Error",
19245
+ errorHint: "q quit \xB7 r retry"
19247
19246
  };
19248
- function getContractStateColor(state) {
19249
- switch (state?.toLowerCase()) {
19250
- case "ready":
19251
- return PALETTE.contractReady;
19252
- case "in_progress":
19253
- return PALETTE.contractInProgress;
19254
- case "delivered":
19255
- return PALETTE.contractDelivered;
19256
- case "done":
19257
- return PALETTE.contractDone;
19258
- case "failed":
19259
- case "blocked":
19260
- return PALETTE.contractFailed;
19261
- case "draft":
19262
- default:
19263
- return PALETTE.contractDraft;
19264
- }
19247
+ function matchCountLabel(matchCount, totalCount, query) {
19248
+ if (!query) return "";
19249
+ const word = matchCount === 1 ? "match" : "matches";
19250
+ return `${matchCount}/${totalCount} ${word} "${query}"`;
19251
+ }
19252
+ function itemCountLabel(n) {
19253
+ return `${n} item${n === 1 ? "" : "s"}`;
19254
+ }
19255
+ function incompleteChildrenLabel(n) {
19256
+ return `${n} incomplete child task${n === 1 ? "" : "s"}:`;
19265
19257
  }
19266
19258
 
19267
19259
  // src/tui/types.ts
@@ -19277,7 +19269,7 @@ var HEADER_ROWS = 2;
19277
19269
  var FOOTER_ROWS = 3;
19278
19270
 
19279
19271
  // src/tui/rows.ts
19280
- function buildRows(tasks) {
19272
+ function buildRows(tasks, collapsedIds) {
19281
19273
  const visibleIds = new Set(tasks.map((t) => t.id));
19282
19274
  const emitted = /* @__PURE__ */ new Set();
19283
19275
  const rows = [];
@@ -19295,12 +19287,20 @@ function buildRows(tasks) {
19295
19287
  const parentVisible = Boolean(parentId && parentId !== task.id && visibleIds.has(parentId));
19296
19288
  if (parentVisible) continue;
19297
19289
  emitted.add(task.id);
19290
+ const children = childrenOf.get(task.id) ?? [];
19291
+ const isCollapsed = children.length > 0 && Boolean(collapsedIds?.has(task.id));
19298
19292
  rows.push({
19299
19293
  task,
19300
19294
  depth: 0,
19301
- orphanParentId: parentId && !parentVisible ? parentId : void 0
19295
+ orphanParentId: parentId && !parentVisible ? parentId : void 0,
19296
+ childCount: children.length > 0 ? children.length : void 0,
19297
+ collapsed: children.length > 0 ? isCollapsed : void 0
19302
19298
  });
19303
- for (const child of childrenOf.get(task.id) ?? []) {
19299
+ if (isCollapsed) {
19300
+ for (const child of children) emitted.add(child.id);
19301
+ continue;
19302
+ }
19303
+ for (const child of children) {
19304
19304
  if (emitted.has(child.id)) continue;
19305
19305
  emitted.add(child.id);
19306
19306
  rows.push({ task: child, depth: 1 });
@@ -19313,6 +19313,13 @@ function buildRows(tasks) {
19313
19313
  }
19314
19314
  return rows;
19315
19315
  }
19316
+ function buildFlatRows(tasks) {
19317
+ return tasks.map((task) => ({
19318
+ task,
19319
+ depth: 0,
19320
+ orphanParentId: task.parentId && task.parentId !== task.id ? task.parentId : void 0
19321
+ }));
19322
+ }
19316
19323
  function windowRows(rows, selectedIndex, viewportHeight) {
19317
19324
  if (rows.length <= viewportHeight) return { visible: rows, start: 0 };
19318
19325
  const half = Math.floor(viewportHeight / 2);
@@ -19321,10 +19328,89 @@ function windowRows(rows, selectedIndex, viewportHeight) {
19321
19328
  return { visible: rows.slice(start, start + viewportHeight), start };
19322
19329
  }
19323
19330
 
19331
+ // src/tui/utils.ts
19332
+ var import_core2 = __toESM(require_dist());
19333
+
19334
+ // src/tui/text.ts
19335
+ function truncate(text, maxWidth) {
19336
+ if (!text) return " ";
19337
+ if (maxWidth <= 0) return " ";
19338
+ if (maxWidth <= 3) return text.slice(0, maxWidth) || " ";
19339
+ if (text.length <= maxWidth) return text;
19340
+ return `${text.slice(0, maxWidth - 1)}\u2026`;
19341
+ }
19342
+ function truncateStart(text, maxWidth) {
19343
+ if (!text) return " ";
19344
+ if (maxWidth <= 0) return " ";
19345
+ if (maxWidth <= 1) return text.slice(-1) || " ";
19346
+ if (text.length <= maxWidth) return text;
19347
+ return `\u2026${text.slice(text.length - (maxWidth - 1))}`;
19348
+ }
19349
+ function wrapText2(text, maxWidth) {
19350
+ if (!text || maxWidth <= 0) return [];
19351
+ const words = text.split(/\s+/);
19352
+ const lines = [];
19353
+ let currentLine = "";
19354
+ for (const word of words) {
19355
+ if (word.length > maxWidth) {
19356
+ if (currentLine) {
19357
+ lines.push(currentLine);
19358
+ currentLine = "";
19359
+ }
19360
+ for (let i = 0; i < word.length; i += maxWidth) {
19361
+ lines.push(word.slice(i, i + maxWidth));
19362
+ }
19363
+ } else if (currentLine.length + 1 + word.length <= maxWidth) {
19364
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
19365
+ } else {
19366
+ if (currentLine) lines.push(currentLine);
19367
+ currentLine = word;
19368
+ }
19369
+ }
19370
+ if (currentLine) lines.push(currentLine);
19371
+ return lines.length > 0 ? lines : [""];
19372
+ }
19373
+ function pad(n) {
19374
+ return " ".repeat(Math.max(0, n));
19375
+ }
19376
+
19377
+ // src/tui/utils.ts
19378
+ function getPriorityColor(priority) {
19379
+ switch (priority?.toLowerCase()) {
19380
+ case "critical":
19381
+ return PALETTE.critical;
19382
+ case "high":
19383
+ return PALETTE.high;
19384
+ case "medium":
19385
+ return PALETTE.medium;
19386
+ case "low":
19387
+ return PALETTE.low;
19388
+ default:
19389
+ return PALETTE.textDim;
19390
+ }
19391
+ }
19392
+ function getDocType(task) {
19393
+ return (task.type || "task").toLowerCase();
19394
+ }
19395
+ function isCompletable(task) {
19396
+ if (!task) return false;
19397
+ return getDocType(task) !== "adr";
19398
+ }
19399
+ function getSubtaskProgress(task) {
19400
+ const subtasks = task.subtasks ?? [];
19401
+ if (subtasks.length === 0) return void 0;
19402
+ const done = subtasks.filter((s) => s.completed).length;
19403
+ return `${done}/${subtasks.length}`;
19404
+ }
19405
+ function getContractState(task) {
19406
+ const contract = task.contract;
19407
+ return contract?.status;
19408
+ }
19409
+
19324
19410
  // src/tui/hooks/useBrainfileLoader.ts
19325
19411
  var import_react34 = __toESM(require_react());
19326
- import * as fs3 from "fs";
19327
- import * as path from "path";
19412
+ import * as fs4 from "fs";
19413
+ import * as path2 from "path";
19328
19414
 
19329
19415
  // ../node_modules/chokidar/esm/index.js
19330
19416
  import { stat as statcb } from "fs";
@@ -19402,7 +19488,7 @@ var ReaddirpStream = class extends Readable {
19402
19488
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
19403
19489
  const statMethod = opts.lstat ? lstat : stat;
19404
19490
  if (wantBigintFsStats) {
19405
- this._stat = (path3) => statMethod(path3, { bigint: true });
19491
+ this._stat = (path4) => statMethod(path4, { bigint: true });
19406
19492
  } else {
19407
19493
  this._stat = statMethod;
19408
19494
  }
@@ -19427,8 +19513,8 @@ var ReaddirpStream = class extends Readable {
19427
19513
  const par = this.parent;
19428
19514
  const fil = par && par.files;
19429
19515
  if (fil && fil.length > 0) {
19430
- const { path: path3, depth } = par;
19431
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path3));
19516
+ const { path: path4, depth } = par;
19517
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path4));
19432
19518
  const awaited = await Promise.all(slice);
19433
19519
  for (const entry of awaited) {
19434
19520
  if (!entry)
@@ -19468,20 +19554,20 @@ var ReaddirpStream = class extends Readable {
19468
19554
  this.reading = false;
19469
19555
  }
19470
19556
  }
19471
- async _exploreDir(path3, depth) {
19557
+ async _exploreDir(path4, depth) {
19472
19558
  let files;
19473
19559
  try {
19474
- files = await readdir(path3, this._rdOptions);
19560
+ files = await readdir(path4, this._rdOptions);
19475
19561
  } catch (error) {
19476
19562
  this._onError(error);
19477
19563
  }
19478
- return { files, depth, path: path3 };
19564
+ return { files, depth, path: path4 };
19479
19565
  }
19480
- async _formatEntry(dirent, path3) {
19566
+ async _formatEntry(dirent, path4) {
19481
19567
  let entry;
19482
19568
  const basename4 = this._isDirent ? dirent.name : dirent;
19483
19569
  try {
19484
- const fullPath = presolve(pjoin(path3, basename4));
19570
+ const fullPath = presolve(pjoin(path4, basename4));
19485
19571
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename4 };
19486
19572
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
19487
19573
  } catch (err) {
@@ -19881,16 +19967,16 @@ var delFromSet = (main, prop, item) => {
19881
19967
  };
19882
19968
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
19883
19969
  var FsWatchInstances = /* @__PURE__ */ new Map();
19884
- function createFsWatchInstance(path3, options, listener, errHandler, emitRaw) {
19970
+ function createFsWatchInstance(path4, options, listener, errHandler, emitRaw) {
19885
19971
  const handleEvent = (rawEvent, evPath) => {
19886
- listener(path3);
19887
- emitRaw(rawEvent, evPath, { watchedPath: path3 });
19888
- if (evPath && path3 !== evPath) {
19889
- fsWatchBroadcast(sysPath.resolve(path3, evPath), KEY_LISTENERS, sysPath.join(path3, evPath));
19972
+ listener(path4);
19973
+ emitRaw(rawEvent, evPath, { watchedPath: path4 });
19974
+ if (evPath && path4 !== evPath) {
19975
+ fsWatchBroadcast(sysPath.resolve(path4, evPath), KEY_LISTENERS, sysPath.join(path4, evPath));
19890
19976
  }
19891
19977
  };
19892
19978
  try {
19893
- return fs_watch(path3, {
19979
+ return fs_watch(path4, {
19894
19980
  persistent: options.persistent
19895
19981
  }, handleEvent);
19896
19982
  } catch (error) {
@@ -19906,12 +19992,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
19906
19992
  listener(val1, val2, val3);
19907
19993
  });
19908
19994
  };
19909
- var setFsWatchListener = (path3, fullPath, options, handlers) => {
19995
+ var setFsWatchListener = (path4, fullPath, options, handlers) => {
19910
19996
  const { listener, errHandler, rawEmitter } = handlers;
19911
19997
  let cont = FsWatchInstances.get(fullPath);
19912
19998
  let watcher;
19913
19999
  if (!options.persistent) {
19914
- watcher = createFsWatchInstance(path3, options, listener, errHandler, rawEmitter);
20000
+ watcher = createFsWatchInstance(path4, options, listener, errHandler, rawEmitter);
19915
20001
  if (!watcher)
19916
20002
  return;
19917
20003
  return watcher.close.bind(watcher);
@@ -19922,7 +20008,7 @@ var setFsWatchListener = (path3, fullPath, options, handlers) => {
19922
20008
  addAndConvert(cont, KEY_RAW, rawEmitter);
19923
20009
  } else {
19924
20010
  watcher = createFsWatchInstance(
19925
- path3,
20011
+ path4,
19926
20012
  options,
19927
20013
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
19928
20014
  errHandler,
@@ -19937,7 +20023,7 @@ var setFsWatchListener = (path3, fullPath, options, handlers) => {
19937
20023
  cont.watcherUnusable = true;
19938
20024
  if (isWindows3 && error.code === "EPERM") {
19939
20025
  try {
19940
- const fd = await open(path3, "r");
20026
+ const fd = await open(path4, "r");
19941
20027
  await fd.close();
19942
20028
  broadcastErr(error);
19943
20029
  } catch (err) {
@@ -19968,7 +20054,7 @@ var setFsWatchListener = (path3, fullPath, options, handlers) => {
19968
20054
  };
19969
20055
  };
19970
20056
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
19971
- var setFsWatchFileListener = (path3, fullPath, options, handlers) => {
20057
+ var setFsWatchFileListener = (path4, fullPath, options, handlers) => {
19972
20058
  const { listener, rawEmitter } = handlers;
19973
20059
  let cont = FsWatchFileInstances.get(fullPath);
19974
20060
  const copts = cont && cont.options;
@@ -19990,7 +20076,7 @@ var setFsWatchFileListener = (path3, fullPath, options, handlers) => {
19990
20076
  });
19991
20077
  const currmtime = curr.mtimeMs;
19992
20078
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
19993
- foreach(cont.listeners, (listener2) => listener2(path3, curr));
20079
+ foreach(cont.listeners, (listener2) => listener2(path4, curr));
19994
20080
  }
19995
20081
  })
19996
20082
  };
@@ -20018,13 +20104,13 @@ var NodeFsHandler = class {
20018
20104
  * @param listener on fs change
20019
20105
  * @returns closer for the watcher instance
20020
20106
  */
20021
- _watchWithNodeFs(path3, listener) {
20107
+ _watchWithNodeFs(path4, listener) {
20022
20108
  const opts = this.fsw.options;
20023
- const directory = sysPath.dirname(path3);
20024
- const basename4 = sysPath.basename(path3);
20109
+ const directory = sysPath.dirname(path4);
20110
+ const basename4 = sysPath.basename(path4);
20025
20111
  const parent = this.fsw._getWatchedDir(directory);
20026
20112
  parent.add(basename4);
20027
- const absolutePath = sysPath.resolve(path3);
20113
+ const absolutePath = sysPath.resolve(path4);
20028
20114
  const options = {
20029
20115
  persistent: opts.persistent
20030
20116
  };
@@ -20034,12 +20120,12 @@ var NodeFsHandler = class {
20034
20120
  if (opts.usePolling) {
20035
20121
  const enableBin = opts.interval !== opts.binaryInterval;
20036
20122
  options.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
20037
- closer = setFsWatchFileListener(path3, absolutePath, options, {
20123
+ closer = setFsWatchFileListener(path4, absolutePath, options, {
20038
20124
  listener,
20039
20125
  rawEmitter: this.fsw._emitRaw
20040
20126
  });
20041
20127
  } else {
20042
- closer = setFsWatchListener(path3, absolutePath, options, {
20128
+ closer = setFsWatchListener(path4, absolutePath, options, {
20043
20129
  listener,
20044
20130
  errHandler: this._boundHandleError,
20045
20131
  rawEmitter: this.fsw._emitRaw
@@ -20061,7 +20147,7 @@ var NodeFsHandler = class {
20061
20147
  let prevStats = stats;
20062
20148
  if (parent.has(basename4))
20063
20149
  return;
20064
- const listener = async (path3, newStats) => {
20150
+ const listener = async (path4, newStats) => {
20065
20151
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
20066
20152
  return;
20067
20153
  if (!newStats || newStats.mtimeMs === 0) {
@@ -20075,11 +20161,11 @@ var NodeFsHandler = class {
20075
20161
  this.fsw._emit(EV.CHANGE, file, newStats2);
20076
20162
  }
20077
20163
  if ((isMacos || isLinux2 || isFreeBSD) && prevStats.ino !== newStats2.ino) {
20078
- this.fsw._closeFile(path3);
20164
+ this.fsw._closeFile(path4);
20079
20165
  prevStats = newStats2;
20080
20166
  const closer2 = this._watchWithNodeFs(file, listener);
20081
20167
  if (closer2)
20082
- this.fsw._addPathCloser(path3, closer2);
20168
+ this.fsw._addPathCloser(path4, closer2);
20083
20169
  } else {
20084
20170
  prevStats = newStats2;
20085
20171
  }
@@ -20111,7 +20197,7 @@ var NodeFsHandler = class {
20111
20197
  * @param item basename of this item
20112
20198
  * @returns true if no more processing is needed for this entry.
20113
20199
  */
20114
- async _handleSymlink(entry, directory, path3, item) {
20200
+ async _handleSymlink(entry, directory, path4, item) {
20115
20201
  if (this.fsw.closed) {
20116
20202
  return;
20117
20203
  }
@@ -20121,7 +20207,7 @@ var NodeFsHandler = class {
20121
20207
  this.fsw._incrReadyCount();
20122
20208
  let linkPath;
20123
20209
  try {
20124
- linkPath = await fsrealpath(path3);
20210
+ linkPath = await fsrealpath(path4);
20125
20211
  } catch (e) {
20126
20212
  this.fsw._emitReady();
20127
20213
  return true;
@@ -20131,12 +20217,12 @@ var NodeFsHandler = class {
20131
20217
  if (dir.has(item)) {
20132
20218
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
20133
20219
  this.fsw._symlinkPaths.set(full, linkPath);
20134
- this.fsw._emit(EV.CHANGE, path3, entry.stats);
20220
+ this.fsw._emit(EV.CHANGE, path4, entry.stats);
20135
20221
  }
20136
20222
  } else {
20137
20223
  dir.add(item);
20138
20224
  this.fsw._symlinkPaths.set(full, linkPath);
20139
- this.fsw._emit(EV.ADD, path3, entry.stats);
20225
+ this.fsw._emit(EV.ADD, path4, entry.stats);
20140
20226
  }
20141
20227
  this.fsw._emitReady();
20142
20228
  return true;
@@ -20165,9 +20251,9 @@ var NodeFsHandler = class {
20165
20251
  return;
20166
20252
  }
20167
20253
  const item = entry.path;
20168
- let path3 = sysPath.join(directory, item);
20254
+ let path4 = sysPath.join(directory, item);
20169
20255
  current.add(item);
20170
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path3, item)) {
20256
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path4, item)) {
20171
20257
  return;
20172
20258
  }
20173
20259
  if (this.fsw.closed) {
@@ -20176,8 +20262,8 @@ var NodeFsHandler = class {
20176
20262
  }
20177
20263
  if (item === target || !target && !previous.has(item)) {
20178
20264
  this.fsw._incrReadyCount();
20179
- path3 = sysPath.join(dir, sysPath.relative(dir, path3));
20180
- this._addToNodeFs(path3, initialAdd, wh, depth + 1);
20265
+ path4 = sysPath.join(dir, sysPath.relative(dir, path4));
20266
+ this._addToNodeFs(path4, initialAdd, wh, depth + 1);
20181
20267
  }
20182
20268
  }).on(EV.ERROR, this._boundHandleError);
20183
20269
  return new Promise((resolve3, reject) => {
@@ -20246,13 +20332,13 @@ var NodeFsHandler = class {
20246
20332
  * @param depth Child path actually targeted for watch
20247
20333
  * @param target Child path actually targeted for watch
20248
20334
  */
20249
- async _addToNodeFs(path3, initialAdd, priorWh, depth, target) {
20335
+ async _addToNodeFs(path4, initialAdd, priorWh, depth, target) {
20250
20336
  const ready = this.fsw._emitReady;
20251
- if (this.fsw._isIgnored(path3) || this.fsw.closed) {
20337
+ if (this.fsw._isIgnored(path4) || this.fsw.closed) {
20252
20338
  ready();
20253
20339
  return false;
20254
20340
  }
20255
- const wh = this.fsw._getWatchHelpers(path3);
20341
+ const wh = this.fsw._getWatchHelpers(path4);
20256
20342
  if (priorWh) {
20257
20343
  wh.filterPath = (entry) => priorWh.filterPath(entry);
20258
20344
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -20268,8 +20354,8 @@ var NodeFsHandler = class {
20268
20354
  const follow = this.fsw.options.followSymlinks;
20269
20355
  let closer;
20270
20356
  if (stats.isDirectory()) {
20271
- const absPath = sysPath.resolve(path3);
20272
- const targetPath = follow ? await fsrealpath(path3) : path3;
20357
+ const absPath = sysPath.resolve(path4);
20358
+ const targetPath = follow ? await fsrealpath(path4) : path4;
20273
20359
  if (this.fsw.closed)
20274
20360
  return;
20275
20361
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -20279,29 +20365,29 @@ var NodeFsHandler = class {
20279
20365
  this.fsw._symlinkPaths.set(absPath, targetPath);
20280
20366
  }
20281
20367
  } else if (stats.isSymbolicLink()) {
20282
- const targetPath = follow ? await fsrealpath(path3) : path3;
20368
+ const targetPath = follow ? await fsrealpath(path4) : path4;
20283
20369
  if (this.fsw.closed)
20284
20370
  return;
20285
20371
  const parent = sysPath.dirname(wh.watchPath);
20286
20372
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
20287
20373
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
20288
- closer = await this._handleDir(parent, stats, initialAdd, depth, path3, wh, targetPath);
20374
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path4, wh, targetPath);
20289
20375
  if (this.fsw.closed)
20290
20376
  return;
20291
20377
  if (targetPath !== void 0) {
20292
- this.fsw._symlinkPaths.set(sysPath.resolve(path3), targetPath);
20378
+ this.fsw._symlinkPaths.set(sysPath.resolve(path4), targetPath);
20293
20379
  }
20294
20380
  } else {
20295
20381
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
20296
20382
  }
20297
20383
  ready();
20298
20384
  if (closer)
20299
- this.fsw._addPathCloser(path3, closer);
20385
+ this.fsw._addPathCloser(path4, closer);
20300
20386
  return false;
20301
20387
  } catch (error) {
20302
20388
  if (this.fsw._handleError(error)) {
20303
20389
  ready();
20304
- return path3;
20390
+ return path4;
20305
20391
  }
20306
20392
  }
20307
20393
  }
@@ -20344,26 +20430,26 @@ function createPattern(matcher) {
20344
20430
  }
20345
20431
  return () => false;
20346
20432
  }
20347
- function normalizePath(path3) {
20348
- if (typeof path3 !== "string")
20433
+ function normalizePath(path4) {
20434
+ if (typeof path4 !== "string")
20349
20435
  throw new Error("string expected");
20350
- path3 = sysPath2.normalize(path3);
20351
- path3 = path3.replace(/\\/g, "/");
20436
+ path4 = sysPath2.normalize(path4);
20437
+ path4 = path4.replace(/\\/g, "/");
20352
20438
  let prepend = false;
20353
- if (path3.startsWith("//"))
20439
+ if (path4.startsWith("//"))
20354
20440
  prepend = true;
20355
20441
  const DOUBLE_SLASH_RE2 = /\/\//;
20356
- while (path3.match(DOUBLE_SLASH_RE2))
20357
- path3 = path3.replace(DOUBLE_SLASH_RE2, "/");
20442
+ while (path4.match(DOUBLE_SLASH_RE2))
20443
+ path4 = path4.replace(DOUBLE_SLASH_RE2, "/");
20358
20444
  if (prepend)
20359
- path3 = "/" + path3;
20360
- return path3;
20445
+ path4 = "/" + path4;
20446
+ return path4;
20361
20447
  }
20362
20448
  function matchPatterns(patterns, testString, stats) {
20363
- const path3 = normalizePath(testString);
20449
+ const path4 = normalizePath(testString);
20364
20450
  for (let index = 0; index < patterns.length; index++) {
20365
20451
  const pattern = patterns[index];
20366
- if (pattern(path3, stats)) {
20452
+ if (pattern(path4, stats)) {
20367
20453
  return true;
20368
20454
  }
20369
20455
  }
@@ -20403,19 +20489,19 @@ var toUnix = (string) => {
20403
20489
  }
20404
20490
  return str;
20405
20491
  };
20406
- var normalizePathToUnix = (path3) => toUnix(sysPath2.normalize(toUnix(path3)));
20407
- var normalizeIgnored = (cwd2 = "") => (path3) => {
20408
- if (typeof path3 === "string") {
20409
- return normalizePathToUnix(sysPath2.isAbsolute(path3) ? path3 : sysPath2.join(cwd2, path3));
20492
+ var normalizePathToUnix = (path4) => toUnix(sysPath2.normalize(toUnix(path4)));
20493
+ var normalizeIgnored = (cwd2 = "") => (path4) => {
20494
+ if (typeof path4 === "string") {
20495
+ return normalizePathToUnix(sysPath2.isAbsolute(path4) ? path4 : sysPath2.join(cwd2, path4));
20410
20496
  } else {
20411
- return path3;
20497
+ return path4;
20412
20498
  }
20413
20499
  };
20414
- var getAbsolutePath = (path3, cwd2) => {
20415
- if (sysPath2.isAbsolute(path3)) {
20416
- return path3;
20500
+ var getAbsolutePath = (path4, cwd2) => {
20501
+ if (sysPath2.isAbsolute(path4)) {
20502
+ return path4;
20417
20503
  }
20418
- return sysPath2.join(cwd2, path3);
20504
+ return sysPath2.join(cwd2, path4);
20419
20505
  };
20420
20506
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
20421
20507
  var DirEntry = class {
@@ -20470,10 +20556,10 @@ var DirEntry = class {
20470
20556
  var STAT_METHOD_F = "stat";
20471
20557
  var STAT_METHOD_L = "lstat";
20472
20558
  var WatchHelper = class {
20473
- constructor(path3, follow, fsw) {
20559
+ constructor(path4, follow, fsw) {
20474
20560
  this.fsw = fsw;
20475
- const watchPath = path3;
20476
- this.path = path3 = path3.replace(REPLACER_RE, "");
20561
+ const watchPath = path4;
20562
+ this.path = path4 = path4.replace(REPLACER_RE, "");
20477
20563
  this.watchPath = watchPath;
20478
20564
  this.fullWatchPath = sysPath2.resolve(watchPath);
20479
20565
  this.dirParts = [];
@@ -20595,20 +20681,20 @@ var FSWatcher = class extends EventEmitter3 {
20595
20681
  this._closePromise = void 0;
20596
20682
  let paths = unifyPaths(paths_);
20597
20683
  if (cwd2) {
20598
- paths = paths.map((path3) => {
20599
- const absPath = getAbsolutePath(path3, cwd2);
20684
+ paths = paths.map((path4) => {
20685
+ const absPath = getAbsolutePath(path4, cwd2);
20600
20686
  return absPath;
20601
20687
  });
20602
20688
  }
20603
- paths.forEach((path3) => {
20604
- this._removeIgnoredPath(path3);
20689
+ paths.forEach((path4) => {
20690
+ this._removeIgnoredPath(path4);
20605
20691
  });
20606
20692
  this._userIgnored = void 0;
20607
20693
  if (!this._readyCount)
20608
20694
  this._readyCount = 0;
20609
20695
  this._readyCount += paths.length;
20610
- Promise.all(paths.map(async (path3) => {
20611
- const res = await this._nodeFsHandler._addToNodeFs(path3, !_internal, void 0, 0, _origAdd);
20696
+ Promise.all(paths.map(async (path4) => {
20697
+ const res = await this._nodeFsHandler._addToNodeFs(path4, !_internal, void 0, 0, _origAdd);
20612
20698
  if (res)
20613
20699
  this._emitReady();
20614
20700
  return res;
@@ -20630,17 +20716,17 @@ var FSWatcher = class extends EventEmitter3 {
20630
20716
  return this;
20631
20717
  const paths = unifyPaths(paths_);
20632
20718
  const { cwd: cwd2 } = this.options;
20633
- paths.forEach((path3) => {
20634
- if (!sysPath2.isAbsolute(path3) && !this._closers.has(path3)) {
20719
+ paths.forEach((path4) => {
20720
+ if (!sysPath2.isAbsolute(path4) && !this._closers.has(path4)) {
20635
20721
  if (cwd2)
20636
- path3 = sysPath2.join(cwd2, path3);
20637
- path3 = sysPath2.resolve(path3);
20722
+ path4 = sysPath2.join(cwd2, path4);
20723
+ path4 = sysPath2.resolve(path4);
20638
20724
  }
20639
- this._closePath(path3);
20640
- this._addIgnoredPath(path3);
20641
- if (this._watched.has(path3)) {
20725
+ this._closePath(path4);
20726
+ this._addIgnoredPath(path4);
20727
+ if (this._watched.has(path4)) {
20642
20728
  this._addIgnoredPath({
20643
- path: path3,
20729
+ path: path4,
20644
20730
  recursive: true
20645
20731
  });
20646
20732
  }
@@ -20704,38 +20790,38 @@ var FSWatcher = class extends EventEmitter3 {
20704
20790
  * @param stats arguments to be passed with event
20705
20791
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
20706
20792
  */
20707
- async _emit(event, path3, stats) {
20793
+ async _emit(event, path4, stats) {
20708
20794
  if (this.closed)
20709
20795
  return;
20710
20796
  const opts = this.options;
20711
20797
  if (isWindows3)
20712
- path3 = sysPath2.normalize(path3);
20798
+ path4 = sysPath2.normalize(path4);
20713
20799
  if (opts.cwd)
20714
- path3 = sysPath2.relative(opts.cwd, path3);
20715
- const args = [path3];
20800
+ path4 = sysPath2.relative(opts.cwd, path4);
20801
+ const args = [path4];
20716
20802
  if (stats != null)
20717
20803
  args.push(stats);
20718
20804
  const awf = opts.awaitWriteFinish;
20719
20805
  let pw;
20720
- if (awf && (pw = this._pendingWrites.get(path3))) {
20806
+ if (awf && (pw = this._pendingWrites.get(path4))) {
20721
20807
  pw.lastChange = /* @__PURE__ */ new Date();
20722
20808
  return this;
20723
20809
  }
20724
20810
  if (opts.atomic) {
20725
20811
  if (event === EVENTS.UNLINK) {
20726
- this._pendingUnlinks.set(path3, [event, ...args]);
20812
+ this._pendingUnlinks.set(path4, [event, ...args]);
20727
20813
  setTimeout(() => {
20728
- this._pendingUnlinks.forEach((entry, path4) => {
20814
+ this._pendingUnlinks.forEach((entry, path5) => {
20729
20815
  this.emit(...entry);
20730
20816
  this.emit(EVENTS.ALL, ...entry);
20731
- this._pendingUnlinks.delete(path4);
20817
+ this._pendingUnlinks.delete(path5);
20732
20818
  });
20733
20819
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
20734
20820
  return this;
20735
20821
  }
20736
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path3)) {
20822
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path4)) {
20737
20823
  event = EVENTS.CHANGE;
20738
- this._pendingUnlinks.delete(path3);
20824
+ this._pendingUnlinks.delete(path4);
20739
20825
  }
20740
20826
  }
20741
20827
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -20753,16 +20839,16 @@ var FSWatcher = class extends EventEmitter3 {
20753
20839
  this.emitWithAll(event, args);
20754
20840
  }
20755
20841
  };
20756
- this._awaitWriteFinish(path3, awf.stabilityThreshold, event, awfEmit);
20842
+ this._awaitWriteFinish(path4, awf.stabilityThreshold, event, awfEmit);
20757
20843
  return this;
20758
20844
  }
20759
20845
  if (event === EVENTS.CHANGE) {
20760
- const isThrottled = !this._throttle(EVENTS.CHANGE, path3, 50);
20846
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path4, 50);
20761
20847
  if (isThrottled)
20762
20848
  return this;
20763
20849
  }
20764
20850
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
20765
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path3) : path3;
20851
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path4) : path4;
20766
20852
  let stats2;
20767
20853
  try {
20768
20854
  stats2 = await stat3(fullPath);
@@ -20793,23 +20879,23 @@ var FSWatcher = class extends EventEmitter3 {
20793
20879
  * @param timeout duration of time to suppress duplicate actions
20794
20880
  * @returns tracking object or false if action should be suppressed
20795
20881
  */
20796
- _throttle(actionType, path3, timeout) {
20882
+ _throttle(actionType, path4, timeout) {
20797
20883
  if (!this._throttled.has(actionType)) {
20798
20884
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
20799
20885
  }
20800
20886
  const action = this._throttled.get(actionType);
20801
20887
  if (!action)
20802
20888
  throw new Error("invalid throttle");
20803
- const actionPath = action.get(path3);
20889
+ const actionPath = action.get(path4);
20804
20890
  if (actionPath) {
20805
20891
  actionPath.count++;
20806
20892
  return false;
20807
20893
  }
20808
20894
  let timeoutObject;
20809
20895
  const clear = () => {
20810
- const item = action.get(path3);
20896
+ const item = action.get(path4);
20811
20897
  const count = item ? item.count : 0;
20812
- action.delete(path3);
20898
+ action.delete(path4);
20813
20899
  clearTimeout(timeoutObject);
20814
20900
  if (item)
20815
20901
  clearTimeout(item.timeoutObject);
@@ -20817,7 +20903,7 @@ var FSWatcher = class extends EventEmitter3 {
20817
20903
  };
20818
20904
  timeoutObject = setTimeout(clear, timeout);
20819
20905
  const thr = { timeoutObject, clear, count: 0 };
20820
- action.set(path3, thr);
20906
+ action.set(path4, thr);
20821
20907
  return thr;
20822
20908
  }
20823
20909
  _incrReadyCount() {
@@ -20831,44 +20917,44 @@ var FSWatcher = class extends EventEmitter3 {
20831
20917
  * @param event
20832
20918
  * @param awfEmit Callback to be called when ready for event to be emitted.
20833
20919
  */
20834
- _awaitWriteFinish(path3, threshold, event, awfEmit) {
20920
+ _awaitWriteFinish(path4, threshold, event, awfEmit) {
20835
20921
  const awf = this.options.awaitWriteFinish;
20836
20922
  if (typeof awf !== "object")
20837
20923
  return;
20838
20924
  const pollInterval = awf.pollInterval;
20839
20925
  let timeoutHandler;
20840
- let fullPath = path3;
20841
- if (this.options.cwd && !sysPath2.isAbsolute(path3)) {
20842
- fullPath = sysPath2.join(this.options.cwd, path3);
20926
+ let fullPath = path4;
20927
+ if (this.options.cwd && !sysPath2.isAbsolute(path4)) {
20928
+ fullPath = sysPath2.join(this.options.cwd, path4);
20843
20929
  }
20844
20930
  const now = /* @__PURE__ */ new Date();
20845
20931
  const writes = this._pendingWrites;
20846
20932
  function awaitWriteFinishFn(prevStat) {
20847
20933
  statcb(fullPath, (err, curStat) => {
20848
- if (err || !writes.has(path3)) {
20934
+ if (err || !writes.has(path4)) {
20849
20935
  if (err && err.code !== "ENOENT")
20850
20936
  awfEmit(err);
20851
20937
  return;
20852
20938
  }
20853
20939
  const now2 = Number(/* @__PURE__ */ new Date());
20854
20940
  if (prevStat && curStat.size !== prevStat.size) {
20855
- writes.get(path3).lastChange = now2;
20941
+ writes.get(path4).lastChange = now2;
20856
20942
  }
20857
- const pw = writes.get(path3);
20943
+ const pw = writes.get(path4);
20858
20944
  const df = now2 - pw.lastChange;
20859
20945
  if (df >= threshold) {
20860
- writes.delete(path3);
20946
+ writes.delete(path4);
20861
20947
  awfEmit(void 0, curStat);
20862
20948
  } else {
20863
20949
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
20864
20950
  }
20865
20951
  });
20866
20952
  }
20867
- if (!writes.has(path3)) {
20868
- writes.set(path3, {
20953
+ if (!writes.has(path4)) {
20954
+ writes.set(path4, {
20869
20955
  lastChange: now,
20870
20956
  cancelWait: () => {
20871
- writes.delete(path3);
20957
+ writes.delete(path4);
20872
20958
  clearTimeout(timeoutHandler);
20873
20959
  return event;
20874
20960
  }
@@ -20879,8 +20965,8 @@ var FSWatcher = class extends EventEmitter3 {
20879
20965
  /**
20880
20966
  * Determines whether user has asked to ignore this path.
20881
20967
  */
20882
- _isIgnored(path3, stats) {
20883
- if (this.options.atomic && DOT_RE.test(path3))
20968
+ _isIgnored(path4, stats) {
20969
+ if (this.options.atomic && DOT_RE.test(path4))
20884
20970
  return true;
20885
20971
  if (!this._userIgnored) {
20886
20972
  const { cwd: cwd2 } = this.options;
@@ -20890,17 +20976,17 @@ var FSWatcher = class extends EventEmitter3 {
20890
20976
  const list = [...ignoredPaths.map(normalizeIgnored(cwd2)), ...ignored];
20891
20977
  this._userIgnored = anymatch(list, void 0);
20892
20978
  }
20893
- return this._userIgnored(path3, stats);
20979
+ return this._userIgnored(path4, stats);
20894
20980
  }
20895
- _isntIgnored(path3, stat4) {
20896
- return !this._isIgnored(path3, stat4);
20981
+ _isntIgnored(path4, stat4) {
20982
+ return !this._isIgnored(path4, stat4);
20897
20983
  }
20898
20984
  /**
20899
20985
  * Provides a set of common helpers and properties relating to symlink handling.
20900
20986
  * @param path file or directory pattern being watched
20901
20987
  */
20902
- _getWatchHelpers(path3) {
20903
- return new WatchHelper(path3, this.options.followSymlinks, this);
20988
+ _getWatchHelpers(path4) {
20989
+ return new WatchHelper(path4, this.options.followSymlinks, this);
20904
20990
  }
20905
20991
  // Directory helpers
20906
20992
  // -----------------
@@ -20932,63 +21018,63 @@ var FSWatcher = class extends EventEmitter3 {
20932
21018
  * @param item base path of item/directory
20933
21019
  */
20934
21020
  _remove(directory, item, isDirectory) {
20935
- const path3 = sysPath2.join(directory, item);
20936
- const fullPath = sysPath2.resolve(path3);
20937
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path3) || this._watched.has(fullPath);
20938
- if (!this._throttle("remove", path3, 100))
21021
+ const path4 = sysPath2.join(directory, item);
21022
+ const fullPath = sysPath2.resolve(path4);
21023
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path4) || this._watched.has(fullPath);
21024
+ if (!this._throttle("remove", path4, 100))
20939
21025
  return;
20940
21026
  if (!isDirectory && this._watched.size === 1) {
20941
21027
  this.add(directory, item, true);
20942
21028
  }
20943
- const wp = this._getWatchedDir(path3);
21029
+ const wp = this._getWatchedDir(path4);
20944
21030
  const nestedDirectoryChildren = wp.getChildren();
20945
- nestedDirectoryChildren.forEach((nested) => this._remove(path3, nested));
21031
+ nestedDirectoryChildren.forEach((nested) => this._remove(path4, nested));
20946
21032
  const parent = this._getWatchedDir(directory);
20947
21033
  const wasTracked = parent.has(item);
20948
21034
  parent.remove(item);
20949
21035
  if (this._symlinkPaths.has(fullPath)) {
20950
21036
  this._symlinkPaths.delete(fullPath);
20951
21037
  }
20952
- let relPath = path3;
21038
+ let relPath = path4;
20953
21039
  if (this.options.cwd)
20954
- relPath = sysPath2.relative(this.options.cwd, path3);
21040
+ relPath = sysPath2.relative(this.options.cwd, path4);
20955
21041
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
20956
21042
  const event = this._pendingWrites.get(relPath).cancelWait();
20957
21043
  if (event === EVENTS.ADD)
20958
21044
  return;
20959
21045
  }
20960
- this._watched.delete(path3);
21046
+ this._watched.delete(path4);
20961
21047
  this._watched.delete(fullPath);
20962
21048
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
20963
- if (wasTracked && !this._isIgnored(path3))
20964
- this._emit(eventName, path3);
20965
- this._closePath(path3);
21049
+ if (wasTracked && !this._isIgnored(path4))
21050
+ this._emit(eventName, path4);
21051
+ this._closePath(path4);
20966
21052
  }
20967
21053
  /**
20968
21054
  * Closes all watchers for a path
20969
21055
  */
20970
- _closePath(path3) {
20971
- this._closeFile(path3);
20972
- const dir = sysPath2.dirname(path3);
20973
- this._getWatchedDir(dir).remove(sysPath2.basename(path3));
21056
+ _closePath(path4) {
21057
+ this._closeFile(path4);
21058
+ const dir = sysPath2.dirname(path4);
21059
+ this._getWatchedDir(dir).remove(sysPath2.basename(path4));
20974
21060
  }
20975
21061
  /**
20976
21062
  * Closes only file-specific watchers
20977
21063
  */
20978
- _closeFile(path3) {
20979
- const closers = this._closers.get(path3);
21064
+ _closeFile(path4) {
21065
+ const closers = this._closers.get(path4);
20980
21066
  if (!closers)
20981
21067
  return;
20982
21068
  closers.forEach((closer) => closer());
20983
- this._closers.delete(path3);
21069
+ this._closers.delete(path4);
20984
21070
  }
20985
- _addPathCloser(path3, closer) {
21071
+ _addPathCloser(path4, closer) {
20986
21072
  if (!closer)
20987
21073
  return;
20988
- let list = this._closers.get(path3);
21074
+ let list = this._closers.get(path4);
20989
21075
  if (!list) {
20990
21076
  list = [];
20991
- this._closers.set(path3, list);
21077
+ this._closers.set(path4, list);
20992
21078
  }
20993
21079
  list.push(closer);
20994
21080
  }
@@ -21017,313 +21103,66 @@ function watch(paths, options = {}) {
21017
21103
  }
21018
21104
 
21019
21105
  // src/tui/hooks/useBrainfileLoader.ts
21020
- var import_core2 = __toESM(require_dist());
21021
- function getV2ChangeSignature(filePath) {
21022
- const dirs = (0, import_core.getV2Dirs)(filePath);
21023
- const parts = [];
21106
+ var import_core4 = __toESM(require_dist());
21107
+
21108
+ // src/tui/actions.ts
21109
+ var import_core3 = __toESM(require_dist());
21110
+ import * as fs3 from "fs";
21111
+ import * as path from "path";
21112
+ import { spawnSync } from "child_process";
21113
+ function readBrainfile(filePath) {
21024
21114
  try {
21025
- const boardEntries = fs3.existsSync(dirs.boardDir) ? fs3.readdirSync(dirs.boardDir).filter((name) => name.endsWith(".md")).sort() : [];
21026
- for (const name of boardEntries) {
21027
- const fullPath = path.join(dirs.boardDir, name);
21028
- try {
21029
- const stat4 = fs3.statSync(fullPath);
21030
- parts.push(`board:${name}:${stat4.mtimeMs}:${stat4.size}`);
21031
- } catch {
21032
- }
21115
+ const content = fs3.readFileSync(filePath, "utf-8");
21116
+ const result = import_core3.Brainfile.parseWithErrors(content);
21117
+ if (!result.board) {
21118
+ return { board: null, content, error: result.error || "Failed to parse brainfile" };
21033
21119
  }
21034
- } catch {
21120
+ return { board: result.board, content };
21121
+ } catch (err) {
21122
+ return { board: null, content: "", error: err instanceof Error ? err.message : String(err) };
21035
21123
  }
21124
+ }
21125
+ function writeBrainfile(filePath, board) {
21036
21126
  try {
21037
- const logsEntries = fs3.existsSync(dirs.logsDir) ? fs3.readdirSync(dirs.logsDir).filter((name) => name.endsWith(".md")).sort() : [];
21038
- for (const name of logsEntries) {
21039
- const fullPath = path.join(dirs.logsDir, name);
21040
- try {
21041
- const stat4 = fs3.statSync(fullPath);
21042
- parts.push(`logs:${name}:${stat4.mtimeMs}:${stat4.size}`);
21043
- } catch {
21044
- }
21045
- }
21046
- } catch {
21127
+ const content = import_core3.Brainfile.serialize(board);
21128
+ fs3.writeFileSync(filePath, content, "utf-8");
21129
+ return { success: true };
21130
+ } catch (err) {
21131
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
21047
21132
  }
21048
- return parts.join("|");
21049
21133
  }
21050
- function useBrainfileLoader(filePath, state, setState) {
21051
- const flashTimeoutRef = (0, import_react34.useRef)(null);
21052
- const loadBrainfile = (0, import_react34.useCallback)((forceRefresh = false) => {
21134
+ function readBoardState(filePath) {
21135
+ if ((0, import_core.isV2)(filePath)) {
21053
21136
  try {
21054
21137
  const content = fs3.readFileSync(filePath, "utf-8");
21055
- const v2 = (0, import_core.isV2)(filePath);
21056
- let board = null;
21057
- let parseError;
21058
- if (v2) {
21059
- try {
21060
- board = (0, import_core.buildBoardFromV2)(filePath);
21061
- } catch (err) {
21062
- parseError = err instanceof Error ? err.message : String(err);
21063
- }
21064
- } else {
21065
- const result = import_core2.Brainfile.parseWithErrors(content);
21066
- if (result.board) {
21067
- board = result.board;
21068
- } else {
21069
- parseError = result.error || "Not a valid brainfile";
21070
- }
21071
- }
21072
- const contentHash = v2 ? (0, import_core2.hashBoardContent)(`${content}
21073
- ${getV2ChangeSignature(filePath)}`) : (0, import_core2.hashBoardContent)(content);
21074
- setState((prev) => {
21075
- if (!forceRefresh && prev.lastContentHash === contentHash) {
21076
- return prev;
21077
- }
21078
- if (board) {
21079
- const prevColumn = prev.board?.columns[prev.activeColumnIndex];
21080
- const prevTaskId = prevColumn?.tasks[prev.selectedTaskIndex]?.id;
21081
- let newColumnIndex = prev.activeColumnIndex;
21082
- let newTaskIndex = 0;
21083
- if (prevColumn) {
21084
- const colIdx = board.columns.findIndex((c) => c.id === prevColumn.id);
21085
- if (colIdx >= 0) {
21086
- newColumnIndex = colIdx;
21087
- if (prevTaskId) {
21088
- const taskIdx = board.columns[colIdx].tasks.findIndex((t) => t.id === prevTaskId);
21089
- if (taskIdx >= 0) newTaskIndex = taskIdx;
21090
- }
21091
- }
21092
- }
21093
- newColumnIndex = Math.min(newColumnIndex, Math.max(0, board.columns.length - 1));
21094
- const col = board.columns[newColumnIndex];
21095
- newTaskIndex = Math.min(newTaskIndex, Math.max(0, (col?.tasks.length || 1) - 1));
21096
- return {
21097
- ...prev,
21098
- board,
21099
- error: null,
21100
- lastUpdated: /* @__PURE__ */ new Date(),
21101
- activeColumnIndex: newColumnIndex,
21102
- selectedTaskIndex: newTaskIndex,
21103
- reloadFlash: true,
21104
- lastContentHash: contentHash
21105
- };
21106
- }
21107
- let errorMessage;
21108
- if (parseError) {
21109
- errorMessage = parseError;
21110
- } else {
21111
- errorMessage = "Not a valid brainfile";
21112
- }
21113
- return {
21114
- ...prev,
21115
- error: errorMessage,
21116
- lastContentHash: contentHash
21117
- };
21118
- });
21119
- if (flashTimeoutRef.current) clearTimeout(flashTimeoutRef.current);
21120
- flashTimeoutRef.current = setTimeout(() => {
21121
- setState((prev) => ({ ...prev, reloadFlash: false }));
21122
- }, 1e3);
21138
+ const board = (0, import_core.buildBoardFromV2)(filePath);
21139
+ return { board, content };
21123
21140
  } catch (err) {
21124
- const errorMsg = err instanceof Error ? err.message : String(err);
21125
- let userMessage = errorMsg;
21126
- if (errorMsg.includes("ENOENT")) {
21127
- userMessage = `File not found: ${filePath}`;
21128
- } else if (errorMsg.includes("EACCES")) {
21129
- userMessage = `Permission denied: ${filePath}`;
21130
- } else if (errorMsg.includes("EISDIR")) {
21131
- userMessage = `Path is a directory: ${filePath}`;
21132
- }
21133
- setState((prev) => ({
21134
- ...prev,
21135
- error: userMessage
21136
- }));
21141
+ return { board: null, content: "", error: err instanceof Error ? err.message : String(err) };
21137
21142
  }
21138
- }, [filePath, setState]);
21139
- (0, import_react34.useEffect)(() => {
21140
- loadBrainfile();
21141
- }, [loadBrainfile]);
21142
- (0, import_react34.useEffect)(() => {
21143
- return () => {
21144
- if (flashTimeoutRef.current) clearTimeout(flashTimeoutRef.current);
21145
- };
21146
- }, []);
21147
- (0, import_react34.useEffect)(() => {
21148
- const v2 = (0, import_core.isV2)(filePath);
21149
- const dirs = v2 ? (0, import_core.getV2Dirs)(filePath) : null;
21150
- const watchPaths = v2 && dirs ? [filePath, dirs.boardDir, dirs.logsDir] : [filePath];
21151
- const watcher = watch(watchPaths, {
21152
- persistent: true,
21153
- ignoreInitial: true,
21154
- usePolling: true,
21155
- interval: 750,
21156
- awaitWriteFinish: {
21157
- stabilityThreshold: 250,
21158
- pollInterval: 100
21159
- },
21160
- depth: v2 ? 1 : 0
21161
- });
21162
- watcher.on("add", () => loadBrainfile());
21163
- watcher.on("change", () => loadBrainfile());
21164
- watcher.on("unlink", () => loadBrainfile());
21165
- watcher.on("error", (err) => {
21166
- const message = err instanceof Error ? err.message : String(err);
21167
- setState((prev) => ({
21168
- ...prev,
21169
- error: `File watcher error: ${message}`
21170
- }));
21171
- });
21172
- return () => {
21173
- watcher.close();
21174
- };
21175
- }, [filePath, loadBrainfile, setState]);
21176
- return { loadBrainfile };
21177
- }
21178
-
21179
- // src/tui/hooks/useKeyboardNavigation.ts
21180
- var import_react35 = __toESM(require_react());
21181
-
21182
- // src/tui/utils.ts
21183
- var import_core3 = __toESM(require_dist());
21184
-
21185
- // src/tui/text.ts
21186
- function truncate(text, maxWidth) {
21187
- if (!text) return " ";
21188
- if (maxWidth <= 0) return " ";
21189
- if (maxWidth <= 3) return text.slice(0, maxWidth) || " ";
21190
- if (text.length <= maxWidth) return text;
21191
- return `${text.slice(0, maxWidth - 1)}\u2026`;
21192
- }
21193
- function wrapText2(text, maxWidth) {
21194
- if (!text || maxWidth <= 0) return [];
21195
- const words = text.split(/\s+/);
21196
- const lines = [];
21197
- let currentLine = "";
21198
- for (const word of words) {
21199
- if (word.length > maxWidth) {
21200
- if (currentLine) {
21201
- lines.push(currentLine);
21202
- currentLine = "";
21203
- }
21204
- for (let i = 0; i < word.length; i += maxWidth) {
21205
- lines.push(word.slice(i, i + maxWidth));
21206
- }
21207
- } else if (currentLine.length + 1 + word.length <= maxWidth) {
21208
- currentLine = currentLine ? `${currentLine} ${word}` : word;
21209
- } else {
21210
- if (currentLine) lines.push(currentLine);
21211
- currentLine = word;
21212
- }
21213
- }
21214
- if (currentLine) lines.push(currentLine);
21215
- return lines.length > 0 ? lines : [""];
21216
- }
21217
- function pad(n) {
21218
- return " ".repeat(Math.max(0, n));
21219
- }
21220
-
21221
- // src/tui/utils.ts
21222
- function getPriorityColor(priority) {
21223
- switch (priority?.toLowerCase()) {
21224
- case "critical":
21225
- return PALETTE.critical;
21226
- case "giga":
21227
- return PALETTE.giga;
21228
- case "high":
21229
- return PALETTE.high;
21230
- case "medium":
21231
- return PALETTE.medium;
21232
- case "low":
21233
- return PALETTE.low;
21234
- default:
21235
- return PALETTE.textDim;
21236
- }
21237
- }
21238
- function getDocType(task) {
21239
- return (task.type || "task").toLowerCase();
21240
- }
21241
- function isCompletable(task) {
21242
- if (!task) return false;
21243
- return getDocType(task) !== "adr";
21244
- }
21245
- function getSubtaskProgress(task) {
21246
- const subtasks = task.subtasks ?? [];
21247
- if (subtasks.length === 0) return void 0;
21248
- const done = subtasks.filter((s) => s.completed).length;
21249
- return `${done}/${subtasks.length}`;
21250
- }
21251
- function getContractState(task) {
21252
- const contract = task.contract;
21253
- return contract?.status;
21254
- }
21255
-
21256
- // src/tui/actions.ts
21257
- var import_core4 = __toESM(require_dist());
21258
- import * as fs4 from "fs";
21259
- import * as path2 from "path";
21260
- import * as os3 from "os";
21261
- import { spawnSync } from "child_process";
21262
- function readBrainfile(filePath) {
21263
- try {
21264
- const content = fs4.readFileSync(filePath, "utf-8");
21265
- const result = import_core4.Brainfile.parseWithErrors(content);
21266
- if (!result.board) {
21267
- return { board: null, content, error: result.error || "Failed to parse brainfile" };
21268
- }
21269
- return { board: result.board, content };
21270
- } catch (err) {
21271
- return { board: null, content: "", error: err instanceof Error ? err.message : String(err) };
21272
- }
21273
- }
21274
- function writeBrainfile(filePath, board) {
21275
- try {
21276
- const content = import_core4.Brainfile.serialize(board);
21277
- fs4.writeFileSync(filePath, content, "utf-8");
21278
- return { success: true };
21279
- } catch (err) {
21280
- return { success: false, error: err instanceof Error ? err.message : String(err) };
21281
- }
21282
- }
21283
- function readBoardState(filePath) {
21284
- if ((0, import_core.isV2)(filePath)) {
21285
- try {
21286
- const content = fs4.readFileSync(filePath, "utf-8");
21287
- const board = (0, import_core.buildBoardFromV2)(filePath);
21288
- return { board, content };
21289
- } catch (err) {
21290
- return { board: null, content: "", error: err instanceof Error ? err.message : String(err) };
21291
- }
21292
- }
21293
- return readBrainfile(filePath);
21294
- }
21295
- function readBoardConfigOnly(filePath) {
21296
- if ((0, import_core.isV2)(filePath)) {
21297
- try {
21298
- const content = fs4.readFileSync(filePath, "utf-8");
21299
- const board = (0, import_core.readV2BoardConfig)(filePath);
21300
- return { board, content };
21301
- } catch (err) {
21302
- return { board: null, content: "", error: err instanceof Error ? err.message : String(err) };
21303
- }
21304
- }
21305
- return readBrainfile(filePath);
21143
+ }
21144
+ return readBrainfile(filePath);
21306
21145
  }
21307
21146
  function resolveV2TaskPath(filePath, taskId) {
21308
21147
  const dirs = (0, import_core.getV2Dirs)(filePath);
21309
- const doc = (0, import_core4.findTask)(dirs.boardDir, taskId);
21148
+ const doc = (0, import_core3.findTask)(dirs.boardDir, taskId);
21310
21149
  if (!doc || !doc.filePath) return null;
21311
21150
  return { taskPath: doc.filePath, task: doc.task };
21312
21151
  }
21313
21152
  function moveTaskAction(filePath, taskId, targetColumnId) {
21314
21153
  const { board, error } = readBoardState(filePath);
21315
21154
  if (!board) return { success: false, error };
21316
- const taskInfo = (0, import_core4.findTaskById)(board, taskId);
21155
+ const taskInfo = (0, import_core3.findTaskById)(board, taskId);
21317
21156
  if (!taskInfo) return { success: false, error: `Task ${taskId} not found` };
21318
- const targetColumn = (0, import_core4.findColumnById)(board, targetColumnId) || (0, import_core4.findColumnByName)(board, targetColumnId);
21157
+ const targetColumn = (0, import_core3.findColumnById)(board, targetColumnId) || (0, import_core3.findColumnByName)(board, targetColumnId);
21319
21158
  if (!targetColumn) return { success: false, error: `Column ${targetColumnId} not found` };
21320
21159
  if (taskInfo.column.id === targetColumn.id) {
21321
21160
  return { success: true, message: "Task already in this column" };
21322
21161
  }
21323
21162
  if ((0, import_core.isV2)(filePath)) {
21324
- const boardFile = (0, import_core4.readBoardConfig)(filePath);
21163
+ const boardFile = (0, import_core3.readBoardConfig)(filePath);
21325
21164
  if (!boardFile) return { success: false, error: `Failed to parse brainfile: ${filePath}` };
21326
- const moveResult = (0, import_core4.moveTaskFileToColumn)(
21165
+ const moveResult = (0, import_core3.moveTaskFileToColumn)(
21327
21166
  (0, import_core.getV2Dirs)(filePath),
21328
21167
  boardFile.config,
21329
21168
  taskId,
@@ -21335,7 +21174,7 @@ function moveTaskAction(filePath, taskId, targetColumnId) {
21335
21174
  message: moveResult.autoCompleted ? `Completed via ${targetColumn.title}` : `Moved to ${targetColumn.title}`
21336
21175
  };
21337
21176
  }
21338
- const result = (0, import_core4.moveTask)(board, taskId, taskInfo.column.id, targetColumn.id, targetColumn.tasks.length);
21177
+ const result = (0, import_core3.moveTask)(board, taskId, taskInfo.column.id, targetColumn.id, targetColumn.tasks.length);
21339
21178
  if (!result.success) return { success: false, error: result.error };
21340
21179
  const writeResult = writeBrainfile(filePath, result.board);
21341
21180
  if (!writeResult.success) return writeResult;
@@ -21344,26 +21183,26 @@ function moveTaskAction(filePath, taskId, targetColumnId) {
21344
21183
  function deleteTaskAction(filePath, taskId) {
21345
21184
  const { board, error } = readBoardState(filePath);
21346
21185
  if (!board) return { success: false, error };
21347
- const taskInfo = (0, import_core4.findTaskById)(board, taskId);
21186
+ const taskInfo = (0, import_core3.findTaskById)(board, taskId);
21348
21187
  if (!taskInfo) return { success: false, error: `Task ${taskId} not found` };
21349
21188
  if ((0, import_core.isV2)(filePath)) {
21350
21189
  const located = resolveV2TaskPath(filePath, taskId);
21351
21190
  if (!located) return { success: false, error: `Task ${taskId} not found` };
21352
- const result2 = (0, import_core4.deleteTaskFile)(located.taskPath);
21191
+ const result2 = (0, import_core3.deleteTaskFile)(located.taskPath);
21353
21192
  if (!result2.success) return { success: false, error: result2.error };
21354
21193
  return { success: true, message: `Deleted ${taskId}` };
21355
21194
  }
21356
- const result = (0, import_core4.deleteTask)(board, taskInfo.column.id, taskId);
21195
+ const result = (0, import_core3.deleteTask)(board, taskInfo.column.id, taskId);
21357
21196
  if (!result.success) return { success: false, error: result.error };
21358
21197
  const writeResult = writeBrainfile(filePath, result.board);
21359
21198
  if (!writeResult.success) return writeResult;
21360
21199
  return { success: true, message: `Deleted ${taskId}` };
21361
21200
  }
21362
21201
  function getArchivePath(filePath) {
21363
- const dir = path2.dirname(filePath);
21364
- const filename = path2.basename(filePath);
21202
+ const dir = path.dirname(filePath);
21203
+ const filename = path.basename(filePath);
21365
21204
  const archiveFilename = filename.replace(/\.md$/, "-archive.md");
21366
- return path2.join(dir, archiveFilename);
21205
+ return path.join(dir, archiveFilename);
21367
21206
  }
21368
21207
  function createEmptyArchiveBoard() {
21369
21208
  return {
@@ -21377,7 +21216,7 @@ function archiveTaskAction(filePath, taskId, options) {
21377
21216
  const dirs = (0, import_core.getV2Dirs)(filePath);
21378
21217
  const located = resolveV2TaskPath(filePath, taskId);
21379
21218
  if (!located) return { success: false, error: `Task ${taskId} not found` };
21380
- const result2 = (0, import_core4.completeTaskFile)(located.taskPath, dirs.logsDir, { force: options?.force });
21219
+ const result2 = (0, import_core3.completeTaskFile)(located.taskPath, dirs.logsDir, { force: options?.force });
21381
21220
  if (!result2.success) {
21382
21221
  return {
21383
21222
  success: false,
@@ -21389,20 +21228,20 @@ function archiveTaskAction(filePath, taskId, options) {
21389
21228
  }
21390
21229
  const { board, error } = readBrainfile(filePath);
21391
21230
  if (!board) return { success: false, error };
21392
- const taskInfo = (0, import_core4.findTaskById)(board, taskId);
21231
+ const taskInfo = (0, import_core3.findTaskById)(board, taskId);
21393
21232
  if (!taskInfo) return { success: false, error: `Task ${taskId} not found` };
21394
21233
  const task = taskInfo.task;
21395
21234
  const columnId = taskInfo.column.id;
21396
- const result = (0, import_core4.deleteTask)(board, columnId, taskId);
21235
+ const result = (0, import_core3.deleteTask)(board, columnId, taskId);
21397
21236
  if (!result.success) return { success: false, error: result.error };
21398
21237
  const writeResult = writeBrainfile(filePath, result.board);
21399
21238
  if (!writeResult.success) return writeResult;
21400
21239
  const archivePath = getArchivePath(filePath);
21401
21240
  let archiveBoard;
21402
- if (fs4.existsSync(archivePath)) {
21241
+ if (fs3.existsSync(archivePath)) {
21403
21242
  try {
21404
- const archiveContent = fs4.readFileSync(archivePath, "utf-8");
21405
- const parseResult = import_core4.Brainfile.parseWithErrors(archiveContent);
21243
+ const archiveContent = fs3.readFileSync(archivePath, "utf-8");
21244
+ const parseResult = import_core3.Brainfile.parseWithErrors(archiveContent);
21406
21245
  if (parseResult.board) {
21407
21246
  archiveBoard = parseResult.board;
21408
21247
  } else {
@@ -21419,8 +21258,8 @@ function archiveTaskAction(filePath, taskId, options) {
21419
21258
  }
21420
21259
  archiveBoard.archive.unshift(task);
21421
21260
  try {
21422
- const archiveContent = import_core4.Brainfile.serialize(archiveBoard);
21423
- fs4.writeFileSync(archivePath, archiveContent, "utf-8");
21261
+ const archiveContent = import_core3.Brainfile.serialize(archiveBoard);
21262
+ fs3.writeFileSync(archivePath, archiveContent, "utf-8");
21424
21263
  } catch (err) {
21425
21264
  return {
21426
21265
  success: false,
@@ -21432,7 +21271,7 @@ function archiveTaskAction(filePath, taskId, options) {
21432
21271
  async function archiveTaskActionAsync(filePath, taskId, options) {
21433
21272
  const { board, error } = readBoardState(filePath);
21434
21273
  if (!board) return { success: false, error };
21435
- const taskInfo = (0, import_core4.findTaskById)(board, taskId);
21274
+ const taskInfo = (0, import_core3.findTaskById)(board, taskId);
21436
21275
  if (!taskInfo) return { success: false, error: `Task ${taskId} not found` };
21437
21276
  const task = taskInfo.task;
21438
21277
  const columnId = taskInfo.column.id;
@@ -21453,7 +21292,7 @@ async function archiveTaskActionAsync(filePath, taskId, options) {
21453
21292
  if (!owner || !repo) {
21454
21293
  return { success: false, error: "GitHub owner/repo not configured. Set archive.destination: github:owner/repo" };
21455
21294
  }
21456
- const payload = (0, import_core4.formatTaskForGitHub)(task, {
21295
+ const payload = (0, import_core3.formatTaskForGitHub)(task, {
21457
21296
  includeMeta: true,
21458
21297
  includeSubtasks: true,
21459
21298
  includeRelatedFiles: true,
@@ -21475,10 +21314,10 @@ async function archiveTaskActionAsync(filePath, taskId, options) {
21475
21314
  if ((0, import_core.isV2)(filePath)) {
21476
21315
  const located = resolveV2TaskPath(filePath, taskId);
21477
21316
  if (located) {
21478
- (0, import_core4.deleteTaskFile)(located.taskPath);
21317
+ (0, import_core3.deleteTaskFile)(located.taskPath);
21479
21318
  }
21480
21319
  } else {
21481
- const deleteResult = (0, import_core4.deleteTask)(board, columnId, taskId);
21320
+ const deleteResult = (0, import_core3.deleteTask)(board, columnId, taskId);
21482
21321
  if (deleteResult.success) {
21483
21322
  writeBrainfile(filePath, deleteResult.board);
21484
21323
  }
@@ -21512,7 +21351,7 @@ async function archiveTaskActionAsync(filePath, taskId, options) {
21512
21351
  return { success: false, error: "No Linear teams found" };
21513
21352
  }
21514
21353
  }
21515
- const payload = (0, import_core4.formatTaskForLinear)(task, {
21354
+ const payload = (0, import_core3.formatTaskForLinear)(task, {
21516
21355
  includeMeta: true,
21517
21356
  includeSubtasks: true,
21518
21357
  includeRelatedFiles: true,
@@ -21534,10 +21373,10 @@ async function archiveTaskActionAsync(filePath, taskId, options) {
21534
21373
  if ((0, import_core.isV2)(filePath)) {
21535
21374
  const located = resolveV2TaskPath(filePath, taskId);
21536
21375
  if (located) {
21537
- (0, import_core4.deleteTaskFile)(located.taskPath);
21376
+ (0, import_core3.deleteTaskFile)(located.taskPath);
21538
21377
  }
21539
21378
  } else {
21540
- const deleteResult = (0, import_core4.deleteTask)(board, columnId, taskId);
21379
+ const deleteResult = (0, import_core3.deleteTask)(board, columnId, taskId);
21541
21380
  if (deleteResult.success) {
21542
21381
  writeBrainfile(filePath, deleteResult.board);
21543
21382
  }
@@ -21546,53 +21385,29 @@ async function archiveTaskActionAsync(filePath, taskId, options) {
21546
21385
  }
21547
21386
  return { success: false, error: `Unknown destination: ${destination}` };
21548
21387
  }
21549
- function patchTaskAction(filePath, taskId, patch) {
21550
- if ((0, import_core.isV2)(filePath)) {
21551
- const located = resolveV2TaskPath(filePath, taskId);
21552
- if (!located) return { success: false, error: `Task ${taskId} not found` };
21553
- const result2 = (0, import_core4.patchTaskFile)(located.taskPath, {
21554
- title: patch.title,
21555
- description: patch.description,
21556
- priority: patch.priority,
21557
- tags: patch.tags,
21558
- assignee: patch.assignee,
21559
- dueDate: patch.dueDate,
21560
- relatedFiles: patch.relatedFiles
21561
- });
21562
- if (!result2.success) return { success: false, error: result2.error };
21563
- return { success: true, message: `Updated ${taskId}` };
21564
- }
21565
- const { board, error } = readBrainfile(filePath);
21566
- if (!board) return { success: false, error };
21567
- const result = (0, import_core4.patchTask)(board, taskId, patch);
21568
- if (!result.success) return { success: false, error: result.error };
21569
- const writeResult = writeBrainfile(filePath, result.board);
21570
- if (!writeResult.success) return writeResult;
21571
- return { success: true, message: `Updated ${taskId}` };
21572
- }
21573
21388
  function cyclePriorityAction(filePath, taskId) {
21574
21389
  const priorities = [void 0, "low", "medium", "high", "critical"];
21575
21390
  if ((0, import_core.isV2)(filePath)) {
21576
21391
  const located = resolveV2TaskPath(filePath, taskId);
21577
21392
  if (!located) return { success: false, error: `Task ${taskId} not found` };
21578
- const doc = (0, import_core4.readTaskFile)(located.taskPath);
21393
+ const doc = (0, import_core3.readTaskFile)(located.taskPath);
21579
21394
  if (!doc) return { success: false, error: `Failed to read task ${taskId}` };
21580
21395
  const currentIndex2 = priorities.indexOf(doc.task.priority);
21581
21396
  const nextIndex2 = (currentIndex2 + 1) % priorities.length;
21582
21397
  const nextPriority2 = priorities[nextIndex2];
21583
- const result2 = (0, import_core4.patchTaskFile)(located.taskPath, { priority: nextPriority2 ?? null });
21398
+ const result2 = (0, import_core3.patchTaskFile)(located.taskPath, { priority: nextPriority2 ?? null });
21584
21399
  if (!result2.success) return { success: false, error: result2.error };
21585
21400
  return { success: true, message: `Priority: ${nextPriority2 || "none"}` };
21586
21401
  }
21587
21402
  const { board, error } = readBrainfile(filePath);
21588
21403
  if (!board) return { success: false, error };
21589
- const taskInfo = (0, import_core4.findTaskById)(board, taskId);
21404
+ const taskInfo = (0, import_core3.findTaskById)(board, taskId);
21590
21405
  if (!taskInfo) return { success: false, error: `Task ${taskId} not found` };
21591
21406
  const currentIndex = priorities.indexOf(taskInfo.task.priority);
21592
21407
  const nextIndex = (currentIndex + 1) % priorities.length;
21593
21408
  const nextPriority = priorities[nextIndex];
21594
21409
  const patch = { priority: nextPriority || null };
21595
- const result = (0, import_core4.patchTask)(board, taskId, patch);
21410
+ const result = (0, import_core3.patchTask)(board, taskId, patch);
21596
21411
  if (!result.success) return { success: false, error: result.error };
21597
21412
  const writeResult = writeBrainfile(filePath, result.board);
21598
21413
  if (!writeResult.success) return writeResult;
@@ -21602,7 +21417,7 @@ function toggleSubtaskAction(filePath, taskId, subtaskId) {
21602
21417
  if ((0, import_core.isV2)(filePath)) {
21603
21418
  const located = resolveV2TaskPath(filePath, taskId);
21604
21419
  if (!located) return { success: false, error: `Task ${taskId} not found` };
21605
- const result2 = (0, import_core4.toggleSubtasksInFile)(located.taskPath, [subtaskId]);
21420
+ const result2 = (0, import_core3.toggleSubtasksInFile)(located.taskPath, [subtaskId]);
21606
21421
  if (!result2.success) {
21607
21422
  return {
21608
21423
  success: false,
@@ -21613,7 +21428,7 @@ function toggleSubtaskAction(filePath, taskId, subtaskId) {
21613
21428
  }
21614
21429
  const { board, error } = readBrainfile(filePath);
21615
21430
  if (!board) return { success: false, error };
21616
- const result = (0, import_core4.toggleSubtask)(board, taskId, subtaskId);
21431
+ const result = (0, import_core3.toggleSubtask)(board, taskId, subtaskId);
21617
21432
  if (!result.success) return { success: false, error: result.error };
21618
21433
  const writeResult = writeBrainfile(filePath, result.board);
21619
21434
  if (!writeResult.success) return writeResult;
@@ -21622,11 +21437,11 @@ function toggleSubtaskAction(filePath, taskId, subtaskId) {
21622
21437
  function addTaskAction(filePath, columnId, taskInput) {
21623
21438
  const { board, error } = readBoardState(filePath);
21624
21439
  if (!board) return { success: false, error };
21625
- const column = (0, import_core4.findColumnById)(board, columnId) || (0, import_core4.findColumnByName)(board, columnId);
21440
+ const column = (0, import_core3.findColumnById)(board, columnId) || (0, import_core3.findColumnByName)(board, columnId);
21626
21441
  if (!column) return { success: false, error: `Column ${columnId} not found` };
21627
21442
  if ((0, import_core.isV2)(filePath)) {
21628
21443
  const dirs = (0, import_core.getV2Dirs)(filePath);
21629
- const result2 = (0, import_core4.addTaskFile)(
21444
+ const result2 = (0, import_core3.addTaskFile)(
21630
21445
  dirs.boardDir,
21631
21446
  {
21632
21447
  title: taskInput.title,
@@ -21646,7 +21461,7 @@ ${taskInput.description.trim()}
21646
21461
  if (!result2.success) return { success: false, error: result2.error };
21647
21462
  return { success: true, message: `Added ${result2.task?.id || "task"}` };
21648
21463
  }
21649
- const result = (0, import_core4.addTask)(board, column.id, taskInput);
21464
+ const result = (0, import_core3.addTask)(board, column.id, taskInput);
21650
21465
  if (!result.success) return { success: false, error: result.error };
21651
21466
  const writeResult = writeBrainfile(filePath, result.board);
21652
21467
  if (!writeResult.success) return writeResult;
@@ -21654,262 +21469,19 @@ ${taskInput.description.trim()}
21654
21469
  const newTask = newColumn?.tasks[newColumn.tasks.length - 1];
21655
21470
  return { success: true, message: `Added ${newTask?.id || "task"}` };
21656
21471
  }
21657
- function newTaskInEditor(filePath, columnId) {
21658
- const { board, error } = readBoardState(filePath);
21659
- if (!board) return { success: false, error };
21660
- const column = (0, import_core4.findColumnById)(board, columnId) || (0, import_core4.findColumnByName)(board, columnId);
21661
- if (!column) return { success: false, error: `Column ${columnId} not found` };
21662
- const template = buildNewTaskTemplate();
21663
- const tempDir = os3.tmpdir();
21664
- const tempFile = path2.join(tempDir, `brainfile-new-task.yaml`);
21665
- try {
21666
- fs4.writeFileSync(tempFile, template, "utf-8");
21667
- } catch (err) {
21668
- return { success: false, error: `Failed to create temp file: ${err}` };
21669
- }
21670
- const editor = process.env.EDITOR || process.env.VISUAL || "nano";
21671
- const result = spawnSync(editor, [tempFile], {
21672
- stdio: "inherit",
21673
- shell: true,
21674
- timeout: 3e5
21675
- });
21676
- process.stdout.write("\x1B[?25l");
21677
- process.stdout.write("\x1B[2K");
21678
- if (result.error) {
21679
- cleanupTempFile(tempFile);
21680
- return { success: false, error: `Failed to open editor: ${result.error.message}` };
21681
- }
21682
- if (result.signal === "SIGTERM") {
21683
- cleanupTempFile(tempFile);
21684
- return { success: false, error: "Editor timed out (5 minute limit)" };
21685
- }
21686
- if (result.status !== 0) {
21687
- cleanupTempFile(tempFile);
21688
- return { success: false, error: `Editor exited with code ${result.status}` };
21689
- }
21690
- let editedYaml;
21691
- try {
21692
- editedYaml = fs4.readFileSync(tempFile, "utf-8");
21693
- } catch (err) {
21694
- cleanupTempFile(tempFile);
21695
- return { success: false, error: `Failed to read edited file: ${err}` };
21696
- }
21697
- cleanupTempFile(tempFile);
21698
- const parseResult = parseNewTaskYaml(editedYaml);
21699
- if (!parseResult.success) {
21700
- return { success: false, error: parseResult.error };
21701
- }
21702
- if (!parseResult.taskInput || !parseResult.taskInput.title) {
21703
- return { success: false, error: "Task title is required" };
21704
- }
21705
- return addTaskAction(filePath, columnId, parseResult.taskInput);
21706
- }
21707
- function buildNewTaskTemplate() {
21708
- return `# New Task
21709
- # Fill in the fields below. Save and close to create the task.
21710
- # Lines starting with # are ignored. Title is required.
21711
-
21712
- title:
21713
- description:
21714
- priority:
21715
- tags:
21716
- assignee:
21717
- dueDate:
21718
- `;
21719
- }
21720
- function parseNewTaskYaml(yaml) {
21721
- try {
21722
- const lines = yaml.split("\n").filter((line) => !line.startsWith("#") && line.trim());
21723
- const taskInput = { title: "" };
21724
- for (const line of lines) {
21725
- const colonIndex = line.indexOf(":");
21726
- if (colonIndex === -1) continue;
21727
- const key = line.substring(0, colonIndex).trim();
21728
- const value = line.substring(colonIndex + 1).trim();
21729
- switch (key) {
21730
- case "title":
21731
- if (value) taskInput.title = value;
21732
- break;
21733
- case "description":
21734
- if (value) taskInput.description = value;
21735
- break;
21736
- case "priority":
21737
- const validPriorities = ["low", "medium", "high", "critical"];
21738
- if (validPriorities.includes(value)) {
21739
- taskInput.priority = value;
21740
- }
21741
- break;
21742
- case "tags":
21743
- if (value) {
21744
- taskInput.tags = value.split(",").map((t) => t.trim()).filter(Boolean);
21745
- }
21746
- break;
21747
- case "assignee":
21748
- if (value) taskInput.assignee = value;
21749
- break;
21750
- case "dueDate":
21751
- if (value) taskInput.dueDate = value;
21752
- break;
21753
- }
21754
- }
21755
- return { success: true, taskInput };
21756
- } catch (err) {
21757
- return { success: false, error: `Failed to parse task: ${err}` };
21758
- }
21759
- }
21760
- function editTaskInEditor(filePath, taskId) {
21761
- const { board, error } = readBoardState(filePath);
21762
- if (!board) {
21763
- return { success: false, error };
21764
- }
21765
- const taskInfo = (0, import_core4.findTaskById)(board, taskId);
21766
- if (!taskInfo) {
21767
- return { success: false, error: `Task ${taskId} not found` };
21768
- }
21769
- const task = taskInfo.task;
21770
- const taskYaml = buildTaskYaml(task);
21771
- const tempDir = os3.tmpdir();
21772
- const tempFile = path2.join(tempDir, `brainfile-edit-${taskId}.yaml`);
21773
- try {
21774
- fs4.writeFileSync(tempFile, taskYaml, "utf-8");
21775
- } catch (err) {
21776
- return { success: false, error: `Failed to create temp file: ${err}` };
21777
- }
21778
- const editor = process.env.EDITOR || process.env.VISUAL || "nano";
21779
- const result = spawnSync(editor, [tempFile], {
21780
- stdio: "inherit",
21781
- shell: true,
21782
- timeout: 3e5
21783
- });
21784
- process.stdout.write("\x1B[?25l");
21785
- process.stdout.write("\x1B[2K");
21786
- if (result.error) {
21787
- cleanupTempFile(tempFile);
21788
- return { success: false, error: `Failed to open editor: ${result.error.message}` };
21789
- }
21790
- if (result.signal === "SIGTERM") {
21791
- cleanupTempFile(tempFile);
21792
- return { success: false, error: "Editor timed out (5 minute limit)" };
21793
- }
21794
- if (result.status !== 0) {
21795
- cleanupTempFile(tempFile);
21796
- return { success: false, error: `Editor exited with code ${result.status}` };
21797
- }
21798
- let editedYaml;
21799
- try {
21800
- editedYaml = fs4.readFileSync(tempFile, "utf-8");
21801
- } catch (err) {
21802
- cleanupTempFile(tempFile);
21803
- return { success: false, error: `Failed to read edited file: ${err}` };
21804
- }
21805
- cleanupTempFile(tempFile);
21806
- const patchResult = parseEditedTaskYaml(editedYaml, task);
21807
- if (!patchResult.success) {
21808
- return { success: false, error: patchResult.error };
21809
- }
21810
- if (!patchResult.patch || Object.keys(patchResult.patch).length === 0) {
21811
- return { success: true, message: "No changes made" };
21812
- }
21813
- return patchTaskAction(filePath, taskId, patchResult.patch);
21814
- }
21815
- function buildTaskYaml(task) {
21816
- const lines = [
21817
- `# Edit task: ${task.id}`,
21818
- `# Save and close to apply changes. Delete a field to clear it.`,
21819
- `# Lines starting with # are ignored.`,
21820
- ``,
21821
- `title: ${task.title}`
21822
- ];
21823
- if (task.description) {
21824
- if (task.description.includes("\n")) {
21825
- lines.push(`description: |`);
21826
- task.description.split("\n").forEach((line) => {
21827
- lines.push(` ${line}`);
21828
- });
21829
- } else {
21830
- lines.push(`description: ${task.description}`);
21831
- }
21832
- } else {
21833
- lines.push(`# description: `);
21834
- }
21835
- lines.push(`priority: ${task.priority || ""}`);
21836
- lines.push(`tags: ${task.tags?.join(", ") || ""}`);
21837
- lines.push(`assignee: ${task.assignee || ""}`);
21838
- lines.push(`dueDate: ${task.dueDate || ""}`);
21839
- if (task.subtasks && task.subtasks.length > 0) {
21840
- lines.push(`subtasks:`);
21841
- task.subtasks.forEach((st) => {
21842
- lines.push(` - [${st.completed ? "x" : " "}] ${st.title}`);
21843
- });
21844
- }
21845
- if (task.relatedFiles && task.relatedFiles.length > 0) {
21846
- lines.push(`relatedFiles: ${task.relatedFiles.join(", ")}`);
21472
+ function resolveTaskFilePath(filePath, taskId) {
21473
+ if (!(0, import_core.isV2)(filePath)) {
21474
+ return {
21475
+ success: false,
21476
+ error: "Editing a document file needs a v2 board. Run: brainfile migrate"
21477
+ };
21847
21478
  }
21848
- return lines.join("\n") + "\n";
21849
- }
21850
- function parseEditedTaskYaml(yaml, originalTask) {
21851
- const patch = {};
21852
21479
  try {
21853
- const lines = yaml.split("\n").filter((line) => !line.startsWith("#") && line.trim());
21854
- for (const line of lines) {
21855
- const colonIndex = line.indexOf(":");
21856
- if (colonIndex === -1) continue;
21857
- const key = line.substring(0, colonIndex).trim();
21858
- const value = line.substring(colonIndex + 1).trim();
21859
- switch (key) {
21860
- case "title":
21861
- if (value && value !== originalTask.title) {
21862
- patch.title = value;
21863
- }
21864
- break;
21865
- case "description":
21866
- if (value !== (originalTask.description || "")) {
21867
- patch.description = value || void 0;
21868
- }
21869
- break;
21870
- case "priority":
21871
- const validPriorities = ["low", "medium", "high", "critical", ""];
21872
- if (validPriorities.includes(value) && value !== (originalTask.priority || "")) {
21873
- patch.priority = value || null;
21874
- }
21875
- break;
21876
- case "tags":
21877
- const newTags = value ? value.split(",").map((t) => t.trim()).filter(Boolean) : [];
21878
- const oldTags = originalTask.tags || [];
21879
- if (JSON.stringify(newTags) !== JSON.stringify(oldTags)) {
21880
- patch.tags = newTags.length > 0 ? newTags : null;
21881
- }
21882
- break;
21883
- case "assignee":
21884
- if (value !== (originalTask.assignee || "")) {
21885
- patch.assignee = value || null;
21886
- }
21887
- break;
21888
- case "dueDate":
21889
- if (value !== (originalTask.dueDate || "")) {
21890
- patch.dueDate = value || null;
21891
- }
21892
- break;
21893
- case "relatedFiles":
21894
- const newFiles = value ? value.split(",").map((f) => f.trim()).filter(Boolean) : [];
21895
- const oldFiles = originalTask.relatedFiles || [];
21896
- if (JSON.stringify(newFiles) !== JSON.stringify(oldFiles)) {
21897
- patch.relatedFiles = newFiles.length > 0 ? newFiles : null;
21898
- }
21899
- break;
21900
- }
21901
- }
21902
- return { success: true, patch };
21480
+ const found = (0, import_core.findV2Task)((0, import_core.getV2Dirs)(filePath), taskId, true);
21481
+ if (!found) return { success: false, error: `Document ${taskId} not found` };
21482
+ return { success: true, path: found.filePath };
21903
21483
  } catch (err) {
21904
- return { success: false, error: `Failed to parse edited YAML: ${err}` };
21905
- }
21906
- }
21907
- function cleanupTempFile(filePath) {
21908
- try {
21909
- if (fs4.existsSync(filePath)) {
21910
- fs4.unlinkSync(filePath);
21911
- }
21912
- } catch {
21484
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
21913
21485
  }
21914
21486
  }
21915
21487
  function copyToClipboard(text) {
@@ -21941,66 +21513,38 @@ function copyToClipboard(text) {
21941
21513
  return { success: false, error: `Failed to copy: ${err}` };
21942
21514
  }
21943
21515
  }
21944
- function addRuleAction(filePath, ruleType, ruleText) {
21945
- const { board, error } = readBoardConfigOnly(filePath);
21946
- if (!board) return { success: false, error };
21947
- if (!board.rules) {
21948
- board.rules = {
21949
- always: [],
21950
- never: [],
21951
- prefer: [],
21952
- context: []
21953
- };
21954
- }
21955
- if (!board.rules[ruleType]) {
21956
- board.rules[ruleType] = [];
21957
- }
21958
- const existingIds = board.rules[ruleType].map((r) => r.id);
21959
- const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0;
21960
- const newRuleId = maxId + 1;
21961
- board.rules[ruleType].push({
21962
- id: newRuleId,
21963
- rule: ruleText.trim()
21964
- });
21965
- const writeResult = writeBrainfile(filePath, board);
21966
- if (!writeResult.success) return writeResult;
21967
- return { success: true, message: `Added ${ruleType} rule #${newRuleId}` };
21968
- }
21969
- function updateRuleAction(filePath, ruleType, ruleId, ruleText) {
21970
- const { board, error } = readBoardConfigOnly(filePath);
21971
- if (!board) return { success: false, error };
21972
- if (!board.rules || !board.rules[ruleType]) {
21973
- return { success: false, error: `No ${ruleType} rules found` };
21974
- }
21975
- const rule = board.rules[ruleType].find((r) => r.id === ruleId);
21976
- if (!rule) {
21977
- return { success: false, error: `Rule #${ruleId} not found` };
21978
- }
21979
- rule.rule = ruleText.trim();
21980
- const writeResult = writeBrainfile(filePath, board);
21981
- if (!writeResult.success) return writeResult;
21982
- return { success: true, message: `Updated ${ruleType} rule #${ruleId}` };
21983
- }
21984
- function deleteRuleAction(filePath, ruleType, ruleId) {
21985
- const { board, error } = readBoardConfigOnly(filePath);
21986
- if (!board) return { success: false, error };
21987
- if (!board.rules || !board.rules[ruleType]) {
21988
- return { success: false, error: `No ${ruleType} rules found` };
21989
- }
21990
- const ruleIndex = board.rules[ruleType].findIndex((r) => r.id === ruleId);
21991
- if (ruleIndex === -1) {
21992
- return { success: false, error: `Rule #${ruleId} not found` };
21516
+ function getTaskActivity(filePath, taskId, limit = 5) {
21517
+ if (!(0, import_core.isV2)(filePath)) return [];
21518
+ try {
21519
+ const dirs = (0, import_core.getV2Dirs)(filePath);
21520
+ const doc = (0, import_core3.findTask)(dirs.boardDir, taskId) ?? (0, import_core3.findTask)(dirs.logsDir, taskId);
21521
+ const entries = [];
21522
+ if (doc) {
21523
+ for (const note of (0, import_core3.parseNotes)(doc.body)) {
21524
+ entries.push({
21525
+ at: note.at,
21526
+ text: note.agent ? `[${note.agent}] ${note.text}` : note.text
21527
+ });
21528
+ }
21529
+ }
21530
+ try {
21531
+ for (const record of (0, import_core3.readLedger)(dirs.logsDir)) {
21532
+ if (record.id !== taskId) continue;
21533
+ entries.push({ at: record.completedAt, text: `completed \xB7 ${record.summary}` });
21534
+ }
21535
+ } catch {
21536
+ }
21537
+ entries.sort((a, b) => (Date.parse(b.at ?? "") || 0) - (Date.parse(a.at ?? "") || 0));
21538
+ return entries.slice(0, limit);
21539
+ } catch {
21540
+ return [];
21993
21541
  }
21994
- board.rules[ruleType].splice(ruleIndex, 1);
21995
- const writeResult = writeBrainfile(filePath, board);
21996
- if (!writeResult.success) return writeResult;
21997
- return { success: true, message: `Deleted ${ruleType} rule #${ruleId}` };
21998
21542
  }
21999
21543
  function loadLogs(filePath) {
22000
21544
  if ((0, import_core.isV2)(filePath)) {
22001
21545
  try {
22002
21546
  const dirs = (0, import_core.getV2Dirs)(filePath);
22003
- const docs = (0, import_core4.readTasksDir)(dirs.logsDir);
21547
+ const docs = (0, import_core3.readTasksDir)(dirs.logsDir);
22004
21548
  const logs = docs.map((doc) => ({ ...doc.task })).sort((a, b) => {
22005
21549
  const aTime = a.completedAt ? Date.parse(a.completedAt) : 0;
22006
21550
  const bTime = b.completedAt ? Date.parse(b.completedAt) : 0;
@@ -22012,12 +21556,12 @@ function loadLogs(filePath) {
22012
21556
  }
22013
21557
  }
22014
21558
  const archivePath = getArchivePath(filePath);
22015
- if (!fs4.existsSync(archivePath)) {
21559
+ if (!fs3.existsSync(archivePath)) {
22016
21560
  return { logs: [] };
22017
21561
  }
22018
21562
  try {
22019
- const archiveContent = fs4.readFileSync(archivePath, "utf-8");
22020
- const parseResult = import_core4.Brainfile.parseWithErrors(archiveContent);
21563
+ const archiveContent = fs3.readFileSync(archivePath, "utf-8");
21564
+ const parseResult = import_core3.Brainfile.parseWithErrors(archiveContent);
22021
21565
  if (parseResult.board && parseResult.board.archive) {
22022
21566
  return { logs: parseResult.board.archive };
22023
21567
  }
@@ -22026,125 +21570,453 @@ function loadLogs(filePath) {
22026
21570
  return { logs: [], error: err instanceof Error ? err.message : String(err) };
22027
21571
  }
22028
21572
  }
22029
- function restoreTaskAction(filePath, taskId, toColumnId) {
22030
- if ((0, import_core.isV2)(filePath)) {
22031
- const dirs = (0, import_core.getV2Dirs)(filePath);
22032
- const { board, error } = readBoardState(filePath);
22033
- if (!board) return { success: false, error };
22034
- const column2 = (0, import_core4.findColumnById)(board, toColumnId) || (0, import_core4.findColumnByName)(board, toColumnId);
22035
- if (!column2) {
22036
- return { success: false, error: `Column ${toColumnId} not found` };
22037
- }
22038
- if ((0, import_core4.findTask)(dirs.boardDir, taskId)) {
22039
- return { success: false, error: `Task ${taskId} already exists in board` };
22040
- }
22041
- const logDoc = (0, import_core4.findTask)(dirs.logsDir, taskId);
22042
- if (!logDoc || !logDoc.filePath) {
22043
- return { success: false, error: `Task ${taskId} not found in logs` };
22044
- }
22045
- const restoredTask = {
22046
- ...logDoc.task,
22047
- column: column2.id,
22048
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22049
- };
22050
- delete restoredTask.completedAt;
22051
- const targetPath = path2.join(dirs.boardDir, `${taskId}.md`);
22052
- try {
22053
- (0, import_core4.writeTaskFile)(targetPath, restoredTask, logDoc.body);
22054
- fs4.unlinkSync(logDoc.filePath);
22055
- return { success: true, message: `Restored ${taskId} to ${column2.title}` };
22056
- } catch (err) {
22057
- return { success: false, error: `Failed to restore task: ${err instanceof Error ? err.message : String(err)}` };
21573
+
21574
+ // src/tui/hooks/useBrainfileLoader.ts
21575
+ function getV2ChangeSignature(filePath) {
21576
+ const dirs = (0, import_core.getV2Dirs)(filePath);
21577
+ const parts = [];
21578
+ try {
21579
+ const boardEntries = fs4.existsSync(dirs.boardDir) ? fs4.readdirSync(dirs.boardDir).filter((name) => name.endsWith(".md")).sort() : [];
21580
+ for (const name of boardEntries) {
21581
+ const fullPath = path2.join(dirs.boardDir, name);
21582
+ try {
21583
+ const stat4 = fs4.statSync(fullPath);
21584
+ parts.push(`board:${name}:${stat4.mtimeMs}:${stat4.size}`);
21585
+ } catch {
21586
+ }
22058
21587
  }
21588
+ } catch {
22059
21589
  }
22060
- const archivePath = getArchivePath(filePath);
22061
- if (!fs4.existsSync(archivePath)) {
22062
- return { success: false, error: "Logs file not found" };
22063
- }
22064
- let archiveBoard;
22065
21590
  try {
22066
- const archiveContent = fs4.readFileSync(archivePath, "utf-8");
22067
- const parseResult = import_core4.Brainfile.parseWithErrors(archiveContent);
22068
- if (!parseResult.board) {
22069
- return { success: false, error: "Failed to parse logs file" };
21591
+ const logsEntries = fs4.existsSync(dirs.logsDir) ? fs4.readdirSync(dirs.logsDir).filter((name) => name.endsWith(".md")).sort() : [];
21592
+ for (const name of logsEntries) {
21593
+ const fullPath = path2.join(dirs.logsDir, name);
21594
+ try {
21595
+ const stat4 = fs4.statSync(fullPath);
21596
+ parts.push(`logs:${name}:${stat4.mtimeMs}:${stat4.size}`);
21597
+ } catch {
21598
+ }
22070
21599
  }
22071
- archiveBoard = parseResult.board;
22072
- } catch (err) {
22073
- return { success: false, error: `Failed to read logs: ${err instanceof Error ? err.message : String(err)}` };
22074
- }
22075
- if (!archiveBoard.archive || archiveBoard.archive.length === 0) {
22076
- return { success: false, error: "Logs are empty" };
22077
- }
22078
- const taskIndex = archiveBoard.archive.findIndex((t) => t.id === taskId);
22079
- if (taskIndex === -1) {
22080
- return { success: false, error: `Task ${taskId} not found in logs` };
22081
- }
22082
- const task = archiveBoard.archive[taskIndex];
22083
- const { board: mainBoard, error: mainBoardError } = readBrainfile(filePath);
22084
- if (!mainBoard) return { success: false, error: mainBoardError };
22085
- const column = (0, import_core4.findColumnById)(mainBoard, toColumnId) || (0, import_core4.findColumnByName)(mainBoard, toColumnId);
22086
- if (!column) {
22087
- return { success: false, error: `Column ${toColumnId} not found` };
22088
- }
22089
- column.tasks.unshift(task);
22090
- archiveBoard.archive.splice(taskIndex, 1);
22091
- const writeResult = writeBrainfile(filePath, mainBoard);
22092
- if (!writeResult.success) return writeResult;
22093
- try {
22094
- const archiveContent = import_core4.Brainfile.serialize(archiveBoard);
22095
- fs4.writeFileSync(archivePath, archiveContent, "utf-8");
22096
- } catch (err) {
22097
- return { success: false, error: `Task restored but failed to update logs: ${err}` };
21600
+ } catch {
22098
21601
  }
22099
- return { success: true, message: `Restored ${taskId} to ${column.title}` };
21602
+ return parts.join("|");
22100
21603
  }
22101
- function deleteArchivedTaskAction(filePath, taskId) {
22102
- if ((0, import_core.isV2)(filePath)) {
22103
- const dirs = (0, import_core.getV2Dirs)(filePath);
22104
- const logDoc = (0, import_core4.findTask)(dirs.logsDir, taskId);
22105
- if (!logDoc || !logDoc.filePath) {
22106
- return { success: false, error: `Task ${taskId} not found in logs` };
21604
+ function useBrainfileLoader(filePath, state, setState) {
21605
+ const flashTimeoutRef = (0, import_react34.useRef)(null);
21606
+ const loadBrainfile = (0, import_react34.useCallback)((forceRefresh = false) => {
21607
+ try {
21608
+ const content = fs4.readFileSync(filePath, "utf-8");
21609
+ const v2 = (0, import_core.isV2)(filePath);
21610
+ let board = null;
21611
+ let parseError;
21612
+ if (v2) {
21613
+ try {
21614
+ board = (0, import_core.buildBoardFromV2)(filePath);
21615
+ } catch (err) {
21616
+ parseError = err instanceof Error ? err.message : String(err);
21617
+ }
21618
+ } else {
21619
+ const result = import_core4.Brainfile.parseWithErrors(content);
21620
+ if (result.board) {
21621
+ board = result.board;
21622
+ } else {
21623
+ parseError = result.error || "Not a valid brainfile";
21624
+ }
21625
+ }
21626
+ const contentHash = v2 ? (0, import_core4.hashBoardContent)(`${content}
21627
+ ${getV2ChangeSignature(filePath)}`) : (0, import_core4.hashBoardContent)(content);
21628
+ const logs = loadLogs(filePath).logs;
21629
+ setState((prev) => {
21630
+ if (!forceRefresh && prev.lastContentHash === contentHash) {
21631
+ return prev;
21632
+ }
21633
+ if (board) {
21634
+ if (prev.doneView) {
21635
+ const prevLogId = prev.logs[prev.selectedTaskIndex]?.id;
21636
+ const foundIdx = prevLogId ? logs.findIndex((t) => t.id === prevLogId) : -1;
21637
+ const nextIndex = foundIdx >= 0 ? foundIdx : Math.max(0, Math.min(prev.selectedTaskIndex, logs.length - 1));
21638
+ return {
21639
+ ...prev,
21640
+ board,
21641
+ logs,
21642
+ error: null,
21643
+ lastUpdated: /* @__PURE__ */ new Date(),
21644
+ selectedTaskIndex: nextIndex,
21645
+ reloadFlash: true,
21646
+ lastContentHash: contentHash
21647
+ };
21648
+ }
21649
+ const prevColumn = prev.board?.columns[prev.activeColumnIndex];
21650
+ const prevTaskId = prevColumn?.tasks[prev.selectedTaskIndex]?.id;
21651
+ let newColumnIndex = prev.activeColumnIndex;
21652
+ let newTaskIndex = 0;
21653
+ if (prevColumn) {
21654
+ const colIdx = board.columns.findIndex((c) => c.id === prevColumn.id);
21655
+ if (colIdx >= 0) {
21656
+ newColumnIndex = colIdx;
21657
+ if (prevTaskId) {
21658
+ const taskIdx = board.columns[colIdx].tasks.findIndex((t) => t.id === prevTaskId);
21659
+ if (taskIdx >= 0) newTaskIndex = taskIdx;
21660
+ }
21661
+ }
21662
+ }
21663
+ newColumnIndex = Math.min(newColumnIndex, Math.max(0, board.columns.length - 1));
21664
+ const col = board.columns[newColumnIndex];
21665
+ newTaskIndex = Math.min(newTaskIndex, Math.max(0, (col?.tasks.length || 1) - 1));
21666
+ return {
21667
+ ...prev,
21668
+ board,
21669
+ logs,
21670
+ error: null,
21671
+ lastUpdated: /* @__PURE__ */ new Date(),
21672
+ activeColumnIndex: newColumnIndex,
21673
+ selectedTaskIndex: newTaskIndex,
21674
+ reloadFlash: true,
21675
+ lastContentHash: contentHash
21676
+ };
21677
+ }
21678
+ let errorMessage;
21679
+ if (parseError) {
21680
+ errorMessage = parseError;
21681
+ } else {
21682
+ errorMessage = "Not a valid brainfile";
21683
+ }
21684
+ return {
21685
+ ...prev,
21686
+ error: errorMessage,
21687
+ lastContentHash: contentHash
21688
+ };
21689
+ });
21690
+ if (flashTimeoutRef.current) clearTimeout(flashTimeoutRef.current);
21691
+ flashTimeoutRef.current = setTimeout(() => {
21692
+ setState((prev) => ({ ...prev, reloadFlash: false }));
21693
+ }, 1e3);
21694
+ } catch (err) {
21695
+ const errorMsg = err instanceof Error ? err.message : String(err);
21696
+ let userMessage = errorMsg;
21697
+ if (errorMsg.includes("ENOENT")) {
21698
+ userMessage = `File not found: ${filePath}`;
21699
+ } else if (errorMsg.includes("EACCES")) {
21700
+ userMessage = `Permission denied: ${filePath}`;
21701
+ } else if (errorMsg.includes("EISDIR")) {
21702
+ userMessage = `Path is a directory: ${filePath}`;
21703
+ }
21704
+ setState((prev) => ({
21705
+ ...prev,
21706
+ error: userMessage
21707
+ }));
21708
+ }
21709
+ }, [filePath, setState]);
21710
+ (0, import_react34.useEffect)(() => {
21711
+ loadBrainfile();
21712
+ }, [loadBrainfile]);
21713
+ (0, import_react34.useEffect)(() => {
21714
+ return () => {
21715
+ if (flashTimeoutRef.current) clearTimeout(flashTimeoutRef.current);
21716
+ };
21717
+ }, []);
21718
+ (0, import_react34.useEffect)(() => {
21719
+ const v2 = (0, import_core.isV2)(filePath);
21720
+ const dirs = v2 ? (0, import_core.getV2Dirs)(filePath) : null;
21721
+ const watchPaths = v2 && dirs ? [filePath, dirs.boardDir, dirs.logsDir] : [filePath];
21722
+ const watcher = watch(watchPaths, {
21723
+ persistent: true,
21724
+ ignoreInitial: true,
21725
+ usePolling: true,
21726
+ interval: 750,
21727
+ awaitWriteFinish: {
21728
+ stabilityThreshold: 250,
21729
+ pollInterval: 100
21730
+ },
21731
+ depth: v2 ? 1 : 0
21732
+ });
21733
+ watcher.on("add", () => loadBrainfile());
21734
+ watcher.on("change", () => loadBrainfile());
21735
+ watcher.on("unlink", () => loadBrainfile());
21736
+ watcher.on("error", (err) => {
21737
+ const message = err instanceof Error ? err.message : String(err);
21738
+ setState((prev) => ({
21739
+ ...prev,
21740
+ error: `File watcher error: ${message}`
21741
+ }));
21742
+ });
21743
+ return () => {
21744
+ watcher.close();
21745
+ };
21746
+ }, [filePath, loadBrainfile, setState]);
21747
+ return { loadBrainfile };
21748
+ }
21749
+
21750
+ // src/tui/hooks/useKeyboardNavigation.ts
21751
+ var import_react36 = __toESM(require_react());
21752
+ import { spawnSync as spawnSync2 } from "child_process";
21753
+
21754
+ // src/tui/detailStops.ts
21755
+ function buildDetailStops(task, children) {
21756
+ const stops = children.map((child) => ({ kind: "child", task: child }));
21757
+ (task.subtasks ?? []).forEach((subtask, index) => {
21758
+ stops.push({ kind: "subtask", subtask, index });
21759
+ });
21760
+ return stops;
21761
+ }
21762
+ function clampDetailCursor(cursor, stopCount) {
21763
+ if (stopCount <= 0) return 0;
21764
+ return Math.max(0, Math.min(cursor, stopCount - 1));
21765
+ }
21766
+
21767
+ // src/tui/components/DetailView.tsx
21768
+ var import_react35 = __toESM(require_react());
21769
+ var MAX_DELIVERABLES = 3;
21770
+ var MAX_VALIDATION_COMMANDS = 2;
21771
+ function computeDetailLayout(task, width, height, children, activity) {
21772
+ const contentWidth = Math.max(20, width - 2);
21773
+ const stacked = contentWidth < 60;
21774
+ const subtasks = task.subtasks ?? [];
21775
+ const files = task.relatedFiles ?? [];
21776
+ const contract = task.contract;
21777
+ const bodyLines = buildBodyLines(task.description, contentWidth);
21778
+ const metaLines = stacked ? 4 : 1;
21779
+ const childrenLines = children.length > 0 ? 1 + children.length : 0;
21780
+ const subtasksLines = subtasks.length > 0 ? 1 + subtasks.length : 0;
21781
+ const contractLines = contract ? 1 + Math.min(contract.deliverables?.length ?? 0, MAX_DELIVERABLES) + Math.min(contract.validation?.commands?.length ?? 0, MAX_VALIDATION_COMMANDS) + (contract.feedback ? 1 : 0) : 0;
21782
+ const activityLines = activity.length > 0 ? 1 + activity.length : 0;
21783
+ const filesLines = files.length > 0 ? 1 : 0;
21784
+ const sectionsWithMargin = [
21785
+ bodyLines.length > 0,
21786
+ childrenLines > 0,
21787
+ subtasksLines > 0,
21788
+ contractLines > 0,
21789
+ activityLines > 0,
21790
+ filesLines > 0
21791
+ ].filter(Boolean).length;
21792
+ const bodyBudget = Math.max(
21793
+ 2,
21794
+ height - 2 - metaLines - 1 - childrenLines - subtasksLines - contractLines - activityLines - filesLines - sectionsWithMargin
21795
+ );
21796
+ return { bodyLines, bodyBudget, bodyOverflows: bodyLines.length > bodyBudget };
21797
+ }
21798
+ function DetailView({
21799
+ task,
21800
+ columnLabel,
21801
+ width,
21802
+ height,
21803
+ breadcrumb,
21804
+ parent,
21805
+ children,
21806
+ activity,
21807
+ cursor,
21808
+ scrollOffset
21809
+ }) {
21810
+ const contentWidth = Math.max(20, width - 2);
21811
+ const stacked = contentWidth < 60;
21812
+ const priority = task.priority;
21813
+ const subtasks = task.subtasks ?? [];
21814
+ const files = task.relatedFiles ?? [];
21815
+ const contract = task.contract;
21816
+ const { bodyLines, bodyBudget, bodyOverflows } = computeDetailLayout(
21817
+ task,
21818
+ width,
21819
+ height,
21820
+ children,
21821
+ activity
21822
+ );
21823
+ const clampedScroll = Math.max(0, Math.min(scrollOffset, Math.max(0, bodyLines.length - bodyBudget)));
21824
+ const visibleBody = bodyLines.slice(clampedScroll, clampedScroll + bodyBudget);
21825
+ const stateText = priority ? `${columnLabel} \xB7 ${priority}` : columnLabel;
21826
+ const breadcrumbBudget = Math.max(6, Math.floor(contentWidth * 0.4));
21827
+ const breadcrumbStr = truncateStart(breadcrumb.join(" \u25B8 ") || task.id, breadcrumbBudget);
21828
+ const headWidth = Math.max(1, contentWidth - stateText.length - 2);
21829
+ const head = truncate(`${breadcrumbStr} ${task.title}`, headWidth);
21830
+ return /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", width, flexShrink: 0 }, /* @__PURE__ */ import_react35.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react35.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.text, bold: true }, head), /* @__PURE__ */ import_react35.default.createElement(Text, null, pad(Math.max(1, contentWidth - head.length - stateText.length))), /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textSecondary }, columnLabel), priority ? /* @__PURE__ */ import_react35.default.createElement(Text, null, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textDim }, " \xB7 "), /* @__PURE__ */ import_react35.default.createElement(Text, { color: getPriorityColor(priority) }, priority)) : null)), /* @__PURE__ */ import_react35.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(contentWidth))), /* @__PURE__ */ import_react35.default.createElement(MetadataBlock, { task, parent, stacked, width }), bodyLines.length > 0 ? /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, /* @__PURE__ */ import_react35.default.createElement(
21831
+ DescriptionHeading,
21832
+ {
21833
+ bodyOverflows,
21834
+ scrollStart: clampedScroll + 1,
21835
+ total: bodyLines.length,
21836
+ contentWidth
22107
21837
  }
22108
- const result = (0, import_core4.deleteTaskFile)(logDoc.filePath);
22109
- if (!result.success) {
22110
- return { success: false, error: result.error || `Failed to delete ${taskId}` };
21838
+ ), visibleBody.map((line, index) => /* @__PURE__ */ import_react35.default.createElement(
21839
+ Text,
21840
+ {
21841
+ key: `body-${index}`,
21842
+ wrap: "truncate",
21843
+ bold: line.kind === "heading",
21844
+ color: line.kind === "code" ? PALETTE.textMuted : line.kind === "heading" ? PALETTE.text : PALETTE.textSecondary
21845
+ },
21846
+ line.text || " "
21847
+ ))) : null, children.length > 0 ? /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, `children (${children.length})`), children.map((child, index) => /* @__PURE__ */ import_react35.default.createElement(
21848
+ StopRow,
21849
+ {
21850
+ key: child.id,
21851
+ selected: index === cursor,
21852
+ left: ` ${child.id} ${child.title}`,
21853
+ right: child.column ?? "",
21854
+ rightColor: PALETTE.textSecondary,
21855
+ contentWidth
21856
+ }
21857
+ ))) : null, subtasks.length > 0 ? /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, `subtasks (${subtasks.filter((s) => s.completed).length}/${subtasks.length})`), subtasks.map((subtask, index) => {
21858
+ const stopIndex = children.length + index;
21859
+ const glyph = subtask.completed ? GLYPHS.subtaskDone : GLYPHS.subtaskOpen;
21860
+ return /* @__PURE__ */ import_react35.default.createElement(
21861
+ StopRow,
21862
+ {
21863
+ key: subtask.id,
21864
+ selected: stopIndex === cursor,
21865
+ left: ` ${glyph} ${subtask.title}`,
21866
+ leftColor: subtask.completed ? PALETTE.textMuted : PALETTE.textSecondary,
21867
+ contentWidth
21868
+ }
21869
+ );
21870
+ })) : null, contract ? /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, /* @__PURE__ */ import_react35.default.createElement(Text, null, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, "contract "), /* @__PURE__ */ import_react35.default.createElement(Text, { color: getContractStateColor(contract.status) }, contract.status)), contract.deliverables?.length ? /* @__PURE__ */ import_react35.default.createElement(
21871
+ LabeledList,
21872
+ {
21873
+ label: "deliverables",
21874
+ items: contract.deliverables.slice(0, MAX_DELIVERABLES).map((d) => d.description ? `${d.path} \u2014 ${d.description}` : d.path),
21875
+ contentWidth,
21876
+ color: PALETTE.textSecondary
22111
21877
  }
22112
- return { success: true, message: `Permanently deleted ${taskId}` };
22113
- }
22114
- const archivePath = getArchivePath(filePath);
22115
- if (!fs4.existsSync(archivePath)) {
22116
- return { success: false, error: "Logs file not found" };
21878
+ ) : null, contract.validation?.commands?.length ? /* @__PURE__ */ import_react35.default.createElement(
21879
+ LabeledList,
21880
+ {
21881
+ label: "validation",
21882
+ items: contract.validation.commands.slice(0, MAX_VALIDATION_COMMANDS),
21883
+ contentWidth,
21884
+ color: PALETTE.textMuted
21885
+ }
21886
+ ) : null, contract.feedback ? /* @__PURE__ */ import_react35.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, " feedback "), /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.error }, truncate(contract.feedback, Math.max(1, contentWidth - 12)))) : null) : null, activity.length > 0 ? /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, "activity"), activity.map((entry, index) => {
21887
+ const stamp = entry.at ? entry.at.slice(5, 10) : " ";
21888
+ const prefix = ` ${stamp} `;
21889
+ return /* @__PURE__ */ import_react35.default.createElement(Text, { key: `activity-${index}`, wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textDim }, prefix), /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textSecondary }, truncate(entry.text, Math.max(1, contentWidth - prefix.length))));
21890
+ })) : null, files.length > 0 ? /* @__PURE__ */ import_react35.default.createElement(Box_default, { paddingLeft: 1, marginTop: 1, width }, /* @__PURE__ */ import_react35.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, "files "), /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textSecondary }, truncate(files.join(" \xB7 "), Math.max(1, contentWidth - 7))))) : null);
21891
+ }
21892
+ function DescriptionHeading({
21893
+ bodyOverflows,
21894
+ scrollStart,
21895
+ total,
21896
+ contentWidth
21897
+ }) {
21898
+ const indicator = bodyOverflows ? `${GLYPHS.scroll} ${scrollStart}/${total}` : "";
21899
+ const gap = Math.max(1, contentWidth - "description".length - indicator.length);
21900
+ return /* @__PURE__ */ import_react35.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, "description"), indicator ? /* @__PURE__ */ import_react35.default.createElement(Text, null, pad(gap)) : null, indicator ? /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textDim }, indicator) : null);
21901
+ }
21902
+ function StopRow({
21903
+ selected,
21904
+ left,
21905
+ right,
21906
+ leftColor,
21907
+ rightColor,
21908
+ contentWidth
21909
+ }) {
21910
+ const rightText = right ?? "";
21911
+ const leftTruncated = truncate(left, Math.max(1, contentWidth - rightText.length - 2));
21912
+ const gap = Math.max(1, contentWidth - leftTruncated.length - rightText.length);
21913
+ return /* @__PURE__ */ import_react35.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: selected ? void 0 : leftColor, inverse: selected }, leftTruncated), /* @__PURE__ */ import_react35.default.createElement(Text, null, pad(gap)), rightText ? /* @__PURE__ */ import_react35.default.createElement(Text, { color: rightColor }, rightText) : null);
21914
+ }
21915
+ function LabeledList({
21916
+ label,
21917
+ items,
21918
+ contentWidth,
21919
+ color
21920
+ }) {
21921
+ const labelWidth = 16;
21922
+ return /* @__PURE__ */ import_react35.default.createElement(import_react35.default.Fragment, null, items.map((item, index) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: `${label}-${index}`, wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, ` ${index === 0 ? label.padEnd(labelWidth - 2) : " ".repeat(labelWidth - 2)}`), /* @__PURE__ */ import_react35.default.createElement(Text, { color }, truncate(item, Math.max(1, contentWidth - labelWidth))))));
21923
+ }
21924
+ function MetadataBlock({
21925
+ task,
21926
+ parent,
21927
+ stacked,
21928
+ width
21929
+ }) {
21930
+ const tags = task.tags?.length ? task.tags.map((t) => `#${t}`).join(" ") : "\u2014";
21931
+ const assignee = task.assignee || "\u2014";
21932
+ const created = task.createdAt ? task.createdAt.slice(0, 10) : "\u2014";
21933
+ const parentLabel = parentLabelFor(task, parent);
21934
+ const fields = [
21935
+ ["tags", tags],
21936
+ ["assignee", assignee],
21937
+ ["created", created],
21938
+ ["parent", parentLabel]
21939
+ ];
21940
+ if (stacked) {
21941
+ return /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, width }, fields.map(([label, value]) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: label, wrap: "truncate" }, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, `${label} `), /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textSecondary }, value))));
22117
21942
  }
22118
- let archiveBoard;
22119
- try {
22120
- const archiveContent = fs4.readFileSync(archivePath, "utf-8");
22121
- const parseResult = import_core4.Brainfile.parseWithErrors(archiveContent);
22122
- if (!parseResult.board) {
22123
- return { success: false, error: "Failed to parse logs file" };
21943
+ return /* @__PURE__ */ import_react35.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react35.default.createElement(Text, { wrap: "truncate" }, fields.map(([label, value], index) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: label }, index > 0 ? /* @__PURE__ */ import_react35.default.createElement(Text, null, pad(4)) : null, /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textMuted }, `${label} `), /* @__PURE__ */ import_react35.default.createElement(Text, { color: PALETTE.textSecondary }, value)))));
21944
+ }
21945
+ function parentLabelFor(task, parent) {
21946
+ if (!task.parentId) return "\u2014";
21947
+ if (!parent) return `${task.parentId} \u2197`;
21948
+ if (getDocType(parent) === "plan") return `plan ${parent.id}`;
21949
+ return parent.id;
21950
+ }
21951
+ function buildBodyLines(description, width) {
21952
+ if (!description) return [];
21953
+ const out = [];
21954
+ let inFence = false;
21955
+ for (const raw of description.split("\n")) {
21956
+ const line = raw.replace(/\s+$/, "");
21957
+ if (line.trim().startsWith("```")) {
21958
+ inFence = !inFence;
21959
+ continue;
21960
+ }
21961
+ if (inFence) {
21962
+ out.push({ text: ` ${truncate(line, Math.max(1, width - 2))}`, kind: "code" });
21963
+ continue;
21964
+ }
21965
+ if (/^#{1,6}\s/.test(line)) {
21966
+ out.push({ text: truncate(line.replace(/^#{1,6}\s*/, ""), width), kind: "heading" });
21967
+ continue;
21968
+ }
21969
+ if (/^\s*[-*+]\s/.test(line)) {
21970
+ const item = line.replace(/^\s*[-*+]\s*/, "");
21971
+ for (const wrapped of wrapText2(item, Math.max(1, width - 4))) {
21972
+ out.push({ text: ` \u2022 ${wrapped}`, kind: "list" });
21973
+ }
21974
+ continue;
21975
+ }
21976
+ if (line.trim() === "") {
21977
+ out.push({ text: "", kind: "text" });
21978
+ continue;
21979
+ }
21980
+ for (const wrapped of wrapText2(line, width)) {
21981
+ out.push({ text: wrapped, kind: "text" });
22124
21982
  }
22125
- archiveBoard = parseResult.board;
22126
- } catch (err) {
22127
- return { success: false, error: `Failed to read logs: ${err instanceof Error ? err.message : String(err)}` };
22128
- }
22129
- if (!archiveBoard.archive || archiveBoard.archive.length === 0) {
22130
- return { success: false, error: "Logs are empty" };
22131
- }
22132
- const taskIndex = archiveBoard.archive.findIndex((t) => t.id === taskId);
22133
- if (taskIndex === -1) {
22134
- return { success: false, error: `Task ${taskId} not found in logs` };
22135
21983
  }
22136
- archiveBoard.archive.splice(taskIndex, 1);
21984
+ return out;
21985
+ }
21986
+
21987
+ // src/tui/tuiState.ts
21988
+ var import_core5 = __toESM(require_dist());
21989
+ import * as fs5 from "fs";
21990
+ import * as path3 from "path";
21991
+ function getTuiStatePath(brainfilePath) {
21992
+ return path3.join((0, import_core5.getBrainfileStateDir)(brainfilePath), import_core5.DOT_BRAINFILE_STATE_DIRNAME, "tui.json");
21993
+ }
21994
+ function readTuiState(brainfilePath) {
21995
+ const statePath = getTuiStatePath(brainfilePath);
21996
+ if (!fs5.existsSync(statePath)) return { collapsed: [] };
22137
21997
  try {
22138
- const archiveContent = import_core4.Brainfile.serialize(archiveBoard);
22139
- fs4.writeFileSync(archivePath, archiveContent, "utf-8");
22140
- } catch (err) {
22141
- return { success: false, error: `Failed to update logs: ${err}` };
21998
+ const parsed = JSON.parse(fs5.readFileSync(statePath, "utf-8"));
21999
+ const collapsed = Array.isArray(parsed.collapsed) ? parsed.collapsed.filter((id) => typeof id === "string") : [];
22000
+ return {
22001
+ collapsed,
22002
+ ...typeof parsed.lastColumn === "string" ? { lastColumn: parsed.lastColumn } : {},
22003
+ ...typeof parsed.lastTypeFilter === "string" ? { lastTypeFilter: parsed.lastTypeFilter } : {}
22004
+ };
22005
+ } catch {
22006
+ return { collapsed: [] };
22142
22007
  }
22143
- return { success: true, message: `Permanently deleted ${taskId}` };
22008
+ }
22009
+ function patchTuiState(brainfilePath, patch) {
22010
+ const current = readTuiState(brainfilePath);
22011
+ writeTuiState(brainfilePath, { ...current, ...patch });
22012
+ }
22013
+ function writeTuiState(brainfilePath, state) {
22014
+ (0, import_core5.ensureDotBrainfileGitignore)(brainfilePath);
22015
+ (0, import_core5.atomicWriteFileSync)(getTuiStatePath(brainfilePath), `${JSON.stringify(state, null, 2)}
22016
+ `);
22144
22017
  }
22145
22018
 
22146
22019
  // src/tui/hooks/useKeyboardNavigation.ts
22147
- var RULE_TYPES = ["always", "never", "prefer", "context"];
22148
22020
  var STATUS_MESSAGE_MS = 3e3;
22149
22021
  function useKeyboardNavigation({
22150
22022
  state,
@@ -22154,18 +22026,28 @@ function useKeyboardNavigation({
22154
22026
  viewportHeight,
22155
22027
  loadBrainfile,
22156
22028
  filePath,
22157
- allColumns
22029
+ allColumns,
22030
+ typeCycleOptions,
22031
+ detailTask,
22032
+ detailChildren,
22033
+ detailParent,
22034
+ detailActivity,
22035
+ detailWidth,
22036
+ detailHeight,
22037
+ doneView,
22038
+ detailArchived
22158
22039
  }) {
22159
- const { exit } = use_app_default();
22160
- const statusTimers = (0, import_react35.useRef)(/* @__PURE__ */ new Set());
22161
- (0, import_react35.useEffect)(
22040
+ const { exit, suspendTerminal } = use_app_default();
22041
+ const { isRawModeSupported } = use_stdin_default();
22042
+ const statusTimers = (0, import_react36.useRef)(/* @__PURE__ */ new Set());
22043
+ (0, import_react36.useEffect)(
22162
22044
  () => () => {
22163
22045
  for (const timer of statusTimers.current) clearTimeout(timer);
22164
22046
  statusTimers.current.clear();
22165
22047
  },
22166
22048
  []
22167
22049
  );
22168
- const showStatus = (0, import_react35.useCallback)(
22050
+ const showStatus = (0, import_react36.useCallback)(
22169
22051
  (text, type) => {
22170
22052
  const timestamp = Date.now();
22171
22053
  setState((prev) => ({ ...prev, statusMessage: { text, type, timestamp } }));
@@ -22179,14 +22061,56 @@ function useKeyboardNavigation({
22179
22061
  },
22180
22062
  [setState]
22181
22063
  );
22064
+ const openInEditor = (0, import_react36.useCallback)(
22065
+ (taskId) => {
22066
+ const located = resolveTaskFilePath(filePath, taskId);
22067
+ if (!located.success) {
22068
+ showStatus(located.error, "error");
22069
+ return;
22070
+ }
22071
+ if (!isRawModeSupported) {
22072
+ showStatus(STATUS.editorNeedsTty, "error");
22073
+ return;
22074
+ }
22075
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
22076
+ void suspendTerminal(async () => {
22077
+ spawnSync2(editor, [located.path], { stdio: "inherit", shell: true });
22078
+ }).then(() => {
22079
+ loadBrainfile(true);
22080
+ }).catch((err) => {
22081
+ showStatus(
22082
+ STATUS.editorFailed(err instanceof Error ? err.message : String(err)),
22083
+ "error"
22084
+ );
22085
+ });
22086
+ },
22087
+ [filePath, showStatus, suspendTerminal, loadBrainfile, isRawModeSupported]
22088
+ );
22089
+ const rememberColumn = (0, import_react36.useCallback)(
22090
+ (columnIndex) => {
22091
+ const id = allColumns[columnIndex]?.id;
22092
+ if (id) patchTuiState(filePath, { lastColumn: id });
22093
+ },
22094
+ [allColumns, filePath]
22095
+ );
22182
22096
  const currentTask = rows[state.selectedTaskIndex]?.task;
22183
22097
  const maxRowIndex = Math.max(0, rows.length - 1);
22098
+ const overlayTarget = state.detailPath.length > 0 ? detailTask : currentTask;
22184
22099
  const setMode = (mode, extra = {}) => setState((prev) => ({ ...prev, mode, ...extra }));
22100
+ const closeOverlay = (opts = {}) => {
22101
+ const clearDetail = opts.docRemoved || state.detailPath.length === 0;
22102
+ setState((prev) => ({
22103
+ ...prev,
22104
+ mode: clearDetail ? "browse" : "detail",
22105
+ detailPath: clearDetail ? [] : prev.detailPath,
22106
+ completeConfirm: null
22107
+ }));
22108
+ };
22185
22109
  const completeTask = (task, force) => {
22186
22110
  const result = archiveTaskAction(filePath, task.id, { force });
22187
22111
  if (result.success) {
22188
- showStatus(result.message || `Completed ${task.id}`, "success");
22189
- setMode("browse", { completeConfirm: null });
22112
+ showStatus(STATUS.completed(task.id), "success");
22113
+ closeOverlay({ docRemoved: true });
22190
22114
  loadBrainfile(true);
22191
22115
  return;
22192
22116
  }
@@ -22200,12 +22124,30 @@ function useKeyboardNavigation({
22200
22124
  });
22201
22125
  return;
22202
22126
  }
22203
- showStatus(result.error || "Failed to complete", "error");
22204
- setMode("browse", { completeConfirm: null });
22127
+ showStatus(result.error || STATUS.completeFailed, "error");
22128
+ closeOverlay();
22129
+ };
22130
+ const openHelp = () => {
22131
+ setState((prev) => ({
22132
+ ...prev,
22133
+ mode: "help",
22134
+ helpReturn: prev.mode === "help" ? prev.helpReturn : prev.mode
22135
+ }));
22136
+ };
22137
+ const closeHelp = () => {
22138
+ setState((prev) => ({
22139
+ ...prev,
22140
+ mode: prev.helpReturn && prev.helpReturn !== "help" ? prev.helpReturn : "browse",
22141
+ helpReturn: null
22142
+ }));
22205
22143
  };
22206
22144
  use_input_default((input, key) => {
22207
22145
  if (state.mode === "help") {
22208
- setMode("browse");
22146
+ if (input === "q" || key.ctrl && input === "c") {
22147
+ exit();
22148
+ return;
22149
+ }
22150
+ closeHelp();
22209
22151
  return;
22210
22152
  }
22211
22153
  if (state.mode === "complete-confirm") {
@@ -22214,44 +22156,50 @@ function useKeyboardNavigation({
22214
22156
  if (target) {
22215
22157
  const result = archiveTaskAction(filePath, target.id, { force: true });
22216
22158
  if (result.success) {
22217
- showStatus(result.message || `Completed ${target.id}`, "success");
22159
+ showStatus(STATUS.completed(target.id), "success");
22218
22160
  loadBrainfile(true);
22161
+ closeOverlay({ docRemoved: true });
22219
22162
  } else {
22220
- showStatus(result.error || "Failed to complete", "error");
22163
+ showStatus(result.error || STATUS.completeFailed, "error");
22164
+ closeOverlay();
22221
22165
  }
22166
+ } else {
22167
+ closeOverlay();
22222
22168
  }
22223
- setMode("browse", { completeConfirm: null });
22224
22169
  return;
22225
22170
  }
22226
22171
  if (input === "n" || input === "N" || key.escape) {
22227
- setMode("browse", { completeConfirm: null });
22228
- showStatus("Complete cancelled", "info");
22172
+ closeOverlay();
22173
+ showStatus(STATUS.completeCancelled, "info");
22229
22174
  }
22230
22175
  return;
22231
22176
  }
22232
22177
  if (state.mode === "delete-confirm") {
22233
22178
  if (input === "y" || input === "Y") {
22234
- if (currentTask) {
22235
- const result = deleteTaskAction(filePath, currentTask.id);
22179
+ if (overlayTarget) {
22180
+ const result = deleteTaskAction(filePath, overlayTarget.id);
22236
22181
  if (result.success) {
22237
- showStatus(`Deleted ${currentTask.id}`, "success");
22182
+ showStatus(STATUS.deleted(overlayTarget.id), "success");
22238
22183
  loadBrainfile(true);
22184
+ closeOverlay({ docRemoved: true });
22239
22185
  } else {
22240
- showStatus(result.error || "Failed to delete", "error");
22186
+ showStatus(result.error || STATUS.deleteFailed, "error");
22187
+ closeOverlay();
22241
22188
  }
22189
+ } else {
22190
+ closeOverlay();
22242
22191
  }
22243
- setMode("browse");
22244
22192
  return;
22245
22193
  }
22246
22194
  if (input === "n" || input === "N" || key.escape) {
22247
- setMode("browse");
22248
- showStatus("Delete cancelled", "info");
22195
+ closeOverlay();
22196
+ showStatus(STATUS.deleteCancelled, "info");
22249
22197
  }
22250
22198
  return;
22251
22199
  }
22252
22200
  if (state.mode === "move") {
22253
22201
  if (key.escape) {
22254
- setMode("browse");
22202
+ closeOverlay();
22255
22203
  return;
22256
22204
  }
22257
22205
  if (key.upArrow || key.leftArrow || input === "k" || input === "h") {
@@ -22267,16 +22215,16 @@ function useKeyboardNavigation({
22267
22215
  }
22268
22216
  if (key.return) {
22269
22217
  const targetColumn = allColumns[state.moveTargetIndex];
22270
- if (currentTask && targetColumn) {
22271
- const result = moveTaskAction(filePath, currentTask.id, targetColumn.id);
22218
+ if (overlayTarget && targetColumn) {
22219
+ const result = moveTaskAction(filePath, overlayTarget.id, targetColumn.id);
22272
22220
  if (result.success) {
22273
- showStatus(result.message || `Moved to ${targetColumn.title}`, "success");
22221
+ showStatus(result.message || STATUS.moved(targetColumn.title), "success");
22274
22222
  loadBrainfile(true);
22275
22223
  } else {
22276
- showStatus(result.error || "Failed to move", "error");
22224
+ showStatus(result.error || STATUS.moveFailed, "error");
22277
22225
  }
22278
22226
  }
22279
- setMode("browse");
22227
+ closeOverlay();
22280
22228
  return;
22281
22229
  }
22282
22230
  const num = Number.parseInt(input, 10);
@@ -22287,26 +22235,30 @@ function useKeyboardNavigation({
22287
22235
  }
22288
22236
  if (state.mode === "add") {
22289
22237
  if (key.escape) {
22290
- setMode("browse", { newTaskTitle: "" });
22238
+ setMode("browse", { newTaskTitle: "", addThenEdit: false });
22291
22239
  return;
22292
22240
  }
22293
22241
  if (key.return) {
22294
22242
  const title = state.newTaskTitle.trim();
22295
- if (title) {
22296
- const column = allColumns[state.activeColumnIndex];
22297
- if (column) {
22298
- const result = addTaskAction(filePath, column.id, { title });
22299
- if (result.success) {
22300
- showStatus(result.message || "Task added", "success");
22301
- loadBrainfile(true);
22302
- } else {
22303
- showStatus(result.error || "Failed to add task", "error");
22243
+ if (!title) {
22244
+ showStatus(STATUS.titleRequired, "error");
22245
+ return;
22246
+ }
22247
+ const column = allColumns[state.activeColumnIndex];
22248
+ if (column) {
22249
+ const result = addTaskAction(filePath, column.id, { title });
22250
+ if (result.success) {
22251
+ showStatus(result.message || STATUS.addedGeneric, "success");
22252
+ loadBrainfile(true);
22253
+ if (state.addThenEdit) {
22254
+ const newId = result.message?.replace(/^Added\s+/, "").trim();
22255
+ if (newId) openInEditor(newId);
22304
22256
  }
22257
+ } else {
22258
+ showStatus(result.error || STATUS.addFailed, "error");
22305
22259
  }
22306
- } else {
22307
- showStatus("Title required", "error");
22308
22260
  }
22309
- setMode("browse", { newTaskTitle: "" });
22261
+ setMode("browse", { newTaskTitle: "", addThenEdit: false });
22310
22262
  return;
22311
22263
  }
22312
22264
  if (key.backspace || key.delete) {
@@ -22345,201 +22297,117 @@ function useKeyboardNavigation({
22345
22297
  return;
22346
22298
  }
22347
22299
  if (state.mode === "detail") {
22348
- const subtasks = currentTask?.subtasks ?? [];
22349
- if (key.escape || input === "q") {
22350
- if (input === "q") {
22351
- exit();
22352
- return;
22300
+ const stops = detailTask ? buildDetailStops(detailTask, detailChildren) : [];
22301
+ const cursor = clampDetailCursor(state.detailCursor, stops.length);
22302
+ const stop = stops[cursor];
22303
+ const layout = detailTask ? computeDetailLayout(detailTask, detailWidth, detailHeight, detailChildren, detailActivity) : null;
22304
+ if (input === "q") {
22305
+ exit();
22306
+ return;
22307
+ }
22308
+ if (key.escape) {
22309
+ if (state.detailPath.length > 1) {
22310
+ setState((prev) => ({
22311
+ ...prev,
22312
+ detailPath: prev.detailPath.slice(0, -1),
22313
+ detailCursor: 0,
22314
+ detailScroll: 0
22315
+ }));
22316
+ } else {
22317
+ setMode("browse", { detailPath: [] });
22353
22318
  }
22354
- setMode("browse");
22355
22319
  return;
22356
22320
  }
22357
22321
  if (input === "?") {
22358
- setMode("help");
22322
+ openHelp();
22359
22323
  return;
22360
22324
  }
22361
- if (input === " ") {
22362
- const subtask = subtasks[state.selectedSubtaskIndex];
22363
- if (currentTask && subtask) {
22364
- const result = toggleSubtaskAction(filePath, currentTask.id, subtask.id);
22365
- if (result.success) {
22366
- showStatus(`Toggled ${subtask.id}`, "success");
22367
- loadBrainfile(true);
22368
- } else {
22369
- showStatus(result.error || "Failed to toggle", "error");
22370
- }
22371
- } else {
22372
- showStatus("No subtasks", "info");
22325
+ if (input === "d") {
22326
+ if (layout) {
22327
+ const maxScroll = Math.max(0, layout.bodyLines.length - layout.bodyBudget);
22328
+ const step = Math.max(1, Math.floor(layout.bodyBudget / 2));
22329
+ setState((prev) => ({
22330
+ ...prev,
22331
+ detailScroll: Math.min(maxScroll, prev.detailScroll + step)
22332
+ }));
22333
+ }
22334
+ return;
22335
+ }
22336
+ if (input === "u") {
22337
+ if (layout) {
22338
+ const step = Math.max(1, Math.floor(layout.bodyBudget / 2));
22339
+ setState((prev) => ({ ...prev, detailScroll: Math.max(0, prev.detailScroll - step) }));
22373
22340
  }
22374
22341
  return;
22375
22342
  }
22376
22343
  if (key.downArrow || input === "j") {
22377
22344
  setState((prev) => ({
22378
22345
  ...prev,
22379
- selectedSubtaskIndex: Math.min(prev.selectedSubtaskIndex + 1, Math.max(0, subtasks.length - 1))
22346
+ detailCursor: Math.min(clampDetailCursor(prev.detailCursor, stops.length) + 1, Math.max(0, stops.length - 1))
22380
22347
  }));
22381
22348
  return;
22382
22349
  }
22383
22350
  if (key.upArrow || input === "k") {
22384
22351
  setState((prev) => ({
22385
22352
  ...prev,
22386
- selectedSubtaskIndex: Math.max(0, prev.selectedSubtaskIndex - 1)
22353
+ detailCursor: Math.max(0, clampDetailCursor(prev.detailCursor, stops.length) - 1)
22387
22354
  }));
22388
22355
  return;
22389
22356
  }
22390
- if (input === "m" && currentTask) {
22391
- setState((prev) => ({ ...prev, mode: "move", moveTargetIndex: prev.activeColumnIndex }));
22392
- return;
22393
- }
22394
- if (input === "c" && currentTask && isCompletable(currentTask)) {
22395
- completeTask(currentTask, false);
22396
- return;
22397
- }
22398
- if (input === "e" && currentTask) {
22399
- const result = editTaskInEditor(filePath, currentTask.id);
22400
- if (result.success) {
22401
- showStatus(result.message || "Task updated", "success");
22402
- loadBrainfile(true);
22403
- } else {
22404
- showStatus(result.error || "Edit failed", "error");
22405
- }
22406
- return;
22407
- }
22408
- if (input === "p" && currentTask) {
22409
- const result = cyclePriorityAction(filePath, currentTask.id);
22410
- if (result.success) {
22411
- showStatus(result.message || "Priority updated", "success");
22412
- loadBrainfile(true);
22413
- } else {
22414
- showStatus(result.error || "Failed to update priority", "error");
22357
+ if (key.return) {
22358
+ if (stop?.kind === "child") {
22359
+ setState((prev) => ({
22360
+ ...prev,
22361
+ detailPath: [...prev.detailPath, stop.task.id],
22362
+ detailCursor: 0,
22363
+ detailScroll: 0
22364
+ }));
22415
22365
  }
22416
- }
22417
- return;
22418
- }
22419
- if (state.mode === "rule-add" || state.mode === "rule-edit") {
22420
- if (key.escape) {
22421
- setMode("browse", { ruleEditText: "", ruleEditId: null });
22422
22366
  return;
22423
22367
  }
22424
- if (key.return) {
22425
- const text = state.ruleEditText.trim();
22426
- if (!text) {
22427
- showStatus("Rule text required", "error");
22368
+ if (input === " ") {
22369
+ if (detailArchived) {
22370
+ showStatus(STATUS.readOnly, "info");
22428
22371
  return;
22429
22372
  }
22430
- const result = state.mode === "rule-add" ? addRuleAction(filePath, state.activeRuleType, text) : state.ruleEditId !== null ? updateRuleAction(filePath, state.activeRuleType, state.ruleEditId, text) : { success: false, error: "No rule selected" };
22431
- if (result.success) {
22432
- showStatus(result.message || "Rule saved", "success");
22433
- loadBrainfile(true);
22434
- } else {
22435
- showStatus(result.error || "Failed to save rule", "error");
22436
- }
22437
- setMode("browse", { ruleEditText: "", ruleEditId: null });
22438
- return;
22439
- }
22440
- if (key.backspace || key.delete) {
22441
- setState((prev) => ({ ...prev, ruleEditText: prev.ruleEditText.slice(0, -1) }));
22442
- return;
22443
- }
22444
- if (input && !key.ctrl && !key.meta) {
22445
- setState((prev) => ({ ...prev, ruleEditText: prev.ruleEditText + input }));
22446
- }
22447
- return;
22448
- }
22449
- if (state.mode === "rule-delete-confirm") {
22450
- const rules = state.board?.rules?.[state.activeRuleType] || [];
22451
- const rule = rules[state.selectedRuleIndex];
22452
- if (input === "y" || input === "Y") {
22453
- if (rule) {
22454
- const result = deleteRuleAction(filePath, state.activeRuleType, rule.id);
22373
+ if (stop?.kind === "subtask" && detailTask) {
22374
+ const result = toggleSubtaskAction(filePath, detailTask.id, stop.subtask.id);
22455
22375
  if (result.success) {
22456
- showStatus(result.message || "Rule deleted", "success");
22376
+ showStatus(STATUS.toggled(stop.subtask.id), "success");
22457
22377
  loadBrainfile(true);
22458
- setState((prev) => ({
22459
- ...prev,
22460
- mode: "browse",
22461
- selectedRuleIndex: Math.max(0, prev.selectedRuleIndex - 1)
22462
- }));
22463
- return;
22378
+ } else {
22379
+ showStatus(result.error || STATUS.toggleFailed, "error");
22464
22380
  }
22465
- showStatus(result.error || "Failed to delete rule", "error");
22381
+ } else {
22382
+ showStatus(STATUS.noSubtasks, "info");
22466
22383
  }
22467
- setMode("browse");
22468
22384
  return;
22469
22385
  }
22470
- if (input === "n" || input === "N" || key.escape) {
22471
- setMode("browse");
22472
- showStatus("Delete cancelled", "info");
22473
- }
22474
- return;
22475
- }
22476
- if (state.mode === "logs-restore") {
22477
- if (key.escape) {
22478
- setMode("browse");
22386
+ if (input === "p") {
22387
+ if (!detailTask?.parentId || !detailParent) {
22388
+ showStatus(STATUS.noParent, "info");
22389
+ return;
22390
+ }
22391
+ const parentId = detailParent.id;
22392
+ setState((prev) => {
22393
+ const stack = prev.detailPath;
22394
+ const nextPath = stack.length > 1 && stack[stack.length - 2] === parentId ? stack.slice(0, -1) : [...stack.slice(0, -1), parentId];
22395
+ return { ...prev, detailPath: nextPath, detailCursor: 0, detailScroll: 0 };
22396
+ });
22479
22397
  return;
22480
22398
  }
22481
- if (key.downArrow || input === "j") {
22482
- setState((prev) => ({
22483
- ...prev,
22484
- logRestoreColumnIndex: Math.min(prev.logRestoreColumnIndex + 1, allColumns.length - 1)
22485
- }));
22399
+ if (input === "m" && detailTask && !detailArchived) {
22400
+ setState((prev) => ({ ...prev, mode: "move", moveTargetIndex: prev.activeColumnIndex }));
22486
22401
  return;
22487
22402
  }
22488
- if (key.upArrow || input === "k") {
22489
- setState((prev) => ({
22490
- ...prev,
22491
- logRestoreColumnIndex: Math.max(prev.logRestoreColumnIndex - 1, 0)
22492
- }));
22403
+ if (input === "c" && detailTask && !detailArchived && isCompletable(detailTask)) {
22404
+ completeTask(detailTask, false);
22493
22405
  return;
22494
22406
  }
22495
- if (key.return) {
22496
- const task = state.logs[state.selectedLogIndex];
22497
- const column = allColumns[state.logRestoreColumnIndex];
22498
- if (task && column) {
22499
- const result = restoreTaskAction(filePath, task.id, column.id);
22500
- if (result.success) {
22501
- showStatus(result.message || "Task restored", "success");
22502
- loadBrainfile(true);
22503
- const logResult = loadLogs(filePath);
22504
- setState((prev) => ({
22505
- ...prev,
22506
- mode: "browse",
22507
- logs: logResult.logs,
22508
- selectedLogIndex: Math.max(0, prev.selectedLogIndex - 1)
22509
- }));
22510
- return;
22511
- }
22512
- showStatus(result.error || "Failed to restore task", "error");
22513
- }
22514
- setMode("browse");
22515
- }
22516
- return;
22517
- }
22518
- if (state.mode === "logs-delete-confirm") {
22519
- const task = state.logs[state.selectedLogIndex];
22520
- if (input === "y" || input === "Y") {
22521
- if (task) {
22522
- const result = deleteArchivedTaskAction(filePath, task.id);
22523
- if (result.success) {
22524
- showStatus(result.message || "Task permanently deleted", "success");
22525
- const logResult = loadLogs(filePath);
22526
- setState((prev) => ({
22527
- ...prev,
22528
- mode: "browse",
22529
- logs: logResult.logs,
22530
- selectedLogIndex: Math.max(0, prev.selectedLogIndex - 1)
22531
- }));
22532
- return;
22533
- }
22534
- showStatus(result.error || "Failed to delete task", "error");
22535
- }
22536
- setMode("browse");
22407
+ if (input === "e" && detailTask) {
22408
+ openInEditor(detailTask.id);
22537
22409
  return;
22538
22410
  }
22539
- if (input === "n" || input === "N" || key.escape) {
22540
- setMode("browse");
22541
- showStatus("Delete cancelled", "info");
22542
- }
22543
22411
  return;
22544
22412
  }
22545
22413
  if (input === "q" || key.ctrl && input === "c") {
@@ -22547,317 +22415,238 @@ function useKeyboardNavigation({
22547
22415
  return;
22548
22416
  }
22549
22417
  if (input === "?") {
22550
- setMode("help");
22418
+ openHelp();
22551
22419
  return;
22552
22420
  }
22553
- if (input === "1") {
22554
- setState((prev) => ({ ...prev, activePanel: "board", mode: "browse" }));
22421
+ if (input === "/") {
22422
+ setMode("filter", { filterQuery: "", selectedTaskIndex: 0 });
22555
22423
  return;
22556
22424
  }
22557
- if (input === "2") {
22558
- setState((prev) => ({ ...prev, activePanel: "rules", mode: "browse" }));
22425
+ if (input === "L") {
22426
+ setState((prev) => ({ ...prev, doneView: !prev.doneView, selectedTaskIndex: 0 }));
22559
22427
  return;
22560
22428
  }
22561
- if (input === "3") {
22562
- setState((prev) => ({ ...prev, activePanel: "logs", mode: "browse" }));
22429
+ if (input === "t") {
22430
+ if (typeCycleOptions.length > 1) {
22431
+ const idx = typeCycleOptions.indexOf(state.activeTypeFilter);
22432
+ const next = typeCycleOptions[(idx < 0 ? 0 : idx + 1) % typeCycleOptions.length];
22433
+ patchTuiState(filePath, { lastTypeFilter: next });
22434
+ setState((prev) => ({ ...prev, activeTypeFilter: next, selectedTaskIndex: 0 }));
22435
+ }
22563
22436
  return;
22564
22437
  }
22565
- if (state.activePanel === "board") {
22566
- if (input === "/") {
22567
- setMode("filter", { filterQuery: "", selectedTaskIndex: 0 });
22568
- return;
22569
- }
22570
- if (input === "r") {
22571
- loadBrainfile(true);
22572
- showStatus("Reloaded", "info");
22573
- return;
22574
- }
22575
- if (key.return) {
22576
- if (currentTask) setMode("detail", { selectedSubtaskIndex: 0 });
22577
- else showStatus("No document selected", "info");
22578
- return;
22579
- }
22580
- if (input === "a" || input === "n") {
22581
- setMode("add", { newTaskTitle: "" });
22582
- return;
22583
- }
22584
- if (input === "m") {
22585
- if (!currentTask) {
22586
- showStatus("No document selected", "error");
22587
- return;
22588
- }
22589
- setState((prev) => ({ ...prev, mode: "move", moveTargetIndex: prev.activeColumnIndex }));
22590
- return;
22591
- }
22592
- if (input === "c") {
22593
- if (!currentTask) {
22594
- showStatus("No document selected", "error");
22595
- return;
22596
- }
22597
- if (!isCompletable(currentTask)) {
22598
- showStatus(`${currentTask.id} cannot be completed`, "info");
22599
- return;
22600
- }
22601
- completeTask(currentTask, false);
22602
- return;
22603
- }
22604
- if (input === "d") {
22605
- if (!currentTask) {
22606
- showStatus("No document selected", "error");
22607
- return;
22608
- }
22609
- setMode("delete-confirm");
22610
- return;
22611
- }
22612
- if (input === "e") {
22613
- if (!currentTask) {
22614
- showStatus("No document selected", "error");
22615
- return;
22616
- }
22617
- const result = editTaskInEditor(filePath, currentTask.id);
22618
- if (result.success) {
22619
- showStatus(result.message || "Task updated", "success");
22620
- loadBrainfile(true);
22621
- } else {
22622
- showStatus(result.error || "Edit failed", "error");
22623
- }
22624
- return;
22625
- }
22626
- if (input === "p") {
22627
- if (!currentTask) return;
22628
- const result = cyclePriorityAction(filePath, currentTask.id);
22629
- if (result.success) {
22630
- showStatus(result.message || "Priority updated", "success");
22631
- loadBrainfile(true);
22632
- } else {
22633
- showStatus(result.error || "Failed to update priority", "error");
22634
- }
22635
- return;
22636
- }
22637
- if (input === "y") {
22638
- if (!currentTask) return;
22639
- const result = copyToClipboard(currentTask.id);
22640
- showStatus(
22641
- result.success ? `Copied ${currentTask.id}` : result.error || "Copy failed",
22642
- result.success ? "success" : "error"
22643
- );
22644
- return;
22645
- }
22646
- if (input === "N") {
22647
- const column = allColumns[state.activeColumnIndex];
22648
- if (column) {
22649
- const result = newTaskInEditor(filePath, column.id);
22650
- if (result.success) {
22651
- showStatus(result.message || "Task created", "success");
22652
- loadBrainfile(true);
22653
- } else {
22654
- showStatus(result.error || "Failed to create task", "error");
22655
- }
22656
- }
22657
- return;
22658
- }
22659
- if (input === "A") {
22660
- if (!currentTask) return;
22661
- showStatus("Moving to logs...", "info");
22662
- archiveTaskActionAsync(filePath, currentTask.id).then((result) => {
22663
- showStatus(
22664
- result.success ? result.message || "Task moved to logs" : result.error || "Move to logs failed",
22665
- result.success ? "success" : "error"
22666
- );
22667
- if (result.success) loadBrainfile(true);
22668
- }).catch((err) => showStatus(`Move to logs failed: ${err}`, "error"));
22669
- return;
22670
- }
22671
- if (key.downArrow || input === "j") {
22672
- setState((prev) => ({
22673
- ...prev,
22674
- selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxRowIndex)
22675
- }));
22676
- return;
22677
- }
22678
- if (key.upArrow || input === "k") {
22679
- setState((prev) => ({
22680
- ...prev,
22681
- selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0)
22682
- }));
22683
- return;
22684
- }
22685
- if (key.ctrl && input === "d") {
22686
- setState((prev) => ({
22687
- ...prev,
22688
- selectedTaskIndex: Math.min(
22689
- prev.selectedTaskIndex + Math.floor(viewportHeight / 2),
22690
- maxRowIndex
22691
- )
22692
- }));
22693
- return;
22694
- }
22695
- if (key.ctrl && input === "u") {
22696
- setState((prev) => ({
22697
- ...prev,
22698
- selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0)
22699
- }));
22700
- return;
22701
- }
22702
- if (input === "g") {
22703
- setState((prev) => ({ ...prev, selectedTaskIndex: 0 }));
22704
- return;
22705
- }
22706
- if (input === "G") {
22707
- setState((prev) => ({ ...prev, selectedTaskIndex: maxRowIndex }));
22708
- return;
22709
- }
22710
- if (key.tab || key.rightArrow || input === "l" || input === "]") {
22711
- if (filteredColumnsLength > 0) {
22712
- setState((prev) => ({
22713
- ...prev,
22714
- activeColumnIndex: (prev.activeColumnIndex + 1) % filteredColumnsLength,
22715
- selectedTaskIndex: 0
22716
- }));
22717
- }
22718
- return;
22719
- }
22720
- if (key.shift && key.tab || key.leftArrow || input === "h" || input === "[") {
22721
- if (filteredColumnsLength > 0) {
22722
- setState((prev) => ({
22723
- ...prev,
22724
- activeColumnIndex: prev.activeColumnIndex === 0 ? filteredColumnsLength - 1 : prev.activeColumnIndex - 1,
22725
- selectedTaskIndex: 0
22726
- }));
22727
- }
22728
- return;
22729
- }
22730
- if (key.escape) {
22731
- setState((prev) => ({ ...prev, filterQuery: "", selectedTaskIndex: 0 }));
22438
+ if (input === " ") {
22439
+ if (doneView) return;
22440
+ const row = rows[state.selectedTaskIndex];
22441
+ if (row && (row.childCount ?? 0) > 0) {
22442
+ const id = row.task.id;
22443
+ setState((prev) => {
22444
+ const next = new Set(prev.collapsedIds);
22445
+ if (next.has(id)) next.delete(id);
22446
+ else next.add(id);
22447
+ patchTuiState(filePath, { collapsed: Array.from(next) });
22448
+ return { ...prev, collapsedIds: next };
22449
+ });
22732
22450
  }
22733
22451
  return;
22734
22452
  }
22735
- if (state.activePanel === "rules") {
22736
- const rules = state.board?.rules?.[state.activeRuleType] || [];
22737
- const maxRuleIndex = Math.max(0, rules.length - 1);
22738
- if (key.leftArrow || input === "h" || key.shift && key.tab) {
22739
- const idx = RULE_TYPES.indexOf(state.activeRuleType);
22740
- setState((prev) => ({
22741
- ...prev,
22742
- activeRuleType: RULE_TYPES[idx === 0 ? RULE_TYPES.length - 1 : idx - 1],
22743
- selectedRuleIndex: 0
22744
- }));
22745
- return;
22746
- }
22747
- if (key.tab || key.rightArrow || input === "l") {
22748
- const idx = RULE_TYPES.indexOf(state.activeRuleType);
22749
- setState((prev) => ({
22750
- ...prev,
22751
- activeRuleType: RULE_TYPES[(idx + 1) % RULE_TYPES.length],
22752
- selectedRuleIndex: 0
22753
- }));
22754
- return;
22453
+ if (input === "r") {
22454
+ loadBrainfile(true);
22455
+ showStatus(STATUS.reloaded, "info");
22456
+ return;
22457
+ }
22458
+ if (key.return) {
22459
+ if (currentTask) {
22460
+ setMode("detail", { detailPath: [currentTask.id], detailCursor: 0, detailScroll: 0 });
22461
+ } else {
22462
+ showStatus(STATUS.noDocument, "info");
22755
22463
  }
22756
- if (key.downArrow || input === "j") {
22757
- setState((prev) => ({
22758
- ...prev,
22759
- selectedRuleIndex: Math.min(prev.selectedRuleIndex + 1, maxRuleIndex)
22760
- }));
22464
+ return;
22465
+ }
22466
+ if (input === "a" || input === "n") {
22467
+ if (doneView) {
22468
+ showStatus(STATUS.readOnly, "info");
22761
22469
  return;
22762
22470
  }
22763
- if (key.upArrow || input === "k") {
22764
- setState((prev) => ({
22765
- ...prev,
22766
- selectedRuleIndex: Math.max(prev.selectedRuleIndex - 1, 0)
22767
- }));
22471
+ setMode("add", { newTaskTitle: "", addThenEdit: false });
22472
+ return;
22473
+ }
22474
+ if (input === "m") {
22475
+ if (doneView) {
22476
+ showStatus(STATUS.readOnly, "info");
22768
22477
  return;
22769
22478
  }
22770
- if (input === "a" || input === "n") {
22771
- setMode("rule-add", { ruleEditText: "", ruleEditId: null });
22479
+ if (!currentTask) {
22480
+ showStatus(STATUS.noDocument, "error");
22772
22481
  return;
22773
22482
  }
22774
- if (input === "e") {
22775
- const rule = rules[state.selectedRuleIndex];
22776
- if (rule) setMode("rule-edit", { ruleEditText: rule.rule, ruleEditId: rule.id });
22777
- else showStatus("No rule selected", "error");
22483
+ setState((prev) => ({ ...prev, mode: "move", moveTargetIndex: prev.activeColumnIndex }));
22484
+ return;
22485
+ }
22486
+ if (input === "c") {
22487
+ if (doneView) {
22488
+ showStatus(STATUS.alreadyCompleted, "info");
22778
22489
  return;
22779
22490
  }
22780
- if (input === "d") {
22781
- if (rules[state.selectedRuleIndex]) setMode("rule-delete-confirm");
22782
- else showStatus("No rule selected", "error");
22491
+ if (!currentTask) {
22492
+ showStatus(STATUS.noDocument, "error");
22783
22493
  return;
22784
22494
  }
22785
- if (input === "g") {
22786
- setState((prev) => ({ ...prev, selectedRuleIndex: 0 }));
22495
+ if (!isCompletable(currentTask)) {
22496
+ showStatus(STATUS.cannotComplete(currentTask.id), "info");
22787
22497
  return;
22788
22498
  }
22789
- if (input === "G") {
22790
- setState((prev) => ({ ...prev, selectedRuleIndex: maxRuleIndex }));
22791
- }
22499
+ completeTask(currentTask, false);
22792
22500
  return;
22793
- }
22794
- if (state.activePanel === "logs") {
22795
- const maxLogIndex = Math.max(0, state.logs.length - 1);
22796
- if (input === "r") {
22797
- const result = loadLogs(filePath);
22798
- setState((prev) => ({ ...prev, logs: result.logs }));
22799
- showStatus("Logs refreshed", "info");
22800
- return;
22801
- }
22802
- if (key.downArrow || input === "j") {
22803
- setState((prev) => ({
22804
- ...prev,
22805
- selectedLogIndex: Math.min(prev.selectedLogIndex + 1, maxLogIndex)
22806
- }));
22501
+ }
22502
+ if (input === "d") {
22503
+ if (doneView) {
22504
+ showStatus(STATUS.readOnly, "info");
22807
22505
  return;
22808
22506
  }
22809
- if (key.upArrow || input === "k") {
22810
- setState((prev) => ({
22811
- ...prev,
22812
- selectedLogIndex: Math.max(prev.selectedLogIndex - 1, 0)
22813
- }));
22507
+ if (!currentTask) {
22508
+ showStatus(STATUS.noDocument, "error");
22814
22509
  return;
22815
22510
  }
22816
- if (key.return) {
22817
- const task = state.logs[state.selectedLogIndex];
22818
- if (task) {
22819
- setState((prev) => {
22820
- const expanded = new Set(prev.expandedLogIds);
22821
- if (expanded.has(task.id)) expanded.delete(task.id);
22822
- else expanded.add(task.id);
22823
- return { ...prev, expandedLogIds: expanded };
22824
- });
22825
- }
22511
+ setMode("delete-confirm");
22512
+ return;
22513
+ }
22514
+ if (input === "e") {
22515
+ if (!currentTask) {
22516
+ showStatus(STATUS.noDocument, "error");
22826
22517
  return;
22827
22518
  }
22828
- if (input === "R") {
22829
- if (state.logs.length === 0) {
22830
- showStatus("No logged tasks", "error");
22831
- return;
22832
- }
22833
- setMode("logs-restore", { logRestoreColumnIndex: 0 });
22519
+ openInEditor(currentTask.id);
22520
+ return;
22521
+ }
22522
+ if (input === "p") {
22523
+ if (!currentTask) return;
22524
+ if (doneView) {
22525
+ showStatus(STATUS.readOnly, "info");
22834
22526
  return;
22835
22527
  }
22836
- if (input === "d") {
22837
- if (state.logs.length === 0) {
22838
- showStatus("No logged tasks", "error");
22839
- return;
22840
- }
22841
- setMode("logs-delete-confirm");
22842
- return;
22528
+ const result = cyclePriorityAction(filePath, currentTask.id);
22529
+ if (result.success) {
22530
+ showStatus(result.message || STATUS.priorityUpdated("updated"), "success");
22531
+ loadBrainfile(true);
22532
+ } else {
22533
+ showStatus(result.error || STATUS.priorityFailed, "error");
22843
22534
  }
22844
- if (input === "g") {
22845
- setState((prev) => ({ ...prev, selectedLogIndex: 0 }));
22535
+ return;
22536
+ }
22537
+ if (input === "y") {
22538
+ if (!currentTask) return;
22539
+ const result = copyToClipboard(currentTask.id);
22540
+ showStatus(
22541
+ result.success ? STATUS.copied(currentTask.id) : result.error || STATUS.copyFailed,
22542
+ result.success ? "success" : "error"
22543
+ );
22544
+ return;
22545
+ }
22546
+ if (input === "N") {
22547
+ if (doneView) {
22548
+ showStatus(STATUS.readOnly, "info");
22846
22549
  return;
22847
22550
  }
22848
- if (input === "G") {
22849
- setState((prev) => ({ ...prev, selectedLogIndex: maxLogIndex }));
22551
+ setMode("add", { newTaskTitle: "", addThenEdit: true });
22552
+ return;
22553
+ }
22554
+ if (input === "A") {
22555
+ if (!currentTask) return;
22556
+ if (doneView) {
22557
+ showStatus(STATUS.readOnly, "info");
22850
22558
  return;
22851
22559
  }
22852
- if (key.escape) {
22853
- setState((prev) => ({ ...prev, expandedLogIds: /* @__PURE__ */ new Set() }));
22560
+ showStatus(STATUS.archiving, "info");
22561
+ archiveTaskActionAsync(filePath, currentTask.id).then((result) => {
22562
+ showStatus(
22563
+ result.success ? result.message || STATUS.archived(currentTask.id) : result.error || STATUS.archiveFailed,
22564
+ result.success ? "success" : "error"
22565
+ );
22566
+ if (result.success) loadBrainfile(true);
22567
+ }).catch((err) => showStatus(`${STATUS.archiveFailed}: ${err}`, "error"));
22568
+ return;
22569
+ }
22570
+ if (key.downArrow || input === "j") {
22571
+ setState((prev) => ({
22572
+ ...prev,
22573
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxRowIndex)
22574
+ }));
22575
+ return;
22576
+ }
22577
+ if (key.upArrow || input === "k") {
22578
+ setState((prev) => ({
22579
+ ...prev,
22580
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0)
22581
+ }));
22582
+ return;
22583
+ }
22584
+ if (key.ctrl && input === "d") {
22585
+ setState((prev) => ({
22586
+ ...prev,
22587
+ selectedTaskIndex: Math.min(
22588
+ prev.selectedTaskIndex + Math.floor(viewportHeight / 2),
22589
+ maxRowIndex
22590
+ )
22591
+ }));
22592
+ return;
22593
+ }
22594
+ if (key.ctrl && input === "u") {
22595
+ setState((prev) => ({
22596
+ ...prev,
22597
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0)
22598
+ }));
22599
+ return;
22600
+ }
22601
+ if (input === "g") {
22602
+ setState((prev) => ({ ...prev, selectedTaskIndex: 0 }));
22603
+ return;
22604
+ }
22605
+ if (input === "G") {
22606
+ setState((prev) => ({ ...prev, selectedTaskIndex: maxRowIndex }));
22607
+ return;
22608
+ }
22609
+ if (key.tab || key.rightArrow || input === "l") {
22610
+ if (!doneView && filteredColumnsLength > 0) {
22611
+ setState((prev) => {
22612
+ const nextIndex = (prev.activeColumnIndex + 1) % filteredColumnsLength;
22613
+ rememberColumn(nextIndex);
22614
+ return { ...prev, activeColumnIndex: nextIndex, selectedTaskIndex: 0 };
22615
+ });
22616
+ }
22617
+ return;
22618
+ }
22619
+ if (key.shift && key.tab || key.leftArrow || input === "h") {
22620
+ if (!doneView && filteredColumnsLength > 0) {
22621
+ setState((prev) => {
22622
+ const nextIndex = prev.activeColumnIndex === 0 ? filteredColumnsLength - 1 : prev.activeColumnIndex - 1;
22623
+ rememberColumn(nextIndex);
22624
+ return { ...prev, activeColumnIndex: nextIndex, selectedTaskIndex: 0 };
22625
+ });
22854
22626
  }
22627
+ return;
22855
22628
  }
22629
+ if (key.escape) {
22630
+ setState((prev) => ({ ...prev, filterQuery: "", selectedTaskIndex: 0 }));
22631
+ }
22632
+ return;
22856
22633
  });
22857
22634
  }
22858
22635
 
22636
+ // src/tui/typeCycle.ts
22637
+ var FIXED_TYPE_ORDER = ["task", "epic", "spec", "plan", "adr"];
22638
+ function getTypeCycleOptions(board) {
22639
+ const configured = Object.keys(
22640
+ board?.types ?? {}
22641
+ );
22642
+ const configuredSet = new Set(configured);
22643
+ const known = FIXED_TYPE_ORDER.filter((type) => type === "task" || configuredSet.has(type));
22644
+ const extra = configured.filter((type) => !FIXED_TYPE_ORDER.includes(type)).sort();
22645
+ return ["all", ...known, ...extra];
22646
+ }
22647
+
22859
22648
  // src/tui/components/HeaderBar.tsx
22860
- var import_react36 = __toESM(require_react());
22649
+ var import_react37 = __toESM(require_react());
22861
22650
  function HeaderBar({
22862
22651
  title,
22863
22652
  columns,
@@ -22867,30 +22656,38 @@ function HeaderBar({
22867
22656
  filterActive,
22868
22657
  matchCount,
22869
22658
  totalCount,
22870
- panelLabel
22659
+ panelLabel,
22660
+ activeType
22871
22661
  }) {
22872
- const affordances = "/ filter ? help";
22662
+ const affordances = CHROME.filterAffordance;
22873
22663
  const boardTitle = truncate(title, 24);
22874
22664
  const separator = ` ${GLYPHS.pointer} `;
22875
- const tabsWidth = panelLabel ? panelLabel.length : columns.reduce((sum, column, index) => {
22665
+ const typeLabel = activeType && activeType !== "all" ? ` \xB7 ${activeType}` : "";
22666
+ const tabsWidth = (panelLabel ? panelLabel.length : columns.reduce((sum, column, index) => {
22876
22667
  const active = index === activeColumnIndex;
22877
22668
  return sum + (index > 0 ? 3 : 0) + `${column.title} ${column.tasks?.length ?? 0}`.length + (active ? 1 : 0);
22878
- }, 0);
22669
+ }, 0)) + typeLabel.length;
22879
22670
  const used = boardTitle.length + separator.length + tabsWidth;
22880
22671
  const gap = Math.max(1, width - 1 - used - affordances.length - 1);
22881
- return /* @__PURE__ */ import_react36.default.createElement(Box_default, { flexDirection: "column" }, /* @__PURE__ */ import_react36.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react36.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.text, bold: true }, boardTitle), /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.textDim }, separator), panelLabel ? /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.text }, panelLabel) : columns.map((column, index) => {
22882
- const active = index === activeColumnIndex;
22883
- const count = column.tasks?.length ?? 0;
22884
- return /* @__PURE__ */ import_react36.default.createElement(Text, { key: column.id }, index > 0 ? /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.textDim }, " \u2502 ") : null, /* @__PURE__ */ import_react36.default.createElement(Text, { color: active ? PALETTE.text : PALETTE.textMuted, bold: active }, `${column.title} ${count}`), active ? /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.text, bold: true }, "*") : null);
22885
- }), /* @__PURE__ */ import_react36.default.createElement(Text, null, pad(gap)), /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.textMuted }, affordances))), filterActive ? /* @__PURE__ */ import_react36.default.createElement(
22886
- FilterLine,
22887
- {
22888
- query: filterQuery,
22889
- width,
22890
- matchCount,
22891
- totalCount
22892
- }
22893
- ) : /* @__PURE__ */ import_react36.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(Math.max(1, width - 2)))));
22672
+ return (
22673
+ // flexShrink={0}: a detail v2 pane can legitimately render taller than the
22674
+ // nominal viewport (§B1's many optional sections). Without this, yoga
22675
+ // shrinks *this* sibling instead of letting the column overflow, and the
22676
+ // header silently loses rows rather than the frame simply growing.
22677
+ /* @__PURE__ */ import_react37.default.createElement(Box_default, { flexDirection: "column", flexShrink: 0 }, /* @__PURE__ */ import_react37.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react37.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.text, bold: true }, boardTitle), /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textDim }, separator), panelLabel ? /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.text }, panelLabel) : columns.map((column, index) => {
22678
+ const active = index === activeColumnIndex;
22679
+ const count = column.tasks?.length ?? 0;
22680
+ return /* @__PURE__ */ import_react37.default.createElement(Text, { key: column.id }, index > 0 ? /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textDim }, " \u2502 ") : null, /* @__PURE__ */ import_react37.default.createElement(Text, { color: active ? PALETTE.text : PALETTE.textMuted, bold: active }, `${column.title} ${count}`), active ? /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.text, bold: true }, "*") : null);
22681
+ }), typeLabel ? /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textDim }, typeLabel) : null, /* @__PURE__ */ import_react37.default.createElement(Text, null, pad(gap)), /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textMuted }, affordances))), filterActive ? /* @__PURE__ */ import_react37.default.createElement(
22682
+ FilterLine,
22683
+ {
22684
+ query: filterQuery,
22685
+ width,
22686
+ matchCount,
22687
+ totalCount
22688
+ }
22689
+ ) : /* @__PURE__ */ import_react37.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(Math.max(1, width - 2)))))
22690
+ );
22894
22691
  }
22895
22692
  function FilterLine({
22896
22693
  query,
@@ -22898,25 +22695,32 @@ function FilterLine({
22898
22695
  matchCount,
22899
22696
  totalCount
22900
22697
  }) {
22901
- const count = matchCount === void 0 || totalCount === void 0 ? "" : `${matchCount}/${totalCount} match "${query}"`;
22698
+ const count = matchCount === void 0 || totalCount === void 0 ? "" : matchCountLabel(matchCount, totalCount, query);
22902
22699
  const input = `/${query}`;
22903
22700
  const gap = Math.max(1, width - 2 - input.length - 1 - count.length);
22904
- return /* @__PURE__ */ import_react36.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react36.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.accent }, input), /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.accent }, GLYPHS.cursor), /* @__PURE__ */ import_react36.default.createElement(Text, null, pad(gap)), /* @__PURE__ */ import_react36.default.createElement(Text, { color: PALETTE.textMuted }, count || " ")));
22701
+ return /* @__PURE__ */ import_react37.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react37.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.accent }, input), /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.accent }, GLYPHS.cursor), /* @__PURE__ */ import_react37.default.createElement(Text, null, pad(gap)), /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textMuted }, count || " ")));
22905
22702
  }
22906
22703
 
22907
22704
  // src/tui/components/FooterBar.tsx
22908
- var import_react37 = __toESM(require_react());
22909
- function browseActions(selected) {
22705
+ var import_react38 = __toESM(require_react());
22706
+ function browseActions(selected, archived = false) {
22707
+ if (archived) return ["\u21B5 detail", "e edit", "t type", "L board", "q quit"];
22910
22708
  const actions = ["\u21B5 detail", "m move"];
22911
22709
  if (isCompletable(selected)) actions.push("c complete");
22912
- actions.push("a add", "tab column", "q quit");
22710
+ actions.push("a add", "t type", "L done", "tab column", "q quit");
22913
22711
  return actions;
22914
22712
  }
22915
- function detailActions(selected) {
22713
+ function detailActions(ctx) {
22916
22714
  const actions = [];
22917
- if (selected?.subtasks?.length) actions.push("space toggle subtask");
22918
- actions.push("e edit", "m move");
22919
- if (isCompletable(selected)) actions.push("c complete");
22715
+ if (ctx.hasChildren) actions.push("\u21B5 open child");
22716
+ if (ctx.hasSubtasks) actions.push("space toggle");
22717
+ if (ctx.bodyOverflows) actions.push("u/d scroll");
22718
+ if (ctx.hasParent) actions.push("p parent");
22719
+ actions.push("e edit");
22720
+ if (!ctx.archived) {
22721
+ actions.push("m move");
22722
+ if (isCompletable(ctx.task)) actions.push("c complete");
22723
+ }
22920
22724
  actions.push("esc back");
22921
22725
  return actions;
22922
22726
  }
@@ -22927,20 +22731,30 @@ function FooterBar({
22927
22731
  stateChip,
22928
22732
  showRule = true
22929
22733
  }) {
22930
- const left = itemCount === void 0 ? actions.join(" \xB7 ") : [`${itemCount} item${itemCount === 1 ? "" : "s"}`, ...actions].join(" \xB7 ");
22734
+ const quit = actions.includes("q quit") ? "q quit" : "";
22735
+ const shown = quit ? actions.filter((action) => action !== "q quit") : actions;
22736
+ const left = itemCount === void 0 ? shown.join(" \xB7 ") : [itemCountLabel(itemCount), ...shown].join(" \xB7 ");
22931
22737
  const chip = stateChip ?? "";
22932
- const available = Math.max(1, width - 2 - chip.length - 2);
22738
+ const right = [quit, chip].filter(Boolean).join(" ");
22739
+ const available = Math.max(1, width - 2 - right.length - 2);
22933
22740
  const text = truncate(left, available);
22934
- const gap = Math.max(1, width - 2 - text.length - chip.length);
22935
- return /* @__PURE__ */ import_react37.default.createElement(Box_default, { flexDirection: "column" }, showRule ? /* @__PURE__ */ import_react37.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(Math.max(1, width - 2)))) : null, /* @__PURE__ */ import_react37.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react37.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textMuted }, text), /* @__PURE__ */ import_react37.default.createElement(Text, null, pad(gap)), /* @__PURE__ */ import_react37.default.createElement(Text, { color: PALETTE.textSecondary }, chip || " "))));
22741
+ const gap = Math.max(1, width - 2 - text.length - right.length);
22742
+ return (
22743
+ // flexShrink={0}: see HeaderBar's matching comment — the footer must not
22744
+ // be the thing that silently loses rows when detail v2 overflows.
22745
+ /* @__PURE__ */ import_react38.default.createElement(Box_default, { flexDirection: "column", flexShrink: 0 }, showRule ? /* @__PURE__ */ import_react38.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react38.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(Math.max(1, width - 2)))) : null, /* @__PURE__ */ import_react38.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react38.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react38.default.createElement(Text, { color: PALETTE.textMuted }, text), /* @__PURE__ */ import_react38.default.createElement(Text, null, pad(gap)), quit ? /* @__PURE__ */ import_react38.default.createElement(Text, { color: PALETTE.textMuted }, quit) : null, quit && chip ? /* @__PURE__ */ import_react38.default.createElement(Text, null, pad(2)) : null, /* @__PURE__ */ import_react38.default.createElement(Text, { color: PALETTE.textSecondary }, chip || " "))))
22746
+ );
22936
22747
  }
22937
22748
 
22938
22749
  // src/tui/components/DocumentRow.tsx
22939
- var import_react38 = __toESM(require_react());
22750
+ var import_react39 = __toESM(require_react());
22940
22751
  var MAX_CHIPS = 3;
22941
22752
  function buildChips(row) {
22942
22753
  const chips = [];
22943
22754
  const { task } = row;
22755
+ if (row.collapsed && row.childCount) {
22756
+ chips.push({ text: `${row.childCount} hidden`, color: PALETTE.textMuted });
22757
+ }
22944
22758
  const contractState = getContractState(task);
22945
22759
  if (contractState) {
22946
22760
  chips.push({ text: contractState, color: getContractStateColor(contractState) });
@@ -22960,10 +22774,11 @@ function buildPriorityMarker(row) {
22960
22774
  if (priority !== "high" && priority !== "critical") return void 0;
22961
22775
  return { text: `${GLYPHS.warning} ${priority}`, color: getPriorityColor(priority) };
22962
22776
  }
22963
- function DocumentRow({ row, selected, width, idWidth }) {
22777
+ function DocumentRow({ row, selected, width, idWidth, archived = false }) {
22964
22778
  const { task, depth, orphanParentId } = row;
22965
- const glyph = getTypeGlyph(task.type);
22966
- const indent = `${pad(1)}${pad(depth * 2)}`;
22779
+ const hasChildren = Boolean(row.childCount);
22780
+ const glyph = hasChildren ? row.collapsed ? GLYPHS.parentCollapsed : GLYPHS.parentExpanded : getTypeGlyph(task.type);
22781
+ const indent = `${selected ? GLYPHS.cursor : " "}${pad(depth * 2)}`;
22967
22782
  const glyphCell = `${glyph || " "} `;
22968
22783
  const idCell = `${task.id.padEnd(idWidth)} `;
22969
22784
  const leftWidth = indent.length + glyphCell.length + idCell.length;
@@ -22979,205 +22794,107 @@ function DocumentRow({ row, selected, width, idWidth }) {
22979
22794
  const title = truncate(task.title, titleWidth);
22980
22795
  const gap = Math.max(1, width - leftWidth - title.length - rightWidth - 1);
22981
22796
  const dim = selected ? void 0 : PALETTE.textMuted;
22982
- return /* @__PURE__ */ import_react38.default.createElement(Text, { wrap: "truncate", inverse: selected }, /* @__PURE__ */ import_react38.default.createElement(Text, null, indent), /* @__PURE__ */ import_react38.default.createElement(Text, { color: selected ? void 0 : PALETTE.textSecondary }, glyphCell), /* @__PURE__ */ import_react38.default.createElement(Text, { color: dim }, idCell), /* @__PURE__ */ import_react38.default.createElement(Text, { color: selected ? void 0 : PALETTE.text }, title), /* @__PURE__ */ import_react38.default.createElement(Text, null, pad(gap)), rightSegments.map((seg, index) => /* @__PURE__ */ import_react38.default.createElement(Text, { key: `${seg.text}-${index}` }, index > 0 ? /* @__PURE__ */ import_react38.default.createElement(Text, null, pad(2)) : null, /* @__PURE__ */ import_react38.default.createElement(Text, { color: selected ? void 0 : seg.color }, seg.text))), /* @__PURE__ */ import_react38.default.createElement(Text, null, pad(1)));
22797
+ return /* @__PURE__ */ import_react39.default.createElement(Text, { wrap: "truncate", inverse: selected }, /* @__PURE__ */ import_react39.default.createElement(Text, null, indent), /* @__PURE__ */ import_react39.default.createElement(Text, { color: selected ? void 0 : PALETTE.textSecondary }, glyphCell), /* @__PURE__ */ import_react39.default.createElement(Text, { color: dim }, idCell), /* @__PURE__ */ import_react39.default.createElement(Text, { color: selected ? void 0 : archived ? PALETTE.textMuted : PALETTE.text }, title), /* @__PURE__ */ import_react39.default.createElement(Text, null, pad(gap)), rightSegments.map((seg, index) => /* @__PURE__ */ import_react39.default.createElement(Text, { key: `${seg.text}-${index}` }, index > 0 ? /* @__PURE__ */ import_react39.default.createElement(Text, null, pad(2)) : null, /* @__PURE__ */ import_react39.default.createElement(Text, { color: selected ? void 0 : seg.color }, seg.text))), /* @__PURE__ */ import_react39.default.createElement(Text, null, pad(1)));
22983
22798
  }
22984
22799
 
22985
22800
  // src/tui/components/DocumentList.tsx
22986
- var import_react39 = __toESM(require_react());
22801
+ var import_react40 = __toESM(require_react());
22987
22802
  function DocumentList({
22988
22803
  rows,
22989
22804
  selectedIndex,
22990
22805
  viewportHeight,
22991
22806
  width,
22992
- emptyMessage = "No documents"
22807
+ archived = false,
22808
+ emptyMessage = EMPTY.noDocuments
22993
22809
  }) {
22994
22810
  if (rows.length === 0) {
22995
- return /* @__PURE__ */ import_react39.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react39.default.createElement(Text, { color: PALETTE.textMuted }, emptyMessage));
22811
+ return /* @__PURE__ */ import_react40.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, emptyMessage));
22996
22812
  }
22997
22813
  const { visible, start } = windowRows(rows, selectedIndex, viewportHeight);
22998
22814
  const idWidth = Math.max(6, ...visible.map((row) => row.task.id.length));
22999
- return /* @__PURE__ */ import_react39.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1 }, visible.map((row, index) => /* @__PURE__ */ import_react39.default.createElement(
22815
+ return /* @__PURE__ */ import_react40.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1 }, visible.map((row, index) => /* @__PURE__ */ import_react40.default.createElement(
23000
22816
  DocumentRow,
23001
22817
  {
23002
22818
  key: row.task.id,
23003
22819
  row,
23004
22820
  selected: start + index === selectedIndex,
23005
22821
  width,
23006
- idWidth
22822
+ idWidth,
22823
+ archived
23007
22824
  }
23008
22825
  )));
23009
22826
  }
23010
22827
 
23011
- // src/tui/components/DetailView.tsx
23012
- var import_react40 = __toESM(require_react());
23013
- function DetailView({
23014
- task,
23015
- columnLabel,
23016
- width,
23017
- height,
23018
- selectedSubtaskIndex,
23019
- parent
23020
- }) {
23021
- const contentWidth = Math.max(20, width - 2);
23022
- const stacked = contentWidth < 60;
23023
- const priority = task.priority;
23024
- const contractState = getContractState(task);
23025
- const subtasks = task.subtasks ?? [];
23026
- const files = task.relatedFiles ?? [];
23027
- const state = priority ? `${columnLabel} \xB7 ${priority}` : columnLabel;
23028
- const headWidth = Math.max(1, contentWidth - state.length - 2);
23029
- const head = truncate(`${task.id} ${task.title}`, headWidth);
23030
- const bodyLines = buildBodyLines(task.description, contentWidth);
23031
- const bodyBudget = Math.max(
23032
- 1,
23033
- height - 8 - (stacked ? 3 : 1) - subtasks.length - (files.length ? 2 : 0)
23034
- );
23035
- const body = bodyLines.slice(0, bodyBudget);
23036
- const bodyTruncated = bodyLines.length > body.length;
23037
- return /* @__PURE__ */ import_react40.default.createElement(Box_default, { flexDirection: "column", width }, /* @__PURE__ */ import_react40.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react40.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.text, bold: true }, head), /* @__PURE__ */ import_react40.default.createElement(Text, null, pad(Math.max(1, contentWidth - head.length - state.length))), /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textSecondary }, columnLabel), priority ? /* @__PURE__ */ import_react40.default.createElement(Text, null, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textDim }, " \xB7 "), /* @__PURE__ */ import_react40.default.createElement(Text, { color: getPriorityColor(priority) }, priority)) : null)), /* @__PURE__ */ import_react40.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(contentWidth))), /* @__PURE__ */ import_react40.default.createElement(MetadataBlock, { task, parent, stacked, width }), /* @__PURE__ */ import_react40.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, body.length === 0 ? /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, "(no description)") : body.map((line, index) => /* @__PURE__ */ import_react40.default.createElement(
23038
- Text,
23039
- {
23040
- key: `body-${index}`,
23041
- wrap: "truncate",
23042
- bold: line.kind === "heading",
23043
- color: line.kind === "code" ? PALETTE.textMuted : line.kind === "heading" ? PALETTE.text : PALETTE.textSecondary
23044
- },
23045
- line.text || " "
23046
- )), bodyTruncated ? /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textDim }, "\u2026") : null), subtasks.length > 0 || contractState ? /* @__PURE__ */ import_react40.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, marginTop: 1 }, /* @__PURE__ */ import_react40.default.createElement(Text, null, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, "subtasks"), /* @__PURE__ */ import_react40.default.createElement(Text, null, pad(Math.max(1, contentWidth - 8 - 8))), /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, "contract")), renderSubtaskAndContractRows(
23047
- subtasks,
23048
- selectedSubtaskIndex,
23049
- contractState,
23050
- contentWidth
23051
- )) : null, files.length > 0 ? /* @__PURE__ */ import_react40.default.createElement(Box_default, { paddingLeft: 1, marginTop: 1, width }, /* @__PURE__ */ import_react40.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, "files "), /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textSecondary }, truncate(files.join(" \xB7 "), Math.max(1, contentWidth - 7))))) : null);
23052
- }
23053
- function renderSubtaskAndContractRows(subtasks, selectedSubtaskIndex, contractState, contentWidth) {
23054
- const rowCount = Math.max(subtasks.length, 1);
23055
- const rows = [];
23056
- for (let index = 0; index < rowCount; index += 1) {
23057
- const subtask = subtasks[index];
23058
- const selected = subtask !== void 0 && index === selectedSubtaskIndex;
23059
- const glyph = subtask?.completed ? GLYPHS.subtaskDone : GLYPHS.subtaskOpen;
23060
- const label = subtask ? ` ${glyph} ${subtask.title}` : " (none)";
23061
- const right = index === 0 ? contractState ?? (subtasks.length > 0 ? "(none)" : void 0) : void 0;
23062
- const rightText = right ?? "";
23063
- const left = truncate(label, Math.max(1, contentWidth - rightText.length - 2));
23064
- rows.push(
23065
- /* @__PURE__ */ import_react40.default.createElement(Text, { key: `sub-${index}`, wrap: "truncate" }, /* @__PURE__ */ import_react40.default.createElement(
23066
- Text,
23067
- {
23068
- color: selected ? void 0 : subtask?.completed ? PALETTE.textMuted : PALETTE.textSecondary,
23069
- inverse: selected
23070
- },
23071
- left
23072
- ), /* @__PURE__ */ import_react40.default.createElement(Text, null, pad(Math.max(1, contentWidth - left.length - rightText.length))), rightText ? /* @__PURE__ */ import_react40.default.createElement(Text, { color: getContractStateColor(right) }, rightText) : null)
23073
- );
23074
- }
23075
- return rows;
23076
- }
23077
- function MetadataBlock({
23078
- task,
23079
- parent,
23080
- stacked,
23081
- width
23082
- }) {
23083
- const tags = task.tags?.length ? task.tags.map((t) => `#${t}`).join(" ") : "\u2014";
23084
- const assignee = task.assignee || "\u2014";
23085
- const created = task.createdAt ? task.createdAt.slice(0, 10) : "\u2014";
23086
- const parentLabel = task.parentId ? parent ? task.parentId : `${task.parentId} \u2197` : "\u2014";
23087
- const fields = [
23088
- ["tags", tags],
23089
- ["assignee", assignee],
23090
- ["created", created],
23091
- ["parent", parentLabel]
23092
- ];
23093
- if (stacked) {
23094
- return /* @__PURE__ */ import_react40.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1, width }, fields.map(([label, value]) => /* @__PURE__ */ import_react40.default.createElement(Text, { key: label, wrap: "truncate" }, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, `${label} `), /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textSecondary }, value))));
23095
- }
23096
- return /* @__PURE__ */ import_react40.default.createElement(Box_default, { paddingLeft: 1, width }, /* @__PURE__ */ import_react40.default.createElement(Text, { wrap: "truncate" }, fields.map(([label, value], index) => /* @__PURE__ */ import_react40.default.createElement(Text, { key: label }, index > 0 ? /* @__PURE__ */ import_react40.default.createElement(Text, null, pad(4)) : null, /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textMuted }, `${label} `), /* @__PURE__ */ import_react40.default.createElement(Text, { color: PALETTE.textSecondary }, value)))));
23097
- }
23098
- function buildBodyLines(description, width) {
23099
- if (!description) return [];
23100
- const out = [];
23101
- let inFence = false;
23102
- for (const raw of description.split("\n")) {
23103
- const line = raw.replace(/\s+$/, "");
23104
- if (line.trim().startsWith("```")) {
23105
- inFence = !inFence;
23106
- continue;
23107
- }
23108
- if (inFence) {
23109
- out.push({ text: ` ${truncate(line, Math.max(1, width - 2))}`, kind: "code" });
23110
- continue;
23111
- }
23112
- if (/^#{1,6}\s/.test(line)) {
23113
- out.push({ text: truncate(line.replace(/^#{1,6}\s*/, ""), width), kind: "heading" });
23114
- continue;
23115
- }
23116
- if (/^\s*[-*+]\s/.test(line)) {
23117
- const item = line.replace(/^\s*[-*+]\s*/, "");
23118
- for (const wrapped of wrapText2(item, Math.max(1, width - 4))) {
23119
- out.push({ text: ` \u2022 ${wrapped}`, kind: "list" });
23120
- }
23121
- continue;
23122
- }
23123
- if (line.trim() === "") {
23124
- out.push({ text: "", kind: "text" });
23125
- continue;
23126
- }
23127
- for (const wrapped of wrapText2(line, width)) {
23128
- out.push({ text: wrapped, kind: "text" });
23129
- }
23130
- }
23131
- return out;
23132
- }
23133
-
23134
22828
  // src/tui/components/HelpOverlay.tsx
23135
22829
  var import_react41 = __toESM(require_react());
23136
- var GROUPS = [
23137
- {
23138
- heading: "nav",
23139
- rows: [
23140
- ["j / k \u2191 \u2193", "move selection"],
23141
- ["tab [ ] \u2190 \u2192", "cycle column"],
23142
- ["g / G", "top / bottom"],
23143
- ["ctrl-d / ctrl-u", "half page"],
23144
- ["\u21B5", "open detail"],
23145
- ["esc", "back / clear filter"],
23146
- ["1 / 2 / 3", "board / rules / logs"]
23147
- ]
23148
- },
23149
- {
23150
- heading: "actions",
23151
- rows: [
23152
- ["a", "add (title only)"],
23153
- ["m", "move to column"],
23154
- ["c", "complete"],
23155
- ["e", "edit in $EDITOR"],
23156
- ["p", "cycle priority"],
23157
- ["space", "toggle subtask (detail)"],
23158
- ["d", "delete"],
23159
- ["y", "copy id"],
23160
- ["r", "reload"]
23161
- ]
23162
- },
23163
- {
23164
- heading: "filter",
23165
- rows: [
23166
- ["/", "open filter"],
23167
- ["p:high", "priority"],
23168
- ["#tag t:tag", "tag"],
23169
- ["@name", "assignee"],
23170
- ["type:epic", "document type"],
23171
- ["contract:ready", "contract state"],
23172
- ["due:overdue", "due date"]
23173
- ]
23174
- }
22830
+ var LIST = {
22831
+ heading: "list",
22832
+ rows: [
22833
+ ["j k \u2191\u2193", "move"],
22834
+ ["h l tab \u2190\u2192", "column"],
22835
+ ["g G", "top / bottom"],
22836
+ ["ctrl-d/u", "half page"],
22837
+ ["\u21B5", "open"],
22838
+ ["t", "type"],
22839
+ ["L", "done"],
22840
+ ["/", "filter"]
22841
+ ]
22842
+ };
22843
+ var DETAIL = {
22844
+ heading: "detail",
22845
+ rows: [
22846
+ ["\u21B5", "open child"],
22847
+ ["space", "toggle subtask"],
22848
+ ["u d", "scroll body"],
22849
+ ["p", "parent"],
22850
+ ["esc", "back"]
22851
+ ]
22852
+ };
22853
+ var ACTIONS = {
22854
+ heading: "actions",
22855
+ rows: [
22856
+ ["a n", "add"],
22857
+ ["N", "add + edit"],
22858
+ ["m", "move"],
22859
+ ["c", "complete"],
22860
+ ["e", "edit"],
22861
+ ["p", "priority"],
22862
+ ["space", "collapse"],
22863
+ ["d", "delete"],
22864
+ ["y", "copy id"],
22865
+ ["r", "reload"],
22866
+ ["A", "archive"]
22867
+ ]
22868
+ };
22869
+ var FILTER = {
22870
+ heading: "filter",
22871
+ rows: [
22872
+ ["p:high", "priority"],
22873
+ ["#tag t:tag", "tag"],
22874
+ ["@name", "assignee"],
22875
+ ["type:epic", "type"],
22876
+ ["contract:ready", "contract"],
22877
+ ["due:overdue", "due"]
22878
+ ]
22879
+ };
22880
+ var COLUMNS = [
22881
+ [LIST, DETAIL],
22882
+ [ACTIONS, FILTER]
23175
22883
  ];
23176
- var KEYS_WIDTH = 16;
22884
+ var KEYS_WIDTH = 14;
23177
22885
  function HelpOverlay({ termWidth, termHeight }) {
23178
- const columnWidth = Math.max(24, Math.floor((termWidth - 4) / GROUPS.length));
22886
+ const columnWidth = Math.max(24, Math.floor((termWidth - 4) / COLUMNS.length));
23179
22887
  const descriptionWidth = Math.max(6, columnWidth - KEYS_WIDTH - 2);
23180
- return /* @__PURE__ */ import_react41.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight, paddingLeft: 1 }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.text, bold: true }, "help"), /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(Math.max(1, termWidth - 2))), /* @__PURE__ */ import_react41.default.createElement(Box_default, { marginTop: 1 }, GROUPS.map((group) => /* @__PURE__ */ import_react41.default.createElement(Box_default, { key: group.heading, flexDirection: "column", width: columnWidth }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textSecondary, bold: true }, group.heading), group.rows.map(([keys, description]) => /* @__PURE__ */ import_react41.default.createElement(Text, { key: keys, wrap: "truncate" }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.text }, keys.padEnd(KEYS_WIDTH)), /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textMuted }, truncate(description, descriptionWidth))))))), /* @__PURE__ */ import_react41.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textDim }, "any key to dismiss \xB7 q quit")));
22888
+ return /* @__PURE__ */ import_react41.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight, paddingLeft: 1 }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.text, bold: true }, CHROME.helpTitle), /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textDim }, RULE.repeat(Math.max(1, termWidth - 2))), /* @__PURE__ */ import_react41.default.createElement(Box_default, { marginTop: 1 }, COLUMNS.map((column, columnIndex) => /* @__PURE__ */ import_react41.default.createElement(Box_default, { key: columnIndex, flexDirection: "column", width: columnWidth }, column.map((group, groupIndex) => /* @__PURE__ */ import_react41.default.createElement(
22889
+ Box_default,
22890
+ {
22891
+ key: group.heading,
22892
+ flexDirection: "column",
22893
+ marginTop: groupIndex === 0 ? 0 : 1
22894
+ },
22895
+ /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textSecondary, bold: true }, group.heading),
22896
+ group.rows.map(([keys, description]) => /* @__PURE__ */ import_react41.default.createElement(Text, { key: keys, wrap: "truncate" }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.text }, keys.padEnd(KEYS_WIDTH)), /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textMuted }, truncate(description, descriptionWidth))))
22897
+ ))))), /* @__PURE__ */ import_react41.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react41.default.createElement(Text, { color: PALETTE.textDim }, CHROME.helpDismiss)));
23181
22898
  }
23182
22899
 
23183
22900
  // src/tui/components/Overlays.tsx
@@ -23198,190 +22915,46 @@ function MoveOverlay({
23198
22915
  return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "move"), /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, ` ${taskId} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.text }, truncate(taskTitle, Math.max(1, width - taskId.length - 6)))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", marginTop: 1, paddingLeft: 2 }, columns.map((column, index) => /* @__PURE__ */ import_react42.default.createElement(Text, { key: column.id, inverse: index === selectedIndex, wrap: "truncate" }, `${index + 1} ${column.title}`))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "\u2191\u2193 select \xB7 1-", columns.length, " jump \xB7 \u21B5 confirm \xB7 esc cancel")));
23199
22916
  }
23200
22917
  function DeleteConfirmOverlay({ taskId, taskTitle, width }) {
23201
- return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.error }, "delete"), /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, ` ${taskId} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.text }, truncate(taskTitle, Math.max(1, width - taskId.length - 6)))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "This cannot be undone.")), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "y delete \xB7 n cancel")));
22918
+ return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.error }, "delete"), /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, ` ${taskId} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.text }, truncate(taskTitle, Math.max(1, width - taskId.length - 6)))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "Deletes the file. This cannot be undone.")), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "y delete \xB7 n cancel")));
23202
22919
  }
23203
22920
  function CompleteConfirmOverlay({ target, width }) {
23204
- return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.warning }, "complete"), /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, ` ${target.id} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.text }, truncate(target.title, Math.max(1, width - target.id.length - 6)))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, `${target.incompleteChildren.length} incomplete child task${target.incompleteChildren.length === 1 ? "" : "s"}:`)), /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 4 }, target.incompleteChildren.map((child) => /* @__PURE__ */ import_react42.default.createElement(Text, { key: child.id, wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, `${child.id} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textSecondary }, truncate(child.title, Math.max(1, width - child.id.length - 8)))))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "y complete anyway \xB7 n cancel")));
23205
- }
23206
- function AddOverlay({ title, columnName }) {
23207
- return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, `add to ${columnName}`), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: title ? PALETTE.text : PALETTE.textMuted }, title || "title\u2026"), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.accent }, GLYPHS.cursor))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "\u21B5 create \xB7 esc cancel")));
23208
- }
23209
-
23210
- // src/tui/components/RulesPanel.tsx
23211
- var import_react43 = __toESM(require_react());
23212
- var RULE_TYPES2 = ["always", "never", "prefer", "context"];
23213
- var RULE_TYPE_LABELS = {
23214
- always: "Always",
23215
- never: "Never",
23216
- prefer: "Prefer",
23217
- context: "Context"
23218
- };
23219
- var RULE_TYPE_COLORS = {
23220
- always: PALETTE.success,
23221
- never: PALETTE.error,
23222
- prefer: PALETTE.warning,
23223
- context: PALETTE.accent
23224
- };
23225
- function RulesPanel({
23226
- rules,
23227
- activeRuleType,
23228
- selectedRuleIndex,
23229
- viewportHeight,
23230
- termWidth,
23231
- mode,
23232
- editText,
23233
- layoutMode = "wide"
23234
- }) {
23235
- const currentRules = rules?.[activeRuleType] || [];
23236
- const maxWidth = Math.max(termWidth - 8, 20);
23237
- const scrollPadding = 2;
23238
- const headerRows = layoutMode === "narrow" ? 4 : 6;
23239
- const visibleRules = Math.max(viewportHeight - headerRows, 3);
23240
- let scrollOffset = 0;
23241
- if (selectedRuleIndex >= visibleRules - scrollPadding) {
23242
- scrollOffset = Math.min(
23243
- selectedRuleIndex - visibleRules + scrollPadding + 1,
23244
- Math.max(0, currentRules.length - visibleRules)
23245
- );
23246
- }
23247
- return /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", paddingX: 1 }, layoutMode === "narrow" ? /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginBottom: 1 }, RULE_TYPES2.map((type) => {
23248
- const count = rules?.[type]?.length || 0;
23249
- const isActive = type === activeRuleType;
23250
- const initial = RULE_TYPE_LABELS[type].charAt(0);
23251
- return /* @__PURE__ */ import_react43.default.createElement(Text, { key: type }, /* @__PURE__ */ import_react43.default.createElement(
23252
- Text,
23253
- {
23254
- color: isActive ? RULE_TYPE_COLORS[type] : PALETTE.textDim,
23255
- bold: isActive
23256
- },
23257
- initial
23258
- ), count > 0 && /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textDim }, "(", count, ")"), /* @__PURE__ */ import_react43.default.createElement(Text, null, " "));
23259
- }), /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textDim }, "h/l")) : /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginBottom: 1 }, RULE_TYPES2.map((type, idx) => {
23260
- const count = rules?.[type]?.length || 0;
23261
- const isActive = type === activeRuleType;
23262
- return /* @__PURE__ */ import_react43.default.createElement(Box_default, { key: type, marginRight: 2 }, /* @__PURE__ */ import_react43.default.createElement(
23263
- Text,
23264
- {
23265
- color: isActive ? RULE_TYPE_COLORS[type] : PALETTE.textMuted,
23266
- bold: isActive,
23267
- inverse: isActive
23268
- },
23269
- " ",
23270
- RULE_TYPE_LABELS[type],
23271
- count > 0 && /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, " (", count, ")"),
23272
- " "
23273
- ), /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textDim }, " [", idx + 1, "]"));
23274
- })), layoutMode === "wide" && /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginBottom: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, "h/l"), " switch type", " ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, "j/k"), " select", " ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, "n"), " new", " ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, "e"), " edit", " ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, "d"), " delete")), /* @__PURE__ */ import_react43.default.createElement(Box_default, null, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.border }, RULE.repeat(Math.max(1, termWidth - 4)))), (mode === "rule-add" || mode === "rule-edit") && /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.accent }, mode === "rule-add" ? "New rule:" : "Edit rule:"), /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginTop: 0 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.text, backgroundColor: PALETTE.bgHighlight }, " ", editText || " ", GLYPHS.cursor, " ")), /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted, dimColor: true }, "Enter to save, Esc to cancel")), mode === "rule-delete-confirm" && currentRules[selectedRuleIndex] && /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", marginY: 1, paddingX: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.error, bold: true }, "Delete this ", activeRuleType, " rule?"), /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, "#", currentRules[selectedRuleIndex].id, ": ", truncate(currentRules[selectedRuleIndex].rule, maxWidth - 10))), /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted }, "Press ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.success }, "y"), " to confirm, ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.error }, "n"), " to cancel"))), mode !== "rule-add" && mode !== "rule-edit" && mode !== "rule-delete-confirm" && /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", marginTop: 1 }, currentRules.length === 0 ? /* @__PURE__ */ import_react43.default.createElement(Box_default, { paddingY: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted }, "No ", activeRuleType, " rules defined. Press ", /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.accent }, "n"), " to add one.")) : currentRules.slice(scrollOffset, scrollOffset + visibleRules).map((rule, displayIdx) => {
23275
- const actualIdx = scrollOffset + displayIdx;
23276
- const isSelected = actualIdx === selectedRuleIndex;
23277
- return /* @__PURE__ */ import_react43.default.createElement(Box_default, { key: rule.id, paddingY: 0 }, /* @__PURE__ */ import_react43.default.createElement(
23278
- Text,
23279
- {
23280
- color: isSelected ? PALETTE.text : PALETTE.textSecondary,
23281
- backgroundColor: isSelected ? PALETTE.bgHighlight : void 0,
23282
- bold: isSelected
23283
- },
23284
- isSelected ? GLYPHS.pointer : " ",
23285
- " ",
23286
- /* @__PURE__ */ import_react43.default.createElement(Text, { color: RULE_TYPE_COLORS[activeRuleType] }, "#", rule.id),
23287
- " ",
23288
- truncate(rule.rule, maxWidth - 8)
23289
- ));
23290
- }), currentRules.length > visibleRules && /* @__PURE__ */ import_react43.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textDim }, scrollOffset + 1, "-", Math.min(scrollOffset + visibleRules, currentRules.length), " of ", currentRules.length))));
22921
+ return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.warning }, "complete"), /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, ` ${target.id} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.text }, truncate(target.title, Math.max(1, width - target.id.length - 6)))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, incompleteChildrenLabel(target.incompleteChildren.length))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 4 }, target.incompleteChildren.map((child) => /* @__PURE__ */ import_react42.default.createElement(Text, { key: child.id, wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, `${child.id} `), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textSecondary }, truncate(child.title, Math.max(1, width - child.id.length - 8)))))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, "y complete anyway \xB7 n cancel")));
23291
22922
  }
23292
-
23293
- // src/tui/components/LogsPanel.tsx
23294
- var import_react44 = __toESM(require_react());
23295
- function LogsPanel({
23296
- logs,
23297
- selectedIndex,
23298
- viewportHeight,
23299
- termWidth,
23300
- expandedIds,
23301
- mode,
23302
- columns,
23303
- restoreColumnIndex,
23304
- layoutMode = "wide"
23305
- }) {
23306
- const maxWidth = Math.max(termWidth - 8, 20);
23307
- const scrollPadding = 2;
23308
- const headerRows = layoutMode === "narrow" ? 4 : 6;
23309
- const visibleTasks = Math.max(viewportHeight - headerRows, 3);
23310
- let scrollOffset = 0;
23311
- if (selectedIndex >= visibleTasks - scrollPadding) {
23312
- scrollOffset = Math.min(
23313
- selectedIndex - visibleTasks + scrollPadding + 1,
23314
- Math.max(0, logs.length - visibleTasks)
23315
- );
23316
- }
23317
- const selectedTask = logs[selectedIndex];
23318
- return /* @__PURE__ */ import_react44.default.createElement(Box_default, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginBottom: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, logs.length, " logged", layoutMode === "wide" ? ` task${logs.length !== 1 ? "s" : ""}` : ""), layoutMode === "wide" && /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginLeft: 2 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textMuted }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, "j/k"), " select", " ", /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, "Enter"), " expand", " ", /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, "R"), " restore", " ", /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, "d"), " delete"))), /* @__PURE__ */ import_react44.default.createElement(Box_default, null, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.border }, RULE.repeat(Math.max(1, termWidth - 4)))), mode === "logs-restore" && selectedTask && /* @__PURE__ */ import_react44.default.createElement(Box_default, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.accent }, 'Restore "', /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.text }, truncate(selectedTask.title, 30)), '" to:'), /* @__PURE__ */ import_react44.default.createElement(Box_default, { flexDirection: "column", marginTop: 1 }, columns.map((col, idx) => /* @__PURE__ */ import_react44.default.createElement(Box_default, { key: col.id }, /* @__PURE__ */ import_react44.default.createElement(
23319
- Text,
23320
- {
23321
- color: idx === restoreColumnIndex ? PALETTE.text : PALETTE.textMuted,
23322
- backgroundColor: idx === restoreColumnIndex ? PALETTE.bgHighlight : void 0,
23323
- bold: idx === restoreColumnIndex
23324
- },
23325
- idx === restoreColumnIndex ? GLYPHS.pointer : " ",
23326
- " ",
23327
- col.title,
23328
- /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textDim }, " (", col.tasks?.length ?? 0, ")")
23329
- )))), /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textMuted }, "j/k to select, Enter to restore, Esc to cancel"))), mode === "logs-delete-confirm" && selectedTask && /* @__PURE__ */ import_react44.default.createElement(Box_default, { flexDirection: "column", marginY: 1, paddingX: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.error, bold: true }, "Permanently delete this logged task?"), /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, selectedTask.id, ": ", truncate(selectedTask.title, maxWidth - 15))), /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.warning }, "This cannot be undone.")), /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textMuted }, "Press ", /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.success }, "y"), " to confirm, ", /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.error }, "n"), " to cancel"))), mode !== "logs-restore" && mode !== "logs-delete-confirm" && /* @__PURE__ */ import_react44.default.createElement(Box_default, { flexDirection: "column", marginTop: 1 }, logs.length === 0 ? /* @__PURE__ */ import_react44.default.createElement(Box_default, { paddingY: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textMuted }, "No logged tasks.")) : logs.slice(scrollOffset, scrollOffset + visibleTasks).map((task, displayIdx) => {
23330
- const actualIdx = scrollOffset + displayIdx;
23331
- const isSelected = actualIdx === selectedIndex;
23332
- const isExpanded = expandedIds.has(task.id);
23333
- return /* @__PURE__ */ import_react44.default.createElement(Box_default, { key: task.id, flexDirection: "column", paddingY: 0 }, /* @__PURE__ */ import_react44.default.createElement(Box_default, null, /* @__PURE__ */ import_react44.default.createElement(
23334
- Text,
23335
- {
23336
- color: isSelected ? PALETTE.text : PALETTE.textSecondary,
23337
- backgroundColor: isSelected ? PALETTE.bgHighlight : void 0,
23338
- bold: isSelected
23339
- },
23340
- isSelected ? GLYPHS.pointer : " ",
23341
- " ",
23342
- isExpanded ? "\u25BC" : GLYPHS.collapsed,
23343
- " ",
23344
- /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textDim }, task.id),
23345
- " ",
23346
- truncate(task.title, maxWidth - task.id.length - 8),
23347
- task.priority && /* @__PURE__ */ import_react44.default.createElement(Text, { color: getPriorityColor(task.priority) }, " [", task.priority, "]")
23348
- )), isExpanded && /* @__PURE__ */ import_react44.default.createElement(Box_default, { flexDirection: "column", marginLeft: 4, marginBottom: 1 }, task.description && /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textMuted }, truncate(task.description, maxWidth - 6)), task.tags && task.tags.length > 0 && /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.accent }, task.tags.map((t) => `#${t}`).join(" ")), task.assignee && /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textSecondary }, "@", task.assignee), task.subtasks && task.subtasks.length > 0 && /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textMuted }, "Subtasks: ", task.subtasks.filter((s) => s.completed).length, "/", task.subtasks.length)));
23349
- }), logs.length > visibleTasks && /* @__PURE__ */ import_react44.default.createElement(Box_default, { marginTop: 1 }, /* @__PURE__ */ import_react44.default.createElement(Text, { color: PALETTE.textDim }, scrollOffset + 1, "-", Math.min(scrollOffset + visibleTasks, logs.length), " of ", logs.length))));
22923
+ function AddOverlay({ title, columnName, thenEdit = false }) {
22924
+ return /* @__PURE__ */ import_react42.default.createElement(Box_default, { flexDirection: "column", flexGrow: 1, paddingLeft: 1 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, thenEdit ? `add to ${columnName}, then edit` : `add to ${columnName}`), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: title ? PALETTE.text : PALETTE.textMuted }, title || "title\u2026"), /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.accent }, GLYPHS.cursor))), /* @__PURE__ */ import_react42.default.createElement(Box_default, { marginTop: 1, paddingLeft: 2 }, /* @__PURE__ */ import_react42.default.createElement(Text, { color: PALETTE.textMuted }, thenEdit ? "\u21B5 create and edit \xB7 esc cancel" : "\u21B5 create \xB7 esc cancel")));
23350
22925
  }
23351
22926
 
23352
22927
  // src/tui/BrainfileTUI.tsx
23353
- var initialState = {
22928
+ var baseInitialState = {
23354
22929
  board: null,
23355
22930
  error: null,
23356
22931
  lastUpdated: /* @__PURE__ */ new Date(),
23357
- activePanel: "board",
23358
22932
  activeColumnIndex: 0,
23359
22933
  selectedTaskIndex: 0,
23360
22934
  mode: "browse",
22935
+ helpReturn: null,
23361
22936
  filterQuery: "",
23362
- selectedSubtaskIndex: 0,
22937
+ activeTypeFilter: "all",
22938
+ doneView: false,
22939
+ detailPath: [],
22940
+ detailCursor: 0,
22941
+ detailScroll: 0,
23363
22942
  moveTargetIndex: 0,
23364
22943
  newTaskTitle: "",
22944
+ addThenEdit: false,
23365
22945
  completeConfirm: null,
23366
22946
  statusMessage: null,
23367
22947
  lastContentHash: null,
23368
22948
  reloadFlash: false,
23369
- activeRuleType: "always",
23370
- selectedRuleIndex: 0,
23371
- ruleEditText: "",
23372
- ruleEditId: null,
23373
- logs: [],
23374
- selectedLogIndex: 0,
23375
- logRestoreColumnIndex: 0,
23376
- expandedLogIds: /* @__PURE__ */ new Set()
22949
+ logs: []
23377
22950
  };
23378
22951
  function BrainfileTUI({ filePath, width, height }) {
23379
22952
  const { stdout } = use_stdout_default();
23380
- const [dimensions, setDimensions] = (0, import_react45.useState)({
22953
+ const [dimensions, setDimensions] = (0, import_react43.useState)({
23381
22954
  width: width ?? stdout?.columns ?? process.stdout.columns ?? 80,
23382
22955
  height: height ?? stdout?.rows ?? process.stdout.rows ?? 24
23383
22956
  });
23384
- (0, import_react45.useEffect)(() => {
22957
+ (0, import_react43.useEffect)(() => {
23385
22958
  if (width !== void 0 && height !== void 0) return void 0;
23386
22959
  const handleResize = () => {
23387
22960
  setDimensions({
@@ -23398,10 +22971,20 @@ function BrainfileTUI({ filePath, width, height }) {
23398
22971
  const termHeight = height ?? dimensions.height;
23399
22972
  const isTooSmall = termWidth < LAYOUT.MIN_WIDTH || termHeight < LAYOUT.MIN_HEIGHT;
23400
22973
  const layoutMode = termWidth >= LAYOUT.DETAIL_PANE_MIN_WIDTH ? "wide" : "narrow";
23401
- const [state, setState] = (0, import_react45.useState)(initialState);
22974
+ const resumeColumn = (0, import_react43.useRef)(void 0);
22975
+ const [state, setState] = (0, import_react43.useState)(() => {
22976
+ const persisted = readTuiState(filePath);
22977
+ resumeColumn.current = persisted.lastColumn;
22978
+ return {
22979
+ ...baseInitialState,
22980
+ collapsedIds: new Set(persisted.collapsed),
22981
+ // 'done' was a pseudo-type before the L toggle; never resume into it.
22982
+ activeTypeFilter: persisted.lastTypeFilter && persisted.lastTypeFilter !== "done" ? persisted.lastTypeFilter : baseInitialState.activeTypeFilter
22983
+ };
22984
+ });
23402
22985
  const viewportHeight = Math.max(termHeight - HEADER_ROWS - FOOTER_ROWS, 3);
23403
22986
  const { loadBrainfile } = useBrainfileLoader(filePath, state, setState);
23404
- const orderedColumns = (0, import_react45.useMemo)(() => {
22987
+ const orderedColumns = (0, import_react43.useMemo)(() => {
23405
22988
  if (!state.board) return [];
23406
22989
  return [...state.board.columns].sort((a, b) => {
23407
22990
  const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
@@ -23409,51 +22992,102 @@ function BrainfileTUI({ filePath, width, height }) {
23409
22992
  return orderA - orderB;
23410
22993
  });
23411
22994
  }, [state.board]);
23412
- const totalCount = (0, import_react45.useMemo)(
23413
- () => orderedColumns.reduce((sum, column) => sum + (column.tasks?.length ?? 0), 0),
23414
- [orderedColumns]
22995
+ const typeCycleOptions = (0, import_react43.useMemo)(() => getTypeCycleOptions(state.board), [state.board]);
22996
+ const activeTypeFilter = typeCycleOptions.includes(state.activeTypeFilter) ? state.activeTypeFilter : "all";
22997
+ const typeFilterActive = activeTypeFilter !== "all";
22998
+ const doneView = state.doneView;
22999
+ const typeFilteredColumns = (0, import_react43.useMemo)(() => {
23000
+ if (!typeFilterActive) return orderedColumns;
23001
+ return orderedColumns.map((column) => ({
23002
+ ...column,
23003
+ tasks: (column.tasks ?? []).filter((task) => getDocType(task) === activeTypeFilter)
23004
+ }));
23005
+ }, [orderedColumns, typeFilterActive, doneView, activeTypeFilter]);
23006
+ const totalCount = (0, import_react43.useMemo)(
23007
+ () => doneView ? typeFilterActive ? state.logs.filter((task) => getDocType(task) === activeTypeFilter).length : state.logs.length : typeFilteredColumns.reduce((sum, column) => sum + (column.tasks?.length ?? 0), 0),
23008
+ [typeFilteredColumns, doneView, state.logs]
23415
23009
  );
23416
23010
  const filterQuery = state.filterQuery.trim();
23417
23011
  const hasFilter = filterQuery.length > 0;
23418
- const filteredColumns = (0, import_react45.useMemo)(() => {
23419
- if (!hasFilter) return orderedColumns;
23420
- return orderedColumns.map((column) => {
23012
+ const filteredColumns = (0, import_react43.useMemo)(() => {
23013
+ if (!hasFilter) return typeFilteredColumns;
23014
+ return typeFilteredColumns.map((column) => {
23421
23015
  const docs = (column.tasks ?? []).map((task) => ({ task, body: task.description ?? "" }));
23422
- const matches = (0, import_core5.searchTasksRanked)(docs, filterQuery);
23016
+ const matches = (0, import_core6.searchTasksRanked)(docs, filterQuery);
23423
23017
  return { ...column, tasks: matches.map((match) => match.doc.task) };
23424
23018
  });
23425
- }, [orderedColumns, hasFilter, filterQuery]);
23426
- const matchCount = (0, import_react45.useMemo)(
23427
- () => filteredColumns.reduce((sum, column) => sum + (column.tasks?.length ?? 0), 0),
23428
- [filteredColumns]
23019
+ }, [typeFilteredColumns, hasFilter, filterQuery]);
23020
+ const filteredLogs = (0, import_react43.useMemo)(() => {
23021
+ if (!doneView) return [];
23022
+ const typed = typeFilterActive ? state.logs.filter((task) => getDocType(task) === activeTypeFilter) : state.logs;
23023
+ if (!hasFilter) return typed;
23024
+ const docs = typed.map((task) => ({ task, body: task.description ?? "" }));
23025
+ return (0, import_core6.searchTasksRanked)(docs, filterQuery).map((match) => match.doc.task);
23026
+ }, [doneView, state.logs, typeFilterActive, activeTypeFilter, hasFilter, filterQuery]);
23027
+ const matchCount = (0, import_react43.useMemo)(
23028
+ () => doneView ? filteredLogs.length : filteredColumns.reduce((sum, column) => sum + (column.tasks?.length ?? 0), 0),
23029
+ [filteredColumns, doneView, filteredLogs]
23429
23030
  );
23430
23031
  const activeColumnIndex = Math.min(
23431
23032
  state.activeColumnIndex,
23432
23033
  Math.max(0, filteredColumns.length - 1)
23433
23034
  );
23434
23035
  const currentColumn = filteredColumns[activeColumnIndex];
23435
- const rows = (0, import_react45.useMemo)(() => buildRows(currentColumn?.tasks ?? []), [currentColumn]);
23036
+ const rows = (0, import_react43.useMemo)(
23037
+ () => doneView ? buildFlatRows(filteredLogs) : typeFilterActive ? buildFlatRows(currentColumn?.tasks ?? []) : buildRows(currentColumn?.tasks ?? [], state.collapsedIds),
23038
+ [currentColumn, doneView, filteredLogs, typeFilterActive, state.collapsedIds]
23039
+ );
23436
23040
  const maxRowIndex = Math.max(0, rows.length - 1);
23437
23041
  const selectedIndex = Math.min(state.selectedTaskIndex, maxRowIndex);
23438
23042
  const selectedTask = rows[selectedIndex]?.task;
23439
- const allBoardTasks = (0, import_react45.useMemo)(
23043
+ const allBoardTasks = (0, import_react43.useMemo)(
23440
23044
  () => orderedColumns.flatMap((column) => column.tasks ?? []),
23441
23045
  [orderedColumns]
23442
23046
  );
23443
- const parentTask = selectedTask?.parentId ? allBoardTasks.find((task) => task.id === selectedTask.parentId) : void 0;
23444
- (0, import_react45.useEffect)(() => {
23047
+ const allDocs = (0, import_react43.useMemo)(
23048
+ () => [...allBoardTasks, ...state.logs],
23049
+ [allBoardTasks, state.logs]
23050
+ );
23051
+ const archivedIds = (0, import_react43.useMemo)(() => new Set(state.logs.map((t) => t.id)), [state.logs]);
23052
+ const detailTaskId = state.detailPath[state.detailPath.length - 1];
23053
+ const detailTask = state.detailPath.length > 0 ? allDocs.find((t) => t.id === detailTaskId) : void 0;
23054
+ const detailChildren = (0, import_react43.useMemo)(
23055
+ () => detailTask ? allDocs.filter((t) => t.parentId === detailTask.id) : [],
23056
+ [allDocs, detailTask]
23057
+ );
23058
+ const detailParent = detailTask?.parentId ? allDocs.find((t) => t.id === detailTask.parentId) : void 0;
23059
+ const detailColumnLabel = (0, import_react43.useMemo)(() => {
23060
+ if (!detailTask) return "";
23061
+ if (archivedIds.has(detailTask.id)) {
23062
+ const at = detailTask.completedAt;
23063
+ return at ? `completed ${at.slice(5, 10)}` : "completed";
23064
+ }
23065
+ return orderedColumns.find((c) => c.tasks?.some((t) => t.id === detailTask.id))?.id ?? "";
23066
+ }, [orderedColumns, detailTask, archivedIds]);
23067
+ const detailActivity = (0, import_react43.useMemo)(
23068
+ () => detailTask ? getTaskActivity(filePath, detailTask.id) : [],
23069
+ [detailTask, filePath, state.lastUpdated]
23070
+ );
23071
+ const detailOpen = state.mode === "detail" && Boolean(detailTask);
23072
+ const fullscreenDetailMode = detailOpen && layoutMode === "narrow";
23073
+ const detailPaneWidth = Math.floor(termWidth * LAYOUT.DETAIL_PANE_FRACTION);
23074
+ const detailViewWidth = fullscreenDetailMode ? termWidth : detailPaneWidth;
23075
+ const detailViewHeight = fullscreenDetailMode ? termHeight - FOOTER_ROWS : viewportHeight;
23076
+ const detailLayout = detailTask ? computeDetailLayout(detailTask, detailViewWidth, detailViewHeight, detailChildren, detailActivity) : null;
23077
+ (0, import_react43.useEffect)(() => {
23078
+ const wanted = resumeColumn.current;
23079
+ if (!wanted || orderedColumns.length === 0) return;
23080
+ resumeColumn.current = void 0;
23081
+ const idx = orderedColumns.findIndex((c) => c.id === wanted);
23082
+ if (idx > 0) setState((prev) => ({ ...prev, activeColumnIndex: idx }));
23083
+ }, [orderedColumns]);
23084
+ (0, import_react43.useEffect)(() => {
23445
23085
  setState((prev) => ({
23446
23086
  ...prev,
23447
23087
  activeColumnIndex: Math.min(prev.activeColumnIndex, Math.max(0, filteredColumns.length - 1)),
23448
23088
  selectedTaskIndex: Math.min(prev.selectedTaskIndex, maxRowIndex)
23449
23089
  }));
23450
23090
  }, [filteredColumns.length, maxRowIndex]);
23451
- (0, import_react45.useEffect)(() => {
23452
- if (state.activePanel === "logs") {
23453
- const result = loadLogs(filePath);
23454
- setState((prev) => ({ ...prev, logs: result.logs }));
23455
- }
23456
- }, [state.activePanel, filePath, state.lastUpdated]);
23457
23091
  useKeyboardNavigation({
23458
23092
  state: { ...state, activeColumnIndex, selectedTaskIndex: selectedIndex },
23459
23093
  setState,
@@ -23462,42 +23096,69 @@ function BrainfileTUI({ filePath, width, height }) {
23462
23096
  viewportHeight,
23463
23097
  loadBrainfile,
23464
23098
  filePath,
23465
- allColumns: orderedColumns
23099
+ allColumns: orderedColumns,
23100
+ typeCycleOptions,
23101
+ detailTask,
23102
+ detailChildren,
23103
+ detailParent,
23104
+ detailActivity,
23105
+ detailWidth: detailViewWidth,
23106
+ detailHeight: detailViewHeight,
23107
+ doneView,
23108
+ detailArchived: Boolean(detailTask && archivedIds.has(detailTask.id))
23466
23109
  });
23467
23110
  if (isTooSmall) {
23468
- return /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight, paddingLeft: 1 }, /* @__PURE__ */ import_react45.default.createElement(Text, { color: PALETTE.error }, "Terminal too small"), /* @__PURE__ */ import_react45.default.createElement(Text, { color: PALETTE.textMuted }, "Minimum ", LAYOUT.MIN_WIDTH, "x", LAYOUT.MIN_HEIGHT, " \xB7 current ", termWidth, "x", termHeight));
23111
+ return /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight, paddingLeft: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.error }, CHROME.tooSmall), /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted }, "Minimum ", LAYOUT.MIN_WIDTH, "\xD7", LAYOUT.MIN_HEIGHT, " \xB7 current ", termWidth, "\xD7", termHeight));
23469
23112
  }
23470
23113
  if (state.error) {
23471
- return /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1 }, /* @__PURE__ */ import_react45.default.createElement(Text, { color: PALETTE.error }, "Error"), /* @__PURE__ */ import_react45.default.createElement(Text, { color: PALETTE.textSecondary }, state.error), /* @__PURE__ */ import_react45.default.createElement(Text, { color: PALETTE.textMuted }, "q quit \xB7 r retry"));
23114
+ return /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", paddingLeft: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.error }, CHROME.errorTitle), /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textSecondary }, state.error), /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted }, CHROME.errorHint));
23472
23115
  }
23473
23116
  if (!state.board) {
23474
- return /* @__PURE__ */ import_react45.default.createElement(Box_default, { paddingLeft: 1 }, /* @__PURE__ */ import_react45.default.createElement(Text, { color: PALETTE.textMuted }, "Loading\u2026"));
23117
+ return /* @__PURE__ */ import_react43.default.createElement(Box_default, { paddingLeft: 1 }, /* @__PURE__ */ import_react43.default.createElement(Text, { color: PALETTE.textMuted }, CHROME.loading));
23475
23118
  }
23476
23119
  if (state.mode === "help") {
23477
- return /* @__PURE__ */ import_react45.default.createElement(HelpOverlay, { termWidth, termHeight });
23120
+ return /* @__PURE__ */ import_react43.default.createElement(HelpOverlay, { termWidth, termHeight });
23478
23121
  }
23479
23122
  const boardTitle = state.board.title || "brainfile";
23480
- const columnLabel = currentColumn?.id ?? "";
23123
+ const columnLabel = doneView ? "done" : currentColumn?.id ?? "";
23481
23124
  const columnName = currentColumn?.title ?? "";
23482
- const detailOpen = state.mode === "detail" && Boolean(selectedTask);
23483
23125
  const fullscreenDetail = detailOpen && layoutMode === "narrow";
23484
- if (fullscreenDetail && selectedTask) {
23485
- return /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight }, /* @__PURE__ */ import_react45.default.createElement(
23126
+ const detailArchived = Boolean(detailTask && archivedIds.has(detailTask.id));
23127
+ const detailFooterCtx = {
23128
+ task: detailTask,
23129
+ hasParent: Boolean(detailParent),
23130
+ hasChildren: detailChildren.length > 0,
23131
+ hasSubtasks: (detailTask?.subtasks?.length ?? 0) > 0,
23132
+ bodyOverflows: detailLayout?.bodyOverflows ?? false,
23133
+ archived: detailArchived
23134
+ };
23135
+ if (fullscreenDetail && detailTask) {
23136
+ return /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight }, /* @__PURE__ */ import_react43.default.createElement(
23486
23137
  DetailView,
23487
23138
  {
23488
- task: selectedTask,
23489
- columnLabel,
23139
+ task: detailTask,
23140
+ columnLabel: detailColumnLabel,
23490
23141
  width: termWidth,
23491
23142
  height: termHeight - FOOTER_ROWS,
23492
- selectedSubtaskIndex: state.selectedSubtaskIndex,
23493
- parent: parentTask
23143
+ breadcrumb: state.detailPath,
23144
+ parent: detailParent,
23145
+ children: detailChildren,
23146
+ activity: detailActivity,
23147
+ cursor: state.detailCursor,
23148
+ scrollOffset: state.detailScroll
23149
+ }
23150
+ ), /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexGrow: 1 }), /* @__PURE__ */ import_react43.default.createElement(Box_default, { height: 1, flexShrink: 0 }, /* @__PURE__ */ import_react43.default.createElement(StatusMessageDisplay, { message: state.statusMessage })), /* @__PURE__ */ import_react43.default.createElement(
23151
+ FooterBar,
23152
+ {
23153
+ width: termWidth,
23154
+ actions: detailActions(detailFooterCtx),
23155
+ stateChip: detailColumnLabel
23494
23156
  }
23495
- ), /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexGrow: 1 }), /* @__PURE__ */ import_react45.default.createElement(Box_default, { height: 1 }, /* @__PURE__ */ import_react45.default.createElement(StatusMessageDisplay, { message: state.statusMessage })), /* @__PURE__ */ import_react45.default.createElement(FooterBar, { width: termWidth, actions: detailActions(selectedTask), stateChip: columnLabel }));
23157
+ ));
23496
23158
  }
23497
- const detailPaneWidth = Math.floor(termWidth * LAYOUT.DETAIL_PANE_FRACTION);
23498
23159
  const listWidth = detailOpen ? termWidth - detailPaneWidth : termWidth;
23499
- const footerActions = detailOpen ? detailActions(selectedTask) : browseActions(selectedTask);
23500
- return /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight }, /* @__PURE__ */ import_react45.default.createElement(
23160
+ const footerActions = detailOpen ? detailActions(detailFooterCtx) : browseActions(selectedTask, doneView);
23161
+ return /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "column", width: termWidth, height: termHeight }, /* @__PURE__ */ import_react43.default.createElement(
23501
23162
  HeaderBar,
23502
23163
  {
23503
23164
  title: boardTitle,
@@ -23508,9 +23169,10 @@ function BrainfileTUI({ filePath, width, height }) {
23508
23169
  filterActive: state.mode === "filter" || hasFilter,
23509
23170
  matchCount,
23510
23171
  totalCount,
23511
- panelLabel: state.activePanel === "rules" ? "rules" : state.activePanel === "logs" ? "logs" : void 0
23172
+ panelLabel: doneView ? "done" : void 0,
23173
+ activeType: typeFilterActive ? activeTypeFilter : "all"
23512
23174
  }
23513
- ), /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexGrow: 1, flexDirection: "column" }, state.activePanel === "board" ? /* @__PURE__ */ import_react45.default.createElement(
23175
+ ), /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexGrow: 1, flexShrink: 0, flexDirection: "column" }, /* @__PURE__ */ import_react43.default.createElement(
23514
23176
  BoardContent,
23515
23177
  {
23516
23178
  state,
@@ -23521,45 +23183,26 @@ function BrainfileTUI({ filePath, width, height }) {
23521
23183
  detailPaneWidth,
23522
23184
  detailOpen,
23523
23185
  selectedTask,
23524
- parentTask,
23525
- columnLabel,
23186
+ detailTask,
23187
+ detailParent,
23188
+ detailChildren,
23189
+ detailActivity,
23190
+ detailColumnLabel,
23526
23191
  columnName,
23527
23192
  columns: orderedColumns,
23528
23193
  termWidth,
23529
- hasFilter
23530
- }
23531
- ) : null, state.activePanel === "rules" ? /* @__PURE__ */ import_react45.default.createElement(
23532
- RulesPanel,
23533
- {
23534
- rules: state.board.rules,
23535
- activeRuleType: state.activeRuleType,
23536
- selectedRuleIndex: state.selectedRuleIndex,
23537
- viewportHeight,
23538
- termWidth,
23539
- mode: state.mode,
23540
- editText: state.ruleEditText,
23541
- layoutMode
23542
- }
23543
- ) : null, state.activePanel === "logs" ? /* @__PURE__ */ import_react45.default.createElement(
23544
- LogsPanel,
23545
- {
23546
- logs: state.logs,
23547
- selectedIndex: state.selectedLogIndex,
23548
- viewportHeight,
23549
- termWidth,
23550
- expandedIds: state.expandedLogIds,
23551
- mode: state.mode,
23552
- columns: orderedColumns,
23553
- restoreColumnIndex: state.logRestoreColumnIndex,
23554
- layoutMode
23194
+ hasFilter,
23195
+ archived: doneView,
23196
+ typeFilter: typeFilterActive ? activeTypeFilter : void 0,
23197
+ thenEdit: state.addThenEdit
23555
23198
  }
23556
- ) : null), /* @__PURE__ */ import_react45.default.createElement(Box_default, { height: 1 }, /* @__PURE__ */ import_react45.default.createElement(StatusMessageDisplay, { message: state.statusMessage })), /* @__PURE__ */ import_react45.default.createElement(
23199
+ )), /* @__PURE__ */ import_react43.default.createElement(Box_default, { height: 1, flexShrink: 0 }, /* @__PURE__ */ import_react43.default.createElement(StatusMessageDisplay, { message: state.statusMessage })), /* @__PURE__ */ import_react43.default.createElement(
23557
23200
  FooterBar,
23558
23201
  {
23559
23202
  width: termWidth,
23560
- itemCount: state.activePanel === "board" && !detailOpen ? rows.length : void 0,
23561
- actions: state.activePanel === "board" ? footerActions : ["j/k move", "a add", "e edit", "d delete", "1 board", "q quit"],
23562
- stateChip: state.activePanel === "board" ? columnLabel : state.activePanel
23203
+ itemCount: detailOpen ? void 0 : rows.length,
23204
+ actions: footerActions,
23205
+ stateChip: columnLabel
23563
23206
  }
23564
23207
  ));
23565
23208
  }
@@ -23572,80 +23215,96 @@ function BoardContent({
23572
23215
  detailPaneWidth,
23573
23216
  detailOpen,
23574
23217
  selectedTask,
23575
- parentTask,
23576
- columnLabel,
23218
+ detailTask,
23219
+ detailParent,
23220
+ detailChildren,
23221
+ detailActivity,
23222
+ detailColumnLabel,
23577
23223
  columnName,
23578
23224
  columns,
23579
23225
  termWidth,
23580
- hasFilter
23226
+ hasFilter,
23227
+ archived,
23228
+ typeFilter,
23229
+ thenEdit
23581
23230
  }) {
23582
- if (state.mode === "move" && selectedTask) {
23583
- return /* @__PURE__ */ import_react45.default.createElement(
23231
+ const overlayTarget = state.detailPath.length > 0 ? detailTask : selectedTask;
23232
+ if (state.mode === "move" && overlayTarget) {
23233
+ return /* @__PURE__ */ import_react43.default.createElement(
23584
23234
  MoveOverlay,
23585
23235
  {
23586
23236
  columns,
23587
23237
  selectedIndex: state.moveTargetIndex,
23588
- taskId: selectedTask.id,
23589
- taskTitle: selectedTask.title,
23238
+ taskId: overlayTarget.id,
23239
+ taskTitle: overlayTarget.title,
23590
23240
  width: termWidth
23591
23241
  }
23592
23242
  );
23593
23243
  }
23594
- if (state.mode === "delete-confirm" && selectedTask) {
23595
- return /* @__PURE__ */ import_react45.default.createElement(
23244
+ if (state.mode === "delete-confirm" && overlayTarget) {
23245
+ return /* @__PURE__ */ import_react43.default.createElement(
23596
23246
  DeleteConfirmOverlay,
23597
23247
  {
23598
- taskId: selectedTask.id,
23599
- taskTitle: selectedTask.title,
23248
+ taskId: overlayTarget.id,
23249
+ taskTitle: overlayTarget.title,
23600
23250
  width: termWidth
23601
23251
  }
23602
23252
  );
23603
23253
  }
23604
23254
  if (state.mode === "complete-confirm" && state.completeConfirm) {
23605
- return /* @__PURE__ */ import_react45.default.createElement(CompleteConfirmOverlay, { target: state.completeConfirm, width: termWidth });
23255
+ return /* @__PURE__ */ import_react43.default.createElement(CompleteConfirmOverlay, { target: state.completeConfirm, width: termWidth });
23606
23256
  }
23607
23257
  if (state.mode === "add") {
23608
- return /* @__PURE__ */ import_react45.default.createElement(AddOverlay, { title: state.newTaskTitle, columnName });
23258
+ return /* @__PURE__ */ import_react43.default.createElement(AddOverlay, { title: state.newTaskTitle, columnName, thenEdit });
23609
23259
  }
23610
- const list = /* @__PURE__ */ import_react45.default.createElement(
23260
+ const list = /* @__PURE__ */ import_react43.default.createElement(
23611
23261
  DocumentList,
23612
23262
  {
23613
23263
  rows,
23614
23264
  selectedIndex,
23615
23265
  viewportHeight,
23616
23266
  width: listWidth,
23617
- emptyMessage: hasFilter ? "No matches \xB7 esc to clear" : "No documents in this column"
23267
+ archived,
23268
+ emptyMessage: hasFilter ? EMPTY.noMatches : archived ? EMPTY.nothingCompleted : typeFilter ? EMPTY.noType(typeFilter) : EMPTY.emptyColumn
23618
23269
  }
23619
23270
  );
23620
- if (!detailOpen || !selectedTask) return list;
23621
- return /* @__PURE__ */ import_react45.default.createElement(Box_default, { flexDirection: "row", flexGrow: 1 }, /* @__PURE__ */ import_react45.default.createElement(Box_default, { width: listWidth, flexDirection: "column" }, list), /* @__PURE__ */ import_react45.default.createElement(Box_default, { width: detailPaneWidth, flexDirection: "column" }, /* @__PURE__ */ import_react45.default.createElement(
23622
- DetailView,
23623
- {
23624
- task: selectedTask,
23625
- columnLabel,
23626
- width: detailPaneWidth,
23627
- height: viewportHeight,
23628
- selectedSubtaskIndex: state.selectedSubtaskIndex,
23629
- parent: parentTask
23630
- }
23631
- )));
23271
+ if (!detailOpen || !detailTask) return list;
23272
+ return (
23273
+ // flexShrink={0} down this whole chain: detail v2 can render more lines
23274
+ // than the nominal viewport (a doc with children + subtasks + a full
23275
+ // contract + activity easily does, in a modest terminal). Letting yoga
23276
+ // shrink to fit corrupts the layout — rows silently lose height and their
23277
+ // text vanishes — rather than the frame simply growing taller, which ink
23278
+ // and real terminals handle fine.
23279
+ /* @__PURE__ */ import_react43.default.createElement(Box_default, { flexDirection: "row", flexGrow: 1, flexShrink: 0 }, /* @__PURE__ */ import_react43.default.createElement(Box_default, { width: listWidth, flexDirection: "column", flexShrink: 0 }, list), /* @__PURE__ */ import_react43.default.createElement(Box_default, { width: detailPaneWidth, flexDirection: "column", flexShrink: 0 }, /* @__PURE__ */ import_react43.default.createElement(
23280
+ DetailView,
23281
+ {
23282
+ task: detailTask,
23283
+ columnLabel: detailColumnLabel,
23284
+ width: detailPaneWidth,
23285
+ height: viewportHeight,
23286
+ breadcrumb: state.detailPath,
23287
+ parent: detailParent,
23288
+ children: detailChildren,
23289
+ activity: detailActivity,
23290
+ cursor: state.detailCursor,
23291
+ scrollOffset: state.detailScroll
23292
+ }
23293
+ )))
23294
+ );
23632
23295
  }
23633
23296
 
23634
23297
  // src/commands/tui.tsx
23635
23298
  async function tuiCommand(options) {
23636
23299
  const filePath = resolveCliBrainfilePath(options.file);
23637
- if (!fs5.existsSync(filePath)) {
23638
- console.error(`Error: File not found: ${filePath}`);
23639
- console.log("");
23640
- console.log("To create a new brainfile, run:");
23641
- console.log(" brainfile init");
23300
+ if (!fs6.existsSync(filePath)) {
23301
+ console.error(`File not found: ${filePath}`);
23302
+ console.error("Create a board with: brainfile init");
23642
23303
  process.exit(1);
23643
23304
  }
23644
23305
  if (!process.stdin.isTTY || !process.stdin.setRawMode) {
23645
- console.error("Error: Terminal UI requires an interactive terminal");
23646
- console.log("");
23647
- console.log("The TUI cannot run in non-interactive environments.");
23648
- console.log("Please run this command in a standard terminal (not piped or in a non-TTY context).");
23306
+ console.error("The board needs an interactive terminal.");
23307
+ console.error("Run this in a real terminal, not a pipe.");
23649
23308
  process.exit(1);
23650
23309
  }
23651
23310
  if (shouldSuggestV2Migration(filePath)) {
@@ -23655,7 +23314,7 @@ async function tuiCommand(options) {
23655
23314
  console.log("");
23656
23315
  markV2MigrationHintShown(filePath);
23657
23316
  }
23658
- const { waitUntilExit } = render_default(/* @__PURE__ */ import_react46.default.createElement(BrainfileTUI, { filePath }));
23317
+ const { waitUntilExit } = render_default(/* @__PURE__ */ import_react44.default.createElement(BrainfileTUI, { filePath }));
23659
23318
  await waitUntilExit();
23660
23319
  process.stdout.write("\x1B[2J");
23661
23320
  process.stdout.write("\x1B[H");