@tomkapa/tayto 0.1.2 → 0.3.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.
@@ -1,19 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DependencyType,
4
+ TaskLevel,
4
5
  TaskStatus,
5
6
  TaskType,
6
7
  UIDependencyType,
7
8
  isTerminalStatus,
8
9
  logger
9
- } from "./chunk-6NQOFUIQ.js";
10
+ } from "./chunk-FUNYPBWJ.js";
10
11
 
11
12
  // src/tui/index.tsx
12
13
  import { render } from "ink";
13
14
 
14
15
  // src/tui/components/App.tsx
15
- import { useReducer, useEffect, useCallback as useCallback2, useMemo } from "react";
16
- import { Box as Box14, Text as Text14, useInput as useInput5, useApp } from "ink";
16
+ import { useReducer, useEffect as useEffect2, useCallback as useCallback2, useMemo as useMemo2, useState as useState6 } from "react";
17
+ import { Box as Box16, Text as Text16, useInput as useInput6, useApp, useStdout as useStdout4 } from "ink";
17
18
 
18
19
  // src/tui/types.ts
19
20
  var ViewType = {
@@ -24,9 +25,144 @@ var ViewType = {
24
25
  ProjectSelector: "project-selector",
25
26
  ProjectCreate: "project-create",
26
27
  DependencyList: "dependency-list",
28
+ EpicPicker: "epic-picker",
27
29
  Help: "help"
28
30
  };
29
31
 
32
+ // src/tui/theme.ts
33
+ var theme = {
34
+ // Body
35
+ fg: "#1E90FF",
36
+ // dodgerblue
37
+ bg: "black",
38
+ logo: "#FFA500",
39
+ // orange
40
+ // Table
41
+ table: {
42
+ fg: "#00FFFF",
43
+ // aqua
44
+ cursorFg: "black",
45
+ cursorBg: "#00FFFF",
46
+ // aqua
47
+ headerFg: "white",
48
+ markColor: "#98FB98",
49
+ // palegreen
50
+ depHighlightBg: "#1A5276",
51
+ // dark steel blue – non-terminal dep rows
52
+ blockedCursorBg: "#C0392B"
53
+ // strong red – selected row blocked by non-terminal dep
54
+ },
55
+ // Status colors (row-level)
56
+ status: {
57
+ new: "#87CEFA",
58
+ // lightskyblue
59
+ modified: "#ADFF2F",
60
+ // greenyellow
61
+ added: "#1E90FF",
62
+ // dodgerblue
63
+ error: "#FF4500",
64
+ // orangered
65
+ pending: "#FF8C00",
66
+ // darkorange
67
+ highlight: "#00FFFF",
68
+ // aqua
69
+ kill: "#9370DB",
70
+ // mediumpurple
71
+ completed: "#778899"
72
+ // lightslategray
73
+ },
74
+ // Frame / borders
75
+ border: "#1E90FF",
76
+ // dodgerblue
77
+ borderFocus: "#00FFFF",
78
+ // aqua
79
+ // Title
80
+ title: "#00FFFF",
81
+ // aqua
82
+ titleHighlight: "#FF00FF",
83
+ // fuchsia
84
+ titleCounter: "#FFEFD5",
85
+ // papayawhip
86
+ titleFilter: "#2E8B57",
87
+ // seagreen
88
+ // Prompt
89
+ prompt: "#5F9EA0",
90
+ // cadetblue
91
+ promptSuggest: "#1E90FF",
92
+ // dodgerblue
93
+ // Dialog
94
+ dialog: {
95
+ fg: "#1E90FF",
96
+ // dodgerblue
97
+ buttonFg: "black",
98
+ buttonBg: "#1E90FF",
99
+ buttonFocusFg: "white",
100
+ buttonFocusBg: "#FF00FF",
101
+ // fuchsia
102
+ label: "#FF00FF",
103
+ // fuchsia
104
+ field: "#1E90FF"
105
+ },
106
+ // Detail/YAML
107
+ yaml: {
108
+ key: "#4682B4",
109
+ // steelblue
110
+ colon: "white",
111
+ value: "#FFEFD5"
112
+ // papayawhip
113
+ },
114
+ // Crumbs
115
+ crumb: {
116
+ fg: "black",
117
+ bg: "#4682B4",
118
+ // steelblue
119
+ activeBg: "#FFA500"
120
+ // orange
121
+ },
122
+ // Flash
123
+ flash: {
124
+ info: "#FFDEAD",
125
+ // navajowhite
126
+ warn: "#FFA500",
127
+ // orange
128
+ error: "#FF4500"
129
+ // orangered
130
+ },
131
+ // Menu / key hints
132
+ menu: {
133
+ key: "#1E90FF",
134
+ // dodgerblue
135
+ numKey: "#FF00FF",
136
+ // fuchsia
137
+ desc: "white"
138
+ }
139
+ };
140
+
141
+ // src/tui/constants.ts
142
+ var STATUS_VALUES = Object.values(TaskStatus);
143
+ var TYPE_VALUES = Object.values(TaskType);
144
+ var PAGE_SIZE = 20;
145
+ var STATUS_COLOR = {
146
+ [TaskStatus.Backlog]: theme.status.completed,
147
+ [TaskStatus.Todo]: theme.status.new,
148
+ [TaskStatus.InProgress]: theme.status.pending,
149
+ [TaskStatus.Review]: theme.status.modified,
150
+ [TaskStatus.Done]: theme.status.added,
151
+ [TaskStatus.Cancelled]: theme.status.kill
152
+ };
153
+ var TYPE_COLOR = {
154
+ [TaskType.Epic]: theme.status.modified,
155
+ [TaskType.Story]: theme.status.highlight,
156
+ [TaskType.TechDebt]: theme.status.pending,
157
+ [TaskType.Bug]: theme.status.error
158
+ };
159
+ var DEP_TYPE_LABEL = {
160
+ [DependencyType.Blocks]: "blocks",
161
+ [DependencyType.RelatesTo]: "relates-to",
162
+ [DependencyType.Duplicates]: "duplicates",
163
+ [UIDependencyType.BlockedBy]: "blocked-by"
164
+ };
165
+
30
166
  // src/tui/state.ts
31
167
  var initialState = {
32
168
  activeView: ViewType.TaskList,
@@ -51,7 +187,13 @@ var initialState = {
51
187
  depSelectedIndex: 0,
52
188
  isAddingDep: false,
53
189
  addDepInput: "",
54
- focusedPanel: "list"
190
+ focusedPanel: "list",
191
+ detailScrollOffset: 0,
192
+ epics: [],
193
+ epicSelectedIndex: 0,
194
+ selectedEpicIds: /* @__PURE__ */ new Set(),
195
+ isEpicReordering: false,
196
+ epicReorderSnapshot: null
55
197
  };
56
198
  function appReducer(state, action) {
57
199
  switch (action.type) {
@@ -111,14 +253,19 @@ function appReducer(state, action) {
111
253
  case "MOVE_CURSOR": {
112
254
  const maxIndex = Math.max(0, state.tasks.length - 1);
113
255
  const newIndex = action.direction === "up" ? Math.max(0, state.selectedIndex - 1) : Math.min(maxIndex, state.selectedIndex + 1);
114
- return { ...state, selectedIndex: newIndex };
256
+ return { ...state, selectedIndex: newIndex, detailScrollOffset: 0 };
257
+ }
258
+ case "PAGE_CURSOR": {
259
+ const maxIndex = Math.max(0, state.tasks.length - 1);
260
+ const newIndex = action.direction === "up" ? Math.max(0, state.selectedIndex - PAGE_SIZE) : Math.min(maxIndex, state.selectedIndex + PAGE_SIZE);
261
+ return { ...state, selectedIndex: newIndex, detailScrollOffset: 0 };
115
262
  }
116
263
  case "SET_CURSOR": {
117
264
  const maxIndex = Math.max(0, state.tasks.length - 1);
118
265
  return { ...state, selectedIndex: Math.max(0, Math.min(action.index, maxIndex)) };
119
266
  }
120
267
  case "SELECT_TASK":
121
- return { ...state, selectedTask: action.task };
268
+ return { ...state, selectedTask: action.task, detailScrollOffset: 0 };
122
269
  case "SET_FORM_DATA":
123
270
  return { ...state, formData: action.data };
124
271
  case "ENTER_REORDER":
@@ -181,151 +328,98 @@ function appReducer(state, action) {
181
328
  return { ...state, addDepInput: action.input };
182
329
  case "SET_PANEL_FOCUS":
183
330
  return { ...state, focusedPanel: action.panel };
331
+ case "DETAIL_SCROLL":
332
+ return {
333
+ ...state,
334
+ detailScrollOffset: action.direction === "up" ? Math.max(0, state.detailScrollOffset - 1) : state.detailScrollOffset + 1
335
+ };
336
+ case "DETAIL_RESET_SCROLL":
337
+ return { ...state, detailScrollOffset: 0 };
338
+ case "SET_EPICS":
339
+ return {
340
+ ...state,
341
+ epics: action.epics,
342
+ epicSelectedIndex: Math.min(state.epicSelectedIndex, Math.max(0, action.epics.length - 1))
343
+ };
344
+ case "EPIC_MOVE_CURSOR": {
345
+ if (state.epics.length === 0) return state;
346
+ const maxIdx = Math.max(0, state.epics.length - 1);
347
+ const newIdx = action.direction === "up" ? Math.max(0, state.epicSelectedIndex - 1) : Math.min(maxIdx, state.epicSelectedIndex + 1);
348
+ return { ...state, epicSelectedIndex: newIdx };
349
+ }
350
+ case "TOGGLE_EPIC": {
351
+ const next = new Set(state.selectedEpicIds);
352
+ if (next.has(action.epicId)) {
353
+ next.delete(action.epicId);
354
+ } else {
355
+ next.add(action.epicId);
356
+ }
357
+ return { ...state, selectedEpicIds: next, selectedIndex: 0 };
358
+ }
359
+ case "CLEAR_EPIC_SELECTION":
360
+ return { ...state, selectedEpicIds: /* @__PURE__ */ new Set(), selectedIndex: 0 };
361
+ case "ENTER_EPIC_REORDER":
362
+ return {
363
+ ...state,
364
+ isEpicReordering: true,
365
+ epicReorderSnapshot: [...state.epics]
366
+ };
367
+ case "EPIC_REORDER_MOVE": {
368
+ if (!state.isEpicReordering) return state;
369
+ const idx = state.epicSelectedIndex;
370
+ const epics = [...state.epics];
371
+ const swapIdx = action.direction === "up" ? idx - 1 : idx + 1;
372
+ if (swapIdx < 0 || swapIdx >= epics.length) return state;
373
+ const current = epics[idx];
374
+ const swap = epics[swapIdx];
375
+ if (!current || !swap) return state;
376
+ epics[idx] = swap;
377
+ epics[swapIdx] = current;
378
+ return {
379
+ ...state,
380
+ epics,
381
+ epicSelectedIndex: swapIdx
382
+ };
383
+ }
384
+ case "EXIT_EPIC_REORDER": {
385
+ if (!action.save && state.epicReorderSnapshot) {
386
+ return {
387
+ ...state,
388
+ isEpicReordering: false,
389
+ epics: state.epicReorderSnapshot,
390
+ epicReorderSnapshot: null
391
+ };
392
+ }
393
+ return {
394
+ ...state,
395
+ isEpicReordering: false,
396
+ epicReorderSnapshot: null
397
+ };
398
+ }
184
399
  }
185
400
  }
186
401
 
187
- // src/tui/theme.ts
188
- var theme = {
189
- // Body
190
- fg: "#1E90FF",
191
- // dodgerblue
192
- bg: "black",
193
- logo: "#FFA500",
194
- // orange
195
- // Table
196
- table: {
197
- fg: "#00FFFF",
198
- // aqua
199
- cursorFg: "black",
200
- cursorBg: "#00FFFF",
201
- // aqua
202
- headerFg: "white",
203
- markColor: "#98FB98",
204
- // palegreen
205
- depHighlightBg: "#1A5276",
206
- // dark steel blue – non-terminal dep rows
207
- blockedCursorBg: "#C0392B"
208
- // strong red – selected row blocked by non-terminal dep
209
- },
210
- // Status colors (row-level)
211
- status: {
212
- new: "#87CEFA",
213
- // lightskyblue
214
- modified: "#ADFF2F",
215
- // greenyellow
216
- added: "#1E90FF",
217
- // dodgerblue
218
- error: "#FF4500",
219
- // orangered
220
- pending: "#FF8C00",
221
- // darkorange
222
- highlight: "#00FFFF",
223
- // aqua
224
- kill: "#9370DB",
225
- // mediumpurple
226
- completed: "#778899"
227
- // lightslategray
228
- },
229
- // Frame / borders
230
- border: "#1E90FF",
231
- // dodgerblue
232
- borderFocus: "#00FFFF",
233
- // aqua
234
- // Title
235
- title: "#00FFFF",
236
- // aqua
237
- titleHighlight: "#FF00FF",
238
- // fuchsia
239
- titleCounter: "#FFEFD5",
240
- // papayawhip
241
- titleFilter: "#2E8B57",
242
- // seagreen
243
- // Prompt
244
- prompt: "#5F9EA0",
245
- // cadetblue
246
- promptSuggest: "#1E90FF",
247
- // dodgerblue
248
- // Dialog
249
- dialog: {
250
- fg: "#1E90FF",
251
- // dodgerblue
252
- buttonFg: "black",
253
- buttonBg: "#1E90FF",
254
- buttonFocusFg: "white",
255
- buttonFocusBg: "#FF00FF",
256
- // fuchsia
257
- label: "#FF00FF",
258
- // fuchsia
259
- field: "#1E90FF"
260
- },
261
- // Detail/YAML
262
- yaml: {
263
- key: "#4682B4",
264
- // steelblue
265
- colon: "white",
266
- value: "#FFEFD5"
267
- // papayawhip
268
- },
269
- // Crumbs
270
- crumb: {
271
- fg: "black",
272
- bg: "#4682B4",
273
- // steelblue
274
- activeBg: "#FFA500"
275
- // orange
276
- },
277
- // Flash
278
- flash: {
279
- info: "#FFDEAD",
280
- // navajowhite
281
- warn: "#FFA500",
282
- // orange
283
- error: "#FF4500"
284
- // orangered
285
- },
286
- // Menu / key hints
287
- menu: {
288
- key: "#1E90FF",
289
- // dodgerblue
290
- numKey: "#FF00FF",
291
- // fuchsia
292
- desc: "white"
293
- }
294
- };
295
-
296
- // src/tui/constants.ts
297
- var STATUS_VALUES = Object.values(TaskStatus);
298
- var TYPE_VALUES = Object.values(TaskType);
299
- var STATUS_COLOR = {
300
- [TaskStatus.Backlog]: theme.status.completed,
301
- [TaskStatus.Todo]: theme.status.new,
302
- [TaskStatus.InProgress]: theme.status.pending,
303
- [TaskStatus.Review]: theme.status.modified,
304
- [TaskStatus.Done]: theme.status.added,
305
- [TaskStatus.Cancelled]: theme.status.kill
306
- };
307
- var TYPE_COLOR = {
308
- [TaskType.Story]: theme.status.highlight,
309
- [TaskType.TechDebt]: theme.status.pending,
310
- [TaskType.Bug]: theme.status.error
311
- };
312
- var DEP_TYPE_LABEL = {
313
- [DependencyType.Blocks]: "blocks",
314
- [DependencyType.RelatesTo]: "relates-to",
315
- [DependencyType.Duplicates]: "duplicates",
316
- [UIDependencyType.BlockedBy]: "blocked-by"
317
- };
318
-
319
402
  // src/tui/components/Header.tsx
320
403
  import { Box, Text } from "ink";
321
404
  import { jsx, jsxs } from "react/jsx-runtime";
322
- function getKeyHints(view, isSearchActive) {
405
+ function getKeyHints(view, isSearchActive, focusedPanel) {
323
406
  if (isSearchActive) {
324
407
  return [
325
408
  { key: "enter", desc: "apply" },
326
409
  { key: "esc", desc: "cancel" }
327
410
  ];
328
411
  }
412
+ if (view === "task-list" && focusedPanel === "epic") {
413
+ return [
414
+ { key: "j/k", desc: "nav" },
415
+ { key: "space", desc: "toggle" },
416
+ { key: "\u2190", desc: "reorder" },
417
+ { key: "0", desc: "clear" },
418
+ { key: "tab", desc: "tasks" },
419
+ { key: "?", desc: "help" },
420
+ { key: "q", desc: "quit" }
421
+ ];
422
+ }
329
423
  if (view === "task-list") {
330
424
  return [
331
425
  { key: "enter", desc: "view" },
@@ -333,11 +427,15 @@ function getKeyHints(view, isSearchActive) {
333
427
  { key: "e", desc: "edit" },
334
428
  { key: "d", desc: "del" },
335
429
  { key: "s", desc: "status" },
430
+ { key: "a", desc: "assign" },
431
+ { key: "A", desc: "unassign" },
336
432
  { key: "\u2190", desc: "reorder" },
337
433
  { key: "/", desc: "search" },
338
434
  { key: "p", desc: "project" },
339
435
  { key: "f", desc: "status-f" },
340
436
  { key: "t", desc: "type-f" },
437
+ { key: "PgDn/Up", desc: "page" },
438
+ { key: "tab", desc: "panel" },
341
439
  { key: "?", desc: "help" },
342
440
  { key: "q", desc: "quit" }
343
441
  ];
@@ -362,10 +460,10 @@ function getKeyHints(view, isSearchActive) {
362
460
  function Header({ state }) {
363
461
  const projectName = state.activeProject?.name ?? "none";
364
462
  const taskCount = state.tasks.length;
365
- const hints = getKeyHints(state.activeView, state.isSearchActive);
463
+ const hints = getKeyHints(state.activeView, state.isSearchActive, state.focusedPanel);
366
464
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
367
465
  /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
368
- /* @__PURE__ */ jsx(Text, { color: theme.logo, bold: true, children: "TaskCLI" }),
466
+ /* @__PURE__ */ jsx(Text, { color: theme.logo, bold: true, children: "Tayto" }),
369
467
  /* @__PURE__ */ jsx(Text, { color: theme.logo, children: "Project:" }),
370
468
  /* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: projectName }),
371
469
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
@@ -426,7 +524,6 @@ function FlashMessage({ message, level }) {
426
524
  // src/tui/components/TaskList.tsx
427
525
  import { Box as Box4, Text as Text4 } from "ink";
428
526
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
429
- var PAGE_SIZE = 20;
430
527
  var COL = {
431
528
  rank: 5,
432
529
  type: 12,
@@ -443,7 +540,8 @@ function TaskList({
443
540
  nonTerminalBlockerIds,
444
541
  nonTerminalDependentIds,
445
542
  isSelectedBlocked,
446
- isFocused = true
543
+ isFocused = true,
544
+ epicFilterActive = false
447
545
  }) {
448
546
  const currentPage = Math.floor(selectedIndex / PAGE_SIZE);
449
547
  const viewStart = currentPage * PAGE_SIZE;
@@ -478,6 +576,7 @@ function TaskList({
478
576
  " ",
479
577
  "REORDER"
480
578
  ] }),
579
+ epicFilterActive && /* @__PURE__ */ jsx4(Text4, { color: theme.titleHighlight, children: " [epic]" }),
481
580
  filterText && /* @__PURE__ */ jsxs2(Text4, { color: theme.titleFilter, children: [
482
581
  " /",
483
582
  filterText
@@ -495,7 +594,7 @@ function TaskList({
495
594
  /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "STATUS".padEnd(COL.status) }),
496
595
  /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "NAME" })
497
596
  ] }),
498
- tasks.length === 0 ? /* @__PURE__ */ jsx4(Box4, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx4(Text4, { color: theme.fg, children: "No tasks found. Press 'c' to create one." }) }) : visibleTasks.map((task, i) => {
597
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", children: tasks.length === 0 ? /* @__PURE__ */ jsx4(Box4, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx4(Text4, { color: theme.fg, children: "No tasks found. Press 'c' to create one." }) }) : visibleTasks.map((task, i) => {
499
598
  const actualIndex = viewStart + i;
500
599
  const isSelected = actualIndex === selectedIndex;
501
600
  const isNonTerminalBlocker = nonTerminalBlockerIds.has(task.id);
@@ -529,8 +628,7 @@ function TaskList({
529
628
  /* @__PURE__ */ jsx4(Text4, { color: STATUS_COLOR[task.status] ?? rowColor, children: task.status.padEnd(COL.status) }),
530
629
  /* @__PURE__ */ jsx4(Text4, { color: rowColor, children: task.name })
531
630
  ] }, task.id);
532
- }),
533
- /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
631
+ }) }),
534
632
  tasks.length > PAGE_SIZE && /* @__PURE__ */ jsx4(Box4, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs2(Text4, { dimColor: true, children: [
535
633
  "[",
536
634
  viewStart + 1,
@@ -546,7 +644,9 @@ function TaskList({
546
644
  }
547
645
 
548
646
  // src/tui/components/TaskDetail.tsx
549
- import { Box as Box6, Text as Text6 } from "ink";
647
+ import { useMemo } from "react";
648
+ import { Box as Box6, Text as Text6, useStdout } from "ink";
649
+ import chalk2 from "chalk";
550
650
 
551
651
  // src/tui/components/Markdown.tsx
552
652
  import { Text as Text5, Box as Box5 } from "ink";
@@ -638,15 +738,10 @@ function createRenderer() {
638
738
  );
639
739
  }
640
740
  var marked = createRenderer();
641
- function Markdown({ content }) {
642
- if (!content.trim()) {
643
- return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No content" });
644
- }
741
+ function renderMarkdown(content) {
742
+ if (!content.trim()) return "";
645
743
  const rendered = marked.parse(content);
646
- if (typeof rendered !== "string") {
647
- return /* @__PURE__ */ jsx5(Text5, { children: content });
648
- }
649
- return /* @__PURE__ */ jsx5(Text5, { children: rendered.trimEnd() });
744
+ return typeof rendered === "string" ? rendered.trimEnd() : content;
650
745
  }
651
746
  function MermaidHint({ content }) {
652
747
  const blocks = extractMermaidBlocks(content);
@@ -675,93 +770,109 @@ function openAllMermaidDiagrams(content) {
675
770
 
676
771
  // src/tui/components/TaskDetail.tsx
677
772
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
678
- function Field({ label, value }) {
679
- return /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
680
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.key, bold: true, children: label }),
681
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.colon, children: ": " }),
682
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: value })
683
- ] });
773
+ var PAD = " ";
774
+ function field(label, value) {
775
+ return PAD + chalk2.hex(theme.yaml.key).bold(label) + chalk2.hex(theme.yaml.colon)(": ") + chalk2.hex(theme.yaml.value)(value);
776
+ }
777
+ function sectionHeader(title) {
778
+ return PAD + chalk2.hex(theme.title).bold(`--- ${title} ---`);
779
+ }
780
+ function markdownLines(content) {
781
+ const rendered = renderMarkdown(content);
782
+ if (!rendered) return [];
783
+ return rendered.split("\n").map((line) => PAD + line);
684
784
  }
785
+ function buildContentLines(task, blockers, dependents, related, duplicates) {
786
+ const lines = [];
787
+ lines.push(field("id", task.id));
788
+ lines.push(field("type", task.type));
789
+ lines.push(field("status", task.status));
790
+ lines.push(field("created", new Date(task.createdAt).toLocaleString()));
791
+ lines.push(field("updated", new Date(task.updatedAt).toLocaleString()));
792
+ if (task.parentId) lines.push(field("parent", task.parentId));
793
+ const hasDeps = blockers.length > 0 || dependents.length > 0 || related.length > 0 || duplicates.length > 0;
794
+ if (hasDeps) {
795
+ lines.push(sectionHeader("dependencies"));
796
+ if (blockers.length > 0) {
797
+ lines.push(
798
+ PAD + chalk2.hex(theme.status.error).bold("blocked by: ") + chalk2.hex(theme.yaml.value)(blockers.map((t) => t.id).join(", "))
799
+ );
800
+ }
801
+ if (dependents.length > 0) {
802
+ lines.push(
803
+ PAD + chalk2.hex(theme.status.added).bold("blocks: ") + chalk2.hex(theme.yaml.value)(dependents.map((t) => t.id).join(", "))
804
+ );
805
+ }
806
+ if (related.length > 0) {
807
+ lines.push(
808
+ PAD + chalk2.hex(theme.status.new).bold("relates to: ") + chalk2.hex(theme.yaml.value)(related.map((t) => t.id).join(", "))
809
+ );
810
+ }
811
+ if (duplicates.length > 0) {
812
+ lines.push(
813
+ PAD + chalk2.hex(theme.status.pending).bold("duplicates: ") + chalk2.hex(theme.yaml.value)(duplicates.map((t) => t.id).join(", "))
814
+ );
815
+ }
816
+ lines.push(PAD + chalk2.dim("press D to manage dependencies"));
817
+ }
818
+ lines.push(sectionHeader("description"));
819
+ if (task.description.trim()) {
820
+ lines.push(...markdownLines(task.description));
821
+ } else {
822
+ lines.push(PAD + chalk2.dim("No description"));
823
+ }
824
+ if (task.technicalNotes.trim()) {
825
+ lines.push(sectionHeader("technical notes"));
826
+ lines.push(...markdownLines(task.technicalNotes));
827
+ }
828
+ if (task.additionalRequirements.trim()) {
829
+ lines.push(sectionHeader("requirements"));
830
+ lines.push(...markdownLines(task.additionalRequirements));
831
+ }
832
+ return lines;
833
+ }
834
+ var CHROME_LINES = 8;
685
835
  function TaskDetail({
686
836
  task,
687
- blockers,
688
- dependents,
689
- related,
690
- duplicates,
691
- isFocused = true
837
+ blockers = [],
838
+ dependents = [],
839
+ related = [],
840
+ duplicates = [],
841
+ isFocused = true,
842
+ scrollOffset = 0
692
843
  }) {
844
+ const { stdout } = useStdout();
693
845
  const allText = `${task.description}
694
846
  ${task.technicalNotes}
695
847
  ${task.additionalRequirements}`;
848
+ const contentLines = useMemo(
849
+ () => buildContentLines(task, blockers, dependents, related, duplicates),
850
+ [task, blockers, dependents, related, duplicates]
851
+ );
852
+ const viewportHeight = Math.max(1, (stdout.rows > 0 ? stdout.rows : 24) - CHROME_LINES);
853
+ const visibleLines = contentLines.slice(scrollOffset, scrollOffset + viewportHeight);
696
854
  return /* @__PURE__ */ jsxs4(
697
855
  Box6,
698
856
  {
699
857
  flexDirection: "column",
700
858
  flexGrow: 1,
701
859
  borderStyle: "bold",
702
- borderColor: isFocused ? theme.borderFocus : theme.border,
703
- children: [
704
- /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
705
- /* @__PURE__ */ jsxs4(Text6, { color: theme.title, bold: true, children: [
706
- " ",
707
- "detail"
708
- ] }),
709
- /* @__PURE__ */ jsx6(Text6, { color: theme.fg, children: "(" }),
710
- /* @__PURE__ */ jsx6(Text6, { color: theme.titleHighlight, bold: true, children: task.name }),
711
- /* @__PURE__ */ jsx6(Text6, { color: theme.fg, children: ")" })
712
- ] }),
713
- /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
714
- /* @__PURE__ */ jsx6(Field, { label: "id", value: task.id }),
715
- /* @__PURE__ */ jsx6(Field, { label: "type", value: task.type }),
716
- /* @__PURE__ */ jsx6(Field, { label: "status", value: task.status }),
717
- /* @__PURE__ */ jsx6(Field, { label: "created", value: new Date(task.createdAt).toLocaleString() }),
718
- /* @__PURE__ */ jsx6(Field, { label: "updated", value: new Date(task.updatedAt).toLocaleString() }),
719
- task.parentId && /* @__PURE__ */ jsx6(Field, { label: "parent", value: task.parentId })
720
- ] }),
721
- (blockers && blockers.length > 0 || dependents && dependents.length > 0 || related && related.length > 0 || duplicates && duplicates.length > 0) && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
722
- /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- dependencies ---" }),
723
- blockers && blockers.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
724
- /* @__PURE__ */ jsxs4(Text6, { color: theme.status.error, bold: true, children: [
725
- "blocked by:",
726
- " "
727
- ] }),
728
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: blockers.map((t) => t.id).join(", ") })
729
- ] }),
730
- dependents && dependents.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
731
- /* @__PURE__ */ jsxs4(Text6, { color: theme.status.added, bold: true, children: [
732
- "blocks:",
733
- " "
734
- ] }),
735
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: dependents.map((t) => t.id).join(", ") })
736
- ] }),
737
- related && related.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
738
- /* @__PURE__ */ jsxs4(Text6, { color: theme.status.new, bold: true, children: [
739
- "relates to:",
740
- " "
741
- ] }),
742
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: related.map((t) => t.id).join(", ") })
743
- ] }),
744
- duplicates && duplicates.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
745
- /* @__PURE__ */ jsxs4(Text6, { color: theme.status.pending, bold: true, children: [
746
- "duplicates:",
747
- " "
748
- ] }),
749
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: duplicates.map((t) => t.id).join(", ") })
860
+ borderColor: isFocused ? theme.borderFocus : theme.border,
861
+ children: [
862
+ /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
863
+ /* @__PURE__ */ jsxs4(Text6, { color: theme.title, bold: true, children: [
864
+ " ",
865
+ "detail"
750
866
  ] }),
751
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press D to manage dependencies" })
752
- ] }),
753
- /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
754
- /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- description ---" }),
755
- task.description.trim() ? /* @__PURE__ */ jsx6(Markdown, { content: task.description }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No description" })
756
- ] }),
757
- task.technicalNotes.trim() && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
758
- /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- technical notes ---" }),
759
- /* @__PURE__ */ jsx6(Markdown, { content: task.technicalNotes })
760
- ] }),
761
- task.additionalRequirements.trim() && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
762
- /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- requirements ---" }),
763
- /* @__PURE__ */ jsx6(Markdown, { content: task.additionalRequirements })
867
+ /* @__PURE__ */ jsx6(Text6, { color: theme.fg, children: "(" }),
868
+ /* @__PURE__ */ jsx6(Text6, { color: theme.titleHighlight, bold: true, children: task.name }),
869
+ /* @__PURE__ */ jsx6(Text6, { color: theme.fg, children: ")" }),
870
+ scrollOffset > 0 && /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
871
+ " \u2191",
872
+ scrollOffset
873
+ ] })
764
874
  ] }),
