diffstalker 0.1.7 → 0.2.1

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 (74) hide show
  1. package/.github/workflows/release.yml +8 -0
  2. package/CHANGELOG.md +36 -0
  3. package/bun.lock +89 -306
  4. package/dist/App.js +895 -520
  5. package/dist/FollowMode.js +85 -0
  6. package/dist/KeyBindings.js +178 -0
  7. package/dist/MouseHandlers.js +156 -0
  8. package/dist/core/ExplorerStateManager.js +632 -0
  9. package/dist/core/FilePathWatcher.js +133 -0
  10. package/dist/core/GitStateManager.js +221 -86
  11. package/dist/git/diff.js +4 -0
  12. package/dist/git/ignoreUtils.js +30 -0
  13. package/dist/git/status.js +2 -34
  14. package/dist/index.js +68 -53
  15. package/dist/ipc/CommandClient.js +165 -0
  16. package/dist/ipc/CommandServer.js +152 -0
  17. package/dist/state/CommitFlowState.js +86 -0
  18. package/dist/state/UIState.js +195 -0
  19. package/dist/types/tabs.js +4 -0
  20. package/dist/ui/Layout.js +252 -0
  21. package/dist/ui/PaneRenderers.js +56 -0
  22. package/dist/ui/modals/BaseBranchPicker.js +110 -0
  23. package/dist/ui/modals/DiscardConfirm.js +77 -0
  24. package/dist/ui/modals/FileFinder.js +232 -0
  25. package/dist/ui/modals/HotkeysModal.js +209 -0
  26. package/dist/ui/modals/ThemePicker.js +107 -0
  27. package/dist/ui/widgets/CommitPanel.js +58 -0
  28. package/dist/ui/widgets/CompareListView.js +238 -0
  29. package/dist/ui/widgets/DiffView.js +281 -0
  30. package/dist/ui/widgets/ExplorerContent.js +89 -0
  31. package/dist/ui/widgets/ExplorerView.js +204 -0
  32. package/dist/ui/widgets/FileList.js +185 -0
  33. package/dist/ui/widgets/Footer.js +50 -0
  34. package/dist/ui/widgets/Header.js +68 -0
  35. package/dist/ui/widgets/HistoryView.js +69 -0
  36. package/dist/utils/displayRows.js +185 -6
  37. package/dist/utils/explorerDisplayRows.js +1 -1
  38. package/dist/utils/fileCategories.js +37 -0
  39. package/dist/utils/fileTree.js +148 -0
  40. package/dist/utils/languageDetection.js +56 -0
  41. package/dist/utils/pathUtils.js +27 -0
  42. package/dist/utils/wordDiff.js +50 -0
  43. package/eslint.metrics.js +16 -0
  44. package/metrics/.gitkeep +0 -0
  45. package/metrics/v0.2.1.json +268 -0
  46. package/package.json +14 -12
  47. package/dist/components/BaseBranchPicker.js +0 -60
  48. package/dist/components/BottomPane.js +0 -101
  49. package/dist/components/CommitPanel.js +0 -58
  50. package/dist/components/CompareListView.js +0 -110
  51. package/dist/components/ExplorerContentView.js +0 -80
  52. package/dist/components/ExplorerView.js +0 -37
  53. package/dist/components/FileList.js +0 -131
  54. package/dist/components/Footer.js +0 -6
  55. package/dist/components/Header.js +0 -107
  56. package/dist/components/HistoryView.js +0 -21
  57. package/dist/components/HotkeysModal.js +0 -108
  58. package/dist/components/Modal.js +0 -19
  59. package/dist/components/ScrollableList.js +0 -125
  60. package/dist/components/ThemePicker.js +0 -42
  61. package/dist/components/TopPane.js +0 -14
  62. package/dist/components/UnifiedDiffView.js +0 -115
  63. package/dist/hooks/useCommitFlow.js +0 -66
  64. package/dist/hooks/useCompareState.js +0 -123
  65. package/dist/hooks/useExplorerState.js +0 -248
  66. package/dist/hooks/useGit.js +0 -156
  67. package/dist/hooks/useHistoryState.js +0 -62
  68. package/dist/hooks/useKeymap.js +0 -167
  69. package/dist/hooks/useLayout.js +0 -154
  70. package/dist/hooks/useMouse.js +0 -87
  71. package/dist/hooks/useTerminalSize.js +0 -20
  72. package/dist/hooks/useWatcher.js +0 -137
  73. package/dist/utils/mouseCoordinates.js +0 -165
  74. package/dist/utils/rowCalculations.js +0 -209
