codex-lens 0.1.27 → 0.1.29
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/dist/aggregator.js +40 -2
- package/dist/public/assets/{main-d-2BqwRB.js → main-8FK9vFAz.js} +40 -38
- package/dist/public/assets/{main-DNXrKVO-.css → main-CnRUPtz5.css} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/watcher.js +16 -0
- package/package.json +1 -1
- package/src/aggregator.js +43 -2
- package/src/components/App.jsx +169 -1
- package/src/components/CodeViewer.jsx +2 -2
- package/src/components/ErrorBoundary.jsx +172 -0
- package/src/components/TerminalPanel.jsx +154 -163
- package/src/git-manager.js +168 -0
- package/src/global.css +304 -0
- package/src/main.jsx +10 -2
- package/src/watcher.js +20 -0
- package/tsconfig.json +24 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { Terminal } from '@xterm/xterm';
|
|
3
3
|
import { FitAddon } from '@xterm/addon-fit';
|
|
4
4
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
@@ -15,42 +15,66 @@ const VIRTUAL_KEYS = [
|
|
|
15
15
|
{ label: 'Ctrl+C', seq: '\x03' },
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
componentWillUnmount() {
|
|
37
|
-
if (this._writeTimer) {
|
|
38
|
-
cancelAnimationFrame(this._writeTimer);
|
|
18
|
+
export function TerminalPanel() {
|
|
19
|
+
const containerRef = useRef(null);
|
|
20
|
+
const terminalRef = useRef(null);
|
|
21
|
+
const fitAddonRef = useRef(null);
|
|
22
|
+
const wsRef = useRef(null);
|
|
23
|
+
const resizeObserverRef = useRef(null);
|
|
24
|
+
const writeBufferRef = useRef('');
|
|
25
|
+
const writeTimerRef = useRef(null);
|
|
26
|
+
const resizeTimerRef = useRef(null);
|
|
27
|
+
|
|
28
|
+
const sendResize = useCallback(() => {
|
|
29
|
+
if (wsRef.current?.readyState === WebSocket.OPEN && terminalRef.current) {
|
|
30
|
+
wsRef.current.send(JSON.stringify({
|
|
31
|
+
type: 'resize',
|
|
32
|
+
cols: terminalRef.current.cols,
|
|
33
|
+
rows: terminalRef.current.rows,
|
|
34
|
+
}));
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const flushWrite = useCallback(() => {
|
|
39
|
+
if (writeTimerRef.current) {
|
|
40
|
+
cancelAnimationFrame(writeTimerRef.current);
|
|
41
|
+
writeTimerRef.current = null;
|
|
43
42
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
43
|
+
if (!writeBufferRef.current || !terminalRef.current) return;
|
|
44
|
+
|
|
45
|
+
const CHUNK_SIZE = 32768;
|
|
46
|
+
if (writeBufferRef.current.length <= CHUNK_SIZE) {
|
|
47
|
+
const buf = writeBufferRef.current;
|
|
48
|
+
writeBufferRef.current = '';
|
|
49
|
+
terminalRef.current.write(buf);
|
|
50
|
+
} else {
|
|
51
|
+
const chunk = writeBufferRef.current.slice(0, CHUNK_SIZE);
|
|
52
|
+
writeBufferRef.current = writeBufferRef.current.slice(CHUNK_SIZE);
|
|
53
|
+
terminalRef.current.write(chunk);
|
|
54
|
+
writeTimerRef.current = requestAnimationFrame(() => {
|
|
55
|
+
flushWrite();
|
|
56
|
+
});
|
|
46
57
|
}
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const throttledWrite = useCallback((data) => {
|
|
61
|
+
writeBufferRef.current += data;
|
|
62
|
+
if (!writeTimerRef.current) {
|
|
63
|
+
writeTimerRef.current = requestAnimationFrame(() => {
|
|
64
|
+
flushWrite();
|
|
65
|
+
});
|
|
49
66
|
}
|
|
50
|
-
}
|
|
67
|
+
}, [flushWrite]);
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
const handleVirtualKey = useCallback((seq) => {
|
|
70
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
71
|
+
wsRef.current.send(JSON.stringify({ type: 'input', data: seq }));
|
|
72
|
+
}
|
|
73
|
+
terminalRef.current?.focus();
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const terminal = new Terminal({
|
|
54
78
|
cursorBlink: true,
|
|
55
79
|
cursorStyle: 'bar',
|
|
56
80
|
fontSize: 13,
|
|
@@ -65,52 +89,54 @@ export class TerminalPanel extends React.Component {
|
|
|
65
89
|
allowProposedApi: true,
|
|
66
90
|
});
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
const fitAddon = new FitAddon();
|
|
93
|
+
terminal.loadAddon(fitAddon);
|
|
94
|
+
terminal.loadAddon(new WebLinksAddon());
|
|
71
95
|
|
|
72
|
-
|
|
96
|
+
terminal.open(containerRef.current);
|
|
97
|
+
terminalRef.current = terminal;
|
|
98
|
+
fitAddonRef.current = fitAddon;
|
|
73
99
|
|
|
74
100
|
requestAnimationFrame(() => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
if (fitAddon) {
|
|
102
|
+
fitAddon.fit();
|
|
103
|
+
terminal.focus();
|
|
78
104
|
}
|
|
79
105
|
});
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
107
|
+
terminal.onData((data) => {
|
|
108
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
109
|
+
wsRef.current.send(JSON.stringify({ type: 'input', data }));
|
|
84
110
|
}
|
|
85
111
|
});
|
|
86
|
-
}
|
|
87
112
|
|
|
88
|
-
|
|
113
|
+
// WebSocket connection
|
|
89
114
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
90
115
|
const host = window.location.hostname;
|
|
91
116
|
const port = window.location.port === '5173' ? '5174' : window.location.port;
|
|
92
117
|
const wsUrl = `${protocol}//${host}:${port}/ws/terminal`;
|
|
93
118
|
|
|
94
|
-
|
|
119
|
+
const ws = new WebSocket(wsUrl);
|
|
120
|
+
wsRef.current = ws;
|
|
95
121
|
|
|
96
|
-
|
|
122
|
+
ws.onopen = () => {
|
|
97
123
|
console.log('[Terminal] Connected to PTY service');
|
|
98
|
-
|
|
124
|
+
sendResize();
|
|
99
125
|
};
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
ws.onmessage = (event) => {
|
|
102
128
|
try {
|
|
103
129
|
const msg = JSON.parse(event.data);
|
|
104
130
|
if (msg.type === 'data') {
|
|
105
|
-
|
|
131
|
+
throttledWrite(msg.data);
|
|
106
132
|
} else if (msg.type === 'exit') {
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
133
|
+
flushWrite();
|
|
134
|
+
if (terminal) {
|
|
135
|
+
terminal.write(`\r\n[Process exited with code ${msg.exitCode ?? '?'}]\r\n`);
|
|
110
136
|
}
|
|
111
137
|
} else if (msg.type === 'state') {
|
|
112
|
-
if (!msg.running &&
|
|
113
|
-
|
|
138
|
+
if (!msg.running && terminal) {
|
|
139
|
+
flushWrite();
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
} catch (e) {
|
|
@@ -118,132 +144,97 @@ export class TerminalPanel extends React.Component {
|
|
|
118
144
|
}
|
|
119
145
|
};
|
|
120
146
|
|
|
121
|
-
|
|
147
|
+
ws.onclose = () => {
|
|
122
148
|
console.log('[Terminal] Disconnected, reconnecting in 3s...');
|
|
123
149
|
setTimeout(() => {
|
|
124
|
-
if (
|
|
125
|
-
|
|
150
|
+
if (containerRef.current) {
|
|
151
|
+
// Reconnect handled by re-mounting effect
|
|
126
152
|
}
|
|
127
153
|
}, 3000);
|
|
128
154
|
};
|
|
129
155
|
|
|
130
|
-
|
|
156
|
+
ws.onerror = (error) => {
|
|
131
157
|
console.error('[Terminal] WebSocket error:', error);
|
|
132
158
|
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
sendResize() {
|
|
136
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.terminal) {
|
|
137
|
-
this.ws.send(JSON.stringify({
|
|
138
|
-
type: 'resize',
|
|
139
|
-
cols: this.terminal.cols,
|
|
140
|
-
rows: this.terminal.rows,
|
|
141
|
-
}));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
159
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this._resizeTimer = setTimeout(() => {
|
|
151
|
-
if (this.fitAddon && this.containerRef.current) {
|
|
160
|
+
// ResizeObserver setup
|
|
161
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
162
|
+
if (resizeTimerRef.current) clearTimeout(resizeTimerRef.current);
|
|
163
|
+
resizeTimerRef.current = setTimeout(() => {
|
|
164
|
+
if (fitAddonRef.current && containerRef.current) {
|
|
152
165
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
fitAddonRef.current.fit();
|
|
167
|
+
sendResize();
|
|
155
168
|
} catch {}
|
|
156
169
|
}
|
|
157
170
|
}, 150);
|
|
158
171
|
});
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
_throttledWrite(data) {
|
|
164
|
-
this._writeBuffer += data;
|
|
165
|
-
if (!this._writeTimer) {
|
|
166
|
-
this._writeTimer = requestAnimationFrame(() => {
|
|
167
|
-
this._flushWrite();
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
_flushWrite() {
|
|
173
|
-
if (this._writeTimer) {
|
|
174
|
-
cancelAnimationFrame(this._writeTimer);
|
|
175
|
-
this._writeTimer = null;
|
|
176
|
-
}
|
|
177
|
-
if (!this._writeBuffer || !this.terminal) return;
|
|
178
|
-
|
|
179
|
-
const CHUNK_SIZE = 32768;
|
|
180
|
-
if (this._writeBuffer.length <= CHUNK_SIZE) {
|
|
181
|
-
const buf = this._writeBuffer;
|
|
182
|
-
this._writeBuffer = '';
|
|
183
|
-
this.terminal.write(buf);
|
|
184
|
-
} else {
|
|
185
|
-
const chunk = this._writeBuffer.slice(0, CHUNK_SIZE);
|
|
186
|
-
this._writeBuffer = this._writeBuffer.slice(CHUNK_SIZE);
|
|
187
|
-
this.terminal.write(chunk);
|
|
188
|
-
this._writeTimer = requestAnimationFrame(() => {
|
|
189
|
-
this._flushWrite();
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
173
|
+
resizeObserver.observe(containerRef.current);
|
|
174
|
+
resizeObserverRef.current = resizeObserver;
|
|
193
175
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
176
|
+
// Cleanup on unmount
|
|
177
|
+
return () => {
|
|
178
|
+
if (writeTimerRef.current) {
|
|
179
|
+
cancelAnimationFrame(writeTimerRef.current);
|
|
180
|
+
}
|
|
181
|
+
if (ws) {
|
|
182
|
+
ws.close();
|
|
183
|
+
wsRef.current = null;
|
|
184
|
+
}
|
|
185
|
+
if (resizeObserver) {
|
|
186
|
+
resizeObserver.disconnect();
|
|
187
|
+
}
|
|
188
|
+
if (terminal) {
|
|
189
|
+
terminal.dispose();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}, [sendResize, throttledWrite, flushWrite]);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div style={{
|
|
196
|
+
height: '100%',
|
|
197
|
+
display: 'flex',
|
|
198
|
+
flexDirection: 'column',
|
|
199
|
+
background: '#0a0a0a',
|
|
200
|
+
}}>
|
|
201
|
+
<div
|
|
202
|
+
ref={containerRef}
|
|
203
|
+
style={{
|
|
204
|
+
flex: 1,
|
|
205
|
+
overflow: 'hidden',
|
|
206
|
+
padding: '4px 8px',
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
203
209
|
<div style={{
|
|
204
|
-
height: '100%',
|
|
205
210
|
display: 'flex',
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
gap: '4px',
|
|
212
|
+
padding: '8px',
|
|
213
|
+
background: '#111',
|
|
214
|
+
borderTop: '1px solid #222',
|
|
215
|
+
flexWrap: 'wrap',
|
|
208
216
|
}}>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
style={{
|
|
230
|
-
padding: '8px 12px',
|
|
231
|
-
border: '1px solid #333',
|
|
232
|
-
borderRadius: '4px',
|
|
233
|
-
background: '#1a1a1a',
|
|
234
|
-
color: '#ccc',
|
|
235
|
-
fontSize: '13px',
|
|
236
|
-
fontFamily: 'Menlo, Monaco, monospace',
|
|
237
|
-
cursor: 'pointer',
|
|
238
|
-
minWidth: '44px',
|
|
239
|
-
minHeight: '44px',
|
|
240
|
-
}}
|
|
241
|
-
>
|
|
242
|
-
{key.label}
|
|
243
|
-
</button>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
217
|
+
{VIRTUAL_KEYS.map((key) => (
|
|
218
|
+
<button
|
|
219
|
+
key={key.label}
|
|
220
|
+
onClick={() => handleVirtualKey(key.seq)}
|
|
221
|
+
style={{
|
|
222
|
+
padding: '8px 12px',
|
|
223
|
+
border: '1px solid #333',
|
|
224
|
+
borderRadius: '4px',
|
|
225
|
+
background: '#1a1a1a',
|
|
226
|
+
color: '#ccc',
|
|
227
|
+
fontSize: '13px',
|
|
228
|
+
fontFamily: 'Menlo, Monaco, monospace',
|
|
229
|
+
cursor: 'pointer',
|
|
230
|
+
minWidth: '44px',
|
|
231
|
+
minHeight: '44px',
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
{key.label}
|
|
235
|
+
</button>
|
|
236
|
+
))}
|
|
246
237
|
</div>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createLogger } from './lib/logger.js';
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('GitManager');
|
|
7
|
+
|
|
8
|
+
class GitManager {
|
|
9
|
+
constructor(projectRoot, wsEmitter) {
|
|
10
|
+
this.projectRoot = projectRoot;
|
|
11
|
+
this.wsEmitter = wsEmitter;
|
|
12
|
+
this.gitDir = join(projectRoot, '.git');
|
|
13
|
+
this.currentStatus = null;
|
|
14
|
+
this.currentBranch = null;
|
|
15
|
+
this._statusTimeout = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isGitRepo() {
|
|
19
|
+
return existsSync(this.gitDir);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
runGitCommand(args) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const proc = spawn('git', args, {
|
|
25
|
+
cwd: this.projectRoot,
|
|
26
|
+
shell: true,
|
|
27
|
+
windowsHide: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let stdout = '';
|
|
31
|
+
let stderr = '';
|
|
32
|
+
|
|
33
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
34
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
35
|
+
|
|
36
|
+
proc.on('close', (code) => {
|
|
37
|
+
resolve({ code, stdout, stderr });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
proc.on('error', (err) => {
|
|
41
|
+
reject(err);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
parsePorcelainStatus(output) {
|
|
47
|
+
const lines = output.trim().split('\n');
|
|
48
|
+
const result = {
|
|
49
|
+
staged: [],
|
|
50
|
+
unstaged: [],
|
|
51
|
+
untracked: [],
|
|
52
|
+
conflicted: []
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
if (!line || line.length < 3) continue;
|
|
57
|
+
|
|
58
|
+
const indexStatus = line[0];
|
|
59
|
+
const workTreeStatus = line[1];
|
|
60
|
+
const path = line.slice(3).trim();
|
|
61
|
+
|
|
62
|
+
const fileInfo = { path, indexStatus, workTreeStatus };
|
|
63
|
+
|
|
64
|
+
// Staged changes (index)
|
|
65
|
+
if (indexStatus !== ' ' && indexStatus !== '?') {
|
|
66
|
+
result.staged.push(fileInfo);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Working tree changes
|
|
70
|
+
if (workTreeStatus === 'M' || workTreeStatus === 'D') {
|
|
71
|
+
result.unstaged.push(fileInfo);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Untracked files
|
|
75
|
+
if (indexStatus === '?' && workTreeStatus === '?') {
|
|
76
|
+
result.untracked.push(fileInfo);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Conflicted files
|
|
80
|
+
if (indexStatus === 'U' || workTreeStatus === 'U') {
|
|
81
|
+
result.conflicted.push(fileInfo);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getStatus() {
|
|
89
|
+
if (!this.isGitRepo()) return null;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const { stdout } = await this.runGitCommand(['status', '--porcelain']);
|
|
93
|
+
this.currentStatus = this.parsePorcelainStatus(stdout);
|
|
94
|
+
return this.currentStatus;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.error(`Failed to get git status: ${error.message}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getCurrentBranch() {
|
|
102
|
+
if (!this.isGitRepo()) return null;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const { stdout } = await this.runGitCommand(['branch', '--show-current']);
|
|
106
|
+
this.currentBranch = stdout.trim();
|
|
107
|
+
return this.currentBranch;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error(`Failed to get branch: ${error.message}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async stageFile(filePath) {
|
|
115
|
+
await this.runGitCommand(['add', filePath]);
|
|
116
|
+
return this.getStatus();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async unstageFile(filePath) {
|
|
120
|
+
await this.runGitCommand(['reset', 'HEAD', '--', filePath]);
|
|
121
|
+
return this.getStatus();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async stageAll() {
|
|
125
|
+
await this.runGitCommand(['add', '-A']);
|
|
126
|
+
return this.getStatus();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async unstageAll() {
|
|
130
|
+
await this.runGitCommand(['reset', 'HEAD']);
|
|
131
|
+
return this.getStatus();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async commit(message) {
|
|
135
|
+
await this.runGitCommand(['commit', '-m', message]);
|
|
136
|
+
return this.getStatus();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async broadcastUpdate() {
|
|
140
|
+
const branch = await this.getCurrentBranch();
|
|
141
|
+
const status = await this.getStatus();
|
|
142
|
+
|
|
143
|
+
this.wsEmitter({
|
|
144
|
+
type: 'git_status',
|
|
145
|
+
data: {
|
|
146
|
+
isRepo: true,
|
|
147
|
+
branch,
|
|
148
|
+
status,
|
|
149
|
+
stagedCount: status?.staged?.length || 0,
|
|
150
|
+
unstagedCount: (status?.unstaged?.length || 0) + (status?.untracked?.length || 0),
|
|
151
|
+
timestamp: new Date().toISOString()
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
scheduleStatusUpdate() {
|
|
157
|
+
if (this._statusTimeout) {
|
|
158
|
+
clearTimeout(this._statusTimeout);
|
|
159
|
+
}
|
|
160
|
+
this._statusTimeout = setTimeout(() => {
|
|
161
|
+
this.broadcastUpdate();
|
|
162
|
+
}, 500);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createGitManager(projectRoot, wsEmitter) {
|
|
167
|
+
return new GitManager(projectRoot, wsEmitter);
|
|
168
|
+
}
|