diffwatch 1.0.8 → 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 +206 -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 -291
- 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,22 +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
|
-
align: 'center',
|
|
159
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 ',
|
|
160
251
|
});
|
|
161
|
-
// Adjust footer to align with the panes
|
|
162
|
-
// Note: We'll adjust this after screen initialization
|
|
163
252
|
screen.append(fileList);
|
|
164
253
|
screen.append(diffView);
|
|
165
254
|
screen.append(searchBox);
|
|
255
|
+
screen.append(notificationBox);
|
|
166
256
|
screen.append(confirmDialog);
|
|
167
257
|
screen.append(footer);
|
|
258
|
+
screen.append(branchBox);
|
|
168
259
|
const updateBorders = () => {
|
|
169
260
|
fileList.style.border.fg = screen.focused === fileList ? 'yellow' : 'white';
|
|
170
261
|
diffView.style.border.fg = screen.focused === diffView ? 'yellow' : 'white';
|
|
@@ -174,6 +265,9 @@ Options:
|
|
|
174
265
|
let lastSelectedPath = null;
|
|
175
266
|
let diffUpdateTimeout = null;
|
|
176
267
|
let currentSearchTerm = '';
|
|
268
|
+
let currentBranch = '';
|
|
269
|
+
let lastFilePaths = [];
|
|
270
|
+
let isUpdatingList = false;
|
|
177
271
|
const scheduleDiffUpdate = () => {
|
|
178
272
|
if (diffUpdateTimeout)
|
|
179
273
|
clearTimeout(diffUpdateTimeout);
|
|
@@ -182,17 +276,18 @@ Options:
|
|
|
182
276
|
}), 150); // 150ms debounce
|
|
183
277
|
};
|
|
184
278
|
const handleScroll = (direction) => {
|
|
185
|
-
|
|
279
|
+
const focused = screen.focused;
|
|
280
|
+
if (focused === fileList) {
|
|
186
281
|
if (direction === 'up') {
|
|
187
|
-
|
|
282
|
+
focused.up(1);
|
|
188
283
|
}
|
|
189
284
|
else {
|
|
190
|
-
|
|
285
|
+
focused.down(1);
|
|
191
286
|
}
|
|
192
287
|
scheduleDiffUpdate();
|
|
193
288
|
screen.render();
|
|
194
289
|
}
|
|
195
|
-
else if (
|
|
290
|
+
else if (focused === diffView) {
|
|
196
291
|
const scrollAmount = direction === 'up' ? -2 : 2;
|
|
197
292
|
diffView.scroll(scrollAmount);
|
|
198
293
|
screen.render();
|
|
@@ -206,9 +301,9 @@ Options:
|
|
|
206
301
|
// Attach custom scroll handlers to widgets (captures wheel even if hovering specific widget)
|
|
207
302
|
// We use widget-level listeners now that screen.mouse is true.
|
|
208
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.
|
|
209
306
|
// The handleScroll function will then decide WHAT to scroll based on focus.
|
|
210
|
-
fileList.on('wheeldown', () => handleScroll('down'));
|
|
211
|
-
fileList.on('wheelup', () => handleScroll('up'));
|
|
212
307
|
diffView.on('wheeldown', () => handleScroll('down'));
|
|
213
308
|
diffView.on('wheelup', () => handleScroll('up'));
|
|
214
309
|
// Also listen on screen for events that might miss the widgets (margins, borders)
|
|
@@ -230,9 +325,25 @@ Options:
|
|
|
230
325
|
console.error(`Failed to open ${filePath}: ${error}`);
|
|
231
326
|
}
|
|
232
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
|
+
});
|
|
233
336
|
const updateDiff = () => __awaiter(this, void 0, void 0, function* () {
|
|
234
337
|
const selectedIndex = fileList.selected;
|
|
235
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
|
+
}
|
|
236
347
|
if (selectedFile) {
|
|
237
348
|
let content = '';
|
|
238
349
|
let label = ` Diff (${selectedFile.path}) `;
|
|
@@ -284,9 +395,10 @@ Options:
|
|
|
284
395
|
});
|
|
285
396
|
const updateFileList = () => __awaiter(this, void 0, void 0, function* () {
|
|
286
397
|
var _a;
|
|
287
|
-
|
|
398
|
+
if (isUpdatingList)
|
|
399
|
+
return;
|
|
400
|
+
isUpdatingList = true;
|
|
288
401
|
const selectedPath = (_a = currentFiles[fileList.selected]) === null || _a === void 0 ? void 0 : _a.path;
|
|
289
|
-
const diffScroll = diffView.scrollTop;
|
|
290
402
|
let files;
|
|
291
403
|
if (currentSearchTerm) {
|
|
292
404
|
files = yield gitHandler.searchFiles(currentSearchTerm);
|
|
@@ -295,7 +407,7 @@ Options:
|
|
|
295
407
|
files = yield gitHandler.getStatus();
|
|
296
408
|
}
|
|
297
409
|
currentFiles = files;
|
|
298
|
-
const items = files.map(f => {
|
|
410
|
+
const items = files.map((f) => {
|
|
299
411
|
let color = '{white-fg}';
|
|
300
412
|
if (f.status === 'added')
|
|
301
413
|
color = '{green-fg}';
|
|
@@ -309,59 +421,49 @@ Options:
|
|
|
309
421
|
color = '{grey-fg}';
|
|
310
422
|
return `${color}${f.path}{/}`;
|
|
311
423
|
});
|
|
312
|
-
fileList.setItems(items);
|
|
313
424
|
const labelTitle = currentSearchTerm ? `Files (${files.length}) - Searching: "${currentSearchTerm}"` : `Files (${files.length})`;
|
|
314
|
-
|
|
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));
|
|
315
430
|
if (items.length > 0) {
|
|
316
|
-
// Restore selection by path if possible
|
|
317
431
|
const newSelectedIndex = selectedPath ? currentFiles.findIndex(f => f.path === selectedPath) : -1;
|
|
318
|
-
|
|
319
|
-
|
|
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;
|
|
320
441
|
if (diffUpdateTimeout) {
|
|
321
442
|
clearTimeout(diffUpdateTimeout);
|
|
322
443
|
diffUpdateTimeout = null;
|
|
323
444
|
}
|
|
324
445
|
yield updateDiff();
|
|
446
|
+
oldFileList.destroy();
|
|
447
|
+
screen.remove(oldFileList);
|
|
448
|
+
fileList = newFileList;
|
|
449
|
+
screen.append(fileList);
|
|
325
450
|
}
|
|
326
451
|
else {
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
const oldFileList = fileList;
|
|
453
|
+
const newFileList = createFileList();
|
|
454
|
+
newFileList.setLabel(labelTitle);
|
|
455
|
+
newFileList.setItems([]);
|
|
329
456
|
diffView.setContent(currentSearchTerm ? `No files match "${currentSearchTerm}".` : 'No changes detected.');
|
|
330
457
|
diffView.setLabel(' Diff () ');
|
|
458
|
+
lastSelectedPath = null;
|
|
459
|
+
lastFilePaths = [];
|
|
460
|
+
oldFileList.destroy();
|
|
461
|
+
screen.remove(oldFileList);
|
|
462
|
+
fileList = newFileList;
|
|
463
|
+
screen.append(fileList);
|
|
331
464
|
}
|
|
332
|
-
// Restore scroll positions if reasonably possible (reset if list changed drastically)
|
|
333
|
-
// Actually, if we filter, the scroll position might be invalid.
|
|
334
|
-
// Ideally we keep it 0 if it was 0 or just let the select() call handle scrolling to the item.
|
|
335
|
-
// The previous implementation blindly restored scrollTop.
|
|
336
|
-
// If the list shrunk, select() should have brought it into view.
|
|
337
|
-
// We only explicitly restore if items.length > 0
|
|
338
|
-
// But setting scroll to previous value might be wrong if the list is now shorter.
|
|
339
|
-
// Safe to only restore diffView scroll as it depends on content, fileList is handled by select.
|
|
340
|
-
diffView.scrollTop = diffScroll;
|
|
341
465
|
screen.render();
|
|
342
|
-
|
|
343
|
-
fileList.on('select item', () => {
|
|
344
|
-
scheduleDiffUpdate();
|
|
345
|
-
});
|
|
346
|
-
fileList.key(['up', 'down'], () => {
|
|
347
|
-
scheduleDiffUpdate();
|
|
348
|
-
});
|
|
349
|
-
screen.key(['escape', 'q', 'C-c'], () => {
|
|
350
|
-
if (!searchBox.hidden) {
|
|
351
|
-
searchBox.hide();
|
|
352
|
-
screen.render();
|
|
353
|
-
fileList.focus();
|
|
354
|
-
}
|
|
355
|
-
else if (!confirmDialog.hidden) {
|
|
356
|
-
// Close the confirmation dialog if it's open
|
|
357
|
-
confirmDialog.hide();
|
|
358
|
-
fileList.focus();
|
|
359
|
-
screen.render();
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
screen.destroy();
|
|
363
|
-
process.exit(0);
|
|
364
|
-
}
|
|
466
|
+
isUpdatingList = false;
|
|
365
467
|
});
|
|
366
468
|
screen.key(['s'], () => {
|
|
367
469
|
searchBox.show();
|
|
@@ -392,12 +494,16 @@ Options:
|
|
|
392
494
|
updateBorders();
|
|
393
495
|
});
|
|
394
496
|
screen.key(['left'], () => {
|
|
395
|
-
fileList
|
|
396
|
-
|
|
497
|
+
if (screen.focused !== fileList) {
|
|
498
|
+
fileList.focus();
|
|
499
|
+
updateBorders();
|
|
500
|
+
}
|
|
397
501
|
});
|
|
398
502
|
screen.key(['right'], () => {
|
|
399
|
-
diffView
|
|
400
|
-
|
|
503
|
+
if (screen.focused !== diffView) {
|
|
504
|
+
diffView.focus();
|
|
505
|
+
updateBorders();
|
|
506
|
+
}
|
|
401
507
|
});
|
|
402
508
|
screen.key(['enter'], () => {
|
|
403
509
|
const selectedIndex = fileList.selected;
|
|
@@ -411,44 +517,74 @@ Options:
|
|
|
411
517
|
const selectedIndex = fileList.selected;
|
|
412
518
|
const selectedFile = currentFiles[selectedIndex];
|
|
413
519
|
if (selectedFile) {
|
|
414
|
-
confirmDialog.setContent(`Press SPACE key to confirm revert or ESC to cancel.`);
|
|
415
520
|
confirmDialog.show();
|
|
521
|
+
confirmDialog.setFront();
|
|
416
522
|
confirmDialog.focus();
|
|
417
523
|
screen.render();
|
|
418
524
|
}
|
|
419
525
|
}));
|
|
420
|
-
// Handle
|
|
526
|
+
// Handle dialog keys
|
|
421
527
|
confirmDialog.key(['space'], () => __awaiter(this, void 0, void 0, function* () {
|
|
422
|
-
confirmDialog.hide();
|
|
423
528
|
const selectedIndex = fileList.selected;
|
|
424
529
|
const selectedFile = currentFiles[selectedIndex];
|
|
425
530
|
if (selectedFile) {
|
|
426
531
|
try {
|
|
427
532
|
yield gitHandler.revertFile(selectedFile.path);
|
|
428
|
-
|
|
429
|
-
// Refresh the file list after reverting
|
|
533
|
+
showNotification(`File ${selectedFile.path} reverted successfully.`, 'green');
|
|
430
534
|
yield updateFileList();
|
|
431
535
|
}
|
|
432
536
|
catch (error) {
|
|
433
537
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
434
|
-
|
|
538
|
+
showNotification(`Error reverting file: ${errorMessage}`, 'red');
|
|
435
539
|
}
|
|
436
540
|
}
|
|
541
|
+
confirmDialog.hide();
|
|
437
542
|
fileList.focus();
|
|
438
543
|
screen.render();
|
|
439
544
|
}));
|
|
440
|
-
|
|
441
|
-
confirmDialog.key(['escape', 'q'], () => {
|
|
545
|
+
confirmDialog.key(['escape'], () => {
|
|
442
546
|
confirmDialog.hide();
|
|
443
547
|
fileList.focus();
|
|
444
548
|
screen.render();
|
|
445
549
|
});
|
|
446
|
-
|
|
447
|
-
yield updateFileList();
|
|
448
|
-
}), 5000);
|
|
550
|
+
yield updateBranch();
|
|
449
551
|
yield updateFileList();
|
|
450
552
|
fileList.focus();
|
|
451
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
|
+
});
|
|
452
588
|
});
|
|
453
589
|
}
|
|
454
590
|
main().catch(err => {
|