@@ -0,0 +1,268 @@
1
+ {
2
+ "timestamp": "2026-01-30T15:45:56.951Z",
3
+ "gitRef": "v0.2.1",
4
+ "gitSha": "3d0506b",
5
+ "summary": {
6
+ "files": 62,
7
+ "lines": 12327,
8
+ "functions": 754,
9
+ "avgCyclomaticComplexity": 5.6,
10
+ "maxCyclomaticComplexity": {
11
+ "value": 64,
12
+ "function": "formatDisplayRow",
13
+ "file": "src/ui/widgets/DiffView.ts:68"
14
+ },
15
+ "avgCognitiveComplexity": 7.6,
16
+ "maxCognitiveComplexity": {
17
+ "value": 96,
18
+ "function": "formatDisplayRow",
19
+ "file": "src/ui/widgets/DiffView.ts:68"
20
+ },
21
+ "smells": 44
22
+ },
23
+ "hotspots": [
24
+ {
25
+ "file": "src/App.ts",
26
+ "lines": 1131,
27
+ "cyclomaticMax": 19,
28
+ "cognitiveMax": 16,
29
+ "smells": 7
30
+ },
31
+ {
32
+ "file": "src/core/ExplorerStateManager.ts",
33
+ "lines": 769,
34
+ "cyclomaticMax": 18,
35
+ "cognitiveMax": 34,
36
+ "smells": 7
37
+ },
38
+ {
39
+ "file": "src/ui/widgets/Header.ts",
40
+ "lines": 89,
41
+ "cyclomaticMax": 14,
42
+ "cognitiveMax": 22,
43
+ "smells": 3
44
+ },
45
+ {
46
+ "file": "src/state/UIState.ts",
47
+ "lines": 280,
48
+ "cyclomaticMax": 10,
49
+ "cognitiveMax": 12,
50
+ "smells": 3
51
+ },
52
+ {
53
+ "file": "src/ipc/CommandClient.ts",
54
+ "lines": 202,
55
+ "cyclomaticMax": 7,
56
+ "cognitiveMax": 6,
57
+ "smells": 3
58
+ },
59
+ {
60
+ "file": "src/utils/displayRows.ts",
61
+ "lines": 459,
62
+ "cyclomaticMax": 42,
63
+ "cognitiveMax": 68,
64
+ "smells": 2
65
+ },
66
+ {
67
+ "file": "src/index.ts",
68
+ "lines": 178,
69
+ "cyclomaticMax": 16,
70
+ "cognitiveMax": 17,
71
+ "smells": 2
72
+ },
73
+ {
74
+ "file": "src/utils/ansiTruncate.ts",
75
+ "lines": 124,
76
+ "cyclomaticMax": 16,
77
+ "cognitiveMax": 24,
78
+ "smells": 2
79
+ },
80
+ {
81
+ "file": "src/utils/languageDetection.ts",
82
+ "lines": 258,
83
+ "cyclomaticMax": 10,
84
+ "cognitiveMax": 8,
85
+ "smells": 2
86
+ },
87
+ {
88
+ "file": "src/ui/modals/HotkeysModal.ts",
89
+ "lines": 242,
90
+ "cyclomaticMax": 7,
91
+ "cognitiveMax": 9,
92
+ "smells": 2
93
+ },
94
+ {
95
+ "file": "src/ui/modals/ThemePicker.ts",
96
+ "lines": 133,
97
+ "cyclomaticMax": 5,
98
+ "cognitiveMax": 7,
99
+ "smells": 2
100
+ },
101
+ {
102
+ "file": "src/ui/widgets/DiffView.ts",
103
+ "lines": 366,
104
+ "cyclomaticMax": 64,
105
+ "cognitiveMax": 96,
106
+ "smells": 1
107
+ },
108
+ {
109
+ "file": "src/git/diff.ts",
110
+ "lines": 567,
111
+ "cyclomaticMax": 27,
112
+ "cognitiveMax": 43,
113
+ "smells": 1
114
+ },
115
+ {
116
+ "file": "src/ui/widgets/ExplorerView.ts",
117
+ "lines": 243,
118
+ "cyclomaticMax": 24,
119
+ "cognitiveMax": 37,
120
+ "smells": 1
121
+ },
122
+ {
123
+ "file": "src/ui/widgets/ExplorerContent.ts",
124
+ "lines": 131,
125
+ "cyclomaticMax": 20,
126
+ "cognitiveMax": 24,
127
+ "smells": 1
128
+ },
129
+ {
130
+ "file": "src/ui/PaneRenderers.ts",
131
+ "lines": 170,
132
+ "cyclomaticMax": 18,
133
+ "cognitiveMax": 6,
134
+ "smells": 1
135
+ },
136
+ {
137
+ "file": "src/ui/widgets/FileList.ts",
138
+ "lines": 225,
139
+ "cyclomaticMax": 16,
140
+ "cognitiveMax": 26,
141
+ "smells": 1
142
+ },
143
+ {
144
+ "file": "src/ui/widgets/CommitPanel.ts",
145
+ "lines": 83,
146
+ "cyclomaticMax": 14,
147
+ "cognitiveMax": 12,
148
+ "smells": 1
149
+ },
150
+ {
151
+ "file": "src/ui/widgets/Footer.ts",
152
+ "lines": 67,
153
+ "cyclomaticMax": 7,
154
+ "cognitiveMax": 7,
155
+ "smells": 1
156
+ },
157
+ {
158
+ "file": "src/core/GitOperationQueue.test.ts",
159
+ "lines": 276,
160
+ "cyclomaticMax": 0,
161
+ "cognitiveMax": 0,
162
+ "smells": 1
163
+ },
164
+ {
165
+ "file": "src/git/status.ts",
166
+ "lines": 310,
167
+ "cyclomaticMax": 28,
168
+ "cognitiveMax": 32,
169
+ "smells": 0
170
+ },
171
+ {
172
+ "file": "src/ui/widgets/CompareListView.ts",
173
+ "lines": 350,
174
+ "cyclomaticMax": 23,
175
+ "cognitiveMax": 21,
176
+ "smells": 0
177
+ },
178
+ {
179
+ "file": "src/MouseHandlers.ts",
180
+ "lines": 212,
181
+ "cyclomaticMax": 17,
182
+ "cognitiveMax": 15,
183
+ "smells": 0
184
+ },
185
+ {
186
+ "file": "src/ipc/CommandServer.ts",
187
+ "lines": 266,
188
+ "cyclomaticMax": 17,
189
+ "cognitiveMax": 6,
190
+ "smells": 0
191
+ },
192
+ {
193
+ "file": "src/utils/diffRowCalculations.ts",
194
+ "lines": 136,
195
+ "cyclomaticMax": 13,
196
+ "cognitiveMax": 24,
197
+ "smells": 0
198
+ },
199
+ {
200
+ "file": "src/utils/lineBreaking.ts",
201
+ "lines": 114,
202
+ "cyclomaticMax": 12,
203
+ "cognitiveMax": 17,
204
+ "smells": 0
205
+ },
206
+ {
207
+ "file": "src/config.ts",
208
+ "lines": 107,
209
+ "cyclomaticMax": 10,
210
+ "cognitiveMax": 13,
211
+ "smells": 0
212
+ },
213
+ {
214
+ "file": "src/core/GitStateManager.ts",
215
+ "lines": 720,
216
+ "cyclomaticMax": 9,
217
+ "cognitiveMax": 13,
218
+ "smells": 0
219
+ },
220
+ {
221
+ "file": "src/ui/modals/FileFinder.ts",
222
+ "lines": 280,
223
+ "cyclomaticMax": 9,
224
+ "cognitiveMax": 13,
225
+ "smells": 0
226
+ },
227
+ {
228
+ "file": "src/ui/widgets/HistoryView.ts",
229
+ "lines": 97,
230
+ "cyclomaticMax": 9,
231
+ "cognitiveMax": 12,
232
+ "smells": 0
233
+ },
234
+ {
235
+ "file": "src/utils/formatPath.ts",
236
+ "lines": 72,
237
+ "cyclomaticMax": 9,
238
+ "cognitiveMax": 11,
239
+ "smells": 0
240
+ },
241
+ {
242
+ "file": "src/utils/explorerDisplayRows.ts",
243
+ "lines": 206,
244
+ "cyclomaticMax": 8,
245
+ "cognitiveMax": 15,
246
+ "smells": 0
247
+ },
248
+ {
249
+ "file": "src/ui/modals/BaseBranchPicker.ts",
250
+ "lines": 134,
251
+ "cyclomaticMax": 6,
252
+ "cognitiveMax": 13,
253
+ "smells": 0
254
+ }
255
+ ],
256
+ "smellsByRule": {
257
+ "@typescript-eslint/no-unused-vars": 22,
258
+ "@typescript-eslint/no-explicit-any": 1,
259
+ "sonarjs/no-ignored-exceptions": 4,
260
+ "sonarjs/slow-regex": 4,
261
+ "sonarjs/updated-loop-counter": 2,
262
+ "prefer-const": 1,
263
+ "sonarjs/no-dead-store": 2,
264
+ "sonarjs/no-all-duplicated-branches": 1,
265
+ "sonarjs/no-nested-conditional": 3,
266
+ "no-control-regex": 4
267
+ }
268
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffstalker",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "description": "Terminal application that displays git diff/status for directories",
5
5
  "author": "yogh-io",
6
6
  "license": "MIT",
@@ -18,18 +18,21 @@
18
18
  "diffstalker": "bin/diffstalker"
19
19
  },
