codeep 1.1.19 → 1.1.21

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
@@ -393,7 +397,11 @@ export declare class App {
393
397
  */
394
398
  private formatMessage;
395
399
  /**
396
- * Format plain text lines
400
+ * Apply inline markdown formatting (bold, italic, inline code) to a line
401
+ */
402
+ private applyInlineMarkdown;
403
+ /**
404
+ * Format plain text lines with markdown support
397
405
  */
398
406
  private formatTextLines;
399
407
  /**
@@ -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);
@@ -202,7 +202,6 @@ const COMMAND_DESCRIPTIONS = {
202
202
  'learn': 'Learn code preferences',
203
203
  };
204
204
  import { helpCategories, keyboardShortcuts } from './components/Help.js';
205
- import { renderStatusScreen } from './components/Status.js';
206
205
  import { handleSettingsKey, SETTINGS } from './components/Settings.js';
207
206
  export class App {
208
207
  screen;
@@ -212,7 +211,6 @@ export class App {
212
211
  streamingContent = '';
213
212
  isStreaming = false;
214
213
  isLoading = false;
215
- currentScreen = 'chat';
216
214
  options;
217
215
  scrollOffset = 0;
218
216
  notification = '';
@@ -231,6 +229,8 @@ export class App {
231
229
  // Inline help state
232
230
  helpOpen = false;
233
231
  helpScrollIndex = 0;
232
+ // Inline status state
233
+ statusOpen = false;
234
234
  // Settings screen state
235
235
  settingsState = {
236
236
  selectedIndex: 0,
@@ -784,19 +784,7 @@ export class App {
784
784
  this.options.onExit();
785
785
  return;
786
786
  }
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
- }
787
+ this.handleChatKey(event);
800
788
  }
801
789
  /**
802
790
  * Handle chat screen keys
@@ -822,6 +810,11 @@ export class App {
822
810
  this.handleInlineConfirmKey(event);
823
811
  return;
824
812
  }
813
+ // If status is open, handle status keys first
814
+ if (this.statusOpen) {
815
+ this.handleInlineStatusKey(event);
816
+ return;
817
+ }
825
818
  // If help is open, handle help keys first
826
819
  if (this.helpOpen) {
827
820
  this.handleInlineHelpKey(event);
@@ -1036,6 +1029,15 @@ export class App {
1036
1029
  this.autocompleteItems = [];
1037
1030
  }
1038
1031
  }
1032
+ /**
1033
+ * Handle inline status keys
1034
+ */
1035
+ handleInlineStatusKey(event) {
1036
+ if (event.key === 'escape' || event.key === 'q') {
1037
+ this.statusOpen = false;
1038
+ this.render();
1039
+ }
1040
+ }
1039
1041
  /**
1040
1042
  * Handle help screen keys
1041
1043
  */
@@ -1515,7 +1517,7 @@ export class App {
1515
1517
  this.render();
1516
1518
  break;
1517
1519
  case 'status':
1518
- this.currentScreen = 'status';
1520
+ this.statusOpen = true;
1519
1521
  this.render();
1520
1522
  break;
1521
1523
  case 'clear':
@@ -1542,15 +1544,7 @@ export class App {
1542
1544
  this.renderIntro();
1543
1545
  return;
1544
1546
  }
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
- }
1547
+ this.renderChat();
1554
1548
  }
1555
1549
  /**
1556
1550
  * Render chat screen
@@ -1603,31 +1597,12 @@ export class App {
1603
1597
  bottomPanelHeight = Math.min(this.autocompleteItems.length + 3, 12);
1604
1598
  }
1605
1599
  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
1600
  // Layout - main UI takes top portion
1610
- const headerLine = 0;
1611
- const messagesStart = logoSpace;
1601
+ const messagesStart = 0;
1612
1602
  const messagesEnd = mainHeight - 4;
1613
1603
  const separatorLine = mainHeight - 3;
1614
1604
  const inputLine = mainHeight - 2;
1615
1605
  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
1606
  // Messages
1632
1607
  const messagesHeight = messagesEnd - messagesStart + 1;
1633
1608
  const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
@@ -1662,6 +1637,10 @@ export class App {
1662
1637
  if (this.helpOpen) {
1663
1638
  this.renderInlineHelp(statusLine + 1, width, height - statusLine - 1);
1664
1639
  }
1640
+ // Inline status renders BELOW status bar
1641
+ if (this.statusOpen) {
1642
+ this.renderInlineStatus(statusLine + 1, width);
1643
+ }
1665
1644
  // Inline search renders BELOW status bar
1666
1645
  if (this.searchOpen) {
1667
1646
  this.renderInlineSearch(statusLine + 1, width, height - statusLine - 1);
@@ -1946,6 +1925,35 @@ export class App {
1946
1925
  /**
1947
1926
  * Render inline help below status bar
1948
1927
  */
