codeep 1.1.12 → 1.1.13
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/bin/codeep.js +1 -1
- package/dist/config/index.js +10 -10
- package/dist/renderer/App.d.ts +430 -0
- package/dist/renderer/App.js +2712 -0
- package/dist/renderer/ChatUI.d.ts +71 -0
- package/dist/renderer/ChatUI.js +286 -0
- package/dist/renderer/Input.d.ts +72 -0
- package/dist/renderer/Input.js +371 -0
- package/dist/renderer/Screen.d.ts +79 -0
- package/dist/renderer/Screen.js +278 -0
- package/dist/renderer/ansi.d.ts +99 -0
- package/dist/renderer/ansi.js +176 -0
- package/dist/renderer/components/Box.d.ts +64 -0
- package/dist/renderer/components/Box.js +90 -0
- package/dist/renderer/components/Help.d.ts +30 -0
- package/dist/renderer/components/Help.js +195 -0
- package/dist/renderer/components/Intro.d.ts +12 -0
- package/dist/renderer/components/Intro.js +128 -0
- package/dist/renderer/components/Login.d.ts +42 -0
- package/dist/renderer/components/Login.js +178 -0
- package/dist/renderer/components/Modal.d.ts +43 -0
- package/dist/renderer/components/Modal.js +207 -0
- package/dist/renderer/components/Permission.d.ts +20 -0
- package/dist/renderer/components/Permission.js +113 -0
- package/dist/renderer/components/SelectScreen.d.ts +26 -0
- package/dist/renderer/components/SelectScreen.js +101 -0
- package/dist/renderer/components/Settings.d.ts +37 -0
- package/dist/renderer/components/Settings.js +333 -0
- package/dist/renderer/components/Status.d.ts +18 -0
- package/dist/renderer/components/Status.js +78 -0
- package/dist/renderer/demo-app.d.ts +6 -0
- package/dist/renderer/demo-app.js +85 -0
- package/dist/renderer/demo.d.ts +6 -0
- package/dist/renderer/demo.js +52 -0
- package/dist/renderer/index.d.ts +16 -0
- package/dist/renderer/index.js +17 -0
- package/dist/renderer/main.d.ts +6 -0
- package/dist/renderer/main.js +1634 -0
- package/dist/utils/agent.d.ts +21 -0
- package/dist/utils/agent.js +29 -0
- package/dist/utils/clipboard.d.ts +15 -0
- package/dist/utils/clipboard.js +95 -0
- package/package.json +7 -11
- package/dist/utils/console.d.ts +0 -55
- package/dist/utils/console.js +0 -188
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal overlay component
|
|
3
|
+
* Renders a box with content on top of existing screen
|
|
4
|
+
*/
|
|
5
|
+
import { fg, style } from '../ansi.js';
|
|
6
|
+
import { createBox, centerBox } from './Box.js';
|
|
7
|
+
// Primary color: #f02a30 (Codeep red)
|
|
8
|
+
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
9
|
+
const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
|
|
10
|
+
/**
|
|
11
|
+
* Render a modal on the screen
|
|
12
|
+
*/
|
|
13
|
+
export function renderModal(screen, options) {
|
|
14
|
+
const { width: screenWidth, height: screenHeight } = screen.getSize();
|
|
15
|
+
// Calculate dimensions
|
|
16
|
+
const contentWidth = Math.max(...options.content.map(l => l.length), options.title.length + 4);
|
|
17
|
+
const modalWidth = options.width || Math.min(contentWidth + 4, screenWidth - 4);
|
|
18
|
+
const modalHeight = options.height || Math.min(options.content.length + 4, screenHeight - 4);
|
|
19
|
+
// Calculate position
|
|
20
|
+
let x, y;
|
|
21
|
+
if (options.centered !== false) {
|
|
22
|
+
const pos = centerBox(screenWidth, screenHeight, modalWidth, modalHeight);
|
|
23
|
+
x = pos.x;
|
|
24
|
+
y = pos.y;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
x = options.x || 0;
|
|
28
|
+
y = options.y || 0;
|
|
29
|
+
}
|
|
30
|
+
// Draw box
|
|
31
|
+
const boxLines = createBox({
|
|
32
|
+
x,
|
|
33
|
+
y,
|
|
34
|
+
width: modalWidth,
|
|
35
|
+
height: modalHeight,
|
|
36
|
+
style: options.boxStyle || 'rounded',
|
|
37
|
+
title: options.title,
|
|
38
|
+
borderColor: options.borderColor || PRIMARY_COLOR,
|
|
39
|
+
titleColor: options.titleColor || PRIMARY_BRIGHT,
|
|
40
|
+
});
|
|
41
|
+
for (const line of boxLines) {
|
|
42
|
+
screen.writeLine(line.y, line.text, line.style);
|
|
43
|
+
}
|
|
44
|
+
// Draw content
|
|
45
|
+
const contentStartY = y + 1;
|
|
46
|
+
const contentStartX = x + 2;
|
|
47
|
+
const maxContentWidth = modalWidth - 4;
|
|
48
|
+
for (let i = 0; i < options.content.length && i < modalHeight - 2; i++) {
|
|
49
|
+
const line = options.content[i];
|
|
50
|
+
const truncated = line.length > maxContentWidth
|
|
51
|
+
? line.slice(0, maxContentWidth - 1) + '…'
|
|
52
|
+
: line;
|
|
53
|
+
screen.write(contentStartX, contentStartY + i, truncated, options.contentColor || '');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Render a help/info modal with key bindings
|
|
58
|
+
*/
|
|
59
|
+
export function renderHelpModal(screen, title, items, footer) {
|
|
60
|
+
const { width: screenWidth, height: screenHeight } = screen.getSize();
|
|
61
|
+
// Format content
|
|
62
|
+
const content = [];
|
|
63
|
+
const keyWidth = Math.max(...items.map(i => i.key.length)) + 2;
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
const paddedKey = item.key.padEnd(keyWidth);
|
|
66
|
+
content.push(`${paddedKey}${item.description}`);
|
|
67
|
+
}
|
|
68
|
+
if (footer) {
|
|
69
|
+
content.push('');
|
|
70
|
+
content.push(footer);
|
|
71
|
+
}
|
|
72
|
+
// Calculate size
|
|
73
|
+
const contentWidth = Math.max(...content.map(l => l.length), title.length + 4);
|
|
74
|
+
const modalWidth = Math.min(contentWidth + 6, screenWidth - 4);
|
|
75
|
+
const modalHeight = Math.min(content.length + 4, screenHeight - 4);
|
|
76
|
+
const { x, y } = centerBox(screenWidth, screenHeight, modalWidth, modalHeight);
|
|
77
|
+
// Draw box
|
|
78
|
+
const boxLines = createBox({
|
|
79
|
+
x,
|
|
80
|
+
y,
|
|
81
|
+
width: modalWidth,
|
|
82
|
+
height: modalHeight,
|
|
83
|
+
style: 'rounded',
|
|
84
|
+
title,
|
|
85
|
+
borderColor: PRIMARY_COLOR,
|
|
86
|
+
titleColor: PRIMARY_BRIGHT,
|
|
87
|
+
});
|
|
88
|
+
for (const line of boxLines) {
|
|
89
|
+
screen.writeLine(line.y, line.text, line.style);
|
|
90
|
+
}
|
|
91
|
+
// Draw content with syntax highlighting for keys
|
|
92
|
+
const contentStartY = y + 1;
|
|
93
|
+
const contentStartX = x + 2;
|
|
94
|
+
for (let i = 0; i < content.length && i < modalHeight - 2; i++) {
|
|
95
|
+
const line = content[i];
|
|
96
|
+
// Highlight key part (before the description)
|
|
97
|
+
if (i < items.length) {
|
|
98
|
+
const item = items[i];
|
|
99
|
+
screen.write(contentStartX, contentStartY + i, item.key.padEnd(keyWidth), fg.yellow);
|
|
100
|
+
screen.write(contentStartX + keyWidth, contentStartY + i, item.description, fg.white);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
screen.write(contentStartX, contentStartY + i, line, fg.gray);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Render a confirmation modal with Yes/No options
|
|
109
|
+
*/
|
|
110
|
+
export function renderConfirmModal(screen, title, message, selectedOption, confirmLabel = 'Yes', cancelLabel = 'No') {
|
|
111
|
+
const { width: screenWidth, height: screenHeight } = screen.getSize();
|
|
112
|
+
// Calculate size
|
|
113
|
+
const contentWidth = Math.max(...message.map(l => l.length), title.length + 4, confirmLabel.length + cancelLabel.length + 12);
|
|
114
|
+
const modalWidth = Math.min(contentWidth + 8, screenWidth - 4);
|
|
115
|
+
const modalHeight = Math.min(message.length + 6, screenHeight - 4);
|
|
116
|
+
const { x, y } = centerBox(screenWidth, screenHeight, modalWidth, modalHeight);
|
|
117
|
+
// Draw box
|
|
118
|
+
const boxLines = createBox({
|
|
119
|
+
x,
|
|
120
|
+
y,
|
|
121
|
+
width: modalWidth,
|
|
122
|
+
height: modalHeight,
|
|
123
|
+
style: 'rounded',
|
|
124
|
+
title,
|
|
125
|
+
borderColor: fg.yellow,
|
|
126
|
+
titleColor: fg.yellow + style.bold,
|
|
127
|
+
});
|
|
128
|
+
for (const line of boxLines) {
|
|
129
|
+
screen.writeLine(line.y, line.text, line.style);
|
|
130
|
+
}
|
|
131
|
+
// Draw message
|
|
132
|
+
const contentStartY = y + 1;
|
|
133
|
+
const contentStartX = x + 2;
|
|
134
|
+
for (let i = 0; i < message.length; i++) {
|
|
135
|
+
screen.write(contentStartX, contentStartY + i, message[i], fg.white);
|
|
136
|
+
}
|
|
137
|
+
// Draw buttons
|
|
138
|
+
const buttonsY = y + modalHeight - 2;
|
|
139
|
+
const yesButton = selectedOption === 'yes'
|
|
140
|
+
? `${style.inverse} ${confirmLabel} ${style.reset}`
|
|
141
|
+
: ` ${confirmLabel} `;
|
|
142
|
+
const noButton = selectedOption === 'no'
|
|
143
|
+
? `${style.inverse} ${cancelLabel} ${style.reset}`
|
|
144
|
+
: ` ${cancelLabel} `;
|
|
145
|
+
const buttonsWidth = confirmLabel.length + cancelLabel.length + 8;
|
|
146
|
+
const buttonsX = x + Math.floor((modalWidth - buttonsWidth) / 2);
|
|
147
|
+
screen.write(buttonsX, buttonsY, yesButton, selectedOption === 'yes' ? PRIMARY_BRIGHT : fg.gray);
|
|
148
|
+
screen.write(buttonsX + confirmLabel.length + 4, buttonsY, noButton, selectedOption === 'no' ? PRIMARY_BRIGHT : fg.gray);
|
|
149
|
+
// Help text
|
|
150
|
+
const helpText = '←/→ Select | Enter Confirm | Esc Cancel';
|
|
151
|
+
const helpX = x + Math.floor((modalWidth - helpText.length) / 2);
|
|
152
|
+
screen.write(helpX, y + modalHeight - 1, helpText, fg.gray);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Render a list selection modal
|
|
156
|
+
*/
|
|
157
|
+
export function renderListModal(screen, title, items, selectedIndex, footer) {
|
|
158
|
+
const { width: screenWidth, height: screenHeight } = screen.getSize();
|
|
159
|
+
// Calculate max visible items (leave room for border, title, footer)
|
|
160
|
+
const maxVisibleItems = Math.max(3, screenHeight - 10);
|
|
161
|
+
const visibleItemCount = Math.min(items.length, maxVisibleItems);
|
|
162
|
+
// Calculate size - include footer in width calculation
|
|
163
|
+
const contentWidth = Math.max(...items.map(l => l.length + 4), title.length + 4, footer ? footer.length + 2 : 0);
|
|
164
|
+
const modalWidth = Math.min(contentWidth + 6, screenWidth - 4);
|
|
165
|
+
// Height: 2 for borders + visibleItemCount + 1 for padding + (footer ? 1 : 0)
|
|
166
|
+
const modalHeight = visibleItemCount + 3 + (footer ? 1 : 0);
|
|
167
|
+
const { x, y } = centerBox(screenWidth, screenHeight, modalWidth, modalHeight);
|
|
168
|
+
// Draw box
|
|
169
|
+
const boxLines = createBox({
|
|
170
|
+
x,
|
|
171
|
+
y,
|
|
172
|
+
width: modalWidth,
|
|
173
|
+
height: modalHeight,
|
|
174
|
+
style: 'rounded',
|
|
175
|
+
title,
|
|
176
|
+
borderColor: PRIMARY_COLOR,
|
|
177
|
+
titleColor: PRIMARY_BRIGHT,
|
|
178
|
+
});
|
|
179
|
+
for (const line of boxLines) {
|
|
180
|
+
screen.writeLine(line.y, line.text, line.style);
|
|
181
|
+
}
|
|
182
|
+
// Calculate visible range (scroll if needed)
|
|
183
|
+
let startIndex = 0;
|
|
184
|
+
if (items.length > visibleItemCount) {
|
|
185
|
+
startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(visibleItemCount / 2), items.length - visibleItemCount));
|
|
186
|
+
}
|
|
187
|
+
// Draw items
|
|
188
|
+
const contentStartY = y + 1;
|
|
189
|
+
const contentStartX = x + 2;
|
|
190
|
+
const visibleItems = items.slice(startIndex, startIndex + visibleItemCount);
|
|
191
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
192
|
+
const item = visibleItems[i];
|
|
193
|
+
const actualIndex = startIndex + i;
|
|
194
|
+
const isSelected = actualIndex === selectedIndex;
|
|
195
|
+
const prefix = isSelected ? '► ' : ' ';
|
|
196
|
+
const itemStyle = isSelected ? PRIMARY_BRIGHT + style.bold : fg.white;
|
|
197
|
+
screen.write(contentStartX, contentStartY + i, prefix + item, itemStyle);
|
|
198
|
+
}
|
|
199
|
+
// Draw footer (truncate if needed)
|
|
200
|
+
if (footer) {
|
|
201
|
+
const maxFooterWidth = modalWidth - 4;
|
|
202
|
+
const displayFooter = footer.length > maxFooterWidth
|
|
203
|
+
? footer.slice(0, maxFooterWidth - 1) + '…'
|
|
204
|
+
: footer;
|
|
205
|
+
screen.write(contentStartX, y + modalHeight - 2, displayFooter, fg.gray);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission screen for granting folder access
|
|
3
|
+
*/
|
|
4
|
+
import { Screen } from '../Screen';
|
|
5
|
+
export type PermissionLevel = 'none' | 'read' | 'write';
|
|
6
|
+
export interface PermissionOptions {
|
|
7
|
+
projectPath: string;
|
|
8
|
+
isProject: boolean;
|
|
9
|
+
currentPermission: PermissionLevel;
|
|
10
|
+
onSelect: (permission: PermissionLevel) => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Render permission screen
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderPermissionScreen(screen: Screen, options: PermissionOptions, selectedIndex: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* Get permission options array for easy indexing
|
|
19
|
+
*/
|
|
20
|
+
export declare function getPermissionOptions(): PermissionLevel[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission screen for granting folder access
|
|
3
|
+
*/
|
|
4
|
+
import { fg, style } from '../ansi.js';
|
|
5
|
+
import { createBox, centerBox } from './Box.js';
|
|
6
|
+
// Primary color: #f02a30 (Codeep red)
|
|
7
|
+
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
8
|
+
const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
|
|
9
|
+
/**
|
|
10
|
+
* Render permission screen
|
|
11
|
+
*/
|
|
12
|
+
export function renderPermissionScreen(screen, options, selectedIndex) {
|
|
13
|
+
const { width, height } = screen.getSize();
|
|
14
|
+
screen.clear();
|
|
15
|
+
// Title
|
|
16
|
+
const title = '═══ Folder Access ═══';
|
|
17
|
+
const titleX = Math.floor((width - title.length) / 2);
|
|
18
|
+
screen.write(titleX, 1, title, PRIMARY_COLOR + style.bold);
|
|
19
|
+
// Box
|
|
20
|
+
const boxWidth = Math.min(60, width - 4);
|
|
21
|
+
const boxHeight = 14;
|
|
22
|
+
const { x: boxX, y: boxY } = centerBox(width, height, boxWidth, boxHeight);
|
|
23
|
+
const boxLines = createBox({
|
|
24
|
+
x: boxX,
|
|
25
|
+
y: boxY,
|
|
26
|
+
width: boxWidth,
|
|
27
|
+
height: boxHeight,
|
|
28
|
+
style: 'rounded',
|
|
29
|
+
borderColor: PRIMARY_COLOR,
|
|
30
|
+
});
|
|
31
|
+
for (const line of boxLines) {
|
|
32
|
+
screen.writeLine(line.y, line.text, line.style);
|
|
33
|
+
}
|
|
34
|
+
// Content
|
|
35
|
+
const contentX = boxX + 3;
|
|
36
|
+
let contentY = boxY + 2;
|
|
37
|
+
// Project path
|
|
38
|
+
const displayPath = truncatePath(options.projectPath, boxWidth - 8);
|
|
39
|
+
screen.write(contentX, contentY, 'Project:', fg.gray);
|
|
40
|
+
screen.write(contentX + 9, contentY, displayPath, fg.white);
|
|
41
|
+
contentY += 2;
|
|
42
|
+
// Description
|
|
43
|
+
if (options.isProject) {
|
|
44
|
+
screen.write(contentX, contentY, 'This looks like a project folder.', fg.white);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
screen.write(contentX, contentY, 'Grant access to enable AI assistance.', fg.white);
|
|
48
|
+
}
|
|
49
|
+
contentY += 2;
|
|
50
|
+
// Options
|
|
51
|
+
const permissionOptions = [
|
|
52
|
+
{
|
|
53
|
+
level: 'read',
|
|
54
|
+
label: 'Read Only',
|
|
55
|
+
desc: 'AI can read files, no modifications'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
level: 'write',
|
|
59
|
+
label: 'Read & Write',
|
|
60
|
+
desc: 'AI can read and modify files (Agent mode)'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
level: 'none',
|
|
64
|
+
label: 'No Access',
|
|
65
|
+
desc: 'Chat without project context'
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
for (let i = 0; i < permissionOptions.length; i++) {
|
|
69
|
+
const opt = permissionOptions[i];
|
|
70
|
+
const isSelected = i === selectedIndex;
|
|
71
|
+
const prefix = isSelected ? '► ' : ' ';
|
|
72
|
+
// Label
|
|
73
|
+
const labelStyle = isSelected ? PRIMARY_BRIGHT + style.bold : fg.white;
|
|
74
|
+
screen.write(contentX, contentY, prefix + opt.label, labelStyle);
|
|
75
|
+
// Description on same line
|
|
76
|
+
const descX = contentX + 20;
|
|
77
|
+
screen.write(descX, contentY, opt.desc, fg.gray);
|
|
78
|
+
contentY++;
|
|
79
|
+
}
|
|
80
|
+
// Current permission indicator
|
|
81
|
+
contentY++;
|
|
82
|
+
if (options.currentPermission !== 'none') {
|
|
83
|
+
screen.write(contentX, contentY, `Current: ${options.currentPermission}`, fg.yellow);
|
|
84
|
+
}
|
|
85
|
+
// Footer
|
|
86
|
+
const footerY = height - 2;
|
|
87
|
+
screen.write(2, footerY, '↑↓ Navigate | Enter Select | Esc Skip', fg.gray);
|
|
88
|
+
screen.showCursor(false);
|
|
89
|
+
screen.fullRender();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get permission options array for easy indexing
|
|
93
|
+
*/
|
|
94
|
+
export function getPermissionOptions() {
|
|
95
|
+
return ['read', 'write', 'none'];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Truncate path for display
|
|
99
|
+
*/
|
|
100
|
+
function truncatePath(path, maxLen) {
|
|
101
|
+
if (path.length <= maxLen)
|
|
102
|
+
return path;
|
|
103
|
+
const parts = path.split('/');
|
|
104
|
+
let result = parts[parts.length - 1];
|
|
105
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
106
|
+
const newResult = parts[i] + '/' + result;
|
|
107
|
+
if (newResult.length + 3 > maxLen) {
|
|
108
|
+
return '.../' + result;
|
|
109
|
+
}
|
|
110
|
+
result = newResult;
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic fullscreen selection component
|
|
3
|
+
* Used for Language, Provider, Model, Protocol selection
|
|
4
|
+
*/
|
|
5
|
+
import { Screen } from '../Screen';
|
|
6
|
+
export interface SelectItem {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SelectScreenState {
|
|
12
|
+
selectedIndex: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render a fullscreen selection screen
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderSelectScreen(screen: Screen, title: string, items: SelectItem[], state: SelectScreenState, currentValue?: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Handle selection screen key
|
|
20
|
+
*/
|
|
21
|
+
export declare function handleSelectKey(key: string, state: SelectScreenState, itemCount: number): {
|
|
22
|
+
handled: boolean;
|
|
23
|
+
close: boolean;
|
|
24
|
+
select: boolean;
|
|
25
|
+
newState: SelectScreenState;
|
|
26
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic fullscreen selection component
|
|
3
|
+
* Used for Language, Provider, Model, Protocol selection
|
|
4
|
+
*/
|
|
5
|
+
import { fg, style } from '../ansi.js';
|
|
6
|
+
// Primary color: #f02a30 (Codeep red)
|
|
7
|
+
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
8
|
+
const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
|
|
9
|
+
/**
|
|
10
|
+
* Render a fullscreen selection screen
|
|
11
|
+
*/
|
|
12
|
+
export function renderSelectScreen(screen, title, items, state, currentValue) {
|
|
13
|
+
const { width, height } = screen.getSize();
|
|
14
|
+
screen.clear();
|
|
15
|
+
// Title
|
|
16
|
+
const titleText = `═══ ${title} ═══`;
|
|
17
|
+
const titleX = Math.floor((width - titleText.length) / 2);
|
|
18
|
+
screen.write(titleX, 0, titleText, PRIMARY_COLOR + style.bold);
|
|
19
|
+
// Calculate visible items with scrolling
|
|
20
|
+
const startY = 2;
|
|
21
|
+
const maxVisible = height - 5; // Leave room for title and footer
|
|
22
|
+
let scrollOffset = 0;
|
|
23
|
+
if (items.length > maxVisible) {
|
|
24
|
+
// Keep selected item in view
|
|
25
|
+
if (state.selectedIndex >= maxVisible - 2) {
|
|
26
|
+
scrollOffset = Math.min(state.selectedIndex - Math.floor(maxVisible / 2), items.length - maxVisible);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisible);
|
|
30
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
31
|
+
const item = visibleItems[i];
|
|
32
|
+
const actualIndex = scrollOffset + i;
|
|
33
|
+
const isSelected = actualIndex === state.selectedIndex;
|
|
34
|
+
const isCurrent = item.key === currentValue;
|
|
35
|
+
const y = startY + i;
|
|
36
|
+
// Selection indicator
|
|
37
|
+
const prefix = isSelected ? '► ' : ' ';
|
|
38
|
+
// Current value indicator
|
|
39
|
+
const currentIndicator = isCurrent ? ' ✓' : '';
|
|
40
|
+
// Label
|
|
41
|
+
const labelColor = isSelected ? PRIMARY_BRIGHT + style.bold : isCurrent ? fg.green : fg.white;
|
|
42
|
+
screen.write(2, y, prefix, isSelected ? PRIMARY_COLOR : '');
|
|
43
|
+
screen.write(4, y, item.label + currentIndicator, labelColor);
|
|
44
|
+
// Description (if any)
|
|
45
|
+
if (item.description && isSelected) {
|
|
46
|
+
const descX = Math.max(4 + item.label.length + currentIndicator.length + 2, 30);
|
|
47
|
+
if (descX + item.description.length < width - 2) {
|
|
48
|
+
screen.write(descX, y, item.description, fg.gray);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Scroll indicators
|
|
53
|
+
if (scrollOffset > 0) {
|
|
54
|
+
screen.write(width - 3, startY, '▲', fg.gray);
|
|
55
|
+
}
|
|
56
|
+
if (scrollOffset + maxVisible < items.length) {
|
|
57
|
+
screen.write(width - 3, startY + maxVisible - 1, '▼', fg.gray);
|
|
58
|
+
}
|
|
59
|
+
// Footer
|
|
60
|
+
const footerY = height - 1;
|
|
61
|
+
screen.write(2, footerY, '↑/↓ Navigate | Enter Select | Esc Cancel', fg.gray);
|
|
62
|
+
screen.showCursor(false);
|
|
63
|
+
screen.fullRender();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Handle selection screen key
|
|
67
|
+
*/
|
|
68
|
+
export function handleSelectKey(key, state, itemCount) {
|
|
69
|
+
const newState = { ...state };
|
|
70
|
+
if (key === 'escape') {
|
|
71
|
+
return { handled: true, close: true, select: false, newState };
|
|
72
|
+
}
|
|
73
|
+
if (key === 'up') {
|
|
74
|
+
newState.selectedIndex = Math.max(0, state.selectedIndex - 1);
|
|
75
|
+
return { handled: true, close: false, select: false, newState };
|
|
76
|
+
}
|
|
77
|
+
if (key === 'down') {
|
|
78
|
+
newState.selectedIndex = Math.min(itemCount - 1, state.selectedIndex + 1);
|
|
79
|
+
return { handled: true, close: false, select: false, newState };
|
|
80
|
+
}
|
|
81
|
+
if (key === 'enter') {
|
|
82
|
+
return { handled: true, close: true, select: true, newState };
|
|
83
|
+
}
|
|
84
|
+
if (key === 'pageup') {
|
|
85
|
+
newState.selectedIndex = Math.max(0, state.selectedIndex - 10);
|
|
86
|
+
return { handled: true, close: false, select: false, newState };
|
|
87
|
+
}
|
|
88
|
+
if (key === 'pagedown') {
|
|
89
|
+
newState.selectedIndex = Math.min(itemCount - 1, state.selectedIndex + 10);
|
|
90
|
+
return { handled: true, close: false, select: false, newState };
|
|
91
|
+
}
|
|
92
|
+
if (key === 'home') {
|
|
93
|
+
newState.selectedIndex = 0;
|
|
94
|
+
return { handled: true, close: false, select: false, newState };
|
|
95
|
+
}
|
|
96
|
+
if (key === 'end') {
|
|
97
|
+
newState.selectedIndex = itemCount - 1;
|
|
98
|
+
return { handled: true, close: false, select: false, newState };
|
|
99
|
+
}
|
|
100
|
+
return { handled: false, close: false, select: false, newState };
|
|
101
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings screen component
|
|
3
|
+
*/
|
|
4
|
+
import { Screen } from '../Screen';
|
|
5
|
+
export interface SettingItem {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
getValue: () => string | number | boolean;
|
|
9
|
+
type: 'number' | 'select';
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
step?: number;
|
|
13
|
+
options?: {
|
|
14
|
+
value: string | number | boolean;
|
|
15
|
+
label: string;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
export declare const SETTINGS: SettingItem[];
|
|
19
|
+
export interface SettingsState {
|
|
20
|
+
selectedIndex: number;
|
|
21
|
+
editing: boolean;
|
|
22
|
+
editValue: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Render settings screen
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderSettingsScreen(screen: Screen, state: SettingsState, hasWriteAccess: boolean, hasProjectContext: boolean): void;
|
|
28
|
+
/**
|
|
29
|
+
* Handle settings key
|
|
30
|
+
* Returns: { handled: boolean, close: boolean, notify?: string }
|
|
31
|
+
*/
|
|
32
|
+
export declare function handleSettingsKey(key: string, ctrl: boolean, state: SettingsState): {
|
|
33
|
+
handled: boolean;
|
|
34
|
+
close: boolean;
|
|
35
|
+
notify?: string;
|
|
36
|
+
newState: SettingsState;
|
|
37
|
+
};
|