codex-lens 0.1.25 → 0.1.27

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Codex Lens</title>
7
- <script type="module" crossorigin src="./assets/main-DJ9sK-1n.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/main-CYNmzqDG.css">
7
+ <script type="module" crossorigin src="./assets/main-d-2BqwRB.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/main-DNXrKVO-.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-lens",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "A visualization tool for Codex that monitors API requests and file system changes",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/aggregator.js CHANGED
@@ -121,7 +121,7 @@ class Aggregator {
121
121
  logger.info(`File saved: ${filePath}`);
122
122
  res.json({ success: true, path: filePath });
123
123
  } catch (error) {
124
- logger.error(`Failed to save file: ${filePath} - ${error.message}`);
124
+ logger.error(`Failed to save file: ${error.message}`);
125
125
  res.status(500).json({ error: error.message });
126
126
  }
127
127
  });
@@ -12,59 +12,23 @@ export function App() {
12
12
  const [latestVersion, setLatestVersion] = useState(null);
13
13
  const [hasUpdate, setHasUpdate] = useState(false);
14
14
  const [projectName, setProjectName] = useState('');
15
+ const [saving, setSaving] = useState(false);
15
16
  const wsRef = useRef(null);
16
- const activeTabIdRef = useRef(null);
17
- const tabsRef = useRef([]);
18
- const saveCurrentFileRef = useRef(null);
19
17
 
20
18
  useEffect(() => {
21
- activeTabIdRef.current = activeTabId;
22
- }, [activeTabId]);
23
-
24
- useEffect(() => {
25
- tabsRef.current = tabs;
26
- }, [tabs]);
27
-
28
- function saveCurrentFile() {
29
- const currentTabId = activeTabIdRef.current;
30
- const currentTabs = tabsRef.current;
31
- const activeTab = currentTabs.find(t => t.id === currentTabId);
32
-
33
- if (!activeTab || activeTab.isDiff) {
34
- console.log('No active tab or is diff, skip save');
35
- return;
36
- }
37
-
38
- const port = window.location.port === '5173' ? '5174' : window.location.port;
39
- const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
40
-
41
- console.log('Saving file:', activeTab.path);
42
-
43
- fetch(`${protocol}//${window.location.hostname}:${port}/api/save-file`, {
44
- method: 'POST',
45
- headers: { 'Content-Type': 'application/json' },
46
- body: JSON.stringify({ path: activeTab.path, content: activeTab.content })
47
- })
48
- .then(response => {
49
- if (response.ok) {
50
- console.log('File saved successfully');
51
- setTabs(prevTabs => prevTabs.map(t =>
52
- t.id === currentTabId ? { ...t, isModified: false } : t
53
- ));
54
- } else {
55
- response.json().then(error => {
56
- console.error('Failed to save file:', error);
57
- });
58
- }
59
- })
60
- .catch(error => {
61
- console.error('Failed to save file:', error);
62
- });
63
- }
19
+ fetchStatus();
20
+ connectWebSocket();
21
+ document.addEventListener('click', handleDocumentClick);
22
+ document.addEventListener('keydown', handleKeyDown);
64
23
 
65
- useEffect(() => {
66
- saveCurrentFileRef.current = saveCurrentFile;
67
- });
24
+ return () => {
25
+ document.removeEventListener('click', handleDocumentClick);
26
+ document.removeEventListener('keydown', handleKeyDown);
27
+ if (wsRef.current) {
28
+ wsRef.current.close();
29
+ }
30
+ };
31
+ }, []);
68
32
 
