gitforest 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 (256) hide show
  1. package/.bunignore +7 -0
  2. package/.github/workflows/ci.yml +73 -0
  3. package/CLAUDE.md +111 -0
  4. package/CONTRIBUTING.md +145 -0
  5. package/README.md +168 -0
  6. package/bun.lock +267 -0
  7. package/bunfig.toml +15 -0
  8. package/cli +0 -0
  9. package/config/gitforest.example.yaml +94 -0
  10. package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
  11. package/docs/ai/VERIFICATION_REPORT.md +87 -0
  12. package/docs/ai/architecture.md +169 -0
  13. package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
  14. package/docs/ai/checks/check-2025-12-02.md +55 -0
  15. package/docs/ai/checks/test-verification-report.md +85 -0
  16. package/docs/ai/implementation-guide.md +776 -0
  17. package/docs/ai/research/gitty-codebase-analysis.md +221 -0
  18. package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
  19. package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
  20. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
  21. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
  22. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
  23. package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
  24. package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
  25. package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
  26. package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
  27. package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
  28. package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
  29. package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
  30. package/docs/ai/tickets/TASK-sitrep.md +28 -0
  31. package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
  32. package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
  33. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
  34. package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
  35. package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
  36. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
  37. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
  38. package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
  39. package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
  40. package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
  41. package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
  42. package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
  43. package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
  44. package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
  45. package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
  46. package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
  47. package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
  48. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
  49. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
  50. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
  51. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
  52. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
  53. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
  54. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
  55. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
  56. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
  57. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
  58. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
  59. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
  60. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
  61. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
  62. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
  63. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
  64. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
  65. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
  66. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
  67. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
  68. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
  69. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
  70. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
  71. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
  72. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
  73. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
  74. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
  75. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
  76. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
  77. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
  78. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
  79. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
  80. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
  81. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
  82. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
  83. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
  84. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
  85. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
  86. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
  87. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
  88. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
  89. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
  90. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
  91. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
  92. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
  93. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
  94. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
  95. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
  96. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
  97. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
  98. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
  99. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
  100. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
  101. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
  102. package/docs/ai/tkt-001-fix-database-error.md +217 -0
  103. package/docs/ai/ui-enhancement-plan.md +562 -0
  104. package/package.json +50 -0
  105. package/src/app.tsx +43 -0
  106. package/src/cli/config.ts +94 -0
  107. package/src/cli/formatters.ts +632 -0
  108. package/src/cli/index.ts +583 -0
  109. package/src/components/CloneDialog.tsx +137 -0
  110. package/src/components/ColumnHeader.tsx +128 -0
  111. package/src/components/CommandPalette.tsx +120 -0
  112. package/src/components/ConfirmDialog.tsx +105 -0
  113. package/src/components/ErrorBoundary.tsx +128 -0
  114. package/src/components/FilterBar.tsx +71 -0
  115. package/src/components/FilterOptionsOverlay.tsx +131 -0
  116. package/src/components/HelpOverlay.tsx +120 -0
  117. package/src/components/Layout.tsx +379 -0
  118. package/src/components/MarkdownRenderer.tsx +127 -0
  119. package/src/components/ProgressBar.tsx +53 -0
  120. package/src/components/ProjectItem.tsx +143 -0
  121. package/src/components/ProjectList.tsx +90 -0
  122. package/src/components/RepoDetailModal.tsx +367 -0
  123. package/src/components/StatusBar.tsx +188 -0
  124. package/src/components/UnifiedProjectItem.tsx +436 -0
  125. package/src/components/ViewModeIndicator.tsx +37 -0
  126. package/src/components/onboarding/CompleteStep.tsx +82 -0
  127. package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
  128. package/src/components/onboarding/DirectoriesStep.tsx +847 -0
  129. package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
  130. package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
  131. package/src/components/onboarding/OnboardingWizard.tsx +130 -0
  132. package/src/components/onboarding/WelcomeStep.tsx +69 -0
  133. package/src/config/loader.ts +263 -0
  134. package/src/config/onboarding.ts +67 -0
  135. package/src/constants.ts +96 -0
  136. package/src/db/index.ts +147 -0
  137. package/src/db/schema.ts +70 -0
  138. package/src/git/commands.ts +283 -0
  139. package/src/git/index.ts +2 -0
  140. package/src/git/operations.ts +93 -0
  141. package/src/git/service.ts +539 -0
  142. package/src/git/status.ts +84 -0
  143. package/src/git/types.ts +5 -0
  144. package/src/github/auth.ts +311 -0
  145. package/src/github/cache.ts +231 -0
  146. package/src/github/cli.ts +22 -0
  147. package/src/github/unified.ts +415 -0
  148. package/src/hooks/useBackgroundFetch.ts +76 -0
  149. package/src/hooks/useConfirmDialogActions.ts +120 -0
  150. package/src/hooks/useKeyBindings.ts +656 -0
  151. package/src/hooks/useProjects.ts +47 -0
  152. package/src/hooks/useUnifiedRepos.ts +317 -0
  153. package/src/index.tsx +494 -0
  154. package/src/operations/batch.ts +280 -0
  155. package/src/operations/commands.ts +140 -0
  156. package/src/operations/index.ts +37 -0
  157. package/src/scanner/index.ts +424 -0
  158. package/src/scanner/markers.ts +43 -0
  159. package/src/scanner/submodules.ts +61 -0
  160. package/src/services/git.ts +484 -0
  161. package/src/services/github.ts +676 -0
  162. package/src/services/index.ts +28 -0
  163. package/src/services/types.ts +99 -0
  164. package/src/state/actions.ts +175 -0
  165. package/src/state/reducer.ts +294 -0
  166. package/src/state/store.tsx +216 -0
  167. package/src/state/types.ts +8 -0
  168. package/src/types/index.ts +383 -0
  169. package/src/ui/theme.ts +44 -0
  170. package/src/utils/array.ts +14 -0
  171. package/src/utils/debug.ts +38 -0
  172. package/src/utils/errors.ts +17 -0
  173. package/src/utils/index.ts +8 -0
  174. package/src/utils/markdown.ts +230 -0
  175. package/src/utils/project-utils.ts +129 -0
  176. package/src/utils/rate-limiter.ts +134 -0
  177. package/src/utils/retry.ts +147 -0
  178. package/src/utils/timeout.ts +56 -0
  179. package/test/integration/app.isolated.tsx +240 -0
  180. package/test/integration/cli-commands.test.ts +287 -0
  181. package/test/integration/cli-validation.test.ts +264 -0
  182. package/test/integration/git-operations.test.ts +218 -0
  183. package/test/integration/scanner.test.ts +228 -0
  184. package/test/preload.ts +18 -0
  185. package/test/unit/cli/commands.test.ts +13 -0
  186. package/test/unit/cli/formatters.test.ts +1116 -0
  187. package/test/unit/cli/github-commands.test.ts +12 -0
  188. package/test/unit/components/CloneDialog.test.tsx +240 -0
  189. package/test/unit/components/ColumnHeader.test.tsx +128 -0
  190. package/test/unit/components/CommandPalette.test.tsx +355 -0
  191. package/test/unit/components/ConfirmDialog.test.tsx +111 -0
  192. package/test/unit/components/ErrorBoundary.test.tsx +139 -0
  193. package/test/unit/components/FilterBar.test.tsx +43 -0
  194. package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
  195. package/test/unit/components/HelpOverlay.test.tsx +90 -0
  196. package/test/unit/components/Layout.test.tsx +328 -0
  197. package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
  198. package/test/unit/components/ProgressBar.test.tsx +138 -0
  199. package/test/unit/components/ProjectItem.test.tsx +182 -0
  200. package/test/unit/components/ProjectList.test.tsx +311 -0
  201. package/test/unit/components/RepoDetailModal.test.tsx +445 -0
  202. package/test/unit/components/StatusBar.test.tsx +112 -0
  203. package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
  204. package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
  205. package/test/unit/components/test-utils.tsx +63 -0
  206. package/test/unit/config/loader.test.ts +692 -0
  207. package/test/unit/db/database.test.ts +978 -0
  208. package/test/unit/db/index.test.ts +314 -0
  209. package/test/unit/fixtures/setup.ts +186 -0
  210. package/test/unit/git/commands-untested.test.ts +205 -0
  211. package/test/unit/git/commands.test.ts +269 -0
  212. package/test/unit/git/operations.test.ts +322 -0
  213. package/test/unit/git/status.test.ts +219 -0
  214. package/test/unit/github/auth.test.ts +317 -0
  215. package/test/unit/github/cache.test.ts +1028 -0
  216. package/test/unit/github/cli.test.ts +135 -0
  217. package/test/unit/github/unified.test.ts +1201 -0
  218. package/test/unit/graceful-shutdown.test.ts +83 -0
  219. package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
  220. package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
  221. package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
  222. package/test/unit/hooks/useProjects.test.tsx +186 -0
  223. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
  224. package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
  225. package/test/unit/mocks/config.ts +109 -0
  226. package/test/unit/mocks/git-service.ts +274 -0
  227. package/test/unit/mocks/github-service.ts +250 -0
  228. package/test/unit/mocks/index.ts +72 -0
  229. package/test/unit/mocks/project.ts +148 -0
  230. package/test/unit/mocks/state-mocks.ts +187 -0
  231. package/test/unit/mocks/unified.ts +169 -0
  232. package/test/unit/operations/batch.test.ts +216 -0
  233. package/test/unit/operations/commands.test.ts +550 -0
  234. package/test/unit/scanner/errors.test.ts +297 -0
  235. package/test/unit/scanner/index.test.ts +1011 -0
  236. package/test/unit/scanner/markers.test.ts +150 -0
  237. package/test/unit/scanner/submodules.test.ts +99 -0
  238. package/test/unit/services/git-errors.test.ts +190 -0
  239. package/test/unit/services/git.test.ts +442 -0
  240. package/test/unit/services/github-errors.test.ts +293 -0
  241. package/test/unit/services/github.test.ts +200 -0
  242. package/test/unit/state/actions.test.ts +217 -0
  243. package/test/unit/state/reducer.test.ts +745 -0
  244. package/test/unit/state/store.test.tsx +711 -0
  245. package/test/unit/types/commands.test.ts +220 -0
  246. package/test/unit/types/schema.test.ts +179 -0
  247. package/test/unit/utils/array.test.ts +73 -0
  248. package/test/unit/utils/debug.test.ts +23 -0
  249. package/test/unit/utils/errors.test.ts +295 -0
  250. package/test/unit/utils/markdown.test.ts +163 -0
  251. package/test/unit/utils/project-utils.test.ts +756 -0
  252. package/test/unit/utils/rate-limiter.test.ts +256 -0
  253. package/test/unit/utils/retry.test.ts +165 -0
  254. package/test/unit/utils/strip-ansi.ts +13 -0
  255. package/test/unit/utils/timeout.test.ts +93 -0
  256. package/tsconfig.json +29 -0