20
20
  "scripts": {
21
- "dev": "bun --watch src/index.tsx",
21
+ "postinstall": "node scripts/patch-neo-blessed.cjs",
22
+ "dev": "bun --watch src/index.ts",
22
23
  "build": "tsc",
23
24
  "build:prod": "tsc && bun build dist/index.js --outfile dist/index.js --minify --target node --packages external",
24
- "bundle": "bun run build:prod && bun build dist/index.js --outdir dist/bundle --minify --target node --external react-devtools-core",
25
+ "bundle": "bun run build:prod && bun build dist/index.js --outdir dist/bundle --minify --target node --external neo-blessed",
25
26
  "start": "bun dist/index.js",
26
27
  "start:bundle": "bun dist/bundle/index.js",
27
- "test": "vitest run",
28
- "test:watch": "vitest",
28
+ "test": "bun test src/*.test.ts src/**/*.test.ts",
29
+ "test:watch": "bun test --watch src/*.test.ts src/**/*.test.ts",
29
30
  "lint": "eslint src/",
30
31
  "lint:fix": "eslint src/ --fix",
31
32
  "format": "prettier --write src/",
32
33
  "format:check": "prettier --check src/",
34
+ "metrics": "bun scripts/collect-metrics.ts",
35
+ "metrics:snapshot": "bun scripts/collect-metrics.ts --save",
33
36
  "prepublishOnly": "bun run build:prod"
34
37
  },
