@tomkapa/tayto 0.2.0 → 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.
@@ -13,8 +13,8 @@ import {
13
13
  import { render } from "ink";
14
14
 
15
15
  // src/tui/components/App.tsx
16
- import { useReducer, useEffect, useCallback as useCallback2, useMemo } from "react";
17
- import { Box as Box16, Text as Text16, useInput as useInput6, 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";
18
18
 
19
19
  // src/tui/types.ts
20
20
  var ViewType = {
@@ -29,6 +29,140 @@ var ViewType = {
29
29
  Help: "help"
30
30
  };
31
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
+
32
166
  // src/tui/state.ts
33
167
  var initialState = {
34
168
  activeView: ViewType.TaskList,
@@ -121,6 +255,11 @@ function appReducer(state, action) {
121
255
  const newIndex = action.direction === "up" ? Math.max(0, state.selectedIndex - 1) : Math.min(maxIndex, state.selectedIndex + 1);
122
256
  return { ...state, selectedIndex: newIndex, detailScrollOffset: 0 };
123
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 };
262
+ }
124
263
  case "SET_CURSOR": {
125
264
  const maxIndex = Math.max(0, state.tasks.length - 1);
126
265
  return { ...state, selectedIndex: Math.max(0, Math.min(action.index, maxIndex)) };
@@ -260,139 +399,6 @@ function appReducer(state, action) {
260
399
  }
261
400
  }
262
401
 
263
- // src/tui/theme.ts
264
- var theme = {
265
- // Body
266
- fg: "#1E90FF",
267
- // dodgerblue
268
- bg: "black",
269
- logo: "#FFA500",
270
- // orange
271
- // Table
272
- table: {
273
- fg: "#00FFFF",
274
- // aqua
275
- cursorFg: "black",
276
- cursorBg: "#00FFFF",
277
- // aqua
278
- headerFg: "white",
279
- markColor: "#98FB98",
280
- // palegreen
281
- depHighlightBg: "#1A5276",
282
- // dark steel blue – non-terminal dep rows
283
- blockedCursorBg: "#C0392B"
284
- // strong red – selected row blocked by non-terminal dep
285
- },
286
- // Status colors (row-level)
287
- status: {
288
- new: "#87CEFA",
289
- // lightskyblue
290
- modified: "#ADFF2F",
291
- // greenyellow
292
- added: "#1E90FF",
293
- // dodgerblue
294
- error: "#FF4500",
295
- // orangered
296
- pending: "#FF8C00",
297
- // darkorange
298
- highlight: "#00FFFF",
299
- // aqua
300
- kill: "#9370DB",
301
- // mediumpurple
302
- completed: "#778899"
303
- // lightslategray
304
- },
305
- // Frame / borders
306
- border: "#1E90FF",
307
- // dodgerblue
308
- borderFocus: "#00FFFF",
309
- // aqua
310
- // Title
311
- title: "#00FFFF",
312
- // aqua
313
- titleHighlight: "#FF00FF",
314
- // fuchsia
315
- titleCounter: "#FFEFD5",
316
- // papayawhip
317
- titleFilter: "#2E8B57",
318
- // seagreen
319
- // Prompt
320
- prompt: "#5F9EA0",
321
- // cadetblue
322
- promptSuggest: "#1E90FF",
323
- // dodgerblue
324
- // Dialog
325
- dialog: {
326
- fg: "#1E90FF",
327
- // dodgerblue
328
- buttonFg: "black",
329
- buttonBg: "#1E90FF",
330
- buttonFocusFg: "white",
331
- buttonFocusBg: "#FF00FF",
332
- // fuchsia
333
- label: "#FF00FF",
334
- // fuchsia
335
- field: "#1E90FF"
336
- },
337
- // Detail/YAML
338
- yaml: {
339
- key: "#4682B4",
340
- // steelblue
341
- colon: "white",
342
- value: "#FFEFD5"
343
- // papayawhip
344
- },
345
- // Crumbs
346
- crumb: {
347
- fg: "black",
348
- bg: "#4682B4",
349
- // steelblue
350
- activeBg: "#FFA500"
351
- // orange
352
- },
353
- // Flash
354
- flash: {
355
- info: "#FFDEAD",
356
- // navajowhite
357
- warn: "#FFA500",
358
- // orange
359
- error: "#FF4500"
360
- // orangered
361
- },
362
- // Menu / key hints
363
- menu: {
364
- key: "#1E90FF",
365
- // dodgerblue
366
- numKey: "#FF00FF",
367
- // fuchsia
368
- desc: "white"
369
- }
370
- };
371
-
372
- // src/tui/constants.ts
373
- var STATUS_VALUES = Object.values(TaskStatus);
374
- var TYPE_VALUES = Object.values(TaskType);
375
- var STATUS_COLOR = {
376
- [TaskStatus.Backlog]: theme.status.completed,
377
- [TaskStatus.Todo]: theme.status.new,
378
- [TaskStatus.InProgress]: theme.status.pending,
379
- [TaskStatus.Review]: theme.status.modified,
380
- [TaskStatus.Done]: theme.status.added,
381
- [TaskStatus.Cancelled]: theme.status.kill
382
- };
383
- var TYPE_COLOR = {
384
- [TaskType.Epic]: theme.status.modified,
385
- [TaskType.Story]: theme.status.highlight,
386
- [TaskType.TechDebt]: theme.status.pending,
387
- [TaskType.Bug]: theme.status.error
388
- };
389
- var DEP_TYPE_LABEL = {
390
- [DependencyType.Blocks]: "blocks",
391
- [DependencyType.RelatesTo]: "relates-to",
392
- [DependencyType.Duplicates]: "duplicates",
393
- [UIDependencyType.BlockedBy]: "blocked-by"
394
- };
395
-
396
402
  // src/tui/components/Header.tsx
397
403
  import { Box, Text } from "ink";
398
404
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -428,6 +434,7 @@ function getKeyHints(view, isSearchActive, focusedPanel) {
428
434
  { key: "p", desc: "project" },
429
435
  { key: "f", desc: "status-f" },
430
436
  { key: "t", desc: "type-f" },
437
+ { key: "PgDn/Up", desc: "page" },
431
438
  { key: "tab", desc: "panel" },
432
439
  { key: "?", desc: "help" },
433
440
  { key: "q", desc: "quit" }
@@ -517,7 +524,6 @@ function FlashMessage({ message, level }) {
517
524
  // src/tui/components/TaskList.tsx
518
525
  import { Box as Box4, Text as Text4 } from "ink";
519
526
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
520
- var PAGE_SIZE = 20;
521
527
  var COL = {
522
528
  rank: 5,
523
529
  type: 12,
@@ -588,7 +594,7 @@ function TaskList({
588
594
  /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "STATUS".padEnd(COL.status) }),
589
595
  /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "NAME" })
590
596
  ] }),
591
- 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) => {
592
598
  const actualIndex = viewStart + i;
593
599
  const isSelected = actualIndex === selectedIndex;
594
600
  const isNonTerminalBlocker = nonTerminalBlockerIds.has(task.id);
@@ -622,8 +628,7 @@ function TaskList({
622
628
  /* @__PURE__ */ jsx4(Text4, { color: STATUS_COLOR[task.status] ?? rowColor, children: task.status.padEnd(COL.status) }),
623
629
  /* @__PURE__ */ jsx4(Text4, { color: rowColor, children: task.name })
624
630
  ] }, task.id);
