codex-lens 0.1.18 → 0.1.20

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-Bno1eqbI.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/main-7-y-Utze.css">
7
+ <script type="module" crossorigin src="./assets/main-CtA0UO3o.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/main-CYNmzqDG.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.18",
3
+ "version": "0.1.20",
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
@@ -9,7 +9,7 @@ import { createLogger } from './lib/logger.js';
9
9
  import { spawnCodex, writeToPty, resizePty, killPty, onPtyData, onPtyExit, getPtyState, getOutputBuffer } from './pty-manager.js';
10
10
  import path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
- import { readFileSync, existsSync } from 'fs';
12
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
13
13
 
14
14
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
15
 
@@ -107,6 +107,25 @@ class Aggregator {
107
107
  res.json({ success: true });
108
108
  });
109
109
 
110
+ app.post('/api/save-file', (req, res) => {
111
+ const { path: filePath, content } = req.body;
112
+ if (!filePath) {
113
+ return res.status(400).json({ error: 'Path is required' });
114
+ }
115
+ if (content === undefined) {
116
+ return res.status(400).json({ error: 'Content is required' });
117
+ }
118
+
119
+ try {
120
+ writeFileSync(filePath, content, 'utf-8');
121
+ logger.info(`File saved: ${filePath}`);
122
+ res.json({ success: true, path: filePath });
123
+ } catch (error) {
124
+ logger.error(`Failed to save file: ${filePath} - ${error.message}`);
125
+ res.status(500).json({ error: error.message });
126
+ }
127
+ });
128
+
110
129
  await new Promise((resolve) => {
111
130
  this.httpServer = createServer(app);
112
131
 
@@ -12,6 +12,7 @@ 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
17
 
17
18
  useEffect(() => {
@@ -66,9 +67,50 @@ export function App() {
66
67
  } else {
67
68
  switchToNextTab();
68
69
  }
70
+ } else if (e.ctrlKey && e.key === 's') {
71
+ e.preventDefault();
72
+ saveCurrentFile();
69
73
  }
70
74
  }
71
75
 
76
+ async function saveCurrentFile() {
77
+ const activeTab = tabs.find(t => t.id === activeTabId);
78
+ if (!activeTab || activeTab.isDiff) return;
79
+
80
+ setSaving(true);
81
+ try {
82
+ const port = window.location.port === '5173' ? '5174' : window.location.port;
83
+ const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:';
84
+ const response = await fetch(`${protocol}//${window.location.hostname}:${port}/api/save-file`, {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ path: activeTab.path, content: activeTab.content })
88
+ });
89
+
90
+ if (response.ok) {
91
+ setTabs(prevTabs => prevTabs.map(t =>
92
+ t.id === activeTabId ? { ...t, isModified: false } : t
93
+ ));
94
+ } else {
95
+ const error = await response.json();
96
+ console.error('Failed to save file:', error);
97
+ }
98
+ } catch (error) {
99
+ console.error('Failed to save file:', error);
100
+ } finally {
101
+ setSaving(false);
102
+ }
103
+ }
104
+
105
+ function handleContentChange(newContent) {
106
+ setTabs(prevTabs => prevTabs.map(t => {
107
+ if (t.id === activeTabId) {
108
+ return { ...t, content: newContent, isModified: true };
109
+ }
110
+ return t;
111
+ }));
112
+ }
113
+
72
114
  function connectWebSocket() {
73
115
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
74
116
  const host = window.location.hostname;
@@ -282,6 +324,8 @@ export function App() {
282
324
  diff={activeTab.diff}
283
325
  isDiff={activeTab.isDiff}
284
326
  filePath={activeTab.path}
327
+ onChange={handleContentChange}
328
+ isModified={activeTab.isModified}
285
329
  />
286
330
  )}
287
331
  </div>