35
38
  "keywords": [
@@ -46,22 +49,21 @@
46
49
  "chokidar": "^4.0.3",
47
50
  "emphasize": "^7.0.0",
48
51
  "fast-diff": "^1.3.0",
49
- "ink": "^6.6.0",
50
- "ink-text-input": "^6.0.0",
51
- "react": "^19.2.0",
52
+ "ignore": "^7.0.5",
53
+ "neo-blessed": "^0.2.0",
52
54
  "simple-git": "^3.27.0",
53
55
  "string-width": "^8.1.0"
54
56
  },
55
57
  "devDependencies": {
56
58
  "@eslint/js": "^9.39.2",
59
+ "@types/blessed": "^0.1.27",
57
60
  "@types/node": "^22.10.7",
58
- "@types/react": "^19.2.0",
59
61
  "eslint": "^9.39.2",
60
62
  "eslint-config-prettier": "^10.1.8",
61
- "eslint-plugin-react-hooks": "^7.0.1",
63
+ "eslint-plugin-sonarjs": "^3.0.6",
64
+ "patch-package": "^8.0.1",
62
65
  "prettier": "^3.8.0",
63
66
  "typescript": "^5.7.3",
64
- "typescript-eslint": "^8.53.1",
65
- "vitest": "^2.1.0"
67
+ "typescript-eslint": "^8.53.1"
66
68
  }
67
69
  }