625
- }),
626
- /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
631
+ }) }),
627
632
  tasks.length > PAGE_SIZE && /* @__PURE__ */ jsx4(Box4, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs2(Text4, { dimColor: true, children: [
628
633
  "[",
629
634
  viewStart + 1,
@@ -639,7 +644,9 @@ function TaskList({
639
644
  }
640
645
 
641
646
  // src/tui/components/TaskDetail.tsx
647
+ import { useMemo } from "react";
642
648
  import { Box as Box6, Text as Text6, useStdout } from "ink";
649
+ import chalk2 from "chalk";
643
650
 
644
651
  // src/tui/components/Markdown.tsx
645
652
  import { Text as Text5, Box as Box5 } from "ink";
@@ -731,15 +738,10 @@ function createRenderer() {
731
738
  );
732
739
  }
733
740
  var marked = createRenderer();
734
- function Markdown({ content }) {
735
- if (!content.trim()) {
736
- return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No content" });
737
- }
741
+ function renderMarkdown(content) {
742
+ if (!content.trim()) return "";
738
743
  const rendered = marked.parse(content);
739
- if (typeof rendered !== "string") {
740
- return /* @__PURE__ */ jsx5(Text5, { children: content });
741
- }
742
- return /* @__PURE__ */ jsx5(Text5, { children: rendered.trimEnd() });
744
+ return typeof rendered === "string" ? rendered.trimEnd() : content;
743
745
  }
744
746
  function MermaidHint({ content }) {
745
747
  const blocks = extractMermaidBlocks(content);
@@ -768,19 +770,74 @@ function openAllMermaidDiagrams(content) {
768
770
 
769
771
  // src/tui/components/TaskDetail.tsx
770
772
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
771
- function Field({ label, value }) {
772
- return /* @__PURE__ */ jsxs4(Box6, { gap: 0, children: [
773
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.key, bold: true, children: label }),
774
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.colon, children: ": " }),
775
- /* @__PURE__ */ jsx6(Text6, { color: theme.yaml.value, children: value })
776
- ] });
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);
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;
777
833
  }
834
+ var CHROME_LINES = 8;
778
835
  function TaskDetail({
779
836
  task,
780
- blockers,
781
- dependents,
782
- related,
783
- duplicates,
837
+ blockers = [],
838
+ dependents = [],
839
+ related = [],
840
+ duplicates = [],
784
841
  isFocused = true,
785
842
  scrollOffset = 0
786
843
  }) {
@@ -788,7 +845,12 @@ function TaskDetail({
788
845
  const allText = `${task.description}
789
846
  ${task.technicalNotes}
790
847
  ${task.additionalRequirements}`;
791
- const viewportHeight = Math.max(5, (stdout.rows > 0 ? stdout.rows : 24) - 4);
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);
792
854
  return /* @__PURE__ */ jsxs4(
793
855
  Box6,
794
856
  {
@@ -810,60 +872,7 @@ ${task.additionalRequirements}`;
810
872
  scrollOffset
811
873
  ] })
812
874
  ] }),
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 })
821
- ] }),
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(", ") })
830
- ] }),
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(", ") })
837
- ] }),
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(", ") })
851
- ] }),
852
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "press D to manage dependencies" })
853
- ] }),
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
- ] }) }),
875
+ /* @__PURE__ */ jsx6(Text6, { children: visibleLines.join("\n") }),
867
876
  /* @__PURE__ */ jsx6(Box6, { flexGrow: 1 }),
868
877
  /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(MermaidHint, { content: allText }) })
869
878
  ]
@@ -1143,13 +1152,13 @@ function TaskForm({ editingTask, allTasks, initialDeps, onSave, onCancel }) {
1143
1152
  const [pickedDeps, setPickedDeps] = useState2(initialDeps ?? []);
1144
1153
  const currentField = FIELDS[focusIndex];
1145
1154
  const launchEditor = useCallback(
1146
- (field) => {
1155
+ (field2) => {
1147
1156
  setEditorActive(true);
1148
1157
  setTimeout(() => {
1149
- const content = values[field.key] ?? "";
1150
- 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`);
1151
1160
  if (result !== null) {
1152
- setValues((v) => ({ ...v, [field.key]: result }));
1161
+ setValues((v) => ({ ...v, [field2.key]: result }));
1153
1162
  }
1154
1163
  setEditorActive(false);
1155
1164
  }, 50);
@@ -1257,37 +1266,37 @@ function TaskForm({ editingTask, allTasks, initialDeps, onSave, onCancel }) {
1257
1266
  " ",
1258
1267
  isEdit ? "edit" : "create"
1259
1268
  ] }) }),