875
+ /* @__PURE__ */ jsx6(Text6, { children: visibleLines.join("\n") }),
765
876
  /* @__PURE__ */ jsx6(Box6, { flexGrow: 1 }),
766
877
  /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(MermaidHint, { content: allText }) })
767
878
  ]
@@ -808,7 +919,7 @@ function openInEditor(content, filename) {
808
919
 
809
920
  // src/tui/components/TaskPicker.tsx
810
921
  import { useState } from "react";
811
- import { Box as Box7, Text as Text7, useInput, useStdout } from "ink";
922
+ import { Box as Box7, Text as Text7, useInput, useStdout as useStdout2 } from "ink";
812
923
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
813
924
  var DEP_TYPE_VALUES = Object.values(UIDependencyType);
814
925
  var DEP_TYPE_COLOR = {
@@ -818,7 +929,7 @@ var DEP_TYPE_COLOR = {
818
929
  [UIDependencyType.BlockedBy]: theme.status.modified
819
930
  };
820
931
  function TaskPicker({ tasks, excludeIds, initialSelection, onConfirm, onCancel }) {
821
- const { stdout } = useStdout();
932
+ const { stdout } = useStdout2();
822
933
  const termHeight = stdout.rows > 0 ? stdout.rows : 24;
823
934
  const maxVisible = Math.max(3, termHeight - 12);
824
935
  const [searchQuery, setSearchQuery] = useState("");
@@ -1041,13 +1152,13 @@ function TaskForm({ editingTask, allTasks, initialDeps, onSave, onCancel }) {
1041
1152
  const [pickedDeps, setPickedDeps] = useState2(initialDeps ?? []);
1042
1153
  const currentField = FIELDS[focusIndex];
1043
1154
  const launchEditor = useCallback(
1044
- (field) => {
1155
+ (field2) => {
1045
1156
  setEditorActive(true);
1046
1157
  setTimeout(() => {
1047
- const content = values[field.key] ?? "";
1048
- const result = openInEditor(content, field.editorFilename ?? `${field.key}.md`);
1158
+ const content = values[field2.key] ?? "";
1159
+ const result = openInEditor(content, field2.editorFilename ?? `${field2.key}.md`);
1049
1160
  if (result !== null) {
1050
- setValues((v) => ({ ...v, [field.key]: result }));
1161
+ setValues((v) => ({ ...v, [field2.key]: result }));
1051
1162
  }
1052
1163
  setEditorActive(false);
1053
1164
  }, 50);
@@ -1155,37 +1266,37 @@ function TaskForm({ editingTask, allTasks, initialDeps, onSave, onCancel }) {
1155
1266
  " ",
1156
1267
  isEdit ? "edit" : "create"
1157
1268
  ] }) }),
1158
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", paddingX: 1, paddingY: 0, children: FIELDS.map((field, i) => {
1269
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", paddingX: 1, paddingY: 0, children: FIELDS.map((field2, i) => {
1159
1270
  const isFocused = i === focusIndex;
1160
- const value = field.key === "dependsOn" ? depSummary : values[field.key] ?? "";
1271
+ const value = field2.key === "dependsOn" ? depSummary : values[field2.key] ?? "";
1161
1272
  const displayValue = value;
1162
1273
  return /* @__PURE__ */ jsxs6(Box8, { gap: 1, children: [
1163
1274
  /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.dialog.label : theme.yaml.key, bold: isFocused, children: [
1164
1275
  isFocused ? ">" : " ",
1165
1276
  " ",
1166
- field.label.padEnd(14)
1277
+ field2.label.padEnd(14)
1167
1278
  ] }),
1168
- field.type === "inline" && /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1279
+ field2.type === "inline" && /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1169
1280
  displayValue,
1170
1281
  isFocused ? /* @__PURE__ */ jsx8(Text8, { color: theme.titleHighlight, children: "_" }) : ""
1171
1282
  ] }),
1172
- field.type === "picker" && /* @__PURE__ */ jsxs6(Text8, { children: [
1283
+ field2.type === "picker" && /* @__PURE__ */ jsxs6(Text8, { children: [
1173
1284
  displayValue ? /* @__PURE__ */ jsx8(Text8, { color: theme.status.added, children: displayValue.length > 60 ? displayValue.slice(0, 60) + "..." : displayValue }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: isFocused ? "press enter to select" : "none" }),
1174
1285
  isFocused && /* @__PURE__ */ jsx8(Text8, { color: theme.menu.key, children: " [enter: open picker]" })
1175
1286
  ] }),
1176
- field.type === "editor" && /* @__PURE__ */ jsxs6(Text8, { children: [
1287
+ field2.type === "editor" && /* @__PURE__ */ jsxs6(Text8, { children: [
1177
1288
  displayValue ? /* @__PURE__ */ jsxs6(Text8, { color: theme.status.added, children: [
1178
1289
  displayValue.split("\n")[0]?.slice(0, 50),
1179
1290
  displayValue.length > 50 || displayValue.includes("\n") ? "..." : ""
1180
1291
  ] }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: isFocused ? "press enter" : "empty" }),
1181
1292
  isFocused && /* @__PURE__ */ jsx8(Text8, { color: theme.menu.key, children: " [enter: $EDITOR]" })
1182
1293
  ] }),
1183
- field.type === "select" && /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1294
+ field2.type === "select" && /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1184
1295
  isFocused ? "< " : " ",
1185
1296
  displayValue,
1186
1297
  isFocused ? " >" : ""
1187
1298
  ] })
1188
- ] }, field.key);
1299
+ ] }, field2.key);
1189
1300
  }) }),