69
33
  async function fetchStatus() {
70
34
  try {
@@ -90,63 +54,26 @@ export function App() {
90
54
  setContextMenu(null);
91
55
  }
92
56
 
93
- useEffect(() => {
94
- fetchStatus();
95
- connectWebSocket();
57
+ function handleKeyDown(e) {
58
+ if (!activeTabId) return;
96
59
 
97
- function handleKeyDown(e) {
98
- if (e.ctrlKey && e.key === 's') {
99
- e.preventDefault();
100
- console.log('Ctrl+S pressed, activeTabId:', activeTabIdRef.current);
101
- if (saveCurrentFileRef.current) {
102
- saveCurrentFileRef.current();
103
- }
104
- return;
60
+ if (e.ctrlKey && e.key === 'w') {
61
+ e.preventDefault();
62
+ closeTab(activeTabId);
63
+ } else if (e.ctrlKey && e.key === 'Tab') {
64
+ e.preventDefault();
65
+ if (e.shiftKey) {
66
+ switchToPrevTab();
67
+ } else {
68
+ switchToNextTab();
105
69
  }
106
-
107
- if (!activeTabIdRef.current) return;
108
-
109
- if (e.ctrlKey && e.key === 'w') {
110
- e.preventDefault();
111
- closeTab(activeTabIdRef.current);
112
- } else if (e.ctrlKey && e.key === 'Tab') {
113
- e.preventDefault();
114
- if (e.shiftKey) {
115
- switchToPrevTab();
116
- } else {
117
- switchToNextTab();
118
- }
70
+ } else if (e.ctrlKey && e.key === 's') {
71
+ e.preventDefault();
72
+ const activeTab = tabs.find(t => t.id === activeTabId);
73
+ if (activeTab && activeTab.modified) {
74
+ saveFile(activeTabId);
119
75
  }
120
76
  }
121
-
122
- document.addEventListener('click', handleDocumentClick);
123
- document.addEventListener('keydown', handleKeyDown);
124
-
125
- const preventBrowserSave = (e) => {
126
- if (e.ctrlKey && e.key === 's') {
127
- e.preventDefault();
128
- e.stopPropagation();
129
- }
130
- };
131
- window.addEventListener('keydown', preventBrowserSave, true);
132
-
133
- return () => {
134
- document.removeEventListener('click', handleDocumentClick);
135
- document.removeEventListener('keydown', handleKeyDown);
136
- window.removeEventListener('keydown', preventBrowserSave, true);
137
- if (wsRef.current) {
138
- wsRef.current.close();
139
- }
140
- };
141
- }, []);
142
-
143
- function handleContentChange(newContent) {
144
- setTabs(prevTabs => prevTabs.map(t => {
145
- if (t.id === activeTabId) {
146
- return { ...t, content: newContent, isModified: true };
147
- }
148
- return t;
149
- }));
150
77
  }
151
78
 
152
79
  function connectWebSocket() {
@@ -218,8 +145,10 @@ export function App() {
218
145
  path: data.path,
219
146
  name: fileName,
220
147
  content: data.content,
148
+ originalContent: data.content,
221
149
  diff: data.diff || null,
222
- isDiff: !!data.diff
150
+ isDiff: !!data.diff,
151
+ modified: false
223
152
  };
224
153
  setActiveTabId(newTab.id);
225
154
  return [...prevTabs, newTab];
@@ -234,7 +163,14 @@ export function App() {
234
163
  const existingTab = prevTabs.find(t => t.path === data.path);
235
164
 
236
165
  if (existingTab) {
237
- const updatedTab = { ...existingTab, content: data.newContent, diff: data.diff, isDiff: true };
166
+ const updatedTab = {
167
+ ...existingTab,
168
+ content: data.newContent,
169
+ originalContent: data.newContent,
170
+ diff: data.diff,
171
+ isDiff: true,
172
+ modified: false
173
+ };
238
174
  setActiveTabId(existingTab.id);
239
175
  return prevTabs.map(t =>
240
176
  t.path === data.path ? updatedTab : t
@@ -245,8 +181,10 @@ export function App() {
245
181
  path: data.path,
246
182
  name: fileName,
247
183
  content: data.newContent,
184
+ originalContent: data.newContent,
248
185
  diff: data.diff,
249
- isDiff: true
186
+ isDiff: true,
187
+ modified: false
250
188
  };
251
189
  setActiveTabId(newTab.id);
252
190
  return [...prevTabs, newTab];
@@ -254,6 +192,99 @@ export function App() {
254
192
  });
255
193
  }
256
194
 
195
+ function handleContentChange(tabId, newContent) {
196
+ setTabs(prevTabs => prevTabs.map(tab => {
197
+ if (tab.id === tabId) {
198
+ const modified = newContent !== tab.originalContent;
199
+ return { ...tab, content: newContent, modified };
200
+ }
201
+ return tab;
202
+ }));
203
+ }
204
+
205
+ async function saveFile(tabId) {
206
+ const tab = tabs.find(t => t.id === tabId);
207
+ if (!tab || !tab.modified) return;
208
+
209
+ setSaving(true);
210
+ try {
211
+ const port = window.location.port === '5173' ? '5174' : window.location.port;
212
+ const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
213
+ const response = await fetch(`${protocol}//${window.location.hostname}:${port}/api/save-file`, {
214
+ method: 'POST',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify({ path: tab.path, content: tab.content })
217
+ });
218
+
219
+ if (response.ok) {
220
+ setTabs(prevTabs => prevTabs.map(t => {
221
+ if (t.id === tabId) {
222
+ return { ...t, originalContent: t.content, modified: false };
223
+ }
224
+ return t;
225
+ }));
226
+ console.log('File saved:', tab.path);
227
+ } else {
228
+ const error = await response.json();
229
+ console.error('Failed to save file:', error.message);
230
+ alert('保存失败: ' + error.message);
231
+ }
232
+ } catch (error) {
233
+ console.error('Failed to save file:', error);
234
+ alert('保存失败: ' + error.message);
235
+ } finally {
236
+ setSaving(false);
237
+ }
238
+ }
239
+
240
+ async function saveAllFiles() {
241
+ const modifiedTabs = tabs.filter(t => t.modified);
242
+ if (modifiedTabs.length === 0) return;
243
+
244
+ setSaving(true);
245
+ const port = window.location.port === '5173' ? '5174' : window.location.port;
246
+ const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
247
+
248
+ let savedCount = 0;
249
+ let failedFiles = [];
250
+
251
+ for (const tab of modifiedTabs) {
252
+ try {
253
+ const response = await fetch(`${protocol}//${window.location.hostname}:${port}/api/save-file`, {
254
+ method: 'POST',
255
+ headers: { 'Content-Type': 'application/json' },
256
+ body: JSON.stringify({ path: tab.path, content: tab.content })
257
+ });
258
+
259
+ if (response.ok) {
260
+ savedCount++;
261
+ } else {
262
+ const error = await response.json();
263
+ failedFiles.push(tab.name);
264
+ console.error('Failed to save file:', tab.name, error.message);
265
+ }
266
+ } catch (error) {
267
+ failedFiles.push(tab.name);
268
+ console.error('Failed to save file:', tab.name, error);
269
+ }
270
+ }
271
+
272
+ if (savedCount > 0) {
273
+ setTabs(prevTabs => prevTabs.map(t => {
274
+ if (t.modified && !failedFiles.includes(t.name)) {
275
+ return { ...t, originalContent: t.content, modified: false };
276
+ }
277
+ return t;
278
+ }));
279
+ }
280
+
281
+ if (failedFiles.length > 0) {
282
+ alert(`以下文件保存失败: ${failedFiles.join(', ')}`);
283
+ }
284
+
285
+ setSaving(false);
286
+ }
287
+
257
288
  function handleFileClick(path) {
258
289
  const existingTab = tabs.find(t => t.path === path);
259
290
  if (existingTab) {
@@ -266,10 +297,17 @@ export function App() {
266
297
  }
267
298
 
268
299
  function closeTab(tabId) {
300
+ const tab = tabs.find(t => t.id === tabId);
301
+ if (tab?.modified) {
302
+ const confirmed = window.confirm(`文件 "${tab.name}" 已修改,是否保存?`);
303
+ if (confirmed) {
304
+ saveFile(tabId);
305
+ }
306
+ }
307
+
269
308
  setTabs(prev => {
270
309
  const newTabs = prev.filter(t => t.id !== tabId);
271
- const currentActiveId = activeTabIdRef.current;
272
- if (currentActiveId === tabId) {
310
+ if (activeTabId === tabId) {
273
311
  const newActiveId = newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null;
274
312
  setActiveTabId(newActiveId);
275
313
  }
@@ -278,11 +316,30 @@ export function App() {
278
316
  }
279
317
 
280
318
  function closeOtherTabs(tabId) {
281
- setTabs(prev => prev.filter(t => t.id === tabId));
319
+ setTabs(prev => {
320
+ const tabsToClose = prev.filter(t => t.id !== tabId);
321
+ tabsToClose.forEach(t => {
322
+ if (t.modified) {
323
+ const confirmed = window.confirm(`文件 "${t.name}" 已修改,是否保存?`);
324
+ if (confirmed) {
325
+ saveFile(t.id);
326
+ }
327
+ }
328
+ });
329
+ return prev.filter(t => t.id === tabId);
330
+ });
282
331
  setActiveTabId(tabId);
283
332
  }
284
333
 
285
334
  function closeAllTabs() {
335
+ tabs.forEach(t => {
336
+ if (t.modified) {
337
+ const confirmed = window.confirm(`文件 "${t.name}" 已修改,是否保存?`);
338
+ if (confirmed) {
339
+ saveFile(t.id);
340
+ }
341
+ }
342
+ });
286
343
  setTabs([]);
287
344
  setActiveTabId(null);
288
345
  }
@@ -292,24 +349,20 @@ export function App() {
292
349
  }
293
350
 
294
351
  function switchToNextTab() {
295
- const currentTabs = tabsRef.current;
296
- const currentActiveId = activeTabIdRef.current;
297
- const currentIndex = currentTabs.findIndex(t => t.id === currentActiveId);
298
- if (currentIndex < currentTabs.length - 1) {
299
- setActiveTabId(currentTabs[currentIndex + 1].id);
300
- } else if (currentTabs.length > 0) {
301
- setActiveTabId(currentTabs[0].id);
352
+ const currentIndex = tabs.findIndex(t => t.id === activeTabId);
353
+ if (currentIndex < tabs.length - 1) {
354
+ setActiveTabId(tabs[currentIndex + 1].id);
355
+ } else if (tabs.length > 0) {
356
+ setActiveTabId(tabs[0].id);
302
357
  }
303
358
  }
304
359
 
305
360
  function switchToPrevTab() {
306
- const currentTabs = tabsRef.current;
307
- const currentActiveId = activeTabIdRef.current;
308
- const currentIndex = currentTabs.findIndex(t => t.id === currentActiveId);
361
+ const currentIndex = tabs.findIndex(t => t.id === activeTabId);
309
362
  if (currentIndex > 0) {
310
- setActiveTabId(currentTabs[currentIndex - 1].id);
311
- } else if (currentTabs.length > 0) {
312
- setActiveTabId(currentTabs[currentTabs.length - 1].id);
363
+ setActiveTabId(tabs[currentIndex - 1].id);
364
+ } else if (tabs.length > 0) {
365
+ setActiveTabId(tabs[tabs.length - 1].id);
313
366
  }
314
367
  }
315
368
 
@@ -367,8 +420,7 @@ export function App() {
367
420
  diff={activeTab.diff}
368
421
  isDiff={activeTab.isDiff}
369
422
  filePath={activeTab.path}
370
- onChange={handleContentChange}
371
- isModified={activeTab.isModified}
423
+ onChange={(value) => handleContentChange(activeTabId, value)}
372
424
  />
373
425
  )}
374
426
  </div>
@@ -377,12 +429,10 @@ export function App() {
377
429
  <ContextMenu
378
430
  x={contextMenu.x}
379
431
  y={contextMenu.y}
380
- tabId={contextMenu.tabId}
432
+ tab={tabs.find(t => t.id === contextMenu.tabId)}
433
+ tabs={tabs}
434
+ saving={saving}
381
435
  onClose={() => setContextMenu(null)}
382
- onSave={() => {
383
- saveCurrentFile();
384
- setContextMenu(null);
385
- }}
386
436
  onCloseTab={() => {
387
437
  closeTab(contextMenu.tabId);
388
438
  setContextMenu(null);
@@ -395,6 +445,14 @@ export function App() {
395
445
  closeAllTabs();
396
446
  setContextMenu(null);
397
447
  }}
448
+ onSave={() => {
449
+ saveFile(contextMenu.tabId);
450
+ setContextMenu(null);
451
+ }}
452
+ onSaveAll={() => {
453
+ saveAllFiles();
454
+ setContextMenu(null);
455
+ }}
398
456
  />
399
457
  )}
400
458
  <div className="panel right-panel">
@@ -413,12 +471,12 @@ function TabBar({ tabs, activeTabId, onTabClick, onTabClose, onContextMenu }) {
413
471
  {tabs.map(tab => (
414
472
  <div
415
473
  key={tab.id}
416
- className={`tab ${activeTabId === tab.id ? 'active' : ''}`}
474
+ className={`tab ${activeTabId === tab.id ? 'active' : ''} ${tab.modified ? 'modified' : ''}`}
417
475
  onClick={() => onTabClick(tab.id)}
418
476
  onContextMenu={(e) => onContextMenu(e, tab.id)}
419
477
  >
420
478
  <span className="tab-name">
421
- {tab.isModified && <span className="tab-modified">*</span>}
479
+ {tab.modified && <span className="tab-modified-mark">●</span>}
422
480
  {tab.name}
423
481
  </span>
424
482
  <button
@@ -436,14 +494,21 @@ function TabBar({ tabs, activeTabId, onTabClick, onTabClose, onContextMenu }) {
436
494
  );
437
495
  }
438
496
 
439
- function ContextMenu({ x, y, tabId, onClose, onSave, onCloseTab, onCloseOtherTabs, onCloseAllTabs }) {
440
- const tabs = tabsRef.current;
441
- const tab = tabs.find(t => t.id === tabId);
442
- const canSave = tab && !tab.isDiff;
443
-
497
+ function ContextMenu({ x, y, tab, tabs, saving, onClose, onCloseTab, onCloseOtherTabs, onCloseAllTabs, onSave, onSaveAll }) {
498
+ const hasModified = tabs?.some(t => t.modified);
499
+
444
500
  return (
445
501
  <div className="context-menu" style={{ left: x, top: y }} onClick={(e) => e.stopPropagation()}>
446
- {canSave && <div className="context-menu-item" onClick={onSave}>保存</div>}
502
+ {tab?.modified && (
503
+ <div className="context-menu-item" onClick={onSave} style={{ color: '#4ade80' }}>
504
+ {saving ? '保存中...' : '保存'}
505
+ </div>
506
+ )}
507
+ {hasModified && (
508
+ <div className="context-menu-item" onClick={onSaveAll} style={{ color: '#4ade80' }}>
509
+ {saving ? '保存中...' : '全部保存'}
510
+ </div>
511
+ )}
447
512
  <div className="context-menu-item" onClick={onCloseTab}>关闭</div>
448
513
  <div className="context-menu-item" onClick={onCloseOtherTabs}>关闭其他</div>
449
514
  <div className="context-menu-item" onClick={onCloseAllTabs}>关闭所有</div>
@@ -1,6 +1,6 @@
1
- import React, { useMemo, useRef, useState, useCallback } from 'react';
1
+ import React, { useMemo, useRef } from 'react';
2
2
  import CodeMirror from '@uiw/react-codemirror';
3
- import { EditorView, Decoration, ViewPlugin, keymap } from '@codemirror/view';
3
+ import { EditorView, Decoration, ViewPlugin, ViewUpdate } from '@codemirror/view';
4
4
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
5
5
  import { tags as t } from '@lezer/highlight';
6
6
  import { RangeSetBuilder } from '@codemirror/state';
@@ -215,11 +215,8 @@ const darkHighlightStyle = HighlightStyle.define([
215
215
 
216
216
  const syntaxTheme = syntaxHighlighting(darkHighlightStyle);
217
217
 
218
- export function CodeViewer({ content, diff, isDiff, filePath, onChange, isModified }) {
218
+ export function CodeViewer({ content, diff, isDiff, filePath, onChange }) {
219
219
  const editorRef = useRef(null);
220
- const [localContent, setLocalContent] = useState(content || '');
221
-
222
- const isEditable = !isDiff;
223
220
 
224
221
  const extensions = useMemo(() => {
225
222
  const exts = [
@@ -257,27 +254,23 @@ export function CodeViewer({ content, diff, isDiff, filePath, onChange, isModifi
257
254
  return content || '';
258
255
  }, [content, diff, isDiff]);
259
256
 
260
- const handleChange = useCallback((value) => {
261
- setLocalContent(value);
262
- if (onChange) {
263
- onChange(value);
264
- }
265
- }, [onChange]);
257
+ const editable = !isDiff;
266
258
 
267
- React.useEffect(() => {
268
- if (!isDiff) {
269
- setLocalContent(content || '');
259
+ function handleChange(value) {
260
+ if (onChange && editable) {
261
+ onChange(value);
270
262
  }
271
- }, [content, isDiff]);
263
+ }
272
264
 
273
265
  return (
274
266
  <div className="code-viewer-codemirror">
275
267
  <CodeMirror
276
- value={isDiff ? code : localContent}
268
+ value={code}
277
269
  height="100%"
278
270
  theme={darkTheme}
279
271
  extensions={extensions}
280
- editable={isEditable}
272
+ editable={editable}
273
+ onChange={handleChange}
281
274
  basicSetup={{
282
275
  lineNumbers: true,
283
276
  foldGutter: false,
@@ -285,7 +278,6 @@ export function CodeViewer({ content, diff, isDiff, filePath, onChange, isModifi
285
278
  highlightSelectionMatches: false,
286
279
  bracketMatching: true,
287
280
  }}
288
- onChange={isEditable ? handleChange : undefined}
289
281
  onCreateEditor={(view) => {
290
282
  editorRef.current = view;
291
283
  }}
package/src/global.css CHANGED
@@ -357,12 +357,6 @@ body {
357
357
  white-space: nowrap;
358
358
  }
359
359
 
360
- .tab-modified {
361
- color: var(--accent-color);
362
- font-weight: bold;
363
- margin-right: 2px;
364
- }
365
-
366
360
  .tab-close {
367
361
  background: none;
368
362
  border: none;
@@ -382,6 +376,16 @@ body {
382
376
  opacity: 1;
383
377
  }
384
378
 
379
+ .tab.modified .tab-name {
380
+ color: var(--accent-color);
381
+ }
382
+
383
+ .tab-modified-mark {
384
+ color: var(--accent-color);
385
+ margin-right: 4px;
386
+ font-size: 10px;
387
+ }
388
+
385
389
  .context-menu {
386
390
  position: fixed;
387
391
  background: linear-gradient(180deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);