diffwatch 1.0.6 → 1.0.9
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.
- package/AGENTS.md +201 -0
- package/dist/index.js +207 -70
- package/dist/index.js.map +1 -1
- package/dist/utils/git.d.ts +1 -0
- package/dist/utils/git.js +11 -0
- package/dist/utils/git.js.map +1 -1
- package/package.json +1 -1
- package/screenshot.jpg +0 -0
- package/src/index.ts +443 -290
- package/src/utils/git.ts +9 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# DiffWatch Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This document provides coding standards and operational guidelines for agentic coding assistants working on the DiffWatch project.
|
|
4
|
+
|
|
5
|
+
## Build, Lint, and Test Commands
|
|
6
|
+
|
|
7
|
+
### Primary Commands
|
|
8
|
+
- **Build**: `npm run build` - Compiles TypeScript to JavaScript using `tsc`
|
|
9
|
+
- **Test**: `npm test` - Runs Jest test suite
|
|
10
|
+
- **Dev**: `npm run dev` - Runs the application in development mode with `ts-node`
|
|
11
|
+
- **Start**: `npm start` - Runs the compiled application from `dist/`
|
|
12
|
+
|
|
13
|
+
### Testing Specific Files
|
|
14
|
+
- Run a single test file: `npm test -- tests/git.test.ts`
|
|
15
|
+
- Run tests matching a pattern: `npm test -- --testNamePattern="should return file status"`
|
|
16
|
+
|
|
17
|
+
### Linting and Type Checking
|
|
18
|
+
- **Type Check**: `npx tsc --noEmit` - TypeScript type checking without compilation
|
|
19
|
+
- **No explicit linting configured** - TypeScript strict mode serves as the primary code quality gate
|
|
20
|
+
|
|
21
|
+
## Code Style Guidelines
|
|
22
|
+
|
|
23
|
+
### Language and Module System
|
|
24
|
+
- **Language**: TypeScript with strict mode enabled
|
|
25
|
+
- **Target**: ES6 JavaScript output
|
|
26
|
+
- **Module System**: CommonJS output (`"type": "commonjs"` in package.json)
|
|
27
|
+
- **Source**: ES6 modules with `import`/`export` syntax
|
|
28
|
+
|
|
29
|
+
### Import/Export Style
|
|
30
|
+
```typescript
|
|
31
|
+
// ES6 imports at the top of files
|
|
32
|
+
import { simpleGit, SimpleGit, StatusResult } from 'simple-git';
|
|
33
|
+
import fs from 'fs/promises';
|
|
34
|
+
import chalk from 'chalk';
|
|
35
|
+
|
|
36
|
+
// Named exports for utilities
|
|
37
|
+
export interface FileStatus {
|
|
38
|
+
path: string;
|
|
39
|
+
status: 'modified' | 'added' | 'deleted' | 'unstaged' | 'unknown' | 'unchanged';
|
|
40
|
+
mtime?: Date;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Default export for main classes
|
|
44
|
+
export class GitHandler {
|
|
45
|
+
// ...
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Naming Conventions
|
|
50
|
+
- **Variables/Functions**: camelCase (`filePath`, `getStatus`, `updateFileList`)
|
|
51
|
+
- **Classes/Interfaces**: PascalCase (`GitHandler`, `FileStatus`)
|
|
52
|
+
- **Constants**: UPPER_CASE (not commonly used in this codebase)
|
|
53
|
+
- **Files**: kebab-case for directories (`diff-formatter.ts`), camelCase for utilities
|
|
54
|
+
- **Test Files**: Match source filename with `.test.ts` extension (`git.test.ts`)
|
|
55
|
+
|
|
56
|
+
### Code Structure and Patterns
|
|
57
|
+
- **Async/Await**: Preferred over Promises for asynchronous operations
|
|
58
|
+
- **Error Handling**: Try-catch blocks with descriptive error messages
|
|
59
|
+
- **Type Safety**: Use interfaces for data structures, avoid `any` type
|
|
60
|
+
- **Class Design**: Private properties with public methods, constructor injection
|
|
61
|
+
- **Functional Programming**: Pure functions where possible, avoid side effects
|
|
62
|
+
|
|
63
|
+
### Formatting and Style
|
|
64
|
+
- **Indentation**: 2 spaces (no tabs)
|
|
65
|
+
- **Semicolons**: Not used (TypeScript ASI)
|
|
66
|
+
- **Quotes**: Single quotes for strings (`'string'`)
|
|
67
|
+
- **Line Length**: No strict limit, break long lines naturally
|
|
68
|
+
- **Blank Lines**: Use sparingly between logical code blocks
|
|
69
|
+
- **Comments**: Minimal comments, rely on descriptive naming
|
|
70
|
+
- **Braces**: Same line for functions/classes, new line for control structures
|
|
71
|
+
|
|
72
|
+
### Example Code Style
|
|
73
|
+
```typescript
|
|
74
|
+
export class GitHandler {
|
|
75
|
+
private git: SimpleGit;
|
|
76
|
+
|
|
77
|
+
constructor(workingDir: string = process.cwd()) {
|
|
78
|
+
this.git = simpleGit(workingDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getStatus(): Promise<FileStatus[]> {
|
|
82
|
+
const status: StatusResult = await this.git.status();
|
|
83
|
+
const uniqueFiles = new Map<string, FileStatus>();
|
|
84
|
+
|
|
85
|
+
status.modified.forEach(path => {
|
|
86
|
+
uniqueFiles.set(path, { path, status: 'modified' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Process and return sorted results
|
|
90
|
+
const fileArray = Array.from(uniqueFiles.values());
|
|
91
|
+
return fileArray.sort((a, b) => {
|
|
92
|
+
const timeDiff = (b.mtime || new Date(0)).getTime() - (a.mtime || new Date(0)).getTime();
|
|
93
|
+
return timeDiff !== 0 ? timeDiff : a.path.localeCompare(b.path);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Error Handling Patterns
|
|
100
|
+
```typescript
|
|
101
|
+
async getFileContent(filePath: string): Promise<string> {
|
|
102
|
+
try {
|
|
103
|
+
return await fs.readFile(filePath, 'utf8');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return `Error reading file: ${error}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async isRepo(): Promise<boolean> {
|
|
110
|
+
try {
|
|
111
|
+
return await this.git.checkIsRepo();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Testing Guidelines
|
|
119
|
+
- **Framework**: Jest with ts-jest for TypeScript support
|
|
120
|
+
- **Mocking**: Mock external dependencies (simple-git, fs/promises)
|
|
121
|
+
- **Test Structure**: `describe` blocks for classes, `it` blocks for methods
|
|
122
|
+
- **Assertions**: Use Jest matchers (`expect().toBe()`, `expect().toEqual()`)
|
|
123
|
+
- **Test Naming**: Descriptive sentences starting with "should"
|
|
124
|
+
- **Coverage**: Aim for critical path coverage, especially Git operations
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
describe('GitHandler', () => {
|
|
128
|
+
let gitHandler: GitHandler;
|
|
129
|
+
let mockGit: any;
|
|
130
|
+
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
mockGit = { checkIsRepo: jest.fn() };
|
|
133
|
+
(simpleGit as jest.Mock).mockReturnValue(mockGit);
|
|
134
|
+
gitHandler = new GitHandler();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return true if directory is a repo', async () => {
|
|
138
|
+
mockGit.checkIsRepo.mockResolvedValue(true);
|
|
139
|
+
const result = await gitHandler.isRepo();
|
|
140
|
+
expect(result).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### File Organization
|
|
146
|
+
- **Source Code**: `src/` directory with subdirectories for utilities
|
|
147
|
+
- **Tests**: `tests/` directory mirroring source structure
|
|
148
|
+
- **Build Output**: `dist/` directory (gitignored)
|
|
149
|
+
- **Configuration**: Root level for `package.json`, `tsconfig.json`, `jest.config.js`
|
|
150
|
+
|
|
151
|
+
### TypeScript Configuration
|
|
152
|
+
- **Strict Mode**: Enabled for type safety
|
|
153
|
+
- **Source Maps**: Enabled for debugging
|
|
154
|
+
- **Declaration Files**: Generated for library usage
|
|
155
|
+
- **Module Resolution**: Node.js style
|
|
156
|
+
- **ES Module Interop**: Enabled for compatibility
|
|
157
|
+
|
|
158
|
+
### Dependencies
|
|
159
|
+
- **Runtime**: chalk (colors), simple-git (Git operations), neo-neo-blessed (TUI)
|
|
160
|
+
- **Dev**: Jest, ts-jest, ts-node, TypeScript
|
|
161
|
+
- **Keep dependencies minimal** - only add when necessary for functionality
|
|
162
|
+
|
|
163
|
+
### Git and Version Control
|
|
164
|
+
- **Branching**: Feature branches for development
|
|
165
|
+
- **Commits**: Descriptive messages focusing on "why" rather than "what"
|
|
166
|
+
- **Tags**: Semantic versioning (current: 1.0.8)
|
|
167
|
+
- **Hooks**: No custom hooks configured
|
|
168
|
+
|
|
169
|
+
### Security Considerations
|
|
170
|
+
- **Input Validation**: Validate file paths and user inputs
|
|
171
|
+
- **Error Messages**: Avoid exposing sensitive system information
|
|
172
|
+
- **Dependencies**: Keep updated, audit regularly
|
|
173
|
+
- **Secrets**: Never commit sensitive data
|
|
174
|
+
|
|
175
|
+
### Performance Guidelines
|
|
176
|
+
- **Async Operations**: Use efficient patterns, avoid blocking operations
|
|
177
|
+
- **Memory Management**: Clean up resources, avoid memory leaks
|
|
178
|
+
- **File I/O**: Handle large files appropriately
|
|
179
|
+
- **Real-time Updates**: Debounce rapid operations (150ms in current code)
|
|
180
|
+
|
|
181
|
+
### Code Review Checklist
|
|
182
|
+
- [ ] TypeScript compiles without errors
|
|
183
|
+
- [ ] Tests pass and cover new functionality
|
|
184
|
+
- [ ] Follows established naming and style conventions
|
|
185
|
+
- [ ] Error handling appropriate for use case
|
|
186
|
+
- [ ] No console.log statements in production code
|
|
187
|
+
- [ ] Dependencies justified and minimal
|
|
188
|
+
- [ ] Documentation updated if public API changes</content>
|
|
189
|
+
<parameter name="filePath">D:\SystemFolders\Downloads\Dev\node\diffwatch\AGENTS.md
|
|
190
|
+
|
|
191
|
+
<!-- GENERAL-RULES-RULES-START -->
|
|
192
|
+
|
|
193
|
+
## You must always follow these rules:
|
|
194
|
+
|
|
195
|
+
- If you are unsure about any requirement, behavior, or implementation detail, ask clarifying questions **before** writing code.
|
|
196
|
+
- At every step, provide a **high-level explanation** of what changes were made and why.
|
|
197
|
+
- After implementing changes or new features, always provide a list of **suggestions or improvements**, even if they differ from the user's original request.
|
|
198
|
+
- If the user requests a change or feature that is an **anti-pattern** or violates well-established best practices, clearly explain the issue and ask for confirmation before proceeding.
|
|
199
|
+
- Always use Context7 MCP when I need library/API documentation, code generation, setup or configuration steps without me having to explicitly ask.
|
|
200
|
+
|
|
201
|
+
<!-- GENERAL-RULES-RULES-END -->
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
19
|
const git_1 = require("./utils/git");
|
|
20
20
|
const diff_formatter_1 = require("./utils/diff-formatter");
|
|
21
|
+
const fs_1 = require("fs");
|
|
22
|
+
const path_1 = require("path");
|
|
23
|
+
const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)((0, path_1.dirname)(__filename), '..', 'package.json'), 'utf-8'));
|
|
21
24
|
function main() {
|
|
22
25
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23
26
|
const args = process.argv.slice(2);
|
|
@@ -31,14 +34,22 @@ Arguments:
|
|
|
31
34
|
|
|
32
35
|
Options:
|
|
33
36
|
-h, --help Show help information
|
|
37
|
+
-v, --version Show version information
|
|
34
38
|
`);
|
|
35
39
|
process.exit(0);
|
|
36
40
|
};
|
|
41
|
+
const showVersion = () => {
|
|
42
|
+
console.log(`diffwatch v${packageJson.version}`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
};
|
|
37
45
|
const positionalArgs = [];
|
|
38
46
|
for (let i = 0; i < args.length; i++) {
|
|
39
47
|
if (args[i] === '-h' || args[i] === '--help') {
|
|
40
48
|
showHelp();
|
|
41
49
|
}
|
|
50
|
+
else if (args[i] === '-v' || args[i] === '--version') {
|
|
51
|
+
showVersion();
|
|
52
|
+
}
|
|
42
53
|
else if (args[i].startsWith('-')) {
|
|
43
54
|
// Ignore unknown flags or handle them if needed
|
|
44
55
|
}
|
|
@@ -72,7 +83,7 @@ Options:
|
|
|
72
83
|
});
|
|
73
84
|
// Explicitly enable mouse tracking
|
|
74
85
|
screen.program.enableMouse();
|
|
75
|
-
|
|
86
|
+
let fileList = blessed.list({
|
|
76
87
|
top: 0,
|
|
77
88
|
left: 0,
|
|
78
89
|
width: '30%',
|
|
@@ -93,6 +104,38 @@ Options:
|
|
|
93
104
|
},
|
|
94
105
|
border: { type: 'line' },
|
|
95
106
|
});
|
|
107
|
+
const createFileList = () => {
|
|
108
|
+
const newList = blessed.list({
|
|
109
|
+
top: 0,
|
|
110
|
+
left: 0,
|
|
111
|
+
width: '30%',
|
|
112
|
+
height: '99%',
|
|
113
|
+
label: 'Files (0)',
|
|
114
|
+
keys: true,
|
|
115
|
+
vi: true,
|
|
116
|
+
mouse: true,
|
|
117
|
+
tags: true,
|
|
118
|
+
scrollbar: {
|
|
119
|
+
ch: ' ',
|
|
120
|
+
track: { bg: 'white' },
|
|
121
|
+
style: { bg: 'blue' },
|
|
122
|
+
},
|
|
123
|
+
style: {
|
|
124
|
+
selected: { fg: 'black', bg: 'white' },
|
|
125
|
+
border: { fg: 'white' },
|
|
126
|
+
},
|
|
127
|
+
border: { type: 'line' },
|
|
128
|
+
});
|
|
129
|
+
newList.on('select item', () => {
|
|
130
|
+
scheduleDiffUpdate();
|
|
131
|
+
});
|
|
132
|
+
newList.key(['up', 'down'], () => {
|
|
133
|
+
scheduleDiffUpdate();
|
|
134
|
+
});
|
|
135
|
+
newList.on('wheeldown', () => handleScroll('down'));
|
|
136
|
+
newList.on('wheelup', () => handleScroll('up'));
|
|
137
|
+
return newList;
|
|
138
|
+
};
|
|
96
139
|
const diffView = blessed.scrollabletext({
|
|
97
140
|
top: 0,
|
|
98
141
|
left: '30%',
|
|
@@ -115,7 +158,7 @@ Options:
|
|
|
115
158
|
});
|
|
116
159
|
const searchBox = blessed.box({
|
|
117
160
|
top: 'center',
|
|
118
|
-
left: '
|
|
161
|
+
left: '40%', // Center of right pane (30% + 70%/2 = 65%)
|
|
119
162
|
width: '50%',
|
|
120
163
|
height: 3,
|
|
121
164
|
label: ' Search ',
|
|
@@ -136,11 +179,11 @@ Options:
|
|
|
136
179
|
// Confirmation dialog for revert
|
|
137
180
|
const confirmDialog = blessed.box({
|
|
138
181
|
top: 'center',
|
|
139
|
-
left: '
|
|
182
|
+
left: '45%', // Center of right pane (30% + 70%/2 = 65%)
|
|
140
183
|
width: '38%',
|
|
141
184
|
label: ' Confirm Revert ',
|
|
142
185
|
height: 3,
|
|
143
|
-
content: 'Press
|
|
186
|
+
content: 'Press SPACE key to confirm revert or ESC to cancel.',
|
|
144
187
|
border: { type: 'line' },
|
|
145
188
|
style: {
|
|
146
189
|
fg: 'yellow',
|
|
@@ -149,21 +192,70 @@ Options:
|
|
|
149
192
|
},
|
|
150
193
|
hidden: true,
|
|
151
194
|
});
|
|
195
|
+
const notificationBox = blessed.box({
|
|
196
|
+
top: 'center',
|
|
197
|
+
left: '45%', // Center of right pane (30% + 70%/2 = 65%)
|
|
198
|
+
width: '38%',
|
|
199
|
+
height: 3,
|
|
200
|
+
label: ' Notification ',
|
|
201
|
+
border: { type: 'line' },
|
|
202
|
+
style: {
|
|
203
|
+
fg: 'green',
|
|
204
|
+
bg: 'black',
|
|
205
|
+
border: { fg: 'green' }
|
|
206
|
+
},
|
|
207
|
+
hidden: true,
|
|
208
|
+
});
|
|
209
|
+
const showNotification = (message, color = 'green') => {
|
|
210
|
+
const previouslyFocused = screen.focused;
|
|
211
|
+
notificationBox.setContent(message);
|
|
212
|
+
notificationBox.style = {
|
|
213
|
+
fg: color,
|
|
214
|
+
bg: 'black',
|
|
215
|
+
border: { fg: color }
|
|
216
|
+
};
|
|
217
|
+
notificationBox.show();
|
|
218
|
+
notificationBox.setFront();
|
|
219
|
+
screen.render();
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
notificationBox.hide();
|
|
222
|
+
if (previouslyFocused && previouslyFocused !== notificationBox) {
|
|
223
|
+
previouslyFocused.focus();
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
fileList.focus();
|
|
227
|
+
}
|
|
228
|
+
updateBorders();
|
|
229
|
+
screen.render();
|
|
230
|
+
}, 3000);
|
|
231
|
+
};
|
|
232
|
+
// Branch display in bottom right
|
|
233
|
+
const branchBox = blessed.box({
|
|
234
|
+
bottom: 0,
|
|
235
|
+
right: 0,
|
|
236
|
+
width: 'shrink',
|
|
237
|
+
height: 1,
|
|
238
|
+
align: 'left',
|
|
239
|
+
content: chalk_1.default.cyan('Branch: '),
|
|
240
|
+
style: {
|
|
241
|
+
fg: 'white',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
152
244
|
// Footer box to show shortcuts - aligned with the panes
|
|
153
245
|
const footer = blessed.box({
|
|
154
246
|
bottom: 0,
|
|
155
247
|
left: 0,
|
|
156
248
|
width: '100%',
|
|
157
249
|
height: 1,
|
|
158
|
-
content: chalk_1.default.
|
|
250
|
+
content: chalk_1.default.green('←→') + ' Switch |' + chalk_1.default.green(' ⏎') + ' Open | ' + chalk_1.default.green('S') + ' Search | ' + chalk_1.default.green('R') + ' Revert | ' + chalk_1.default.green('Q') + ' Quit ',
|
|
159
251
|
});
|
|
160
|
-
// Adjust footer to align with the panes
|
|
161
|
-
// Note: We'll adjust this after screen initialization
|
|
162
252
|
screen.append(fileList);
|
|
163
253
|
screen.append(diffView);
|
|
164
254
|
screen.append(searchBox);
|
|
255
|
+
screen.append(notificationBox);
|
|
165
256
|
screen.append(confirmDialog);
|
|
166
257
|
screen.append(footer);
|
|
258
|
+
screen.append(branchBox);
|
|
167
259
|
const updateBorders = () => {
|
|
168
260
|
fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
|
|
169
261
|
diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
|
|
@@ -173,6 +265,9 @@ Options:
|
|
|
173
265
|
let lastSelectedPath = null;
|
|
174
266
|
let diffUpdateTimeout = null;
|
|
175
267
|
let currentSearchTerm = '';
|
|
268
|
+
let currentBranch = '';
|
|
269
|
+
let lastFilePaths = [];
|
|
270
|
+
let isUpdatingList = false;
|
|
176
271
|
const scheduleDiffUpdate = () => {
|
|
177
272
|
if (diffUpdateTimeout)
|
|
178
273
|
clearTimeout(diffUpdateTimeout);
|
|
@@ -181,17 +276,18 @@ Options:
|
|
|
181
276
|
}), 150); // 150ms debounce
|
|
182
277
|
};
|
|
183
278
|
const handleScroll = (direction) => {
|
|
184
|
-
|
|
279
|
+
const focused = screen.focused;
|
|
280
|
+
if (focused === fileList) {
|
|
185
281
|
if (direction === 'up') {
|
|
186
|
-
|
|
282
|
+
focused.up(1);
|
|
187
283
|
}
|
|
188
284
|
else {
|
|
189
|
-
|
|
285
|
+
focused.down(1);
|
|
190
286
|
}
|
|
191
287
|
scheduleDiffUpdate();
|
|
192
288
|
screen.render();
|
|
193
289
|
}
|
|
194
|
-
else if (
|
|
290
|
+
else if (focused === diffView) {
|
|
195
291
|
const scrollAmount = direction === 'up' ? -2 : 2;
|
|
196
292
|
diffView.scroll(scrollAmount);
|
|
197
293
|
screen.render();
|
|
@@ -205,9 +301,9 @@ Options:
|
|
|
205
301
|
// Attach custom scroll handlers to widgets (captures wheel even if hovering specific widget)
|
|
206
302
|
// We use widget-level listeners now that screen.mouse is true.
|
|
207
303
|
// We attach to both to ensure the event is caught regardless of where the mouse is.
|
|
304
|
+
// We use widget-level listeners now that screen.mouse is true.
|
|
305
|
+
// We attach to both to ensure the event is caught regardless of where the mouse is.
|
|
208
306
|
// The handleScroll function will then decide WHAT to scroll based on focus.
|
|
209
|
-
fileList.on('wheeldown', () => handleScroll('down'));
|
|
210
|
-
fileList.on('wheelup', () => handleScroll('up'));
|
|
211
307
|
diffView.on('wheeldown', () => handleScroll('down'));
|
|
212
308
|
diffView.on('wheelup', () => handleScroll('up'));
|
|
213
309
|
// Also listen on screen for events that might miss the widgets (margins, borders)
|
|
@@ -229,9 +325,25 @@ Options:
|
|
|
229
325
|
console.error(`Failed to open ${filePath}: ${error}`);
|
|
230
326
|
}
|
|
231
327
|
};
|
|
328
|
+
const updateBranch = () => __awaiter(this, void 0, void 0, function* () {
|
|
329
|
+
const branch = yield gitHandler.getBranch();
|
|
330
|
+
if (branch !== currentBranch) {
|
|
331
|
+
currentBranch = branch;
|
|
332
|
+
branchBox.setContent(chalk_1.default.cyan('Branch: ') + chalk_1.default.yellow(branch));
|
|
333
|
+
screen.render();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
232
336
|
const updateDiff = () => __awaiter(this, void 0, void 0, function* () {
|
|
233
337
|
const selectedIndex = fileList.selected;
|
|
234
338
|
const selectedFile = currentFiles[selectedIndex];
|
|
339
|
+
if (!selectedFile) {
|
|
340
|
+
// No valid file selected, clear diff view
|
|
341
|
+
diffView.setContent('Select a file to view diff.');
|
|
342
|
+
diffView.setLabel(' Diff () ');
|
|
343
|
+
lastSelectedPath = null;
|
|
344
|
+
screen.render();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
235
347
|
if (selectedFile) {
|
|
236
348
|
let content = '';
|
|
237
349
|
let label = ` Diff (${selectedFile.path}) `;
|
|
@@ -283,9 +395,10 @@ Options:
|
|
|
283
395
|
});
|
|
284
396
|
const updateFileList = () => __awaiter(this, void 0, void 0, function* () {
|
|
285
397
|
var _a;
|
|
286
|
-
|
|
398
|
+
if (isUpdatingList)
|
|
399
|
+
return;
|
|
400
|
+
isUpdatingList = true;
|
|
287
401
|
const selectedPath = (_a = currentFiles[fileList.selected]) === null || _a === void 0 ? void 0 : _a.path;
|
|
288
|
-
const diffScroll = diffView.scrollTop;
|
|
289
402
|
let files;
|
|
290
403
|
if (currentSearchTerm) {
|
|
291
404
|
files = yield gitHandler.searchFiles(currentSearchTerm);
|
|
@@ -294,7 +407,7 @@ Options:
|
|
|
294
407
|
files = yield gitHandler.getStatus();
|
|
295
408
|
}
|
|
296
409
|
currentFiles = files;
|
|
297
|
-
const items = files.map(f => {
|
|
410
|
+
const items = files.map((f) => {
|
|
298
411
|
let color = '{white-fg}';
|
|
299
412
|
if (f.status === 'added')
|
|
300
413
|
color = '{green-fg}';
|
|
@@ -308,59 +421,49 @@ Options:
|
|
|
308
421
|
color = '{grey-fg}';
|
|
309
422
|
return `${color}${f.path}{/}`;
|
|
310
423
|
});
|
|
311
|
-
fileList.setItems(items);
|
|
312
424
|
const labelTitle = currentSearchTerm ? `Files (${files.length}) - Searching: "${currentSearchTerm}"` : `Files (${files.length})`;
|
|
313
|
-
|
|
425
|
+
const newFilePaths = files.map(f => f.path);
|
|
426
|
+
const selectedFileStillExists = selectedPath ? newFilePaths.includes(selectedPath) : false;
|
|
427
|
+
const listChanged = lastFilePaths.length !== newFilePaths.length ||
|
|
428
|
+
lastFilePaths.some(path => !newFilePaths.includes(path)) ||
|
|
429
|
+
newFilePaths.some(path => !lastFilePaths.includes(path));
|
|
314
430
|
if (items.length > 0) {
|
|
315
|
-
// Restore selection by path if possible
|
|
316
431
|
const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
|
|
317
|
-
|
|
318
|
-
|
|
432
|
+
const oldFileList = fileList;
|
|
433
|
+
const newFileList = createFileList();
|
|
434
|
+
newFileList.setLabel(labelTitle);
|
|
435
|
+
newFileList.setItems(items);
|
|
436
|
+
newFileList.select(newSelectedIndex >= 0 ? newSelectedIndex : 0);
|
|
437
|
+
if (!selectedFileStillExists || listChanged) {
|
|
438
|
+
lastSelectedPath = null;
|
|
439
|
+
}
|
|
440
|
+
lastFilePaths = newFilePaths;
|
|
319
441
|
if (diffUpdateTimeout) {
|
|
320
442
|
clearTimeout(diffUpdateTimeout);
|
|
321
443
|
diffUpdateTimeout = null;
|
|
322
444
|
}
|
|
323
445
|
yield updateDiff();
|
|
446
|
+
oldFileList.destroy();
|
|
447
|
+
screen.remove(oldFileList);
|
|
448
|
+
fileList = newFileList;
|
|
449
|
+
screen.append(fileList);
|
|
324
450
|
}
|
|
325
451
|
else {
|
|
326
|
-
|
|
327
|
-
|
|
452
|
+
const oldFileList = fileList;
|
|
453
|
+
const newFileList = createFileList();
|
|
454
|
+
newFileList.setLabel(labelTitle);
|
|
455
|
+
newFileList.setItems([]);
|
|
328
456
|
diffView.setContent(currentSearchTerm ? `No files match "${currentSearchTerm}".` : 'No changes detected.');
|
|
329
457
|
diffView.setLabel(' Diff () ');
|
|
458
|
+
lastSelectedPath = null;
|
|
459
|
+
lastFilePaths = [];
|
|
460
|
+
oldFileList.destroy();
|
|
461
|
+
screen.remove(oldFileList);
|
|
462
|
+
fileList = newFileList;
|
|
463
|
+
screen.append(fileList);
|
|
330
464
|
}
|
|
331
|
-
// Restore scroll positions if reasonably possible (reset if list changed drastically)
|
|
332
|
-
// Actually, if we filter, the scroll position might be invalid.
|
|
333
|
-
// Ideally we keep it 0 if it was 0 or just let the select() call handle scrolling to the item.
|
|
334
|
-
// The previous implementation blindly restored scrollTop.
|
|
335
|
-
// If the list shrunk, select() should have brought it into view.
|
|
336
|
-
// We only explicitly restore if items.length > 0
|
|
337
|
-
// But setting scroll to previous value might be wrong if the list is now shorter.
|
|
338
|
-
// Safe to only restore diffView scroll as it depends on content, fileList is handled by select.
|
|
339
|
-
diffView.scrollTop = diffScroll;
|
|
340
465
|
screen.render();
|
|
341
|
-
|
|
342
|
-
fileList.on('select item', () => {
|
|
343
|
-
scheduleDiffUpdate();
|
|
344
|
-
});
|
|
345
|
-
fileList.key(['up', 'down'], () => {
|
|
346
|
-
scheduleDiffUpdate();
|
|
347
|
-
});
|
|
348
|
-
screen.key(['escape', 'q', 'C-c'], () => {
|
|
349
|
-
if (!searchBox.hidden) {
|
|
350
|
-
searchBox.hide();
|
|
351
|
-
screen.render();
|
|
352
|
-
fileList.focus();
|
|
353
|
-
}
|
|
354
|
-
else if (!confirmDialog.hidden) {
|
|
355
|
-
// Close the confirmation dialog if it's open
|
|
356
|
-
confirmDialog.hide();
|
|
357
|
-
fileList.focus();
|
|
358
|
-
screen.render();
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
screen.destroy();
|
|
362
|
-
process.exit(0);
|
|
363
|
-
}
|
|
466
|
+
isUpdatingList = false;
|
|
364
467
|
});
|
|
365
468
|
screen.key(['s'], () => {
|
|
366
469
|
searchBox.show();
|
|
@@ -391,12 +494,16 @@ Options:
|
|
|
391
494
|
updateBorders();
|
|
392
495
|
});
|
|
393
496
|
screen.key(['left'], () => {
|
|
394
|
-
fileList
|
|
395
|
-
|
|
497
|
+
if (screen.focused !== fileList) {
|
|
498
|
+
fileList.focus();
|
|
499
|
+
updateBorders();
|
|
500
|
+
}
|
|
396
501
|
});
|
|
397
502
|
screen.key(['right'], () => {
|
|
398
|
-
diffView
|
|
399
|
-
|
|
503
|
+
if (screen.focused !== diffView) {
|
|
504
|
+
diffView.focus();
|
|
505
|
+
updateBorders();
|
|
506
|
+
}
|
|
400
507
|
});
|
|
401
508
|
screen.key(['enter'], () => {
|
|
402
509
|
const selectedIndex = fileList.selected;
|
|
@@ -410,44 +517,74 @@ Options:
|
|
|
410
517
|
const selectedIndex = fileList.selected;
|
|
411
518
|
const selectedFile = currentFiles[selectedIndex];
|
|
412
519
|
if (selectedFile) {
|
|
413
|
-
confirmDialog.setContent(`Press SPACE key to confirm revert or ESC to cancel.`);
|
|
414
520
|
confirmDialog.show();
|
|
521
|
+
confirmDialog.setFront();
|
|
415
522
|
confirmDialog.focus();
|
|
416
523
|
screen.render();
|
|
417
524
|
}
|
|
418
525
|
}));
|
|
419
|
-
// Handle
|
|
526
|
+
// Handle dialog keys
|
|
420
527
|
confirmDialog.key(['space'], () => __awaiter(this, void 0, void 0, function* () {
|
|
421
|
-
confirmDialog.hide();
|
|
422
528
|
const selectedIndex = fileList.selected;
|
|
423
529
|
const selectedFile = currentFiles[selectedIndex];
|
|
424
530
|
if (selectedFile) {
|
|
425
531
|
try {
|
|
426
532
|
yield gitHandler.revertFile(selectedFile.path);
|
|
427
|
-
|
|
428
|
-
// Refresh the file list after reverting
|
|
533
|
+
showNotification(`File ${selectedFile.path} reverted successfully.`, 'green');
|
|
429
534
|
yield updateFileList();
|
|
430
535
|
}
|
|
431
536
|
catch (error) {
|
|
432
537
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
433
|
-
|
|
538
|
+
showNotification(`Error reverting file: ${errorMessage}`, 'red');
|
|
434
539
|
}
|
|
435
540
|
}
|
|
541
|
+
confirmDialog.hide();
|
|
436
542
|
fileList.focus();
|
|
437
543
|
screen.render();
|
|
438
544
|
}));
|
|
439
|
-
|
|
440
|
-
confirmDialog.key(['escape', 'q'], () => {
|
|
545
|
+
confirmDialog.key(['escape'], () => {
|
|
441
546
|
confirmDialog.hide();
|
|
442
547
|
fileList.focus();
|
|
443
548
|
screen.render();
|
|
444
549
|
});
|
|
445
|
-
|
|
446
|
-
yield updateFileList();
|
|
447
|
-
}), 5000);
|
|
550
|
+
yield updateBranch();
|
|
448
551
|
yield updateFileList();
|
|
449
552
|
fileList.focus();
|
|
450
553
|
updateBorders();
|
|
554
|
+
// Set up periodic updates to detect new git changes
|
|
555
|
+
const updateInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
556
|
+
const currentlyFocused = screen.focused;
|
|
557
|
+
const wasFileListFocused = currentlyFocused === fileList;
|
|
558
|
+
yield updateBranch();
|
|
559
|
+
yield updateFileList();
|
|
560
|
+
// Restore focus to the same pane it was on before the update
|
|
561
|
+
if (wasFileListFocused) {
|
|
562
|
+
fileList.focus();
|
|
563
|
+
}
|
|
564
|
+
else if (currentlyFocused === diffView) {
|
|
565
|
+
diffView.focus();
|
|
566
|
+
}
|
|
567
|
+
updateBorders();
|
|
568
|
+
}), 2000); // Check for changes every 2 seconds
|
|
569
|
+
// Clean up interval on exit
|
|
570
|
+
screen.key(['escape', 'q', 'C-c'], () => {
|
|
571
|
+
// Don't handle ESC if confirmation dialog has focus or is visible
|
|
572
|
+
if (screen.focused === confirmDialog || !confirmDialog.hidden) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
if (!searchBox.hidden) {
|
|
576
|
+
searchBox.hide();
|
|
577
|
+
screen.render();
|
|
578
|
+
fileList.focus();
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// Clear interval only when actually exiting
|
|
583
|
+
clearInterval(updateInterval);
|
|
584
|
+
screen.destroy();
|
|
585
|
+
process.exit(0);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
451
588
|
});
|
|
452
589
|
}
|
|
453
590
|
main().catch(err => {
|