@@ -0,0 +1,128 @@
1
+ import { Box, Text } from "ink";
2
+ import { NF, COL } from "./UnifiedProjectItem.tsx";
3
+ import { palette } from "../ui/theme.ts";
4
+ import type { SortField, SortDirection } from "../types/index.ts";
5
+
6
+ interface ColumnHeaderProps {
7
+ sortBy?: SortField;
8
+ sortDirection?: SortDirection;
9
+ }
10
+
11
+ /**
12
+ * Column headers for the project list
13
+ * Matches column widths from UnifiedProjectItem exactly
14
+ * Highlights the currently sorted column
15
+ */
16
+ export function ColumnHeader({ sortBy = 'status', sortDirection = 'desc' }: ColumnHeaderProps) {
17
+ // Helper to get header styling based on sort state
18
+ const getStyle = (field: SortField) => {
19
+ const isSorted = sortBy === field;
20
+ return {
21
+ color: isSorted ? palette.text.warning : palette.text.muted,
22
+ bold: isSorted,
23
+ underline: isSorted,
24
+ arrow: isSorted ? (sortDirection === 'desc' ? '↓' : '↑') : '',
25
+ bgColor: undefined,
26
+ };
27
+ };
28
+
29
+ const formatHeader = (
30
+ label: string,
31
+ style: ReturnType<typeof getStyle>,
32
+ width: number,
33
+ padStart = false
34
+ ) => {
35
+ const text = `${label}${style.arrow ? ` ${style.arrow}` : ''}`;
36
+ const padded = padStart ? text.padStart(width) : text.padEnd(width);
37
+ return (
38
+ <Text
39
+ color={style.color}
40
+ bold={style.bold}
41
+ underline={style.underline}
42
+ backgroundColor={style.bgColor}
43
+ >
44
+ {padded}
45
+ </Text>
46
+ );
47
+ };
48
+
49
+ const nameStyle = getStyle('name');
50
+ const statusStyle = getStyle('status');
51
+ const branchStyle = getStyle('branch');
52
+ const syncStyle = getStyle('sync');
53
+ const langStyle = getStyle('language');
54
+ const starsStyle = getStyle('stars');
55
+ const forksStyle = getStyle('forks');
56
+ const activityStyle = getStyle('lastActivity');
57
+ const sizeStyle = getStyle('size');
58
+
59
+ return (
60
+ <Box>
61
+ {/* Cursor space */}
62
+ <Text color="gray">{' '}</Text>
63
+
64
+ {/* Selection "Sel " */}
65
+ <Text color="gray">{'Sel '.padEnd(COL.sel)}</Text>
66
+
67
+ {/* Status header - just icon */}
68
+ <Text color={statusStyle.color} bold={statusStyle.bold} backgroundColor={statusStyle.bgColor}>
69
+ {`${statusStyle.arrow || '●'}`.padEnd(COL.status)}
70
+ </Text>
71
+
72
+ {/* Source icon + Name header */}
73
+ <Text color="gray">{NF.repo}</Text>
74
+ <Text color="gray">{' '}</Text>
75
+ {formatHeader('Name', nameStyle, COL.name - 2)}
76
+
77
+ {/* Gap */}
78
+ <Text>{' '}</Text>
79
+
80
+ {/* Branch header */}
81
+ {formatHeader(`${NF.git_branch_oct} Branch`, branchStyle, COL.branch)}
82
+
83
+ {/* Gap */}
84
+ <Text>{' '}</Text>
85
+
86
+ {/* Sync header */}
87
+ {formatHeader('Sync', syncStyle, COL.sync)}
88
+
89
+ {/* Gap */}
90
+ <Text>{' '}</Text>
91
+
92
+ {/* Language icon */}
93
+ {formatHeader(NF.code, langStyle, COL.lang, true)}
94
+
95
+ {/* Gap */}
96
+ <Text>{' '}</Text>
97
+
98
+ {/* Stars header */}
99
+ {formatHeader(NF.star, starsStyle, COL.stars, true)}
100
+
101
+ {/* Gap */}
102
+ <Text>{' '}</Text>
103
+
104
+ {/* Forks header */}
105
+ {formatHeader(NF.repo_forked, forksStyle, COL.forks, true)}
106
+
107
+ {/* Gap */}
108
+ <Text>{' '}</Text>
109
+
110
+ {/* Updated header */}
111
+ {formatHeader(NF.clock, activityStyle, COL.updated, true)}
112
+
113
+ {/* Gap */}
114
+ <Text>{' '}</Text>
115
+
116
+ {/* Size header */}
117
+ {formatHeader(NF.database, sizeStyle, COL.size, true)}
118
+ </Box>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Separator line under headers
124
+ * No longer needed - cleaner without it
125
+ */
126
+ export function ColumnSeparator() {
127
+ return null;
128
+ }
@@ -0,0 +1,120 @@
1
+ import { Box, Text } from "ink";
2
+ import type { CommandConfig, UnifiedRepo } from "../types/index.ts";
3
+
4
+ export interface CommandPaletteProps {
5
+ commands: CommandConfig[];
6
+ selectedRepos: UnifiedRepo[];
7
+ onClose: () => void;
8
+ }
9
+
10
+ export function CommandPalette({
11
+ commands,
12
+ selectedRepos,
13
+ onClose: _onClose,
14
+ }: CommandPaletteProps) {
15
+ // onClose is handled by parent through keybindings
16
+ void _onClose;
17
+ const repoCount = selectedRepos.length;
18
+ const repoNames = selectedRepos.slice(0, 3).map((r) => r.name);
19
+ const moreCount = repoCount > 3 ? repoCount - 3 : 0;
20
+
21
+ // Get the first local path for context
22
+ const targetPath = selectedRepos[0]?.localPath || selectedRepos[0]?.local?.path;
23
+
24
+ if (commands.length === 0) {
25
+ return (
26
+ <Box
27
+ flexDirection="column"
28
+ borderStyle="round"
29
+ borderColor="cyan"
30
+ paddingX={2}
31
+ paddingY={1}
32
+ >
33
+ <Box marginBottom={1}>
34
+ <Text bold color="cyan">
35
+ Command Palette
36
+ </Text>
37
+ </Box>
38
+
39
+ <Text dimColor>
40
+ No custom commands configured.
41
+ </Text>
42
+ <Text dimColor>
43
+ Add commands to your config file:
44
+ </Text>
45
+ <Box marginTop={1} flexDirection="column">
46
+ <Text color="gray">commands:</Text>
47
+ <Text color="gray"> - name: "Open in Editor"</Text>
48
+ <Text color="gray"> key: "e"</Text>
49
+ <Text color="gray"> command: "code ."</Text>
50
+ </Box>
51
+
52
+ <Box marginTop={1}>
53
+ <Text dimColor>[Esc] Close</Text>
54
+ </Box>
55
+ </Box>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <Box
61
+ flexDirection="column"
62
+ borderStyle="round"
63
+ borderColor="cyan"
64
+ paddingX={2}
65
+ paddingY={1}
66
+ >
67
+ {/* Header */}
68
+ <Box marginBottom={1}>
69
+ <Text bold color="cyan">
70
+ Command Palette
71
+ </Text>
72
+ </Box>
73
+
74
+ {/* Target info */}
75
+ <Box marginBottom={1} flexDirection="column">
76
+ <Box gap={1}>
77
+ <Text>Target:</Text>
78
+ <Text color="green">
79
+ {repoNames.join(", ")}
80
+ {moreCount > 0 && <Text dimColor> +{moreCount} more</Text>}
81
+ </Text>
82
+ </Box>
83
+ {targetPath && (
84
+ <Box gap={1}>
85
+ <Text dimColor>Path:</Text>
86
+ <Text dimColor>{targetPath}</Text>
87
+ </Box>
88
+ )}
89
+ </Box>
90
+
91
+ {/* Commands list */}
92
+ <Box flexDirection="column" gap={0}>
93
+ <Box marginBottom={1}>
94
+ <Text bold underline>
95
+ Available Commands
96
+ </Text>
97
+ </Box>
98
+ {commands.map((cmd) => (
99
+ <Box key={cmd.key} gap={1}>
100
+ <Box width={6}>
101
+ <Text color="yellow">[{cmd.key}]</Text>
102
+ </Box>
103
+ <Box width={24}>
104
+ <Text>{cmd.name}</Text>
105
+ </Box>
106
+ <Text dimColor>{cmd.command}</Text>
107
+ {cmd.confirm && <Text color="red"> (confirm)</Text>}
108
+ {cmd.background && <Text color="blue"> (bg)</Text>}
109
+ </Box>
110
+ ))}
111
+ </Box>
112
+
113
+ {/* Footer */}
114
+ <Box marginTop={1} gap={2}>
115
+ <Text dimColor>[key] Execute</Text>
116
+ <Text dimColor>[Esc] Close</Text>
117
+ </Box>
118
+ </Box>
119
+ );
120
+ }
@@ -0,0 +1,105 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ export interface ConfirmDialogProps {
5
+ title: string;
6
+ message: string;
7
+ items: string[];
8
+ showVisibilityToggle?: boolean;
9
+ defaultVisibility?: "private" | "public";
10
+ onConfirm: (options: { visibility?: "private" | "public" }) => void;
11
+ onCancel: () => void;
12
+ }
13
+
14
+ export function ConfirmDialog({
15
+ title,
16
+ message,
17
+ items,
18
+ showVisibilityToggle = false,
19
+ defaultVisibility = "private",
20
+ onConfirm,
21
+ onCancel,
22
+ }: ConfirmDialogProps) {
23
+ const [visibility, setVisibility] = useState<"private" | "public">(
24
+ defaultVisibility
25
+ );
26
+
27
+ useInput((input, key) => {
28
+ if (input === "y" || input === "Y") {
29
+ onConfirm({ visibility: showVisibilityToggle ? visibility : undefined });
30
+ } else if (input === "n" || input === "N" || key.escape) {
31
+ onCancel();
32
+ } else if (showVisibilityToggle && (input === "v" || input === "V")) {
33
+ setVisibility((prev) => (prev === "private" ? "public" : "private"));
34
+ }
35
+ });
36
+
37
+ const maxItems = 5;
38
+ const displayItems = items.slice(0, maxItems);
39
+ const remainingCount = items.length - maxItems;
40
+
41
+ return (
42
+ <Box
43
+ flexDirection="column"
44
+ borderStyle="round"
45
+ borderColor="yellow"
46
+ paddingX={2}
47
+ paddingY={1}
48
+ width={50}
49
+ >
50
+ <Box marginBottom={1}>
51
+ <Text bold color="yellow">
52
+ {title}
53
+ </Text>
54
+ </Box>
55
+
56
+ <Box marginBottom={1}>
57
+ <Text>{message}</Text>
58
+ </Box>
59
+
60
+ <Box flexDirection="column" marginBottom={1}>
61
+ {displayItems.map((item) => (
62
+ <Text key={item} color="cyan">
63
+ {" • "}
64
+ {item}
65
+ </Text>
66
+ ))}
67
+ {remainingCount > 0 && (
68
+ <Text dimColor>{" "}...and {remainingCount} more</Text>
69
+ )}
70
+ </Box>
71
+
72
+ {showVisibilityToggle && (
73
+ <Box marginBottom={1}>
74
+ <Text>Visibility: </Text>
75
+ <Text
76
+ bold
77
+ color={visibility === "private" ? "green" : "yellow"}
78
+ >
79
+ {visibility === "private" ? "[●] Private" : "[ ] Private"}
80
+ </Text>
81
+ <Text> </Text>
82
+ <Text
83
+ bold
84
+ color={visibility === "public" ? "green" : "yellow"}
85
+ >
86
+ {visibility === "public" ? "[●] Public" : "[ ] Public"}
87
+ </Text>
88
+ <Text dimColor> (v to toggle)</Text>
89
+ </Box>
90
+ )}
91
+
92
+ <Box>
93
+ <Text dimColor>Press </Text>
94
+ <Text color="green" bold>
95
+ y
96
+ </Text>
97
+ <Text dimColor> to confirm, </Text>
98
+ <Text color="red" bold>
99
+ n
100
+ </Text>
101
+ <Text dimColor> to cancel</Text>
102
+ </Box>
103
+ </Box>
104
+ );
105
+ }
@@ -0,0 +1,128 @@
1
+ import React, { Component, type ReactNode } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+
4
+ interface Props {
5
+ children: ReactNode;
6
+ fallback?: ReactNode;
7
+ onRetry?: () => void;
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean;
12
+ error: Error | null;
13
+ errorInfo: React.ErrorInfo | null;
14
+ }
15
+
16
+ export class ErrorBoundary extends Component<Props, State> {
17
+ constructor(props: Props) {
18
+ super(props);
19
+ this.state = {
20
+ hasError: false,
21
+ error: null,
22
+ errorInfo: null,
23
+ };
24
+ }
25
+
26
+ static getDerivedStateFromError(error: Error): Partial<State> {
27
+ return {
28
+ hasError: true,
29
+ error,
30
+ };
31
+ }
32
+
33
+ override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
34
+ console.error("Error caught by ErrorBoundary:", error);
35
+ console.error("Error info:", errorInfo);
36
+
37
+ this.setState({
38
+ error,
39
+ errorInfo,
40
+ });
41
+ }
42
+
43
+ handleRetry = () => {
44
+ this.setState({
45
+ hasError: false,
46
+ error: null,
47
+ errorInfo: null,
48
+ });
49
+
50
+ if (this.props.onRetry) {
51
+ this.props.onRetry();
52
+ }
53
+ };
54
+
55
+ override render() {
56
+ if (this.state.hasError && this.state.error) {
57
+ if (this.props.fallback) {
58
+ return this.props.fallback;
59
+ }
60
+
61
+ return (
62
+ <ErrorFallback
63
+ error={this.state.error}
64
+ onRetry={this.handleRetry}
65
+ />
66
+ );
67
+ }
68
+
69
+ return this.props.children;
70
+ }
71
+ }
72
+
73
+ interface ErrorFallbackProps {
74
+ error: Error;
75
+ onRetry: () => void;
76
+ }
77
+
78
+ function ErrorFallback({ error, onRetry }: ErrorFallbackProps) {
79
+ useInput(() => {
80
+ onRetry();
81
+ });
82
+
83
+ return (
84
+ <Box
85
+ flexDirection="column"
86
+ borderStyle="round"
87
+ borderColor="red"
88
+ paddingX={2}
89
+ paddingY={1}
90
+ >
91
+ <Box marginBottom={1}>
92
+ <Text bold color="red">
93
+ ⚠️ An Error Occurred
94
+ </Text>
95
+ </Box>
96
+
97
+ <Box marginBottom={1}>
98
+ <Text color="yellow" bold>
99
+ Error:
100
+ </Text>
101
+ <Text>{error.message}</Text>
102
+ </Box>
103
+
104
+ {error.stack && (
105
+ <Box flexDirection="column" marginBottom={1}>
106
+ <Box marginBottom={1}>
107
+ <Text dimColor>
108
+ Stack trace (first 5 lines):
109
+ </Text>
110
+ </Box>
111
+ <Box flexDirection="column">
112
+ {error.stack.split('\n').slice(0, 5).map((line, index) => (
113
+ <Text key={index} dimColor>
114
+ {line.trim()}
115
+ </Text>
116
+ ))}
117
+ </Box>
118
+ </Box>
119
+ )}
120
+
121
+ <Box marginTop={1}>
122
+ <Text dimColor>
123
+ Press any key to retry
124
+ </Text>
125
+ </Box>
126
+ </Box>
127
+ );
128
+ }
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { TextInput } from "@inkjs/ui";
4
+ import { useStore } from "../state/store.tsx";
5
+ import { setFilter, setMode } from "../state/actions.ts";
6
+ import { ViewModeIndicator } from "./ViewModeIndicator.tsx";
7
+ import { palette } from "../ui/theme.ts";
8
+
9
+ export function FilterBar() {
10
+ const { state, dispatch } = useStore();
11
+ const {
12
+ filterText,
13
+ mode,
14
+ sortBy,
15
+ sortDirection,
16
+ viewMode,
17
+ isLoadingGitHub,
18
+ githubError,
19
+ quickFilter,
20
+ languageFilter,
21
+ } = state;
22
+
23
+ const isFilterMode = mode === "filter";
24
+ const sortArrow = sortDirection === "desc" ? "↓" : "↑";
25
+
26
+ return (
27
+ <Box gap={2} alignItems="center" flexWrap="wrap">
28
+ {/* View mode indicator */}
29
+ <ViewModeIndicator
30
+ mode={viewMode}
31
+ isLoadingGitHub={isLoadingGitHub}
32
+ githubError={githubError}
33
+ />
34
+
35
+ {/* Filter input */}
36
+ <Box>
37
+ <Text color={isFilterMode ? "cyan" : "gray"}>Filter: </Text>
38
+ {isFilterMode ? (
39
+ <TextInput
40
+ key={filterText} // Force remount when filter text changes externally
41
+ defaultValue={filterText}
42
+ onChange={(value) => dispatch(setFilter(value))}
43
+ placeholder="type to filter..."
44
+ />
45
+ ) : (
46
+ <Text color={filterText ? "white" : "gray"}>
47
+ {filterText || "(press / to filter)"}
48
+ </Text>
49
+ )}
50
+ </Box>
51
+
52
+ {/* Active filters summary */}
53
+ <Box gap={1} alignItems="center" flexWrap="wrap">
54
+ {quickFilter !== "all" && (
55
+ <Text color={palette.text.warning}>[{quickFilter}]</Text>
56
+ )}
57
+ {languageFilter && (
58
+ <Text color={palette.text.info}>Lang:{languageFilter}</Text>
59
+ )}
60
+ </Box>
61
+
62
+ {/* Sort indicator */}
63
+ <Box flexGrow={1} justifyContent="flex-end" gap={1} alignItems="center">
64
+ <Text color={palette.text.muted} bold>
65
+ Sort: {sortBy} {sortArrow}
66
+ </Text>
67
+ <Text color="gray">(press s to change)</Text>
68
+ </Box>
69
+ </Box>
70
+ );
71
+ }
@@ -0,0 +1,131 @@
1
+ import { Box, Text } from "ink";
2
+ import { useStore } from "../state/store.tsx";
3
+
4
+ export function FilterOptionsOverlay() {
5
+ const { state } = useStore();
6
+ const { quickFilter, viewMode, sortBy, languageFilter } = state;
7
+
8
+ // Highlight the currently active filter
9
+ const isActive = (filter: string) => quickFilter === filter;
10
+
11
+ return (
12
+ <Box
13
+ flexDirection="column"
14
+ borderStyle="round"
15
+ borderColor="cyan"
16
+ paddingX={2}
17
+ paddingY={1}
18
+ >
19
+ {/* Title */}
20
+ <Box marginBottom={1}>
21
+ <Text bold color="cyan">Filter Options</Text>
22
+ </Box>
23
+
24
+ {/* Quick Filters section */}
25
+ <Box marginBottom={1}>
26
+ <Text bold underline>Quick Filters:</Text>
27
+ </Box>
28
+ <Box flexDirection="column" marginBottom={1}>
29
+ <Box gap={2}>
30
+ <FilterKey num="0" label="All repos" active={isActive("all")} />
31
+ <FilterKey num="1" label="Dirty (uncommitted)" active={isActive("dirty")} />
32
+ </Box>
33
+ <Box gap={2}>
34
+ <FilterKey num="2" label="Unpushed commits" active={isActive("unpushed")} />
35
+ <FilterKey num="3" label="No remote configured" active={isActive("no-remote")} />
36
+ </Box>
37
+ <Box gap={2}>
38
+ <FilterKey num="4" label="GitHub-only" active={isActive("github-only")} />
39
+ <FilterKey num="5" label="Local-only" active={isActive("local-only")} />
40
+ </Box>
41
+ <Box gap={2}>
42
+ <FilterKey num="6" label="Private repos" active={isActive("private")} />
43
+ <FilterKey num="7" label="Public repos" active={isActive("public")} />
44
+ </Box>
45
+ <Box gap={2}>
46
+ <FilterKey num="8" label="Archived" active={isActive("archived")} />
47
+ <FilterKey num="9" label="Forks" active={isActive("forks")} />
48
+ </Box>
49
+ </Box>
50
+
51
+ {/* View Modes */}
52
+ <Box marginBottom={1}>
53
+ <Text bold underline>View Modes:</Text>
54
+ <Text dimColor> (Tab to cycle)</Text>
55
+ </Box>
56
+ <Box marginBottom={1}>
57
+ <ViewModeOption mode="local" current={viewMode} />
58
+ <Text dimColor> → </Text>
59
+ <ViewModeOption mode="github" current={viewMode} />
60
+ <Text dimColor> → </Text>
61
+ <ViewModeOption mode="combined" current={viewMode} />
62
+ </Box>
63
+
64
+ {/* Sort */}
65
+ <Box marginBottom={1}>
66
+ <Text bold underline>Sort:</Text>
67
+ <Text dimColor> (s to cycle)</Text>
68
+ </Box>
69
+ <Box marginBottom={1}>
70
+ <SortOption field="status" current={sortBy} />
71
+ <Text dimColor> → </Text>
72
+ <SortOption field="name" current={sortBy} />
73
+ <Text dimColor> → </Text>
74
+ <SortOption field="lastActivity" current={sortBy} />
75
+ <Text dimColor> → </Text>
76
+ <SortOption field="stars" current={sortBy} />
77
+ <Text dimColor> → </Text>
78
+ <SortOption field="size" current={sortBy} />
79
+ </Box>
80
+
81
+ {/* Search */}
82
+ <Box marginBottom={1}>
83
+ <Text bold underline>Search:</Text>
84
+ </Box>
85
+ <Box marginBottom={1}>
86
+ <Text color="yellow">/</Text>
87
+ <Text> Start text search (by name, description, path)</Text>
88
+ </Box>
89
+
90
+ {/* Language Filter */}
91
+ <Box marginBottom={1}>
92
+ <Text bold underline>Language Filter:</Text>
93
+ </Box>
94
+ <Box marginBottom={1}>
95
+ <Text>Type language name to filter (e.g., typescript, rust)</Text>
96
+ </Box>
97
+ <Box marginBottom={1}>
98
+ <Text>Current: </Text>
99
+ <Text color={languageFilter ? "cyan" : "gray"}>
100
+ {languageFilter || "[none]"}
101
+ </Text>
102
+ </Box>
103
+
104
+ {/* Footer */}
105
+ <Box marginTop={1}>
106
+ <Text dimColor>Press any key to close</Text>
107
+ </Box>
108
+ </Box>
109
+ );
110
+ }
111
+
112
+ // Helper components
113
+ function FilterKey({ num, label, active }: { num: string; label: string; active: boolean }) {
114
+ return (
115
+ <Box width={24}>
116
+ <Text color="yellow">{num}</Text>
117
+ <Text color={active ? "cyan" : undefined}> {label}</Text>
118
+ {active && <Text color="green"> ✓</Text>}
119
+ </Box>
120
+ );
121
+ }
122
+
123
+ function ViewModeOption({ mode, current }: { mode: string; current: string }) {
124
+ const isActive = mode === current;
125
+ return <Text color={isActive ? "cyan" : "gray"}>{mode}</Text>;
126
+ }
127
+
128
+ function SortOption({ field, current }: { field: string; current: string }) {
129
+ const isActive = field === current;
130
+ return <Text color={isActive ? "cyan" : "gray"}>{field}</Text>;
131
+ }