diffwatch 2.0.8 → 2.1.0

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/README.md CHANGED
@@ -19,22 +19,6 @@ A TUI app for watching git repository file changes with diffs.
19
19
 
20
20
  ## Installation
21
21
 
22
- ### Prerequisites
23
-
24
- **Bun runtime is required** to run DiffWatch. If you don't have it installed, install it first:
25
-
26
- **macOS/Linux:**
27
- ```bash
28
- curl -fsSL https://bun.sh/install | bash
29
- ```
30
-
31
- **Windows (PowerShell):**
32
- ```powershell
33
- irm bun.sh/install.ps1 | iex
34
- ```
35
-
36
- Or visit [bun.sh](https://bun.sh) for other installation methods.
37
-
38
22
  ### Option 1: Install from npm (recommended)
39
23
 
40
24
  ```bash
@@ -82,18 +66,6 @@ diffwatch [OPTIONS]
82
66
  - `--help` or `-h` - Show help message
83
67
  - `--version` or `-v` - Show version number
84
68
 
85
- ### Examples
86
-
87
- Watch the repository in the current directory:
88
- ```bash
89
- diffwatch
90
- ```
91
-
92
- Watch a specific project:
93
- ```bash
94
- diffwatch --path ~/projects/my-app
95
- ```
96
-
97
69
  ## Keyboard Shortcuts
98
70
 
99
71
  - `↑` / `↓` - Navigate through the file list
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffwatch",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "A TUI app for watching git repository file changes with diffs.",
5
5
  "keywords": [
6
6
  "git",
@@ -10,7 +10,14 @@
10
10
  "cli",
11
11
  "git-gui",
12
12
  "git-diff",
13
- "terminal-ui"
13
+ "terminal-ui",
14
+ "ai",
15
+ "agent",
16
+ "codex",
17
+ "claude",
18
+ "opencode",
19
+ "github-cli",
20
+ "gemini-cli"
14
21
  ],
15
22
  "homepage": "https://github.com/sarfraznawaz2005/diffwatch#readme",
16
23
  "bugs": {
@@ -1,271 +1,271 @@
1
- import React, { useEffect, useState, useMemo, useRef } from 'react';
2
- import { useKeyboard, useRenderer } from '@opentui/react';
3
- import { getRawDiff } from '../utils/git';
4
- import * as Diff2Html from 'diff2html';
5
- import * as fsPromises from 'fs/promises';
6
- import * as path from 'path';
7
- import { POLLING_INTERVAL } from '../constants';
8
- import { isBinaryFile } from 'isbinaryfile';
9
-
10
- interface DiffViewerProps {
11
- filename?: string;
12
- focused: boolean;
13
- searchQuery?: string;
14
- status?: 'modified' | 'new' | 'deleted' | 'renamed' | 'unknown' | 'unstaged' | 'unchanged' | 'ignored';
15
- repoPath?: string;
16
- }
17
-
18
- export function DiffViewer({ filename, focused, searchQuery, status, repoPath }: DiffViewerProps) {
19
- const [rawDiff, setRawDiff] = useState('');
20
- const [fileContent, setFileContent] = useState('');
21
- const [loading, setLoading] = useState(false);
22
- const [isBinary, setIsBinary] = useState(false);
23
- const scrollRef = useRef<any>(null);
24
- const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
25
- const renderer = useRenderer();
26
-
27
- const loadContent = async (showLoading = true) => {
28
- if (!filename) {
29
- setRawDiff('');
30
- setFileContent('');
31
- setIsBinary(false);
32
- return;
33
- }
34
-
35
- if (showLoading) setLoading(true);
36
-
37
- if (status === 'new' || status === 'unchanged') {
38
- try {
39
- const absPath = path.isAbsolute(filename) ? filename : path.join(repoPath || process.cwd(), filename);
40
- const content = await fsPromises.readFile(absPath, 'utf-8');
41
- const binary = await isBinaryFile(absPath);
42
- if (binary) {
43
- setFileContent('');
44
- setRawDiff('');
45
- setIsBinary(true);
46
- } else {
47
- setFileContent(content);
48
- setRawDiff('');
49
- setIsBinary(false);
50
- }
51
- } catch (e) {
52
- setFileContent('');
53
- setIsBinary(false);
54
- }
55
- if (showLoading) setLoading(false);
56
- } else {
57
- const diff = await getRawDiff(filename);
58
- setRawDiff(diff);
59
- setFileContent('');
60
- setIsBinary(false);
61
- if (showLoading) setLoading(false);
62
- }
63
- };
64
-
65
- useEffect(() => {
66
- loadContent(true);
67
- }, [filename, status, repoPath]);
68
-
69
- useEffect(() => {
70
- if (!filename) return;
71
-
72
- // Clear any existing interval to prevent duplicates
73
- if (pollingIntervalRef.current) {
74
- clearInterval(pollingIntervalRef.current);
75
- }
76
-
77
- pollingIntervalRef.current = setInterval(async () => {
78
- await loadContent(false);
79
- }, POLLING_INTERVAL);
80
-
81
- // Cleanup function
82
- return () => {
83
- if (pollingIntervalRef.current) {
84
- clearInterval(pollingIntervalRef.current);
85
- pollingIntervalRef.current = null;
86
- }
87
- };
88
- }, [filename, status, repoPath]);
89
-
90
- const diffData = useMemo(() => {
91
- if (!rawDiff) return null;
92
- const parsed = Diff2Html.parse(rawDiff, {
93
- matching: 'lines'
94
- });
95
- return parsed[0] || null;
96
- }, [rawDiff]);
97
-
98
- useKeyboard((key) => {
99
- if (!focused || !scrollRef.current) return;
100
-
101
- if (key.name === 'up') {
102
- scrollRef.current.scrollTop = Math.max(0, scrollRef.current.scrollTop - 1);
103
- } else if (key.name === 'down') {
104
- scrollRef.current.scrollTop = scrollRef.current.scrollTop + 1;
105
- } else if (key.name === 'pageup') {
106
- scrollRef.current.scrollTop = Math.max(0, scrollRef.current.scrollTop - 10);
107
- } else if (key.name === 'pagedown') {
108
- scrollRef.current.scrollTop = scrollRef.current.scrollTop + 10;
109
- }
110
- });
111
-
112
- const hasChanges = diffData && diffData.blocks.length > 0;
113
- const isNewFile = status === 'new';
114
- const contentLines = useMemo(() => {
115
- if (!fileContent) return [];
116
- return fileContent.split('\n');
117
- }, [fileContent]);
118
-
119
- // Calculate the max line width based on available width in the diff viewer
120
- // The diff viewer takes up 67% of the total width, and we need to account for:
121
- // - Line numbers (6 chars: " 1234: ")
122
- // - Prefix (+/-/space) (1 char)
123
- // - Borders (2 chars: left and right)
124
- // - Some padding (1 char)
125
- // Total overhead: 6 (line numbers) + 1 (prefix) + 2 (borders) = 9 chars
126
- const calculateMaxLineWidth = (): number => {
127
- const estimatedWidth = Math.floor((renderer.width * 0.67) - 9); // 9 chars for line numbers, prefix, and borders
128
- return Math.max(20, estimatedWidth); // minimum width of 20
129
- };
130
-
131
- // Soft wrap lines based on available width
132
- const wrapLine = (text: string, maxLength: number): string[] => {
133
- if (text.length <= maxLength) return [text];
134
-
135
- const lines: string[] = [];
136
- let remaining = text;
137
-
138
- while (remaining.length > 0) {
139
- if (remaining.length <= maxLength) {
140
- lines.push(remaining);
141
- break;
142
- }
143
-
144
- // Find best break point (prefer spaces)
145
- let breakIndex = maxLength;
146
- const lastSpace = remaining.lastIndexOf(' ', maxLength);
147
-
148
- if (lastSpace > maxLength * 0.5) {
149
- breakIndex = lastSpace + 1;
150
- }
151
-
152
- lines.push(remaining.substring(0, breakIndex));
153
- remaining = remaining.substring(breakIndex);
154
- }
155
-
156
- return lines;
157
- };
158
-
159
- const maxLineWidth = calculateMaxLineWidth();
160
-
161
- return (
162
- <box
163
- border
164
- title={` Diff (${filename || 'None'}) `}
165
- width="67%"
166
- borderColor={focused ? 'yellow' : 'grey'}
167
- flexDirection="column"
168
- >
169
- {loading ? (
170
- <text>Loading diff...</text>
171
- ) : !filename ? (
172
- <text>Select a file to view changes.</text>
173
- ) : isBinary ? (
174
- <text fg="gray">Binary file - content not displayed.</text>
175
- ) : isNewFile ? (
176
- <scrollbox flexGrow={1} ref={scrollRef}>
177
- {contentLines.flatMap((line, i) => {
178
- const wrappedLines = wrapLine(line, maxLineWidth);
179
-
180
- return wrappedLines.map((wrappedLine, wrapIdx) => {
181
- const isFirstLine = wrapIdx === 0;
182
- let renderedParts: React.ReactNode = wrappedLine;
183
-
184
- if (searchQuery && line.toLowerCase().includes(searchQuery.toLowerCase())) {
185
- // Sanitize search query for regex to prevent ReDoS attacks
186
- const sanitizedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
187
- const parts = wrappedLine.split(new RegExp(`(${sanitizedQuery})`, 'gi'));
188
- renderedParts = parts.map((part, idx) =>
189
- part.toLowerCase() === searchQuery.toLowerCase()
190
- ? <span key={idx} fg="black" bg="yellow">{part}</span>
191
- : part
192
- );
193
- }
194
-
195
- return (
196
- <box key={`${i}-${wrapIdx}`} flexDirection="row" height={1}>
197
- {isFirstLine ? (
198
- <text fg="gray" width={6}>{`${String(i + 1).padStart(4)}: `}</text>
199
- ) : (
200
- <text fg="gray" width={6}> </text>
201
- )}
202
- <text fg="green" flexGrow={1}>
203
- {renderedParts}
204
- </text>
205
- </box>
206
- );
207
- });
208
- })}
209
- </scrollbox>
210
- ) : !hasChanges ? (
211
- <text fg="gray">No changes detected.</text>
212
- ) : (
213
- <scrollbox flexGrow={1} ref={scrollRef}>
214
- {diffData.blocks.map((block, bIdx) => (
215
- <box key={bIdx} flexDirection="column" marginBottom={1}>
216
- <text fg="cyan">{` ${block.header}`}</text>
217
- <text fg="cyan">{` ${block.header}`}</text>
218
- {block.lines.map((line, lIdx) => {
219
- let fg = 'white';
220
- let prefix = ' ';
221
- if (line.type === 'insert') { fg = 'green'; prefix = '+'; }
222
- else if (line.type === 'delete') { fg = 'brightRed'; prefix = '-'; }
223
-
224
- const ln = line.type === 'insert' ? line.newNumber : line.oldNumber;
225
- const content = line.content.substring(1);
226
-
227
- const lnText = ln ? `${String(ln).padStart(4)}: ` : ' ';
228
-
229
- // Wrap content if it's too long
230
- const wrappedContent = wrapLine(content, maxLineWidth);
231
-
232
- return (
233
- <>
234
- {wrappedContent.map((wrappedLine, wrapIdx) => {
235
- const isFirstLine = wrapIdx === 0;
236
-
237
- let renderedParts: React.ReactNode;
238
- if (searchQuery && content.toLowerCase().includes(searchQuery.toLowerCase())) {
239
- // Sanitize search query for regex to prevent ReDoS attacks
240
- const sanitizedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
- const parts = wrappedLine.split(new RegExp(`(${sanitizedQuery})`, 'gi'));
242
- renderedParts = parts.map((part, i) =>
243
- part.toLowerCase() === searchQuery.toLowerCase()
244
- ? <span key={i} fg="black" bg="yellow">{part}</span>
245
- : part
246
- );
247
- } else {
248
- renderedParts = wrappedLine;
249
- }
250
-
251
- return (
252
- <box key={`${bIdx}-${lIdx}-${wrapIdx}`} flexDirection="row" height={1}>
253
- <text fg="gray" width={lnText.length}>
254
- {isFirstLine ? lnText : ' '}
255
- </text>
256
- <text fg={fg} flexGrow={1}>
257
- {isFirstLine ? `${prefix}${renderedParts}` : ` ${renderedParts}`}
258
- </text>
259
- </box>
260
- );
261
- })}
262
- </>
263
- );
264
- })}
265
- </box>
266
- ))}
267
- </scrollbox>
268
- )}
269
- </box>
270
- );
271
- }
1
+ import React, { useEffect, useState, useMemo, useRef } from 'react';
2
+ import { useKeyboard, useRenderer } from '@opentui/react';
3
+ import { getRawDiff } from '../utils/git';
4
+ import * as Diff2Html from 'diff2html';
5
+ import * as fsPromises from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { POLLING_INTERVAL } from '../constants';
8
+ import { isBinaryFile } from 'isbinaryfile';
9
+
10
+ interface DiffViewerProps {
11
+ filename?: string;
12
+ focused: boolean;
13
+ searchQuery?: string;
14
+ status?: 'modified' | 'new' | 'deleted' | 'renamed' | 'unknown' | 'unstaged' | 'unchanged' | 'ignored';
15
+ repoPath?: string;
16
+ }
17
+
18
+ export function DiffViewer({ filename, focused, searchQuery, status, repoPath }: DiffViewerProps) {
19
+ const [rawDiff, setRawDiff] = useState('');
20
+ const [fileContent, setFileContent] = useState('');
21
+ const [loading, setLoading] = useState(false);
22
+ const [isBinary, setIsBinary] = useState(false);
23
+ const scrollRef = useRef<any>(null);
24
+ const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
25
+ const renderer = useRenderer();
26
+
27
+ const loadContent = async (showLoading = true) => {
28
+ if (!filename) {
29
+ setRawDiff('');
30
+ setFileContent('');
31
+ setIsBinary(false);
32
+ return;
33
+ }
34
+
35
+ if (showLoading) setLoading(true);
36
+
37
+ if (status === 'new' || status === 'unchanged') {
38
+ try {
39
+ const absPath = path.isAbsolute(filename) ? filename : path.join(repoPath || process.cwd(), filename);
40
+ const content = await fsPromises.readFile(absPath, 'utf-8');
41
+ const binary = await isBinaryFile(absPath);
42
+ if (binary) {
43
+ setFileContent('');
44
+ setRawDiff('');
45
+ setIsBinary(true);
46
+ } else {
47
+ setFileContent(content);
48
+ setRawDiff('');
49
+ setIsBinary(false);
50
+ }
51
+ } catch (e) {
52
+ setFileContent('');
53
+ setIsBinary(false);
54
+ }
55
+ if (showLoading) setLoading(false);
56
+ } else {
57
+ const diff = await getRawDiff(filename);
58
+ setRawDiff(diff);
59
+ setFileContent('');
60
+ setIsBinary(false);
61
+ if (showLoading) setLoading(false);
62
+ }
63
+ };
64
+
65
+ useEffect(() => {
66
+ loadContent(true);
67
+ }, [filename, status, repoPath]);
68
+
69
+ useEffect(() => {
70
+ if (!filename) return;
71
+
72
+ // Clear any existing interval to prevent duplicates
73
+ if (pollingIntervalRef.current) {
74
+ clearInterval(pollingIntervalRef.current);
75
+ }
76
+
77
+ pollingIntervalRef.current = setInterval(async () => {
78
+ await loadContent(false);
79
+ }, POLLING_INTERVAL);
80
+
81
+ // Cleanup function
82
+ return () => {
83
+ if (pollingIntervalRef.current) {
84
+ clearInterval(pollingIntervalRef.current);
85
+ pollingIntervalRef.current = null;
86
+ }
87
+ };
88
+ }, [filename, status, repoPath]);
89
+
90
+ const diffData = useMemo(() => {
91
+ if (!rawDiff) return null;
92
+ const parsed = Diff2Html.parse(rawDiff, {
93
+ matching: 'lines'
94
+ });
95
+ return parsed[0] || null;
96
+ }, [rawDiff]);
97
+
98
+ useKeyboard((key) => {
99
+ if (!focused || !scrollRef.current) return;
100
+
101
+ if (key.name === 'up') {
102
+ scrollRef.current.scrollTop = Math.max(0, scrollRef.current.scrollTop - 1);
103
+ } else if (key.name === 'down') {
104
+ scrollRef.current.scrollTop = scrollRef.current.scrollTop + 1;
105
+ } else if (key.name === 'pageup') {
106
+ scrollRef.current.scrollTop = Math.max(0, scrollRef.current.scrollTop - 10);
107
+ } else if (key.name === 'pagedown') {
108
+ scrollRef.current.scrollTop = scrollRef.current.scrollTop + 10;
109
+ }
110
+ });
111
+
112
+ const hasChanges = diffData && diffData.blocks.length > 0;
113
+ const isNewFile = status === 'new';
114
+ const contentLines = useMemo(() => {
115
+ if (!fileContent) return [];
116
+ return fileContent.split('\n');
117
+ }, [fileContent]);
118
+
119
+ // Calculate the max line width based on available width in the diff viewer
120
+ // The diff viewer takes up 70% of the total width, and we need to account for:
121
+ // - Line numbers (6 chars: " 1234: ")
122
+ // - Prefix (+/-/space) (1 char)
123
+ // - Borders (2 chars: left and right)
124
+ // - Some padding (1 char)
125
+ // Total overhead: 6 (line numbers) + 1 (prefix) + 2 (borders) = 9 chars
126
+ const calculateMaxLineWidth = (): number => {
127
+ const estimatedWidth = Math.floor((renderer.width * 0.70) - 9); // 9 chars for line numbers, prefix, and borders
128
+ return Math.max(20, estimatedWidth); // minimum width of 20
129
+ };
130
+
131
+ // Soft wrap lines based on available width
132
+ const wrapLine = (text: string, maxLength: number): string[] => {
133
+ if (text.length <= maxLength) return [text];
134
+
135
+ const lines: string[] = [];
136
+ let remaining = text;
137
+
138
+ while (remaining.length > 0) {
139
+ if (remaining.length <= maxLength) {
140
+ lines.push(remaining);
141
+ break;
142
+ }
143
+
144
+ // Find best break point (prefer spaces)
145
+ let breakIndex = maxLength;
146
+ const lastSpace = remaining.lastIndexOf(' ', maxLength);
147
+
148
+ if (lastSpace > maxLength * 0.5) {
149
+ breakIndex = lastSpace + 1;
150
+ }
151
+
152
+ lines.push(remaining.substring(0, breakIndex));
153
+ remaining = remaining.substring(breakIndex);
154
+ }
155
+
156
+ return lines;
157
+ };
158
+
159
+ const maxLineWidth = calculateMaxLineWidth();
160
+
161
+ return (
162
+ <box
163
+ border
164
+ title={` Diff (${filename || 'None'}) `}
165
+ width="70%"
166
+ borderColor={focused ? 'yellow' : 'grey'}
167
+ flexDirection="column"
168
+ >
169
+ {loading ? (
170
+ <text>Loading diff...</text>
171
+ ) : !filename ? (
172
+ <text>Select a file to view changes.</text>
173
+ ) : isBinary ? (
174
+ <text fg="gray">Binary file - content not displayed.</text>
175
+ ) : isNewFile ? (
176
+ <scrollbox flexGrow={1} ref={scrollRef}>
177
+ {contentLines.flatMap((line, i) => {
178
+ const wrappedLines = wrapLine(line, maxLineWidth);
179
+
180
+ return wrappedLines.map((wrappedLine, wrapIdx) => {
181
+ const isFirstLine = wrapIdx === 0;
182
+ let renderedParts: React.ReactNode = wrappedLine;
183
+
184
+ if (searchQuery && line.toLowerCase().includes(searchQuery.toLowerCase())) {
185
+ // Sanitize search query for regex to prevent ReDoS attacks
186
+ const sanitizedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
187
+ const parts = wrappedLine.split(new RegExp(`(${sanitizedQuery})`, 'gi'));
188
+ renderedParts = parts.map((part, idx) =>
189
+ part.toLowerCase() === searchQuery.toLowerCase()
190
+ ? <span key={idx} fg="black" bg="yellow">{part}</span>
191
+ : part
192
+ );
193
+ }
194
+
195
+ return (
196
+ <box key={`${i}-${wrapIdx}`} flexDirection="row" height={1}>
197
+ {isFirstLine ? (
198
+ <text fg="gray" width={6}>{`${String(i + 1).padStart(4)}: `}</text>
199
+ ) : (
200
+ <text fg="gray" width={6}> </text>
201
+ )}
202
+ <text fg="brightGreen" flexGrow={1}>
203
+ {renderedParts}
204
+ </text>
205
+ </box>
206
+ );
207
+ });
208
+ })}
209
+ </scrollbox>
210
+ ) : !hasChanges ? (
211
+ <text fg="gray">No changes detected.</text>
212
+ ) : (
213
+ <scrollbox flexGrow={1} ref={scrollRef}>
214
+ {diffData.blocks.map((block, bIdx) => (
215
+ <box key={bIdx} flexDirection="column" marginBottom={1}>
216
+ <text fg="cyan">{` ${block.header}`}</text>
217
+ <text fg="cyan">{` ${block.header}`}</text>
218
+ {block.lines.map((line, lIdx) => {
219
+ let fg = 'white';
220
+ let prefix = ' ';
221
+ if (line.type === 'insert') { fg = 'brightGreen'; prefix = '+'; }
222
+ else if (line.type === 'delete') { fg = 'brightRed'; prefix = '-'; }
223
+
224
+ const ln = line.type === 'insert' ? line.newNumber : line.oldNumber;
225
+ const content = line.content.substring(1);
226
+
227
+ const lnText = ln ? `${String(ln).padStart(4)}: ` : ' ';
228
+
229
+ // Wrap content if it's too long
230
+ const wrappedContent = wrapLine(content, maxLineWidth);
231
+
232
+ return (
233
+ <>
234
+ {wrappedContent.map((wrappedLine, wrapIdx) => {
235
+ const isFirstLine = wrapIdx === 0;
236
+
237
+ let renderedParts: React.ReactNode;
238
+ if (searchQuery && content.toLowerCase().includes(searchQuery.toLowerCase())) {
239
+ // Sanitize search query for regex to prevent ReDoS attacks
240
+ const sanitizedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
+ const parts = wrappedLine.split(new RegExp(`(${sanitizedQuery})`, 'gi'));
242
+ renderedParts = parts.map((part, i) =>
243
+ part.toLowerCase() === searchQuery.toLowerCase()
244
+ ? <span key={i} fg="black" bg="yellow">{part}</span>
245
+ : part
246
+ );
247
+ } else {
248
+ renderedParts = wrappedLine;
249
+ }
250
+
251
+ return (
252
+ <box key={`${bIdx}-${lIdx}-${wrapIdx}`} flexDirection="row" height={1}>
253
+ <text fg="gray" width={lnText.length}>
254
+ {isFirstLine ? lnText : ' '}
255
+ </text>
256
+ <text fg={fg} flexGrow={1}>
257
+ {isFirstLine ? `${prefix}${renderedParts}` : ` ${renderedParts}`}
258
+ </text>
259
+ </box>
260
+ );
261
+ })}
262
+ </>
263
+ );
264
+ })}
265
+ </box>
266
+ ))}
267
+ </scrollbox>
268
+ )}
269
+ </box>
270
+ );
271
+ }
@@ -31,13 +31,13 @@ export function FileList({ files, selectedIndex, focused, searchQuery, onSelect,
31
31
  // Calculate the max length for file paths based on available width
32
32
  // Account for the symbol, space, and padding
33
33
  const calculateMaxPathLength = (): number => {
34
- // Get the width of the container (33% of total width)
34
+ // Get the width of the container (30% of total width)
35
35
  // Since we can't directly measure the rendered width, we'll estimate based on a percentage
36
- // considering that the container is 33% of the total width
36
+ // considering that the container is 30% of the total width
37
37
  // and we need to account for the symbol (1 char) + space (1 char) before the filename
38
38
  // Also account for borders (left and right = 2 chars total)
39
39
  // Total overhead: 1 (symbol) + 1 (space after symbol) + 2 (borders) = 4 chars
40
- const estimatedWidth = Math.floor((renderer.width * 0.33) - 4); // 4 chars for symbol + space + borders
40
+ const estimatedWidth = Math.floor((renderer.width * 0.30) - 4); // 4 chars for symbol + space + borders
41
41
  return Math.max(10, estimatedWidth); // minimum length of 10
42
42
  };
43
43
 
@@ -62,7 +62,7 @@ export function FileList({ files, selectedIndex, focused, searchQuery, onSelect,
62
62
  <box
63
63
  border
64
64
  title={title}
65
- width="33%"
65
+ width="30%"
66
66
  height="100%"
67
67
  borderColor={focused ? 'yellow' : 'grey'}
68
68
  flexDirection="column"
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const BUILD_VERSION = "2.0.8";
1
+ export const BUILD_VERSION = "2.1.0";