codeep 2.9.0 → 2.11.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 +3 -3
- package/dist/acp/commands.js +29 -1
- package/dist/acp/protocol.d.ts +4 -0
- package/dist/acp/server.js +7 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +31 -21
- package/dist/config/providers.js +20 -12
- package/dist/renderer/App.d.ts +7 -1
- package/dist/renderer/App.js +78 -50
- package/dist/renderer/commands.js +57 -9
- package/dist/renderer/components/Help.d.ts +0 -5
- package/dist/renderer/components/Help.js +1 -68
- package/dist/renderer/components/Settings.d.ts +0 -5
- package/dist/renderer/components/Settings.js +1 -80
- package/dist/renderer/highlight.js +16 -0
- package/dist/renderer/index.d.ts +1 -2
- package/dist/renderer/index.js +1 -2
- package/dist/utils/tokenTracker.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -4
- package/dist/renderer/ChatUI.d.ts +0 -71
- package/dist/renderer/ChatUI.js +0 -286
- package/dist/renderer/demo-app.d.ts +0 -6
- package/dist/renderer/demo-app.js +0 -85
- package/dist/renderer/demo.d.ts +0 -6
- package/dist/renderer/demo.js +0 -52
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Help screen component
|
|
3
3
|
*/
|
|
4
|
-
import { fg
|
|
4
|
+
import { fg } from '../ansi.js';
|
|
5
5
|
// Primary color: #f02a30 (Codeep red)
|
|
6
6
|
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
7
7
|
/**
|
|
@@ -204,70 +204,3 @@ export function getHelpTotalPages(screenHeight) {
|
|
|
204
204
|
itemCount += keyboardShortcuts.length;
|
|
205
205
|
return Math.max(1, Math.ceil(itemCount / availableHeight));
|
|
206
206
|
}
|
|
207
|
-
/**
|
|
208
|
-
* Render full help screen
|
|
209
|
-
*/
|
|
210
|
-
export function renderHelpScreen(screen, page = 0) {
|
|
211
|
-
const { width, height } = screen.getSize();
|
|
212
|
-
screen.clear();
|
|
213
|
-
// Title
|
|
214
|
-
const title = '═══ Codeep Help ═══';
|
|
215
|
-
const titleX = Math.floor((width - title.length) / 2);
|
|
216
|
-
screen.write(titleX, 0, title, PRIMARY_COLOR + style.bold);
|
|
217
|
-
// Calculate layout
|
|
218
|
-
const contentStartY = 2;
|
|
219
|
-
const contentEndY = height - 3;
|
|
220
|
-
const availableHeight = contentEndY - contentStartY;
|
|
221
|
-
// Collect all items with categories
|
|
222
|
-
const allItems = [];
|
|
223
|
-
for (const category of helpCategories) {
|
|
224
|
-
// Category header
|
|
225
|
-
allItems.push({ text: '', style: '' });
|
|
226
|
-
allItems.push({ text: ` ${category.title}`, style: fg.yellow + style.bold });
|
|
227
|
-
// Items
|
|
228
|
-
for (const item of category.items) {
|
|
229
|
-
const keyPadded = item.key.padEnd(20);
|
|
230
|
-
allItems.push({
|
|
231
|
-
text: ` ${keyPadded} ${item.description}`,
|
|
232
|
-
style: '',
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Add keyboard shortcuts section
|
|
237
|
-
allItems.push({ text: '', style: '' });
|
|
238
|
-
allItems.push({ text: ' Keyboard Shortcuts', style: fg.yellow + style.bold });
|
|
239
|
-
for (const shortcut of keyboardShortcuts) {
|
|
240
|
-
const keyPadded = shortcut.key.padEnd(12);
|
|
241
|
-
allItems.push({
|
|
242
|
-
text: ` ${keyPadded} ${shortcut.description}`,
|
|
243
|
-
style: '',
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
// Pagination
|
|
247
|
-
const totalPages = Math.ceil(allItems.length / availableHeight);
|
|
248
|
-
const startIndex = page * availableHeight;
|
|
249
|
-
const visibleItems = allItems.slice(startIndex, startIndex + availableHeight);
|
|
250
|
-
// Render items
|
|
251
|
-
for (let i = 0; i < visibleItems.length; i++) {
|
|
252
|
-
const item = visibleItems[i];
|
|
253
|
-
// Highlight command part (starts with /)
|
|
254
|
-
if (item.text.includes('/')) {
|
|
255
|
-
const match = item.text.match(/^(\s*)(\S+)(\s+)(.*)$/);
|
|
256
|
-
if (match) {
|
|
257
|
-
const [, indent, cmd, space, desc] = match;
|
|
258
|
-
screen.write(0, contentStartY + i, indent, '');
|
|
259
|
-
screen.write(indent.length, contentStartY + i, cmd, fg.green);
|
|
260
|
-
screen.write(indent.length + cmd.length, contentStartY + i, space + desc, fg.white);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
screen.write(0, contentStartY + i, item.text, item.style || fg.white);
|
|
265
|
-
}
|
|
266
|
-
// Footer
|
|
267
|
-
const footerY = height - 1;
|
|
268
|
-
const pageInfo = totalPages > 1 ? `Page ${page + 1}/${totalPages} | ←→ Navigate | ` : '';
|
|
269
|
-
const footer = `${pageInfo}Esc Close`;
|
|
270
|
-
screen.write(2, footerY, footer, fg.gray);
|
|
271
|
-
screen.showCursor(false);
|
|
272
|
-
screen.fullRender();
|
|
273
|
-
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings screen component
|
|
3
3
|
*/
|
|
4
|
-
import { Screen } from '../Screen';
|
|
5
4
|
export interface SettingItem {
|
|
6
5
|
key: string;
|
|
7
6
|
label: string;
|
|
@@ -21,10 +20,6 @@ export interface SettingsState {
|
|
|
21
20
|
editing: boolean;
|
|
22
21
|
editValue: string;
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Render settings screen
|
|
26
|
-
*/
|
|
27
|
-
export declare function renderSettingsScreen(screen: Screen, state: SettingsState, hasWriteAccess: boolean, hasProjectContext: boolean): void;
|
|
28
23
|
/**
|
|
29
24
|
* Handle settings key
|
|
30
25
|
* Returns: { handled: boolean, close: boolean, notify?: string }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings screen component
|
|
3
3
|
*/
|
|
4
|
-
import { fg
|
|
4
|
+
import { fg } from '../ansi.js';
|
|
5
5
|
import { config } from '../../config/index.js';
|
|
6
6
|
import { updateRateLimits } from '../../utils/ratelimit.js';
|
|
7
7
|
// Primary color: #f02a30 (Codeep red)
|
|
@@ -272,85 +272,6 @@ function formatValue(setting) {
|
|
|
272
272
|
}
|
|
273
273
|
return String(value);
|
|
274
274
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Render settings screen
|
|
277
|
-
*/
|
|
278
|
-
export function renderSettingsScreen(screen, state, hasWriteAccess, hasProjectContext) {
|
|
279
|
-
const { width, height } = screen.getSize();
|
|
280
|
-
screen.clear();
|
|
281
|
-
// Title
|
|
282
|
-
const title = '═══ Settings ═══';
|
|
283
|
-
const titleX = Math.floor((width - title.length) / 2);
|
|
284
|
-
screen.write(titleX, 0, title, PRIMARY_COLOR + style.bold);
|
|
285
|
-
// Settings list
|
|
286
|
-
const startY = 2;
|
|
287
|
-
const maxVisible = height - 7;
|
|
288
|
-
const scrollOffset = Math.max(0, state.selectedIndex - maxVisible + 3);
|
|
289
|
-
for (let i = 0; i < SETTINGS.length && i < maxVisible; i++) {
|
|
290
|
-
const settingIdx = i + scrollOffset;
|
|
291
|
-
if (settingIdx >= SETTINGS.length)
|
|
292
|
-
break;
|
|
293
|
-
const setting = SETTINGS[settingIdx];
|
|
294
|
-
const isSelected = settingIdx === state.selectedIndex;
|
|
295
|
-
const y = startY + i;
|
|
296
|
-
// Prefix
|
|
297
|
-
const prefix = isSelected ? '► ' : ' ';
|
|
298
|
-
screen.write(2, y, prefix, isSelected ? PRIMARY_COLOR : '');
|
|
299
|
-
// Label
|
|
300
|
-
const labelColor = isSelected ? PRIMARY_BRIGHT : fg.white;
|
|
301
|
-
screen.write(4, y, setting.label + ':', labelColor);
|
|
302
|
-
// Value
|
|
303
|
-
const valueX = 30;
|
|
304
|
-
if (state.editing && isSelected) {
|
|
305
|
-
screen.write(valueX, y, state.editValue + '█', fg.cyan);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
screen.write(valueX, y, formatValue(setting), fg.green);
|
|
309
|
-
}
|
|
310
|
-
// Hint
|
|
311
|
-
if (isSelected && !state.editing) {
|
|
312
|
-
const hintX = valueX + formatValue(setting).length + 2;
|
|
313
|
-
if (setting.type === 'number') {
|
|
314
|
-
screen.write(hintX, y, '(←/→ adjust, Enter edit)', fg.gray);
|
|
315
|
-
}
|
|
316
|
-
else if (setting.type === 'text') {
|
|
317
|
-
screen.write(hintX, y, '(Enter to edit)', fg.gray);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
screen.write(hintX, y, '(←/→ or Enter toggle)', fg.gray);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Agent status message
|
|
325
|
-
const agentMode = config.get('agentMode');
|
|
326
|
-
const statusY = height - 4;
|
|
327
|
-
let statusMessage;
|
|
328
|
-
let statusColor;
|
|
329
|
-
if (agentMode === 'on') {
|
|
330
|
-
if (!hasWriteAccess || !hasProjectContext) {
|
|
331
|
-
statusMessage = '⚠️ Agent needs permission - use /grant';
|
|
332
|
-
statusColor = fg.yellow;
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
statusMessage = '✓ Agent will run automatically';
|
|
336
|
-
statusColor = fg.green;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else if (agentMode === 'manual') {
|
|
340
|
-
statusMessage = 'ℹ️ Manual mode - use /agent <task>';
|
|
341
|
-
statusColor = fg.gray;
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
statusMessage = 'ℹ️ Agent disabled';
|
|
345
|
-
statusColor = fg.gray;
|
|
346
|
-
}
|
|
347
|
-
screen.write(2, statusY, statusMessage, statusColor);
|
|
348
|
-
// Footer
|
|
349
|
-
const footerY = height - 1;
|
|
350
|
-
screen.write(2, footerY, '↑/↓ Navigate | ←/→ Adjust | Enter Edit | Esc Close', fg.gray);
|
|
351
|
-
screen.showCursor(state.editing);
|
|
352
|
-
screen.fullRender();
|
|
353
|
-
}
|
|
354
275
|
/**
|
|
355
276
|
* Handle settings key
|
|
356
277
|
* Returns: { handled: boolean, close: boolean, notify?: string }
|
|
@@ -34,6 +34,22 @@ const LANG_ALIASES = {
|
|
|
34
34
|
export function highlightCode(code, lang) {
|
|
35
35
|
const normalizedLang = LANG_ALIASES[lang.toLowerCase()] || lang.toLowerCase();
|
|
36
36
|
const keywords = KEYWORDS[normalizedLang] || KEYWORDS['js'] || [];
|
|
37
|
+
// Diffs are line-oriented: +added/-removed/@@hunk. The agent emits
|
|
38
|
+
// ```diff on every edit confirmation, so without this branch the most
|
|
39
|
+
// common block in an agent run fell through to JS keyword colors.
|
|
40
|
+
if (normalizedLang === 'diff' || normalizedLang === 'patch') {
|
|
41
|
+
return code.split('\n').map(line => {
|
|
42
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
43
|
+
return SYNTAX.codeLang + line + '\x1b[0m';
|
|
44
|
+
if (line.startsWith('@@'))
|
|
45
|
+
return SYNTAX.operator + line + '\x1b[0m';
|
|
46
|
+
if (line.startsWith('+'))
|
|
47
|
+
return SYNTAX.string + line + '\x1b[0m'; // green — additions
|
|
48
|
+
if (line.startsWith('-'))
|
|
49
|
+
return fg.rgb(224, 108, 117) + line + '\x1b[0m'; // red — removals
|
|
50
|
+
return line;
|
|
51
|
+
}).join('\n');
|
|
52
|
+
}
|
|
37
53
|
if (normalizedLang === 'html' || normalizedLang === 'xml' || normalizedLang === 'svg') {
|
|
38
54
|
return code.replace(/(<\/?)(\w[\w-]*)((?:\s+[\w-]+(?:=(?:"[^"]*"|'[^']*'|\S+))?)*)(\s*\/?>)/g, (_match, open, tag, attrs, close) => {
|
|
39
55
|
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');
|
package/dist/renderer/index.d.ts
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
export { cursor, screen, fg, bg, style, styled, stripAnsi, visibleLength, truncate, wordWrap } from './ansi';
|
|
9
9
|
export { Screen, Cell } from './Screen';
|
|
10
10
|
export { Input, LineEditor, KeyEvent, KeyHandler } from './Input';
|
|
11
|
-
export { ChatUI, ChatMessage, ChatUIOptions } from './ChatUI';
|
|
12
11
|
export { App, AppOptions, Message } from './App';
|
|
13
12
|
export { createBox, centerBox, BoxStyle, BoxOptions } from './components/Box';
|
|
14
13
|
export { renderModal, renderHelpModal, renderListModal, ModalOptions } from './components/Modal';
|
|
15
|
-
export {
|
|
14
|
+
export { helpCategories, keyboardShortcuts } from './components/Help';
|
|
16
15
|
export { renderStatusScreen, StatusInfo } from './components/Status';
|
package/dist/renderer/index.js
CHANGED
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
export { cursor, screen, fg, bg, style, styled, stripAnsi, visibleLength, truncate, wordWrap } from './ansi.js';
|
|
9
9
|
export { Screen } from './Screen.js';
|
|
10
10
|
export { Input, LineEditor } from './Input.js';
|
|
11
|
-
export { ChatUI } from './ChatUI.js';
|
|
12
11
|
export { App } from './App.js';
|
|
13
12
|
// Components
|
|
14
13
|
export { createBox, centerBox } from './components/Box.js';
|
|
15
14
|
export { renderModal, renderHelpModal, renderListModal } from './components/Modal.js';
|
|
16
|
-
export {
|
|
15
|
+
export { helpCategories, keyboardShortcuts } from './components/Help.js';
|
|
17
16
|
export { renderStatusScreen } from './components/Status.js';
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// and produce phantom estimates against the wrong context size.
|
|
8
8
|
const MODEL_CONTEXT_WINDOWS = {
|
|
9
9
|
// Z.AI / ZhipuAI
|
|
10
|
+
'glm-5.2[1m]': 1_000_000,
|
|
11
|
+
'glm-5.2': 200_000,
|
|
10
12
|
'glm-5.1': 131_072,
|
|
11
13
|
'glm-5': 80_000,
|
|
12
14
|
'glm-5-turbo': 202_752,
|
|
@@ -42,6 +44,12 @@ export function getModelContextWindow(model) {
|
|
|
42
44
|
// by hand can produce phantom cost estimates against stale rates.
|
|
43
45
|
const MODEL_PRICING = {
|
|
44
46
|
// Z.AI / ZhipuAI
|
|
47
|
+
// GLM-5.2 per-token pricing isn't published yet — mirror GLM-5.1 (same tier,
|
|
48
|
+
// its successor) provisionally so /cost stays sane; update when z.ai posts it.
|
|
49
|
+
// Note: on the GLM Coding Plan (the default `z.ai` provider) billing is a flat
|
|
50
|
+
// subscription, so this only affects the pay-per-use estimate.
|
|
51
|
+
'glm-5.2[1m]': { inputPer1M: 1.00, outputPer1M: 3.20 },
|
|
52
|
+
'glm-5.2': { inputPer1M: 1.00, outputPer1M: 3.20 },
|
|
45
53
|
'glm-5.1': { inputPer1M: 1.00, outputPer1M: 3.20 },
|
|
46
54
|
'glm-5': { inputPer1M: 0.72, outputPer1M: 2.30 },
|
|
47
55
|
'glm-5-turbo': { inputPer1M: 1.20, outputPer1M: 4.00 },
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "2.
|
|
1
|
+
export declare const VERSION = "2.11.0";
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/gen-version.js — do not edit by hand.
|
|
2
2
|
// Baked from package.json at build time so the bun-compiled binary reports
|
|
3
3
|
// the right version (it has no package.json on disk to read at runtime).
|
|
4
|
-
export const VERSION = '2.
|
|
4
|
+
export const VERSION = '2.11.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
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",
|
|
@@ -9,11 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "node scripts/gen-version.js && node --import tsx src/renderer/main.ts",
|
|
12
|
-
"prepack": "node scripts/gen-version.js && tsc
|
|
12
|
+
"prepack": "node scripts/gen-version.js && tsc && node scripts/fix-imports.js",
|
|
13
13
|
"build": "node scripts/gen-version.js && tsc && node scripts/fix-imports.js",
|
|
14
14
|
"start": "node dist/renderer/main.js",
|
|
15
|
-
"demo:renderer": "node --import tsx src/renderer/demo.ts",
|
|
16
|
-
"demo:app": "node --import tsx src/renderer/demo-app.ts",
|
|
17
15
|
"build:binary": "npm run build && pkg dist/renderer/main.js --targets node18-macos-arm64,node18-macos-x64,node18-linux-x64 --output bin/codeep",
|
|
18
16
|
"test": "vitest run",
|
|
19
17
|
"test:watch": "vitest",
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple Chat UI - Proof of Concept
|
|
3
|
-
* Demonstrates custom renderer without Ink
|
|
4
|
-
*/
|
|
5
|
-
export interface ChatMessage {
|
|
6
|
-
role: 'user' | 'assistant' | 'system';
|
|
7
|
-
content: string;
|
|
8
|
-
}
|
|
9
|
-
export interface ChatUIOptions {
|
|
10
|
-
onSubmit: (message: string) => void;
|
|
11
|
-
onExit: () => void;
|
|
12
|
-
}
|
|
13
|
-
export declare class ChatUI {
|
|
14
|
-
private screen;
|
|
15
|
-
private input;
|
|
16
|
-
private editor;
|
|
17
|
-
private messages;
|
|
18
|
-
private streamingContent;
|
|
19
|
-
private isStreaming;
|
|
20
|
-
private options;
|
|
21
|
-
private scrollOffset;
|
|
22
|
-
constructor(options: ChatUIOptions);
|
|
23
|
-
/**
|
|
24
|
-
* Start the UI
|
|
25
|
-
*/
|
|
26
|
-
start(): void;
|
|
27
|
-
/**
|
|
28
|
-
* Stop the UI
|
|
29
|
-
*/
|
|
30
|
-
stop(): void;
|
|
31
|
-
/**
|
|
32
|
-
* Add a message to chat
|
|
33
|
-
*/
|
|
34
|
-
addMessage(message: ChatMessage): void;
|
|
35
|
-
/**
|
|
36
|
-
* Start streaming response
|
|
37
|
-
*/
|
|
38
|
-
startStreaming(): void;
|
|
39
|
-
/**
|
|
40
|
-
* Add chunk to streaming response
|
|
41
|
-
*/
|
|
42
|
-
addStreamChunk(chunk: string): void;
|
|
43
|
-
/**
|
|
44
|
-
* End streaming and add as message
|
|
45
|
-
*/
|
|
46
|
-
endStreaming(): void;
|
|
47
|
-
/**
|
|
48
|
-
* Handle keyboard input
|
|
49
|
-
*/
|
|
50
|
-
private handleKey;
|
|
51
|
-
/**
|
|
52
|
-
* Render the entire UI
|
|
53
|
-
*/
|
|
54
|
-
render(): void;
|
|
55
|
-
/**
|
|
56
|
-
* Full render (alias for render, used on start)
|
|
57
|
-
*/
|
|
58
|
-
private fullRender;
|
|
59
|
-
/**
|
|
60
|
-
* Format a message into lines
|
|
61
|
-
*/
|
|
62
|
-
private formatMessage;
|
|
63
|
-
/**
|
|
64
|
-
* Get messages formatted for visible area (including streaming)
|
|
65
|
-
*/
|
|
66
|
-
private getVisibleMessages;
|
|
67
|
-
/**
|
|
68
|
-
* Simple word wrap
|
|
69
|
-
*/
|
|
70
|
-
private wordWrap;
|
|
71
|
-
}
|
package/dist/renderer/ChatUI.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple Chat UI - Proof of Concept
|
|
3
|
-
* Demonstrates custom renderer without Ink
|
|
4
|
-
*/
|
|
5
|
-
import { Screen } from './Screen.js';
|
|
6
|
-
import { Input, LineEditor } from './Input.js';
|
|
7
|
-
import { fg } from './ansi.js';
|
|
8
|
-
export class ChatUI {
|
|
9
|
-
screen;
|
|
10
|
-
input;
|
|
11
|
-
editor;
|
|
12
|
-
messages = [];
|
|
13
|
-
streamingContent = '';
|
|
14
|
-
isStreaming = false;
|
|
15
|
-
options;
|
|
16
|
-
scrollOffset = 0;
|
|
17
|
-
constructor(options) {
|
|
18
|
-
this.screen = new Screen();
|
|
19
|
-
this.input = new Input();
|
|
20
|
-
this.editor = new LineEditor();
|
|
21
|
-
this.options = options;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Start the UI
|
|
25
|
-
*/
|
|
26
|
-
start() {
|
|
27
|
-
this.screen.init();
|
|
28
|
-
this.input.start();
|
|
29
|
-
// Handle keyboard input
|
|
30
|
-
this.input.onKey((event) => this.handleKey(event));
|
|
31
|
-
// Initial render - use full render first time
|
|
32
|
-
this.fullRender();
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Stop the UI
|
|
36
|
-
*/
|
|
37
|
-
stop() {
|
|
38
|
-
this.input.stop();
|
|
39
|
-
this.screen.cleanup();
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Add a message to chat
|
|
43
|
-
*/
|
|
44
|
-
addMessage(message) {
|
|
45
|
-
this.messages.push(message);
|
|
46
|
-
this.scrollOffset = 0; // Reset scroll to bottom
|
|
47
|
-
this.render();
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Start streaming response
|
|
51
|
-
*/
|
|
52
|
-
startStreaming() {
|
|
53
|
-
this.isStreaming = true;
|
|
54
|
-
this.streamingContent = '';
|
|
55
|
-
this.render();
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Add chunk to streaming response
|
|
59
|
-
*/
|
|
60
|
-
addStreamChunk(chunk) {
|
|
61
|
-
this.streamingContent += chunk;
|
|
62
|
-
this.render();
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* End streaming and add as message
|
|
66
|
-
*/
|
|
67
|
-
endStreaming() {
|
|
68
|
-
if (this.streamingContent) {
|
|
69
|
-
this.messages.push({
|
|
70
|
-
role: 'assistant',
|
|
71
|
-
content: this.streamingContent,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
this.streamingContent = '';
|
|
75
|
-
this.isStreaming = false;
|
|
76
|
-
this.render();
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Handle keyboard input
|
|
80
|
-
*/
|
|
81
|
-
handleKey(event) {
|
|
82
|
-
// Ctrl+C or Ctrl+D to exit
|
|
83
|
-
if (event.ctrl && (event.key === 'c' || event.key === 'd')) {
|
|
84
|
-
this.stop();
|
|
85
|
-
this.options.onExit();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
// Escape to cancel streaming
|
|
89
|
-
if (event.key === 'escape' && this.isStreaming) {
|
|
90
|
-
this.endStreaming();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
// Ctrl+L to clear
|
|
94
|
-
if (event.ctrl && event.key === 'l') {
|
|
95
|
-
this.messages = [];
|
|
96
|
-
this.render();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Page up/down for scrolling
|
|
100
|
-
if (event.key === 'pageup') {
|
|
101
|
-
this.scrollOffset = Math.min(this.scrollOffset + 5, this.messages.length - 1);
|
|
102
|
-
this.render();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (event.key === 'pagedown') {
|
|
106
|
-
this.scrollOffset = Math.max(this.scrollOffset - 5, 0);
|
|
107
|
-
this.render();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
// Enter to submit
|
|
111
|
-
if (event.key === 'enter') {
|
|
112
|
-
const value = this.editor.getValue().trim();
|
|
113
|
-
if (value) {
|
|
114
|
-
this.editor.addToHistory(value);
|
|
115
|
-
this.editor.clear();
|
|
116
|
-
// Add user message
|
|
117
|
-
this.addMessage({ role: 'user', content: value });
|
|
118
|
-
// Callback
|
|
119
|
-
this.options.onSubmit(value);
|
|
120
|
-
}
|
|
121
|
-
this.render();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
// Handle editor keys
|
|
125
|
-
if (this.editor.handleKey(event)) {
|
|
126
|
-
this.render();
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Render the entire UI
|
|
131
|
-
*/
|
|
132
|
-
render() {
|
|
133
|
-
const { width, height } = this.screen.getSize();
|
|
134
|
-
this.screen.clear();
|
|
135
|
-
// Layout:
|
|
136
|
-
// - Line 0: Header
|
|
137
|
-
// - Lines 1 to height-4: Messages
|
|
138
|
-
// - Line height-3: Separator
|
|
139
|
-
// - Line height-2: Input
|
|
140
|
-
// - Line height-1: Status bar
|
|
141
|
-
const headerLine = 0;
|
|
142
|
-
const messagesStart = 1;
|
|
143
|
-
const messagesEnd = height - 4;
|
|
144
|
-
const separatorLine = height - 3;
|
|
145
|
-
const inputLine = height - 2;
|
|
146
|
-
const statusLine = height - 1;
|
|
147
|
-
// Header
|
|
148
|
-
const header = ' Codeep Chat ';
|
|
149
|
-
const headerPadding = '─'.repeat(Math.max(0, (width - header.length) / 2));
|
|
150
|
-
this.screen.writeLine(headerLine, headerPadding + header + headerPadding, fg.cyan);
|
|
151
|
-
// Messages area (including streaming content)
|
|
152
|
-
const messagesHeight = messagesEnd - messagesStart + 1;
|
|
153
|
-
const messagesToRender = this.getVisibleMessages(messagesHeight, width - 2);
|
|
154
|
-
let y = messagesStart;
|
|
155
|
-
for (const line of messagesToRender) {
|
|
156
|
-
if (y > messagesEnd)
|
|
157
|
-
break;
|
|
158
|
-
this.screen.writeLine(y, line.text, line.style);
|
|
159
|
-
y++;
|
|
160
|
-
}
|
|
161
|
-
// Separator
|
|
162
|
-
this.screen.horizontalLine(separatorLine, '─', fg.gray);
|
|
163
|
-
// Input line
|
|
164
|
-
const prompt = '> ';
|
|
165
|
-
const inputValue = this.editor.getValue();
|
|
166
|
-
const cursorPos = this.editor.getCursorPos();
|
|
167
|
-
const maxInputWidth = width - prompt.length - 1;
|
|
168
|
-
// Calculate what part of input to show and where cursor should be
|
|
169
|
-
let displayValue;
|
|
170
|
-
let cursorX;
|
|
171
|
-
if (inputValue.length <= maxInputWidth) {
|
|
172
|
-
// Input fits - show all, cursor at actual position
|
|
173
|
-
displayValue = inputValue;
|
|
174
|
-
cursorX = prompt.length + cursorPos;
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
// Input too long - scroll to keep cursor visible
|
|
178
|
-
// Keep cursor roughly in the middle-right of visible area
|
|
179
|
-
const visibleStart = Math.max(0, cursorPos - Math.floor(maxInputWidth * 0.7));
|
|
180
|
-
const visibleEnd = visibleStart + maxInputWidth;
|
|
181
|
-
if (visibleStart > 0) {
|
|
182
|
-
displayValue = '…' + inputValue.slice(visibleStart + 1, visibleEnd);
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
displayValue = inputValue.slice(0, maxInputWidth);
|
|
186
|
-
}
|
|
187
|
-
// Cursor position relative to visible portion
|
|
188
|
-
cursorX = prompt.length + (cursorPos - visibleStart);
|
|
189
|
-
if (visibleStart > 0) {
|
|
190
|
-
cursorX = prompt.length + (cursorPos - visibleStart);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
this.screen.writeLine(inputLine, prompt + displayValue, fg.green);
|
|
194
|
-
// Position cursor
|
|
195
|
-
this.screen.setCursor(cursorX, inputLine);
|
|
196
|
-
this.screen.showCursor(true);
|
|
197
|
-
// Status bar
|
|
198
|
-
const statusLeft = ` ${this.messages.length} messages`;
|
|
199
|
-
const statusRight = this.isStreaming ? 'Streaming... (Esc to cancel)' : 'Enter to send | Ctrl+C to exit';
|
|
200
|
-
const statusPadding = ' '.repeat(Math.max(0, width - statusLeft.length - statusRight.length));
|
|
201
|
-
this.screen.writeLine(statusLine, statusLeft + statusPadding + statusRight, fg.gray);
|
|
202
|
-
// Render to terminal (use fullRender for now - more reliable)
|
|
203
|
-
this.screen.fullRender();
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Full render (alias for render, used on start)
|
|
207
|
-
*/
|
|
208
|
-
fullRender() {
|
|
209
|
-
this.render();
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Format a message into lines
|
|
213
|
-
*/
|
|
214
|
-
formatMessage(role, content, maxWidth) {
|
|
215
|
-
const lines = [];
|
|
216
|
-
// Role indicator
|
|
217
|
-
const roleStyle = role === 'user' ? fg.green : role === 'assistant' ? fg.cyan : fg.yellow;
|
|
218
|
-
const roleLabel = role === 'user' ? '> ' : role === 'assistant' ? ' ' : '# ';
|
|
219
|
-
// Split content into lines
|
|
220
|
-
const contentLines = content.split('\n');
|
|
221
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
222
|
-
const line = contentLines[i];
|
|
223
|
-
const prefix = i === 0 ? roleLabel : ' ';
|
|
224
|
-
const prefixStyle = i === 0 ? roleStyle : '';
|
|
225
|
-
// Word wrap long lines
|
|
226
|
-
if (line.length > maxWidth - prefix.length) {
|
|
227
|
-
const wrapped = this.wordWrap(line, maxWidth - prefix.length);
|
|
228
|
-
for (let j = 0; j < wrapped.length; j++) {
|
|
229
|
-
lines.push({
|
|
230
|
-
text: (j === 0 ? prefix : ' ') + wrapped[j],
|
|
231
|
-
style: j === 0 ? prefixStyle : '',
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
lines.push({
|
|
237
|
-
text: prefix + line,
|
|
238
|
-
style: prefixStyle,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// Add empty line after message
|
|
243
|
-
lines.push({ text: '', style: '' });
|
|
244
|
-
return lines;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Get messages formatted for visible area (including streaming)
|
|
248
|
-
*/
|
|
249
|
-
getVisibleMessages(height, width) {
|
|
250
|
-
const allLines = [];
|
|
251
|
-
for (const msg of this.messages) {
|
|
252
|
-
const msgLines = this.formatMessage(msg.role, msg.content, width);
|
|
253
|
-
allLines.push(...msgLines);
|
|
254
|
-
}
|
|
255
|
-
// Add streaming content if active
|
|
256
|
-
if (this.isStreaming && this.streamingContent) {
|
|
257
|
-
const streamLines = this.formatMessage('assistant', this.streamingContent + '▊', width);
|
|
258
|
-
allLines.push(...streamLines);
|
|
259
|
-
}
|
|
260
|
-
// Apply scroll offset and return last 'height' lines
|
|
261
|
-
const startIndex = Math.max(0, allLines.length - height - this.scrollOffset);
|
|
262
|
-
const endIndex = allLines.length - this.scrollOffset;
|
|
263
|
-
return allLines.slice(startIndex, endIndex);
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Simple word wrap
|
|
267
|
-
*/
|
|
268
|
-
wordWrap(text, maxWidth) {
|
|
269
|
-
const words = text.split(' ');
|
|
270
|
-
const lines = [];
|
|
271
|
-
let currentLine = '';
|
|
272
|
-
for (const word of words) {
|
|
273
|
-
if (currentLine.length + word.length + 1 > maxWidth && currentLine) {
|
|
274
|
-
lines.push(currentLine);
|
|
275
|
-
currentLine = word;
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
currentLine += (currentLine ? ' ' : '') + word;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (currentLine) {
|
|
282
|
-
lines.push(currentLine);
|
|
283
|
-
}
|
|
284
|
-
return lines.length > 0 ? lines : [''];
|
|
285
|
-
}
|
|
286
|
-
}
|