1190
1301
  /* @__PURE__ */ jsx8(Box8, { flexGrow: 1 }),
1191
1302
  /* @__PURE__ */ jsx8(Box8, { paddingX: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "tab: next | shift+tab: prev | ctrl+s: save | esc: cancel" }) }),
@@ -1353,26 +1464,26 @@ function ProjectForm({ onSave, onCancel }) {
1353
1464
  " ",
1354
1465
  "new project"
1355
1466
  ] }) }),
1356
- /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 0, children: FIELDS2.map((field, i) => {
1467
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 0, children: FIELDS2.map((field2, i) => {
1357
1468
  const isFocused = i === focusIndex;
1358
- const value = values[field.key] ?? "";
1469
+ const value = values[field2.key] ?? "";
1359
1470
  return /* @__PURE__ */ jsxs8(Box10, { gap: 1, children: [
1360
1471
  /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.dialog.label : theme.yaml.key, bold: isFocused, children: [
1361
1472
  isFocused ? ">" : " ",
1362
1473
  " ",
1363
- field.label.padEnd(14)
1474
+ field2.label.padEnd(14)
1364
1475
  ] }),
1365
- field.type === "inline" && /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1476
+ field2.type === "inline" && /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1366
1477
  value,
1367
1478
  isFocused ? /* @__PURE__ */ jsx10(Text10, { color: theme.titleHighlight, children: "_" }) : "",
1368
- field.key === "key" && !value && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: isFocused ? " (auto from name)" : "" })
1479
+ field2.key === "key" && !value && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: isFocused ? " (auto from name)" : "" })
1369
1480
  ] }),
