backlog.md 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.
Files changed (162) hide show
  1. package/.backlog/archive/drafts/readme.md +3 -0
  2. package/.backlog/archive/drafts/task-41 - temporary-test-task.md +13 -0
  3. package/.backlog/archive/readme.md +6 -0
  4. package/.backlog/archive/tasks/readme.md +3 -0
  5. package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +14 -0
  6. package/.backlog/config.yml +7 -0
  7. package/.backlog/decisions/readme.md +7 -0
  8. package/.backlog/docs/readme.md +20 -0
  9. package/.backlog/drafts/readme.md +3 -0
  10. package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +21 -0
  11. package/.backlog/drafts/task-28 - add-code-of-conduct.md +20 -0
  12. package/.backlog/drafts/task-30 - create-changelog.md +19 -0
  13. package/.backlog/milestones/m-0 - project-setup.md +8 -0
  14. package/.backlog/milestones/m-1 - cli.md +8 -0
  15. package/.backlog/milestones/m-2 - cli-kanban.md +8 -0
  16. package/.backlog/milestones/m-3 - gui.md +8 -0
  17. package/.backlog/milestones/m-4 - gui-kanban.md +8 -0
  18. package/.backlog/milestones/m-5 - gui-advanced.md +12 -0
  19. package/.backlog/milestones/readme.md +3 -0
  20. package/.backlog/readme.md +5 -0
  21. package/.backlog/tasks/readme.md +37 -0
  22. package/.backlog/tasks/task-1 - cli-setup-core-project.md +23 -0
  23. package/.backlog/tasks/task-10 - gui-init-packaging.md +23 -0
  24. package/.backlog/tasks/task-11 - gui-kanban-board.md +26 -0
  25. package/.backlog/tasks/task-12 - gui-advanced.md +25 -0
  26. package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +53 -0
  27. package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +40 -0
  28. package/.backlog/tasks/task-14 - gui-introduction-screens.md +21 -0
  29. package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +20 -0
  30. package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +20 -0
  31. package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +20 -0
  32. package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +20 -0
  33. package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +55 -0
  34. package/.backlog/tasks/task-2 - cli-core-logic-library.md +28 -0
  35. package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +32 -0
  36. package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +31 -0
  37. package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +24 -0
  38. package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +30 -0
  39. package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +38 -0
  40. package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +19 -0
  41. package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +28 -0
  42. package/.backlog/tasks/task-27 - add-contributing-guidelines.md +27 -0
  43. package/.backlog/tasks/task-29 - add-github-templates.md +28 -0
  44. package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +63 -0
  45. package/.backlog/tasks/task-31 - update-readme-for-open-source.md +26 -0
  46. package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +31 -0
  47. package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +20 -0
  48. package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +26 -0
  49. package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +24 -0
  50. package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +24 -0
  51. package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +19 -0
  52. package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +25 -0
  53. package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +31 -0
  54. package/.backlog/tasks/task-4 - cli-task-management-commands.md +28 -0
  55. package/.backlog/tasks/task-4.1 - cli-task-create.md +27 -0
  56. package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +51 -0
  57. package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +23 -0
  58. package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +53 -0
  59. package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +58 -0
  60. package/.backlog/tasks/task-4.2 - cli-task-list-view.md +25 -0
  61. package/.backlog/tasks/task-4.3 - cli-task-edit.md +24 -0
  62. package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +27 -0
  63. package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +28 -0
  64. package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +35 -0
  65. package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +40 -0
  66. package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +48 -0
  67. package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +66 -0
  68. package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +38 -0
  69. package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +93 -0
  70. package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +42 -0
  71. package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +44 -0
  72. package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +45 -0
  73. package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +49 -0
  74. package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +55 -0
  75. package/.backlog/tasks/task-42 - visual-hierarchy.md +54 -0
  76. package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +56 -0
  77. package/.backlog/tasks/task-44 - checklist-alignment.md +24 -0
  78. package/.backlog/tasks/task-45 - safe-line-wrapping.md +23 -0
  79. package/.backlog/tasks/task-46 - split-pane-layout.md +24 -0
  80. package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +43 -0
  81. package/.backlog/tasks/task-48 - footer-hint-line.md +21 -0
  82. package/.backlog/tasks/task-49 - status-styling.md +53 -0
  83. package/.backlog/tasks/task-5 - cli-docs-decisions.md +57 -0
  84. package/.backlog/tasks/task-50 - borders-&-padding.md +22 -0
  85. package/.backlog/tasks/task-51 - code-path-styling.md +23 -0
  86. package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +29 -0
  87. package/.backlog/tasks/task-6 - cli-packaging.md +65 -0
  88. package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +49 -0
  89. package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +64 -0
  90. package/.backlog/tasks/task-7 - cli-kanban-view.md +60 -0
  91. package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +62 -0
  92. package/.backlog/tasks/task-8 - gui-project-setup.md +21 -0
  93. package/.backlog/tasks/task-9 - gui-task-crud.md +24 -0
  94. package/.cursorrules +223 -0
  95. package/.gitattributes +2 -0
  96. package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
  97. package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
  98. package/.github/PULL_REQUEST_TEMPLATE.md +8 -0
  99. package/.github/workflows/ci.yml +36 -0
  100. package/.husky/pre-commit +1 -0
  101. package/AGENTS.md +65 -0
  102. package/CLAUDE.md +87 -0
  103. package/CONTRIBUTING.md +19 -0
  104. package/DEVELOPMENT.md +37 -0
  105. package/LICENSE +21 -0
  106. package/biome.json +31 -0
  107. package/bun.lock +152 -0
  108. package/cli/.cursorrules-xh86jabm.md +82 -0
  109. package/cli/AGENTS-xh86jabm.md +82 -0
  110. package/cli/CLAUDE-xh86jabm.md +82 -0
  111. package/cli/backlog +0 -0
  112. package/cli/cli.js +19622 -0
  113. package/cli/index.js +2 -0
  114. package/docs/npm-publishing.md +69 -0
  115. package/package.json +47 -0
  116. package/readme.md +97 -0
  117. package/scripts/build.js +73 -0
  118. package/src/agent-instructions.ts +54 -0
  119. package/src/board.ts +263 -0
  120. package/src/cli.ts +806 -0
  121. package/src/constants/index.ts +48 -0
  122. package/src/core/backlog.ts +183 -0
  123. package/src/core/remote-tasks.ts +168 -0
  124. package/src/file-system/operations.ts +515 -0
  125. package/src/git/operations.ts +189 -0
  126. package/src/guidelines/.cursorrules.md +82 -0
  127. package/src/guidelines/AGENTS.md +82 -0
  128. package/src/guidelines/CLAUDE.md +82 -0
  129. package/src/guidelines/index.ts +7 -0
  130. package/src/index.ts +30 -0
  131. package/src/markdown/parser.ts +145 -0
  132. package/src/markdown/serializer.ts +71 -0
  133. package/src/test/agent-instructions.test.ts +62 -0
  134. package/src/test/board.test.ts +291 -0
  135. package/src/test/build.test.ts +28 -0
  136. package/src/test/checklist.test.ts +273 -0
  137. package/src/test/cli.test.ts +1300 -0
  138. package/src/test/code-path.test.ts +204 -0
  139. package/src/test/core.test.ts +330 -0
  140. package/src/test/filesystem.test.ts +435 -0
  141. package/src/test/git.test.ts +26 -0
  142. package/src/test/heading.test.ts +102 -0
  143. package/src/test/line-wrapping.test.ts +252 -0
  144. package/src/test/local-install.test.ts +34 -0
  145. package/src/test/markdown.test.ts +526 -0
  146. package/src/test/parallel-loading.test.ts +160 -0
  147. package/src/test/parent-id-normalization.test.ts +48 -0
  148. package/src/test/remote-id-conflict.test.ts +60 -0
  149. package/src/test/status-icon.test.ts +93 -0
  150. package/src/types/blessed.d.ts +14 -0
  151. package/src/types/index.ts +55 -0
  152. package/src/types/raw.d.ts +4 -0
  153. package/src/ui/board.ts +322 -0
  154. package/src/ui/checklist.ts +103 -0
  155. package/src/ui/code-path.ts +113 -0
  156. package/src/ui/heading.ts +121 -0
  157. package/src/ui/loading.ts +216 -0
  158. package/src/ui/status-icon.ts +53 -0
  159. package/src/ui/task-list.ts +168 -0
  160. package/src/ui/task-viewer.ts +640 -0
  161. package/src/ui/tui.ts +301 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,640 @@