1260
- /* @__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) => {
1261
1270
  const isFocused = i === focusIndex;
1262
- const value = field.key === "dependsOn" ? depSummary : values[field.key] ?? "";
1271
+ const value = field2.key === "dependsOn" ? depSummary : values[field2.key] ?? "";
1263
1272
  const displayValue = value;
1264
1273
  return /* @__PURE__ */ jsxs6(Box8, { gap: 1, children: [
1265
1274
  /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.dialog.label : theme.yaml.key, bold: isFocused, children: [
1266
1275
  isFocused ? ">" : " ",
1267
1276
  " ",
1268
- field.label.padEnd(14)
1277
+ field2.label.padEnd(14)
1269
1278
  ] }),
1270
- 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: [
1271
1280
  displayValue,
1272
1281
  isFocused ? /* @__PURE__ */ jsx8(Text8, { color: theme.titleHighlight, children: "_" }) : ""
1273
1282
  ] }),
1274
- field.type === "picker" && /* @__PURE__ */ jsxs6(Text8, { children: [
1283
+ field2.type === "picker" && /* @__PURE__ */ jsxs6(Text8, { children: [
1275
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" }),
1276
1285
  isFocused && /* @__PURE__ */ jsx8(Text8, { color: theme.menu.key, children: " [enter: open picker]" })
1277
1286
  ] }),
1278
- field.type === "editor" && /* @__PURE__ */ jsxs6(Text8, { children: [
1287
+ field2.type === "editor" && /* @__PURE__ */ jsxs6(Text8, { children: [
1279
1288
  displayValue ? /* @__PURE__ */ jsxs6(Text8, { color: theme.status.added, children: [
1280
1289
  displayValue.split("\n")[0]?.slice(0, 50),
1281
1290
  displayValue.length > 50 || displayValue.includes("\n") ? "..." : ""
1282
1291
  ] }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: isFocused ? "press enter" : "empty" }),
1283
1292
  isFocused && /* @__PURE__ */ jsx8(Text8, { color: theme.menu.key, children: " [enter: $EDITOR]" })
1284
1293
  ] }),
1285
- 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: [
1286
1295
  isFocused ? "< " : " ",
1287
1296
  displayValue,
1288
1297
  isFocused ? " >" : ""
1289
1298
  ] })
1290
- ] }, field.key);
1299
+ ] }, field2.key);
1291
1300
  }) }),