1928
+ renderInlineStatus(startY, width) {
1929
+ const status = this.options.getStatus();
1930
+ let y = startY;
1931
+ // Separator line
1932
+ this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1933
+ // Title
1934
+ this.screen.writeLine(y++, 'Status', PRIMARY_COLOR + style.bold);
1935
+ const items = [
1936
+ { label: 'Version', value: 'v' + status.version, color: fg.white },
1937
+ { label: 'Provider', value: status.provider, color: fg.white },
1938
+ { label: 'Model', value: status.model, color: fg.white },
1939
+ { label: 'Agent Mode', value: status.agentMode.toUpperCase(), color: status.agentMode === 'on' ? fg.green : status.agentMode === 'manual' ? fg.yellow : fg.gray },
1940
+ { label: 'Project', value: status.projectPath, color: fg.white },
1941
+ { label: 'Write Access', value: status.hasWriteAccess ? 'Yes' : 'No', color: status.hasWriteAccess ? fg.green : fg.red },
1942
+ { label: 'Session', value: status.sessionId || 'New', color: fg.white },
1943
+ { label: 'Messages', value: status.messageCount.toString(), color: fg.white },
1944
+ { label: 'Platform', value: process.platform, color: fg.white },
1945
+ { label: 'Node', value: process.version, color: fg.white },
1946
+ { label: 'Terminal', value: width + 'x' + this.screen.getSize().height, color: fg.white },
1947
+ ];
1948
+ const labelWidth = Math.max(...items.map(i => i.label.length)) + 2;
1949
+ for (const item of items) {
1950
+ this.screen.write(2, y, item.label + ':', fg.gray);
1951
+ this.screen.write(2 + labelWidth, y, item.value, item.color);
1952
+ y++;
1953
+ }
1954
+ y++;
1955
+ this.screen.writeLine(y, 'Esc close', fg.gray);
1956
+ }
1949
1957
  renderInlineHelp(startY, width, availableHeight) {
1950
1958
  // Build all help items
1951
1959
  const allItems = [];
@@ -2326,6 +2334,20 @@ export class App {
2326
2334
  */
2327
2335
  getVisibleMessages(height, width) {
2328
2336
  const allLines = [];
2337
+ // Logo at the top, scrolls with content
2338
+ if (height >= 20) {
2339
+ const logoWidth = LOGO_LINES[0].length;
2340
+ const logoX = Math.max(0, Math.floor((width - logoWidth) / 2));
2341
+ const pad = ' '.repeat(logoX);
2342
+ for (const line of LOGO_LINES) {
2343
+ allLines.push({ text: pad + line, style: PRIMARY_COLOR, raw: false });
2344
+ }
2345
+ allLines.push({ text: '', style: '' });
2346
+ }
2347
+ else {
2348
+ allLines.push({ text: ' Codeep', style: PRIMARY_COLOR, raw: false });
2349
+ allLines.push({ text: '', style: '' });
2350
+ }
2329
2351
  for (const msg of this.messages) {
2330
2352
  const msgLines = this.formatMessage(msg.role, msg.content, width);
2331
2353
  allLines.push(...msgLines);
@@ -2335,10 +2357,7 @@ export class App {
2335
2357
  allLines.push(...streamLines);
2336
2358
  }
2337
2359
  // 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
2360
  const totalLines = allLines.length;
2341
- // Clamp scrollOffset to valid range
2342
2361
  const maxScroll = Math.max(0, totalLines - height);
2343
2362
  if (this.scrollOffset > maxScroll) {
2344
2363
  this.scrollOffset = maxScroll;
@@ -2385,7 +2404,64 @@ export class App {
2385
2404
  return lines;
2386
2405
  }
2387
2406
  /**
2388
- * Format plain text lines
2407
+ * Apply inline markdown formatting (bold, italic, inline code) to a line
2408
+ */
2409
+ applyInlineMarkdown(text) {
2410
+ let result = '';
2411
+ let hasFormatting = false;
2412
+ let i = 0;
2413
+ while (i < text.length) {
2414
+ // Inline code: `code`
2415
+ if (text[i] === '`' && text[i + 1] !== '`') {
2416
+ const end = text.indexOf('`', i + 1);
2417
+ if (end !== -1) {
2418
+ const code = text.slice(i + 1, end);
2419
+ result += fg.rgb(209, 154, 102) + code + '\x1b[0m';
2420
+ hasFormatting = true;
2421
+ i = end + 1;
2422
+ continue;
2423
+ }
2424
+ }
2425
+ // Bold + italic: ***text***
2426
+ if (text.slice(i, i + 3) === '***') {
2427
+ const end = text.indexOf('***', i + 3);
2428
+ if (end !== -1) {
2429
+ const inner = text.slice(i + 3, end);
2430
+ result += style.bold + style.italic + fg.white + inner + '\x1b[0m';
2431
+ hasFormatting = true;
2432
+ i = end + 3;
2433
+ continue;
2434
+ }
2435
+ }
2436
+ // Bold: **text**
2437
+ if (text.slice(i, i + 2) === '**') {
2438
+ const end = text.indexOf('**', i + 2);
2439
+ if (end !== -1) {
2440
+ const inner = text.slice(i + 2, end);
2441
+ result += style.bold + fg.white + inner + '\x1b[0m';
2442
+ hasFormatting = true;
2443
+ i = end + 2;
2444
+ continue;
2445
+ }
2446
+ }
2447
+ // Italic: *text*
2448
+ if (text[i] === '*' && text[i + 1] !== '*') {
2449
+ const end = text.indexOf('*', i + 1);
2450
+ if (end !== -1 && end > i + 1) {
2451
+ const inner = text.slice(i + 1, end);
2452
+ result += style.italic + inner + '\x1b[0m';
2453
+ hasFormatting = true;
2454
+ i = end + 1;
2455
+ continue;
2456
+ }
2457
+ }
2458
+ result += text[i];
2459
+ i++;
2460
+ }
2461
+ return { formatted: result, hasFormatting };
2462
+ }
2463
+ /**
2464
+ * Format plain text lines with markdown support
2389
2465
  */
2390
2466
  formatTextLines(text, maxWidth, firstPrefix, firstStyle) {
2391
2467
  const lines = [];
@@ -2394,21 +2470,79 @@ export class App {
2394
2470
  const line = contentLines[i];
2395
2471
  const prefix = i === 0 ? firstPrefix : ' ';
2396
2472
  const prefixStyle = i === 0 ? firstStyle : '';
2397
- if (line.length > maxWidth - prefix.length) {
2398
- const wrapped = this.wordWrap(line, maxWidth - prefix.length);
2399
- for (let j = 0; j < wrapped.length; j++) {
2473
+ // Heading: ## or ### etc.
2474
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
2475
+ if (headingMatch) {
2476
+ const level = headingMatch[1].length;
2477
+ const headingText = headingMatch[2];
2478
+ const headingColor = level <= 2 ? fg.rgb(97, 175, 239) : fg.rgb(198, 120, 221);
2479
+ lines.push({
2480
+ text: prefix + headingColor + style.bold + headingText + '\x1b[0m',
2481
+ style: prefixStyle,
2482
+ raw: true,
2483
+ });
2484
+ continue;
2485
+ }
2486
+ // Horizontal rule: --- or *** or ___
2487
+ if (/^[-*_]{3,}\s*$/.test(line)) {
2488
+ const ruleWidth = Math.min(maxWidth - 4, 40);
2489
+ lines.push({
2490
+ text: prefix + fg.gray + '─'.repeat(ruleWidth) + '\x1b[0m',
2491
+ style: prefixStyle,
2492
+ raw: true,
2493
+ });
2494
+ continue;
2495
+ }
2496
+ // List items: - item or * item or numbered 1. item
2497
+ const listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.+)$/);
2498
+ if (listMatch) {
2499
+ const indent = listMatch[1];
2500
+ const bullet = listMatch[2];
2501
+ const content = listMatch[3];
2502
+ const { formatted, hasFormatting } = this.applyInlineMarkdown(content);
2503
+ const bulletChar = bullet === '-' || bullet === '*' ? '•' : bullet;
2504
+ if (hasFormatting) {
2505
+ lines.push({
2506
+ text: prefix + indent + fg.gray + bulletChar + '\x1b[0m' + ' ' + formatted,
2507
+ style: prefixStyle,
2508
+ raw: true,
2509
+ });
2510
+ }
2511
+ else {
2400
2512
  lines.push({
2401
- text: (j === 0 ? prefix : ' ') + wrapped[j],
2402
- style: j === 0 ? prefixStyle : '',
2513
+ text: prefix + indent + bulletChar + ' ' + content,
2514
+ style: prefixStyle,
2403
2515
  });
2404
2516
  }
2517
+ continue;
2405
2518
  }
2406
- else {
2519
+ // Regular text with possible inline markdown
2520
+ const { formatted, hasFormatting } = this.applyInlineMarkdown(line);
2521
+ if (hasFormatting) {
2407
2522
  lines.push({
2408
- text: prefix + line,
2523
+ text: prefix + formatted,
2409
2524
  style: prefixStyle,
2525
+ raw: true,
2410
2526
  });
2411
2527
  }
2528
+ else {
2529
+ // Plain text - word wrap as before
2530
+ if (stringWidth(line) > maxWidth - prefix.length) {
2531
+ const wrapped = this.wordWrap(line, maxWidth - prefix.length);
2532
+ for (let j = 0; j < wrapped.length; j++) {
2533
+ lines.push({
2534
+ text: (j === 0 ? prefix : ' ') + wrapped[j],
2535
+ style: j === 0 ? prefixStyle : '',
2536
+ });
2537
+ }
2538
+ }
2539
+ else {
2540
+ lines.push({
2541
+ text: prefix + line,
2542
+ style: prefixStyle,
2543
+ });
2544
+ }
2545
+ }
2412
2546
  }
2413
2547
  return lines;
2414
2548
  }
@@ -2675,7 +2809,7 @@ export class App {
2675
2809
  const lines = [];
2676
2810
  let currentLine = '';
2677
2811
  for (const word of words) {
2678
- if (currentLine.length + word.length + 1 > maxWidth && currentLine) {
2812
+ if (stringWidth(currentLine) + stringWidth(word) + 1 > maxWidth && currentLine) {
2679
2813
  lines.push(currentLine);
2680
2814
  currentLine = word;
2681
2815
  }
@@ -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.19",
3
+ "version": "1.1.21",
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",