schub 0.1.2 → 0.1.4

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 (80) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +12830 -3057
  3. package/package.json +5 -2
  4. package/skills/create-proposal/SKILL.md +5 -1
  5. package/skills/create-tasks/SKILL.md +5 -4
  6. package/skills/implement-task/SKILL.md +6 -1
  7. package/skills/review-proposal/SKILL.md +3 -2
  8. package/skills/update-roadmap/SKILL.md +23 -0
  9. package/src/changes.test.ts +166 -0
  10. package/src/changes.ts +159 -54
  11. package/src/commands/adr.test.ts +6 -5
  12. package/src/commands/changes.test.ts +136 -14
  13. package/src/commands/changes.ts +102 -1
  14. package/src/commands/cookbook.test.ts +6 -5
  15. package/src/commands/init.test.ts +69 -2
  16. package/src/commands/init.ts +48 -5
  17. package/src/commands/review.test.ts +7 -6
  18. package/src/commands/review.ts +1 -1
  19. package/src/commands/roadmap.test.ts +84 -0
  20. package/src/commands/roadmap.ts +84 -0
  21. package/src/commands/tasks-create.test.ts +22 -22
  22. package/src/commands/tasks-implement.test.ts +253 -0
  23. package/src/commands/tasks-implement.ts +121 -0
  24. package/src/commands/tasks-list.test.ts +27 -27
  25. package/src/commands/tasks-update.test.ts +92 -0
  26. package/src/commands/tasks.ts +98 -1
  27. package/src/features/roadmap/index.ts +230 -0
  28. package/src/features/roadmap/roadmap.test.ts +77 -0
  29. package/src/features/tasks/constants.ts +1 -0
  30. package/src/features/tasks/create.ts +10 -8
  31. package/src/features/tasks/filesystem.test.ts +285 -18
  32. package/src/features/tasks/filesystem.ts +152 -39
  33. package/src/features/tasks/graph.ts +18 -3
  34. package/src/features/tasks/index.ts +10 -1
  35. package/src/features/tasks/worktree.ts +48 -0
  36. package/src/frontmatter.ts +115 -0
  37. package/src/index.test.ts +42 -6
  38. package/src/index.ts +226 -109
  39. package/src/opencode.test.ts +53 -0
  40. package/src/opencode.ts +74 -0
  41. package/src/tasks.ts +2 -0
  42. package/src/tui/App.test.tsx +418 -0
  43. package/src/tui/App.tsx +343 -0
  44. package/src/tui/components/PlanView.test.tsx +101 -0
  45. package/src/tui/components/PlanView.tsx +89 -0
  46. package/src/tui/components/PreviewPage.test.tsx +69 -0
  47. package/src/tui/components/PreviewPage.tsx +87 -0
  48. package/src/tui/components/ProposalDetailView.test.tsx +169 -0
  49. package/src/tui/components/ProposalDetailView.tsx +166 -0
  50. package/src/tui/components/RoadmapView.test.tsx +85 -0
  51. package/src/tui/components/RoadmapView.tsx +369 -0
  52. package/src/tui/components/StatusView.test.tsx +1351 -0
  53. package/src/tui/components/StatusView.tsx +519 -0
  54. package/src/tui/components/markdown-renderer.test.ts +46 -0
  55. package/src/tui/components/markdown-renderer.ts +89 -0
  56. package/src/tui/components/status-view-data.ts +322 -0
  57. package/src/tui/components/status-view-render.tsx +329 -0
  58. package/src/tui/index.ts +16 -0
  59. package/templates/create-proposal/adr-template.md +6 -4
  60. package/templates/create-proposal/cookbook-template.md +5 -3
  61. package/templates/create-proposal/proposal-template.md +8 -6
  62. package/templates/create-roadmap/roadmap.md +5 -0
  63. package/templates/create-tasks/task-template.md +9 -4
  64. package/templates/review-proposal/q&a-template.md +8 -3
  65. package/templates/review-proposal/review-me-template.md +6 -4
  66. package/templates/setup-project/project-overview-template.md +5 -0
  67. package/templates/setup-project/project-setup-template.md +5 -0
  68. package/templates/setup-project/project-wow-template.md +5 -0
  69. package/src/App.test.tsx +0 -93
  70. package/src/App.tsx +0 -155
  71. package/src/components/PlanView.test.tsx +0 -113
  72. package/src/components/PlanView.tsx +0 -160
  73. package/src/components/StatusView.test.tsx +0 -380
  74. package/src/components/StatusView.tsx +0 -367
  75. package/src/ide.ts +0 -7
  76. package/templates/templates-parity.test.ts +0 -45
  77. /package/src/{clipboard.ts → tui/clipboard.ts} +0 -0
  78. /package/src/{components → tui/components}/statusColor.ts +0 -0
  79. /package/src/{terminal.test.ts → tui/terminal.test.ts} +0 -0
  80. /package/src/{terminal.ts → tui/terminal.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import { normalize, sep } from "node:path";
2
+ import {
3
+ type ChangeInfo,
4
+ type ChangeOverviewInfo,
5
+ listChangeOverview,
6
+ listChanges,
7
+ updateChangeStatus,
8
+ } from "../../changes";
9
+ import {
10
+ listTasks,
11
+ loadTaskDependencies,
12
+ TASK_STATUSES,
13
+ type TaskInfo,
14
+ type TaskStatus,
15
+ trimTaskTitle,
16
+ } from "../../features/tasks";
17
+
18
+ type ChangeTaskCount = {
19
+ completed: number;
20
+ total: number;
21
+ };
22
+
23
+ type ChangeSortable = {
24
+ id: string;
25
+ title: string;
26
+ };
27
+
28
+ export type StatusGroup = {
29
+ label: string;
30
+ order: number;
31
+ items: ChangeOverviewInfo[];
32
+ };
33
+
34
+ export type TaskStatusGroup = {
35
+ label: string;
36
+ order: number;
37
+ items: TaskInfo[];
38
+ };
39
+
40
+ const TASK_STATUS_LABELS: Record<TaskStatus, string> = {
41
+ backlog: "Backlog",
42
+ ready: "Ready",
43
+ wip: "WIP",
44
+ blocked: "Blocked",
45
+ done: "Done",
46
+ archived: "Archived",
47
+ };
48
+
49
+ const ACTIVE_TASK_STATUSES = ["blocked", "wip", "ready", "backlog"] as const;
50
+ const CHANGE_TASK_STATUSES = [...ACTIVE_TASK_STATUSES, "done"] as const;
51
+ const READY_TO_IMPLEMENT_STATUSES = new Set(["backlog", "ready"]);
52
+ const AUTO_MARK_STATUSES = new Set(["accepted", "wip"]);
53
+
54
+ const compareText = (left: string, right: string): number =>
55
+ left.localeCompare(right, undefined, { sensitivity: "base" });
56
+
57
+ const taskSortTitle = (task: TaskInfo) => {
58
+ if (task.status === "blocked") {
59
+ const reason = task.blockedReason?.trim();
60
+ if (reason) {
61
+ return reason;
62
+ }
63
+ }
64
+
65
+ const title = trimTaskTitle(task.title);
66
+ return title || task.id;
67
+ };
68
+
69
+ const changeSortTitle = (change: ChangeSortable) => {
70
+ const title = formatChangeTitle(change.id, change.title);
71
+ return title || change.id;
72
+ };
73
+
74
+ const sortByTaskTitle = (left: TaskInfo, right: TaskInfo): number => {
75
+ const titleCompare = compareText(taskSortTitle(left), taskSortTitle(right));
76
+ if (titleCompare !== 0) {
77
+ return titleCompare;
78
+ }
79
+
80
+ return compareText(left.id, right.id);
81
+ };
82
+
83
+ export const formatChangeId = (value: string) => {
84
+ const match = value.match(/^([Cc]\d+)_/);
85
+ return match ? match[1].toUpperCase() : value;
86
+ };
87
+
88
+ export const formatChangeTitle = (changeId: string, title: string) => {
89
+ const trimmedTitle = title.trim();
90
+ if (trimmedTitle !== changeId) {
91
+ return trimmedTitle;
92
+ }
93
+
94
+ const match = changeId.match(/^([Cc]\d+)_(.+)$/);
95
+ if (!match) {
96
+ return trimmedTitle;
97
+ }
98
+
99
+ return match[2]
100
+ .split("-")
101
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
102
+ .join(" ");
103
+ };
104
+
105
+ const sortByChangeTitle = (left: ChangeSortable, right: ChangeSortable) => {
106
+ const titleCompare = compareText(changeSortTitle(left), changeSortTitle(right));
107
+ if (titleCompare !== 0) {
108
+ return titleCompare;
109
+ }
110
+ return compareText(left.id, right.id);
111
+ };
112
+
113
+ const groupChangesByStatus = (changes: ChangeOverviewInfo[]) => {
114
+ const grouped = new Map<string, StatusGroup>();
115
+ for (const change of changes) {
116
+ const key = `${change.statusOrder}-${change.statusLabel}`;
117
+ const current = grouped.get(key) ?? {
118
+ label: change.statusLabel,
119
+ order: change.statusOrder,
120
+ items: [],
121
+ };
122
+ current.items.push(change);
123
+ grouped.set(key, current);
124
+ }
125
+ const sorted = Array.from(grouped.values()).sort((left, right) => {
126
+ const orderCompare = left.order - right.order;
127
+ if (orderCompare !== 0) {
128
+ return orderCompare;
129
+ }
130
+ return compareText(left.label, right.label);
131
+ });
132
+ for (const group of sorted) {
133
+ group.items.sort(sortByChangeTitle);
134
+ }
135
+ return sorted;
136
+ };
137
+
138
+ export const buildShowAllGroups = (schubDir: string | null) => {
139
+ if (!schubDir) {
140
+ return [];
141
+ }
142
+ return groupChangesByStatus(listChangeOverview(schubDir));
143
+ };
144
+
145
+ const buildTaskStatusGroups = (tasks: TaskInfo[]): TaskStatusGroup[] => {
146
+ const groups = new Map<TaskStatus, TaskStatusGroup>();
147
+
148
+ TASK_STATUSES.forEach((status, index) => {
149
+ groups.set(status, {
150
+ label: TASK_STATUS_LABELS[status],
151
+ order: index,
152
+ items: [],
153
+ });
154
+ });
155
+
156
+ for (const task of tasks) {
157
+ const group = groups.get(task.status);
158
+ if (group) {
159
+ group.items.push(task);
160
+ }
161
+ }
162
+
163
+ for (const group of groups.values()) {
164
+ group.items.sort(sortByTaskTitle);
165
+ }
166
+
167
+ return TASK_STATUSES.map((status) => groups.get(status))
168
+ .filter((group): group is TaskStatusGroup => Boolean(group))
169
+ .filter((group) => group.items.length > 0);
170
+ };
171
+
172
+ export const buildShowAllTaskGroups = (schubDir: string | null) => {
173
+ if (!schubDir) {
174
+ return [];
175
+ }
176
+
177
+ return buildTaskStatusGroups(listTasks(schubDir, TASK_STATUSES));
178
+ };
179
+
180
+ export const isTaskItem = (item: ChangeInfo | TaskInfo): item is TaskInfo => {
181
+ const parts = normalize(item.path).split(sep);
182
+ return parts.includes("tasks");
183
+ };
184
+
185
+ const canAutoMarkChange = (status: string) => AUTO_MARK_STATUSES.has(status.trim().toLowerCase());
186
+
187
+ export const autoMarkChangesDone = (schubDir: string) => {
188
+ const allTasks = loadTaskDependencies(schubDir, CHANGE_TASK_STATUSES);
189
+ const changeTaskCounts = new Map<string, ChangeTaskCount>();
190
+
191
+ for (const task of allTasks) {
192
+ if (!task.changeId) {
193
+ continue;
194
+ }
195
+
196
+ const current = changeTaskCounts.get(task.changeId) ?? { completed: 0, total: 0 };
197
+ const completed = current.completed + (task.status === "done" ? 1 : 0);
198
+ changeTaskCounts.set(task.changeId, { completed, total: current.total + 1 });
199
+ }
200
+
201
+ if (changeTaskCounts.size === 0) {
202
+ return;
203
+ }
204
+
205
+ for (const change of listChanges(schubDir)) {
206
+ const counts = changeTaskCounts.get(change.id);
207
+ if (!counts || counts.total === 0 || counts.completed !== counts.total) {
208
+ continue;
209
+ }
210
+
211
+ if (canAutoMarkChange(change.status)) {
212
+ updateChangeStatus(schubDir, change.id, "Done");
213
+ }
214
+ }
215
+ };
216
+
217
+ export const buildStatusData = (schubDir: string | null) => {
218
+ if (!schubDir) {
219
+ return {
220
+ pendingReview: [],
221
+ pendingImplementation: [],
222
+ pendingImplementationNoTasks: [],
223
+ drafts: [],
224
+ readyToImplement: [],
225
+ blocked: [],
226
+ wip: [],
227
+ ready: [],
228
+ backlog: [],
229
+ pendingImplementationCounts: new Map<string, string>(),
230
+ };
231
+ }
232
+
233
+ const allTasks = loadTaskDependencies(schubDir, CHANGE_TASK_STATUSES);
234
+ const tasksById = new Map(allTasks.map((task) => [task.id, task]));
235
+
236
+ const blockedTasks: TaskInfo[] = [];
237
+ const wipTasks: TaskInfo[] = [];
238
+ const readyTasks: TaskInfo[] = [];
239
+ const backlogTasks: TaskInfo[] = [];
240
+
241
+ const activeChangeIds = new Set<string>();
242
+ const changeTaskCounts = new Map<string, ChangeTaskCount>();
243
+
244
+ for (const task of allTasks) {
245
+ if (task.status === "blocked") blockedTasks.push(task);
246
+ else if (task.status === "wip") wipTasks.push(task);
247
+ else if (task.status === "ready") readyTasks.push(task);
248
+ else if (task.status === "backlog") backlogTasks.push(task);
249
+
250
+ if (task.changeId && task.status !== "done") {
251
+ activeChangeIds.add(task.changeId);
252
+ }
253
+
254
+ if (task.changeId) {
255
+ const current = changeTaskCounts.get(task.changeId) ?? { completed: 0, total: 0 };
256
+ const completed = current.completed + (task.status === "done" ? 1 : 0);
257
+ changeTaskCounts.set(task.changeId, { completed, total: current.total + 1 });
258
+ }
259
+ }
260
+
261
+ const readyToImplementTasks = allTasks
262
+ .filter((task) => READY_TO_IMPLEMENT_STATUSES.has(task.status))
263
+ .filter((task) => task.dependsOn.every((dependencyId) => tasksById.get(dependencyId)?.status === "done"))
264
+ .sort(sortByTaskTitle);
265
+
266
+ const allChanges = listChanges(schubDir);
267
+ const pendingReviewChanges: ChangeInfo[] = [];
268
+ const pendingImplementationChanges: ChangeInfo[] = [];
269
+ const draftChanges: ChangeInfo[] = [];
270
+
271
+ for (const change of allChanges) {
272
+ const normalized = change.status.toLowerCase();
273
+ if (normalized.includes("review")) {
274
+ pendingReviewChanges.push(change);
275
+ continue;
276
+ }
277
+
278
+ const counts = changeTaskCounts.get(change.id);
279
+ const allTasksCompleted = Boolean(counts && counts.total > 0 && counts.completed === counts.total);
280
+
281
+ if (activeChangeIds.has(change.id)) {
282
+ pendingImplementationChanges.push(change);
283
+ continue;
284
+ }
285
+
286
+ if ((normalized.includes("implementing") || normalized.includes("accepted")) && !allTasksCompleted) {
287
+ pendingImplementationChanges.push(change);
288
+ continue;
289
+ }
290
+
291
+ if (normalized.includes("draft")) {
292
+ draftChanges.push(change);
293
+ }
294
+ }
295
+
296
+ const pendingImplementationWithTasks: ChangeInfo[] = [];
297
+ const pendingImplementationNoTasks: ChangeInfo[] = [];
298
+ const pendingImplementationCounts = new Map<string, string>();
299
+
300
+ for (const change of pendingImplementationChanges.sort(sortByChangeTitle)) {
301
+ const counts = changeTaskCounts.get(change.id);
302
+ if (counts && counts.total > 0) {
303
+ pendingImplementationWithTasks.push(change);
304
+ pendingImplementationCounts.set(change.id, `${counts.completed}/${counts.total}`);
305
+ } else {
306
+ pendingImplementationNoTasks.push(change);
307
+ }
308
+ }
309
+
310
+ return {
311
+ pendingReview: pendingReviewChanges.sort(sortByChangeTitle),
312
+ pendingImplementation: pendingImplementationWithTasks,
313
+ pendingImplementationNoTasks,
314
+ drafts: draftChanges.sort(sortByChangeTitle),
315
+ readyToImplement: readyToImplementTasks,
316
+ blocked: blockedTasks.sort(sortByTaskTitle),
317
+ wip: wipTasks.sort(sortByTaskTitle),
318
+ ready: readyTasks.sort(sortByTaskTitle),
319
+ backlog: backlogTasks.sort(sortByTaskTitle),
320
+ pendingImplementationCounts,
321
+ };
322
+ };
@@ -0,0 +1,329 @@
1
+ import { Spinner } from "@inkjs/ui";
2
+ import { Box, Text } from "ink";
3
+ import type { ChangeInfo } from "../../changes";
4
+ import { type TaskInfo, trimTaskTitle } from "../../features/tasks";
5
+ import {
6
+ formatChangeId,
7
+ formatChangeTitle,
8
+ isTaskItem,
9
+ type StatusGroup,
10
+ type TaskStatusGroup,
11
+ } from "./status-view-data";
12
+
13
+ export type ShowAllRow = {
14
+ kind: "show-all";
15
+ id: string;
16
+ title: string;
17
+ };
18
+
19
+ export type StatusItem = ChangeInfo | TaskInfo | ShowAllRow;
20
+
21
+ export const isShowAllRow = (item: StatusItem | null): item is ShowAllRow =>
22
+ Boolean(item && "kind" in item && item.kind === "show-all");
23
+
24
+ type MainViewProps = {
25
+ selection: number;
26
+ pendingReview: ChangeInfo[];
27
+ pendingImplementation: ChangeInfo[];
28
+ pendingImplementationNoTasks: ChangeInfo[];
29
+ drafts: ChangeInfo[];
30
+ pendingImplementationCounts: Map<string, string>;
31
+ readyToImplement: TaskInfo[];
32
+ blocked: TaskInfo[];
33
+ wip: TaskInfo[];
34
+ ready: TaskInfo[];
35
+ backlog: TaskInfo[];
36
+ showAllRow: ShowAllRow;
37
+ showAllTasksRow: ShowAllRow;
38
+ };
39
+
40
+ export const StatusMainView = ({
41
+ selection,
42
+ pendingReview,
43
+ pendingImplementation,
44
+ pendingImplementationNoTasks,
45
+ drafts,
46
+ pendingImplementationCounts,
47
+ readyToImplement,
48
+ blocked,
49
+ wip,
50
+ ready,
51
+ backlog,
52
+ showAllRow,
53
+ showAllTasksRow,
54
+ }: MainViewProps) => {
55
+ const renderRow = (item: StatusItem, index: number, detail?: string) => {
56
+ const selected = index === selection;
57
+
58
+ if (isShowAllRow(item)) {
59
+ return (
60
+ <Box key={item.id} marginLeft={1}>
61
+ <Text color={selected ? "blue" : "gray"}>{selected ? "›" : " "}</Text>
62
+ <Box marginLeft={1}>
63
+ <Text color="white" bold={selected}>
64
+ {item.title}
65
+ </Text>
66
+ </Box>
67
+ </Box>
68
+ );
69
+ }
70
+
71
+ const task = isTaskItem(item) ? item : null;
72
+ const title = task ? trimTaskTitle(task.title) : "";
73
+ const isBlockedTask = task?.status === "blocked";
74
+ const blockedReason = isBlockedTask ? task.blockedReason?.trim() : undefined;
75
+ const checklistIndicator =
76
+ task && task.status === "wip" && typeof task.checklistTotal === "number"
77
+ ? ` (${task.checklistRemaining ?? 0}/${task.checklistTotal})`
78
+ : "";
79
+ const changeTitle = task ? "" : formatChangeTitle(item.id, item.title);
80
+ const changeDetail = detail ? ` ${detail}` : "";
81
+ const taskTitle = task ? (isBlockedTask ? (blockedReason ?? "") : `${title}${checklistIndicator}`) : "";
82
+ const displayTitle = task ? taskTitle : `${changeTitle}${changeDetail}`.trim();
83
+ const displayId = task ? item.id : formatChangeId(item.id);
84
+ const showSpinner = task?.status === "wip";
85
+
86
+ return (
87
+ <Box key={item.id} marginLeft={1}>
88
+ <Text color={selected ? "blue" : "gray"}>{selected ? "›" : " "}</Text>
89
+ <Box marginLeft={1}>
90
+ <Text color="white" bold={selected}>
91
+ {displayId}
92
+ </Text>
93
+ {displayTitle ? <Text color="gray"> {displayTitle}</Text> : null}
94
+ {showSpinner ? (
95
+ <Box marginLeft={1}>
96
+ <Spinner />
97
+ </Box>
98
+ ) : null}
99
+ </Box>
100
+ </Box>
101
+ );
102
+ };
103
+
104
+ let currentIndex = 0;
105
+
106
+ const renderSection = (title: string, items: StatusItem[], detailById?: Map<string, string>, emptyState?: string) => {
107
+ if (items.length === 0) {
108
+ if (!emptyState) {
109
+ return null;
110
+ }
111
+
112
+ return (
113
+ <Box flexDirection="column" marginBottom={1}>
114
+ <Box marginBottom={0}>
115
+ <Text color="white">{title}</Text>
116
+ </Box>
117
+ <Box marginLeft={1}>
118
+ <Text color="gray">{emptyState}</Text>
119
+ </Box>
120
+ </Box>
121
+ );
122
+ }
123
+
124
+ const sectionStartIndex = currentIndex;
125
+ const sectionItems = items.map((item, i) => renderRow(item, sectionStartIndex + i, detailById?.get(item.id)));
126
+ currentIndex += items.length;
127
+
128
+ return (
129
+ <Box flexDirection="column" marginBottom={1}>
130
+ <Box marginBottom={0}>
131
+ <Text color="white">{title}</Text>
132
+ </Box>
133
+ {sectionItems}
134
+ </Box>
135
+ );
136
+ };
137
+
138
+ const renderShowAllRow = (row: ShowAllRow) => {
139
+ const rowIndex = currentIndex;
140
+ currentIndex += 1;
141
+
142
+ return (
143
+ <Box flexDirection="column" marginBottom={1}>
144
+ {renderRow(row, rowIndex)}
145
+ </Box>
146
+ );
147
+ };
148
+
149
+ return (
150
+ <Box flexDirection="column">
151
+ <Box flexDirection="column" marginBottom={1}>
152
+ <Box marginBottom={1}>
153
+ <Text bold color="white">
154
+ Change Proposals
155
+ </Text>
156
+ </Box>
157
+ {renderSection("Pending Review", pendingReview)}
158
+ {renderSection("Pending Implementation", pendingImplementation, pendingImplementationCounts)}
159
+ {renderSection("No Tasks Defined", pendingImplementationNoTasks)}
160
+ {renderSection("Drafts", drafts)}
161
+ {pendingReview.length === 0 &&
162
+ pendingImplementation.length === 0 &&
163
+ pendingImplementationNoTasks.length === 0 &&
164
+ drafts.length === 0 && (
165
+ <Box marginLeft={1}>
166
+ <Text color="gray">No active proposals.</Text>
167
+ </Box>
168
+ )}
169
+ {renderShowAllRow(showAllRow)}
170
+ </Box>
171
+
172
+ <Box flexDirection="column">
173
+ <Box marginBottom={1}>
174
+ <Text bold color="white">
175
+ Tasks
176
+ </Text>
177
+ </Box>
178
+ {renderSection("Ready to Implement", readyToImplement, undefined, "No tasks ready for implementation.")}
179
+ {renderSection("Blocked", blocked)}
180
+ {renderSection("WIP", wip)}
181
+ {renderSection("Ready", ready)}
182
+ {renderSection("Backlog", backlog)}
183
+ {blocked.length === 0 && wip.length === 0 && ready.length === 0 && backlog.length === 0 && (
184
+ <Box marginLeft={1}>
185
+ <Text color="gray">No active tasks.</Text>
186
+ </Box>
187
+ )}
188
+ {renderShowAllRow(showAllTasksRow)}
189
+ </Box>
190
+ </Box>
191
+ );
192
+ };
193
+
194
+ type ShowAllViewProps = {
195
+ selection: number;
196
+ showAllGroups: StatusGroup[];
197
+ };
198
+
199
+ export const StatusShowAllView = ({ selection, showAllGroups }: ShowAllViewProps) => {
200
+ const renderRow = (item: ChangeInfo, index: number) => {
201
+ const selected = index === selection;
202
+ const displayId = formatChangeId(item.id);
203
+ const displayTitle = formatChangeTitle(item.id, item.title);
204
+
205
+ return (
206
+ <Box key={item.id} marginLeft={1}>
207
+ <Text color={selected ? "blue" : "gray"}>{selected ? "›" : " "}</Text>
208
+ <Box marginLeft={1}>
209
+ <Text color="white" bold={selected}>
210
+ {displayId}
211
+ </Text>
212
+ {displayTitle ? <Text color="gray"> {displayTitle}</Text> : null}
213
+ </Box>
214
+ </Box>
215
+ );
216
+ };
217
+
218
+ let showAllIndex = 0;
219
+
220
+ const renderShowAllSection = (title: string, items: ChangeInfo[]) => {
221
+ if (items.length === 0) return null;
222
+
223
+ const sectionStartIndex = showAllIndex;
224
+ const sectionItems = items.map((item, i) => renderRow(item, sectionStartIndex + i));
225
+ showAllIndex += items.length;
226
+
227
+ return (
228
+ <Box key={title} flexDirection="column" marginBottom={1}>
229
+ <Box marginBottom={0}>
230
+ <Text color="white">{title}</Text>
231
+ </Box>
232
+ {sectionItems}
233
+ </Box>
234
+ );
235
+ };
236
+
237
+ return (
238
+ <Box flexDirection="column">
239
+ <Box marginBottom={1} flexDirection="row">
240
+ <Text bold color="white">
241
+ All Proposals
242
+ </Text>
243
+ <Text color="gray"> · esc to return</Text>
244
+ </Box>
245
+ {showAllGroups.length === 0 ? (
246
+ <Box marginLeft={1}>
247
+ <Text color="gray">No proposals found.</Text>
248
+ </Box>
249
+ ) : (
250
+ showAllGroups.map((group) => renderShowAllSection(group.label, group.items))
251
+ )}
252
+ </Box>
253
+ );
254
+ };
255
+
256
+ type ShowAllTasksViewProps = {
257
+ selection: number;
258
+ showAllTaskGroups: TaskStatusGroup[];
259
+ };
260
+
261
+ export const StatusShowAllTasksView = ({ selection, showAllTaskGroups }: ShowAllTasksViewProps) => {
262
+ const renderRow = (task: TaskInfo, index: number) => {
263
+ const selected = index === selection;
264
+ const title = trimTaskTitle(task.title);
265
+ const isBlockedTask = task.status === "blocked";
266
+ const blockedReason = isBlockedTask ? task.blockedReason?.trim() : undefined;
267
+ const checklistIndicator =
268
+ task.status === "wip" && typeof task.checklistTotal === "number"
269
+ ? ` (${task.checklistRemaining ?? 0}/${task.checklistTotal})`
270
+ : "";
271
+ const taskTitle = isBlockedTask ? (blockedReason ?? "") : `${title}${checklistIndicator}`;
272
+ const displayTitle = taskTitle.trim();
273
+ const showSpinner = task.status === "wip";
274
+
275
+ return (
276
+ <Box key={task.id} marginLeft={1}>
277
+ <Text color={selected ? "blue" : "gray"}>{selected ? "›" : " "}</Text>
278
+ <Box marginLeft={1}>
279
+ <Text color="white" bold={selected}>
280
+ {task.id}
281
+ </Text>
282
+ {displayTitle ? <Text color="gray"> {displayTitle}</Text> : null}
283
+ {showSpinner ? (
284
+ <Box marginLeft={1}>
285
+ <Spinner />
286
+ </Box>
287
+ ) : null}
288
+ </Box>
289
+ </Box>
290
+ );
291
+ };
292
+
293
+ let showAllIndex = 0;
294
+
295
+ const renderShowAllSection = (group: TaskStatusGroup) => {
296
+ if (group.items.length === 0) return null;
297
+
298
+ const sectionStartIndex = showAllIndex;
299
+ const sectionItems = group.items.map((item, i) => renderRow(item, sectionStartIndex + i));
300
+ showAllIndex += group.items.length;
301
+
302
+ return (
303
+ <Box key={group.label} flexDirection="column" marginBottom={1}>
304
+ <Box marginBottom={0}>
305
+ <Text color="white">{group.label}</Text>
306
+ </Box>
307
+ {sectionItems}
308
+ </Box>
309
+ );
310
+ };
311
+
312
+ return (
313
+ <Box flexDirection="column">
314
+ <Box marginBottom={1} flexDirection="row">
315
+ <Text bold color="white">
316
+ All Tasks
317
+ </Text>
318
+ <Text color="gray"> · esc to return</Text>
319
+ </Box>
320
+ {showAllTaskGroups.length === 0 ? (
321
+ <Box marginLeft={1}>
322
+ <Text color="gray">No tasks found.</Text>
323
+ </Box>
324
+ ) : (
325
+ showAllTaskGroups.map((group) => renderShowAllSection(group))
326
+ )}
327
+ </Box>
328
+ );
329
+ };
@@ -0,0 +1,16 @@
1
+ import { render } from "ink";
2
+ import React from "react";
3
+ import App from "./App";
4
+ import { applyTerminalPrelude, applyTerminalReset } from "./terminal";
5
+
6
+ const registerTerminalReset = () => {
7
+ process.once("exit", () => {
8
+ applyTerminalReset();
9
+ });
10
+ };
11
+
12
+ export const runTui = () => {
13
+ applyTerminalPrelude();
14
+ registerTerminalReset();
15
+ render(React.createElement(App));
16
+ };
@@ -1,8 +1,10 @@
1
- # ADR: [TITLE]
1
+ ---
2
+ status: "[Proposed | Accepted | Deprecated | Superseded]"
3
+ date: "[YYYY-MM-DD]"
4
+ related: "[PR link, spec link, plan link, issue link]"
5
+ ---
2
6
 
3
- **Status**: [Proposed | Accepted | Deprecated | Superseded]
4
- **Date**: [YYYY-MM-DD]
5
- **Related**: [PR link, spec link, plan link, issue link]
7
+ # ADR: [TITLE]
6
8
 
7
9
  > Use ADRs to capture the **WHY**, not the implementation details.
8
10
  > Keep it short and specific: context → decision → rationale → consequences.
@@ -1,7 +1,9 @@
1
- # Cookbook - {{CHANGE_TITLE}}
1
+ ---
2
+ change_id: "{{CHANGE_ID}}"
3
+ created: "{{DATE}}"
4
+ ---
2
5
 
3
- **Change ID**: `{{CHANGE_ID}}`
4
- **Created**: {{DATE}}
6
+ # Cookbook - {{CHANGE_TITLE}}
5
7
 
6
8
  ## Overview
7
9