1292
1301
  /* @__PURE__ */ jsx8(Box8, { flexGrow: 1 }),
1293
1302
  /* @__PURE__ */ jsx8(Box8, { paddingX: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "tab: next | shift+tab: prev | ctrl+s: save | esc: cancel" }) }),
@@ -1455,26 +1464,26 @@ function ProjectForm({ onSave, onCancel }) {
1455
1464
  " ",
1456
1465
  "new project"
1457
1466
  ] }) }),
1458
- /* @__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) => {
1459
1468
  const isFocused = i === focusIndex;
1460
- const value = values[field.key] ?? "";
1469
+ const value = values[field2.key] ?? "";
1461
1470
  return /* @__PURE__ */ jsxs8(Box10, { gap: 1, children: [
1462
1471
  /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.dialog.label : theme.yaml.key, bold: isFocused, children: [
1463
1472
  isFocused ? ">" : " ",
1464
1473
  " ",
1465
- field.label.padEnd(14)
1474
+ field2.label.padEnd(14)
1466
1475
  ] }),
1467
- 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: [
1468
1477
  value,
1469
1478
  isFocused ? /* @__PURE__ */ jsx10(Text10, { color: theme.titleHighlight, children: "_" }) : "",
1470
- 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)" : "" })
1471
1480
  ] }),
1472
- 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: [
1473
1482
  isFocused ? "< " : " ",
1474
1483
  value,
1475
1484
  isFocused ? " >" : ""
1476
1485
  ] })
1477
- ] }, field.key);
1486
+ ] }, field2.key);
1478
1487
  }) }),
1479
1488
  /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1480
1489
  /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "tab: next | shift+tab: prev | ctrl+s: save | esc: cancel" }) })
