codeep 1.1.36 → 1.2.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 +90 -4
- package/dist/api/index.js +64 -2
- package/dist/renderer/App.d.ts +5 -0
- package/dist/renderer/App.js +164 -227
- package/dist/renderer/components/Export.d.ts +22 -0
- package/dist/renderer/components/Export.js +64 -0
- package/dist/renderer/components/Help.js +5 -1
- package/dist/renderer/components/Logout.d.ts +29 -0
- package/dist/renderer/components/Logout.js +91 -0
- package/dist/renderer/components/Search.d.ts +30 -0
- package/dist/renderer/components/Search.js +83 -0
- package/dist/renderer/components/Settings.js +20 -0
- package/dist/renderer/components/Status.d.ts +6 -0
- package/dist/renderer/components/Status.js +20 -1
- package/dist/renderer/main.js +296 -142
- package/dist/utils/agent.d.ts +5 -0
- package/dist/utils/agent.js +238 -3
- package/dist/utils/agent.test.d.ts +1 -0
- package/dist/utils/agent.test.js +250 -0
- package/dist/utils/diffPreview.js +104 -35
- package/dist/utils/gitignore.d.ts +24 -0
- package/dist/utils/gitignore.js +161 -0
- package/dist/utils/gitignore.test.d.ts +1 -0
- package/dist/utils/gitignore.test.js +167 -0
- package/dist/utils/skills.d.ts +21 -0
- package/dist/utils/skills.js +51 -0
- package/dist/utils/smartContext.js +8 -0
- package/dist/utils/smartContext.test.d.ts +1 -0
- package/dist/utils/smartContext.test.js +382 -0
- package/dist/utils/tokenTracker.d.ts +52 -0
- package/dist/utils/tokenTracker.js +86 -0
- package/dist/utils/tools.d.ts +16 -0
- package/dist/utils/tools.js +146 -19
- package/dist/utils/tools.test.d.ts +1 -0
- package/dist/utils/tools.test.js +664 -0
- package/package.json +1 -1
package/dist/renderer/App.js
CHANGED
|
@@ -199,6 +199,9 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
199
199
|
'copy': 'Copy code block',
|
|
200
200
|
'paste': 'Paste from clipboard',
|
|
201
201
|
'apply': 'Apply file changes',
|
|
202
|
+
'add': 'Add file to context',
|
|
203
|
+
'drop': 'Remove file from context',
|
|
204
|
+
'multiline': 'Toggle multi-line input',
|
|
202
205
|
'test': 'Generate/run tests',
|
|
203
206
|
'docs': 'Add documentation',
|
|
204
207
|
'refactor': 'Improve code quality',
|
|
@@ -221,6 +224,9 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
221
224
|
};
|
|
222
225
|
import { helpCategories, keyboardShortcuts } from './components/Help.js';
|
|
223
226
|
import { handleSettingsKey, SETTINGS } from './components/Settings.js';
|
|
227
|
+
import { renderExportPanel, handleExportKey as handleExportKeyComponent } from './components/Export.js';
|
|
228
|
+
import { renderLogoutPanel, handleLogoutKey as handleLogoutKeyComponent } from './components/Logout.js';
|
|
229
|
+
import { renderSearchPanel, handleSearchKey as handleSearchKeyComponent } from './components/Search.js';
|
|
224
230
|
export class App {
|
|
225
231
|
screen;
|
|
226
232
|
input;
|
|
@@ -306,6 +312,8 @@ export class App {
|
|
|
306
312
|
introProgress = 0;
|
|
307
313
|
introInterval = null;
|
|
308
314
|
introCallback = null;
|
|
315
|
+
// Multi-line input state
|
|
316
|
+
isMultilineMode = false;
|
|
309
317
|
// Inline login state
|
|
310
318
|
loginOpen = false;
|
|
311
319
|
loginStep = 'provider';
|
|
@@ -322,8 +330,16 @@ export class App {
|
|
|
322
330
|
'sessions', 'new', 'rename', 'search', 'export',
|
|
323
331
|
'agent', 'agent-dry', 'stop', 'undo', 'undo-all', 'history', 'changes',
|
|
324
332
|
'diff', 'commit', 'git-commit', 'push', 'pull', 'scan', 'review',
|
|
325
|
-
'copy', 'paste', 'apply',
|
|
333
|
+
'copy', 'paste', 'apply', 'add', 'drop',
|
|
326
334
|
'test', 'docs', 'refactor', 'fix', 'explain', 'optimize', 'debug', 'skills',
|
|
335
|
+
'amend', 'pr', 'changelog', 'branch', 'stash', 'unstash',
|
|
336
|
+
'build', 'deploy', 'release', 'publish',
|
|
337
|
+
'component', 'api', 'model', 'hook', 'service', 'page', 'form', 'crud',
|
|
338
|
+
'security', 'profile', 'log', 'types', 'cleanup', 'modernize', 'migrate',
|
|
339
|
+
'split', 'rename', 'coverage', 'e2e', 'mock', 'readme', 'translate',
|
|
340
|
+
'docker', 'ci', 'env', 'k8s', 'terraform', 'nginx', 'monitor',
|
|
341
|
+
'test-fix', 'api-docs',
|
|
342
|
+
'multiline',
|
|
327
343
|
'provider', 'model', 'protocol', 'lang', 'grant', 'login', 'logout',
|
|
328
344
|
'context-save', 'context-load', 'context-clear', 'learn',
|
|
329
345
|
'c', 't', 'd', 'r', 'f', 'e', 'o', 'b', 'p',
|
|
@@ -880,6 +896,13 @@ export class App {
|
|
|
880
896
|
this.render();
|
|
881
897
|
return;
|
|
882
898
|
}
|
|
899
|
+
// In multiline mode, Escape submits the buffered input
|
|
900
|
+
if (this.isMultilineMode && !this.isLoading && !this.isStreaming) {
|
|
901
|
+
if (this.editor.getValue().trim()) {
|
|
902
|
+
this.submitInput();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
883
906
|
if (this.isAgentRunning && this.options.onStopAgent) {
|
|
884
907
|
this.options.onStopAgent();
|
|
885
908
|
return;
|
|
@@ -997,25 +1020,20 @@ export class App {
|
|
|
997
1020
|
}
|
|
998
1021
|
// Enter to submit (only if not in autocomplete)
|
|
999
1022
|
if (event.key === 'enter' && !this.isLoading && !this.isStreaming && !this.showAutocomplete) {
|
|
1000
|
-
const
|
|
1001
|
-
if
|
|
1002
|
-
|
|
1003
|
-
this.editor.
|
|
1004
|
-
this.
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
this.setLoading(true);
|
|
1013
|
-
this.options.onSubmit(value).catch(err => {
|
|
1014
|
-
this.notify(`Error: ${err.message}`);
|
|
1015
|
-
this.setLoading(false);
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1023
|
+
const rawValue = this.editor.getValue();
|
|
1024
|
+
// Backslash continuation: if line ends with \, add newline instead of submitting
|
|
1025
|
+
if (rawValue.endsWith('\\')) {
|
|
1026
|
+
this.editor.setValue(rawValue.slice(0, -1) + '\n');
|
|
1027
|
+
this.render();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
// Multiline mode: Enter adds newline, Ctrl+Enter submits
|
|
1031
|
+
if (this.isMultilineMode && !event.ctrl) {
|
|
1032
|
+
this.editor.insert('\n');
|
|
1033
|
+
this.render();
|
|
1034
|
+
return;
|
|
1018
1035
|
}
|
|
1036
|
+
this.submitInput();
|
|
1019
1037
|
return;
|
|
1020
1038
|
}
|
|
1021
1039
|
// Handle paste detection
|
|
@@ -1110,109 +1128,75 @@ export class App {
|
|
|
1110
1128
|
* Handle search screen keys
|
|
1111
1129
|
*/
|
|
1112
1130
|
handleSearchKey(event) {
|
|
1113
|
-
|
|
1114
|
-
this.searchOpen
|
|
1115
|
-
this.
|
|
1116
|
-
this.
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
callback(selectedResult.messageIndex);
|
|
1137
|
-
}
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1131
|
+
const state = {
|
|
1132
|
+
searchOpen: this.searchOpen,
|
|
1133
|
+
searchQuery: this.searchQuery,
|
|
1134
|
+
searchResults: this.searchResults,
|
|
1135
|
+
searchIndex: this.searchIndex,
|
|
1136
|
+
searchCallback: this.searchCallback,
|
|
1137
|
+
};
|
|
1138
|
+
const callback = this.searchCallback;
|
|
1139
|
+
handleSearchKeyComponent(event, state, {
|
|
1140
|
+
onClose: () => {
|
|
1141
|
+
this.searchOpen = false;
|
|
1142
|
+
this.searchCallback = null;
|
|
1143
|
+
},
|
|
1144
|
+
onRender: () => {
|
|
1145
|
+
this.searchIndex = state.searchIndex;
|
|
1146
|
+
this.render();
|
|
1147
|
+
},
|
|
1148
|
+
onResult: (messageIndex) => {
|
|
1149
|
+
if (callback) {
|
|
1150
|
+
callback(messageIndex);
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
});
|
|
1140
1154
|
}
|
|
1141
1155
|
/**
|
|
1142
1156
|
* Handle export screen keys
|
|
1143
1157
|
*/
|
|
1144
1158
|
handleExportKey(event) {
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
this.
|
|
1148
|
-
this.exportCallback
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
this.
|
|
1154
|
-
this.render();
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
if (event.key === 'down') {
|
|
1158
|
-
this.exportIndex = this.exportIndex < formats.length - 1 ? this.exportIndex + 1 : 0;
|
|
1159
|
-
this.render();
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (event.key === 'enter') {
|
|
1163
|
-
const selectedFormat = formats[this.exportIndex];
|
|
1164
|
-
const callback = this.exportCallback;
|
|
1165
|
-
this.exportOpen = false;
|
|
1166
|
-
this.exportCallback = null;
|
|
1159
|
+
const state = {
|
|
1160
|
+
exportOpen: this.exportOpen,
|
|
1161
|
+
exportIndex: this.exportIndex,
|
|
1162
|
+
exportCallback: this.exportCallback,
|
|
1163
|
+
};
|
|
1164
|
+
const syncState = () => {
|
|
1165
|
+
this.exportOpen = state.exportOpen;
|
|
1166
|
+
this.exportIndex = state.exportIndex;
|
|
1167
|
+
this.exportCallback = state.exportCallback;
|
|
1167
1168
|
this.render();
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1169
|
+
};
|
|
1170
|
+
handleExportKeyComponent(event, state, {
|
|
1171
|
+
onClose: syncState,
|
|
1172
|
+
onRender: syncState,
|
|
1173
|
+
onExport: (format) => {
|
|
1174
|
+
const callback = this.exportCallback;
|
|
1175
|
+
syncState();
|
|
1176
|
+
if (callback) {
|
|
1177
|
+
callback(format);
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
});
|
|
1173
1181
|
}
|
|
1174
1182
|
/**
|
|
1175
1183
|
* Handle logout picker keys
|
|
1176
1184
|
*/
|
|
1177
1185
|
handleLogoutKey(event) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
this.
|
|
1182
|
-
this.logoutCallback
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
this.logoutIndex = Math.min(totalOptions - 1, this.logoutIndex + 1);
|
|
1193
|
-
this.render();
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
if (event.key === 'enter') {
|
|
1197
|
-
const callback = this.logoutCallback;
|
|
1198
|
-
this.logoutOpen = false;
|
|
1199
|
-
this.logoutCallback = null;
|
|
1200
|
-
let result = null;
|
|
1201
|
-
if (this.logoutIndex < this.logoutProviders.length) {
|
|
1202
|
-
result = this.logoutProviders[this.logoutIndex].id;
|
|
1203
|
-
}
|
|
1204
|
-
else if (this.logoutIndex === this.logoutProviders.length) {
|
|
1205
|
-
result = 'all';
|
|
1206
|
-
}
|
|
1207
|
-
else {
|
|
1208
|
-
result = null; // Cancel
|
|
1209
|
-
}
|
|
1210
|
-
this.render();
|
|
1211
|
-
if (callback) {
|
|
1212
|
-
callback(result);
|
|
1213
|
-
}
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1186
|
+
const state = {
|
|
1187
|
+
logoutOpen: this.logoutOpen,
|
|
1188
|
+
logoutIndex: this.logoutIndex,
|
|
1189
|
+
logoutProviders: this.logoutProviders,
|
|
1190
|
+
logoutCallback: this.logoutCallback,
|
|
1191
|
+
};
|
|
1192
|
+
handleLogoutKeyComponent(event, state, {
|
|
1193
|
+
onClose: () => { },
|
|
1194
|
+
onRender: () => this.render(),
|
|
1195
|
+
onSelect: () => { },
|
|
1196
|
+
});
|
|
1197
|
+
this.logoutOpen = state.logoutOpen;
|
|
1198
|
+
this.logoutIndex = state.logoutIndex;
|
|
1199
|
+
this.logoutCallback = state.logoutCallback;
|
|
1216
1200
|
}
|
|
1217
1201
|
/**
|
|
1218
1202
|
* Handle login keys
|
|
@@ -1536,6 +1520,28 @@ export class App {
|
|
|
1536
1520
|
return;
|
|
1537
1521
|
}
|
|
1538
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Submit the current input buffer (used by Enter and Escape-in-multiline)
|
|
1525
|
+
*/
|
|
1526
|
+
submitInput() {
|
|
1527
|
+
const value = this.editor.getValue().trim();
|
|
1528
|
+
if (!value)
|
|
1529
|
+
return;
|
|
1530
|
+
this.editor.addToHistory(value);
|
|
1531
|
+
this.editor.clear();
|
|
1532
|
+
this.showAutocomplete = false;
|
|
1533
|
+
if (value.startsWith('/')) {
|
|
1534
|
+
this.handleCommand(value);
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
this.addMessage({ role: 'user', content: value });
|
|
1538
|
+
this.setLoading(true);
|
|
1539
|
+
this.options.onSubmit(value).catch(err => {
|
|
1540
|
+
this.notify(`Error: ${err.message}`);
|
|
1541
|
+
this.setLoading(false);
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1539
1545
|
/**
|
|
1540
1546
|
* Handle command
|
|
1541
1547
|
*/
|
|
@@ -1557,6 +1563,12 @@ export class App {
|
|
|
1557
1563
|
this.clearMessages();
|
|
1558
1564
|
this.notify('Chat cleared');
|
|
1559
1565
|
break;
|
|
1566
|
+
case 'multiline':
|
|
1567
|
+
this.isMultilineMode = !this.isMultilineMode;
|
|
1568
|
+
this.notify(this.isMultilineMode
|
|
1569
|
+
? 'Multi-line mode ON — Enter adds line, Esc sends'
|
|
1570
|
+
: 'Multi-line mode OFF — Enter sends');
|
|
1571
|
+
break;
|
|
1560
1572
|
case 'exit':
|
|
1561
1573
|
case 'quit':
|
|
1562
1574
|
this.stop();
|
|
@@ -1829,12 +1841,18 @@ export class App {
|
|
|
1829
1841
|
this.screen.showCursor(false);
|
|
1830
1842
|
return;
|
|
1831
1843
|
}
|
|
1832
|
-
|
|
1844
|
+
// Build prompt prefix — show line count for multi-line buffers
|
|
1845
|
+
const lines = inputValue.split('\n');
|
|
1846
|
+
const lineCount = lines.length;
|
|
1847
|
+
const prompt = lineCount > 1 ? `[${lineCount}] > ` : this.isMultilineMode ? 'M> ' : '> ';
|
|
1833
1848
|
const maxInputWidth = width - prompt.length - 1;
|
|
1834
1849
|
// Show placeholder when input is empty
|
|
1835
1850
|
if (!inputValue) {
|
|
1836
1851
|
this.screen.write(0, y, prompt, fg.green);
|
|
1837
|
-
|
|
1852
|
+
const placeholder = this.isMultilineMode
|
|
1853
|
+
? 'Multi-line mode (Enter=newline, Esc=send)...'
|
|
1854
|
+
: 'Type a message or /command...';
|
|
1855
|
+
this.screen.write(prompt.length, y, placeholder, fg.gray);
|
|
1838
1856
|
if (!hideCursor) {
|
|
1839
1857
|
this.screen.setCursor(prompt.length, y);
|
|
1840
1858
|
this.screen.showCursor(true);
|
|
@@ -1844,22 +1862,29 @@ export class App {
|
|
|
1844
1862
|
}
|
|
1845
1863
|
return;
|
|
1846
1864
|
}
|
|
1865
|
+
// For multi-line content, show the last line being edited
|
|
1866
|
+
const lastLine = lines[lines.length - 1];
|
|
1867
|
+
const displayInput = lineCount > 1 ? lastLine : inputValue;
|
|
1868
|
+
// Cursor position within the last line
|
|
1869
|
+
const charsBeforeLastLine = lineCount > 1 ? inputValue.lastIndexOf('\n') + 1 : 0;
|
|
1870
|
+
const cursorInLine = cursorPos - charsBeforeLastLine;
|
|
1847
1871
|
let displayValue;
|
|
1848
1872
|
let cursorX;
|
|
1849
|
-
if (
|
|
1850
|
-
displayValue =
|
|
1851
|
-
cursorX = prompt.length +
|
|
1873
|
+
if (displayInput.length <= maxInputWidth) {
|
|
1874
|
+
displayValue = displayInput;
|
|
1875
|
+
cursorX = prompt.length + Math.max(0, cursorInLine);
|
|
1852
1876
|
}
|
|
1853
1877
|
else {
|
|
1854
|
-
const
|
|
1878
|
+
const effectiveCursor = Math.max(0, cursorInLine);
|
|
1879
|
+
const visibleStart = Math.max(0, effectiveCursor - Math.floor(maxInputWidth * 0.7));
|
|
1855
1880
|
const visibleEnd = visibleStart + maxInputWidth;
|
|
1856
1881
|
if (visibleStart > 0) {
|
|
1857
|
-
displayValue = '…' +
|
|
1882
|
+
displayValue = '…' + displayInput.slice(visibleStart + 1, visibleEnd);
|
|
1858
1883
|
}
|
|
1859
1884
|
else {
|
|
1860
|
-
displayValue =
|
|
1885
|
+
displayValue = displayInput.slice(0, maxInputWidth);
|
|
1861
1886
|
}
|
|
1862
|
-
cursorX = prompt.length + (
|
|
1887
|
+
cursorX = prompt.length + (effectiveCursor - visibleStart);
|
|
1863
1888
|
}
|
|
1864
1889
|
this.screen.writeLine(y, prompt + displayValue, fg.green);
|
|
1865
1890
|
// Hide cursor when menu/settings is open
|
|
@@ -1867,7 +1892,7 @@ export class App {
|
|
|
1867
1892
|
this.screen.showCursor(false);
|
|
1868
1893
|
}
|
|
1869
1894
|
else {
|
|
1870
|
-
this.screen.setCursor(cursorX, y);
|
|
1895
|
+
this.screen.setCursor(Math.min(cursorX, width - 1), y);
|
|
1871
1896
|
this.screen.showCursor(true);
|
|
1872
1897
|
}
|
|
1873
1898
|
}
|
|
@@ -2619,122 +2644,34 @@ export class App {
|
|
|
2619
2644
|
* Render inline search screen
|
|
2620
2645
|
*/
|
|
2621
2646
|
renderInlineSearch(startY, width, availableHeight) {
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
this.screen.write(7, y, `"${this.searchQuery}"`, fg.cyan);
|
|
2630
|
-
if (this.searchResults.length > 0) {
|
|
2631
|
-
this.screen.write(9 + this.searchQuery.length, y, ` (${this.searchResults.length} ${this.searchResults.length === 1 ? 'result' : 'results'})`, fg.gray);
|
|
2632
|
-
}
|
|
2633
|
-
y++;
|
|
2634
|
-
y++;
|
|
2635
|
-
if (this.searchResults.length === 0) {
|
|
2636
|
-
this.screen.writeLine(y++, 'No results found.', fg.yellow);
|
|
2637
|
-
}
|
|
2638
|
-
else {
|
|
2639
|
-
const maxVisible = availableHeight - 6;
|
|
2640
|
-
const visibleStart = Math.max(0, this.searchIndex - Math.floor(maxVisible / 2));
|
|
2641
|
-
const visibleResults = this.searchResults.slice(visibleStart, visibleStart + maxVisible);
|
|
2642
|
-
for (let i = 0; i < visibleResults.length; i++) {
|
|
2643
|
-
const result = visibleResults[i];
|
|
2644
|
-
const actualIndex = visibleStart + i;
|
|
2645
|
-
const isSelected = actualIndex === this.searchIndex;
|
|
2646
|
-
const prefix = isSelected ? '▸ ' : ' ';
|
|
2647
|
-
const roleColor = result.role === 'user' ? fg.green : fg.blue;
|
|
2648
|
-
// First line: role and message number
|
|
2649
|
-
this.screen.write(0, y, prefix, isSelected ? PRIMARY_COLOR : '');
|
|
2650
|
-
this.screen.write(2, y, `[${result.role.toUpperCase()}]`, roleColor + style.bold);
|
|
2651
|
-
this.screen.write(2 + result.role.length + 2, y, ` Message #${result.messageIndex + 1}`, fg.gray);
|
|
2652
|
-
y++;
|
|
2653
|
-
// Second line: matched text (truncated)
|
|
2654
|
-
const maxTextWidth = width - 4;
|
|
2655
|
-
const matchedText = result.matchedText.length > maxTextWidth
|
|
2656
|
-
? result.matchedText.slice(0, maxTextWidth - 3) + '...'
|
|
2657
|
-
: result.matchedText;
|
|
2658
|
-
this.screen.writeLine(y, ' ' + matchedText, fg.white);
|
|
2659
|
-
y++;
|
|
2660
|
-
if (i < visibleResults.length - 1)
|
|
2661
|
-
y++; // spacing between results
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
// Footer
|
|
2665
|
-
y = startY + availableHeight - 1;
|
|
2666
|
-
this.screen.writeLine(y, '↑↓ Navigate • Enter Jump to message • Esc Close', fg.gray);
|
|
2647
|
+
renderSearchPanel(this.screen, startY, width, availableHeight, {
|
|
2648
|
+
searchOpen: this.searchOpen,
|
|
2649
|
+
searchQuery: this.searchQuery,
|
|
2650
|
+
searchResults: this.searchResults,
|
|
2651
|
+
searchIndex: this.searchIndex,
|
|
2652
|
+
searchCallback: this.searchCallback,
|
|
2653
|
+
});
|
|
2667
2654
|
}
|
|
2668
2655
|
/**
|
|
2669
2656
|
* Render inline export screen
|
|
2670
2657
|
*/
|
|
2671
2658
|
renderInlineExport(startY, width) {
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
let y = startY;
|
|
2678
|
-
// Separator line
|
|
2679
|
-
this.screen.horizontalLine(y++, '─', fg.green);
|
|
2680
|
-
// Title
|
|
2681
|
-
this.screen.writeLine(y++, 'Export Chat', fg.green + style.bold);
|
|
2682
|
-
y++;
|
|
2683
|
-
this.screen.writeLine(y++, 'Select export format:', fg.white);
|
|
2684
|
-
y++;
|
|
2685
|
-
for (let i = 0; i < formats.length; i++) {
|
|
2686
|
-
const format = formats[i];
|
|
2687
|
-
const isSelected = i === this.exportIndex;
|
|
2688
|
-
const prefix = isSelected ? '› ' : ' ';
|
|
2689
|
-
this.screen.write(0, y, prefix, isSelected ? fg.green : '');
|
|
2690
|
-
this.screen.write(2, y, format.name.padEnd(12), isSelected ? fg.green + style.bold : fg.white);
|
|
2691
|
-
this.screen.write(14, y, ' - ' + format.desc, fg.gray);
|
|
2692
|
-
y++;
|
|
2693
|
-
}
|
|
2694
|
-
y++;
|
|
2695
|
-
this.screen.writeLine(y, '↑↓ Navigate • Enter Export • Esc Cancel', fg.gray);
|
|
2659
|
+
renderExportPanel(this.screen, startY, width, {
|
|
2660
|
+
exportOpen: this.exportOpen,
|
|
2661
|
+
exportIndex: this.exportIndex,
|
|
2662
|
+
exportCallback: this.exportCallback,
|
|
2663
|
+
});
|
|
2696
2664
|
}
|
|
2697
2665
|
/**
|
|
2698
2666
|
* Render inline logout picker
|
|
2699
2667
|
*/
|
|
2700
2668
|
renderInlineLogout(startY, width) {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
if (this.logoutProviders.length === 0) {
|
|
2708
|
-
this.screen.writeLine(y++, 'No providers configured.', fg.yellow);
|
|
2709
|
-
this.screen.writeLine(y++, 'Press Escape to go back.', fg.gray);
|
|
2710
|
-
return;
|
|
2711
|
-
}
|
|
2712
|
-
// Provider options
|
|
2713
|
-
for (let i = 0; i < this.logoutProviders.length; i++) {
|
|
2714
|
-
const provider = this.logoutProviders[i];
|
|
2715
|
-
const isSelected = i === this.logoutIndex;
|
|
2716
|
-
const prefix = isSelected ? '→ ' : ' ';
|
|
2717
|
-
this.screen.write(0, y, prefix, isSelected ? fg.green : '');
|
|
2718
|
-
this.screen.write(2, y, provider.name, isSelected ? fg.green + style.bold : fg.white);
|
|
2719
|
-
if (provider.isCurrent) {
|
|
2720
|
-
this.screen.write(2 + provider.name.length + 1, y, '(current)', fg.cyan);
|
|
2721
|
-
}
|
|
2722
|
-
y++;
|
|
2723
|
-
}
|
|
2724
|
-
// "All" option
|
|
2725
|
-
const allIndex = this.logoutProviders.length;
|
|
2726
|
-
const isAllSelected = this.logoutIndex === allIndex;
|
|
2727
|
-
this.screen.write(0, y, isAllSelected ? '→ ' : ' ', isAllSelected ? fg.red : '');
|
|
2728
|
-
this.screen.write(2, y, 'Logout from all providers', isAllSelected ? fg.red + style.bold : fg.yellow);
|
|
2729
|
-
y++;
|
|
2730
|
-
// "Cancel" option
|
|
2731
|
-
const cancelIndex = this.logoutProviders.length + 1;
|
|
2732
|
-
const isCancelSelected = this.logoutIndex === cancelIndex;
|
|
2733
|
-
this.screen.write(0, y, isCancelSelected ? '→ ' : ' ', isCancelSelected ? fg.blue : '');
|
|
2734
|
-
this.screen.write(2, y, 'Cancel', isCancelSelected ? fg.blue + style.bold : fg.gray);
|
|
2735
|
-
y++;
|
|
2736
|
-
y++;
|
|
2737
|
-
this.screen.writeLine(y, '↑↓ Navigate • Enter Select • Esc Cancel', fg.gray);
|
|
2669
|
+
renderLogoutPanel(this.screen, startY, width, {
|
|
2670
|
+
logoutOpen: this.logoutOpen,
|
|
2671
|
+
logoutIndex: this.logoutIndex,
|
|
2672
|
+
logoutProviders: this.logoutProviders,
|
|
2673
|
+
logoutCallback: this.logoutCallback,
|
|
2674
|
+
});
|
|
2738
2675
|
}
|
|
2739
2676
|
/**
|
|
2740
2677
|
* Render inline login dialog
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export panel component
|
|
3
|
+
*/
|
|
4
|
+
import { Screen } from '../Screen';
|
|
5
|
+
import { KeyEvent } from '../Input';
|
|
6
|
+
export interface ExportState {
|
|
7
|
+
exportOpen: boolean;
|
|
8
|
+
exportIndex: number;
|
|
9
|
+
exportCallback: ((format: 'md' | 'json' | 'txt') => void) | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Render inline export panel
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderExportPanel(screen: Screen, startY: number, width: number, state: ExportState): void;
|
|
15
|
+
/**
|
|
16
|
+
* Handle export panel key events
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleExportKey(event: KeyEvent, state: ExportState, callbacks: {
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
onRender: () => void;
|
|
21
|
+
onExport: (format: 'md' | 'json' | 'txt') => void;
|
|
22
|
+
}): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export panel component
|
|
3
|
+
*/
|
|
4
|
+
import { fg, style } from '../ansi.js';
|
|
5
|
+
// Primary color: #f02a30 (Codeep red)
|
|
6
|
+
const PRIMARY_COLOR = fg.rgb(240, 42, 48);
|
|
7
|
+
const FORMATS = [
|
|
8
|
+
{ id: 'md', name: 'Markdown', desc: 'Formatted with headers and separators' },
|
|
9
|
+
{ id: 'json', name: 'JSON', desc: 'Structured data format' },
|
|
10
|
+
{ id: 'txt', name: 'Plain Text', desc: 'Simple text format' },
|
|
11
|
+
];
|
|
12
|
+
const FORMAT_IDS = ['md', 'json', 'txt'];
|
|
13
|
+
/**
|
|
14
|
+
* Render inline export panel
|
|
15
|
+
*/
|
|
16
|
+
export function renderExportPanel(screen, startY, width, state) {
|
|
17
|
+
let y = startY;
|
|
18
|
+
// Separator line
|
|
19
|
+
screen.horizontalLine(y++, '─', fg.green);
|
|
20
|
+
// Title
|
|
21
|
+
screen.writeLine(y++, 'Export Chat', fg.green + style.bold);
|
|
22
|
+
y++;
|
|
23
|
+
screen.writeLine(y++, 'Select export format:', fg.white);
|
|
24
|
+
y++;
|
|
25
|
+
for (let i = 0; i < FORMATS.length; i++) {
|
|
26
|
+
const format = FORMATS[i];
|
|
27
|
+
const isSelected = i === state.exportIndex;
|
|
28
|
+
const prefix = isSelected ? '› ' : ' ';
|
|
29
|
+
screen.write(0, y, prefix, isSelected ? fg.green : '');
|
|
30
|
+
screen.write(2, y, format.name.padEnd(12), isSelected ? fg.green + style.bold : fg.white);
|
|
31
|
+
screen.write(14, y, ' - ' + format.desc, fg.gray);
|
|
32
|
+
y++;
|
|
33
|
+
}
|
|
34
|
+
y++;
|
|
35
|
+
screen.writeLine(y, '↑↓ Navigate • Enter Export • Esc Cancel', fg.gray);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Handle export panel key events
|
|
39
|
+
*/
|
|
40
|
+
export function handleExportKey(event, state, callbacks) {
|
|
41
|
+
if (event.key === 'escape') {
|
|
42
|
+
state.exportOpen = false;
|
|
43
|
+
state.exportCallback = null;
|
|
44
|
+
callbacks.onClose();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (event.key === 'up') {
|
|
48
|
+
state.exportIndex = state.exportIndex > 0 ? state.exportIndex - 1 : FORMAT_IDS.length - 1;
|
|
49
|
+
callbacks.onRender();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (event.key === 'down') {
|
|
53
|
+
state.exportIndex = state.exportIndex < FORMAT_IDS.length - 1 ? state.exportIndex + 1 : 0;
|
|
54
|
+
callbacks.onRender();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.key === 'enter') {
|
|
58
|
+
const selectedFormat = FORMAT_IDS[state.exportIndex];
|
|
59
|
+
state.exportOpen = false;
|
|
60
|
+
state.exportCallback = null;
|
|
61
|
+
callbacks.onExport(selectedFormat);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -61,6 +61,9 @@ export const helpCategories = [
|
|
|
61
61
|
{ key: '/copy [n]', description: 'Copy code block to clipboard' },
|
|
62
62
|
{ key: '/paste', description: 'Paste from clipboard' },
|
|
63
63
|
{ key: '/apply', description: 'Apply file changes from AI' },
|
|
64
|
+
{ key: '/add <path>', description: 'Add file to context' },
|
|
65
|
+
{ key: '/drop [path]', description: 'Remove file (or all) from context' },
|
|
66
|
+
{ key: '/multiline', description: 'Toggle multi-line input mode' },
|
|
64
67
|
],
|
|
65
68
|
},
|
|
66
69
|
{
|
|
@@ -105,7 +108,8 @@ export const helpCategories = [
|
|
|
105
108
|
*/
|
|
106
109
|
export const keyboardShortcuts = [
|
|
107
110
|
{ key: 'Enter', description: 'Send message' },
|
|
108
|
-
{ key: '
|
|
111
|
+
{ key: '\\+Enter', description: 'Continue on next line' },
|
|
112
|
+
{ key: 'Esc', description: 'Cancel/Close (send in multiline)' },
|
|
109
113
|
{ key: 'Ctrl+L', description: 'Clear screen' },
|
|
110
114
|
{ key: 'Ctrl+C', description: 'Exit' },
|
|
111
115
|
{ key: '↑/↓', description: 'Input history' },
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logout panel component
|
|
3
|
+
*/
|
|
4
|
+
import { Screen } from '../Screen';
|
|
5
|
+
import { KeyEvent } from '../Input';
|
|
6
|
+
export interface LogoutProvider {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
isCurrent: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface LogoutState {
|
|
12
|
+
logoutOpen: boolean;
|
|
13
|
+
logoutIndex: number;
|
|
14
|
+
logoutProviders: LogoutProvider[];
|
|
15
|
+
logoutCallback: ((providerId: string | 'all' | null) => void) | null;
|
|
16
|
+
}
|
|
17
|
+
export interface LogoutCallbacks {
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
onRender: () => void;
|
|
20
|
+
onSelect: (result: string | 'all' | null) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Render inline logout picker
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderLogoutPanel(screen: Screen, startY: number, width: number, state: LogoutState): void;
|
|
26
|
+
/**
|
|
27
|
+
* Handle logout picker keys
|
|
28
|
+
*/
|
|
29
|
+
export declare function handleLogoutKey(event: KeyEvent, state: LogoutState, callbacks: LogoutCallbacks): void;
|