1
+ /* Enhanced task viewer for displaying task details in a structured format */
2
+
3
+ import { createRequire } from "node:module";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { Core } from "../core/backlog.ts";
6
+ import { parseMarkdown } from "../markdown/parser.ts";
7
+ import type { Task } from "../types/index.ts";
8
+ import { formatChecklistItem, parseCheckboxLine } from "./checklist.ts";
9
+ import { transformCodePaths, transformCodePathsPlain } from "./code-path.ts";
10
+ import { formatHeading } from "./heading.ts";
11
+ import { formatStatusWithIcon, getStatusColor, getStatusIcon } from "./status-icon.ts";
12
+ import { TaskList } from "./task-list.ts";
13
+
14
+ /**
15
+ * Extract only the Description section content from markdown, avoiding duplication
16
+ */
17
+ function extractDescriptionSection(content: string): string | null {
18
+ if (!content) return null;
19
+
20
+ // Look for ## Description section
21
+ const regex = /## Description\s*\n([\s\S]*?)(?=\n## |$)/i;
22
+ const match = content.match(regex);
23
+ return match?.[1]?.trim() || null;
24
+ }
25
+
26
+ /**
27
+ * Extract checkbox lines from Acceptance Criteria section for display
28
+ */
29
+ function extractAcceptanceCriteriaWithCheckboxes(content: string): string[] {
30
+ if (!content) return [];
31
+
32
+ // Look for ## Acceptance Criteria section
33
+ const regex = /## Acceptance Criteria\s*\n([\s\S]*?)(?=\n## |$)/i;
34
+ const match = content.match(regex);
35
+ if (!match) return [];
36
+
37
+ return match[1]
38
+ .split("\n")
39
+ .map((line) => line.trim())
40
+ .filter((line) => line.startsWith("- [ ]") || line.startsWith("- [x]"));
41
+ }
42
+
43
+ // Load blessed dynamically
44
+ // biome-ignore lint/suspicious/noExplicitAny: blessed is dynamically loaded
45
+ async function loadBlessed(): Promise<any | null> {
46
+ if (output.isTTY === false) return null;
47
+ try {
48
+ const require = createRequire(import.meta.url);
49
+ const blessed = require("blessed");
50
+ return blessed;
51
+ } catch {
52
+ try {
53
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic import
54
+ const mod = (await import("blessed")) as any;
55
+ return mod.default ?? mod;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Display task details in a split-pane UI with task list on left and detail on right
64
+ */
65
+ export async function viewTaskEnhanced(task: Task, content: string): Promise<void> {
66
+ const blessed = await loadBlessed();
67
+ if (!blessed) {
68
+ // Fallback to formatted plain text
69
+ console.log(formatTaskPlainText(task, content));
70
+ return;
71
+ }
72
+
73
+ // Get project root and load all tasks
74
+ const cwd = process.cwd();
75
+ const core = new Core(cwd);
76
+ const allTasks = await core.filesystem.listTasks();
77
+
78
+ // Find the initial selected task index
79
+ const initialIndex = allTasks.findIndex((t) => t.id === task.id);
80
+ let currentSelectedTask = task;
81
+ let currentSelectedContent = content;
82
+
83
+ const screen = blessed.screen({
84
+ smartCSR: true,
85
+ title: "Backlog Tasks",
86
+ });
87
+
88
+ // Main container using grid layout
89
+ const container = blessed.box({
90
+ parent: screen,
91
+ width: "100%",
92
+ height: "100%",
93
+ autoPadding: true,
94
+ });
95
+
96
+ // Task list pane (left 40%)
97
+ const taskListPane = blessed.box({
98
+ parent: container,
99
+ top: 0,
100
+ left: 0,
101
+ width: "40%",
102
+ height: "100%-1", // Leave space for help bar
103
+ });
104
+
105
+ // Detail pane (right 60%) with border and padding
106
+ const detailPane = blessed.box({
107
+ parent: container,
108
+ top: 0,
109
+ left: "40%",
110
+ // left: taskListPane.width,
111
+ // left : "0%",
112
+ width: "60%",
113
+ height: "100%-1", // Leave space for help bar
114
+ // border: {
115
+ // type: "line",
116
+ // },
117
+ // padding: {
118
+ // left: 1,
119
+ // },
120
+ // style: {
121
+ // border: { fg: "gray" },
122
+ // },
123
+ });
124
+
125
+ // Create task list
126
+ const taskList = new TaskList({
127
+ parent: taskListPane,
128
+ tasks: allTasks,
129
+ selectedIndex: Math.max(0, initialIndex),
130
+ onSelect: (selectedTask: Task, index: number) => {
131
+ currentSelectedTask = selectedTask;
132
+ // Load the content for the selected task asynchronously
133
+ (async () => {
134
+ try {
135
+ const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
136
+ const normalizedId = selectedTask.id.startsWith("task-") ? selectedTask.id : `task-${selectedTask.id}`;
137
+ const taskFile = files.find((f) => f.startsWith(`${normalizedId} -`));
138
+
139
+ if (taskFile) {
140
+ const filePath = `${core.filesystem.tasksDir}/${taskFile}`;
141
+ currentSelectedContent = await Bun.file(filePath).text();
142
+ } else {
143
+ currentSelectedContent = "";
144
+ }
145
+ } catch (error) {
146
+ currentSelectedContent = "";
147
+ }
148
+
149
+ // Refresh the detail pane
150
+ refreshDetailPane();
151
+ })();
152
+ },
153
+ });
154
+
155
+ // Detail pane components
156
+ // biome-ignore lint/suspicious/noExplicitAny: blessed components don't have proper types
157
+ let headerBox: any;
158
+ // biome-ignore lint/suspicious/noExplicitAny: blessed components don't have proper types
159
+ let metadataBox: any;
160
+ // biome-ignore lint/suspicious/noExplicitAny: blessed components don't have proper types
161
+ let descriptionBox: any;
162
+ // biome-ignore lint/suspicious/noExplicitAny: blessed components don't have proper types
163
+ let bottomBox: any;
164
+
165
+ function refreshDetailPane() {
166
+ // Clear existing detail pane content
167
+ if (headerBox) headerBox.destroy();
168
+ if (metadataBox) metadataBox.destroy();
169
+ if (descriptionBox) descriptionBox.destroy();
170
+ if (bottomBox) bottomBox.destroy();
171
+
172
+ // Update screen title
173
+ screen.title = `Task ${currentSelectedTask.id} - ${currentSelectedTask.title}`;
174
+
175
+ // Fixed header section with task ID, title, status, date, and tags
176
+ headerBox = blessed.box({
177
+ parent: detailPane,
178
+ top: 0,
179
+ left: 0,
180
+ width: "100%-2", // Account for border
181
+ height: 3,
182
+ border: "line",
183
+ style: {
184
+ border: { fg: "blue" },
185
+ },
186
+ tags: true,
187
+ wrap: true,
188
+ scrollable: false, // Header should never scroll
189
+ });
190
+
191
+ // Format header content with key metadata
192
+ const headerContent = [];
193
+ headerContent.push(` {bold}{blue-fg}${currentSelectedTask.id}{/blue-fg}{/bold} - ${currentSelectedTask.title}`);
194
+
195
+ // Second line with status, date, and assignee
196
+ const statusLine = [];
197
+ statusLine.push(
198
+ `{${getStatusColor(currentSelectedTask.status)}-fg}${formatStatusWithIcon(currentSelectedTask.status)}{/}`,
199
+ );
200
+ statusLine.push(`{gray-fg}${currentSelectedTask.createdDate}{/}`);
201
+
202
+ if (currentSelectedTask.assignee?.length) {
203
+ const assigneeList = currentSelectedTask.assignee.map((a) => (a.startsWith("@") ? a : `@${a}`)).join(", ");
204
+ statusLine.push(`{cyan-fg}${assigneeList}{/}`);
205
+ }
206
+
207
+ // Add labels to header if they exist
208
+ if (currentSelectedTask.labels?.length) {
209
+ statusLine.push(`${currentSelectedTask.labels.map((l) => `{yellow-fg}[${l}]{/}`).join(" ")}`);
210
+ }
211
+
212
+ headerContent.push(` ${statusLine.join(" • ")}`);
213
+
214
+ headerBox.setContent(headerContent.join("\n"));
215
+
216
+ // Scrollable body container beneath the header
217
+ const bodyContainer = blessed.box({
218
+ parent: detailPane,
219
+ top: 3, // Start below the fixed header
220
+ left: 0,
221
+ width: "100%-2", // Account for border
222
+ height: "100%-4", // Fill remaining space below header
223
+ scrollable: true,
224
+ alwaysScroll: true,
225
+ keys: true,
226
+ mouse: true,
227
+ tags: true,
228
+ wrap: true,
229
+ padding: { left: 1, right: 1, top: 1 },
230
+ });
231
+
232
+ // Build the scrollable body content
233
+ const bodyContent = [];
234
+
235
+ // Add additional metadata section
236
+ if (
237
+ currentSelectedTask.reporter ||
238
+ currentSelectedTask.updatedDate ||
239
+ currentSelectedTask.milestone ||
240
+ currentSelectedTask.parentTaskId ||
241
+ currentSelectedTask.subtasks?.length ||
242
+ currentSelectedTask.dependencies?.length
243
+ ) {
244
+ bodyContent.push(formatHeading("Details", 2));
245
+
246
+ const metadata = [];
247
+ if (currentSelectedTask.reporter) {
248
+ const reporterText = currentSelectedTask.reporter.startsWith("@")
249
+ ? currentSelectedTask.reporter
250
+ : `@${currentSelectedTask.reporter}`;
251
+ metadata.push(`{bold}Reporter:{/bold} {cyan-fg}${reporterText}{/}`);
252
+ }
253
+
254
+ if (currentSelectedTask.updatedDate) {
255
+ metadata.push(`{bold}Updated:{/bold} ${currentSelectedTask.updatedDate}`);
256
+ }
257
+
258
+ if (currentSelectedTask.milestone) {
259
+ metadata.push(`{bold}Milestone:{/bold} {magenta-fg}${currentSelectedTask.milestone}{/}`);
260
+ }
261
+
262
+ if (currentSelectedTask.parentTaskId) {
263
+ metadata.push(`{bold}Parent:{/bold} {blue-fg}${currentSelectedTask.parentTaskId}{/}`);
264
+ }
265
+
266
+ if (currentSelectedTask.subtasks?.length) {
267
+ metadata.push(
268
+ `{bold}Subtasks:{/bold} ${currentSelectedTask.subtasks.length} task${currentSelectedTask.subtasks.length > 1 ? "s" : ""}`,
269
+ );
270
+ }
271
+
272
+ if (currentSelectedTask.dependencies?.length) {
273
+ metadata.push(`{bold}Dependencies:{/bold} ${currentSelectedTask.dependencies.join(", ")}`);
274
+ }
275
+
276
+ bodyContent.push(metadata.join("\n"));
277
+ bodyContent.push("");
278
+ }
279
+
280
+ // Description section
281
+ bodyContent.push(formatHeading("Description", 2));
282
+ // Extract only the Description section content, not the full markdown
283
+ const extractedDescription = extractDescriptionSection(currentSelectedTask.description);
284
+ const descriptionContent = extractedDescription
285
+ ? transformCodePaths(extractedDescription)
286
+ : "{gray-fg}No description provided{/}";
287
+ bodyContent.push(descriptionContent);
288
+ bodyContent.push("");
289
+
290
+ // Acceptance criteria section
291
+ bodyContent.push(formatHeading("Acceptance Criteria", 2));
292
+ // Extract checkbox lines from raw content to preserve checkbox state
293
+ const checkboxLines = extractAcceptanceCriteriaWithCheckboxes(currentSelectedContent);
294
+ if (checkboxLines.length > 0) {
295
+ const formattedCriteria = checkboxLines.map((line) => {
296
+ const checkboxItem = parseCheckboxLine(line);
297
+ if (checkboxItem) {
298
+ // Use nice Unicode symbols for checkboxes in TUI
299
+ return formatChecklistItem(checkboxItem, {
300
+ padding: " ",
301
+ checkedSymbol: "{green-fg}✓{/}",
302
+ uncheckedSymbol: "{gray-fg}○{/}",
303
+ });
304
+ }
305
+ // Handle non-checkbox lines
306
+ return ` ${line}`;
307
+ });
308
+ const criteriaContent = styleCodePaths(formattedCriteria.join("\n"));
309
+ bodyContent.push(criteriaContent);
310
+ } else if (currentSelectedTask.acceptanceCriteria?.length) {
311
+ // Fallback to parsed criteria if no checkboxes found in raw content
312
+ const criteriaContent = styleCodePaths(
313
+ currentSelectedTask.acceptanceCriteria.map((text) => ` • ${text}`).join("\n"),
314
+ );
315
+ bodyContent.push(criteriaContent);
316
+ } else {
317
+ bodyContent.push("{gray-fg}No acceptance criteria defined{/}");
318
+ }
319
+
320
+ // Set the complete body content
321
+ bodyContainer.setContent(bodyContent.join("\n"));
322
+
323
+ // Store reference to body container for focus management
324
+ descriptionBox = bodyContainer;
325
+
326
+ screen.render();
327
+ }
328
+
329
+ await taskList.create({
330
+ parent: taskListPane,
331
+ tasks: allTasks,
332
+ selectedIndex: Math.max(0, initialIndex),
333
+ width: "100%",
334
+ height: "100%",
335
+ });
336
+
337
+ // Initial render of detail pane
338
+ refreshDetailPane();
339
+
340
+ return new Promise<void>((resolve) => {
341
+ // Footer hint line
342
+ const helpBar = blessed.box({
343
+ parent: screen,
344
+ bottom: 0,
345
+ left: 0,
346
+ width: "100%",
347
+ height: 1,
348
+ border: "line",
349
+ content: " ↑/↓ navigate · Tab switch pane · ←/→ scroll · q/Esc quit ",
350
+ style: {
351
+ fg: "gray",
352
+ border: { fg: "gray" },
353
+ },
354
+ });
355
+
356
+ // Focus management
357
+ const focusableElements = [taskList.getListBox(), descriptionBox];
358
+ let focusIndex = 0; // Start with task list
359
+ focusableElements[focusIndex].focus();
360
+
361
+ // Tab navigation between panes
362
+ screen.key(["tab"], () => {
363
+ focusIndex = (focusIndex + 1) % focusableElements.length;
364
+ focusableElements[focusIndex].focus();
365
+ screen.render();
366
+ });
367
+
368
+ screen.key(["S-tab"], () => {
369
+ focusIndex = (focusIndex - 1 + focusableElements.length) % focusableElements.length;
370
+ focusableElements[focusIndex].focus();
371
+ screen.render();
372
+ });
373
+
374
+ // Exit keys
375
+ screen.key(["escape", "q", "C-c"], () => {
376
+ screen.destroy();
377
+ resolve();
378
+ });
379
+
380
+ screen.render();
381
+ });
382
+ }
383
+
384
+ /**
385
+ * Generate enhanced detail content structure (reusable)
386
+ */
387
+ function generateDetailContent(task: Task, rawContent = ""): { headerContent: string[]; bodyContent: string[] } {
388
+ // Format header content with key metadata
389
+ const headerContent = [];
390
+ headerContent.push(` {bold}{blue-fg}${task.id}{/blue-fg}{/bold} - ${task.title}`);
391
+
392
+ // Second line with status, date, and assignee
393
+ const statusLine = [];
394
+ statusLine.push(`{${getStatusColor(task.status)}-fg}${formatStatusWithIcon(task.status)}{/}`);
395
+ statusLine.push(`{gray-fg}${task.createdDate}{/}`);
396
+
397
+ if (task.assignee?.length) {
398
+ const assigneeList = task.assignee.map((a) => (a.startsWith("@") ? a : `@${a}`)).join(", ");
399
+ statusLine.push(`{cyan-fg}${assigneeList}{/}`);
400
+ }
401
+
402
+ // Add labels to header if they exist
403
+ if (task.labels?.length) {
404
+ statusLine.push(`${task.labels.map((l) => `{yellow-fg}[${l}]{/}`).join(" ")}`);
405
+ }
406
+
407
+ headerContent.push(` ${statusLine.join(" • ")}`);
408
+
409
+ // Build the scrollable body content
410
+ const bodyContent = [];
411
+
412
+ // Add additional metadata section
413
+ if (
414
+ task.reporter ||
415
+ task.updatedDate ||
416
+ task.milestone ||
417
+ task.parentTaskId ||
418
+ task.subtasks?.length ||
419
+ task.dependencies?.length
420
+ ) {
421
+ bodyContent.push(formatHeading("Details", 2));
422
+
423
+ const metadata = [];
424
+ if (task.reporter) {
425
+ const reporterText = task.reporter.startsWith("@") ? task.reporter : `@${task.reporter}`;
426
+ metadata.push(`{bold}Reporter:{/bold} {cyan-fg}${reporterText}{/}`);
427
+ }
428
+
429
+ if (task.updatedDate) {
430
+ metadata.push(`{bold}Updated:{/bold} ${task.updatedDate}`);
431
+ }
432
+
433
+ if (task.milestone) {
434
+ metadata.push(`{bold}Milestone:{/bold} {magenta-fg}${task.milestone}{/}`);
435
+ }
436
+
437
+ if (task.parentTaskId) {
438
+ metadata.push(`{bold}Parent:{/bold} {blue-fg}${task.parentTaskId}{/}`);
439
+ }
440
+
441
+ if (task.subtasks?.length) {
442
+ metadata.push(`{bold}Subtasks:{/bold} ${task.subtasks.length} task${task.subtasks.length > 1 ? "s" : ""}`);
443
+ }
444
+
445
+ if (task.dependencies?.length) {
446
+ metadata.push(`{bold}Dependencies:{/bold} ${task.dependencies.join(", ")}`);
447
+ }
448
+
449
+ bodyContent.push(metadata.join("\n"));
450
+ bodyContent.push("");
451
+ }
452
+
453
+ // Description section
454
+ bodyContent.push(formatHeading("Description", 2));
455
+ // Extract only the Description section content, not the full markdown
456
+ const extractedDescription = extractDescriptionSection(task.description);
457
+ const descriptionContent = extractedDescription
458
+ ? transformCodePaths(extractedDescription)
459
+ : "{gray-fg}No description provided{/}";
460
+ bodyContent.push(descriptionContent);
461
+ bodyContent.push("");
462
+
463
+ // Acceptance criteria section
464
+ bodyContent.push(formatHeading("Acceptance Criteria", 2));
465
+ // Extract checkbox lines from raw content to preserve checkbox state
466
+ const checkboxLines = extractAcceptanceCriteriaWithCheckboxes(rawContent);
467
+ if (checkboxLines.length > 0) {
468
+ const formattedCriteria = checkboxLines.map((line) => {
469
+ const checkboxItem = parseCheckboxLine(line);
470
+ if (checkboxItem) {
471
+ // Use nice Unicode symbols for checkboxes in TUI
472
+ return formatChecklistItem(checkboxItem, {
473
+ padding: " ",
474
+ checkedSymbol: "{green-fg}✓{/}",
475
+ uncheckedSymbol: "{gray-fg}○{/}",
476
+ });
477
+ }
478
+ // Handle non-checkbox lines
479
+ return ` ${line}`;
480
+ });
481
+ const criteriaContent = styleCodePaths(formattedCriteria.join("\n"));
482
+ bodyContent.push(criteriaContent);
483
+ } else if (task.acceptanceCriteria?.length) {
484
+ // Fallback to parsed criteria if no checkboxes found in raw content
485
+ const criteriaContent = styleCodePaths(task.acceptanceCriteria.map((text) => ` • ${text}`).join("\n"));
486
+ bodyContent.push(criteriaContent);
487
+ } else {
488
+ bodyContent.push("{gray-fg}No acceptance criteria defined{/}");
489
+ }
490
+
491
+ return { headerContent, bodyContent };
492
+ }
493
+
494
+ /**
495
+ * Display task details in a popup (for board view) using enhanced detail structure
496
+ */
497
+ // biome-ignore lint/suspicious/noExplicitAny: blessed types
498
+ export async function createTaskPopup(screen: any, task: Task, content: string): Promise<any> {
499
+ const blessed = await loadBlessed();
500
+ if (!blessed) return null;
501
+
502
+ // Create main popup first
503
+ const popup = blessed.box({
504
+ parent: screen,
505
+ top: "center",
506
+ left: "center",
507
+ width: "85%",
508
+ height: "80%",
509
+ border: "line",
510
+ style: {
511
+ border: { fg: "gray" },
512
+ },
513
+ keys: true,
514
+ tags: true,
515
+ autoPadding: true,
516
+ });
517
+
518
+ // Create background overlay positioned relative to popup
519
+ // Using offset positioning: -2 chars left/right, -1 char top/bottom
520
+ const background = blessed.box({
521
+ parent: screen,
522
+ top: popup.top - 1,
523
+ left: popup.left - 2,
524
+ width: popup.width + 4,
525
+ height: popup.height + 2,
526
+ style: {
527
+ bg: "black",
528
+ },
529
+ });
530
+
531
+ // Move popup to front
532
+ popup.setFront();
533
+
534
+ // Generate enhanced detail content
535
+ const { headerContent, bodyContent } = generateDetailContent(task, content);
536
+
537
+ // Fixed header section with task ID, title, status, date, and tags
538
+ const headerBox = blessed.box({
539
+ parent: popup,
540
+ top: 0,
541
+ left: 0,
542
+ width: "100%-2", // Account for border
543
+ height: 3,
544
+ border: "line",
545
+ style: {
546
+ border: { fg: "blue" },
547
+ },
548
+ tags: true,
549
+ wrap: true,
550
+ scrollable: false, // Header should never scroll
551
+ content: headerContent.join("\n"),
552
+ });
553
+
554
+ // Escape indicator
555
+ const escIndicator = blessed.box({
556
+ parent: popup,
557
+ content: " Esc ",
558
+ top: -1,
559
+ right: 1,
560
+ width: 5,
561
+ height: 1,
562
+ style: {
563
+ fg: "white",
564
+ bg: "blue",
565
+ },
566
+ });
567
+
568
+ // Scrollable body container beneath the header
569
+ const contentArea = blessed.box({
570
+ parent: popup,
571
+ top: 3, // Start below the fixed header
572
+ left: 0,
573
+ width: "100%-2", // Account for border
574
+ height: "100%-5", // Leave more space for bottom border
575
+ scrollable: true,
576
+ alwaysScroll: false,
577
+ keys: true,
578
+ mouse: true,
579
+ tags: true,
580
+ wrap: true,
581
+ padding: { left: 1, right: 1, top: 1 },
582
+ content: bodyContent.join("\n"),
583
+ });
584
+
585
+ // Set up close handler
586
+ const closePopup = () => {
587
+ background.destroy();
588
+ popup.destroy();
589
+ screen.render();
590
+ };
591
+
592
+ // Focus content area for scrolling
593
+ contentArea.focus();
594
+
595
+ return {
596
+ background,
597
+ popup,
598
+ contentArea,
599
+ close: closePopup,
600
+ };
601
+ }
602
+
603
+ function formatTaskPlainText(task: Task, content: string): string {
604
+ const lines = [];
605
+ lines.push(`Task ${task.id} - ${task.title}`);
606
+ lines.push("=".repeat(50));
607
+ lines.push("");
608
+ lines.push(`Status: ${formatStatusWithIcon(task.status)}`);
609
+ if (task.assignee?.length)
610
+ lines.push(`Assignee: ${task.assignee.map((a) => (a.startsWith("@") ? a : `@${a}`)).join(", ")}`);
611
+ if (task.reporter) lines.push(`Reporter: ${task.reporter.startsWith("@") ? task.reporter : `@${task.reporter}`}`);
612
+ lines.push(`Created: ${task.createdDate}`);
613
+ if (task.updatedDate) lines.push(`Updated: ${task.updatedDate}`);
614
+ if (task.labels?.length) lines.push(`Labels: ${task.labels.join(", ")}`);
615
+ if (task.milestone) lines.push(`Milestone: ${task.milestone}`);
616
+ if (task.parentTaskId) lines.push(`Parent: ${task.parentTaskId}`);
617
+ if (task.subtasks?.length) lines.push(`Subtasks: ${task.subtasks.length}`);
618
+ if (task.dependencies?.length) lines.push(`Dependencies: ${task.dependencies.join(", ")}`);
619
+ lines.push("");
620
+ lines.push("Description:");
621
+ lines.push("-".repeat(50));
622
+ lines.push(transformCodePathsPlain(task.description || "No description provided"));
623
+ lines.push("");
624
+ if (task.acceptanceCriteria?.length) {
625
+ lines.push("Acceptance Criteria:");
626
+ lines.push("-".repeat(50));
627
+ for (const c of task.acceptanceCriteria) {
628
+ lines.push(transformCodePathsPlain(c));
629
+ }
630
+ lines.push("");
631
+ }
632
+ lines.push("Content:");
633
+ lines.push("-".repeat(50));
634
+ lines.push(transformCodePathsPlain(content));
635
+ return lines.join("\n");
636
+ }
637
+
638
+ function styleCodePaths(content: string): string {
639
+ return transformCodePaths(content);
640
+ }