@@ -1,60 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useMemo } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import { Modal, centerModal } from './Modal.js';
5
- export function BaseBranchPicker({ candidates, currentBranch, onSelect, onCancel, width, height, }) {
6
- const [inputValue, setInputValue] = useState('');
7
- const [selectedIndex, setSelectedIndex] = useState(0);
8
- // Filter candidates based on input
9
- const filteredCandidates = useMemo(() => {
10
- if (!inputValue)
11
- return candidates;
12
- const lower = inputValue.toLowerCase();
13
- return candidates.filter((c) => c.toLowerCase().includes(lower));
14
- }, [candidates, inputValue]);
15
- // Clamp selected index to valid range
16
- const clampedIndex = Math.min(selectedIndex, Math.max(0, filteredCandidates.length - 1));
17
- useInput((input, key) => {
18
- if (key.escape) {
19
- onCancel();
20
- }
21
- else if (key.return) {
22
- // If input matches no candidates but has value, use the input as custom branch
23
- if (filteredCandidates.length === 0 && inputValue) {
24
- onSelect(inputValue);
25
- }
26
- else if (filteredCandidates.length > 0) {
27
- onSelect(filteredCandidates[clampedIndex]);
28
- }
29
- }
30
- else if (key.upArrow) {
31
- setSelectedIndex((prev) => Math.max(0, prev - 1));
32
- }
33
- else if (key.downArrow) {
34
- setSelectedIndex((prev) => Math.min(filteredCandidates.length - 1, prev + 1));
35
- }
36
- else if (key.backspace || key.delete) {
37
- setInputValue((prev) => prev.slice(0, -1));
38
- setSelectedIndex(0);
39
- }
40
- else if (input && !key.ctrl && !key.meta) {
41
- setInputValue((prev) => prev + input);
42
- setSelectedIndex(0);
43
- }
44
- });
45
- // Calculate box dimensions
46
- const boxWidth = Math.min(60, width - 4);
47
- const maxListHeight = Math.min(10, height - 10);
48
- const boxHeight = Math.min(maxListHeight + 9, height - 4); // +9 for header, input, footer, borders
49
- // Center the modal
50
- const { x, y } = centerModal(boxWidth, boxHeight, width, height);
51
- // Visible candidates (with scroll)
52
- const scrollOffset = Math.max(0, clampedIndex - maxListHeight + 1);
53
- const visibleCandidates = filteredCandidates.slice(scrollOffset, scrollOffset + maxListHeight);
54
- return (_jsx(Modal, { x: x, y: y, width: boxWidth, height: boxHeight, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", width: boxWidth, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', "Select Base Branch", ' '] }) }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Filter: " }), _jsx(Text, { color: "cyan", children: inputValue }), _jsx(Text, { color: "cyan", children: "\u258C" })] }), _jsx(Box, { flexDirection: "column", height: maxListHeight, children: visibleCandidates.length > 0 ? (visibleCandidates.map((branch, index) => {
55
- const actualIndex = scrollOffset + index;
56
- const isSelected = actualIndex === clampedIndex;
57
- const isCurrent = branch === currentBranch;
58
- return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▸ ' : ' ' }), _jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : undefined, children: branch }), isCurrent && _jsx(Text, { dimColor: true, children: " (current)" })] }, branch));
59
- })) : inputValue ? (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " No matches. Press Enter to use: " }), _jsx(Text, { color: "yellow", children: inputValue })] })) : (_jsx(Text, { dimColor: true, children: " No candidates found" })) }), filteredCandidates.length > maxListHeight && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [scrollOffset > 0 ? '↑ ' : ' ', scrollOffset + maxListHeight < filteredCandidates.length ? '↓ more' : ''] }) })), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel" }) })] }) }));
60
- }
@@ -1,101 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
- import { Box, Text } from 'ink';
4
- import { UnifiedDiffView } from './UnifiedDiffView.js';
5
- import { CommitPanel } from './CommitPanel.js';
6
- import { ExplorerContentView } from './ExplorerContentView.js';
7
- import { shortenPath } from '../utils/formatPath.js';
8
- import { buildDiffDisplayRows, buildHistoryDisplayRows, buildCompareDisplayRows, getDisplayRowsLineNumWidth, wrapDisplayRows, } from '../utils/displayRows.js';
9
- export function BottomPane({ bottomTab, currentPane, terminalWidth, bottomPaneHeight, diffScrollOffset, currentTheme, diff, selectedFile, stagedCount, onCommit, onCommitCancel, getHeadCommitMessage, onCommitInputFocusChange, historySelectedCommit, historyCommitDiff, compareDiff, compareLoading, compareError, compareListSelection, compareSelectionDiff, wrapMode, explorerSelectedFile = null, explorerFileScrollOffset = 0, showMiddleDots = false, }) {
10
- const isDiffFocused = currentPane !== 'files' &&
11
- currentPane !== 'history' &&
12
- currentPane !== 'compare' &&
13
- currentPane !== 'explorer';
14
- // Build display rows based on current tab
15
- const displayRows = useMemo(() => {
16
- if (bottomTab === 'diff') {
17
- return buildDiffDisplayRows(diff);
18
- }
19
- if (bottomTab === 'history') {
20
- return buildHistoryDisplayRows(historySelectedCommit, historyCommitDiff);
21
- }
22
- if (bottomTab === 'compare') {
23
- // If a specific commit is selected, show that commit's diff
24
- if (compareListSelection?.type === 'commit' && compareSelectionDiff) {
25
- return buildDiffDisplayRows(compareSelectionDiff);
26
- }
27
- // Otherwise show combined compare diff
28
- return buildCompareDisplayRows(compareDiff);
29
- }
30
- return [];
31
- }, [
32
- bottomTab,
33
- diff,
34
- historySelectedCommit,
35
- historyCommitDiff,
36
- compareListSelection,
37
- compareSelectionDiff,
38
- compareDiff,
39
- ]);
40
- // Wrap display rows if wrap mode is enabled
41
- const wrappedRows = useMemo(() => {
42
- if (!wrapMode || displayRows.length === 0)
43
- return displayRows;
44
- // Calculate content width: width - paddingX(1) - lineNum - space(1) - symbol(1) - space(1) - paddingX(1)
45
- const lineNumWidth = getDisplayRowsLineNumWidth(displayRows);
46
- const contentWidth = terminalWidth - lineNumWidth - 5;
47
- return wrapDisplayRows(displayRows, contentWidth, wrapMode);
48
- }, [displayRows, terminalWidth, wrapMode]);
49
- // Build header right-side content
50
- const renderHeaderRight = () => {
51
- if (selectedFile && bottomTab === 'diff') {
52
- return _jsx(Text, { dimColor: true, children: shortenPath(selectedFile.path, terminalWidth - 10) });
53
- }
54
- if (bottomTab === 'history' && historySelectedCommit) {
55
- return (_jsxs(Text, { dimColor: true, children: [historySelectedCommit.shortHash, " - ", historySelectedCommit.message.slice(0, 50)] }));
56
- }
57
- if (bottomTab === 'compare' && compareListSelection) {
58
- if (compareListSelection.type === 'commit') {
59
- const commit = compareDiff?.commits[compareListSelection.index];
60
- return (_jsxs(Text, { dimColor: true, children: [commit?.shortHash ?? '', " - ", commit?.message.slice(0, 40) ?? ''] }));
61
- }
62
- else {
63
- const path = compareDiff?.files[compareListSelection.index]?.path ?? '';
64
- return _jsx(Text, { dimColor: true, children: shortenPath(path, terminalWidth - 10) });
65
- }
66
- }
67
- if (bottomTab === 'explorer' && explorerSelectedFile) {
68
- return _jsx(Text, { dimColor: true, children: shortenPath(explorerSelectedFile.path, terminalWidth - 10) });
69
- }
70
- return null;
71
- };
72
- // Render content based on tab
73
- const renderContent = () => {
74
- // Commit tab is special - not a diff view
75
- if (bottomTab === 'commit') {
76
- return (_jsx(CommitPanel, { isActive: currentPane === 'commit', stagedCount: stagedCount, onCommit: onCommit, onCancel: onCommitCancel, getHeadMessage: getHeadCommitMessage, onInputFocusChange: onCommitInputFocusChange }));
77
- }
78
- // Compare tab loading/error states
79
- if (bottomTab === 'compare') {
80
- if (compareLoading) {
81
- return _jsx(Text, { dimColor: true, children: "Loading compare diff..." });
82
- }
83
- if (compareError) {
84
- return _jsx(Text, { color: "red", children: compareError });
85
- }
86
- if (!compareDiff) {
87
- return _jsx(Text, { dimColor: true, children: "No base branch found (no origin/main or origin/master)" });
88
- }
89
- if (compareDiff.files.length === 0) {
90
- return _jsxs(Text, { dimColor: true, children: ["No changes compared to ", compareDiff.baseBranch] });
91
- }
92
- }
93
- // All diff views use UnifiedDiffView
94
- return (_jsx(UnifiedDiffView, { rows: wrappedRows, maxHeight: bottomPaneHeight - 1, scrollOffset: diffScrollOffset, theme: currentTheme, width: terminalWidth, wrapMode: wrapMode }));
95
- };
96
- // Explorer tab content
97
- if (bottomTab === 'explorer') {
98
- return (_jsxs(Box, { flexDirection: "column", height: bottomPaneHeight, width: terminalWidth, overflowY: "hidden", children: [_jsxs(Box, { width: terminalWidth, children: [_jsx(Text, { bold: true, color: isDiffFocused ? 'cyan' : undefined, children: "FILE" }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: renderHeaderRight() })] }), _jsx(ExplorerContentView, { filePath: explorerSelectedFile?.path ?? null, content: explorerSelectedFile?.content ?? null, maxHeight: bottomPaneHeight - 1, scrollOffset: explorerFileScrollOffset, truncated: explorerSelectedFile?.truncated, wrapMode: wrapMode, width: terminalWidth, showMiddleDots: showMiddleDots })] }));
99
- }
100
- return (_jsxs(Box, { flexDirection: "column", height: bottomPaneHeight, width: terminalWidth, overflowX: "hidden", overflowY: "hidden", children: [_jsxs(Box, { width: terminalWidth, children: [_jsx(Text, { bold: true, color: isDiffFocused ? 'cyan' : undefined, children: bottomTab === 'commit' ? 'COMMIT' : 'DIFF' }), _jsx(Box, { flexGrow: 1, justifyContent: "flex-end", children: renderHeaderRight() })] }), renderContent()] }));
101
- }
@@ -1,58 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect } from 'react';
3
- import { Box, Text, useInput } from 'ink';
4
- import TextInput from 'ink-text-input';
5
- import { useCommitFlow } from '../hooks/useCommitFlow.js';
6
- export function CommitPanel({ isActive, stagedCount, onCommit, onCancel, getHeadMessage, onInputFocusChange, }) {
7
- const { message, amend, isCommitting, error, inputFocused, setMessage, toggleAmend, setInputFocused, handleSubmit, } = useCommitFlow({
8
- stagedCount,
9
- onCommit,
10
- onSuccess: onCancel,
11
- getHeadMessage,
12
- });
13
- // Notify parent of focus state changes
14
- useEffect(() => {
15
- onInputFocusChange?.(inputFocused);
16
- }, [inputFocused, onInputFocusChange]);
17
- // Keyboard handling
18
- useInput((input, key) => {
19
- if (!isActive)
20
- return;
21
- // When input is focused, Escape unfocuses it (but stays on commit tab)
22
- // When input is unfocused, Escape cancels and goes back to diff
23
- if (key.escape) {
24
- if (inputFocused) {
25
- setInputFocused(false);
26
- }
27
- else {
28
- onCancel();
29
- }
30
- return;
31
- }
32
- // When input is unfocused, allow refocusing with 'i' or Enter
33
- if (!inputFocused) {
34
- if (input === 'i' || key.return) {
35
- setInputFocused(true);
36
- return;
37
- }
38
- // Toggle amend with 'a' when unfocused
39
- if (input === 'a') {
40
- toggleAmend();
41
- return;
42
- }
43
- return; // Don't handle other keys - let them bubble up to useKeymap
44
- }
45
- // When input is focused, only handle special keys
46
- // Toggle amend with 'a' when message is empty
47
- if (input === 'a' && !message) {
48
- toggleAmend();
49
- return;
50
- }
51
- }, { isActive });
52
- if (!isActive) {
53
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Press '2' or 'c' to open commit panel" }) }));
54
- }
55
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Commit Message" }), amend && _jsx(Text, { color: "yellow", children: " (amending)" })] }), _jsx(Box, { borderStyle: "round", borderColor: inputFocused ? 'cyan' : undefined, paddingX: 1, children: inputFocused ? (_jsx(TextInput, { value: message, onChange: setMessage, onSubmit: handleSubmit, placeholder: "Enter commit message..." })) : (_jsx(Text, { dimColor: !message, children: message || 'Press i or Enter to edit...' })) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { color: amend ? 'green' : 'gray', children: ["[", amend ? 'x' : ' ', "] Amend"] }), _jsx(Text, { dimColor: true, children: "(a)" })] }), error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: error }) })), isCommitting && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Committing..." }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Staged: ", stagedCount, " file(s) |", ' ', inputFocused
56
- ? 'Enter: commit | Esc: unfocus'
57
- : 'i/Enter: edit | Esc: cancel | 1/3: switch tab'] }) })] }));
58
- }
@@ -1,110 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useMemo } from 'react';
3
- import { Box, Text } from 'ink';
4
- import { shortenPath } from '../utils/formatPath.js';
5
- import { formatDate } from '../utils/formatDate.js';
6
- import { formatCommitDisplay } from '../utils/commitFormat.js';
7
- // Re-export from utils for backwards compatibility
8
- export { getCompareItemIndexFromRow } from '../utils/rowCalculations.js';
9
- function CommitRow({ commit, isSelected, isActive, width, }) {
10
- const dateStr = formatDate(commit.date);
11
- // Fixed parts: indent(2) + hash(7) + spaces(4) + date + parens(2)
12
- const baseWidth = 2 + 7 + 4 + dateStr.length + 2;
13
- const remainingWidth = width - baseWidth;
14
- const { displayMessage, displayRefs } = formatCommitDisplay(commit.message, commit.refs, remainingWidth);
15
- return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: commit.shortHash }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected && isActive ? 'cyan' : undefined, bold: isSelected && isActive, inverse: isSelected && isActive, children: displayMessage }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["(", dateStr, ")"] }), displayRefs && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "green", children: displayRefs })] }))] }));
16
- }
17
- function FileRow({ file, isSelected, isActive, maxPathLength, }) {
18
- const statusColors = {
19
- added: 'green',
20
- modified: 'yellow',
21
- deleted: 'red',
22
- renamed: 'blue',
23
- };
24
- const statusChars = {
25
- added: 'A',
26
- modified: 'M',
27
- deleted: 'D',
28
- renamed: 'R',
29
- };
30
- const isUncommitted = file.isUncommitted ?? false;
31
- // Account for stats: " (+123 -456)" and possible "[uncommitted]"
32
- const statsLength = 5 + String(file.additions).length + String(file.deletions).length;
33
- const uncommittedLength = isUncommitted ? 14 : 0;
34
- const availableForPath = maxPathLength - statsLength - uncommittedLength;
35
- return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), isUncommitted && (_jsx(Text, { color: "magenta", bold: true, children: "*" })), _jsx(Text, { color: isUncommitted ? 'magenta' : statusColors[file.status], bold: true, children: statusChars[file.status] }), _jsxs(Text, { bold: isSelected && isActive, color: isSelected && isActive ? 'cyan' : isUncommitted ? 'magenta' : undefined, inverse: isSelected && isActive, children: [' ', shortenPath(file.path, availableForPath)] }), _jsx(Text, { dimColor: true, children: " (" }), _jsxs(Text, { color: "green", children: ["+", file.additions] }), _jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { color: "red", children: ["-", file.deletions] }), _jsx(Text, { dimColor: true, children: ")" }), isUncommitted && (_jsxs(Text, { color: "magenta", dimColor: true, children: [' ', "[uncommitted]"] }))] }));
36
- }
37
- export function CompareListView({ commits, files, selectedItem, scrollOffset, maxHeight, isActive, width, }) {
38
- // Note: expand/collapse functionality is prepared but not exposed yet
39
- const commitsExpanded = true;
40
- const filesExpanded = true;
41
- // Build flat list of rows
42
- const rows = useMemo(() => {
43
- const result = [];
44
- // Commits section
45
- if (commits.length > 0) {
46
- result.push({ type: 'section-header', sectionType: 'commits' });
47
- if (commitsExpanded) {
48
- commits.forEach((commit, i) => {
49
- result.push({ type: 'commit', commitIndex: i, commit });
50
- });
51
- }
52
- }
53
- // Files section
54
- if (files.length > 0) {
55
- if (commits.length > 0) {
56
- result.push({ type: 'spacer' });
57
- }
58
- result.push({ type: 'section-header', sectionType: 'files' });
59
- if (filesExpanded) {
60
- files.forEach((file, i) => {
61
- result.push({ type: 'file', fileIndex: i, file });
62
- });
63
- }
64
- }
65
- return result;
66
- }, [commits, files, commitsExpanded, filesExpanded]);
67
- const visibleRows = rows.slice(scrollOffset, scrollOffset + maxHeight);
68
- if (commits.length === 0 && files.length === 0) {
69
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No changes compared to base branch" }) }));
70
- }
71
- return (_jsx(Box, { flexDirection: "column", children: visibleRows.map((row, i) => {
72
- const key = `row-${scrollOffset + i}`;
73
- if (row.type === 'section-header') {
74
- const isCommits = row.sectionType === 'commits';
75
- const expanded = isCommits ? commitsExpanded : filesExpanded;
76
- const count = isCommits ? commits.length : files.length;
77
- const label = isCommits ? 'Commits' : 'Files';
78
- return (_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: "cyan", children: [expanded ? '▼' : '▶', " ", label] }), _jsxs(Text, { dimColor: true, children: [" (", count, ")"] })] }, key));
79
- }
80
- if (row.type === 'spacer') {
81
- return _jsx(Text, { children: " " }, key);
82
- }
83
- if (row.type === 'commit' && row.commit !== undefined && row.commitIndex !== undefined) {
84
- const isSelected = selectedItem?.type === 'commit' && selectedItem.index === row.commitIndex;
85
- return (_jsx(CommitRow, { commit: row.commit, isSelected: isSelected, isActive: isActive, width: width }, key));
86
- }
87
- if (row.type === 'file' && row.file !== undefined && row.fileIndex !== undefined) {
88
- const isSelected = selectedItem?.type === 'file' && selectedItem.index === row.fileIndex;
89
- return (_jsx(FileRow, { file: row.file, isSelected: isSelected, isActive: isActive, maxPathLength: width - 5 }, key));
90
- }
91
- return null;
92
- }) }));
93
- }
94
- // Helper to get total row count for scrolling
95
- export function getCompareListTotalRows(commits, files, commitsExpanded = true, filesExpanded = true) {
96
- let count = 0;
97
- if (commits.length > 0) {
98
- count += 1; // header
99
- if (commitsExpanded)
100
- count += commits.length;
101
- }
102
- if (files.length > 0) {
103
- if (commits.length > 0)
104
- count += 1; // spacer
105
- count += 1; // header
106
- if (filesExpanded)
107
- count += files.length;
108
- }
109
- return count;
110
- }