codeep 1.1.20 → 1.1.22
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/renderer/App.d.ts +6 -2
- package/dist/renderer/App.js +95 -53
- package/dist/renderer/Screen.js +18 -3
- package/dist/renderer/ansi.d.ts +9 -0
- package/dist/renderer/ansi.js +84 -8
- package/dist/renderer/index.d.ts +1 -1
- package/dist/renderer/main.js +7 -0
- package/package.json +1 -1
package/dist/renderer/App.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface Message {
|
|
|
8
8
|
role: 'user' | 'assistant' | 'system';
|
|
9
9
|
content: string;
|
|
10
10
|
}
|
|
11
|
-
export type AppScreen = 'chat' | 'status';
|
|
12
11
|
export interface ConfirmOptions {
|
|
13
12
|
title: string;
|
|
14
13
|
message: string[];
|
|
@@ -34,7 +33,6 @@ export declare class App {
|
|
|
34
33
|
private streamingContent;
|
|
35
34
|
private isStreaming;
|
|
36
35
|
private isLoading;
|
|
37
|
-
private currentScreen;
|
|
38
36
|
private options;
|
|
39
37
|
private scrollOffset;
|
|
40
38
|
private notification;
|
|
@@ -49,6 +47,7 @@ export declare class App {
|
|
|
49
47
|
private pasteInfoOpen;
|
|
50
48
|
private helpOpen;
|
|
51
49
|
private helpScrollIndex;
|
|
50
|
+
private statusOpen;
|
|
52
51
|
private settingsState;
|
|
53
52
|
private showAutocomplete;
|
|
54
53
|
private autocompleteIndex;
|
|
@@ -278,6 +277,10 @@ export declare class App {
|
|
|
278
277
|
* Update autocomplete suggestions
|
|
279
278
|
*/
|
|
280
279
|
private updateAutocomplete;
|
|
280
|
+
/**
|
|
281
|
+
* Handle inline status keys
|
|
282
|
+
*/
|
|
283
|
+
private handleInlineStatusKey;
|
|
281
284
|
/**
|
|
282
285
|
* Handle help screen keys
|
|
283
286
|
*/
|
|
@@ -347,6 +350,7 @@ export declare class App {
|
|
|
347
350
|
/**
|
|
348
351
|
* Render inline help below status bar
|
|
349
352
|
*/
|
|
353
|
+
private renderInlineStatus;
|
|
350
354
|
private renderInlineHelp;
|
|
351
355
|
/**
|
|
352
356
|
* Render inline autocomplete below status bar
|
package/dist/renderer/App.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Screen } from './Screen.js';
|
|
6
6
|
import { Input, LineEditor } from './Input.js';
|
|
7
|
-
import { fg, style } from './ansi.js';
|
|
7
|
+
import { fg, style, stringWidth } from './ansi.js';
|
|
8
8
|
import clipboardy from 'clipboardy';
|
|
9
9
|
// Primary color: #f02a30 (Codeep red)
|
|
10
10
|
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
@@ -30,11 +30,14 @@ const KEYWORDS = {
|
|
|
30
30
|
go: ['func', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'break', 'continue', 'fallthrough', 'default', 'go', 'select', 'chan', 'defer', 'panic', 'recover', 'type', 'struct', 'interface', 'map', 'package', 'import', 'const', 'var', 'nil', 'true', 'false', 'iota', 'make', 'new', 'append', 'len', 'cap', 'copy', 'delete'],
|
|
31
31
|
rust: ['fn', 'let', 'mut', 'const', 'static', 'return', 'if', 'else', 'match', 'for', 'while', 'loop', 'break', 'continue', 'struct', 'enum', 'trait', 'impl', 'type', 'where', 'use', 'mod', 'pub', 'crate', 'self', 'super', 'async', 'await', 'move', 'ref', 'true', 'false', 'Some', 'None', 'Ok', 'Err', 'Self', 'dyn', 'unsafe', 'extern'],
|
|
32
32
|
sh: ['if', 'then', 'else', 'elif', 'fi', 'case', 'esac', 'for', 'while', 'until', 'do', 'done', 'in', 'function', 'return', 'local', 'export', 'readonly', 'declare', 'typeset', 'unset', 'shift', 'exit', 'break', 'continue', 'source', 'alias', 'echo', 'printf', 'read', 'test', 'true', 'false'],
|
|
33
|
+
html: ['html', 'head', 'body', 'div', 'span', 'p', 'a', 'img', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'tr', 'td', 'th', 'form', 'input', 'button', 'select', 'option', 'textarea', 'label', 'section', 'article', 'nav', 'header', 'footer', 'main', 'aside', 'meta', 'link', 'script', 'style', 'title', 'DOCTYPE'],
|
|
34
|
+
css: ['import', 'media', 'keyframes', 'font-face', 'supports', 'charset', 'namespace', 'page', 'inherit', 'initial', 'unset', 'none', 'auto', 'block', 'inline', 'flex', 'grid', 'absolute', 'relative', 'fixed', 'sticky', 'static', 'hidden', 'visible', 'solid', 'dashed', 'dotted', 'transparent', 'important'],
|
|
33
35
|
};
|
|
34
36
|
// Map language aliases
|
|
35
37
|
const LANG_ALIASES = {
|
|
36
38
|
javascript: 'js', typescript: 'ts', python: 'py', golang: 'go',
|
|
37
39
|
bash: 'sh', shell: 'sh', zsh: 'sh', tsx: 'ts', jsx: 'js',
|
|
40
|
+
htm: 'html', scss: 'css', sass: 'css', less: 'css',
|
|
38
41
|
};
|
|
39
42
|
/**
|
|
40
43
|
* Syntax highlighter for code with better token handling
|
|
@@ -42,6 +45,20 @@ const LANG_ALIASES = {
|
|
|
42
45
|
function highlightCode(code, lang) {
|
|
43
46
|
const normalizedLang = LANG_ALIASES[lang.toLowerCase()] || lang.toLowerCase();
|
|
44
47
|
const keywords = KEYWORDS[normalizedLang] || KEYWORDS['js'] || [];
|
|
48
|
+
// HTML: highlight tags, attributes, and values
|
|
49
|
+
if (normalizedLang === 'html' || normalizedLang === 'xml' || normalizedLang === 'svg') {
|
|
50
|
+
return code.replace(/(<\/?)(\w[\w-]*)((?:\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|\S+))?)*)(\s*\/?>)/g, (_match, open, tag, attrs, close) => {
|
|
51
|
+
const highlightedAttrs = attrs.replace(/([\w-]+)(=)("[^"]*"|'[^']*')/g, (_m, attr, eq, val) => SYNTAX.function + attr + '\x1b[0m' + SYNTAX.operator + eq + '\x1b[0m' + SYNTAX.string + val + '\x1b[0m');
|
|
52
|
+
return SYNTAX.punctuation + open + '\x1b[0m' + SYNTAX.keyword + tag + '\x1b[0m' + highlightedAttrs + SYNTAX.punctuation + close + '\x1b[0m';
|
|
53
|
+
}).replace(/<!--[\s\S]*?-->/g, (comment) => SYNTAX.comment + comment + '\x1b[0m');
|
|
54
|
+
}
|
|
55
|
+
// CSS: highlight selectors, properties, and values
|
|
56
|
+
if (normalizedLang === 'css') {
|
|
57
|
+
return code
|
|
58
|
+
.replace(/\/\*[\s\S]*?\*\//g, (comment) => SYNTAX.comment + comment + '\x1b[0m')
|
|
59
|
+
.replace(/([\w-]+)(\s*:\s*)([^;{}]+)/g, (_m, prop, colon, val) => SYNTAX.function + prop + '\x1b[0m' + colon + SYNTAX.string + val + '\x1b[0m')
|
|
60
|
+
.replace(/([.#]?[\w-]+(?:\s*[,>+~]\s*[.#]?[\w-]+)*)\s*\{/g, (match, selector) => SYNTAX.keyword + selector + '\x1b[0m' + ' {');
|
|
61
|
+
}
|
|
45
62
|
// Tokenize and highlight
|
|
46
63
|
let result = '';
|
|
47
64
|
let i = 0;
|
|
@@ -202,7 +219,6 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
202
219
|
'learn': 'Learn code preferences',
|
|
203
220
|
};
|
|
204
221
|
import { helpCategories, keyboardShortcuts } from './components/Help.js';
|
|
205
|
-
import { renderStatusScreen } from './components/Status.js';
|
|
206
222
|
import { handleSettingsKey, SETTINGS } from './components/Settings.js';
|
|
207
223
|
export class App {
|
|
208
224
|
screen;
|
|
@@ -212,7 +228,6 @@ export class App {
|
|
|
212
228
|
streamingContent = '';
|
|
213
229
|
isStreaming = false;
|
|
214
230
|
isLoading = false;
|
|
215
|
-
currentScreen = 'chat';
|
|
216
231
|
options;
|
|
217
232
|
scrollOffset = 0;
|
|
218
233
|
notification = '';
|
|
@@ -231,6 +246,8 @@ export class App {
|
|
|
231
246
|
// Inline help state
|
|
232
247
|
helpOpen = false;
|
|
233
248
|
helpScrollIndex = 0;
|
|
249
|
+
// Inline status state
|
|
250
|
+
statusOpen = false;
|
|
234
251
|
// Settings screen state
|
|
235
252
|
settingsState = {
|
|
236
253
|
selectedIndex: 0,
|
|
@@ -784,19 +801,7 @@ export class App {
|
|
|
784
801
|
this.options.onExit();
|
|
785
802
|
return;
|
|
786
803
|
}
|
|
787
|
-
|
|
788
|
-
switch (this.currentScreen) {
|
|
789
|
-
case 'status':
|
|
790
|
-
if (event.key === 'escape' || event.key === 'q') {
|
|
791
|
-
this.currentScreen = 'chat';
|
|
792
|
-
this.render();
|
|
793
|
-
}
|
|
794
|
-
break;
|
|
795
|
-
case 'chat':
|
|
796
|
-
default:
|
|
797
|
-
this.handleChatKey(event);
|
|
798
|
-
break;
|
|
799
|
-
}
|
|
804
|
+
this.handleChatKey(event);
|
|
800
805
|
}
|
|
801
806
|
/**
|
|
802
807
|
* Handle chat screen keys
|
|
@@ -822,6 +827,11 @@ export class App {
|
|
|
822
827
|
this.handleInlineConfirmKey(event);
|
|
823
828
|
return;
|
|
824
829
|
}
|
|
830
|
+
// If status is open, handle status keys first
|
|
831
|
+
if (this.statusOpen) {
|
|
832
|
+
this.handleInlineStatusKey(event);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
825
835
|
// If help is open, handle help keys first
|
|
826
836
|
if (this.helpOpen) {
|
|
827
837
|
this.handleInlineHelpKey(event);
|
|
@@ -1036,6 +1046,15 @@ export class App {
|
|
|
1036
1046
|
this.autocompleteItems = [];
|
|
1037
1047
|
}
|
|
1038
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Handle inline status keys
|
|
1051
|
+
*/
|
|
1052
|
+
handleInlineStatusKey(event) {
|
|
1053
|
+
if (event.key === 'escape' || event.key === 'q') {
|
|
1054
|
+
this.statusOpen = false;
|
|
1055
|
+
this.render();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1039
1058
|
/**
|
|
1040
1059
|
* Handle help screen keys
|
|
1041
1060
|
*/
|
|
@@ -1515,7 +1534,7 @@ export class App {
|
|
|
1515
1534
|
this.render();
|
|
1516
1535
|
break;
|
|
1517
1536
|
case 'status':
|
|
1518
|
-
this.
|
|
1537
|
+
this.statusOpen = true;
|
|
1519
1538
|
this.render();
|
|
1520
1539
|
break;
|
|
1521
1540
|
case 'clear':
|
|
@@ -1542,15 +1561,7 @@ export class App {
|
|
|
1542
1561
|
this.renderIntro();
|
|
1543
1562
|
return;
|
|
1544
1563
|
}
|
|
1545
|
-
|
|
1546
|
-
case 'status':
|
|
1547
|
-
renderStatusScreen(this.screen, this.options.getStatus());
|
|
1548
|
-
break;
|
|
1549
|
-
case 'chat':
|
|
1550
|
-
default:
|
|
1551
|
-
this.renderChat();
|
|
1552
|
-
break;
|
|
1553
|
-
}
|
|
1564
|
+
this.renderChat();
|
|
1554
1565
|
}
|
|
1555
1566
|
/**
|
|
1556
1567
|
* Render chat screen
|
|
@@ -1603,31 +1614,12 @@ export class App {
|
|
|
1603
1614
|
bottomPanelHeight = Math.min(this.autocompleteItems.length + 3, 12);
|
|
1604
1615
|
}
|
|
1605
1616
|
const mainHeight = height - bottomPanelHeight;
|
|
1606
|
-
// Determine if we have enough space for logo (need at least 20 lines)
|
|
1607
|
-
const showLogo = height >= 24;
|
|
1608
|
-
const logoSpace = showLogo ? LOGO_HEIGHT + 1 : 1; // +1 for tagline or simple header
|
|
1609
1617
|
// Layout - main UI takes top portion
|
|
1610
|
-
const
|
|
1611
|
-
const messagesStart = logoSpace;
|
|
1618
|
+
const messagesStart = 0;
|
|
1612
1619
|
const messagesEnd = mainHeight - 4;
|
|
1613
1620
|
const separatorLine = mainHeight - 3;
|
|
1614
1621
|
const inputLine = mainHeight - 2;
|
|
1615
1622
|
const statusLine = mainHeight - 1;
|
|
1616
|
-
// Header - show logo if space permits, otherwise simple text
|
|
1617
|
-
if (showLogo) {
|
|
1618
|
-
// Center the logo
|
|
1619
|
-
const logoWidth = LOGO_LINES[0].length;
|
|
1620
|
-
const logoX = Math.max(0, Math.floor((width - logoWidth) / 2));
|
|
1621
|
-
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
1622
|
-
this.screen.write(logoX, headerLine + i, LOGO_LINES[i], PRIMARY_COLOR);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
else {
|
|
1626
|
-
// Simple header for small terminals
|
|
1627
|
-
const header = ' Codeep ';
|
|
1628
|
-
const headerPadding = '─'.repeat(Math.max(0, (width - header.length) / 2));
|
|
1629
|
-
this.screen.writeLine(headerLine, headerPadding + header + headerPadding, PRIMARY_COLOR);
|
|
1630
|
-
}
|
|
1631
1623
|
// Messages
|
|
1632
1624
|
const messagesHeight = messagesEnd - messagesStart + 1;
|
|
1633
1625
|
const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
|
|
@@ -1662,6 +1654,10 @@ export class App {
|
|
|
1662
1654
|
if (this.helpOpen) {
|
|
1663
1655
|
this.renderInlineHelp(statusLine + 1, width, height - statusLine - 1);
|
|
1664
1656
|
}
|
|
1657
|
+
// Inline status renders BELOW status bar
|
|
1658
|
+
if (this.statusOpen) {
|
|
1659
|
+
this.renderInlineStatus(statusLine + 1, width);
|
|
1660
|
+
}
|
|
1665
1661
|
// Inline search renders BELOW status bar
|
|
1666
1662
|
if (this.searchOpen) {
|
|
1667
1663
|
this.renderInlineSearch(statusLine + 1, width, height - statusLine - 1);
|
|
@@ -1946,6 +1942,35 @@ export class App {
|
|
|
1946
1942
|
/**
|
|
1947
1943
|
* Render inline help below status bar
|
|
1948
1944
|
*/
|
|
1945
|
+
renderInlineStatus(startY, width) {
|
|
1946
|
+
const status = this.options.getStatus();
|
|
1947
|
+
let y = startY;
|
|
1948
|
+
// Separator line
|
|
1949
|
+
this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
|
|
1950
|
+
// Title
|
|
1951
|
+
this.screen.writeLine(y++, 'Status', PRIMARY_COLOR + style.bold);
|
|
1952
|
+
const items = [
|
|
1953
|
+
{ label: 'Version', value: 'v' + status.version, color: fg.white },
|
|
1954
|
+
{ label: 'Provider', value: status.provider, color: fg.white },
|
|
1955
|
+
{ label: 'Model', value: status.model, color: fg.white },
|
|
1956
|
+
{ label: 'Agent Mode', value: status.agentMode.toUpperCase(), color: status.agentMode === 'on' ? fg.green : status.agentMode === 'manual' ? fg.yellow : fg.gray },
|
|
1957
|
+
{ label: 'Project', value: status.projectPath, color: fg.white },
|
|
1958
|
+
{ label: 'Write Access', value: status.hasWriteAccess ? 'Yes' : 'No', color: status.hasWriteAccess ? fg.green : fg.red },
|
|
1959
|
+
{ label: 'Session', value: status.sessionId || 'New', color: fg.white },
|
|
1960
|
+
{ label: 'Messages', value: status.messageCount.toString(), color: fg.white },
|
|
1961
|
+
{ label: 'Platform', value: process.platform, color: fg.white },
|
|
1962
|
+
{ label: 'Node', value: process.version, color: fg.white },
|
|
1963
|
+
{ label: 'Terminal', value: width + 'x' + this.screen.getSize().height, color: fg.white },
|
|
1964
|
+
];
|
|
1965
|
+
const labelWidth = Math.max(...items.map(i => i.label.length)) + 2;
|
|
1966
|
+
for (const item of items) {
|
|
1967
|
+
this.screen.write(2, y, item.label + ':', fg.gray);
|
|
1968
|
+
this.screen.write(2 + labelWidth, y, item.value, item.color);
|
|
1969
|
+
y++;
|
|
1970
|
+
}
|
|
1971
|
+
y++;
|
|
1972
|
+
this.screen.writeLine(y, 'Esc close', fg.gray);
|
|
1973
|
+
}
|
|
1949
1974
|
renderInlineHelp(startY, width, availableHeight) {
|
|
1950
1975
|
// Build all help items
|
|
1951
1976
|
const allItems = [];
|
|
@@ -2326,6 +2351,20 @@ export class App {
|
|
|
2326
2351
|
*/
|
|
2327
2352
|
getVisibleMessages(height, width) {
|
|
2328
2353
|
const allLines = [];
|
|
2354
|
+
// Logo at the top, scrolls with content
|
|
2355
|
+
if (height >= 20) {
|
|
2356
|
+
const logoWidth = LOGO_LINES[0].length;
|
|
2357
|
+
const logoX = Math.max(0, Math.floor((width - logoWidth) / 2));
|
|
2358
|
+
const pad = ' '.repeat(logoX);
|
|
2359
|
+
for (const line of LOGO_LINES) {
|
|
2360
|
+
allLines.push({ text: pad + line, style: PRIMARY_COLOR, raw: false });
|
|
2361
|
+
}
|
|
2362
|
+
allLines.push({ text: '', style: '' });
|
|
2363
|
+
}
|
|
2364
|
+
else {
|
|
2365
|
+
allLines.push({ text: ' Codeep', style: PRIMARY_COLOR, raw: false });
|
|
2366
|
+
allLines.push({ text: '', style: '' });
|
|
2367
|
+
}
|
|
2329
2368
|
for (const msg of this.messages) {
|
|
2330
2369
|
const msgLines = this.formatMessage(msg.role, msg.content, width);
|
|
2331
2370
|
allLines.push(...msgLines);
|
|
@@ -2335,10 +2374,7 @@ export class App {
|
|
|
2335
2374
|
allLines.push(...streamLines);
|
|
2336
2375
|
}
|
|
2337
2376
|
// Calculate visible window based on scroll offset
|
|
2338
|
-
// scrollOffset=0 means show the most recent (bottom) lines
|
|
2339
|
-
// scrollOffset>0 means scroll up to see older messages
|
|
2340
2377
|
const totalLines = allLines.length;
|
|
2341
|
-
// Clamp scrollOffset to valid range
|
|
2342
2378
|
const maxScroll = Math.max(0, totalLines - height);
|
|
2343
2379
|
if (this.scrollOffset > maxScroll) {
|
|
2344
2380
|
this.scrollOffset = maxScroll;
|
|
@@ -2355,7 +2391,7 @@ export class App {
|
|
|
2355
2391
|
const roleStyle = role === 'user' ? fg.green : role === 'assistant' ? PRIMARY_COLOR : fg.yellow;
|
|
2356
2392
|
const roleLabel = role === 'user' ? '> ' : role === 'assistant' ? ' ' : '# ';
|
|
2357
2393
|
// Parse content for code blocks
|
|
2358
|
-
const codeBlockRegex = /```(
|
|
2394
|
+
const codeBlockRegex = /```([^\n]*)\n([\s\S]*?)```/g;
|
|
2359
2395
|
let lastIndex = 0;
|
|
2360
2396
|
let match;
|
|
2361
2397
|
let isFirstLine = true;
|
|
@@ -2368,7 +2404,13 @@ export class App {
|
|
|
2368
2404
|
isFirstLine = false;
|
|
2369
2405
|
}
|
|
2370
2406
|
// Add code block with syntax highlighting
|
|
2371
|
-
const
|
|
2407
|
+
const rawLang = (match[1] || 'text').trim();
|
|
2408
|
+
// Handle filepath:name.ext format - extract extension as language
|
|
2409
|
+
let lang = rawLang;
|
|
2410
|
+
if (rawLang.includes(':') || rawLang.includes('.')) {
|
|
2411
|
+
const ext = rawLang.split('.').pop() || rawLang;
|
|
2412
|
+
lang = ext;
|
|
2413
|
+
}
|
|
2372
2414
|
const code = match[2];
|
|
2373
2415
|
const codeLines = this.formatCodeBlock(code, lang, maxWidth);
|
|
2374
2416
|
lines.push(...codeLines);
|
|
@@ -2508,7 +2550,7 @@ export class App {
|
|
|
2508
2550
|
}
|
|
2509
2551
|
else {
|
|
2510
2552
|
// Plain text - word wrap as before
|
|
2511
|
-
if (line
|
|
2553
|
+
if (stringWidth(line) > maxWidth - prefix.length) {
|
|
2512
2554
|
const wrapped = this.wordWrap(line, maxWidth - prefix.length);
|
|
2513
2555
|
for (let j = 0; j < wrapped.length; j++) {
|
|
2514
2556
|
lines.push({
|
|
@@ -2790,7 +2832,7 @@ export class App {
|
|
|
2790
2832
|
const lines = [];
|
|
2791
2833
|
let currentLine = '';
|
|
2792
2834
|
for (const word of words) {
|
|
2793
|
-
if (currentLine
|
|
2835
|
+
if (stringWidth(currentLine) + stringWidth(word) + 1 > maxWidth && currentLine) {
|
|
2794
2836
|
lines.push(currentLine);
|
|
2795
2837
|
currentLine = word;
|
|
2796
2838
|
}
|
package/dist/renderer/Screen.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Screen buffer with diff-based rendering
|
|
3
3
|
* Only writes changes to terminal - minimizes flickering
|
|
4
4
|
*/
|
|
5
|
-
import { cursor, screen, style, visibleLength } from './ansi.js';
|
|
5
|
+
import { cursor, screen, style, visibleLength, charWidth } from './ansi.js';
|
|
6
6
|
export class Screen {
|
|
7
7
|
width;
|
|
8
8
|
height;
|
|
@@ -73,10 +73,15 @@ export class Screen {
|
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
|
+
const w = charWidth(char);
|
|
76
77
|
if (col >= 0 && col < this.width) {
|
|
77
78
|
this.buffer[y][col] = { char, style: currentStyle };
|
|
79
|
+
// Wide char: fill next cell with empty placeholder
|
|
80
|
+
if (w === 2 && col + 1 < this.width) {
|
|
81
|
+
this.buffer[y][col + 1] = { char: '', style: currentStyle };
|
|
82
|
+
}
|
|
78
83
|
}
|
|
79
|
-
col
|
|
84
|
+
col += w;
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -132,10 +137,15 @@ export class Screen {
|
|
|
132
137
|
}
|
|
133
138
|
else {
|
|
134
139
|
// Regular character
|
|
140
|
+
const w = charWidth(text[i]);
|
|
135
141
|
if (col < this.width) {
|
|
136
142
|
this.buffer[y][col] = { char: text[i], style: currentStyle };
|
|
137
|
-
|
|
143
|
+
// Wide char: fill next cell with empty placeholder
|
|
144
|
+
if (w === 2 && col + 1 < this.width) {
|
|
145
|
+
this.buffer[y][col + 1] = { char: '', style: currentStyle };
|
|
146
|
+
}
|
|
138
147
|
}
|
|
148
|
+
col += w;
|
|
139
149
|
i++;
|
|
140
150
|
}
|
|
141
151
|
}
|
|
@@ -216,6 +226,11 @@ export class Screen {
|
|
|
216
226
|
if (cell.char === renderedCell.char && cell.style === renderedCell.style) {
|
|
217
227
|
continue;
|
|
218
228
|
}
|
|
229
|
+
// Skip wide-char placeholder cells
|
|
230
|
+
if (cell.char === '') {
|
|
231
|
+
this.rendered[y][x] = { ...cell };
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
219
234
|
// Move cursor and write
|
|
220
235
|
output += cursor.to(y + 1, x + 1);
|
|
221
236
|
if (cell.style !== lastStyle) {
|
package/dist/renderer/ansi.d.ts
CHANGED
|
@@ -81,6 +81,15 @@ export declare const style: {
|
|
|
81
81
|
* Helper to create styled text
|
|
82
82
|
*/
|
|
83
83
|
export declare function styled(text: string, ...styles: string[]): string;
|
|
84
|
+
/**
|
|
85
|
+
* Get terminal display width of a single character
|
|
86
|
+
* CJK, fullwidth, and emoji characters take 2 columns
|
|
87
|
+
*/
|
|
88
|
+
export declare function charWidth(char: string): number;
|
|
89
|
+
/**
|
|
90
|
+
* Get terminal display width of a string (excluding ANSI codes)
|
|
91
|
+
*/
|
|
92
|
+
export declare function stringWidth(str: string): number;
|
|
84
93
|
/**
|
|
85
94
|
* Strip ANSI codes from string (for length calculation)
|
|
86
95
|
*/
|
package/dist/renderer/ansi.js
CHANGED
|
@@ -101,6 +101,84 @@ export function styled(text, ...styles) {
|
|
|
101
101
|
return text;
|
|
102
102
|
return styles.join('') + text + style.reset;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Get terminal display width of a single character
|
|
106
|
+
* CJK, fullwidth, and emoji characters take 2 columns
|
|
107
|
+
*/
|
|
108
|
+
export function charWidth(char) {
|
|
109
|
+
const code = char.codePointAt(0);
|
|
110
|
+
if (code === undefined)
|
|
111
|
+
return 0;
|
|
112
|
+
// Control characters
|
|
113
|
+
if (code < 32 || (code >= 0x7f && code < 0xa0))
|
|
114
|
+
return 0;
|
|
115
|
+
// CJK Unified Ideographs
|
|
116
|
+
if (code >= 0x4e00 && code <= 0x9fff)
|
|
117
|
+
return 2;
|
|
118
|
+
// CJK Unified Ideographs Extension A
|
|
119
|
+
if (code >= 0x3400 && code <= 0x4dbf)
|
|
120
|
+
return 2;
|
|
121
|
+
// CJK Unified Ideographs Extension B
|
|
122
|
+
if (code >= 0x20000 && code <= 0x2a6df)
|
|
123
|
+
return 2;
|
|
124
|
+
// CJK Compatibility Ideographs
|
|
125
|
+
if (code >= 0xf900 && code <= 0xfaff)
|
|
126
|
+
return 2;
|
|
127
|
+
// CJK Radicals / Kangxi Radicals
|
|
128
|
+
if (code >= 0x2e80 && code <= 0x2fdf)
|
|
129
|
+
return 2;
|
|
130
|
+
// CJK Strokes / Enclosed CJK
|
|
131
|
+
if (code >= 0x31c0 && code <= 0x33ff)
|
|
132
|
+
return 2;
|
|
133
|
+
// CJK Symbols and Punctuation
|
|
134
|
+
if (code >= 0x3000 && code <= 0x303f)
|
|
135
|
+
return 2;
|
|
136
|
+
// Hiragana, Katakana
|
|
137
|
+
if (code >= 0x3040 && code <= 0x30ff)
|
|
138
|
+
return 2;
|
|
139
|
+
// Katakana Phonetic Extensions
|
|
140
|
+
if (code >= 0x31f0 && code <= 0x31ff)
|
|
141
|
+
return 2;
|
|
142
|
+
// Hangul Jamo
|
|
143
|
+
if (code >= 0x1100 && code <= 0x11ff)
|
|
144
|
+
return 2;
|
|
145
|
+
// Hangul Syllables
|
|
146
|
+
if (code >= 0xac00 && code <= 0xd7af)
|
|
147
|
+
return 2;
|
|
148
|
+
// Hangul Jamo Extended-A/B
|
|
149
|
+
if (code >= 0xa960 && code <= 0xa97f)
|
|
150
|
+
return 2;
|
|
151
|
+
if (code >= 0xd7b0 && code <= 0xd7ff)
|
|
152
|
+
return 2;
|
|
153
|
+
// Fullwidth Forms
|
|
154
|
+
if (code >= 0xff01 && code <= 0xff60)
|
|
155
|
+
return 2;
|
|
156
|
+
if (code >= 0xffe0 && code <= 0xffe6)
|
|
157
|
+
return 2;
|
|
158
|
+
// Bopomofo
|
|
159
|
+
if (code >= 0x3100 && code <= 0x312f)
|
|
160
|
+
return 2;
|
|
161
|
+
// Emoji ranges (common)
|
|
162
|
+
if (code >= 0x1f300 && code <= 0x1f9ff)
|
|
163
|
+
return 2;
|
|
164
|
+
if (code >= 0x1fa00 && code <= 0x1fa6f)
|
|
165
|
+
return 2;
|
|
166
|
+
if (code >= 0x1fa70 && code <= 0x1faff)
|
|
167
|
+
return 2;
|
|
168
|
+
if (code >= 0x2600 && code <= 0x27bf)
|
|
169
|
+
return 2;
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get terminal display width of a string (excluding ANSI codes)
|
|
174
|
+
*/
|
|
175
|
+
export function stringWidth(str) {
|
|
176
|
+
let width = 0;
|
|
177
|
+
for (const char of str) {
|
|
178
|
+
width += charWidth(char);
|
|
179
|
+
}
|
|
180
|
+
return width;
|
|
181
|
+
}
|
|
104
182
|
/**
|
|
105
183
|
* Strip ANSI codes from string (for length calculation)
|
|
106
184
|
*/
|
|
@@ -112,14 +190,14 @@ export function stripAnsi(str) {
|
|
|
112
190
|
* Get visible length of string (excluding ANSI codes)
|
|
113
191
|
*/
|
|
114
192
|
export function visibleLength(str) {
|
|
115
|
-
return stripAnsi(str)
|
|
193
|
+
return stringWidth(stripAnsi(str));
|
|
116
194
|
}
|
|
117
195
|
/**
|
|
118
196
|
* Truncate string to visible length, preserving ANSI codes
|
|
119
197
|
*/
|
|
120
198
|
export function truncate(str, maxLength, suffix = '...') {
|
|
121
199
|
const visible = stripAnsi(str);
|
|
122
|
-
if (visible
|
|
200
|
+
if (stringWidth(visible) <= maxLength)
|
|
123
201
|
return str;
|
|
124
202
|
// Simple truncation - may cut ANSI codes
|
|
125
203
|
// For proper handling, we'd need to parse ANSI sequences
|
|
@@ -138,13 +216,11 @@ export function truncate(str, maxLength, suffix = '...') {
|
|
|
138
216
|
}
|
|
139
217
|
}
|
|
140
218
|
else {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
visibleCount++;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
219
|
+
const w = charWidth(char);
|
|
220
|
+
if (visibleCount + w > maxLength - suffix.length)
|
|
146
221
|
break;
|
|
147
|
-
|
|
222
|
+
result += char;
|
|
223
|
+
visibleCount += w;
|
|
148
224
|
}
|
|
149
225
|
}
|
|
150
226
|
return result + style.reset + suffix;
|
package/dist/renderer/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { cursor, screen, fg, bg, style, styled, stripAnsi, visibleLength, trunca
|
|
|
9
9
|
export { Screen, Cell } from './Screen';
|
|
10
10
|
export { Input, LineEditor, KeyEvent, KeyHandler } from './Input';
|
|
11
11
|
export { ChatUI, ChatMessage, ChatUIOptions } from './ChatUI';
|
|
12
|
-
export { App,
|
|
12
|
+
export { App, AppOptions, Message } from './App';
|
|
13
13
|
export { createBox, centerBox, BoxStyle, BoxOptions } from './components/Box';
|
|
14
14
|
export { renderModal, renderHelpModal, renderListModal, ModalOptions } from './components/Modal';
|
|
15
15
|
export { renderHelpScreen, helpCategories, keyboardShortcuts } from './components/Help';
|
package/dist/renderer/main.js
CHANGED
|
@@ -620,6 +620,13 @@ function handleCommand(command, args) {
|
|
|
620
620
|
return;
|
|
621
621
|
}
|
|
622
622
|
const newName = args.join('-');
|
|
623
|
+
// Save current session first so there's a file to rename
|
|
624
|
+
const messages = app.getMessages();
|
|
625
|
+
if (messages.length === 0) {
|
|
626
|
+
app.notify('No messages to save. Start a conversation first.');
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
saveSession(sessionId, messages, projectPath);
|
|
623
630
|
if (renameSession(sessionId, newName, projectPath)) {
|
|
624
631
|
sessionId = newName;
|
|
625
632
|
app.notify(`Session renamed to: ${newName}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.22",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|