@@ -1490,6 +1499,8 @@ var SECTIONS = [
1490
1499
  keys: [
1491
1500
  ["j/k", "Up/Down"],
1492
1501
  ["g/G", "Top/Bottom"],
1502
+ ["PgDn", "Page down"],
1503
+ ["PgUp", "Page up"],
1493
1504
  ["enter", "View"],
1494
1505
  ["esc", "Back"]
1495
1506
  ]
@@ -1778,7 +1789,7 @@ function EpicPanel({
1778
1789
  selectedEpicIds.size
1779
1790
  ] })
1780
1791
  ] }),
1781
- epics.length === 0 ? /* @__PURE__ */ jsx14(Box14, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No epics" }) }) : visibleEpics.map((epic, i) => {
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) => {
1782
1793
  const actualIndex = viewStart + i;
1783
1794
  const isSelected = actualIndex === selectedIndex && isFocused;
1784
1795
  const isChecked = selectedEpicIds.has(epic.id);
@@ -1799,8 +1810,7 @@ function EpicPanel({
1799
1810
  " ",
1800
1811
  epic.name
1801
1812
  ] }) }, epic.id);
1802
- }),
1803
- /* @__PURE__ */ jsx14(Box14, { flexGrow: 1 }),
1813
+ }) }),
1804
1814
  epics.length > PAGE_SIZE2 && /* @__PURE__ */ jsx14(Box14, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
1805
1815
  "[",
1806
1816
  viewStart + 1,
@@ -1950,6 +1960,41 @@ function EpicPicker({ epics, currentEpicId, onSelect, onCancel }) {
1950
1960
  ] });
1951
1961
  }
1952
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
+
1953
1998
  // src/tui/components/App.tsx
1954
1999
  import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
