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.
@@ -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
@@ -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
- // Screen-specific handling
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.currentScreen = 'status';
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
- switch (this.currentScreen) {
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 headerLine = 0;
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 = /```(\w*)\n([\s\S]*?)```/g;
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 lang = match[1] || 'text';
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.length > maxWidth - prefix.length) {
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.length + word.length + 1 > maxWidth && currentLine) {
2835
+ if (stringWidth(currentLine) + stringWidth(word) + 1 > maxWidth && currentLine) {
2794
2836
  lines.push(currentLine);
2795
2837
  currentLine = word;
2796
2838
  }
@@ -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
- col++;
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) {
@@ -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
  */
@@ -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).length;
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.length <= maxLength)
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
- if (visibleCount < maxLength - suffix.length) {
142
- result += char;
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;
@@ -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, AppScreen, AppOptions, Message } from './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';
@@ -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.20",
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",