@tomkapa/tayto 0.1.2 → 0.2.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
16
  import { useReducer, useEffect, useCallback as useCallback2, useMemo } from "react";
16
- import { Box as Box14, Text as Text14, useInput as useInput5, useApp } from "ink";
17
+ import { Box as Box16, Text as Text16, useInput as useInput6, useApp } from "ink";
17
18
 
18
19
  // src/tui/types.ts
19
20
  var ViewType = {
@@ -24,6 +25,7 @@ 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
 
@@ -51,7 +53,13 @@ var initialState = {
51
53
  depSelectedIndex: 0,
52
54
  isAddingDep: false,
53
55
  addDepInput: "",
54
- focusedPanel: "list"
56
+ focusedPanel: "list",
57
+ detailScrollOffset: 0,
58
+ epics: [],
59
+ epicSelectedIndex: 0,
60
+ selectedEpicIds: /* @__PURE__ */ new Set(),
61
+ isEpicReordering: false,
62
+ epicReorderSnapshot: null
55
63
  };
56
64
  function appReducer(state, action) {
57
65
  switch (action.type) {
@@ -111,14 +119,14 @@ function appReducer(state, action) {
111
119
  case "MOVE_CURSOR": {
112
120
  const maxIndex = Math.max(0, state.tasks.length - 1);
113
121
  const newIndex = action.direction === "up" ? Math.max(0, state.selectedIndex - 1) : Math.min(maxIndex, state.selectedIndex + 1);
114
- return { ...state, selectedIndex: newIndex };
122
+ return { ...state, selectedIndex: newIndex, detailScrollOffset: 0 };
115
123
  }
116
124
  case "SET_CURSOR": {
117
125
  const maxIndex = Math.max(0, state.tasks.length - 1);
118
126
  return { ...state, selectedIndex: Math.max(0, Math.min(action.index, maxIndex)) };
119
127
  }
120
128
  case "SELECT_TASK":
121
- return { ...state, selectedTask: action.task };
129
+ return { ...state, selectedTask: action.task, detailScrollOffset: 0 };
122
130
  case "SET_FORM_DATA":
123
131
  return { ...state, formData: action.data };
124
132
  case "ENTER_REORDER":
@@ -181,6 +189,74 @@ function appReducer(state, action) {
181
189
  return { ...state, addDepInput: action.input };
182
190
  case "SET_PANEL_FOCUS":
183
191
  return { ...state, focusedPanel: action.panel };
192
+ case "DETAIL_SCROLL":
193
+ return {
194
+ ...state,
195
+ detailScrollOffset: action.direction === "up" ? Math.max(0, state.detailScrollOffset - 1) : state.detailScrollOffset + 1
196
+ };
197
+ case "DETAIL_RESET_SCROLL":
198
+ return { ...state, detailScrollOffset: 0 };
199
+ case "SET_EPICS":
200
+ return {
201
+ ...state,
202
+ epics: action.epics,
203
+ epicSelectedIndex: Math.min(state.epicSelectedIndex, Math.max(0, action.epics.length - 1))
204
+ };
205
+ case "EPIC_MOVE_CURSOR": {
206
+ if (state.epics.length === 0) return state;
207
+ const maxIdx = Math.max(0, state.epics.length - 1);
208
+ const newIdx = action.direction === "up" ? Math.max(0, state.epicSelectedIndex - 1) : Math.min(maxIdx, state.epicSelectedIndex + 1);
209
+ return { ...state, epicSelectedIndex: newIdx };
210
+ }
211
+ case "TOGGLE_EPIC": {
212
+ const next = new Set(state.selectedEpicIds);
213
+ if (next.has(action.epicId)) {
214
+ next.delete(action.epicId);
215
+ } else {
216
+ next.add(action.epicId);
217
+ }
218
+ return { ...state, selectedEpicIds: next, selectedIndex: 0 };
219
+ }
220
+ case "CLEAR_EPIC_SELECTION":
221
+ return { ...state, selectedEpicIds: /* @__PURE__ */ new Set(), selectedIndex: 0 };
222
+ case "ENTER_EPIC_REORDER":
223
+ return {
224
+ ...state,
225
+ isEpicReordering: true,
226
+ epicReorderSnapshot: [...state.epics]
227
+ };
228
+ case "EPIC_REORDER_MOVE": {
229
+ if (!state.isEpicReordering) return state;
230
+ const idx = state.epicSelectedIndex;
231
+ const epics = [...state.epics];
232
+ const swapIdx = action.direction === "up" ? idx - 1 : idx + 1;
233
+ if (swapIdx < 0 || swapIdx >= epics.length) return state;
234
+ const current = epics[idx];
235
+ const swap = epics[swapIdx];
236
+ if (!current || !swap) return state;
237
+ epics[idx] = swap;
238
+ epics[swapIdx] = current;
239
+ return {
240
+ ...state,
241
+ epics,
242
+ epicSelectedIndex: swapIdx
243
+ };
244
+ }
245
+ case "EXIT_EPIC_REORDER": {
246
+ if (!action.save && state.epicReorderSnapshot) {
247
+ return {
248
+ ...state,
249
+ isEpicReordering: false,
250
+ epics: state.epicReorderSnapshot,
251
+ epicReorderSnapshot: null
252
+ };
253
+ }
254
+ return {
255
+ ...state,
256
+ isEpicReordering: false,
257
+ epicReorderSnapshot: null
258
+ };
259
+ }
184
260
  }
185
261
  }
186
262
 
@@ -305,6 +381,7 @@ var STATUS_COLOR = {
305
381
  [TaskStatus.Cancelled]: theme.status.kill
306
382
  };
307
383
  var TYPE_COLOR = {
384
+ [TaskType.Epic]: theme.status.modified,
308
385
  [TaskType.Story]: theme.status.highlight,
309
386
  [TaskType.TechDebt]: theme.status.pending,
310
387
  [TaskType.Bug]: theme.status.error
@@ -319,13 +396,24 @@ var DEP_TYPE_LABEL = {
319
396
  // src/tui/components/Header.tsx
320
397
  import { Box, Text } from "ink";
321
398
  import { jsx, jsxs } from "react/jsx-runtime";
322
- function getKeyHints(view, isSearchActive) {
399
+ function getKeyHints(view, isSearchActive, focusedPanel) {
323
400
  if (isSearchActive) {
324
401
  return [
325
402
  { key: "enter", desc: "apply" },
326
403
  { key: "esc", desc: "cancel" }
327
404
  ];
328
405
  }
406
+ if (view === "task-list" && focusedPanel === "epic") {
407
+ return [
408
+ { key: "j/k", desc: "nav" },
409
+ { key: "space", desc: "toggle" },
410
+ { key: "\u2190", desc: "reorder" },
411
+ { key: "0", desc: "clear" },
412
+ { key: "tab", desc: "tasks" },
413
+ { key: "?", desc: "help" },
414
+ { key: "q", desc: "quit" }
415
+ ];
416
+ }
329
417
  if (view === "task-list") {
330
418
  return [
331
419
  { key: "enter", desc: "view" },
@@ -333,11 +421,14 @@ function getKeyHints(view, isSearchActive) {
333
421
  { key: "e", desc: "edit" },
334
422
  { key: "d", desc: "del" },
335
423
  { key: "s", desc: "status" },
424
+ { key: "a", desc: "assign" },
425
+ { key: "A", desc: "unassign" },
336
426
  { key: "\u2190", desc: "reorder" },
337
427
  { key: "/", desc: "search" },
338
428
  { key: "p", desc: "project" },
339
429
  { key: "f", desc: "status-f" },
340
430
  { key: "t", desc: "type-f" },
431
+ { key: "tab", desc: "panel" },
341
432
  { key: "?", desc: "help" },
342
433
  { key: "q", desc: "quit" }
343
434
  ];
@@ -362,10 +453,10 @@ function getKeyHints(view, isSearchActive) {
362
453
  function Header({ state }) {
363
454
  const projectName = state.activeProject?.name ?? "none";
364
455
  const taskCount = state.tasks.length;
365
- const hints = getKeyHints(state.activeView, state.isSearchActive);
456
+ const hints = getKeyHints(state.activeView, state.isSearchActive, state.focusedPanel);
366
457
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
367
458
  /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
368
- /* @__PURE__ */ jsx(Text, { color: theme.logo, bold: true, children: "TaskCLI" }),
459
+ /* @__PURE__ */ jsx(Text, { color: theme.logo, bold: true, children: "Tayto" }),
369
460
  /* @__PURE__ */ jsx(Text, { color: theme.logo, children: "Project:" }),
370
461
  /* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: projectName }),
371
462
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
@@ -443,7 +534,8 @@ function TaskList({
443
534
  nonTerminalBlockerIds,
444
535
  nonTerminalDependentIds,
445
536
  isSelectedBlocked,
446
- isFocused = true
537
+ isFocused = true,
538
+ epicFilterActive = false
447
539
  }) {
448
540
  const currentPage = Math.floor(selectedIndex / PAGE_SIZE);
449
541
  const viewStart = currentPage * PAGE_SIZE;
@@ -478,6 +570,7 @@ function TaskList({
478
570
  " ",
479
571
  "REORDER"
480
572
  ] }),
573
+ epicFilterActive && /* @__PURE__ */ jsx4(Text4, { color: theme.titleHighlight, children: " [epic]" }),
481
574
  filterText && /* @__PURE__ */ jsxs2(Text4, { color: theme.titleFilter, children: [
482
575
  " /",
483
576
  filterText
@@ -546,7 +639,7 @@ function TaskList({
546
639
  }
547
640
 
548
641
  // src/tui/components/TaskDetail.tsx
549
- import { Box as Box6, Text as Text6 } from "ink";
642
+ import { Box as Box6, Text as Text6, useStdout } from "ink";
550
643
 
551
644
  // src/tui/components/Markdown.tsx
552
645
  import { Text as Text5, Box as Box5 } from "ink";
@@ -688,11 +781,14 @@ function TaskDetail({
688
781
  dependents,
689
782
  related,
690
783
  duplicates,
691
- isFocused = true
784
+ isFocused = true,
785
+ scrollOffset = 0
692
786
  }) {
787
+ const { stdout } = useStdout();
693
788
  const allText = `${task.description}
694
789
  ${task.technicalNotes}
695
790
  ${task.additionalRequirements}`;
791
+ const viewportHeight = Math.max(5, (stdout.rows > 0 ? stdout.rows : 24) - 4);
696
792
  return /* @__PURE__ */ jsxs4(
697
793
  Box6,
698
794
  {
@@ -708,60 +804,66 @@ ${task.additionalRequirements}`;
708
804
  ] }),
709
805
  /* @__PURE__ */ jsx6(Text6, { color: theme.fg, children: "(" }),
710
806
  /* @__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 })
807
+ /* @__PURE__ */ jsx6(Text6, { color: theme.fg, children: ")" }),
808
+ scrollOffset > 0 && /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
809
+ " \u2191",
810
+ scrollOffset
811
+ ] })
720
812
  ] }),
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(", ") })
813
+ /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", height: viewportHeight, overflowY: "hidden", children: /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", marginTop: -scrollOffset, children: [
814
+ /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
815
+ /* @__PURE__ */ jsx6(Field, { label: "id", value: task.id }),
816
+ /* @__PURE__ */ jsx6(Field, { label: "type", value: task.type }),
817
+ /* @__PURE__ */ jsx6(Field, { label: "status", value: task.status }),
818
+ /* @__PURE__ */ jsx6(Field, { label: "created", value: new Date(task.createdAt).toLocaleString() }),
819
+ /* @__PURE__ */ jsx6(Field, { label: "updated", value: new Date(task.updatedAt).toLocaleString() }),
820
+ task.parentId && /* @__PURE__ */ jsx6(Field, { label: "parent", value: task.parentId })
729
821
  ] }),
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
- " "
822
+ (blockers && blockers.length > 0 || dependents && dependents.length > 0 || related && related.length > 0 || duplicates && duplicates.length > 0) && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
823
+ /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- dependencies ---" }),
824
+ blockers && blockers.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
825
+ /* @__PURE__ */ jsxs4(Text6, { color: theme.status.error, bold: true, children: [
826
+ "blocked by:",
827
+ " "
828
+ ] }),
829
+ /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: blockers.map((t) => t.id).join(", ") })
734
830
  ] }),
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
- " "
831
+ dependents && dependents.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
832
+ /* @__PURE__ */ jsxs4(Text6, { color: theme.status.added, bold: true, children: [
833
+ "blocks:",
834
+ " "
835
+ ] }),
836
+ /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: dependents.map((t) => t.id).join(", ") })
741
837
  ] }),
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
- " "
838
+ related && related.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
839
+ /* @__PURE__ */ jsxs4(Text6, { color: theme.status.new, bold: true, children: [
840
+ "relates to:",
841
+ " "
842
+ ] }),
843
+ /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: related.map((t) => t.id).join(", ") })
844
+ ] }),
845
+ duplicates && duplicates.length > 0 && /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
846
+ /* @__PURE__ */ jsxs4(Text6, { color: theme.status.pending, bold: true, children: [
847
+ "duplicates:",
848
+ " "
849
+ ] }),
850
+ /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: duplicates.map((t) => t.id).join(", ") })
748
851
  ] }),
749
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: duplicates.map((t) => t.id).join(", ") })
852
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press D to manage dependencies" })
750
853
  ] }),
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 })
764
- ] }),
854
+ /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
855
+ /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- description ---" }),
856
+ task.description.trim() ? /* @__PURE__ */ jsx6(Markdown, { content: task.description }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No description" })
857
+ ] }),
858
+ task.technicalNotes.trim() && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
859
+ /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- technical notes ---" }),
860
+ /* @__PURE__ */ jsx6(Markdown, { content: task.technicalNotes })
861
+ ] }),
862
+ task.additionalRequirements.trim() && /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", paddingX: 1, children: [
863
+ /* @__PURE__ */ jsx6(Text6, { color: theme.title, bold: true, children: "--- requirements ---" }),
864
+ /* @__PURE__ */ jsx6(Markdown, { content: task.additionalRequirements })
865
+ ] })
866
+ ] }) }),
765
867
  /* @__PURE__ */ jsx6(Box6, { flexGrow: 1 }),
766
868
  /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(MermaidHint, { content: allText }) })
767
869
  ]
@@ -808,7 +910,7 @@ function openInEditor(content, filename) {
808
910
 
809
911
  // src/tui/components/TaskPicker.tsx
810
912
  import { useState } from "react";
811
- import { Box as Box7, Text as Text7, useInput, useStdout } from "ink";
913
+ import { Box as Box7, Text as Text7, useInput, useStdout as useStdout2 } from "ink";
812
914
  import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
813
915
  var DEP_TYPE_VALUES = Object.values(UIDependencyType);
814
916
  var DEP_TYPE_COLOR = {
@@ -818,7 +920,7 @@ var DEP_TYPE_COLOR = {
818
920
  [UIDependencyType.BlockedBy]: theme.status.modified
819
921
  };
820
922
  function TaskPicker({ tasks, excludeIds, initialSelection, onConfirm, onCancel }) {
821
- const { stdout } = useStdout();
923
+ const { stdout } = useStdout2();
822
924
  const termHeight = stdout.rows > 0 ? stdout.rows : 24;
823
925
  const maxVisible = Math.max(3, termHeight - 12);
824
926
  const [searchQuery, setSearchQuery] = useState("");
@@ -1634,8 +1736,222 @@ function DependencyList({
1634
1736
  ] });
1635
1737
  }
1636
1738
 
1637
- // src/tui/components/App.tsx
1739
+ // src/tui/components/EpicPanel.tsx
1740
+ import { Box as Box14, Text as Text14 } from "ink";
1638
1741
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1742
+ var PAGE_SIZE2 = 20;
1743
+ function EpicPanel({
1744
+ epics,
1745
+ selectedIndex,
1746
+ selectedEpicIds,
1747
+ isFocused,
1748
+ isReordering = false
1749
+ }) {
1750
+ const filterActive = selectedEpicIds.size > 0;
1751
+ const currentPage = Math.floor(selectedIndex / PAGE_SIZE2);
1752
+ const viewStart = currentPage * PAGE_SIZE2;
1753
+ const visibleEpics = epics.slice(viewStart, viewStart + PAGE_SIZE2);
1754
+ return /* @__PURE__ */ jsxs12(
1755
+ Box14,
1756
+ {
1757
+ flexDirection: "column",
1758
+ width: 48,
1759
+ borderStyle: "bold",
1760
+ borderColor: isFocused ? theme.borderFocus : theme.border,
1761
+ children: [
1762
+ /* @__PURE__ */ jsxs12(Box14, { children: [
1763
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.title, bold: true, children: [
1764
+ " ",
1765
+ "epics"
1766
+ ] }),
1767
+ /* @__PURE__ */ jsxs12(Text14, { color: theme.titleCounter, bold: true, children: [
1768
+ "[",
1769
+ epics.length,
1770
+ "]"
1771
+ ] }),
1772
+ isReordering && /* @__PURE__ */ jsxs12(Text14, { color: theme.flash.warn, bold: true, children: [
1773
+ " ",
1774
+ "REORDER"
1775
+ ] }),
1776
+ filterActive && /* @__PURE__ */ jsxs12(Text14, { color: theme.titleFilter, children: [
1777
+ " *",
1778
+ selectedEpicIds.size
1779
+ ] })
1780
+ ] }),
1781
+ epics.length === 0 ? /* @__PURE__ */ jsx14(Box14, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No epics" }) }) : visibleEpics.map((epic, i) => {
1782
+ const actualIndex = viewStart + i;
1783
+ const isSelected = actualIndex === selectedIndex && isFocused;
1784
+ const isChecked = selectedEpicIds.has(epic.id);
1785
+ const marker = isChecked ? "[x]" : "[ ]";
1786
+ const statusColor = STATUS_COLOR[epic.status] ?? theme.table.fg;
1787
+ if (isSelected) {
1788
+ const cursorBg = isReordering ? theme.flash.warn : theme.table.cursorBg;
1789
+ return /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { backgroundColor: cursorBg, color: theme.table.cursorFg, bold: true, children: [
1790
+ isReordering ? "~ " : " ",
1791
+ marker,
1792
+ " ",
1793
+ epic.name
1794
+ ] }) }, epic.id);
1795
+ }
1796
+ return /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { color: isChecked ? theme.titleHighlight : statusColor, children: [
1797
+ " ",
1798
+ marker,
1799
+ " ",
1800
+ epic.name
1801
+ ] }) }, epic.id);
1802
+ }),
1803
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1 }),
1804
+ epics.length > PAGE_SIZE2 && /* @__PURE__ */ jsx14(Box14, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
1805
+ "[",
1806
+ viewStart + 1,
1807
+ "-",
1808
+ Math.min(viewStart + PAGE_SIZE2, epics.length),
1809
+ "/",
1810
+ epics.length,
1811
+ "]"
1812
+ ] }) })
1813
+ ]
1814
+ }
1815
+ );
1816
+ }
1817
+
1818
+ // src/tui/components/EpicPicker.tsx
1819
+ import { useState as useState5 } from "react";
1820
+ import { Box as Box15, Text as Text15, useInput as useInput5, useStdout as useStdout3 } from "ink";
1821
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1822
+ function EpicPicker({ epics, currentEpicId, onSelect, onCancel }) {
1823
+ const { stdout } = useStdout3();
1824
+ const termHeight = stdout.rows > 0 ? stdout.rows : 24;
1825
+ const maxVisible = Math.max(3, termHeight - 10);
1826
+ const [searchQuery, setSearchQuery] = useState5("");
1827
+ const [isSearching, setIsSearching] = useState5(false);
1828
+ const [cursorIndex, setCursorIndex] = useState5(0);
1829
+ const filtered = epics.filter((e) => {
1830
+ if (!searchQuery.trim()) return true;
1831
+ const q = searchQuery.toLowerCase();
1832
+ return e.id.toLowerCase().includes(q) || e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q);
1833
+ });
1834
+ let viewStart = 0;
1835
+ if (cursorIndex >= viewStart + maxVisible) {
1836
+ viewStart = cursorIndex - maxVisible + 1;
1837
+ }
1838
+ if (cursorIndex < viewStart) {
1839
+ viewStart = cursorIndex;
1840
+ }
1841
+ const visible = filtered.slice(viewStart, viewStart + maxVisible);
1842
+ useInput5((input, key) => {
1843
+ if (isSearching) {
1844
+ if (key.escape) {
1845
+ setIsSearching(false);
1846
+ return;
1847
+ }
1848
+ if (key.return) {
1849
+ setIsSearching(false);
1850
+ setCursorIndex(0);
1851
+ return;
1852
+ }
1853
+ if (key.backspace || key.delete) {
1854
+ setSearchQuery((q) => q.slice(0, -1));
1855
+ setCursorIndex(0);
1856
+ return;
1857
+ }
1858
+ if (input && !key.ctrl && !key.meta) {
1859
+ setSearchQuery((q) => q + input);
1860
+ setCursorIndex(0);
1861
+ return;
1862
+ }
1863
+ return;
1864
+ }
1865
+ if (key.upArrow || input === "k") {
1866
+ setCursorIndex((i) => Math.max(0, i - 1));
1867
+ return;
1868
+ }
1869
+ if (key.downArrow || input === "j") {
1870
+ setCursorIndex((i) => Math.min(filtered.length - 1, i + 1));
1871
+ return;
1872
+ }
1873
+ if (key.return) {
1874
+ const epic = filtered[cursorIndex];
1875
+ if (epic) {
1876
+ onSelect(epic.id);
1877
+ }
1878
+ return;
1879
+ }
1880
+ if (input === "x") {
1881
+ onSelect(null);
1882
+ return;
1883
+ }
1884
+ if (input === "/") {
1885
+ setIsSearching(true);
1886
+ setSearchQuery("");
1887
+ return;
1888
+ }
1889
+ if (key.escape || input === "q") {
1890
+ onCancel();
1891
+ return;
1892
+ }
1893
+ });
1894
+ return /* @__PURE__ */ jsxs13(Box15, { flexDirection: "column", borderStyle: "bold", borderColor: theme.borderFocus, flexGrow: 1, children: [
1895
+ /* @__PURE__ */ jsxs13(Box15, { gap: 0, children: [
1896
+ /* @__PURE__ */ jsxs13(Text15, { color: theme.title, bold: true, children: [
1897
+ " ",
1898
+ "assign to epic"
1899
+ ] }),
1900
+ /* @__PURE__ */ jsxs13(Text15, { color: theme.titleCounter, bold: true, children: [
1901
+ " ",
1902
+ "[",
1903
+ epics.length,
1904
+ "]"
1905
+ ] })
1906
+ ] }),
1907
+ isSearching ? /* @__PURE__ */ jsxs13(Box15, { borderStyle: "round", borderColor: theme.prompt, paddingX: 1, children: [
1908
+ /* @__PURE__ */ jsx15(Text15, { color: theme.prompt, children: "/" }),
1909
+ /* @__PURE__ */ jsx15(Text15, { color: theme.prompt, children: searchQuery }),
1910
+ /* @__PURE__ */ jsx15(Text15, { color: theme.promptSuggest, children: "_" })
1911
+ ] }) : searchQuery ? /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: theme.titleFilter, children: [
1912
+ "/",
1913
+ searchQuery
1914
+ ] }) }) : null,
1915
+ /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: theme.table.headerFg, bold: true, children: [
1916
+ " ",
1917
+ "ID".padEnd(14),
1918
+ "STATUS".padEnd(14),
1919
+ "NAME"
1920
+ ] }) }),
1921
+ 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) => {
1922
+ const actualIndex = viewStart + i;
1923
+ const isCursor = actualIndex === cursorIndex;
1924
+ const isCurrent = epic.id === currentEpicId;
1925
+ const marker = isCurrent ? "* " : " ";
1926
+ const statusColor = STATUS_COLOR[epic.status] ?? theme.table.fg;
1927
+ return /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: isCursor ? /* @__PURE__ */ jsxs13(Text15, { backgroundColor: theme.table.cursorBg, color: theme.table.cursorFg, bold: true, children: [
1928
+ "> ",
1929
+ epic.id.padEnd(14),
1930
+ epic.status.padEnd(14),
1931
+ epic.name
1932
+ ] }) : /* @__PURE__ */ jsxs13(Fragment3, { children: [
1933
+ /* @__PURE__ */ jsx15(Text15, { color: isCurrent ? theme.titleHighlight : theme.table.fg, children: marker }),
1934
+ /* @__PURE__ */ jsx15(Text15, { color: theme.yaml.value, children: epic.id.padEnd(14) }),
1935
+ /* @__PURE__ */ jsx15(Text15, { color: statusColor, children: epic.status.padEnd(14) }),
1936
+ /* @__PURE__ */ jsx15(Text15, { color: isCurrent ? theme.titleHighlight : theme.table.fg, children: epic.name })
1937
+ ] }) }, epic.id);
1938
+ }),
1939
+ /* @__PURE__ */ jsx15(Box15, { flexGrow: 1 }),
1940
+ filtered.length > maxVisible && /* @__PURE__ */ jsx15(Box15, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs13(Text15, { dimColor: true, children: [
1941
+ "[",
1942
+ viewStart + 1,
1943
+ "-",
1944
+ Math.min(viewStart + maxVisible, filtered.length),
1945
+ "/",
1946
+ filtered.length,
1947
+ "]"
1948
+ ] }) }),
1949
+ /* @__PURE__ */ jsx15(Box15, { paddingX: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "enter: assign | x: unassign | /: search | esc: cancel" }) })
1950
+ ] });
1951
+ }
1952
+
1953
+ // src/tui/components/App.tsx
1954
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1639
1955
  var STATUS_CYCLE = [
1640
1956
  TaskStatus.Backlog,
1641
1957
  TaskStatus.Todo,
@@ -1658,10 +1974,13 @@ function App({ container, initialProject }) {
1658
1974
  }, [container]);
1659
1975
  const loadTasks = useCallback2(() => {
1660
1976
  logger.startSpan("TUI.loadTasks", () => {
1661
- const filter = { ...state.filter };
1977
+ const filter = { ...state.filter, level: TaskLevel.Work };
1662
1978
  if (state.activeProject) {
1663
1979
  filter.projectId = state.activeProject.id;
1664
1980
  }
1981
+ if (state.selectedEpicIds.size > 0) {
1982
+ filter.parentIds = [...state.selectedEpicIds];
1983
+ }
1665
1984
  const result = container.taskService.listTasks(filter);
1666
1985
  if (result.ok) {
1667
1986
  logger.info(`TUI.loadTasks: loaded ${result.value.length} tasks`);
@@ -1671,7 +1990,19 @@ function App({ container, initialProject }) {
1671
1990
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1672
1991
  }
1673
1992
  });
1674
- }, [container, state.filter, state.activeProject]);
1993
+ }, [container, state.filter, state.activeProject, state.selectedEpicIds]);
1994
+ const loadEpics = useCallback2(() => {
1995
+ if (!state.activeProject) return;
1996
+ const result = container.taskService.listTasks({
1997
+ projectId: state.activeProject.id,
1998
+ level: TaskLevel.Epic
1999
+ });
2000
+ if (result.ok) {
2001
+ dispatch({ type: "SET_EPICS", epics: result.value });
2002
+ } else {
2003
+ logger.error("TUI.loadEpics: failed", result.error);
2004
+ }
2005
+ }, [container, state.activeProject]);
1675
2006
  const loadDeps = useCallback2(
1676
2007
  (taskId) => {
1677
2008
  const blockersResult = container.dependencyService.listBlockers(taskId);
@@ -1700,11 +2031,12 @@ function App({ container, initialProject }) {
1700
2031
  dispatch({ type: "SELECT_TASK", task: result.value });
1701
2032
  }
1702
2033
  loadTasks();
2034
+ loadEpics();
1703
2035
  } else {
1704
2036
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1705
2037
  }
1706
2038
  },
1707
- [container, state.selectedTask, loadTasks]
2039
+ [container, state.selectedTask, loadTasks, loadEpics]
1708
2040
  );
1709
2041
  const saveReorder = useCallback2(() => {
1710
2042
  const tasks = state.tasks;
@@ -1722,6 +2054,22 @@ function App({ container, initialProject }) {
1722
2054
  });
1723
2055
  loadTasks();
1724
2056
  }, [container, state.tasks, state.selectedIndex, loadTasks]);
2057
+ const saveEpicReorder = useCallback2(() => {
2058
+ const epics = state.epics;
2059
+ const idx = state.epicSelectedIndex;
2060
+ const epic = epics[idx];
2061
+ if (!epic) return;
2062
+ const prev = epics[idx - 1];
2063
+ const next = epics[idx + 1];
2064
+ 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 });
2065
+ dispatch({ type: "EXIT_EPIC_REORDER", save: result.ok });
2066
+ dispatch({
2067
+ type: "FLASH",
2068
+ message: result.ok ? "Epic rank saved" : result.error.message,
2069
+ level: result.ok ? "info" : "error"
2070
+ });
2071
+ loadEpics();
2072
+ }, [container, state.epics, state.epicSelectedIndex, loadEpics]);
1725
2073
  useEffect(() => {
1726
2074
  loadProjects();
1727
2075
  }, [loadProjects]);
@@ -1745,8 +2093,9 @@ function App({ container, initialProject }) {
1745
2093
  useEffect(() => {
1746
2094
  if (state.activeProject) {
1747
2095
  loadTasks();
2096
+ loadEpics();
1748
2097
  }
1749
- }, [state.activeProject, state.filter, loadTasks]);
2098
+ }, [state.activeProject, state.filter, loadTasks, loadEpics]);
1750
2099
  useEffect(() => {
1751
2100
  if (state.flash) {
1752
2101
  const timer = setTimeout(() => {
@@ -1758,7 +2107,7 @@ function App({ container, initialProject }) {
1758
2107
  }
1759
2108
  return void 0;
1760
2109
  }, [state.flash]);
1761
- useInput5((input, key) => {
2110
+ useInput6((input, key) => {
1762
2111
  if (state.confirmDelete) {
1763
2112
  if (input === "y") {
1764
2113
  const result = container.taskService.deleteTask(state.confirmDelete.id);
@@ -1769,6 +2118,7 @@ function App({ container, initialProject }) {
1769
2118
  dispatch({ type: "GO_BACK" });
1770
2119
  }
1771
2120
  loadTasks();
2121
+ loadEpics();
1772
2122
  } else {
1773
2123
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1774
2124
  dispatch({ type: "CANCEL_DELETE" });
@@ -1782,7 +2132,7 @@ function App({ container, initialProject }) {
1782
2132
  dispatch({ type: "GO_BACK" });
1783
2133
  return;
1784
2134
  }
1785
- if (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit || state.activeView === ViewType.ProjectSelector || state.activeView === ViewType.ProjectCreate) {
2135
+ if (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit || state.activeView === ViewType.ProjectSelector || state.activeView === ViewType.ProjectCreate || state.activeView === ViewType.EpicPicker) {
1786
2136
  return;
1787
2137
  }
1788
2138
  if (state.activeView === ViewType.DependencyList && state.isAddingDep) {
@@ -1853,6 +2203,26 @@ function App({ container, initialProject }) {
1853
2203
  }
1854
2204
  return;
1855
2205
  }
2206
+ if (state.isEpicReordering) {
2207
+ if (key.upArrow || input === "k") {
2208
+ dispatch({ type: "EPIC_REORDER_MOVE", direction: "up" });
2209
+ return;
2210
+ }
2211
+ if (key.downArrow || input === "j") {
2212
+ dispatch({ type: "EPIC_REORDER_MOVE", direction: "down" });
2213
+ return;
2214
+ }
2215
+ if (key.rightArrow) {
2216
+ saveEpicReorder();
2217
+ return;
2218
+ }
2219
+ if (key.escape || key.leftArrow) {
2220
+ dispatch({ type: "EXIT_EPIC_REORDER", save: false });
2221
+ dispatch({ type: "FLASH", message: "Epic reorder cancelled", level: "info" });
2222
+ return;
2223
+ }
2224
+ return;
2225
+ }
1856
2226
  if (state.isReordering) {
1857
2227
  if (key.upArrow || input === "k") {
1858
2228
  dispatch({ type: "REORDER_MOVE", direction: "up" });
@@ -1889,13 +2259,49 @@ function App({ container, initialProject }) {
1889
2259
  dispatch({ type: "NAVIGATE_TO", view: ViewType.Help });
1890
2260
  return;
1891
2261
  }
1892
- if (key.tab && state.activeView === ViewType.TaskList && previewTask) {
1893
- dispatch({
1894
- type: "SET_PANEL_FOCUS",
1895
- panel: state.focusedPanel === "list" ? "detail" : "list"
1896
- });
2262
+ if (key.tab && state.activeView === ViewType.TaskList) {
2263
+ const panels = previewTask ? ["epic", "list", "detail"] : ["epic", "list"];
2264
+ const curIdx = panels.indexOf(state.focusedPanel);
2265
+ const nextPanel = panels[(curIdx + 1) % panels.length] ?? "list";
2266
+ dispatch({ type: "SET_PANEL_FOCUS", panel: nextPanel });
1897
2267
  return;
1898
2268
  }
2269
+ if (state.activeView === ViewType.TaskList && state.focusedPanel === "epic") {
2270
+ if (key.upArrow || input === "k") {
2271
+ dispatch({ type: "EPIC_MOVE_CURSOR", direction: "up" });
2272
+ return;
2273
+ }
2274
+ if (key.downArrow || input === "j") {
2275
+ dispatch({ type: "EPIC_MOVE_CURSOR", direction: "down" });
2276
+ return;
2277
+ }
2278
+ if (input === " " || key.return) {
2279
+ const epic = state.epics[state.epicSelectedIndex];
2280
+ if (epic) {
2281
+ dispatch({ type: "TOGGLE_EPIC", epicId: epic.id });
2282
+ }
2283
+ return;
2284
+ }
2285
+ if (input === "0") {
2286
+ dispatch({ type: "CLEAR_EPIC_SELECTION" });
2287
+ return;
2288
+ }
2289
+ if (key.leftArrow) {
2290
+ if (state.epics.length > 0) {
2291
+ dispatch({ type: "ENTER_EPIC_REORDER" });
2292
+ dispatch({
2293
+ type: "FLASH",
2294
+ message: "Reorder: \u2191\u2193 move, \u2192 save, \u2190 cancel",
2295
+ level: "info"
2296
+ });
2297
+ }
2298
+ return;
2299
+ }
2300
+ if (input === "q") {
2301
+ exit();
2302
+ return;
2303
+ }
2304
+ }
1899
2305
  if (state.activeView === ViewType.TaskList && state.focusedPanel === "list") {
1900
2306
  if (key.upArrow || input === "k") {
1901
2307
  dispatch({ type: "MOVE_CURSOR", direction: "up" });
@@ -1949,6 +2355,30 @@ function App({ container, initialProject }) {
1949
2355
  }
1950
2356
  return;
1951
2357
  }
2358
+ if (input === "a") {
2359
+ const task = state.tasks[state.selectedIndex];
2360
+ if (task) {
2361
+ dispatch({ type: "SELECT_TASK", task });
2362
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.EpicPicker });
2363
+ }
2364
+ return;
2365
+ }
2366
+ if (input === "A") {
2367
+ const task = state.tasks[state.selectedIndex];
2368
+ if (task && task.parentId) {
2369
+ const result = container.taskService.updateTask(task.id, { parentId: null });
2370
+ if (result.ok) {
2371
+ dispatch({ type: "FLASH", message: "Unassigned from epic", level: "info" });
2372
+ loadTasks();
2373
+ loadEpics();
2374
+ } else {
2375
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2376
+ }
2377
+ } else if (task) {
2378
+ dispatch({ type: "FLASH", message: "Task has no epic", level: "warn" });
2379
+ }
2380
+ return;
2381
+ }
1952
2382
  if (input === "/") {
1953
2383
  dispatch({ type: "SET_SEARCH_ACTIVE", active: true });
1954
2384
  dispatch({ type: "SET_SEARCH_QUERY", query: "" });
@@ -1959,6 +2389,22 @@ function App({ container, initialProject }) {
1959
2389
  return;
1960
2390
  }
1961
2391
  if (key.leftArrow) {
2392
+ if (state.selectedEpicIds.size > 0) {
2393
+ dispatch({
2394
+ type: "FLASH",
2395
+ message: "Clear epic filter (0) before reordering",
2396
+ level: "warn"
2397
+ });
2398
+ return;
2399
+ }
2400
+ if (state.filter.status || state.filter.type || state.filter.search) {
2401
+ dispatch({
2402
+ type: "FLASH",
2403
+ message: "Clear filters (0) before reordering",
2404
+ level: "warn"
2405
+ });
2406
+ return;
2407
+ }
1962
2408
  if (state.tasks.length > 0) {
1963
2409
  dispatch({ type: "ENTER_REORDER" });
1964
2410
  dispatch({ type: "FLASH", message: "Reorder: \u2191\u2193 move, \u2192 save, \u2190 cancel", level: "info" });
@@ -1987,6 +2433,14 @@ function App({ container, initialProject }) {
1987
2433
  }
1988
2434
  }
1989
2435
  if (state.activeView === ViewType.TaskList && state.focusedPanel === "detail" && previewTask) {
2436
+ if (key.upArrow || input === "k") {
2437
+ dispatch({ type: "DETAIL_SCROLL", direction: "up" });
2438
+ return;
2439
+ }
2440
+ if (key.downArrow || input === "j") {
2441
+ dispatch({ type: "DETAIL_SCROLL", direction: "down" });
2442
+ return;
2443
+ }
1990
2444
  if (input === "e") {
1991
2445
  dispatch({ type: "SELECT_TASK", task: previewTask });
1992
2446
  dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
@@ -2022,6 +2476,14 @@ ${previewTask.additionalRequirements}`;
2022
2476
  }
2023
2477
  }
2024
2478
  if (state.activeView === ViewType.TaskDetail) {
2479
+ if (key.upArrow || input === "k") {
2480
+ dispatch({ type: "DETAIL_SCROLL", direction: "up" });
2481
+ return;
2482
+ }
2483
+ if (key.downArrow || input === "j") {
2484
+ dispatch({ type: "DETAIL_SCROLL", direction: "down" });
2485
+ return;
2486
+ }
2025
2487
  if (input === "e" && state.selectedTask) {
2026
2488
  dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
2027
2489
  return;
@@ -2141,22 +2603,53 @@ ${state.selectedTask.additionalRequirements}`;
2141
2603
  loadDeps(taskId);
2142
2604
  dispatch({ type: "GO_BACK" });
2143
2605
  loadTasks();
2606
+ loadEpics();
2144
2607
  } else {
2145
2608
  const result = container.taskService.createTask(data, state.activeProject?.id);
2146
2609
  if (result.ok) {
2147
2610
  dispatch({ type: "FLASH", message: "Task created", level: "info" });
2148
2611
  dispatch({ type: "GO_BACK" });
2149
2612
  loadTasks();
2613
+ loadEpics();
2150
2614
  } else {
2151
2615
  dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2152
2616
  }
2153
2617
  }
2154
2618
  },
2155
- [container, state.activeView, state.selectedTask, state.activeProject, loadTasks, loadDeps]
2619
+ [
2620
+ container,
2621
+ state.activeView,
2622
+ state.selectedTask,
2623
+ state.activeProject,
2624
+ loadTasks,
2625
+ loadDeps,
2626
+ loadEpics
2627
+ ]
2156
2628
  );
2157
2629
  const handleFormCancel = useCallback2(() => {
2158
2630
  dispatch({ type: "GO_BACK" });
2159
2631
  }, []);
2632
+ const handleEpicPickerSelect = useCallback2(
2633
+ (epicId) => {
2634
+ if (!state.selectedTask) return;
2635
+ const result = container.taskService.updateTask(state.selectedTask.id, {
2636
+ parentId: epicId
2637
+ });
2638
+ if (result.ok) {
2639
+ const msg = epicId ? `Assigned to ${epicId}` : "Unassigned from epic";
2640
+ dispatch({ type: "FLASH", message: msg, level: "info" });
2641
+ loadTasks();
2642
+ loadEpics();
2643
+ } else {
2644
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2645
+ }
2646
+ dispatch({ type: "GO_BACK" });
2647
+ },
2648
+ [container, state.selectedTask, loadTasks, loadEpics]
2649
+ );
2650
+ const handleEpicPickerCancel = useCallback2(() => {
2651
+ dispatch({ type: "GO_BACK" });
2652
+ }, []);
2160
2653
  const handleProjectSelect = useCallback2((project) => {
2161
2654
  dispatch({ type: "SET_ACTIVE_PROJECT", project });
2162
2655
  dispatch({ type: "GO_BACK" });
@@ -2167,7 +2660,11 @@ ${state.selectedTask.additionalRequirements}`;
2167
2660
  const result = container.projectService.updateProject(project.id, { isDefault: true });
2168
2661
  if (result.ok) {
2169
2662
  logger.info(`TUI.setDefault: set key=${result.value.key} as default`);
2170
- dispatch({ type: "FLASH", message: `Default project: ${result.value.name}`, level: "info" });
2663
+ dispatch({
2664
+ type: "FLASH",
2665
+ message: `Default project: ${result.value.name}`,
2666
+ level: "info"
2667
+ });
2171
2668
  loadProjects();
2172
2669
  } else {
2173
2670
  logger.error("TUI.setDefault: failed", result.error);
@@ -2234,12 +2731,22 @@ ${state.selectedTask.additionalRequirements}`;
2234
2731
  loadDeps(previewTaskId);
2235
2732
  }
2236
2733
  }, [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(
2734
+ return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", height: "100%", children: [
2735
+ /* @__PURE__ */ jsx16(Header, { state }),
2736
+ /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", flexGrow: 1, children: [
2737
+ state.confirmDelete && /* @__PURE__ */ jsx16(ConfirmDialog, { task: state.confirmDelete }),
2738
+ !state.confirmDelete && state.activeView === ViewType.TaskList && /* @__PURE__ */ jsxs14(Box16, { flexDirection: "row", flexGrow: 1, children: [
2739
+ /* @__PURE__ */ jsx16(
2740
+ EpicPanel,
2741
+ {
2742
+ epics: state.epics,
2743
+ selectedIndex: state.epicSelectedIndex,
2744
+ selectedEpicIds: state.selectedEpicIds,
2745
+ isFocused: state.focusedPanel === "epic",
2746
+ isReordering: state.isEpicReordering
2747
+ }
2748
+ ),
2749
+ /* @__PURE__ */ jsx16(Box16, { flexGrow: 2, children: /* @__PURE__ */ jsx16(
2243
2750
  TaskList,
2244
2751
  {
2245
2752
  tasks: state.tasks,
@@ -2256,10 +2763,11 @@ ${state.selectedTask.additionalRequirements}`;
2256
2763
  state.depDependents.filter((t) => !isTerminalStatus(t.status)).map((t) => t.id)
2257
2764
  ),
2258
2765
  isSelectedBlocked: state.depBlockers.some((t) => !isTerminalStatus(t.status)),
2259
- isFocused: state.focusedPanel === "list"
2766
+ isFocused: state.focusedPanel === "list",
2767
+ epicFilterActive: state.selectedEpicIds.size > 0
2260
2768
  }
2261
2769
  ) }),
2262
- /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, children: previewTask ? /* @__PURE__ */ jsx14(
2770
+ /* @__PURE__ */ jsx16(Box16, { flexGrow: 1, children: previewTask ? /* @__PURE__ */ jsx16(
2263
2771
  TaskDetail,
2264
2772
  {
2265
2773
  task: previewTask,
@@ -2267,36 +2775,38 @@ ${state.selectedTask.additionalRequirements}`;
2267
2775
  dependents: state.depDependents,
2268
2776
  related: state.depRelated,
2269
2777
  duplicates: state.depDuplicates,
2270
- isFocused: state.focusedPanel === "detail"
2778
+ isFocused: state.focusedPanel === "detail",
2779
+ scrollOffset: state.detailScrollOffset
2271
2780
  }
2272
- ) : /* @__PURE__ */ jsxs12(
2273
- Box14,
2781
+ ) : /* @__PURE__ */ jsxs14(
2782
+ Box16,
2274
2783
  {
2275
2784
  flexDirection: "column",
2276
2785
  flexGrow: 1,
2277
2786
  borderStyle: "bold",
2278
2787
  borderColor: theme.border,
2279
2788
  children: [
2280
- /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { color: theme.title, bold: true, children: [
2789
+ /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsxs14(Text16, { color: theme.title, bold: true, children: [
2281
2790
  " ",
2282
2791
  "detail"
2283
2792
  ] }) }),
2284
- /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No task selected" }) })
2793
+ /* @__PURE__ */ jsx16(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx16(Text16, { dimColor: true, children: "No task selected" }) })
2285
2794
  ]
2286
2795
  }
2287
2796
  ) })
2288
2797
  ] }),
2289
- !state.confirmDelete && state.activeView === ViewType.TaskDetail && state.selectedTask && /* @__PURE__ */ jsx14(
2798
+ !state.confirmDelete && state.activeView === ViewType.TaskDetail && state.selectedTask && /* @__PURE__ */ jsx16(
2290
2799
  TaskDetail,
2291
2800
  {
2292
2801
  task: state.selectedTask,
2293
2802
  blockers: state.depBlockers,
2294
2803
  dependents: state.depDependents,
2295
2804
  related: state.depRelated,
2296
- duplicates: state.depDuplicates
2805
+ duplicates: state.depDuplicates,
2806
+ scrollOffset: state.detailScrollOffset
2297
2807
  }
2298
2808
  ),
2299
- !state.confirmDelete && state.activeView === ViewType.DependencyList && state.selectedTask && /* @__PURE__ */ jsx14(
2809
+ !state.confirmDelete && state.activeView === ViewType.DependencyList && state.selectedTask && /* @__PURE__ */ jsx16(
2300
2810
  DependencyList,
2301
2811
  {
2302
2812
  task: state.selectedTask,
@@ -2309,7 +2819,7 @@ ${state.selectedTask.additionalRequirements}`;
2309
2819
  addDepInput: state.addDepInput
2310
2820
  }
2311
2821
  ),
2312
- !state.confirmDelete && (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit) && /* @__PURE__ */ jsx14(
2822
+ !state.confirmDelete && (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit) && /* @__PURE__ */ jsx16(
2313
2823
  TaskForm,
2314
2824
  {
2315
2825
  editingTask: state.activeView === ViewType.TaskEdit ? state.selectedTask : null,
@@ -2319,7 +2829,16 @@ ${state.selectedTask.additionalRequirements}`;
2319
2829
  onCancel: handleFormCancel
2320
2830
  }
2321
2831
  ),
2322
- !state.confirmDelete && state.activeView === ViewType.ProjectSelector && /* @__PURE__ */ jsx14(
2832
+ !state.confirmDelete && state.activeView === ViewType.EpicPicker && state.selectedTask && /* @__PURE__ */ jsx16(
2833
+ EpicPicker,
2834
+ {
2835
+ epics: state.epics,
2836
+ currentEpicId: state.selectedTask.parentId,
2837
+ onSelect: handleEpicPickerSelect,
2838
+ onCancel: handleEpicPickerCancel
2839
+ }
2840
+ ),
2841
+ !state.confirmDelete && state.activeView === ViewType.ProjectSelector && /* @__PURE__ */ jsx16(
2323
2842
  ProjectSelector,
2324
2843
  {
2325
2844
  projects: state.projects,
@@ -2330,18 +2849,18 @@ ${state.selectedTask.additionalRequirements}`;
2330
2849
  onCancel: handleProjectCancel
2331
2850
  }
2332
2851
  ),
2333
- !state.confirmDelete && state.activeView === ViewType.ProjectCreate && /* @__PURE__ */ jsx14(ProjectForm, { onSave: handleProjectFormSave, onCancel: handleProjectFormCancel }),
2334
- !state.confirmDelete && state.activeView === ViewType.Help && /* @__PURE__ */ jsx14(HelpOverlay, {})
2852
+ !state.confirmDelete && state.activeView === ViewType.ProjectCreate && /* @__PURE__ */ jsx16(ProjectForm, { onSave: handleProjectFormSave, onCancel: handleProjectFormCancel }),
2853
+ !state.confirmDelete && state.activeView === ViewType.Help && /* @__PURE__ */ jsx16(HelpOverlay, {})
2335
2854
  ] }),
2336
- /* @__PURE__ */ jsx14(Crumbs, { breadcrumbs: state.breadcrumbs }),
2337
- state.flash && /* @__PURE__ */ jsx14(FlashMessage, { message: state.flash.message, level: state.flash.level })
2855
+ /* @__PURE__ */ jsx16(Crumbs, { breadcrumbs: state.breadcrumbs }),
2856
+ state.flash && /* @__PURE__ */ jsx16(FlashMessage, { message: state.flash.message, level: state.flash.level })
2338
2857
  ] });
2339
2858
  }
2340
2859
 
2341
2860
  // src/tui/index.tsx
2342
- import { jsx as jsx15 } from "react/jsx-runtime";
2861
+ import { jsx as jsx17 } from "react/jsx-runtime";
2343
2862
  async function launchTUI(container, initialProject) {
2344
- const instance = render(/* @__PURE__ */ jsx15(App, { container, initialProject }), {
2863
+ const instance = render(/* @__PURE__ */ jsx17(App, { container, initialProject }), {
2345
2864
  exitOnCtrlC: true
2346
2865
  });
2347
2866
  await instance.waitUntilExit();
@@ -2349,4 +2868,4 @@ async function launchTUI(container, initialProject) {
2349
2868
  export {
2350
2869
  launchTUI
2351
2870
  };
2352
- //# sourceMappingURL=tui-JNZRBEIQ.js.map
2871
+ //# sourceMappingURL=tui-FTXYP3HM.js.map