@@ -325,7 +369,10 @@ function TabBar({ tabs, activeTabId, onTabClick, onTabClose, onContextMenu }) {
325
369
  onClick={() => onTabClick(tab.id)}
326
370
  onContextMenu={(e) => onContextMenu(e, tab.id)}
327
371
  >
328
- <span className="tab-name">{tab.name}</span>
372
+ <span className="tab-name">
373
+ {tab.isModified && <span className="tab-modified">*</span>}
374
+ {tab.name}
375
+ </span>
329
376
  <button
330
377
  className="tab-close"
331
378
  onClick={(e) => {
@@ -1,6 +1,6 @@
1
- import React, { useMemo, useRef } from 'react';
1
+ import React, { useMemo, useRef, useState, useCallback } from 'react';
2
2
  import CodeMirror from '@uiw/react-codemirror';
3
- import { EditorView, Decoration, ViewPlugin, ViewUpdate } from '@codemirror/view';
3
+ import { EditorView, Decoration, ViewPlugin, keymap } 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';
@@ -67,6 +67,14 @@ const removedLineDecoration = Decoration.line({
67
67
  attributes: { 'data-diff-type': 'removed' }
68
68
  });
69
69
 
70
+ const addedMarkDecoration = Decoration.mark({
71
+ class: 'cm-diff-added-mark'
72
+ });
73
+
74
+ const removedMarkDecoration = Decoration.mark({
75
+ class: 'cm-diff-removed-mark'
76
+ });
77
+
70
78
  function createDiffHighlightPlugin(diffData) {
71
79
  return ViewPlugin.fromClass(class {
72
80
  decorations;
@@ -96,8 +104,14 @@ function createDiffHighlightPlugin(diffData) {
96
104
  if (diffLine) {
97
105
  if (diffLine.added) {
98
106
  builder.add(line.from, line.from, addedLineDecoration);
107
+ if (line.length > 0) {
108
+ builder.add(line.from, line.from + 1, addedMarkDecoration);
109
+ }
99
110
  } else if (diffLine.removed) {
100
111
  builder.add(line.from, line.from, removedLineDecoration);
112
+ if (line.length > 0) {
113
+ builder.add(line.from, line.from + 1, removedMarkDecoration);
114
+ }
101
115
  }
102
116
  }
103
117
  }
@@ -167,17 +181,17 @@ const darkTheme = EditorView.theme({
167
181
  },
168
182
  '.cm-diff-added-line': {
169
183
  backgroundColor: 'rgba(34, 197, 94, 0.15)',
170
- borderLeft: '3px solid #22c55e',
171
184
  },
172
- '.cm-diff-added-line .cm-gutterElement': {
185
+ '.cm-diff-added-mark': {
173
186
  color: '#4ade80',
187
+ fontWeight: 'bold',
174
188
  },
175
189
  '.cm-diff-removed-line': {
176
190
  backgroundColor: 'rgba(239, 68, 68, 0.15)',
177
- borderLeft: '3px solid #ef4444',
178
191
  },
179
- '.cm-diff-removed-line .cm-gutterElement': {
192
+ '.cm-diff-removed-mark': {
180
193
  color: '#f87171',
194
+ fontWeight: 'bold',
181
195
  },
182
196
  }, { dark: true });
183
197
 
@@ -201,8 +215,11 @@ const darkHighlightStyle = HighlightStyle.define([
201
215
 
202
216
  const syntaxTheme = syntaxHighlighting(darkHighlightStyle);
203
217
 
204
- export function CodeViewer({ content, diff, isDiff, filePath }) {
218
+ export function CodeViewer({ content, diff, isDiff, filePath, onChange, isModified }) {
205
219
  const editorRef = useRef(null);
220
+ const [localContent, setLocalContent] = useState(content || '');
221
+
222
+ const isEditable = !isDiff;
206
223
 
207
224
  const extensions = useMemo(() => {
208
225
  const exts = [
@@ -228,19 +245,39 @@ export function CodeViewer({ content, diff, isDiff, filePath }) {
228
245
 
229
246
  const code = useMemo(() => {
230
247
  if (isDiff && diff) {
231
- return diff.map(line => line.content).join('\n');
248
+ return diff.map(line => {
249
+ if (line.added) {
250
+ return '+' + line.content;
251
+ } else if (line.removed) {
252
+ return '-' + line.content;
253
+ }
254
+ return ' ' + line.content;
255
+ }).join('\n');
232
256
  }
233
257
  return content || '';
234
258
  }, [content, diff, isDiff]);
235
259
 
260
+ const handleChange = useCallback((value) => {
261
+ setLocalContent(value);
262
+ if (onChange) {
263
+ onChange(value);
264
+ }
265
+ }, [onChange]);
266
+
267
+ React.useEffect(() => {
268
+ if (!isDiff) {
269
+ setLocalContent(content || '');
270
+ }
271
+ }, [content, isDiff]);
272
+
236
273
  return (
237
274
  <div className="code-viewer-codemirror">
238
275
  <CodeMirror
239
- value={code}
276
+ value={isDiff ? code : localContent}
240
277
  height="100%"
241
278
  theme={darkTheme}
242
279
  extensions={extensions}
243
- editable={false}
280
+ editable={isEditable}
244
281
  basicSetup={{
245
282
  lineNumbers: true,
246
283
  foldGutter: false,
@@ -248,6 +285,7 @@ export function CodeViewer({ content, diff, isDiff, filePath }) {
248
285
  highlightSelectionMatches: false,
249
286
  bracketMatching: true,
250
287
  }}
288
+ onChange={isEditable ? handleChange : undefined}
251
289
  onCreateEditor={(view) => {
252
290
  editorRef.current = view;
253
291
  }}
package/src/global.css CHANGED
@@ -357,6 +357,12 @@ 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
+
360
366
  .tab-close {
361
367
  background: none;
362
368
  border: none;