@tomkapa/tayto 0.1.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.
@@ -0,0 +1,2352 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DependencyType,
4
+ TaskStatus,
5
+ TaskType,
6
+ UIDependencyType,
7
+ isTerminalStatus,
8
+ logger
9
+ } from "./chunk-6NQOFUIQ.js";
10
+
11
+ // src/tui/index.tsx
12
+ import { render } from "ink";
13
+
14
+ // 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";
17
+
18
+ // src/tui/types.ts
19
+ var ViewType = {
20
+ TaskList: "task-list",
21
+ TaskDetail: "task-detail",
22
+ TaskCreate: "task-create",
23
+ TaskEdit: "task-edit",
24
+ ProjectSelector: "project-selector",
25
+ ProjectCreate: "project-create",
26
+ DependencyList: "dependency-list",
27
+ Help: "help"
28
+ };
29
+
30
+ // src/tui/state.ts
31
+ var initialState = {
32
+ activeView: ViewType.TaskList,
33
+ breadcrumbs: [ViewType.TaskList],
34
+ tasks: [],
35
+ selectedIndex: 0,
36
+ selectedTask: null,
37
+ projects: [],
38
+ activeProject: null,
39
+ filter: {},
40
+ searchQuery: "",
41
+ isSearchActive: false,
42
+ isReordering: false,
43
+ reorderSnapshot: null,
44
+ flash: null,
45
+ confirmDelete: null,
46
+ formData: null,
47
+ depBlockers: [],
48
+ depDependents: [],
49
+ depRelated: [],
50
+ depDuplicates: [],
51
+ depSelectedIndex: 0,
52
+ isAddingDep: false,
53
+ addDepInput: "",
54
+ focusedPanel: "list"
55
+ };
56
+ function appReducer(state, action) {
57
+ switch (action.type) {
58
+ case "NAVIGATE_TO":
59
+ return {
60
+ ...state,
61
+ breadcrumbs: [...state.breadcrumbs, action.view],
62
+ activeView: action.view,
63
+ confirmDelete: null,
64
+ focusedPanel: "list"
65
+ };
66
+ case "GO_BACK": {
67
+ const crumbs = state.breadcrumbs.length > 1 ? state.breadcrumbs.slice(0, -1) : [ViewType.TaskList];
68
+ return {
69
+ ...state,
70
+ activeView: crumbs[crumbs.length - 1] ?? ViewType.TaskList,
71
+ breadcrumbs: crumbs,
72
+ confirmDelete: null,
73
+ isSearchActive: false,
74
+ formData: null,
75
+ focusedPanel: "list"
76
+ };
77
+ }
78
+ case "SET_TASKS":
79
+ return {
80
+ ...state,
81
+ tasks: action.tasks,
82
+ selectedIndex: Math.min(state.selectedIndex, Math.max(0, action.tasks.length - 1))
83
+ };
84
+ case "SET_PROJECTS":
85
+ return { ...state, projects: action.projects };
86
+ case "SET_ACTIVE_PROJECT":
87
+ return { ...state, activeProject: action.project };
88
+ case "SET_FILTER":
89
+ return {
90
+ ...state,
91
+ filter: { ...state.filter, ...action.filter },
92
+ selectedIndex: 0
93
+ };
94
+ case "CLEAR_FILTER":
95
+ return { ...state, filter: {}, selectedIndex: 0, searchQuery: "" };
96
+ case "SET_SEARCH_ACTIVE":
97
+ return { ...state, isSearchActive: action.active };
98
+ case "SET_SEARCH_QUERY":
99
+ return { ...state, searchQuery: action.query };
100
+ case "FLASH":
101
+ return {
102
+ ...state,
103
+ flash: { message: action.message, level: action.level }
104
+ };
105
+ case "CLEAR_FLASH":
106
+ return { ...state, flash: null };
107
+ case "CONFIRM_DELETE":
108
+ return { ...state, confirmDelete: action.task };
109
+ case "CANCEL_DELETE":
110
+ return { ...state, confirmDelete: null };
111
+ case "MOVE_CURSOR": {
112
+ const maxIndex = Math.max(0, state.tasks.length - 1);
113
+ const newIndex = action.direction === "up" ? Math.max(0, state.selectedIndex - 1) : Math.min(maxIndex, state.selectedIndex + 1);
114
+ return { ...state, selectedIndex: newIndex };
115
+ }
116
+ case "SET_CURSOR": {
117
+ const maxIndex = Math.max(0, state.tasks.length - 1);
118
+ return { ...state, selectedIndex: Math.max(0, Math.min(action.index, maxIndex)) };
119
+ }
120
+ case "SELECT_TASK":
121
+ return { ...state, selectedTask: action.task };
122
+ case "SET_FORM_DATA":
123
+ return { ...state, formData: action.data };
124
+ case "ENTER_REORDER":
125
+ return {
126
+ ...state,
127
+ isReordering: true,
128
+ reorderSnapshot: [...state.tasks]
129
+ };
130
+ case "REORDER_MOVE": {
131
+ if (!state.isReordering) return state;
132
+ const idx = state.selectedIndex;
133
+ const tasks = [...state.tasks];
134
+ const swapIdx = action.direction === "up" ? idx - 1 : idx + 1;
135
+ if (swapIdx < 0 || swapIdx >= tasks.length) return state;
136
+ const current = tasks[idx];
137
+ const swap = tasks[swapIdx];
138
+ if (!current || !swap) return state;
139
+ tasks[idx] = swap;
140
+ tasks[swapIdx] = current;
141
+ return {
142
+ ...state,
143
+ tasks,
144
+ selectedIndex: swapIdx
145
+ };
146
+ }
147
+ case "EXIT_REORDER": {
148
+ if (!action.save && state.reorderSnapshot) {
149
+ return {
150
+ ...state,
151
+ isReordering: false,
152
+ tasks: state.reorderSnapshot,
153
+ reorderSnapshot: null
154
+ };
155
+ }
156
+ return {
157
+ ...state,
158
+ isReordering: false,
159
+ reorderSnapshot: null
160
+ };
161
+ }
162
+ case "SET_DEPS":
163
+ return {
164
+ ...state,
165
+ depBlockers: action.blockers,
166
+ depDependents: action.dependents,
167
+ depRelated: action.related,
168
+ depDuplicates: action.duplicates,
169
+ depSelectedIndex: 0
170
+ };
171
+ case "DEP_MOVE_CURSOR": {
172
+ const total = state.depBlockers.length + state.depDependents.length + state.depRelated.length + state.depDuplicates.length;
173
+ if (total === 0) return state;
174
+ const maxIdx = Math.max(0, total - 1);
175
+ const newIdx = action.direction === "up" ? Math.max(0, state.depSelectedIndex - 1) : Math.min(maxIdx, state.depSelectedIndex + 1);
176
+ return { ...state, depSelectedIndex: newIdx };
177
+ }
178
+ case "SET_ADDING_DEP":
179
+ return { ...state, isAddingDep: action.active, addDepInput: "" };
180
+ case "SET_ADD_DEP_INPUT":
181
+ return { ...state, addDepInput: action.input };
182
+ case "SET_PANEL_FOCUS":
183
+ return { ...state, focusedPanel: action.panel };
184
+ }
185
+ }
186
+
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
+ // src/tui/components/Header.tsx
320
+ import { Box, Text } from "ink";
321
+ import { jsx, jsxs } from "react/jsx-runtime";
322
+ function getKeyHints(view, isSearchActive) {
323
+ if (isSearchActive) {
324
+ return [
325
+ { key: "enter", desc: "apply" },
326
+ { key: "esc", desc: "cancel" }
327
+ ];
328
+ }
329
+ if (view === "task-list") {
330
+ return [
331
+ { key: "enter", desc: "view" },
332
+ { key: "c", desc: "create" },
333
+ { key: "e", desc: "edit" },
334
+ { key: "d", desc: "del" },
335
+ { key: "s", desc: "status" },
336
+ { key: "\u2190", desc: "reorder" },
337
+ { key: "/", desc: "search" },
338
+ { key: "p", desc: "project" },
339
+ { key: "f", desc: "status-f" },
340
+ { key: "t", desc: "type-f" },
341
+ { key: "?", desc: "help" },
342
+ { key: "q", desc: "quit" }
343
+ ];
344
+ }
345
+ if (view === "task-detail") {
346
+ return [
347
+ { key: "e", desc: "edit" },
348
+ { key: "s", desc: "status" },
349
+ { key: "d", desc: "del" },
350
+ { key: "m", desc: "mermaid" },
351
+ { key: "esc", desc: "back" },
352
+ { key: "?", desc: "help" },
353
+ { key: "q", desc: "quit" }
354
+ ];
355
+ }
356
+ return [
357
+ { key: "esc", desc: "back" },
358
+ { key: "?", desc: "help" },
359
+ { key: "q", desc: "quit" }
360
+ ];
361
+ }
362
+ function Header({ state }) {
363
+ const projectName = state.activeProject?.name ?? "none";
364
+ const taskCount = state.tasks.length;
365
+ const hints = getKeyHints(state.activeView, state.isSearchActive);
366
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
367
+ /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
368
+ /* @__PURE__ */ jsx(Text, { color: theme.logo, bold: true, children: "TaskCLI" }),
369
+ /* @__PURE__ */ jsx(Text, { color: theme.logo, children: "Project:" }),
370
+ /* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: projectName }),
371
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "|" }),
372
+ /* @__PURE__ */ jsx(Text, { color: theme.logo, children: "Tasks:" }),
373
+ /* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: taskCount })
374
+ ] }),
375
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: hints.map((h) => /* @__PURE__ */ jsxs(Box, { children: [
376
+ /* @__PURE__ */ jsxs(Text, { color: theme.menu.key, bold: true, children: [
377
+ "<",
378
+ h.key,
379
+ ">"
380
+ ] }),
381
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: h.desc })
382
+ ] }, h.key)) })
383
+ ] });
384
+ }
385
+
386
+ // src/tui/components/Crumbs.tsx
387
+ import { Box as Box2, Text as Text2 } from "ink";
388
+ import { jsx as jsx2 } from "react/jsx-runtime";
389
+ var VIEW_LABELS = {
390
+ "task-list": "tasks",
391
+ "task-detail": "detail",
392
+ "task-create": "create",
393
+ "task-edit": "edit",
394
+ "project-selector": "projects",
395
+ help: "help"
396
+ };
397
+ function Crumbs({ breadcrumbs }) {
398
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", gap: 0, width: "100%", children: breadcrumbs.map((crumb, i) => {
399
+ const isActive = i === breadcrumbs.length - 1;
400
+ const label = ` ${VIEW_LABELS[crumb] ?? crumb} `;
401
+ return /* @__PURE__ */ jsx2(
402
+ Text2,
403
+ {
404
+ color: theme.crumb.fg,
405
+ backgroundColor: isActive ? theme.crumb.activeBg : theme.crumb.bg,
406
+ bold: isActive,
407
+ children: label
408
+ },
409
+ `${crumb}-${i}`
410
+ );
411
+ }) });
412
+ }
413
+
414
+ // src/tui/components/FlashMessage.tsx
415
+ import { Box as Box3, Text as Text3 } from "ink";
416
+ import { jsx as jsx3 } from "react/jsx-runtime";
417
+ var LEVEL_COLOR = {
418
+ info: theme.flash.info,
419
+ warn: theme.flash.warn,
420
+ error: theme.flash.error
421
+ };
422
+ function FlashMessage({ message, level }) {
423
+ return /* @__PURE__ */ jsx3(Box3, { justifyContent: "center", width: "100%", children: /* @__PURE__ */ jsx3(Text3, { color: LEVEL_COLOR[level], bold: level === "error", children: message }) });
424
+ }
425
+
426
+ // src/tui/components/TaskList.tsx
427
+ import { Box as Box4, Text as Text4 } from "ink";
428
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
429
+ var PAGE_SIZE = 20;
430
+ var COL = {
431
+ rank: 5,
432
+ type: 12,
433
+ status: 14
434
+ };
435
+ function TaskList({
436
+ tasks,
437
+ selectedIndex,
438
+ searchQuery,
439
+ isSearchActive,
440
+ isReordering,
441
+ filter,
442
+ activeProjectName,
443
+ nonTerminalBlockerIds,
444
+ nonTerminalDependentIds,
445
+ isSelectedBlocked,
446
+ isFocused = true
447
+ }) {
448
+ const currentPage = Math.floor(selectedIndex / PAGE_SIZE);
449
+ const viewStart = currentPage * PAGE_SIZE;
450
+ const visibleTasks = tasks.slice(viewStart, viewStart + PAGE_SIZE);
451
+ const filterParts = [];
452
+ if (filter.status) filterParts.push(`status:${filter.status}`);
453
+ if (filter.type) filterParts.push(`type:${filter.type}`);
454
+ if (filter.search) filterParts.push(filter.search);
455
+ const filterText = filterParts.length > 0 ? filterParts.join(" ") : "";
456
+ return /* @__PURE__ */ jsxs2(
457
+ Box4,
458
+ {
459
+ flexDirection: "column",
460
+ flexGrow: 1,
461
+ borderStyle: "bold",
462
+ borderColor: isFocused ? theme.borderFocus : theme.border,
463
+ children: [
464
+ /* @__PURE__ */ jsxs2(Box4, { children: [
465
+ /* @__PURE__ */ jsxs2(Text4, { color: theme.title, bold: true, children: [
466
+ " ",
467
+ "tasks"
468
+ ] }),
469
+ /* @__PURE__ */ jsx4(Text4, { color: theme.fg, children: "(" }),
470
+ /* @__PURE__ */ jsx4(Text4, { color: theme.titleHighlight, bold: true, children: activeProjectName }),
471
+ /* @__PURE__ */ jsx4(Text4, { color: theme.fg, children: ")" }),
472
+ /* @__PURE__ */ jsxs2(Text4, { color: theme.titleCounter, bold: true, children: [
473
+ "[",
474
+ tasks.length,
475
+ "]"
476
+ ] }),
477
+ isReordering && /* @__PURE__ */ jsxs2(Text4, { color: theme.flash.warn, bold: true, children: [
478
+ " ",
479
+ "REORDER"
480
+ ] }),
481
+ filterText && /* @__PURE__ */ jsxs2(Text4, { color: theme.titleFilter, children: [
482
+ " /",
483
+ filterText
484
+ ] })
485
+ ] }),
486
+ isSearchActive && /* @__PURE__ */ jsxs2(Box4, { borderStyle: "round", borderColor: theme.prompt, paddingX: 1, children: [
487
+ /* @__PURE__ */ jsx4(Text4, { color: theme.prompt, children: "/" }),
488
+ /* @__PURE__ */ jsx4(Text4, { color: theme.prompt, children: searchQuery }),
489
+ /* @__PURE__ */ jsx4(Text4, { color: theme.promptSuggest, children: "_" })
490
+ ] }),
491
+ /* @__PURE__ */ jsxs2(Box4, { children: [
492
+ /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: " " }),
493
+ /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "#".padEnd(COL.rank) }),
494
+ /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "TYPE".padEnd(COL.type) }),
495
+ /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "STATUS".padEnd(COL.status) }),
496
+ /* @__PURE__ */ jsx4(Text4, { color: theme.table.headerFg, bold: true, children: "NAME" })
497
+ ] }),
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) => {
499
+ const actualIndex = viewStart + i;
500
+ const isSelected = actualIndex === selectedIndex;
501
+ const isNonTerminalBlocker = nonTerminalBlockerIds.has(task.id);
502
+ const isNonTerminalDependent = nonTerminalDependentIds.has(task.id);
503
+ const rowColor = STATUS_COLOR[task.status] ?? theme.table.fg;
504
+ const rowNum = `${actualIndex + 1}`;
505
+ const depMarker = isNonTerminalBlocker ? "\u25B2 " : isNonTerminalDependent ? "\u25BC " : " ";
506
+ if (isSelected) {
507
+ const cursorBg = isReordering ? theme.flash.warn : isSelectedBlocked ? theme.table.blockedCursorBg : theme.table.cursorBg;
508
+ return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs2(Text4, { backgroundColor: cursorBg, color: theme.table.cursorFg, bold: true, children: [
509
+ isReordering ? "~ " : "> ",
510
+ rowNum.padEnd(COL.rank),
511
+ task.type.padEnd(COL.type),
512
+ task.status.padEnd(COL.status),
513
+ task.name
514
+ ] }) }, task.id);
515
+ }
516
+ if (isNonTerminalBlocker || isNonTerminalDependent) {
517
+ return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs2(Text4, { backgroundColor: theme.table.depHighlightBg, color: theme.table.fg, bold: true, children: [
518
+ depMarker,
519
+ rowNum.padEnd(COL.rank),
520
+ task.type.padEnd(COL.type),
521
+ task.status.padEnd(COL.status),
522
+ task.name
523
+ ] }) }, task.id);
524
+ }
525
+ return /* @__PURE__ */ jsxs2(Box4, { children: [
526
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
527
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: rowNum.padEnd(COL.rank) }),
528
+ /* @__PURE__ */ jsx4(Text4, { color: TYPE_COLOR[task.type] ?? rowColor, children: task.type.padEnd(COL.type) }),
529
+ /* @__PURE__ */ jsx4(Text4, { color: STATUS_COLOR[task.status] ?? rowColor, children: task.status.padEnd(COL.status) }),
530
+ /* @__PURE__ */ jsx4(Text4, { color: rowColor, children: task.name })
531
+ ] }, task.id);
532
+ }),
533
+ /* @__PURE__ */ jsx4(Box4, { flexGrow: 1 }),
534
+ tasks.length > PAGE_SIZE && /* @__PURE__ */ jsx4(Box4, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs2(Text4, { dimColor: true, children: [
535
+ "[",
536
+ viewStart + 1,
537
+ "-",
538
+ Math.min(viewStart + PAGE_SIZE, tasks.length),
539
+ "/",
540
+ tasks.length,
541
+ "]"
542
+ ] }) })
543
+ ]
544
+ }
545
+ );
546
+ }
547
+
548
+ // src/tui/components/TaskDetail.tsx
549
+ import { Box as Box6, Text as Text6 } from "ink";
550
+
551
+ // src/tui/components/Markdown.tsx
552
+ import { Text as Text5, Box as Box5 } from "ink";
553
+ import { Marked } from "marked";
554
+ import { markedTerminal } from "marked-terminal";
555
+ import chalk from "chalk";
556
+
557
+ // src/tui/mermaid.ts
558
+ import { writeFileSync, mkdtempSync } from "fs";
559
+ import { join } from "path";
560
+ import { tmpdir } from "os";
561
+ import { exec } from "child_process";
562
+ function openMermaidInBrowser(code) {
563
+ const dir = mkdtempSync(join(tmpdir(), "task-mermaid-"));
564
+ const filepath = join(dir, "diagram.html");
565
+ const html = `<!DOCTYPE html>
566
+ <html><head>
567
+ <meta charset="utf-8">
568
+ <title>Mermaid Diagram</title>
569
+ <style>
570
+ body { background: #1a1a2e; display: flex; justify-content: center; padding: 2rem; }
571
+ .mermaid { background: #16213e; padding: 2rem; border-radius: 8px; }
572
+ </style>
573
+ </head><body>
574
+ <pre class="mermaid">
575
+ ${code.replace(/</g, "&lt;").replace(/>/g, "&gt;")}
576
+ </pre>
577
+ <script type="module">
578
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
579
+ mermaid.initialize({ startOnLoad: true, theme: 'dark' });
580
+ </script>
581
+ </body></html>`;
582
+ writeFileSync(filepath, html, "utf-8");
583
+ const cmd = process.platform === "darwin" ? `open "${filepath}"` : process.platform === "win32" ? `start "" "${filepath}"` : `xdg-open "${filepath}"`;
584
+ exec(cmd);
585
+ }
586
+
587
+ // src/tui/components/Markdown.tsx
588
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
589
+ function extractMermaidBlocks(content) {
590
+ const lexer = new Marked();
591
+ const tokens = lexer.lexer(content);
592
+ const blocks = [];
593
+ function walk(items) {
594
+ for (const token of items) {
595
+ if (token.type === "code" && token.lang === "mermaid") {
596
+ blocks.push(token.text);
597
+ }
598
+ if ("tokens" in token && Array.isArray(token.tokens)) {
599
+ walk(token.tokens);
600
+ }
601
+ }
602
+ }
603
+ walk(tokens);
604
+ return blocks;
605
+ }
606
+ function createRenderer() {
607
+ const mermaidExtension = {
608
+ renderer: {
609
+ code(token) {
610
+ if (token.lang !== "mermaid") return false;
611
+ const lines = token.text.split("\n");
612
+ const preview = lines.slice(0, 6);
613
+ const maxLen = Math.max(...preview.map((l) => l.length), 30);
614
+ const border = "\u2500".repeat(maxLen + 2);
615
+ const framed = preview.map((l) => ` ${chalk.dim("\u2502")} ${chalk.hex(theme.table.fg)(l)}`).join("\n");
616
+ const truncated = lines.length > 6 ? `
617
+ ${chalk.dim("\u2502")} ${chalk.dim(`... ${lines.length - 6} more lines`)}` : "";
618
+ return [
619
+ "",
620
+ ` ${chalk.hex(theme.title).bold("\u250C Mermaid Diagram")} ${chalk.hex(theme.menu.key)("[press m to open in browser]")}`,
621
+ ` ${chalk.dim(border)}`,
622
+ framed,
623
+ truncated,
624
+ ` ${chalk.dim(border)}`,
625
+ ""
626
+ ].join("\n");
627
+ }
628
+ }
629
+ };
630
+ const terminalExt = markedTerminal;
631
+ return new Marked(
632
+ terminalExt({
633
+ showSectionPrefix: false,
634
+ reflowText: true,
635
+ tab: 2
636
+ }),
637
+ mermaidExtension
638
+ );
639
+ }
640
+ var marked = createRenderer();
641
+ function Markdown({ content }) {
642
+ if (!content.trim()) {
643
+ return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No content" });
644
+ }
645
+ 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() });
650
+ }
651
+ function MermaidHint({ content }) {
652
+ const blocks = extractMermaidBlocks(content);
653
+ if (blocks.length === 0) return null;
654
+ return /* @__PURE__ */ jsxs3(Box5, { children: [
655
+ /* @__PURE__ */ jsxs3(Text5, { color: theme.menu.key, bold: true, children: [
656
+ "<m>",
657
+ " "
658
+ ] }),
659
+ /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
660
+ "Open ",
661
+ blocks.length,
662
+ " mermaid diagram",
663
+ blocks.length > 1 ? "s" : "",
664
+ " in browser"
665
+ ] })
666
+ ] });
667
+ }
668
+ function openAllMermaidDiagrams(content) {
669
+ const blocks = extractMermaidBlocks(content);
670
+ for (const block of blocks) {
671
+ openMermaidInBrowser(block);
672
+ }
673
+ return blocks.length;
674
+ }
675
+
676
+ // src/tui/components/TaskDetail.tsx
677
+ 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
+ ] });
684
+ }
685
+ function TaskDetail({
686
+ task,
687
+ blockers,
688
+ dependents,
689
+ related,
690
+ duplicates,
691
+ isFocused = true
692
+ }) {
693
+ const allText = `${task.description}
694
+ ${task.technicalNotes}
695
+ ${task.additionalRequirements}`;
696
+ return /* @__PURE__ */ jsxs4(
697
+ Box6,
698
+ {
699
+ flexDirection: "column",
700
+ flexGrow: 1,
701
+ 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(", ") })
750
+ ] }),
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
+ ] }),
765
+ /* @__PURE__ */ jsx6(Box6, { flexGrow: 1 }),
766
+ /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(MermaidHint, { content: allText }) })
767
+ ]
768
+ }
769
+ );
770
+ }
771
+
772
+ // src/tui/components/TaskForm.tsx
773
+ import { useState as useState2, useCallback } from "react";
774
+ import { Box as Box8, Text as Text8, useInput as useInput2 } from "ink";
775
+
776
+ // src/tui/editor.ts
777
+ import { writeFileSync as writeFileSync2, readFileSync, unlinkSync, mkdtempSync as mkdtempSync2 } from "fs";
778
+ import { join as join2 } from "path";
779
+ import { tmpdir as tmpdir2 } from "os";
780
+ import { spawnSync } from "child_process";
781
+ function getEditor() {
782
+ return process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
783
+ }
784
+ function openInEditor(content, filename) {
785
+ const dir = mkdtempSync2(join2(tmpdir2(), "task-"));
786
+ const filepath = join2(dir, filename);
787
+ writeFileSync2(filepath, content, "utf-8");
788
+ const editor = getEditor();
789
+ const result = spawnSync(editor, [filepath], {
790
+ stdio: "inherit",
791
+ shell: true
792
+ });
793
+ if (result.status !== 0) {
794
+ try {
795
+ unlinkSync(filepath);
796
+ } catch {
797
+ }
798
+ return null;
799
+ }
800
+ try {
801
+ const edited = readFileSync(filepath, "utf-8");
802
+ unlinkSync(filepath);
803
+ return edited;
804
+ } catch {
805
+ return null;
806
+ }
807
+ }
808
+
809
+ // src/tui/components/TaskPicker.tsx
810
+ import { useState } from "react";
811
+ import { Box as Box7, Text as Text7, useInput, useStdout } from "ink";
812
+ import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
813
+ var DEP_TYPE_VALUES = Object.values(UIDependencyType);
814
+ var DEP_TYPE_COLOR = {
815
+ [DependencyType.Blocks]: theme.status.error,
816
+ [DependencyType.RelatesTo]: theme.status.new,
817
+ [DependencyType.Duplicates]: theme.status.pending,
818
+ [UIDependencyType.BlockedBy]: theme.status.modified
819
+ };
820
+ function TaskPicker({ tasks, excludeIds, initialSelection, onConfirm, onCancel }) {
821
+ const { stdout } = useStdout();
822
+ const termHeight = stdout.rows > 0 ? stdout.rows : 24;
823
+ const maxVisible = Math.max(3, termHeight - 12);
824
+ const [searchQuery, setSearchQuery] = useState("");
825
+ const [isSearching, setIsSearching] = useState(false);
826
+ const [cursorIndex, setCursorIndex] = useState(0);
827
+ const [selected, setSelected] = useState(() => {
828
+ const map = /* @__PURE__ */ new Map();
829
+ if (initialSelection) {
830
+ for (const dep of initialSelection) {
831
+ map.set(dep.id, dep);
832
+ }
833
+ }
834
+ return map;
835
+ });
836
+ const available = tasks.filter((t) => {
837
+ if (excludeIds?.has(t.id)) return false;
838
+ if (!searchQuery.trim()) return true;
839
+ const q = searchQuery.toLowerCase();
840
+ return t.id.toLowerCase().includes(q) || t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q);
841
+ });
842
+ let viewStart = 0;
843
+ if (cursorIndex >= viewStart + maxVisible) {
844
+ viewStart = cursorIndex - maxVisible + 1;
845
+ }
846
+ if (cursorIndex < viewStart) {
847
+ viewStart = cursorIndex;
848
+ }
849
+ const visible = available.slice(viewStart, viewStart + maxVisible);
850
+ useInput((input, key) => {
851
+ if (isSearching) {
852
+ if (key.escape) {
853
+ setIsSearching(false);
854
+ return;
855
+ }
856
+ if (key.return) {
857
+ setIsSearching(false);
858
+ setCursorIndex(0);
859
+ return;
860
+ }
861
+ if (key.backspace || key.delete) {
862
+ setSearchQuery((q) => q.slice(0, -1));
863
+ setCursorIndex(0);
864
+ return;
865
+ }
866
+ if (input && !key.ctrl && !key.meta) {
867
+ setSearchQuery((q) => q + input);
868
+ setCursorIndex(0);
869
+ return;
870
+ }
871
+ return;
872
+ }
873
+ if (key.upArrow || input === "k") {
874
+ setCursorIndex((i) => Math.max(0, i - 1));
875
+ return;
876
+ }
877
+ if (key.downArrow || input === "j") {
878
+ setCursorIndex((i) => Math.min(available.length - 1, i + 1));
879
+ return;
880
+ }
881
+ if (input === " " || input === "t") {
882
+ const task = available[cursorIndex];
883
+ if (!task) return;
884
+ setSelected((prev) => {
885
+ const next = new Map(prev);
886
+ const entry = prev.get(task.id);
887
+ if (!entry) {
888
+ next.set(task.id, { id: task.id, name: task.name, type: DependencyType.Blocks });
889
+ } else {
890
+ const idx = DEP_TYPE_VALUES.indexOf(entry.type);
891
+ if (idx < DEP_TYPE_VALUES.length - 1) {
892
+ const nextType = DEP_TYPE_VALUES[idx + 1] ?? DependencyType.Blocks;
893
+ next.set(task.id, { ...entry, type: nextType });
894
+ } else {
895
+ next.delete(task.id);
896
+ }
897
+ }
898
+ return next;
899
+ });
900
+ return;
901
+ }
902
+ if (input === "x") {
903
+ const task = available[cursorIndex];
904
+ if (!task) return;
905
+ setSelected((prev) => {
906
+ const next = new Map(prev);
907
+ next.delete(task.id);
908
+ return next;
909
+ });
910
+ return;
911
+ }
912
+ if (input === "/") {
913
+ setIsSearching(true);
914
+ setSearchQuery("");
915
+ return;
916
+ }
917
+ if (key.return) {
918
+ onConfirm(Array.from(selected.values()));
919
+ return;
920
+ }
921
+ if (key.escape || input === "q") {
922
+ onCancel();
923
+ return;
924
+ }
925
+ });
926
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "bold", borderColor: theme.borderFocus, flexGrow: 1, children: [
927
+ /* @__PURE__ */ jsxs5(Box7, { gap: 0, children: [
928
+ /* @__PURE__ */ jsxs5(Text7, { color: theme.title, bold: true, children: [
929
+ " ",
930
+ "select dependencies"
931
+ ] }),
932
+ /* @__PURE__ */ jsxs5(Text7, { color: theme.titleCounter, bold: true, children: [
933
+ " ",
934
+ "[",
935
+ selected.size,
936
+ " selected]"
937
+ ] })
938
+ ] }),
939
+ isSearching ? /* @__PURE__ */ jsxs5(Box7, { borderStyle: "round", borderColor: theme.prompt, paddingX: 1, children: [
940
+ /* @__PURE__ */ jsx7(Text7, { color: theme.prompt, children: "/" }),
941
+ /* @__PURE__ */ jsx7(Text7, { color: theme.prompt, children: searchQuery }),
942
+ /* @__PURE__ */ jsx7(Text7, { color: theme.promptSuggest, children: "_" })
943
+ ] }) : searchQuery ? /* @__PURE__ */ jsx7(Box7, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { color: theme.titleFilter, children: [
944
+ "/",
945
+ searchQuery
946
+ ] }) }) : null,
947
+ /* @__PURE__ */ jsx7(Box7, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { color: theme.table.headerFg, bold: true, children: [
948
+ " ",
949
+ "SEL".padEnd(5),
950
+ "ID".padEnd(14),
951
+ "REL TYPE".padEnd(12),
952
+ "STATUS".padEnd(14),
953
+ "NAME"
954
+ ] }) }),
955
+ available.length === 0 ? /* @__PURE__ */ jsx7(Box7, { paddingX: 2, paddingY: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No tasks match the filter" }) }) : visible.map((task, i) => {
956
+ const actualIndex = viewStart + i;
957
+ const isCursor = actualIndex === cursorIndex;
958
+ const entry = selected.get(task.id);
959
+ const isChecked = !!entry;
960
+ const checkMark = isChecked ? "[x]" : "[ ]";
961
+ const depType = entry ? DEP_TYPE_LABEL[entry.type] ?? entry.type : "";
962
+ const depColor = entry ? DEP_TYPE_COLOR[entry.type] ?? theme.table.fg : theme.table.fg;
963
+ return /* @__PURE__ */ jsx7(Box7, { paddingX: 1, children: isCursor ? /* @__PURE__ */ jsxs5(Text7, { backgroundColor: theme.table.cursorBg, color: theme.table.cursorFg, bold: true, children: [
964
+ "> ",
965
+ checkMark.padEnd(5),
966
+ task.id.padEnd(14),
967
+ depType.padEnd(12),
968
+ task.status.padEnd(14),
969
+ task.name
970
+ ] }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
971
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
972
+ /* @__PURE__ */ jsx7(Text7, { color: isChecked ? theme.status.added : theme.table.fg, children: checkMark.padEnd(5) }),
973
+ /* @__PURE__ */ jsx7(Text7, { color: theme.yaml.value, children: task.id.padEnd(14) }),
974
+ /* @__PURE__ */ jsx7(Text7, { color: depColor, children: depType.padEnd(12) }),
975
+ /* @__PURE__ */ jsx7(Text7, { color: theme.status.completed, children: task.status.padEnd(14) }),
976
+ /* @__PURE__ */ jsx7(Text7, { color: theme.table.fg, children: task.name })
977
+ ] }) }, task.id);
978
+ }),
979
+ /* @__PURE__ */ jsx7(Box7, { flexGrow: 1 }),
980
+ available.length > maxVisible && /* @__PURE__ */ jsx7(Box7, { justifyContent: "flex-end", paddingRight: 1, children: /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
981
+ "[",
982
+ viewStart + 1,
983
+ "-",
984
+ Math.min(viewStart + maxVisible, available.length),
985
+ "/",
986
+ available.length,
987
+ "]"
988
+ ] }) }),
989
+ selected.size > 0 && /* @__PURE__ */ jsxs5(Box7, { paddingX: 1, flexDirection: "column", children: [
990
+ /* @__PURE__ */ jsx7(Text7, { color: theme.table.headerFg, bold: true, children: "Selected:" }),
991
+ /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { children: Array.from(selected.values()).map((d) => `${d.id} (${DEP_TYPE_LABEL[d.type] ?? d.type})`).join(", ") }) })
992
+ ] }),
993
+ /* @__PURE__ */ jsx7(Box7, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
994
+ "space/t: select & cycle type (blocks ",
995
+ "->",
996
+ " relates-to ",
997
+ "->",
998
+ " duplicates ",
999
+ "->",
1000
+ " blocked-by",
1001
+ " ",
1002
+ "->",
1003
+ " off) | x: deselect | /: search | enter: confirm | esc: cancel"
1004
+ ] }) })
1005
+ ] });
1006
+ }
1007
+
1008
+ // src/tui/components/TaskForm.tsx
1009
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1010
+ var FIELDS = [
1011
+ { label: "Name", key: "name", type: "inline" },
1012
+ { label: "Type", key: "type", type: "select", options: TYPE_VALUES },
1013
+ { label: "Status", key: "status", type: "select", options: STATUS_VALUES },
1014
+ { label: "Depends On", key: "dependsOn", type: "picker" },
1015
+ { label: "Description", key: "description", type: "editor", editorFilename: "description.md" },
1016
+ {
1017
+ label: "Tech Notes",
1018
+ key: "technicalNotes",
1019
+ type: "editor",
1020
+ editorFilename: "technical-notes.md"
1021
+ },
1022
+ {
1023
+ label: "Requirements",
1024
+ key: "additionalRequirements",
1025
+ type: "editor",
1026
+ editorFilename: "requirements.md"
1027
+ }
1028
+ ];
1029
+ function TaskForm({ editingTask, allTasks, initialDeps, onSave, onCancel }) {
1030
+ const [focusIndex, setFocusIndex] = useState2(0);
1031
+ const [values, setValues] = useState2({
1032
+ name: editingTask?.name ?? "",
1033
+ type: editingTask?.type ?? TaskType.Story,
1034
+ status: editingTask?.status ?? TaskStatus.Backlog,
1035
+ description: editingTask?.description ?? "",
1036
+ technicalNotes: editingTask?.technicalNotes ?? "",
1037
+ additionalRequirements: editingTask?.additionalRequirements ?? ""
1038
+ });
1039
+ const [editorActive, setEditorActive] = useState2(false);
1040
+ const [pickerActive, setPickerActive] = useState2(false);
1041
+ const [pickedDeps, setPickedDeps] = useState2(initialDeps ?? []);
1042
+ const currentField = FIELDS[focusIndex];
1043
+ const launchEditor = useCallback(
1044
+ (field) => {
1045
+ setEditorActive(true);
1046
+ setTimeout(() => {
1047
+ const content = values[field.key] ?? "";
1048
+ const result = openInEditor(content, field.editorFilename ?? `${field.key}.md`);
1049
+ if (result !== null) {
1050
+ setValues((v) => ({ ...v, [field.key]: result }));
1051
+ }
1052
+ setEditorActive(false);
1053
+ }, 50);
1054
+ },
1055
+ [values]
1056
+ );
1057
+ const handlePickerConfirm = useCallback((selected) => {
1058
+ setPickedDeps(selected);
1059
+ setPickerActive(false);
1060
+ }, []);
1061
+ const handlePickerCancel = useCallback(() => {
1062
+ setPickerActive(false);
1063
+ }, []);
1064
+ useInput2(
1065
+ (input, key) => {
1066
+ if (editorActive || pickerActive) return;
1067
+ if (key.escape) {
1068
+ onCancel();
1069
+ return;
1070
+ }
1071
+ if (!currentField) return;
1072
+ if (key.tab) {
1073
+ if (key.shift) {
1074
+ setFocusIndex((i) => Math.max(0, i - 1));
1075
+ } else {
1076
+ setFocusIndex((i) => Math.min(FIELDS.length - 1, i + 1));
1077
+ }
1078
+ return;
1079
+ }
1080
+ if (input === "s" && key.ctrl) {
1081
+ const nameVal = values["name"];
1082
+ if (typeof nameVal === "string" && nameVal.trim()) {
1083
+ onSave({
1084
+ name: nameVal,
1085
+ description: values["description"] ?? "",
1086
+ type: values["type"] ?? TaskType.Story,
1087
+ status: values["status"] ?? TaskStatus.Backlog,
1088
+ technicalNotes: values["technicalNotes"] ?? "",
1089
+ additionalRequirements: values["additionalRequirements"] ?? "",
1090
+ dependsOn: pickedDeps.map((d) => ({ id: d.id, type: d.type }))
1091
+ });
1092
+ }
1093
+ return;
1094
+ }
1095
+ if (currentField.type === "inline") {
1096
+ const currentValue = values[currentField.key] ?? "";
1097
+ if (key.backspace || key.delete) {
1098
+ setValues((v) => ({ ...v, [currentField.key]: currentValue.slice(0, -1) }));
1099
+ } else if (key.return) {
1100
+ setFocusIndex((i) => Math.min(FIELDS.length - 1, i + 1));
1101
+ } else if (input && !key.ctrl && !key.meta) {
1102
+ setValues((v) => ({ ...v, [currentField.key]: currentValue + input }));
1103
+ }
1104
+ }
1105
+ if (currentField.type === "picker") {
1106
+ if (key.return) {
1107
+ setPickerActive(true);
1108
+ } else if (key.downArrow) {
1109
+ setFocusIndex((i) => Math.min(FIELDS.length - 1, i + 1));
1110
+ } else if (key.upArrow) {
1111
+ setFocusIndex((i) => Math.max(0, i - 1));
1112
+ }
1113
+ }
1114
+ if (currentField.type === "editor") {
1115
+ if (key.return) {
1116
+ launchEditor(currentField);
1117
+ } else if (key.downArrow) {
1118
+ setFocusIndex((i) => Math.min(FIELDS.length - 1, i + 1));
1119
+ } else if (key.upArrow) {
1120
+ setFocusIndex((i) => Math.max(0, i - 1));
1121
+ }
1122
+ }
1123
+ if (currentField.type === "select") {
1124
+ const options = currentField.options ?? [];
1125
+ const currentValue = values[currentField.key] ?? "";
1126
+ const currentIndex = options.indexOf(currentValue);
1127
+ if (key.rightArrow || key.return || input === " ") {
1128
+ const nextIndex = (currentIndex + 1) % options.length;
1129
+ setValues((v) => ({ ...v, [currentField.key]: options[nextIndex] ?? "" }));
1130
+ } else if (key.leftArrow) {
1131
+ const prevIndex = (currentIndex - 1 + options.length) % options.length;
1132
+ setValues((v) => ({ ...v, [currentField.key]: options[prevIndex] ?? "" }));
1133
+ }
1134
+ }
1135
+ },
1136
+ { isActive: !editorActive && !pickerActive }
1137
+ );
1138
+ if (pickerActive) {
1139
+ const pickerExclude = editingTask ? { excludeIds: /* @__PURE__ */ new Set([editingTask.id]) } : {};
1140
+ return /* @__PURE__ */ jsx8(
1141
+ TaskPicker,
1142
+ {
1143
+ tasks: allTasks,
1144
+ ...pickerExclude,
1145
+ initialSelection: pickedDeps,
1146
+ onConfirm: handlePickerConfirm,
1147
+ onCancel: handlePickerCancel
1148
+ }
1149
+ );
1150
+ }
1151
+ const isEdit = editingTask !== null;
1152
+ const depSummary = pickedDeps.length > 0 ? pickedDeps.map((d) => `${d.id} (${DEP_TYPE_LABEL[d.type] ?? d.type})`).join(", ") : "";
1153
+ return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", flexGrow: 1, borderStyle: "bold", borderColor: theme.borderFocus, children: [
1154
+ /* @__PURE__ */ jsx8(Box8, { gap: 0, children: /* @__PURE__ */ jsxs6(Text8, { color: theme.title, bold: true, children: [
1155
+ " ",
1156
+ isEdit ? "edit" : "create"
1157
+ ] }) }),
1158
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", paddingX: 1, paddingY: 0, children: FIELDS.map((field, i) => {
1159
+ const isFocused = i === focusIndex;
1160
+ const value = field.key === "dependsOn" ? depSummary : values[field.key] ?? "";
1161
+ const displayValue = value;
1162
+ return /* @__PURE__ */ jsxs6(Box8, { gap: 1, children: [
1163
+ /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.dialog.label : theme.yaml.key, bold: isFocused, children: [
1164
+ isFocused ? ">" : " ",
1165
+ " ",
1166
+ field.label.padEnd(14)
1167
+ ] }),
1168
+ field.type === "inline" && /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1169
+ displayValue,
1170
+ isFocused ? /* @__PURE__ */ jsx8(Text8, { color: theme.titleHighlight, children: "_" }) : ""
1171
+ ] }),
1172
+ field.type === "picker" && /* @__PURE__ */ jsxs6(Text8, { children: [
1173
+ 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
+ isFocused && /* @__PURE__ */ jsx8(Text8, { color: theme.menu.key, children: " [enter: open picker]" })
1175
+ ] }),
1176
+ field.type === "editor" && /* @__PURE__ */ jsxs6(Text8, { children: [
1177
+ displayValue ? /* @__PURE__ */ jsxs6(Text8, { color: theme.status.added, children: [
1178
+ displayValue.split("\n")[0]?.slice(0, 50),
1179
+ displayValue.length > 50 || displayValue.includes("\n") ? "..." : ""
1180
+ ] }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: isFocused ? "press enter" : "empty" }),
1181
+ isFocused && /* @__PURE__ */ jsx8(Text8, { color: theme.menu.key, children: " [enter: $EDITOR]" })
1182
+ ] }),
1183
+ field.type === "select" && /* @__PURE__ */ jsxs6(Text8, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1184
+ isFocused ? "< " : " ",
1185
+ displayValue,
1186
+ isFocused ? " >" : ""
1187
+ ] })
1188
+ ] }, field.key);
1189
+ }) }),
1190
+ /* @__PURE__ */ jsx8(Box8, { flexGrow: 1 }),
1191
+ /* @__PURE__ */ jsx8(Box8, { paddingX: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "tab: next | shift+tab: prev | ctrl+s: save | esc: cancel" }) }),
1192
+ editorActive && /* @__PURE__ */ jsx8(Box8, { paddingX: 1, children: /* @__PURE__ */ jsx8(Text8, { color: theme.flash.warn, bold: true, children: "Editor open... save and close to return" }) })
1193
+ ] });
1194
+ }
1195
+
1196
+ // src/tui/components/ProjectSelector.tsx
1197
+ import { useState as useState3 } from "react";
1198
+ import { Box as Box9, Text as Text9, useInput as useInput3 } from "ink";
1199
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1200
+ function ProjectSelector({
1201
+ projects,
1202
+ activeProject,
1203
+ onSelect,
1204
+ onCreate,
1205
+ onSetDefault,
1206
+ onCancel
1207
+ }) {
1208
+ const [selectedIndex, setSelectedIndex] = useState3(() => {
1209
+ if (!activeProject) return 0;
1210
+ const idx = projects.findIndex((p) => p.id === activeProject.id);
1211
+ return idx >= 0 ? idx : 0;
1212
+ });
1213
+ useInput3((input, key) => {
1214
+ if (key.escape || input === "q") {
1215
+ onCancel();
1216
+ return;
1217
+ }
1218
+ if (key.return) {
1219
+ const project = projects[selectedIndex];
1220
+ if (project) {
1221
+ onSelect(project);
1222
+ }
1223
+ return;
1224
+ }
1225
+ if (input === "c") {
1226
+ onCreate();
1227
+ return;
1228
+ }
1229
+ if (input === "d") {
1230
+ const project = projects[selectedIndex];
1231
+ if (project) {
1232
+ onSetDefault(project);
1233
+ }
1234
+ return;
1235
+ }
1236
+ if (key.upArrow || input === "k") {
1237
+ setSelectedIndex((i) => Math.max(0, i - 1));
1238
+ }
1239
+ if (key.downArrow || input === "j") {
1240
+ setSelectedIndex((i) => Math.min(projects.length - 1, i + 1));
1241
+ }
1242
+ });
1243
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", flexGrow: 1, borderStyle: "bold", borderColor: theme.borderFocus, children: [
1244
+ /* @__PURE__ */ jsxs7(Box9, { gap: 0, children: [
1245
+ /* @__PURE__ */ jsxs7(Text9, { color: theme.title, bold: true, children: [
1246
+ " ",
1247
+ "projects"
1248
+ ] }),
1249
+ /* @__PURE__ */ jsxs7(Text9, { color: theme.titleCounter, bold: true, children: [
1250
+ "[",
1251
+ projects.length,
1252
+ "]"
1253
+ ] })
1254
+ ] }),
1255
+ /* @__PURE__ */ jsxs7(Box9, { paddingX: 1, children: [
1256
+ /* @__PURE__ */ jsx9(Text9, { color: theme.table.headerFg, bold: true, children: " NAME".padEnd(30) }),
1257
+ /* @__PURE__ */ jsx9(Text9, { color: theme.table.headerFg, bold: true, children: "DESCRIPTION" })
1258
+ ] }),
1259
+ projects.length === 0 ? /* @__PURE__ */ jsx9(Box9, { paddingX: 1, paddingY: 1, children: /* @__PURE__ */ jsx9(Text9, { color: theme.fg, children: "No projects. Press 'c' to create one." }) }) : projects.map((project, i) => {
1260
+ const isSelected = i === selectedIndex;
1261
+ const isActive = project.id === activeProject?.id;
1262
+ const activeMarker = isActive ? "*" : " ";
1263
+ const defaultMarker = project.isDefault ? "D" : " ";
1264
+ const marker = `${activeMarker}${defaultMarker}`;
1265
+ if (isSelected) {
1266
+ return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsxs7(Text9, { backgroundColor: theme.table.cursorBg, color: theme.table.cursorFg, bold: true, children: [
1267
+ marker,
1268
+ " ",
1269
+ project.name.padEnd(27),
1270
+ project.description
1271
+ ] }) }, project.id);
1272
+ }
1273
+ return /* @__PURE__ */ jsxs7(Box9, { paddingX: 1, children: [
1274
+ /* @__PURE__ */ jsxs7(Text9, { color: isActive ? theme.status.modified : theme.table.fg, children: [
1275
+ marker,
1276
+ " ",
1277
+ project.name.padEnd(27)
1278
+ ] }),
1279
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: project.description })
1280
+ ] }, project.id);
1281
+ }),
1282
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1 }),
1283
+ /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "enter: select | d: set default | c: create | esc: back" }) })
1284
+ ] });
1285
+ }
1286
+
1287
+ // src/tui/components/ProjectForm.tsx
1288
+ import { useState as useState4 } from "react";
1289
+ import { Box as Box10, Text as Text10, useInput as useInput4 } from "ink";
1290
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1291
+ var FIELDS2 = [
1292
+ { label: "Name", key: "name", type: "inline" },
1293
+ { label: "Key", key: "key", type: "inline" },
1294
+ { label: "Description", key: "description", type: "inline" },
1295
+ { label: "Default", key: "isDefault", type: "toggle" }
1296
+ ];
1297
+ function ProjectForm({ onSave, onCancel }) {
1298
+ const [focusIndex, setFocusIndex] = useState4(0);
1299
+ const [values, setValues] = useState4({
1300
+ name: "",
1301
+ key: "",
1302
+ description: "",
1303
+ isDefault: "no"
1304
+ });
1305
+ const currentField = FIELDS2[focusIndex];
1306
+ useInput4((input, key) => {
1307
+ if (key.escape) {
1308
+ onCancel();
1309
+ return;
1310
+ }
1311
+ if (!currentField) return;
1312
+ if (key.tab) {
1313
+ if (key.shift) {
1314
+ setFocusIndex((i) => Math.max(0, i - 1));
1315
+ } else {
1316
+ setFocusIndex((i) => Math.min(FIELDS2.length - 1, i + 1));
1317
+ }
1318
+ return;
1319
+ }
1320
+ if (input === "s" && key.ctrl) {
1321
+ const nameVal = values["name"];
1322
+ if (typeof nameVal === "string" && nameVal.trim()) {
1323
+ onSave({
1324
+ name: nameVal,
1325
+ key: values["key"] ?? "",
1326
+ description: values["description"] ?? "",
1327
+ isDefault: values["isDefault"] === "yes"
1328
+ });
1329
+ }
1330
+ return;
1331
+ }
1332
+ if (currentField.type === "inline") {
1333
+ const currentValue = values[currentField.key] ?? "";
1334
+ if (key.backspace || key.delete) {
1335
+ setValues((v) => ({ ...v, [currentField.key]: currentValue.slice(0, -1) }));
1336
+ } else if (key.return) {
1337
+ setFocusIndex((i) => Math.min(FIELDS2.length - 1, i + 1));
1338
+ } else if (input && !key.ctrl && !key.meta) {
1339
+ setValues((v) => ({ ...v, [currentField.key]: currentValue + input }));
1340
+ }
1341
+ }
1342
+ if (currentField.type === "toggle") {
1343
+ if (key.return || key.rightArrow || key.leftArrow || input === " ") {
1344
+ setValues((v) => ({
1345
+ ...v,
1346
+ [currentField.key]: v[currentField.key] === "yes" ? "no" : "yes"
1347
+ }));
1348
+ }
1349
+ }
1350
+ });
1351
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", flexGrow: 1, borderStyle: "bold", borderColor: theme.borderFocus, children: [
1352
+ /* @__PURE__ */ jsx10(Box10, { gap: 0, children: /* @__PURE__ */ jsxs8(Text10, { color: theme.title, bold: true, children: [
1353
+ " ",
1354
+ "new project"
1355
+ ] }) }),
1356
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 0, children: FIELDS2.map((field, i) => {
1357
+ const isFocused = i === focusIndex;
1358
+ const value = values[field.key] ?? "";
1359
+ return /* @__PURE__ */ jsxs8(Box10, { gap: 1, children: [
1360
+ /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.dialog.label : theme.yaml.key, bold: isFocused, children: [
1361
+ isFocused ? ">" : " ",
1362
+ " ",
1363
+ field.label.padEnd(14)
1364
+ ] }),
1365
+ field.type === "inline" && /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1366
+ value,
1367
+ isFocused ? /* @__PURE__ */ jsx10(Text10, { color: theme.titleHighlight, children: "_" }) : "",
1368
+ field.key === "key" && !value && /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: isFocused ? " (auto from name)" : "" })
1369
+ ] }),
1370
+ field.type === "toggle" && /* @__PURE__ */ jsxs8(Text10, { color: isFocused ? theme.yaml.value : theme.table.fg, children: [
1371
+ isFocused ? "< " : " ",
1372
+ value,
1373
+ isFocused ? " >" : ""
1374
+ ] })
1375
+ ] }, field.key);
1376
+ }) }),
1377
+ /* @__PURE__ */ jsx10(Box10, { flexGrow: 1 }),
1378
+ /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "tab: next | shift+tab: prev | ctrl+s: save | esc: cancel" }) })
1379
+ ] });
1380
+ }
1381
+
1382
+ // src/tui/components/HelpOverlay.tsx
1383
+ import { Box as Box11, Text as Text11 } from "ink";
1384
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1385
+ var SECTIONS = [
1386
+ {
1387
+ title: "NAVIGATION",
1388
+ keys: [
1389
+ ["j/k", "Up/Down"],
1390
+ ["g/G", "Top/Bottom"],
1391
+ ["enter", "View"],
1392
+ ["esc", "Back"]
1393
+ ]
1394
+ },
1395
+ {
1396
+ title: "ACTIONS",
1397
+ keys: [
1398
+ ["c", "Create"],
1399
+ ["e", "Edit"],
1400
+ ["d", "Delete"],
1401
+ ["s", "Status cycle"],
1402
+ ["D", "Dependencies"]
1403
+ ]
1404
+ },
1405
+ {
1406
+ title: "REORDER",
1407
+ keys: [
1408
+ ["\u2190", "Enter reorder"],
1409
+ ["\u2191\u2193", "Move task"],
1410
+ ["\u2192", "Save position"],
1411
+ ["esc/\u2190", "Cancel"]
1412
+ ]
1413
+ },
1414
+ {
1415
+ title: "FILTER",
1416
+ keys: [
1417
+ ["/", "Search"],
1418
+ ["f", "Status filter"],
1419
+ ["t", "Type filter"],
1420
+ ["0", "Clear filters"]
1421
+ ]
1422
+ },
1423
+ {
1424
+ title: "DEPS VIEW",
1425
+ keys: [
1426
+ ["a", "Add blocker"],
1427
+ ["x", "Remove dep"],
1428
+ ["enter", "Go to task"],
1429
+ ["esc", "Back"]
1430
+ ]
1431
+ },
1432
+ {
1433
+ title: "GENERAL",
1434
+ keys: [
1435
+ ["p", "Projects"],
1436
+ ["?", "Help"],
1437
+ ["q", "Quit"]
1438
+ ]
1439
+ }
1440
+ ];
1441
+ function HelpOverlay() {
1442
+ return /* @__PURE__ */ jsxs9(
1443
+ Box11,
1444
+ {
1445
+ flexDirection: "column",
1446
+ borderStyle: "bold",
1447
+ borderColor: theme.borderFocus,
1448
+ paddingX: 2,
1449
+ paddingY: 1,
1450
+ children: [
1451
+ /* @__PURE__ */ jsxs9(Text11, { color: theme.title, bold: true, children: [
1452
+ " ",
1453
+ "Help"
1454
+ ] }),
1455
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
1456
+ /* @__PURE__ */ jsx11(Box11, { flexDirection: "row", gap: 4, children: SECTIONS.map((section) => /* @__PURE__ */ jsxs9(Box11, { flexDirection: "column", children: [
1457
+ /* @__PURE__ */ jsx11(Text11, { color: theme.table.headerFg, bold: true, children: section.title }),
1458
+ section.keys.map(([key, desc]) => /* @__PURE__ */ jsxs9(Box11, { gap: 1, children: [
1459
+ /* @__PURE__ */ jsxs9(Text11, { color: theme.menu.key, bold: true, children: [
1460
+ "<",
1461
+ (key ?? "").padEnd(5),
1462
+ ">"
1463
+ ] }),
1464
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: desc })
1465
+ ] }, key))
1466
+ ] }, section.title)) }),
1467
+ /* @__PURE__ */ jsx11(Text11, { children: " " }),
1468
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "Press any key to close" })
1469
+ ]
1470
+ }
1471
+ );
1472
+ }
1473
+
1474
+ // src/tui/components/ConfirmDialog.tsx
1475
+ import { Box as Box12, Text as Text12 } from "ink";
1476
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1477
+ function ConfirmDialog({ task }) {
1478
+ return /* @__PURE__ */ jsxs10(
1479
+ Box12,
1480
+ {
1481
+ flexDirection: "column",
1482
+ borderStyle: "bold",
1483
+ borderColor: theme.status.error,
1484
+ paddingX: 3,
1485
+ paddingY: 1,
1486
+ alignSelf: "center",
1487
+ children: [
1488
+ /* @__PURE__ */ jsx12(Text12, { color: theme.dialog.label, bold: true, children: "<Delete>" }),
1489
+ /* @__PURE__ */ jsx12(Text12, { children: " " }),
1490
+ /* @__PURE__ */ jsxs10(Text12, { color: theme.dialog.fg, children: [
1491
+ 'Delete task "',
1492
+ task.name,
1493
+ '"?'
1494
+ ] }),
1495
+ /* @__PURE__ */ jsx12(Text12, { children: " " }),
1496
+ /* @__PURE__ */ jsxs10(Box12, { gap: 3, children: [
1497
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(
1498
+ Text12,
1499
+ {
1500
+ backgroundColor: theme.dialog.buttonFocusBg,
1501
+ color: theme.dialog.buttonFocusFg,
1502
+ bold: true,
1503
+ children: " y: OK "
1504
+ }
1505
+ ) }),
1506
+ /* @__PURE__ */ jsx12(Box12, { children: /* @__PURE__ */ jsx12(Text12, { backgroundColor: theme.dialog.buttonBg, color: theme.dialog.buttonFg, children: " n: Cancel " }) })
1507
+ ] })
1508
+ ]
1509
+ }
1510
+ );
1511
+ }
1512
+
1513
+ // src/tui/components/DependencyList.tsx
1514
+ import { Box as Box13, Text as Text13 } from "ink";
1515
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1516
+ function TaskRow({
1517
+ task,
1518
+ globalIndex,
1519
+ selectedIndex
1520
+ }) {
1521
+ const isSelected = globalIndex === selectedIndex;
1522
+ const statusColor = STATUS_COLOR[task.status] ?? theme.table.fg;
1523
+ return /* @__PURE__ */ jsx13(Box13, { children: isSelected ? /* @__PURE__ */ jsxs11(Text13, { backgroundColor: theme.table.cursorBg, color: theme.table.cursorFg, bold: true, children: [
1524
+ "> ",
1525
+ task.id.padEnd(12),
1526
+ task.status.padEnd(14),
1527
+ task.name
1528
+ ] }) : /* @__PURE__ */ jsxs11(Fragment2, { children: [
1529
+ /* @__PURE__ */ jsx13(Text13, { children: " " }),
1530
+ /* @__PURE__ */ jsx13(Text13, { color: theme.yaml.value, children: task.id.padEnd(12) }),
1531
+ /* @__PURE__ */ jsx13(Text13, { color: statusColor, children: task.status.padEnd(14) }),
1532
+ /* @__PURE__ */ jsx13(Text13, { color: theme.table.fg, children: task.name })
1533
+ ] }) }, task.id);
1534
+ }
1535
+ function DependencyList({
1536
+ task,
1537
+ blockers,
1538
+ dependents,
1539
+ related,
1540
+ duplicates,
1541
+ selectedIndex,
1542
+ isAddingDep,
1543
+ addDepInput
1544
+ }) {
1545
+ let offset = 0;
1546
+ const blockersOffset = offset;
1547
+ offset += blockers.length;
1548
+ const dependentsOffset = offset;
1549
+ offset += dependents.length;
1550
+ const relatedOffset = offset;
1551
+ offset += related.length;
1552
+ const duplicatesOffset = offset;
1553
+ return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", flexGrow: 1, borderStyle: "bold", borderColor: theme.borderFocus, children: [
1554
+ /* @__PURE__ */ jsxs11(Box13, { gap: 0, children: [
1555
+ /* @__PURE__ */ jsxs11(Text13, { color: theme.title, bold: true, children: [
1556
+ " ",
1557
+ "dependencies"
1558
+ ] }),
1559
+ /* @__PURE__ */ jsx13(Text13, { color: theme.fg, children: "(" }),
1560
+ /* @__PURE__ */ jsx13(Text13, { color: theme.titleHighlight, bold: true, children: task.name }),
1561
+ /* @__PURE__ */ jsx13(Text13, { color: theme.fg, children: ")" })
1562
+ ] }),
1563
+ /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
1564
+ /* @__PURE__ */ jsxs11(Text13, { color: theme.table.headerFg, bold: true, children: [
1565
+ "BLOCKED BY (",
1566
+ blockers.length,
1567
+ ")"
1568
+ ] }),
1569
+ blockers.length === 0 ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " No blockers" }) : blockers.map((t, i) => /* @__PURE__ */ jsx13(
1570
+ TaskRow,
1571
+ {
1572
+ task: t,
1573
+ globalIndex: blockersOffset + i,
1574
+ selectedIndex
1575
+ },
1576
+ t.id
1577
+ ))
1578
+ ] }),
1579
+ /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
1580
+ /* @__PURE__ */ jsxs11(Text13, { color: theme.table.headerFg, bold: true, children: [
1581
+ "BLOCKS (",
1582
+ dependents.length,
1583
+ ")"
1584
+ ] }),
1585
+ dependents.length === 0 ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " No dependents" }) : dependents.map((t, i) => /* @__PURE__ */ jsx13(
1586
+ TaskRow,
1587
+ {
1588
+ task: t,
1589
+ globalIndex: dependentsOffset + i,
1590
+ selectedIndex
1591
+ },
1592
+ t.id
1593
+ ))
1594
+ ] }),
1595
+ /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
1596
+ /* @__PURE__ */ jsxs11(Text13, { color: theme.table.headerFg, bold: true, children: [
1597
+ "RELATES TO (",
1598
+ related.length,
1599
+ ")"
1600
+ ] }),
1601
+ related.length === 0 ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " No related tasks" }) : related.map((t, i) => /* @__PURE__ */ jsx13(
1602
+ TaskRow,
1603
+ {
1604
+ task: t,
1605
+ globalIndex: relatedOffset + i,
1606
+ selectedIndex
1607
+ },
1608
+ t.id
1609
+ ))
1610
+ ] }),
1611
+ /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [
1612
+ /* @__PURE__ */ jsxs11(Text13, { color: theme.table.headerFg, bold: true, children: [
1613
+ "DUPLICATES (",
1614
+ duplicates.length,
1615
+ ")"
1616
+ ] }),
1617
+ duplicates.length === 0 ? /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: " No duplicate tasks" }) : duplicates.map((t, i) => /* @__PURE__ */ jsx13(
1618
+ TaskRow,
1619
+ {
1620
+ task: t,
1621
+ globalIndex: duplicatesOffset + i,
1622
+ selectedIndex
1623
+ },
1624
+ t.id
1625
+ ))
1626
+ ] }),
1627
+ /* @__PURE__ */ jsx13(Box13, { flexGrow: 1 }),
1628
+ isAddingDep && /* @__PURE__ */ jsxs11(Box13, { borderStyle: "round", borderColor: theme.prompt, paddingX: 1, children: [
1629
+ /* @__PURE__ */ jsx13(Text13, { color: theme.prompt, children: "depends on (id or id:type): " }),
1630
+ /* @__PURE__ */ jsx13(Text13, { color: theme.prompt, children: addDepInput }),
1631
+ /* @__PURE__ */ jsx13(Text13, { color: theme.promptSuggest, children: "_" })
1632
+ ] }),
1633
+ /* @__PURE__ */ jsx13(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "a: add dep (id or id:relates-to) | x: remove selected | enter: go to task | esc: back" }) })
1634
+ ] });
1635
+ }
1636
+
1637
+ // src/tui/components/App.tsx
1638
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1639
+ var STATUS_CYCLE = [
1640
+ TaskStatus.Backlog,
1641
+ TaskStatus.Todo,
1642
+ TaskStatus.InProgress,
1643
+ TaskStatus.Review,
1644
+ TaskStatus.Done
1645
+ ];
1646
+ function App({ container, initialProject }) {
1647
+ const { exit } = useApp();
1648
+ const [state, dispatch] = useReducer(appReducer, initialState);
1649
+ const loadProjects = useCallback2(() => {
1650
+ const result = container.projectService.listProjects();
1651
+ if (result.ok) {
1652
+ logger.info(`TUI.loadProjects: loaded ${result.value.length} projects`);
1653
+ dispatch({ type: "SET_PROJECTS", projects: result.value });
1654
+ } else {
1655
+ logger.error("TUI.loadProjects: failed", result.error);
1656
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1657
+ }
1658
+ }, [container]);
1659
+ const loadTasks = useCallback2(() => {
1660
+ logger.startSpan("TUI.loadTasks", () => {
1661
+ const filter = { ...state.filter };
1662
+ if (state.activeProject) {
1663
+ filter.projectId = state.activeProject.id;
1664
+ }
1665
+ const result = container.taskService.listTasks(filter);
1666
+ if (result.ok) {
1667
+ logger.info(`TUI.loadTasks: loaded ${result.value.length} tasks`);
1668
+ dispatch({ type: "SET_TASKS", tasks: result.value });
1669
+ } else {
1670
+ logger.error("TUI.loadTasks: failed", result.error);
1671
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1672
+ }
1673
+ });
1674
+ }, [container, state.filter, state.activeProject]);
1675
+ const loadDeps = useCallback2(
1676
+ (taskId) => {
1677
+ const blockersResult = container.dependencyService.listBlockers(taskId);
1678
+ const dependentsResult = container.dependencyService.listDependents(taskId);
1679
+ const relatedResult = container.dependencyService.listRelated(taskId);
1680
+ const duplicatesResult = container.dependencyService.listDuplicates(taskId);
1681
+ dispatch({
1682
+ type: "SET_DEPS",
1683
+ blockers: blockersResult.ok ? blockersResult.value : [],
1684
+ dependents: dependentsResult.ok ? dependentsResult.value : [],
1685
+ related: relatedResult.ok ? relatedResult.value : [],
1686
+ duplicates: duplicatesResult.ok ? duplicatesResult.value : []
1687
+ });
1688
+ },
1689
+ [container]
1690
+ );
1691
+ const cycleStatus = useCallback2(
1692
+ (task) => {
1693
+ const currentIndex = STATUS_CYCLE.indexOf(task.status);
1694
+ const nextStatus = STATUS_CYCLE[(currentIndex + 1) % STATUS_CYCLE.length];
1695
+ if (!nextStatus) return;
1696
+ const result = container.taskService.updateTask(task.id, { status: nextStatus });
1697
+ if (result.ok) {
1698
+ dispatch({ type: "FLASH", message: `Status -> ${nextStatus}`, level: "info" });
1699
+ if (state.selectedTask?.id === task.id) {
1700
+ dispatch({ type: "SELECT_TASK", task: result.value });
1701
+ }
1702
+ loadTasks();
1703
+ } else {
1704
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1705
+ }
1706
+ },
1707
+ [container, state.selectedTask, loadTasks]
1708
+ );
1709
+ const saveReorder = useCallback2(() => {
1710
+ const tasks = state.tasks;
1711
+ const idx = state.selectedIndex;
1712
+ const task = tasks[idx];
1713
+ if (!task) return;
1714
+ const prev = tasks[idx - 1];
1715
+ const next = tasks[idx + 1];
1716
+ const result = prev ? container.taskService.rerankTask({ taskId: task.id, afterId: prev.id }) : next ? container.taskService.rerankTask({ taskId: task.id, beforeId: next.id }) : container.taskService.rerankTask({ taskId: task.id, position: 1 });
1717
+ dispatch({ type: "EXIT_REORDER", save: result.ok });
1718
+ dispatch({
1719
+ type: "FLASH",
1720
+ message: result.ok ? "Rank saved" : result.error.message,
1721
+ level: result.ok ? "info" : "error"
1722
+ });
1723
+ loadTasks();
1724
+ }, [container, state.tasks, state.selectedIndex, loadTasks]);
1725
+ useEffect(() => {
1726
+ loadProjects();
1727
+ }, [loadProjects]);
1728
+ useEffect(() => {
1729
+ if (state.projects.length > 0 && !state.activeProject) {
1730
+ logger.info(`TUI.resolveProject: resolving initialProject=${initialProject ?? "(default)"}`);
1731
+ const result = container.projectService.resolveProject(initialProject);
1732
+ if (result.ok) {
1733
+ logger.info(
1734
+ `TUI.resolveProject: resolved to key=${result.value.key} name=${result.value.name}`
1735
+ );
1736
+ dispatch({ type: "SET_ACTIVE_PROJECT", project: result.value });
1737
+ } else {
1738
+ logger.error("TUI.resolveProject: failed", result.error);
1739
+ const fallback = state.projects[0] ?? null;
1740
+ logger.info(`TUI.resolveProject: falling back to ${fallback?.name ?? "null"}`);
1741
+ dispatch({ type: "SET_ACTIVE_PROJECT", project: fallback });
1742
+ }
1743
+ }
1744
+ }, [state.projects, state.activeProject, initialProject, container]);
1745
+ useEffect(() => {
1746
+ if (state.activeProject) {
1747
+ loadTasks();
1748
+ }
1749
+ }, [state.activeProject, state.filter, loadTasks]);
1750
+ useEffect(() => {
1751
+ if (state.flash) {
1752
+ const timer = setTimeout(() => {
1753
+ dispatch({ type: "CLEAR_FLASH" });
1754
+ }, 3e3);
1755
+ return () => {
1756
+ clearTimeout(timer);
1757
+ };
1758
+ }
1759
+ return void 0;
1760
+ }, [state.flash]);
1761
+ useInput5((input, key) => {
1762
+ if (state.confirmDelete) {
1763
+ if (input === "y") {
1764
+ const result = container.taskService.deleteTask(state.confirmDelete.id);
1765
+ if (result.ok) {
1766
+ dispatch({ type: "FLASH", message: "Task deleted", level: "info" });
1767
+ dispatch({ type: "CANCEL_DELETE" });
1768
+ if (state.activeView === ViewType.TaskDetail) {
1769
+ dispatch({ type: "GO_BACK" });
1770
+ }
1771
+ loadTasks();
1772
+ } else {
1773
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1774
+ dispatch({ type: "CANCEL_DELETE" });
1775
+ }
1776
+ } else if (input === "n" || key.escape) {
1777
+ dispatch({ type: "CANCEL_DELETE" });
1778
+ }
1779
+ return;
1780
+ }
1781
+ if (state.activeView === ViewType.Help) {
1782
+ dispatch({ type: "GO_BACK" });
1783
+ return;
1784
+ }
1785
+ if (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit || state.activeView === ViewType.ProjectSelector || state.activeView === ViewType.ProjectCreate) {
1786
+ return;
1787
+ }
1788
+ if (state.activeView === ViewType.DependencyList && state.isAddingDep) {
1789
+ if (key.escape) {
1790
+ dispatch({ type: "SET_ADDING_DEP", active: false });
1791
+ return;
1792
+ }
1793
+ if (key.return && state.addDepInput.trim() && state.selectedTask) {
1794
+ const raw = state.addDepInput.trim();
1795
+ const colonIdx = raw.lastIndexOf(":");
1796
+ const validTypes = /* @__PURE__ */ new Set(["blocks", "relates-to", "duplicates"]);
1797
+ let depId;
1798
+ let depType;
1799
+ if (colonIdx > 0) {
1800
+ const maybetype = raw.slice(colonIdx + 1);
1801
+ if (validTypes.has(maybetype)) {
1802
+ depId = raw.slice(0, colonIdx);
1803
+ depType = maybetype;
1804
+ } else {
1805
+ depId = raw;
1806
+ }
1807
+ } else {
1808
+ depId = raw;
1809
+ }
1810
+ const result = container.dependencyService.addDependency({
1811
+ taskId: state.selectedTask.id,
1812
+ dependsOnId: depId,
1813
+ type: depType
1814
+ });
1815
+ if (result.ok) {
1816
+ dispatch({ type: "FLASH", message: "Dependency added", level: "info" });
1817
+ loadDeps(state.selectedTask.id);
1818
+ } else {
1819
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
1820
+ }
1821
+ dispatch({ type: "SET_ADDING_DEP", active: false });
1822
+ return;
1823
+ }
1824
+ if (key.backspace || key.delete) {
1825
+ dispatch({ type: "SET_ADD_DEP_INPUT", input: state.addDepInput.slice(0, -1) });
1826
+ return;
1827
+ }
1828
+ if (input && !key.ctrl && !key.meta) {
1829
+ dispatch({ type: "SET_ADD_DEP_INPUT", input: state.addDepInput + input });
1830
+ return;
1831
+ }
1832
+ return;
1833
+ }
1834
+ if (state.isSearchActive) {
1835
+ if (key.escape) {
1836
+ dispatch({ type: "SET_SEARCH_ACTIVE", active: false });
1837
+ dispatch({ type: "SET_SEARCH_QUERY", query: "" });
1838
+ dispatch({ type: "SET_FILTER", filter: { search: void 0 } });
1839
+ return;
1840
+ }
1841
+ if (key.return) {
1842
+ dispatch({ type: "SET_SEARCH_ACTIVE", active: false });
1843
+ dispatch({ type: "SET_FILTER", filter: { search: state.searchQuery || void 0 } });
1844
+ return;
1845
+ }
1846
+ if (key.backspace || key.delete) {
1847
+ dispatch({ type: "SET_SEARCH_QUERY", query: state.searchQuery.slice(0, -1) });
1848
+ return;
1849
+ }
1850
+ if (input && !key.ctrl && !key.meta) {
1851
+ dispatch({ type: "SET_SEARCH_QUERY", query: state.searchQuery + input });
1852
+ return;
1853
+ }
1854
+ return;
1855
+ }
1856
+ if (state.isReordering) {
1857
+ if (key.upArrow || input === "k") {
1858
+ dispatch({ type: "REORDER_MOVE", direction: "up" });
1859
+ return;
1860
+ }
1861
+ if (key.downArrow || input === "j") {
1862
+ dispatch({ type: "REORDER_MOVE", direction: "down" });
1863
+ return;
1864
+ }
1865
+ if (key.rightArrow) {
1866
+ saveReorder();
1867
+ return;
1868
+ }
1869
+ if (key.escape || key.leftArrow) {
1870
+ dispatch({ type: "EXIT_REORDER", save: false });
1871
+ dispatch({ type: "FLASH", message: "Reorder cancelled", level: "info" });
1872
+ return;
1873
+ }
1874
+ return;
1875
+ }
1876
+ if (input === "q" && state.activeView === ViewType.TaskList && state.focusedPanel === "list") {
1877
+ exit();
1878
+ return;
1879
+ }
1880
+ if (input === "q" || key.escape) {
1881
+ if (state.activeView === ViewType.TaskList && state.focusedPanel === "detail") {
1882
+ dispatch({ type: "SET_PANEL_FOCUS", panel: "list" });
1883
+ return;
1884
+ }
1885
+ dispatch({ type: "GO_BACK" });
1886
+ return;
1887
+ }
1888
+ if (input === "?") {
1889
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.Help });
1890
+ return;
1891
+ }
1892
+ if (key.tab && state.activeView === ViewType.TaskList && previewTask) {
1893
+ dispatch({
1894
+ type: "SET_PANEL_FOCUS",
1895
+ panel: state.focusedPanel === "list" ? "detail" : "list"
1896
+ });
1897
+ return;
1898
+ }
1899
+ if (state.activeView === ViewType.TaskList && state.focusedPanel === "list") {
1900
+ if (key.upArrow || input === "k") {
1901
+ dispatch({ type: "MOVE_CURSOR", direction: "up" });
1902
+ return;
1903
+ }
1904
+ if (key.downArrow || input === "j") {
1905
+ dispatch({ type: "MOVE_CURSOR", direction: "down" });
1906
+ return;
1907
+ }
1908
+ if (input === "g") {
1909
+ dispatch({ type: "SET_CURSOR", index: 0 });
1910
+ return;
1911
+ }
1912
+ if (input === "G") {
1913
+ dispatch({ type: "SET_CURSOR", index: state.tasks.length - 1 });
1914
+ return;
1915
+ }
1916
+ if (key.return) {
1917
+ const task = state.tasks[state.selectedIndex];
1918
+ if (task) {
1919
+ dispatch({ type: "SELECT_TASK", task });
1920
+ loadDeps(task.id);
1921
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskDetail });
1922
+ }
1923
+ return;
1924
+ }
1925
+ if (input === "c") {
1926
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskCreate });
1927
+ return;
1928
+ }
1929
+ if (input === "e") {
1930
+ const task = state.tasks[state.selectedIndex];
1931
+ if (task) {
1932
+ dispatch({ type: "SELECT_TASK", task });
1933
+ loadDeps(task.id);
1934
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
1935
+ }
1936
+ return;
1937
+ }
1938
+ if (input === "d") {
1939
+ const task = state.tasks[state.selectedIndex];
1940
+ if (task) {
1941
+ dispatch({ type: "CONFIRM_DELETE", task });
1942
+ }
1943
+ return;
1944
+ }
1945
+ if (input === "s") {
1946
+ const task = state.tasks[state.selectedIndex];
1947
+ if (task) {
1948
+ cycleStatus(task);
1949
+ }
1950
+ return;
1951
+ }
1952
+ if (input === "/") {
1953
+ dispatch({ type: "SET_SEARCH_ACTIVE", active: true });
1954
+ dispatch({ type: "SET_SEARCH_QUERY", query: "" });
1955
+ return;
1956
+ }
1957
+ if (input === "p") {
1958
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.ProjectSelector });
1959
+ return;
1960
+ }
1961
+ if (key.leftArrow) {
1962
+ if (state.tasks.length > 0) {
1963
+ dispatch({ type: "ENTER_REORDER" });
1964
+ dispatch({ type: "FLASH", message: "Reorder: \u2191\u2193 move, \u2192 save, \u2190 cancel", level: "info" });
1965
+ }
1966
+ return;
1967
+ }
1968
+ if (input === "f") {
1969
+ const currentStatus = state.filter.status;
1970
+ const currentIndex = currentStatus ? STATUS_VALUES.indexOf(currentStatus) : -1;
1971
+ const nextIndex = currentIndex + 1;
1972
+ const nextStatus = nextIndex < STATUS_VALUES.length ? STATUS_VALUES[nextIndex] : void 0;
1973
+ dispatch({ type: "SET_FILTER", filter: { status: nextStatus } });
1974
+ return;
1975
+ }
1976
+ if (input === "t") {
1977
+ const currentType = state.filter.type;
1978
+ const currentIndex = currentType ? TYPE_VALUES.indexOf(currentType) : -1;
1979
+ const nextIndex = currentIndex + 1;
1980
+ const nextType = nextIndex < TYPE_VALUES.length ? TYPE_VALUES[nextIndex] : void 0;
1981
+ dispatch({ type: "SET_FILTER", filter: { type: nextType } });
1982
+ return;
1983
+ }
1984
+ if (input === "0") {
1985
+ dispatch({ type: "CLEAR_FILTER" });
1986
+ return;
1987
+ }
1988
+ }
1989
+ if (state.activeView === ViewType.TaskList && state.focusedPanel === "detail" && previewTask) {
1990
+ if (input === "e") {
1991
+ dispatch({ type: "SELECT_TASK", task: previewTask });
1992
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
1993
+ return;
1994
+ }
1995
+ if (input === "d") {
1996
+ dispatch({ type: "CONFIRM_DELETE", task: previewTask });
1997
+ return;
1998
+ }
1999
+ if (input === "s") {
2000
+ cycleStatus(previewTask);
2001
+ return;
2002
+ }
2003
+ if (input === "m") {
2004
+ const allText = `${previewTask.description}
2005
+ ${previewTask.technicalNotes}
2006
+ ${previewTask.additionalRequirements}`;
2007
+ const count = openAllMermaidDiagrams(allText);
2008
+ if (count > 0) {
2009
+ dispatch({
2010
+ type: "FLASH",
2011
+ message: `Opened ${count} diagram${count > 1 ? "s" : ""} in browser`,
2012
+ level: "info"
2013
+ });
2014
+ }
2015
+ return;
2016
+ }
2017
+ if (input === "D") {
2018
+ dispatch({ type: "SELECT_TASK", task: previewTask });
2019
+ loadDeps(previewTask.id);
2020
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.DependencyList });
2021
+ return;
2022
+ }
2023
+ }
2024
+ if (state.activeView === ViewType.TaskDetail) {
2025
+ if (input === "e" && state.selectedTask) {
2026
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskEdit });
2027
+ return;
2028
+ }
2029
+ if (input === "d" && state.selectedTask) {
2030
+ dispatch({ type: "CONFIRM_DELETE", task: state.selectedTask });
2031
+ return;
2032
+ }
2033
+ if (input === "s" && state.selectedTask) {
2034
+ cycleStatus(state.selectedTask);
2035
+ return;
2036
+ }
2037
+ if (input === "m" && state.selectedTask) {
2038
+ const allText = `${state.selectedTask.description}
2039
+ ${state.selectedTask.technicalNotes}
2040
+ ${state.selectedTask.additionalRequirements}`;
2041
+ const count = openAllMermaidDiagrams(allText);
2042
+ if (count > 0) {
2043
+ dispatch({
2044
+ type: "FLASH",
2045
+ message: `Opened ${count} diagram${count > 1 ? "s" : ""} in browser`,
2046
+ level: "info"
2047
+ });
2048
+ }
2049
+ return;
2050
+ }
2051
+ if (input === "D" && state.selectedTask) {
2052
+ loadDeps(state.selectedTask.id);
2053
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.DependencyList });
2054
+ return;
2055
+ }
2056
+ }
2057
+ if (state.activeView === ViewType.DependencyList && state.selectedTask) {
2058
+ if (key.upArrow || input === "k") {
2059
+ dispatch({ type: "DEP_MOVE_CURSOR", direction: "up" });
2060
+ return;
2061
+ }
2062
+ if (key.downArrow || input === "j") {
2063
+ dispatch({ type: "DEP_MOVE_CURSOR", direction: "down" });
2064
+ return;
2065
+ }
2066
+ if (input === "a") {
2067
+ dispatch({ type: "SET_ADDING_DEP", active: true });
2068
+ return;
2069
+ }
2070
+ if (input === "x") {
2071
+ const allItems = [
2072
+ ...state.depBlockers,
2073
+ ...state.depDependents,
2074
+ ...state.depRelated,
2075
+ ...state.depDuplicates
2076
+ ];
2077
+ const selected = allItems[state.depSelectedIndex];
2078
+ if (selected) {
2079
+ const result = container.dependencyService.removeDependencyBetween(
2080
+ state.selectedTask.id,
2081
+ selected.id
2082
+ );
2083
+ if (result.ok) {
2084
+ dispatch({ type: "FLASH", message: "Dependency removed", level: "info" });
2085
+ loadDeps(state.selectedTask.id);
2086
+ } else {
2087
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2088
+ }
2089
+ }
2090
+ return;
2091
+ }
2092
+ if (key.return) {
2093
+ const allItems = [
2094
+ ...state.depBlockers,
2095
+ ...state.depDependents,
2096
+ ...state.depRelated,
2097
+ ...state.depDuplicates
2098
+ ];
2099
+ const selected = allItems[state.depSelectedIndex];
2100
+ if (selected) {
2101
+ dispatch({ type: "SELECT_TASK", task: selected });
2102
+ loadDeps(selected.id);
2103
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.TaskDetail });
2104
+ }
2105
+ return;
2106
+ }
2107
+ }
2108
+ });
2109
+ const handleFormSave = useCallback2(
2110
+ (data) => {
2111
+ if (state.activeView === ViewType.TaskEdit && state.selectedTask) {
2112
+ const taskId = state.selectedTask.id;
2113
+ const result = container.taskService.updateTask(taskId, data);
2114
+ if (!result.ok) {
2115
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2116
+ return;
2117
+ }
2118
+ const currentDepsResult = container.dependencyService.listAllDeps(taskId);
2119
+ const currentDeps = currentDepsResult.ok ? currentDepsResult.value : [];
2120
+ const newDeps = data.dependsOn ?? [];
2121
+ const currentKeys = new Set(currentDeps.map((d) => `${d.dependsOnId}:${d.type}`));
2122
+ const newKeys = new Set(newDeps.map((d) => `${d.id}:${d.type}`));
2123
+ for (const dep of currentDeps) {
2124
+ if (!newKeys.has(`${dep.dependsOnId}:${dep.type}`)) {
2125
+ container.dependencyService.removeDependency({
2126
+ taskId,
2127
+ dependsOnId: dep.dependsOnId
2128
+ });
2129
+ }
2130
+ }
2131
+ for (const entry of newDeps) {
2132
+ if (currentKeys.has(`${entry.id}:${entry.type}`)) continue;
2133
+ container.dependencyService.addDependency({
2134
+ taskId,
2135
+ dependsOnId: entry.id,
2136
+ type: entry.type
2137
+ });
2138
+ }
2139
+ dispatch({ type: "FLASH", message: "Task updated", level: "info" });
2140
+ dispatch({ type: "SELECT_TASK", task: result.value });
2141
+ loadDeps(taskId);
2142
+ dispatch({ type: "GO_BACK" });
2143
+ loadTasks();
2144
+ } else {
2145
+ const result = container.taskService.createTask(data, state.activeProject?.id);
2146
+ if (result.ok) {
2147
+ dispatch({ type: "FLASH", message: "Task created", level: "info" });
2148
+ dispatch({ type: "GO_BACK" });
2149
+ loadTasks();
2150
+ } else {
2151
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2152
+ }
2153
+ }
2154
+ },
2155
+ [container, state.activeView, state.selectedTask, state.activeProject, loadTasks, loadDeps]
2156
+ );
2157
+ const handleFormCancel = useCallback2(() => {
2158
+ dispatch({ type: "GO_BACK" });
2159
+ }, []);
2160
+ const handleProjectSelect = useCallback2((project) => {
2161
+ dispatch({ type: "SET_ACTIVE_PROJECT", project });
2162
+ dispatch({ type: "GO_BACK" });
2163
+ dispatch({ type: "FLASH", message: `Switched to: ${project.name}`, level: "info" });
2164
+ }, []);
2165
+ const handleSetDefault = useCallback2(
2166
+ (project) => {
2167
+ const result = container.projectService.updateProject(project.id, { isDefault: true });
2168
+ if (result.ok) {
2169
+ logger.info(`TUI.setDefault: set key=${result.value.key} as default`);
2170
+ dispatch({ type: "FLASH", message: `Default project: ${result.value.name}`, level: "info" });
2171
+ loadProjects();
2172
+ } else {
2173
+ logger.error("TUI.setDefault: failed", result.error);
2174
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2175
+ }
2176
+ },
2177
+ [container, loadProjects]
2178
+ );
2179
+ const handleProjectCreate = useCallback2(() => {
2180
+ dispatch({ type: "NAVIGATE_TO", view: ViewType.ProjectCreate });
2181
+ }, []);
2182
+ const handleProjectFormSave = useCallback2(
2183
+ (data) => {
2184
+ const result = container.projectService.createProject({
2185
+ name: data.name,
2186
+ key: data.key || void 0,
2187
+ description: data.description || void 0,
2188
+ isDefault: data.isDefault
2189
+ });
2190
+ if (result.ok) {
2191
+ logger.info(`TUI.createProject: created key=${result.value.key} name=${result.value.name}`);
2192
+ dispatch({
2193
+ type: "FLASH",
2194
+ message: `Project created: ${result.value.name}`,
2195
+ level: "info"
2196
+ });
2197
+ dispatch({ type: "SET_ACTIVE_PROJECT", project: result.value });
2198
+ dispatch({ type: "GO_BACK" });
2199
+ dispatch({ type: "GO_BACK" });
2200
+ loadProjects();
2201
+ } else {
2202
+ logger.error("TUI.createProject: failed", result.error);
2203
+ dispatch({ type: "FLASH", message: result.error.message, level: "error" });
2204
+ }
2205
+ },
2206
+ [container, loadProjects]
2207
+ );
2208
+ const handleProjectFormCancel = useCallback2(() => {
2209
+ dispatch({ type: "GO_BACK" });
2210
+ }, []);
2211
+ const handleProjectCancel = useCallback2(() => {
2212
+ dispatch({ type: "GO_BACK" });
2213
+ }, []);
2214
+ const previewTask = state.tasks[state.selectedIndex] ?? null;
2215
+ const initialDepsForEdit = useMemo(() => {
2216
+ return [
2217
+ ...state.depBlockers.map((t) => ({ id: t.id, name: t.name, type: DependencyType.Blocks })),
2218
+ ...state.depRelated.map((t) => ({ id: t.id, name: t.name, type: DependencyType.RelatesTo })),
2219
+ ...state.depDuplicates.map((t) => ({
2220
+ id: t.id,
2221
+ name: t.name,
2222
+ type: DependencyType.Duplicates
2223
+ }))
2224
+ ];
2225
+ }, [state.depBlockers, state.depRelated, state.depDuplicates]);
2226
+ const allProjectTasks = useMemo(() => {
2227
+ if (!state.activeProject) return [];
2228
+ const result = container.taskService.listTasks({ projectId: state.activeProject.id });
2229
+ return result.ok ? result.value : [];
2230
+ }, [container, state.activeProject, state.tasks]);
2231
+ const previewTaskId = previewTask?.id ?? null;
2232
+ useEffect(() => {
2233
+ if (state.activeView === ViewType.TaskList && previewTaskId) {
2234
+ loadDeps(previewTaskId);
2235
+ }
2236
+ }, [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(
2243
+ TaskList,
2244
+ {
2245
+ tasks: state.tasks,
2246
+ selectedIndex: state.selectedIndex,
2247
+ searchQuery: state.searchQuery,
2248
+ isSearchActive: state.isSearchActive,
2249
+ isReordering: state.isReordering,
2250
+ filter: state.filter,
2251
+ activeProjectName: state.activeProject?.name ?? "none",
2252
+ nonTerminalBlockerIds: new Set(
2253
+ state.depBlockers.filter((t) => !isTerminalStatus(t.status)).map((t) => t.id)
2254
+ ),
2255
+ nonTerminalDependentIds: new Set(
2256
+ state.depDependents.filter((t) => !isTerminalStatus(t.status)).map((t) => t.id)
2257
+ ),
2258
+ isSelectedBlocked: state.depBlockers.some((t) => !isTerminalStatus(t.status)),
2259
+ isFocused: state.focusedPanel === "list"
2260
+ }
2261
+ ) }),
2262
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, children: previewTask ? /* @__PURE__ */ jsx14(
2263
+ TaskDetail,
2264
+ {
2265
+ task: previewTask,
2266
+ blockers: state.depBlockers,
2267
+ dependents: state.depDependents,
2268
+ related: state.depRelated,
2269
+ duplicates: state.depDuplicates,
2270
+ isFocused: state.focusedPanel === "detail"
2271
+ }
2272
+ ) : /* @__PURE__ */ jsxs12(
2273
+ Box14,
2274
+ {
2275
+ flexDirection: "column",
2276
+ flexGrow: 1,
2277
+ borderStyle: "bold",
2278
+ borderColor: theme.border,
2279
+ children: [
2280
+ /* @__PURE__ */ jsx14(Box14, { children: /* @__PURE__ */ jsxs12(Text14, { color: theme.title, bold: true, children: [
2281
+ " ",
2282
+ "detail"
2283
+ ] }) }),
2284
+ /* @__PURE__ */ jsx14(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No task selected" }) })
2285
+ ]
2286
+ }
2287
+ ) })
2288
+ ] }),
2289
+ !state.confirmDelete && state.activeView === ViewType.TaskDetail && state.selectedTask && /* @__PURE__ */ jsx14(
2290
+ TaskDetail,
2291
+ {
2292
+ task: state.selectedTask,
2293
+ blockers: state.depBlockers,
2294
+ dependents: state.depDependents,
2295
+ related: state.depRelated,
2296
+ duplicates: state.depDuplicates
2297
+ }
2298
+ ),
2299
+ !state.confirmDelete && state.activeView === ViewType.DependencyList && state.selectedTask && /* @__PURE__ */ jsx14(
2300
+ DependencyList,
2301
+ {
2302
+ task: state.selectedTask,
2303
+ blockers: state.depBlockers,
2304
+ dependents: state.depDependents,
2305
+ related: state.depRelated,
2306
+ duplicates: state.depDuplicates,
2307
+ selectedIndex: state.depSelectedIndex,
2308
+ isAddingDep: state.isAddingDep,
2309
+ addDepInput: state.addDepInput
2310
+ }
2311
+ ),
2312
+ !state.confirmDelete && (state.activeView === ViewType.TaskCreate || state.activeView === ViewType.TaskEdit) && /* @__PURE__ */ jsx14(
2313
+ TaskForm,
2314
+ {
2315
+ editingTask: state.activeView === ViewType.TaskEdit ? state.selectedTask : null,
2316
+ allTasks: allProjectTasks,
2317
+ initialDeps: state.activeView === ViewType.TaskEdit ? initialDepsForEdit : void 0,
2318
+ onSave: handleFormSave,
2319
+ onCancel: handleFormCancel
2320
+ }
2321
+ ),
2322
+ !state.confirmDelete && state.activeView === ViewType.ProjectSelector && /* @__PURE__ */ jsx14(
2323
+ ProjectSelector,
2324
+ {
2325
+ projects: state.projects,
2326
+ activeProject: state.activeProject,
2327
+ onSelect: handleProjectSelect,
2328
+ onCreate: handleProjectCreate,
2329
+ onSetDefault: handleSetDefault,
2330
+ onCancel: handleProjectCancel
2331
+ }
2332
+ ),
2333
+ !state.confirmDelete && state.activeView === ViewType.ProjectCreate && /* @__PURE__ */ jsx14(ProjectForm, { onSave: handleProjectFormSave, onCancel: handleProjectFormCancel }),
2334
+ !state.confirmDelete && state.activeView === ViewType.Help && /* @__PURE__ */ jsx14(HelpOverlay, {})
2335
+ ] }),
2336
+ /* @__PURE__ */ jsx14(Crumbs, { breadcrumbs: state.breadcrumbs }),
2337
+ state.flash && /* @__PURE__ */ jsx14(FlashMessage, { message: state.flash.message, level: state.flash.level })
2338
+ ] });
2339
+ }
2340
+
2341
+ // src/tui/index.tsx
2342
+ import { jsx as jsx15 } from "react/jsx-runtime";
2343
+ async function launchTUI(container, initialProject) {
2344
+ const instance = render(/* @__PURE__ */ jsx15(App, { container, initialProject }), {
2345
+ exitOnCtrlC: true
2346
+ });
2347
+ await instance.waitUntilExit();
2348
+ }
2349
+ export {
2350
+ launchTUI
2351
+ };
2352
+ //# sourceMappingURL=tui-JNZRBEIQ.js.map