1955
2000
  var STATUS_CYCLE = [
@@ -1959,9 +2004,25 @@ var STATUS_CYCLE = [
1959
2004
  TaskStatus.Review,
1960
2005
  TaskStatus.Done
1961
2006
  ];
2007
+ var EPIC_PANEL_WIDTH = 48;
1962
2008
  function App({ container, initialProject }) {
1963
2009
  const { exit } = useApp();
2010
+ const { stdout } = useStdout4();
1964
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;
1965
2026
  const loadProjects = useCallback2(() => {
1966
2027
  const result = container.projectService.listProjects();
1967
2028
  if (result.ok) {
@@ -2070,10 +2131,16 @@ function App({ container, initialProject }) {
2070
2131
  });
2071
2132
  loadEpics();
2072
2133
  }, [container, state.epics, state.epicSelectedIndex, loadEpics]);
2073
- useEffect(() => {
2134
+ const refetchAll = useCallback2(() => {
2135
+ loadProjects();
2136
+ loadTasks();
2137
+ loadEpics();
2138
+ }, [loadProjects, loadTasks, loadEpics]);
2139
+ useAutoRefetch(container.dbPath, refetchAll);
2140
+ useEffect2(() => {
2074
2141
  loadProjects();
2075
2142
  }, [loadProjects]);
2076
- useEffect(() => {
2143
+ useEffect2(() => {
2077
2144
  if (state.projects.length > 0 && !state.activeProject) {
2078
2145
  logger.info(`TUI.resolveProject: resolving initialProject=${initialProject ?? "(default)"}`);
2079
2146
  const result = container.projectService.resolveProject(initialProject);
@@ -2090,13 +2157,13 @@ function App({ container, initialProject }) {
2090
2157
  }
2091
2158
  }
2092
2159
  }, [state.projects, state.activeProject, initialProject, container]);
2093
- useEffect(() => {
2160
+ useEffect2(() => {
2094
2161
  if (state.activeProject) {
2095
2162
  loadTasks();
2096
2163
  loadEpics();
2097
2164
  }
2098
2165
  }, [state.activeProject, state.filter, loadTasks, loadEpics]);
2099
- useEffect(() => {
2166
+ useEffect2(() => {
2100
2167
  if (state.flash) {
2101
2168
  const timer = setTimeout(() => {
2102
2169
  dispatch({ type: "CLEAR_FLASH" });
@@ -2319,6 +2386,14 @@ function App({ container, initialProject }) {
2319
2386
  dispatch({ type: "SET_CURSOR", index: state.tasks.length - 1 });
2320
2387
  return;
2321
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
+ }
2322
2397
  if (key.return) {
2323
2398
  const task = state.tasks[state.selectedIndex];
2324
2399
  if (task) {
@@ -2709,7 +2784,7 @@ ${state.selectedTask.additionalRequirements}`;
2709
2784
  dispatch({ type: "GO_BACK" });
2710
2785
  }, []);
2711
2786
  const previewTask = state.tasks[state.selectedIndex] ?? null;
2712
- const initialDepsForEdit = useMemo(() => {
2787
+ const initialDepsForEdit = useMemo2(() => {
2713
2788
  return [
2714
2789
  ...state.depBlockers.map((t) => ({ id: t.id, name: t.name, type: DependencyType.Blocks })),
2715
2790
  ...state.depRelated.map((t) => ({ id: t.id, name: t.name, type: DependencyType.RelatesTo })),
@@ -2720,20 +2795,20 @@ ${state.selectedTask.additionalRequirements}`;
2720
2795
  }))
2721
2796
  ];
2722
2797
  }, [state.depBlockers, state.depRelated, state.depDuplicates]);
2723
- const allProjectTasks = useMemo(() => {
2798
+ const allProjectTasks = useMemo2(() => {
2724
2799
  if (!state.activeProject) return [];
2725
2800
  const result = container.taskService.listTasks({ projectId: state.activeProject.id });
2726
2801
  return result.ok ? result.value : [];
2727
2802
  }, [container, state.activeProject, state.tasks]);
2728
2803
  const previewTaskId = previewTask?.id ?? null;
2729
- useEffect(() => {
2804
+ useEffect2(() => {
2730
2805
  if (state.activeView === ViewType.TaskList && previewTaskId) {
2731
2806
  loadDeps(previewTaskId);
2732
2807
  }
2733
2808
  }, [state.activeView, previewTaskId, loadDeps]);
2734
- return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", height: "100%", children: [
2809
+ return /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", height: stdout.rows, children: [
2735
2810
  /* @__PURE__ */ jsx16(Header, { state }),
2736
- /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", flexGrow: 1, children: [
2811
+ /* @__PURE__ */ jsxs14(Box16, { flexDirection: "column", flexGrow: 1, overflowY: "hidden", children: [
2737
2812
  state.confirmDelete && /* @__PURE__ */ jsx16(ConfirmDialog, { task: state.confirmDelete }),
2738
2813
  !state.confirmDelete && state.activeView === ViewType.TaskList && /* @__PURE__ */ jsxs14(Box16, { flexDirection: "row", flexGrow: 1, children: [
2739
2814
  /* @__PURE__ */ jsx16(
@@ -2746,7 +2821,7 @@ ${state.selectedTask.additionalRequirements}`;
2746
2821
  isReordering: state.isEpicReordering
2747
2822
  }
2748
2823
  ),
2749
- /* @__PURE__ */ jsx16(Box16, { flexGrow: 2, children: /* @__PURE__ */ jsx16(
2824
+ /* @__PURE__ */ jsx16(Box16, { width: taskListWidth, children: /* @__PURE__ */ jsx16(
2750
2825
  TaskList,
2751
2826
  {
2752
2827
  tasks: state.tasks,
@@ -2767,7 +2842,7 @@ ${state.selectedTask.additionalRequirements}`;
2767
2842
  epicFilterActive: state.selectedEpicIds.size > 0
2768
2843
  }
2769
2844
  ) }),
2770
- /* @__PURE__ */ jsx16(Box16, { flexGrow: 1, children: previewTask ? /* @__PURE__ */ jsx16(
2845
+ /* @__PURE__ */ jsx16(Box16, { width: taskDetailWidth, children: previewTask ? /* @__PURE__ */ jsx16(
2771
2846
  TaskDetail,
2772
2847
  {
2773
2848
  task: previewTask,
@@ -2868,4 +2943,4 @@ async function launchTUI(container, initialProject) {
2868
2943
  export {
2869
2944
  launchTUI
2870
2945
  };
2871
- //# sourceMappingURL=tui-FTXYP3HM.js.map
2946
+ //# sourceMappingURL=tui-5JJH67YY.js.map