1370
- field.type === "toggle" && /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1481
+ field2.type === "toggle" && /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1371
1482
  isFocused ? "< " : " ",
1372
1483
  value,
1373
1484
  isFocused ? " >" : ""
1374
1485
  ] })
1375
- ] }, field.key);
1486
+ ] }, field2.key);
1376
1487
  }) }),
1377
1488
  /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1378
1489
  /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "tab: next | shift+tab: prev | ctrl+s: save | esc: cancel" }) })
@@ -1388,6 +1499,8 @@ var SECTIONS = [
1388
1499
  keys: [
1389
1500
  ["j/k", "Up/Down"],
1390
1501
  ["g/G", "Top/Bottom"],
1502
+ ["PgDn", "Page down"],
1503
+ ["PgUp", "Page up"],
1391
1504
  ["enter", "View"],
1392
1505
  ["esc", "Back"]
1393
1506
  ]
@@ -1634,8 +1747,256 @@ function DependencyList({
1634
1747
  ] });
1635
1748
  }
1636
1749
 
1637
- // src/tui/components/App.tsx
1750
+ // src/tui/components/EpicPanel.tsx
1751
+ import { Box as Box14, Text as Text14 } from "ink";
1638
1752
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1753
+ var PAGE_SIZE2 = 20;
1754
+ function EpicPanel({
1755
+ epics,
1756
+ selectedIndex,
1757
+ selectedEpicIds,
1758
+ isFocused,
1759
+ isReordering = false
1760
+ }) {
1761
+ const filterActive = selectedEpicIds.size > 0;
1762
+ const currentPage = Math.floor(selectedIndex / PAGE_SIZE2);
1763
+ const viewStart = currentPage * PAGE_SIZE2;
1764
+ const visibleEpics = epics.slice(viewStart, viewStart + PAGE_SIZE2);
1765
+ return /* @__PURE__ */ jsxs12(
1766
+ Box14,
1767
+ {
1768
+ flexDirection: "column",
1769
+ width: 48,
1770
+ borderStyle: "bold",
1771
+ borderColor: isFocused ? theme.borderFocus : theme.border,
1772
+ children: [
1773
+ /* @__PURE__ */ jsxs12(Box14, { children: [
1774
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.title, bold: true, children: [
1775
+ " ",
1776
+ "epics"
1777
+ ] }),
1778
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.titleCounter, bold: true, children: [
1779
+ "[",
1780
+ epics.length,
1781
+ "]"
1782
+ ] }),
1783
+ isReordering && /* @__PURE__ */ jsxs12(Text14, { color: theme.flash.warn, bold: true, children: [
1784
+ " ",
1785
+ "REORDER"
1786
+ ] }),
1787
+ filterActive && /* @__PURE__ */ jsxs12(Text14, { color: theme.titleFilter, children: [
1788
+ " *",
1789
+ selectedEpicIds.size
1790
+ ] })
1791
+ ] }),
1792
+ /* @__PURE__ */ jsx14(Box14, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", children: epics.length === 0 ? /* @__PURE__ */ jsx14(Box14, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No epics" }) }) : visibleEpics.map((epic, i) => {
1793
+ const actualIndex = viewStart + i;
1794
+ const isSelected = actualIndex === selectedIndex && isFocused;
1795
+ const isChecked = selectedEpicIds.has(epic.id);
1796
+ const marker = isChecked ? "[x]" : "[ ]";
1797
+ const statusColor = STATUS_COLOR[epic.status] ?? theme.table.fg;
1798
+ if (isSelected) {
1799
+ const cursorBg = isReordering ? theme.flash.warn : theme.table.cursorBg;
1800
+ return /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { backgroundColor: cursorBg, color: theme.table.cursorFg, bold: true, children: [
1801
+ isReordering ? "~ " : " ",
1802
+ marker,
1803
+ " ",
1804
+ epic.name
1805
+ ] }) }, epic.id);
1806
+ }
1807
+ return /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { color: isChecked ? theme.titleHighlight : statusColor, children: [
1808
+ " ",
1809
+ marker,
1810
+ " ",
1811
+ epic.name
1812
+ ] }) }, epic.id);
1813
+ }) }),
1814
+ epics.length > PAGE_SIZE2 && /* @__PURE__ */ jsx14(Box14, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
1815
+ "[",
1816
+ viewStart + 1,
1817
+ "-",
1818
+ Math.min(viewStart + PAGE_SIZE2, epics.length),
1819
+ "/",
1820
+ epics.length,
1821
+ "]"
1822
+ ] }) })
1823
+ ]
1824
+ }
1825
+ );
1826
+ }
1827
+
1828
+ // src/tui/components/EpicPicker.tsx
1829
+ import { useState as useState5 } from "react";
1830
+ import { Box as Box15, Text as Text15, useInput as useInput5, useStdout as useStdout3 } from "ink";
1831
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1832
+ function EpicPicker({ epics, currentEpicId, onSelect, onCancel }) {
1833
+ const { stdout } = useStdout3();
1834
+ const termHeight = stdout.rows > 0 ? stdout.rows : 24;
1835
+ const maxVisible = Math.max(3, termHeight - 10);
1836
+ const [searchQuery, setSearchQuery] = useState5("");
1837
+ const [isSearching, setIsSearching] = useState5(false);
1838
+ const [cursorIndex, setCursorIndex] = useState5(0);
1839
+ const filtered = epics.filter((e) => {
1840
+ if (!searchQuery.trim()) return true;
1841
+ const q = searchQuery.toLowerCase();
1842
+ return e.id.toLowerCase().includes(q) || e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q);
1843
+ });
1844
+ let viewStart = 0;
1845
+ if (cursorIndex >= viewStart + maxVisible) {
1846
+ viewStart = cursorIndex - maxVisible + 1;
1847
+ }
1848
+ if (cursorIndex < viewStart) {
1849
+ viewStart = cursorIndex;
1850
+ }
1851
+ const visible = filtered.slice(viewStart, viewStart + maxVisible);
1852
+ useInput5((input, key) => {
1853
+ if (isSearching) {
1854
+ if (key.escape) {
1855
+ setIsSearching(false);
1856
+ return;
1857
+ }
1858
+ if (key.return) {
1859
+ setIsSearching(false);
1860
+ setCursorIndex(0);
1861
+ return;
1862
+ }
1863
+ if (key.backspace || key.delete) {
1864
+ setSearchQuery((q) => q.slice(0, -1));
1865
+ setCursorIndex(0);
1866
+ return;
1867
+ }
1868
+ if (input && !key.ctrl && !key.meta) {
1869
+ setSearchQuery((q) => q + input);
1870
+ setCursorIndex(0);
1871
+ return;
1872
+ }
1873
+ return;
1874
+ }
1875
+ if (key.upArrow || input === "k") {
1876
+ setCursorIndex((i) => Math.max(0, i - 1));
1877
+ return;
1878
+ }
1879
+ if (key.downArrow || input === "j") {
1880
+ setCursorIndex((i) => Math.min(filtered.length - 1, i + 1));
1881
+ return;
1882
+ }
1883
+ if (key.return) {
1884
+ const epic = filtered[cursorIndex];
1885
+ if (epic) {
1886
+ onSelect(epic.id);
1887
+ }
1888
+ return;
1889
+ }
1890
+ if (input === "x") {
1891
+ onSelect(null);
1892
+ return;
1893
+ }
1894
+ if (input === "/") {
1895
+ setIsSearching(true);
1896
+ setSearchQuery("");
1897
+ return;
1898
+ }
1899
+ if (key.escape || input === "q") {
1900
+ onCancel();
1901
+ return;
1902
+ }
1903
+ });
1904
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", borderStyle: "bold", borderColor: theme.borderFocus, flexGrow: 1, children: [
1905
+ /* @__PURE__ */ jsxs13(Box15, { gap: 0, children: [
1906
+ /* @__PURE__ */ jsxs13(Text15, { color: theme.title, bold: true, children: [
1907
+ " ",
1908
+ "assign to epic"
1909
+ ] }),
1910
+ /* @__PURE__ */ jsxs13(Text15, { color: theme.titleCounter, bold: true, children: [
1911
+ " ",
1912
+ "[",
1913
+ epics.length,
1914
+ "]"
1915
+ ] })
1916
+ ] }),
1917
+ isSearching ? /* @__PURE__ */ jsxs13(Box15, { borderStyle: "round", borderColor: theme.prompt, paddingX: 1, children: [
1918
+ /* @__PURE__ */ jsx15(Text15, { color: theme.prompt, children: "/" }),
1919
+ /* @__PURE__ */ jsx15(Text15, { color: theme.prompt, children: searchQuery }),
1920
+ /* @__PURE__ */ jsx15(Text15, { color: theme.promptSuggest, children: "_" })
1921
+ ] }) : searchQuery ? /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: theme.titleFilter, children: [
1922
+ "/",
1923
+ searchQuery
1924
+ ] }) }) : null,
1925
+ /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: theme.table.headerFg, bold: true, children: [
1926
+ " ",
1927
+ "ID".padEnd(14),
1928
+ "STATUS".padEnd(14),
1929
+ "NAME"
1930
+ ] }) }),
1931
+ filtered.length === 0 ? /* @__PURE__ */ jsx15(Box15, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "No epics match the filter" }) }) : visible.map((epic, i) => {
1932
+ const actualIndex = viewStart + i;
1933
+ const isCursor = actualIndex === cursorIndex;
1934
+ const isCurrent = epic.id === currentEpicId;
1935
+ const marker = isCurrent ? "* " : " ";
1936
+ const statusColor = STATUS_COLOR[epic.status] ?? theme.table.fg;
1937
+ return /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: isCursor ? /* @__PURE__ */ jsxs13(Text15, { backgroundColor: theme.table.cursorBg, color: theme.table.cursorFg, bold: true, children: [
1938
+ "> ",
1939
+ epic.id.padEnd(14),
1940
+ epic.status.padEnd(14),
1941
+ epic.name
1942
+ ] }) : /* @__PURE__ */ jsxs13(Fragment3, { children: [
1943
+ /* @__PURE__ */ jsx15(Text15, { color: isCurrent ? theme.titleHighlight : theme.table.fg, children: marker }),
1944
+ /* @__PURE__ */ jsx15(Text15, { color: theme.yaml.value, children: epic.id.padEnd(14) }),
1945
+ /* @__PURE__ */ jsx15(Text15, { color: statusColor, children: epic.status.padEnd(14) }),
1946
+ /* @__PURE__ */ jsx15(Text15, { color: isCurrent ? theme.titleHighlight : theme.table.fg, children: epic.name })
1947
+ ] }) }, epic.id);
1948
+ }),
1949
+ /* @__PURE__ */ jsx15(Box15, { flexGrow: 1 }),
1950
+ filtered.length > maxVisible && /* @__PURE__ */ jsx15(Box15, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs13(Text15, { dimColor: true, children: [
1951
+ "[",
1952
+ viewStart + 1,
1953
+ "-",
1954
+ Math.min(viewStart + maxVisible, filtered.length),
1955
+ "/",
1956
+ filtered.length,
1957
+ "]"
1958
+ ] }) }),
1959
+ /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "enter: assign | x: unassign | /: search | esc: cancel" }) })
1960
+ ] });
1961
+ }
1962
+
1963
+ // src/tui/useAutoRefetch.ts
1964
+ import { useEffect, useRef } from "react";
1965
+ import { watchFile, unwatchFile } from "fs";
1966
+ var POLL_INTERVAL_MS = 1e3;
1967
+ var DEBOUNCE_MS = 200;
1968
+ function useAutoRefetch(dbPath, onRefetch) {
1969
+ const callbackRef = useRef(onRefetch);
1970
+ callbackRef.current = onRefetch;
1971
+ useEffect(() => {
1972
+ let debounceTimer = null;
1973
+ const handleChange = () => {
1974
+ if (debounceTimer) clearTimeout(debounceTimer);
1975
+ debounceTimer = setTimeout(() => {
1976
+ logger.info("useAutoRefetch: external db change detected, refetching");
1977
+ callbackRef.current();
1978
+ }, DEBOUNCE_MS);
1979
+ };
1980
+ const walPath = `${dbPath}-wal`;
1981
+ const listener = (curr, prev) => {
1982
+ if (curr.mtimeMs !== prev.mtimeMs) {
1983
+ handleChange();
1984
+ }
1985
+ };
1986
+ watchFile(dbPath, { interval: POLL_INTERVAL_MS }, listener);
1987
+ watchFile(walPath, { interval: POLL_INTERVAL_MS }, listener);
1988
+ logger.info(`useAutoRefetch: watching ${dbPath} (poll ${POLL_INTERVAL_MS}ms)`);
1989
+ return () => {
1990
+ unwatchFile(dbPath, listener);
1991
+ unwatchFile(walPath, listener);
1992
+ if (debounceTimer) clearTimeout(debounceTimer);
1993
+ logger.info("useAutoRefetch: stopped watching");
1994
+ };
1995
+ }, [dbPath]);
1996
+ }
1997
+
1998
+ // src/tui/components/App.tsx
1999
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1639
2000
  var STATUS_CYCLE = [
1640
2001
  TaskStatus.Backlog,
1641
2002
  TaskStatus.Todo,
@@ -1643,9 +2004,25 @@ var STATUS_CYCLE = [
1643
2004
  TaskStatus.Review,
1644
2005
  TaskStatus.Done
1645
2006
  ];
2007
+ var EPIC_PANEL_WIDTH = 48;
1646
2008
  function App({ container, initialProject }) {
1647
2009
  const { exit } = useApp();
2010
+ const { stdout } = useStdout4();
1648
2011
  const [state, dispatch] = useReducer(appReducer, initialState);
2012
+ const [, setResizeTick] = useState6(0);
2013
+ useEffect2(() => {
2014
+ const onResize = () => {
2015
+ setResizeTick((n) => n + 1);
2016
+ };
2017
+ stdout.on("resize", onResize);
2018
+ return () => {
2019
+ stdout.off("resize", onResize);
2020
+ };
2021
+ }, [stdout]);
2022
+ const termWidth = stdout.columns > 0 ? stdout.columns : 120;
2023
+ const remaining = Math.max(0, termWidth - EPIC_PANEL_WIDTH - 2);
2024
+ const taskListWidth = Math.floor(remaining * 0.6);
2025
+ const taskDetailWidth = remaining - taskListWidth;
1649
2026
  const loadProjects = useCallback2(() => {
1650
2027
  const result = container.projectService.listProjects();
1651
2028
  if (result.ok) {
@@ -1658,10 +2035,13 @@ function App({ container, initialProject }) {
1658
2035
  }, [container]);
1659
2036
  const loadTasks = useCallback2(() => {
1660
2037
  logger.startSpan("TUI.loadTasks", () => {
1661
- const filter = { ...state.filter };
2038
+ const filter = { ...state.filter, level: TaskLevel.Work };
1662
2039
  if (state.activeProject) {
1663
2040
  filter.projectId = state.activeProject.id;
1664
2041
  }
2042
+ if (state.selectedEpicIds.size > 0) {
2043
+ filter.parentIds = [...state.selectedEpicIds];
2044
+ }
1665
2045
  const result = container.taskService.listTasks(filter);
1666
2046
  if (result.ok) {
1667
2047
  logger.info(`TUI.loadTasks: loaded ${result.value.length} tasks`);
@@ -1671,7 +2051,19 @@ function App({ container, initialProject }) {
1671
2051
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1672
2052
  }
1673
2053
  });
1674
- }, [container, state.filter, state.activeProject]);
2054
+ }, [container, state.filter, state.activeProject, state.selectedEpicIds]);
2055
+ const loadEpics = useCallback2(() => {
2056
+ if (!state.activeProject) return;
2057
+ const result = container.taskService.listTasks({
2058
+ projectId: state.activeProject.id,
2059
+ level: TaskLevel.Epic
2060
+ });
2061
+ if (result.ok) {
2062
+ dispatch({ type: "SET_EPICS", epics: result.value });
2063
+ } else {
2064
+ logger.error("TUI.loadEpics: failed", result.error);
2065
+ }
2066
+ }, [container, state.activeProject]);
1675
2067
  const loadDeps = useCallback2(
1676
2068
  (taskId) => {
1677
2069
  const blockersResult = container.dependencyService.listBlockers(taskId);
@@ -1700,11 +2092,12 @@ function App({ container, initialProject }) {
1700
2092
  dispatch({ type: "SELECT_TASK", task: result.value });
1701
2093
  }
1702
2094
  loadTasks();
2095
+ loadEpics();
1703
2096
  } else {
1704
2097
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1705
2098
  }
1706
2099
  },
1707
- [container, state.selectedTask, loadTasks]
2100
+ [container, state.selectedTask, loadTasks, loadEpics]
1708
2101
  );
1709
2102
  const saveReorder = useCallback2(() => {
1710
2103
  const tasks = state.tasks;
@@ -1722,10 +2115,32 @@ function App({ container, initialProject }) {
1722
2115
  });
1723
2116
  loadTasks();
1724
2117
  }, [container, state.tasks, state.selectedIndex, loadTasks]);
1725
- useEffect(() => {
2118
+ const saveEpicReorder = useCallback2(() => {
2119
+ const epics = state.epics;
2120
+ const idx = state.epicSelectedIndex;
2121
+ const epic = epics[idx];
2122
+ if (!epic) return;
2123
+ const prev = epics[idx - 1];
2124
+ const next = epics[idx + 1];
2125
+ const result = prev ? container.taskService.rerankTask({ taskId: epic.id, afterId: prev.id }) : next ? container.taskService.rerankTask({ taskId: epic.id, beforeId: next.id }) : container.taskService.rerankTask({ taskId: epic.id, position: 1 });
2126
+ dispatch({ type: "EXIT_EPIC_REORDER", save: result.ok });
2127
+ dispatch({
2128
+ type: "FLASH",
2129
+ message: result.ok ? "Epic rank saved" : result.error.message,
2130
+ level: result.ok ? "info" : "error"
2131
+ });
2132
+ loadEpics();
2133
+ }, [container, state.epics, state.epicSelectedIndex, loadEpics]);
2134
+ const refetchAll = useCallback2(() => {
2135
+ loadProjects();
2136
+ loadTasks();
2137
+ loadEpics();
2138
+ }, [loadProjects, loadTasks, loadEpics]);
2139
+ useAutoRefetch(container.dbPath, refetchAll);
2140
+ useEffect2(() => {
1726
2141
  loadProjects();
1727
2142
  }, [loadProjects]);
1728
- useEffect(() => {
2143
+ useEffect2(() => {
1729
2144
  if (state.projects.length > 0 && !state.activeProject) {
1730
2145
  logger.info(`TUI.resolveProject: resolving initialProject=${initialProject ?? "(default)"}`);
1731
2146
  const result = container.projectService.resolveProject(initialProject);
@@ -1742,12 +2157,13 @@ function App({ container, initialProject }) {
1742
2157
  }
1743
2158
  }
1744
2159
  }, [state.projects, state.activeProject, initialProject, container]);
1745
- useEffect(() => {
2160
+ useEffect2(() => {
1746
2161
  if (state.activeProject) {
1747
2162
  loadTasks();
2163
+ loadEpics();
1748
2164
  }
1749
- }, [state.activeProject, state.filter, loadTasks]);
1750
- useEffect(() => {
2165
+ }, [state.activeProject, state.filter, loadTasks, loadEpics]);
2166
+ useEffect2(() => {
1751
2167
  if (state.flash) {
1752
2168
  const timer = setTimeout(() => {
1753
2169
  dispatch({ type: "CLEAR_FLASH" });
@@ -1758,7 +2174,7 @@ function App({ container, initialProject }) {
1758
2174
  }
1759
2175
  return void 0;
1760
2176
  }, [state.flash]);
1761
- useInput5((input, key) => {
2177
+ useInput6((input, key) => {
1762
2178
  if (state.confirmDelete) {
1763
2179
  if (input === "y") {
1764
2180
  const result = container.taskService.deleteTask(state.confirmDelete.id);
@@ -1769,6 +2185,7 @@ function App({ container, initialProject }) {
1769
2185
  dispatch({ type: "GO_BACK" });
1770
2186
  }
1771
2187
  loadTasks();
2188
+ loadEpics();
1772
2189
  } else {
1773
2190
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1774
2191
  dispatch({ type: "CANCEL_DELETE" });
@@ -1782,7 +2199,7 @@ function App({ container, initialProject }) {
1782
2199
  dispatch({ type: "GO_BACK" });
1783
2200
  return;
1784
2201
  }
1785
- if (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit || state.activeView === ViewType.ProjectSelector || state.activeView === ViewType.ProjectCreate) {
2202
+ if (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit || state.activeView === ViewType.ProjectSelector || state.activeView === ViewType.ProjectCreate || state.activeView === ViewType.EpicPicker) {
1786
2203
  return;
1787
2204
  }
1788
2205
  if (state.activeView === ViewType.DependencyList && state.isAddingDep) {
@@ -1853,6 +2270,26 @@ function App({ container, initialProject }) {
1853
2270
  }
1854
2271
  return;
1855
2272
  }
2273
+ if (state.isEpicReordering) {
2274
+ if (key.upArrow || input === "k") {
2275
+ dispatch({ type: "EPIC_REORDER_MOVE", direction: "up" });
2276
+ return;
2277
+ }
2278
+ if (key.downArrow || input === "j") {
2279
+ dispatch({ type: "EPIC_REORDER_MOVE", direction: "down" });
2280
+ return;
2281
+ }
2282
+ if (key.rightArrow) {
2283
+ saveEpicReorder();
2284
+ return;
2285
+ }
2286
+ if (key.escape || key.leftArrow) {
2287
+ dispatch({ type: "EXIT_EPIC_REORDER", save: false });
2288
+ dispatch({ type: "FLASH", message: "Epic reorder cancelled", level: "info" });
2289
+ return;
2290
+ }
2291
+ return;
2292
+ }
1856
2293
  if (state.isReordering) {
1857
2294
  if (key.upArrow || input === "k") {
1858
2295
  dispatch({ type: "REORDER_MOVE", direction: "up" });
@@ -1889,13 +2326,49 @@ function App({ container, initialProject }) {
1889
2326
  dispatch({ type: "NAVIGATE_TO", view: ViewType.Help });
1890
2327
  return;
1891
2328
  }
1892
- if (key.tab && state.activeView === ViewType.TaskList && previewTask) {
1893
- dispatch({
1894
- type: "SET_PANEL_FOCUS",
1895
- panel: state.focusedPanel === "list" ? "detail" : "list"
1896
- });
2329
+ if (key.tab && state.activeView === ViewType.TaskList) {
2330
+ const panels = previewTask ? ["epic", "list", "detail"] : ["epic", "list"];
2331
+ const curIdx = panels.indexOf(state.focusedPanel);
2332
+ const nextPanel = panels[(curIdx + 1) % panels.length] ?? "list";
2333
+ dispatch({ type: "SET_PANEL_FOCUS", panel: nextPanel });
1897
2334
  return;
1898
2335
  }
2336
+ if (state.activeView === ViewType.TaskList && state.focusedPanel === "epic") {
2337
+ if (key.upArrow || input === "k") {
2338
+ dispatch({ type: "EPIC_MOVE_CURSOR", direction: "up" });
2339
+ return;
2340
+ }
2341
+ if (key.downArrow || input === "j") {
2342
+ dispatch({ type: "EPIC_MOVE_CURSOR", direction: "down" });
2343
+ return;
2344
+ }
2345
+ if (input === " " || key.return) {
2346
+ const epic = state.epics[state.epicSelectedIndex];
2347
+ if (epic) {
2348
+ dispatch({ type: "TOGGLE_EPIC", epicId: epic.id });
2349
+ }
2350
+ return;
2351
+ }
2352
+ if (input === "0") {
2353
+ dispatch({ type: "CLEAR_EPIC_SELECTION" });
2354
+ return;
2355
+ }
2356
+ if (key.leftArrow) {
2357
+ if (state.epics.length > 0) {
2358
+ dispatch({ type: "ENTER_EPIC_REORDER" });
2359
+ dispatch({
2360
+ type: "FLASH",
2361
+ message: "Reorder: \u2191\u2193 move, \u2192 save, \u2190 cancel",
2362
+ level: "info"
2363
+ });
2364
+ }
2365
+ return;
2366
+ }
2367
+ if (input === "q") {
2368
+ exit();
2369
+ return;
2370
+ }
2371
+ }
1899
2372
  if (state.activeView === ViewType.TaskList && state.focusedPanel === "list") {
1900
2373
  if (key.upArrow || input === "k") {
1901
2374
  dispatch({ type: "MOVE_CURSOR", direction: "up" });
@@ -1913,6 +2386,14 @@ function App({ container, initialProject }) {
1913
2386
  dispatch({ type: "SET_CURSOR", index: state.tasks.length - 1 });
1914
2387
  return;
1915
2388
  }
2389
+ if (key.pageDown || key.ctrl && input === "d") {
2390
+ dispatch({ type: "PAGE_CURSOR", direction: "down" });
2391
+ return;
2392
+ }
2393
+ if (key.pageUp || key.ctrl && input === "u") {
2394
+ dispatch({ type: "PAGE_CURSOR", direction: "up" });
2395
+ return;
2396
+ }
1916
2397
  if (key.return) {
1917
2398
  const task = state.tasks[state.selectedIndex];
1918
2399
  if (task) {
@@ -1949,6 +2430,30 @@ function App({ container, initialProject }) {
1949
2430
  }
1950
2431
  return;
1951
2432
  }
2433
+ if (input === "a") {
2434
+ const task = state.tasks[state.selectedIndex];
2435
+ if (task) {
2436
+ dispatch({ type: "SELECT_TASK", task });
2437
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.EpicPicker });
2438
+ }
2439
+ return;
2440
+ }
2441
+ if (input === "A") {
2442
+ const task = state.tasks[state.selectedIndex];
2443
+ if (task && task.parentId) {
2444
+ const result = container.taskService.updateTask(task.id, { parentId: null });
2445
+ if (result.ok) {
2446
+ dispatch({ type: "FLASH", message: "Unassigned from epic", level: "info" });
2447
+ loadTasks();
2448
+ loadEpics();
2449
+ } else {
2450
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2451
+ }
2452
+ } else if (task) {
2453
+ dispatch({ type: "FLASH", message: "Task has no epic", level: "warn" });
2454
+ }
2455
+ return;
2456
+ }
1952
2457
  if (input === "/") {
1953
2458
  dispatch({ type: "SET_SEARCH_ACTIVE", active: true });
1954
2459
  dispatch({ type: "SET_SEARCH_QUERY", query: "" });
@@ -1959,6 +2464,22 @@ function App({ container, initialProject }) {
1959
2464
  return;
1960
2465
  }
1961
2466
  if (key.leftArrow) {
2467
+ if (state.selectedEpicIds.size > 0) {
2468
+ dispatch({
2469
+ type: "FLASH",
2470
+ message: "Clear epic filter (0) before reordering",
2471
+ level: "warn"
2472
+ });
2473
+ return;
2474
+ }
2475
+ if (state.filter.status || state.filter.type || state.filter.search) {
2476
+ dispatch({
2477
+ type: "FLASH",
2478
+ message: "Clear filters (0) before reordering",
2479
+ level: "warn"
2480
+ });
2481
+ return;
2482
+ }
1962
2483
  if (state.tasks.length > 0) {
1963
2484
  dispatch({ type: "ENTER_REORDER" });
1964
2485
  dispatch({ type: "FLASH", message: "Reorder: \u2191\u2193 move, \u2192 save, \u2190 cancel", level: "info" });
@@ -1987,6 +2508,14 @@ function App({ container, initialProject }) {
1987
2508
  }
1988
2509
  }
1989
2510
  if (state.activeView === ViewType.TaskList && state.focusedPanel === "detail" && previewTask) {
2511
+ if (key.upArrow || input === "k") {
2512
+ dispatch({ type: "DETAIL_SCROLL", direction: "up" });
2513
+ return;
2514
+ }
2515
+ if (key.downArrow || input === "j") {
2516
+ dispatch({ type: "DETAIL_SCROLL", direction: "down" });
2517
+ return;
2518
+ }
1990
2519
  if (input === "e") {
1991
2520
  dispatch({ type: "SELECT_TASK", task: previewTask });
1992
2521
  dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
@@ -2022,6 +2551,14 @@ ${previewTask.additionalRequirements}`;
2022
2551
  }
2023
2552
  }
2024
2553
  if (state.activeView === ViewType.TaskDetail) {
2554
+ if (key.upArrow || input === "k") {
2555
+ dispatch({ type: "DETAIL_SCROLL", direction: "up" });
2556
+ return;
2557
+ }
2558
+ if (key.downArrow || input === "j") {
2559
+ dispatch({ type: "DETAIL_SCROLL", direction: "down" });
2560
+ return;
2561
+ }
2025
2562
  if (input === "e" && state.selectedTask) {
2026
2563
  dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
2027
2564
  return;
@@ -2141,22 +2678,53 @@ ${state.selectedTask.additionalRequirements}`;
2141
2678
  loadDeps(taskId);
2142
2679
  dispatch({ type: "GO_BACK" });
2143
2680
  loadTasks();
2681
+ loadEpics();
2144
2682
  } else {
2145
2683
  const result = container.taskService.createTask(data, state.activeProject?.id);
2146
2684
  if (result.ok) {
2147
2685
  dispatch({ type: "FLASH", message: "Task created", level: "info" });
2148
2686
  dispatch({ type: "GO_BACK" });
2149
2687
  loadTasks();
2688
+ loadEpics();
2150
2689
  } else {
2151
2690
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2152
2691
  }
2153
2692
  }
2154
2693
  },
2155
- [container, state.activeView, state.selectedTask, state.activeProject, loadTasks, loadDeps]
2694
+ [
2695
+ container,
2696
+ state.activeView,
2697
+ state.selectedTask,
2698
+ state.activeProject,
2699
+ loadTasks,
2700
+ loadDeps,
2701
+ loadEpics
2702
+ ]
2156
2703
  );
2157
2704
  const handleFormCancel = useCallback2(() => {
2158
2705
  dispatch({ type: "GO_BACK" });
2159
2706
  }, []);
2707
+ const handleEpicPickerSelect = useCallback2(
2708
+ (epicId) => {
2709
+ if (!state.selectedTask) return;
2710
+ const result = container.taskService.updateTask(state.selectedTask.id, {
2711
+ parentId: epicId
2712
+ });
2713
+ if (result.ok) {
2714
+ const msg = epicId ? `Assigned to ${epicId}` : "Unassigned from epic";
2715
+ dispatch({ type: "FLASH", message: msg, level: "info" });
2716
+ loadTasks();
2717
+ loadEpics();
2718
+ } else {
2719
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2720
+ }
2721
+ dispatch({ type: "GO_BACK" });
2722
+ },
2723
+ [container, state.selectedTask, loadTasks, loadEpics]
2724
+ );
2725
+ const handleEpicPickerCancel = useCallback2(() => {
2726
+ dispatch({ type: "GO_BACK" });
2727
+ }, []);
2160
2728
  const handleProjectSelect = useCallback2((project) => {
2161
2729
  dispatch({ type: "SET_ACTIVE_PROJECT", project });
2162
2730
  dispatch({ type: "GO_BACK" });
@@ -2167,7 +2735,11 @@ ${state.selectedTask.additionalRequirements}`;
2167
2735
  const result = container.projectService.updateProject(project.id, { isDefault: true });
2168
2736
  if (result.ok) {
2169
2737
  logger.info(`TUI.setDefault: set key=${result.value.key} as default`);
2170
- dispatch({ type: "FLASH", message: `Default project: ${result.value.name}`, level: "info" });
2738
+ dispatch({
2739
+ type: "FLASH",
2740
+ message: `Default project: ${result.value.name}`,
2741
+ level: "info"
2742
+ });
2171
2743
  loadProjects();
2172
2744
  } else {
2173
2745
  logger.error("TUI.setDefault: failed", result.error);
@@ -2212,7 +2784,7 @@ ${state.selectedTask.additionalRequirements}`;
2212
2784
  dispatch({ type: "GO_BACK" });
2213
2785
  }, []);
2214
2786
  const previewTask = state.tasks[state.selectedIndex] ?? null;
2215
- const initialDepsForEdit = useMemo(() => {
2787
+ const initialDepsForEdit = useMemo2(() => {
2216
2788
  return [
2217
2789
  ...state.depBlockers.map((t) => ({ id: t.id, name: t.name, type: DependencyType.Blocks })),
2218
2790
  ...state.depRelated.map((t) => ({ id: t.id, name: t.name, type: DependencyType.RelatesTo })),
@@ -2223,23 +2795,33 @@ ${state.selectedTask.additionalRequirements}`;
2223
2795
  }))
2224
2796
  ];
2225
2797
  }, [state.depBlockers, state.depRelated, state.depDuplicates]);
2226
- const allProjectTasks = useMemo(() => {
2798
+ const allProjectTasks = useMemo2(() => {
2227
2799
  if (!state.activeProject) return [];
2228
2800
  const result = container.taskService.listTasks({ projectId: state.activeProject.id });
2229
2801
  return result.ok ? result.value : [];
2230
2802
  }, [container, state.activeProject, state.tasks]);
2231
2803
  const previewTaskId = previewTask?.id ?? null;
2232
- useEffect(() => {
2804
+ useEffect2(() => {
2233
2805
  if (state.activeView === ViewType.TaskList && previewTaskId) {
2234
2806
  loadDeps(previewTaskId);
2235
2807
  }
2236
2808
  }, [state.activeView, previewTaskId, loadDeps]);
2237
- return /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", height: "100%", children: [
2238
- /* @__PURE__ */ jsx14(Header, { state }),
2239
- /* @__PURE__ */ jsxs12(Box14, { flexDirection: "column", flexGrow: 1, children: [
2240
- state.confirmDelete && /* @__PURE__ */ jsx14(ConfirmDialog, { task: state.confirmDelete }),
2241
- !state.confirmDelete && state.activeView === ViewType.TaskList && /* @__PURE__ */ jsxs12(Box14, { flexDirection: "row", flexGrow: 1, children: [
2242
- /* @__PURE__ */ jsx14(Box14, { width: "50%", children: /* @__PURE__ */ jsx14(
2809
+ return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", height: stdout.rows, children: [
2810
+ /* @__PURE__ */ jsx16(Header, { state }),
2811
+ /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", children: [
2812
+ state.confirmDelete && /* @__PURE__ */ jsx16(ConfirmDialog, { task: state.confirmDelete }),
2813
+ !state.confirmDelete && state.activeView === ViewType.TaskList && /* @__PURE__ */ jsxs14(Box16, { flexDirection: "row", flexGrow: 1, children: [
2814
+ /* @__PURE__ */ jsx16(
2815
+ EpicPanel,
2816
+ {
2817
+ epics: state.epics,
2818
+ selectedIndex: state.epicSelectedIndex,
2819
+ selectedEpicIds: state.selectedEpicIds,
2820
+ isFocused: state.focusedPanel === "epic",
2821
+ isReordering: state.isEpicReordering
2822
+ }
2823
+ ),
2824
+ /* @__PURE__ */ jsx16(Box16, { width: taskListWidth, children: /* @__PURE__ */ jsx16(
2243
2825
  TaskList,
2244
2826
  {
2245
2827
  tasks: state.tasks,
@@ -2256,10 +2838,11 @@ ${state.selectedTask.additionalRequirements}`;
2256
2838
  state.depDependents.filter((t) => !isTerminalStatus(t.status)).map((t) => t.id)
2257
2839
  ),
2258
2840
  isSelectedBlocked: state.depBlockers.some((t) => !isTerminalStatus(t.status)),
2259
- isFocused: state.focusedPanel === "list"
2841
+ isFocused: state.focusedPanel === "list",
2842
+ epicFilterActive: state.selectedEpicIds.size > 0
2260
2843
  }
2261
2844
  ) }),
2262
- /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, children: previewTask ? /* @__PURE__ */ jsx14(
2845
+ /* @__PURE__ */ jsx16(Box16, { width: taskDetailWidth, children: previewTask ? /* @__PURE__ */ jsx16(
2263
2846
  TaskDetail,
2264
2847
  {
2265
2848
  task: previewTask,
@@ -2267,36 +2850,38 @@ ${state.selectedTask.additionalRequirements}`;
2267
2850
  dependents: state.depDependents,
2268
2851
  related: state.depRelated,
2269
2852
  duplicates: state.depDuplicates,
2270
- isFocused: state.focusedPanel === "detail"
2853
+ isFocused: state.focusedPanel === "detail",
2854
+ scrollOffset: state.detailScrollOffset
2271
2855
  }
2272
- ) : /* @__PURE__ */ jsxs12(
2273
- Box14,
2856
+ ) : /* @__PURE__ */ jsxs14(
2857
+ Box16,
2274
2858
  {
2275
2859
  flexDirection: "column",
2276
2860
  flexGrow: 1,
2277
2861
  borderStyle: "bold",
2278
2862
  borderColor: theme.border,
2279
2863
  children: [
2280
- /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { color: theme.title, bold: true, children: [
2864
+ /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsxs14(Text16, { color: theme.title, bold: true, children: [
2281
2865
  " ",
2282
2866
  "detail"
2283
2867
  ] }) }),
2284
- /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No task selected" }) })
2868
+ /* @__PURE__ */ jsx16(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "No task selected" }) })
2285
2869
  ]
2286
2870
  }
2287
2871
  ) })
2288
2872
  ] }),
2289
- !state.confirmDelete && state.activeView === ViewType.TaskDetail && state.selectedTask && /* @__PURE__ */ jsx14(
2873
+ !state.confirmDelete && state.activeView === ViewType.TaskDetail && state.selectedTask && /* @__PURE__ */ jsx16(
2290
2874
  TaskDetail,
2291
2875
  {
2292
2876
  task: state.selectedTask,
2293
2877
  blockers: state.depBlockers,
2294
2878
  dependents: state.depDependents,
2295
2879
  related: state.depRelated,
2296
- duplicates: state.depDuplicates
2880
+ duplicates: state.depDuplicates,
2881
+ scrollOffset: state.detailScrollOffset
2297
2882
  }
2298
2883
  ),
2299
- !state.confirmDelete && state.activeView === ViewType.DependencyList && state.selectedTask && /* @__PURE__ */ jsx14(
2884
+ !state.confirmDelete && state.activeView === ViewType.DependencyList && state.selectedTask && /* @__PURE__ */ jsx16(
2300
2885
  DependencyList,
2301
2886
  {
2302
2887
  task: state.selectedTask,
@@ -2309,7 +2894,7 @@ ${state.selectedTask.additionalRequirements}`;
2309
2894
  addDepInput: state.addDepInput
2310
2895
  }
2311
2896
  ),
2312
- !state.confirmDelete && (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit) && /* @__PURE__ */ jsx14(
2897
+ !state.confirmDelete && (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit) && /* @__PURE__ */ jsx16(
2313
2898
  TaskForm,
2314
2899
  {
2315
2900
  editingTask: state.activeView === ViewType.TaskEdit ? state.selectedTask : null,
@@ -2319,7 +2904,16 @@ ${state.selectedTask.additionalRequirements}`;
2319
2904
  onCancel: handleFormCancel
2320
2905
  }
2321
2906
  ),
2322
- !state.confirmDelete && state.activeView === ViewType.ProjectSelector && /* @__PURE__ */ jsx14(
2907
+ !state.confirmDelete && state.activeView === ViewType.EpicPicker && state.selectedTask && /* @__PURE__ */ jsx16(
2908
+ EpicPicker,
2909
+ {
2910
+ epics: state.epics,
2911
+ currentEpicId: state.selectedTask.parentId,
2912
+ onSelect: handleEpicPickerSelect,
2913
+ onCancel: handleEpicPickerCancel
2914
+ }
2915
+ ),
2916
+ !state.confirmDelete && state.activeView === ViewType.ProjectSelector && /* @__PURE__ */ jsx16(
2323
2917
  ProjectSelector,
2324
2918
  {
2325
2919
  projects: state.projects,
@@ -2330,18 +2924,18 @@ ${state.selectedTask.additionalRequirements}`;
2330
2924
  onCancel: handleProjectCancel
2331
2925
  }
2332
2926
  ),
2333
- !state.confirmDelete && state.activeView === ViewType.ProjectCreate && /* @__PURE__ */ jsx14(ProjectForm, { onSave: handleProjectFormSave, onCancel: handleProjectFormCancel }),
2334
- !state.confirmDelete && state.activeView === ViewType.Help && /* @__PURE__ */ jsx14(HelpOverlay, {})
2927
+ !state.confirmDelete && state.activeView === ViewType.ProjectCreate && /* @__PURE__ */ jsx16(ProjectForm, { onSave: handleProjectFormSave, onCancel: handleProjectFormCancel }),
2928
+ !state.confirmDelete && state.activeView === ViewType.Help && /* @__PURE__ */ jsx16(HelpOverlay, {})
2335
2929
  ] }),
2336
- /* @__PURE__ */ jsx14(Crumbs, { breadcrumbs: state.breadcrumbs }),
2337
- state.flash && /* @__PURE__ */ jsx14(FlashMessage, { message: state.flash.message, level: state.flash.level })
2930
+ /* @__PURE__ */ jsx16(Crumbs, { breadcrumbs: state.breadcrumbs }),
2931
+ state.flash && /* @__PURE__ */ jsx16(FlashMessage, { message: state.flash.message, level: state.flash.level })
2338
2932
  ] });
2339
2933
  }
2340
2934
 
2341
2935
  // src/tui/index.tsx
2342
- import { jsx as jsx15 } from "react/jsx-runtime";
2936
+ import { jsx as jsx17 } from "react/jsx-runtime";
2343
2937
  async function launchTUI(container, initialProject) {
2344
- const instance = render(/* @__PURE__ */ jsx15(App, { container, initialProject }), {
2938
+ const instance = render(/* @__PURE__ */ jsx17(App, { container, initialProject }), {
2345
2939
  exitOnCtrlC: true
2346
2940
  });
2347
2941
  await instance.waitUntilExit();
@@ -2349,4 +2943,4 @@ async function launchTUI(container, initialProject) {
2349
2943
  export {
2350
2944
  launchTUI
2351
2945
  };
2352
- //# sourceMappingURL=tui-JNZRBEIQ.js.map
2946
+ //# sourceMappingURL=tui-5JJH67YY.js.map