ralph-cli-sandboxed 0.4.1 → 0.4.2
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 +30 -0
- package/dist/commands/action.js +9 -9
- package/dist/commands/chat.js +13 -12
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/docker.js +102 -66
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +3 -1
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +17 -9
- package/dist/commands/prd.js +4 -1
- package/dist/commands/run.js +40 -25
- package/dist/commands/slack.js +2 -2
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +1 -1
- package/dist/providers/discord.d.ts +28 -0
- package/dist/providers/discord.js +227 -14
- package/dist/providers/slack.d.ts +41 -1
- package/dist/providers/slack.js +389 -8
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +185 -5
- package/dist/responders/claude-code-responder.d.ts +48 -0
- package/dist/responders/claude-code-responder.js +203 -0
- package/dist/responders/cli-responder.d.ts +62 -0
- package/dist/responders/cli-responder.js +298 -0
- package/dist/responders/llm-responder.d.ts +135 -0
- package/dist/responders/llm-responder.js +582 -0
- package/dist/templates/macos-scripts.js +2 -4
- package/dist/templates/prompts.js +4 -2
- package/dist/tui/ConfigEditor.js +19 -5
- package/dist/tui/components/ArrayEditor.js +1 -1
- package/dist/tui/components/EditorPanel.js +10 -6
- package/dist/tui/components/HelpPanel.d.ts +1 -1
- package/dist/tui/components/HelpPanel.js +1 -1
- package/dist/tui/components/JsonSnippetEditor.js +8 -5
- package/dist/tui/components/KeyValueEditor.js +54 -9
- package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
- package/dist/tui/components/LLMProvidersEditor.js +357 -0
- package/dist/tui/components/ObjectEditor.js +1 -1
- package/dist/tui/components/Preview.js +1 -1
- package/dist/tui/components/RespondersEditor.d.ts +22 -0
- package/dist/tui/components/RespondersEditor.js +437 -0
- package/dist/tui/components/SectionNav.js +27 -3
- package/dist/utils/chat-client.d.ts +4 -0
- package/dist/utils/chat-client.js +12 -5
- package/dist/utils/config.d.ts +84 -0
- package/dist/utils/config.js +78 -1
- package/dist/utils/daemon-client.d.ts +21 -0
- package/dist/utils/daemon-client.js +28 -1
- package/dist/utils/llm-client.d.ts +82 -0
- package/dist/utils/llm-client.js +185 -0
- package/dist/utils/message-queue.js +6 -6
- package/dist/utils/notification.d.ts +6 -1
- package/dist/utils/notification.js +103 -2
- package/dist/utils/prd-validator.js +60 -19
- package/dist/utils/prompt.js +22 -12
- package/dist/utils/responder-logger.d.ts +47 -0
- package/dist/utils/responder-logger.js +129 -0
- package/dist/utils/responder-presets.d.ts +92 -0
- package/dist/utils/responder-presets.js +156 -0
- package/dist/utils/responder.d.ts +88 -0
- package/dist/utils/responder.js +207 -0
- package/dist/utils/stream-json.js +6 -6
- package/docs/CHAT-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/chat-architecture.md +251 -0
- package/package.json +11 -1
|
@@ -177,7 +177,7 @@ export function ArrayEditor({ label, items, onConfirm, onCancel, isFocused = tru
|
|
|
177
177
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add" ? "Add New Item" : `Edit Item ${editingIndex + 1}` }) }), _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [">", " "] }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
|
|
178
178
|
}
|
|
179
179
|
// Render list mode
|
|
180
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", editItems.length, " items)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" [", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), editItems.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No items" }) })) : (visibleItems.map((item, visibleIndex) => {
|
|
180
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", editItems.length, " items)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" ", "[", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), editItems.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No items" }) })) : (visibleItems.map((item, visibleIndex) => {
|
|
181
181
|
// Check if this is the "Add item" option
|
|
182
182
|
if (typeof item === "object" && "isAddOption" in item) {
|
|
183
183
|
const actualIndex = editItems.length;
|
|
@@ -213,7 +213,7 @@ export function EditorPanel({ config, selectedSection, selectedField, onSelectFi
|
|
|
213
213
|
if (!currentSection || fields.length === 0) {
|
|
214
214
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: breadcrumb }) }), _jsx(Text, { dimColor: true, children: "No fields in this section" })] }));
|
|
215
215
|
}
|
|
216
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: breadcrumb }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" (", highlightedIndex + 1, "/", totalFields, ")"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? "▲ more" : "" }) })), visibleFields.map((field) => {
|
|
216
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: breadcrumb }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" ", "(", highlightedIndex + 1, "/", totalFields, ")"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? "▲ more" : "" }) })), visibleFields.map((field) => {
|
|
217
217
|
const actualIndex = fields.findIndex((f) => f.path === field.path);
|
|
218
218
|
const isHighlighted = actualIndex === highlightedIndex;
|
|
219
219
|
const value = getValueAtPath(config, field.path);
|
|
@@ -221,12 +221,16 @@ export function EditorPanel({ config, selectedSection, selectedField, onSelectFi
|
|
|
221
221
|
const fieldHasError = hasFieldError(validationErrors, field.path);
|
|
222
222
|
const fieldErrors = getFieldErrors(validationErrors, field.path);
|
|
223
223
|
// Color based on field type, but red if there's an error
|
|
224
|
-
const typeColor = fieldHasError
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
const typeColor = fieldHasError
|
|
225
|
+
? "red"
|
|
226
|
+
: field.type === "array"
|
|
227
|
+
? "yellow"
|
|
228
|
+
: field.type === "object"
|
|
229
|
+
? "magenta"
|
|
230
|
+
: field.type === "boolean"
|
|
231
|
+
? "blue"
|
|
228
232
|
: undefined;
|
|
229
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : fieldHasError ? "red" : undefined, children: isHighlighted ? "▸ " : fieldHasError ? "✗ " : " " }), _jsx(Text, { bold: isHighlighted, color: fieldHasError ? "red" : isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: field.label }), _jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { color: typeColor, dimColor: value === undefined || value === null, children: displayValue }), (field.type === "array" || field.type === "object") &&
|
|
233
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : fieldHasError ? "red" : undefined, children: isHighlighted ? "▸ " : fieldHasError ? "✗ " : " " }), _jsx(Text, { bold: isHighlighted, color: fieldHasError ? "red" : isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: field.label }), _jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { color: typeColor, dimColor: value === undefined || value === null, children: displayValue }), (field.type === "array" || field.type === "object") && _jsx(Text, { dimColor: true, children: " \u2192" })] }), fieldHasError && fieldErrors.length > 0 && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: "red", dimColor: true, children: fieldErrors[0].message }) }))] }, field.path));
|
|
230
234
|
}), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollDown ? "cyan" : "gray", dimColor: !canScrollDown, children: canScrollDown ? "▼ more" : "" }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["j/k: navigate | Enter: edit | J: edit as JSON", hasOverflow && " | PgUp/Dn: scroll", " | Esc: back"] }) })] }));
|
|
231
235
|
}
|
|
232
236
|
export default EditorPanel;
|
|
@@ -9,5 +9,5 @@ export interface HelpPanelProps {
|
|
|
9
9
|
* HelpPanel displays all keyboard shortcuts for the config editor.
|
|
10
10
|
* Toggle with the ? key.
|
|
11
11
|
*/
|
|
12
|
-
export declare function HelpPanel({ visible, onClose
|
|
12
|
+
export declare function HelpPanel({ visible, onClose }: HelpPanelProps): React.ReactElement | null;
|
|
13
13
|
export default HelpPanel;
|
|
@@ -54,7 +54,7 @@ const SHORTCUT_GROUPS = [
|
|
|
54
54
|
* HelpPanel displays all keyboard shortcuts for the config editor.
|
|
55
55
|
* Toggle with the ? key.
|
|
56
56
|
*/
|
|
57
|
-
export function HelpPanel({ visible, onClose
|
|
57
|
+
export function HelpPanel({ visible, onClose }) {
|
|
58
58
|
// Handle keyboard input to close help
|
|
59
59
|
useInput((input, key) => {
|
|
60
60
|
if (input === "?" || key.escape) {
|
|
@@ -75,13 +75,16 @@ function validateJsonStructure(value, label) {
|
|
|
75
75
|
if (serverConfig.args && !Array.isArray(serverConfig.args)) {
|
|
76
76
|
warnings.push(`Server "${name}": "args" should be an array`);
|
|
77
77
|
}
|
|
78
|
-
if (serverConfig.env &&
|
|
78
|
+
if (serverConfig.env &&
|
|
79
|
+
(typeof serverConfig.env !== "object" || Array.isArray(serverConfig.env))) {
|
|
79
80
|
warnings.push(`Server "${name}": "env" should be an object`);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
// Daemon Actions validation
|
|
84
|
-
if (label.toLowerCase().includes("action") &&
|
|
85
|
+
if (label.toLowerCase().includes("action") &&
|
|
86
|
+
typeof value === "object" &&
|
|
87
|
+
!Array.isArray(value)) {
|
|
85
88
|
const actions = value;
|
|
86
89
|
for (const [name, config] of Object.entries(actions)) {
|
|
87
90
|
if (typeof config !== "object" || config === null) {
|
|
@@ -227,7 +230,7 @@ function highlightJson(json, maxLines, maxLineWidth = 60) {
|
|
|
227
230
|
elements.push(_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [lineNum, " "] }), tokens] }, i));
|
|
228
231
|
}
|
|
229
232
|
if (hasMore) {
|
|
230
|
-
elements.push(_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["
|
|
233
|
+
elements.push(_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" ... (", lines.length - maxLines, " more lines)"] }) }, "more"));
|
|
231
234
|
}
|
|
232
235
|
return elements;
|
|
233
236
|
}
|
|
@@ -373,8 +376,8 @@ export function JsonSnippetEditor({ label, value, onConfirm, onCancel, isFocused
|
|
|
373
376
|
// Account for border (2) and padding (2) when calculating preview width
|
|
374
377
|
const previewWidth = Math.max(40, maxWidth - 4);
|
|
375
378
|
const previewLines = highlightJson(editText, previewMaxLines, previewWidth);
|
|
376
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit as JSON: ", label] }), copied && _jsx(Text, { color: "green", children: " Copied!" })] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: previewLines }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: parseError ? (_jsx(Box, { children: _jsxs(Text, { color: "red", bold: true, children: ["Error: ", parseError.line && parseError.column
|
|
379
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit as JSON: ", label] }), copied && _jsx(Text, { color: "green", children: " Copied!" })] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: previewLines }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: parseError ? (_jsx(Box, { children: _jsxs(Text, { color: "red", bold: true, children: ["Error:", " ", parseError.line && parseError.column
|
|
377
380
|
? `Line ${parseError.line}:${parseError.column} - `
|
|
378
|
-
: "", parseError.message] }) })) : warningCount > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", bold: true, children: [warningCount, " warning", warningCount > 1 ? "s" : "", ":"] }), warnings.slice(0, 3).map((w, i) => (_jsxs(Text, { color: "yellow", dimColor: true, children: [" - ", w] }, i))), warningCount > 3 &&
|
|
381
|
+
: "", parseError.message] }) })) : warningCount > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", bold: true, children: [warningCount, " warning", warningCount > 1 ? "s" : "", ":"] }), warnings.slice(0, 3).map((w, i) => (_jsxs(Text, { color: "yellow", dimColor: true, children: [" ", "- ", w] }, i))), warningCount > 3 && _jsxs(Text, { dimColor: true, children: [" ... and ", warningCount - 3, " more"] })] })) : (_jsx(Text, { color: "green", children: "Valid JSON" })) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "e/Enter: edit | s: save | c: copy | f: format" }), _jsx(Text, { dimColor: true, children: "j/k: scroll | PgUp/Dn: page | Esc: cancel" })] })] }));
|
|
379
382
|
}
|
|
380
383
|
export default JsonSnippetEditor;
|
|
@@ -26,9 +26,21 @@ export const PROVIDER_HINTS = {
|
|
|
26
26
|
],
|
|
27
27
|
// Chat provider hints
|
|
28
28
|
slack: [
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
{
|
|
30
|
+
key: "botToken",
|
|
31
|
+
description: "Slack Bot Token (xoxb-...) from OAuth & Permissions",
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "appToken",
|
|
36
|
+
description: "Slack App Token (xapp-...) from Basic Information > App-Level Tokens",
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "signingSecret",
|
|
41
|
+
description: "Slack Signing Secret from Basic Information > App Credentials",
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
32
44
|
{ key: "allowedChannelIds", description: "Only respond in these channel IDs (security)" },
|
|
33
45
|
{ key: "enabled", description: "Enable/disable Slack integration" },
|
|
34
46
|
],
|
|
@@ -38,11 +50,44 @@ export const PROVIDER_HINTS = {
|
|
|
38
50
|
{ key: "enabled", description: "Enable/disable Telegram integration" },
|
|
39
51
|
],
|
|
40
52
|
discord: [
|
|
41
|
-
{
|
|
53
|
+
{
|
|
54
|
+
key: "botToken",
|
|
55
|
+
description: "Discord Bot Token from Developer Portal > Bot > Token",
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
42
58
|
{ key: "allowedGuildIds", description: "Only respond in these server/guild IDs (security)" },
|
|
43
59
|
{ key: "allowedChannelIds", description: "Only respond in these channel IDs (security)" },
|
|
44
60
|
{ key: "enabled", description: "Enable/disable Discord integration" },
|
|
45
61
|
],
|
|
62
|
+
// LLM provider hints
|
|
63
|
+
anthropic: [
|
|
64
|
+
{ key: "type", description: "Provider type (anthropic)", required: true },
|
|
65
|
+
{
|
|
66
|
+
key: "model",
|
|
67
|
+
description: "Model name (e.g., claude-sonnet-4-20250514, claude-opus-4-20250514)",
|
|
68
|
+
required: true,
|
|
69
|
+
},
|
|
70
|
+
{ key: "apiKey", description: "API key (defaults to ANTHROPIC_API_KEY env var)" },
|
|
71
|
+
{ key: "baseUrl", description: "Custom API base URL (optional)" },
|
|
72
|
+
],
|
|
73
|
+
openai: [
|
|
74
|
+
{ key: "type", description: "Provider type (openai)", required: true },
|
|
75
|
+
{ key: "model", description: "Model name (e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo)", required: true },
|
|
76
|
+
{ key: "apiKey", description: "API key (defaults to OPENAI_API_KEY env var)" },
|
|
77
|
+
{ key: "baseUrl", description: "Custom API base URL (for OpenAI-compatible services)" },
|
|
78
|
+
],
|
|
79
|
+
ollama: [
|
|
80
|
+
{ key: "type", description: "Provider type (ollama)", required: true },
|
|
81
|
+
{ key: "model", description: "Model name (e.g., llama3, mistral, codellama)", required: true },
|
|
82
|
+
{ key: "baseUrl", description: "Ollama server URL (default: http://localhost:11434)" },
|
|
83
|
+
],
|
|
84
|
+
// Generic LLM provider hint for unknown providers
|
|
85
|
+
llmprovider: [
|
|
86
|
+
{ key: "type", description: "Provider type (anthropic, openai, or ollama)", required: true },
|
|
87
|
+
{ key: "model", description: "Model name", required: true },
|
|
88
|
+
{ key: "apiKey", description: "API key (optional, uses env var if not set)" },
|
|
89
|
+
{ key: "baseUrl", description: "Custom API base URL (optional)" },
|
|
90
|
+
],
|
|
46
91
|
};
|
|
47
92
|
/**
|
|
48
93
|
* KeyValueEditor component for editing key-value pairs with provider-specific hints.
|
|
@@ -64,7 +109,7 @@ export function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused
|
|
|
64
109
|
}, [providerName]);
|
|
65
110
|
// Get hints that are not already in entries
|
|
66
111
|
const availableHints = useMemo(() => {
|
|
67
|
-
return providerHints.filter(hint => !(hint.key in editEntries));
|
|
112
|
+
return providerHints.filter((hint) => !(hint.key in editEntries));
|
|
68
113
|
}, [providerHints, editEntries]);
|
|
69
114
|
// Get sorted keys for consistent ordering
|
|
70
115
|
const keys = Object.keys(editEntries).sort();
|
|
@@ -263,17 +308,17 @@ export function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused
|
|
|
263
308
|
// Render value input mode (for add or edit)
|
|
264
309
|
if (mode === "add-value" || mode === "edit-value") {
|
|
265
310
|
// Find hint description for the current key
|
|
266
|
-
const currentHint = providerHints.find(h => h.key === newKey);
|
|
311
|
+
const currentHint = providerHints.find((h) => h.key === newKey);
|
|
267
312
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add-value" ? "Add New Entry - Enter Value" : `Edit Value for "${newKey}"` }) }), mode === "add-value" && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(Text, { color: "yellow", children: newKey }), currentHint && currentHint.required && _jsx(Text, { color: "red", children: " (required)" })] })), currentHint && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: currentHint.description }) })), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Value: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleValueSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
|
|
268
313
|
}
|
|
269
314
|
// Check for missing required keys
|
|
270
|
-
const missingRequired = providerHints.filter(h => h.required && !(h.key in editEntries));
|
|
315
|
+
const missingRequired = providerHints.filter((h) => h.required && !(h.key in editEntries));
|
|
271
316
|
// Render list mode
|
|
272
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), providerName && _jsxs(Text, { dimColor: true, children: [" - ", providerName] })] }), missingRequired.length > 0 && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0 Missing required: " }), _jsx(Text, { color: "yellow", children: missingRequired.map(h => h.key).join(", ") })] })), keys.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (keys.map((key, index) => {
|
|
317
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), providerName && _jsxs(Text, { dimColor: true, children: [" - ", providerName] })] }), missingRequired.length > 0 && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0 Missing required: " }), _jsx(Text, { color: "yellow", children: missingRequired.map((h) => h.key).join(", ") })] })), keys.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (keys.map((key, index) => {
|
|
273
318
|
const isHighlighted = index === highlightedIndex;
|
|
274
319
|
const isExpanded = expandedKeys.has(key);
|
|
275
320
|
const value = editEntries[key];
|
|
276
|
-
const hint = providerHints.find(h => h.key === key);
|
|
321
|
+
const hint = providerHints.find((h) => h.key === key);
|
|
277
322
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: isExpanded ? "▼ " : "▶ " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : hint?.required ? "yellow" : "yellow", inverse: isHighlighted, children: key }), hint?.required && _jsx(Text, { color: "red", children: " *" }), !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { dimColor: true, children: value.length > 30 ? value.substring(0, 30) + "..." : value })] }))] }), isExpanded && (_jsx(Box, { marginLeft: 6, children: _jsx(Text, { color: "green", children: value || "(empty)" }) }))] }, `entry-${key}`));
|
|
278
323
|
})), hasHints && (_jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === keys.length ? "green" : undefined, children: highlightedIndex === keys.length ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: highlightedIndex === keys.length, color: highlightedIndex === keys.length ? "green" : "gray", inverse: highlightedIndex === keys.length, children: "+ Add from hints" })] })), _jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "green" : undefined, children: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: highlightedIndex === (hasHints ? keys.length + 1 : keys.length), color: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "green" : "gray", inverse: highlightedIndex === (hasHints ? keys.length + 1 : keys.length), children: "+ Add custom entry" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Tab/Space: expand | Enter/e: edit" }), _jsx(Text, { dimColor: true, children: "d: delete | s: save | Esc: cancel" })] })] }));
|
|
279
324
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { LLMProvidersConfig } from "../../utils/config.js";
|
|
3
|
+
export interface LLMProvidersEditorProps {
|
|
4
|
+
/** The label to display for this field */
|
|
5
|
+
label: string;
|
|
6
|
+
/** The current LLM providers config */
|
|
7
|
+
providers: LLMProvidersConfig;
|
|
8
|
+
/** Called when the user confirms the edit */
|
|
9
|
+
onConfirm: (newProviders: LLMProvidersConfig) => void;
|
|
10
|
+
/** Called when the user cancels the edit (Esc) */
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
/** Whether this editor has focus */
|
|
13
|
+
isFocused?: boolean;
|
|
14
|
+
/** Maximum height for the list (for scrolling) */
|
|
15
|
+
maxHeight?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* LLMProvidersEditor component for editing LLM provider configurations.
|
|
19
|
+
* Provides a user-friendly interface for adding, editing, and removing LLM providers.
|
|
20
|
+
*/
|
|
21
|
+
export declare function LLMProvidersEditor({ label, providers, onConfirm, onCancel, isFocused, maxHeight, }: LLMProvidersEditorProps): React.ReactElement;
|
|
22
|
+
export default LLMProvidersEditor;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
/**
|
|
6
|
+
* Provider type options for dropdown.
|
|
7
|
+
*/
|
|
8
|
+
const PROVIDER_TYPES = ["anthropic", "openai", "ollama"];
|
|
9
|
+
/**
|
|
10
|
+
* Default models for each provider type.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MODELS = {
|
|
13
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
14
|
+
openai: "gpt-4o",
|
|
15
|
+
ollama: "llama3",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Model suggestions for each provider type.
|
|
19
|
+
*/
|
|
20
|
+
const MODEL_SUGGESTIONS = {
|
|
21
|
+
anthropic: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-5-haiku-20241022"],
|
|
22
|
+
openai: ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo", "o1-preview", "o1-mini"],
|
|
23
|
+
ollama: ["llama3", "llama3.1", "mistral", "codellama", "mixtral", "phi"],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* LLMProvidersEditor component for editing LLM provider configurations.
|
|
27
|
+
* Provides a user-friendly interface for adding, editing, and removing LLM providers.
|
|
28
|
+
*/
|
|
29
|
+
export function LLMProvidersEditor({ label, providers, onConfirm, onCancel, isFocused = true, maxHeight = 15, }) {
|
|
30
|
+
const [editProviders, setEditProviders] = useState({ ...providers });
|
|
31
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
32
|
+
const [mode, setMode] = useState("list");
|
|
33
|
+
const [editText, setEditText] = useState("");
|
|
34
|
+
const [editingProvider, setEditingProvider] = useState(null);
|
|
35
|
+
const [typeIndex, setTypeIndex] = useState(0);
|
|
36
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
37
|
+
// Get sorted provider names
|
|
38
|
+
const providerNames = useMemo(() => Object.keys(editProviders).sort(), [editProviders]);
|
|
39
|
+
// Total options includes all providers plus "+ Add provider" option
|
|
40
|
+
const totalOptions = providerNames.length + 1;
|
|
41
|
+
// Calculate visible range for scrolling
|
|
42
|
+
const visibleCount = Math.min(maxHeight - 6, totalOptions); // Reserve lines for header, footer, hints
|
|
43
|
+
const visibleProviders = useMemo(() => {
|
|
44
|
+
const endIndex = Math.min(scrollOffset + visibleCount, providerNames.length);
|
|
45
|
+
return providerNames.slice(scrollOffset, endIndex);
|
|
46
|
+
}, [scrollOffset, visibleCount, providerNames]);
|
|
47
|
+
// Auto-scroll to keep highlighted item visible
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
if (highlightedIndex < scrollOffset) {
|
|
50
|
+
setScrollOffset(highlightedIndex);
|
|
51
|
+
}
|
|
52
|
+
else if (highlightedIndex >= scrollOffset + visibleCount) {
|
|
53
|
+
setScrollOffset(Math.max(0, highlightedIndex - visibleCount + 1));
|
|
54
|
+
}
|
|
55
|
+
}, [highlightedIndex, scrollOffset, visibleCount]);
|
|
56
|
+
// Navigation handlers
|
|
57
|
+
const handleNavigateUp = useCallback(() => {
|
|
58
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
|
|
59
|
+
}, [totalOptions]);
|
|
60
|
+
const handleNavigateDown = useCallback(() => {
|
|
61
|
+
setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
|
|
62
|
+
}, [totalOptions]);
|
|
63
|
+
// Delete the highlighted provider
|
|
64
|
+
const handleDelete = useCallback(() => {
|
|
65
|
+
if (highlightedIndex < providerNames.length) {
|
|
66
|
+
const nameToDelete = providerNames[highlightedIndex];
|
|
67
|
+
const newProviders = { ...editProviders };
|
|
68
|
+
delete newProviders[nameToDelete];
|
|
69
|
+
setEditProviders(newProviders);
|
|
70
|
+
// Adjust highlighted index if needed
|
|
71
|
+
const newNames = Object.keys(newProviders);
|
|
72
|
+
if (highlightedIndex >= newNames.length && newNames.length > 0) {
|
|
73
|
+
setHighlightedIndex(newNames.length - 1);
|
|
74
|
+
}
|
|
75
|
+
else if (newNames.length === 0) {
|
|
76
|
+
setHighlightedIndex(0);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, [highlightedIndex, providerNames, editProviders]);
|
|
80
|
+
// Start editing or adding a provider
|
|
81
|
+
const handleStartEdit = useCallback(() => {
|
|
82
|
+
if (highlightedIndex < providerNames.length) {
|
|
83
|
+
// Edit existing provider
|
|
84
|
+
const name = providerNames[highlightedIndex];
|
|
85
|
+
const config = editProviders[name];
|
|
86
|
+
setEditingProvider({ name, config: { ...config } });
|
|
87
|
+
setMode("edit-provider");
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Add new provider - start with name input
|
|
91
|
+
setEditText("");
|
|
92
|
+
setMode("add-name");
|
|
93
|
+
}
|
|
94
|
+
}, [highlightedIndex, providerNames, editProviders]);
|
|
95
|
+
// Handle name submission when adding new provider
|
|
96
|
+
const handleNameSubmit = useCallback(() => {
|
|
97
|
+
const trimmedName = editText.trim();
|
|
98
|
+
if (trimmedName) {
|
|
99
|
+
// Check if name already exists
|
|
100
|
+
if (editProviders[trimmedName]) {
|
|
101
|
+
// Edit existing instead
|
|
102
|
+
const config = editProviders[trimmedName];
|
|
103
|
+
setEditingProvider({ name: trimmedName, config: { ...config } });
|
|
104
|
+
setMode("edit-provider");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Create new provider with default values
|
|
108
|
+
setEditingProvider({
|
|
109
|
+
name: trimmedName,
|
|
110
|
+
config: {
|
|
111
|
+
type: "anthropic",
|
|
112
|
+
model: DEFAULT_MODELS.anthropic,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
setTypeIndex(0);
|
|
116
|
+
setMode("select-type");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
setMode("list");
|
|
121
|
+
}
|
|
122
|
+
setEditText("");
|
|
123
|
+
}, [editText, editProviders]);
|
|
124
|
+
// Handle type selection
|
|
125
|
+
const handleTypeSelect = useCallback(() => {
|
|
126
|
+
if (editingProvider) {
|
|
127
|
+
const selectedType = PROVIDER_TYPES[typeIndex];
|
|
128
|
+
setEditingProvider({
|
|
129
|
+
...editingProvider,
|
|
130
|
+
config: {
|
|
131
|
+
...editingProvider.config,
|
|
132
|
+
type: selectedType,
|
|
133
|
+
model: DEFAULT_MODELS[selectedType],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
setEditText(DEFAULT_MODELS[selectedType]);
|
|
137
|
+
setMode("edit-model");
|
|
138
|
+
}
|
|
139
|
+
}, [editingProvider, typeIndex]);
|
|
140
|
+
// Handle model submission
|
|
141
|
+
const handleModelSubmit = useCallback(() => {
|
|
142
|
+
if (editingProvider) {
|
|
143
|
+
const trimmedModel = editText.trim() || DEFAULT_MODELS[editingProvider.config.type];
|
|
144
|
+
setEditingProvider({
|
|
145
|
+
...editingProvider,
|
|
146
|
+
config: {
|
|
147
|
+
...editingProvider.config,
|
|
148
|
+
model: trimmedModel,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
// For ollama, skip API key and go to baseUrl
|
|
152
|
+
if (editingProvider.config.type === "ollama") {
|
|
153
|
+
setEditText(editingProvider.config.baseUrl || "http://localhost:11434");
|
|
154
|
+
setMode("edit-baseurl");
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
setEditText(editingProvider.config.apiKey || "");
|
|
158
|
+
setMode("edit-apikey");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, [editingProvider, editText]);
|
|
162
|
+
// Handle API key submission
|
|
163
|
+
const handleApiKeySubmit = useCallback(() => {
|
|
164
|
+
if (editingProvider) {
|
|
165
|
+
const apiKey = editText.trim() || undefined;
|
|
166
|
+
setEditingProvider({
|
|
167
|
+
...editingProvider,
|
|
168
|
+
config: {
|
|
169
|
+
...editingProvider.config,
|
|
170
|
+
apiKey,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
setEditText(editingProvider.config.baseUrl || "");
|
|
174
|
+
setMode("edit-baseurl");
|
|
175
|
+
}
|
|
176
|
+
}, [editingProvider, editText]);
|
|
177
|
+
// Handle base URL submission and save provider
|
|
178
|
+
const handleBaseUrlSubmit = useCallback(() => {
|
|
179
|
+
if (editingProvider) {
|
|
180
|
+
const baseUrl = editText.trim() || undefined;
|
|
181
|
+
const newProviders = {
|
|
182
|
+
...editProviders,
|
|
183
|
+
[editingProvider.name]: {
|
|
184
|
+
...editingProvider.config,
|
|
185
|
+
baseUrl,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
// Clean up undefined values
|
|
189
|
+
if (!newProviders[editingProvider.name].apiKey) {
|
|
190
|
+
delete newProviders[editingProvider.name].apiKey;
|
|
191
|
+
}
|
|
192
|
+
if (!newProviders[editingProvider.name].baseUrl) {
|
|
193
|
+
delete newProviders[editingProvider.name].baseUrl;
|
|
194
|
+
}
|
|
195
|
+
setEditProviders(newProviders);
|
|
196
|
+
setEditingProvider(null);
|
|
197
|
+
setMode("list");
|
|
198
|
+
setEditText("");
|
|
199
|
+
// Update highlighted index to the new/edited provider
|
|
200
|
+
const sortedNames = Object.keys(newProviders).sort();
|
|
201
|
+
const newIndex = sortedNames.indexOf(editingProvider.name);
|
|
202
|
+
if (newIndex >= 0) {
|
|
203
|
+
setHighlightedIndex(newIndex);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, [editingProvider, editText, editProviders]);
|
|
207
|
+
// Cancel editing
|
|
208
|
+
const handleCancel = useCallback(() => {
|
|
209
|
+
setMode("list");
|
|
210
|
+
setEditText("");
|
|
211
|
+
setEditingProvider(null);
|
|
212
|
+
}, []);
|
|
213
|
+
// Handle keyboard input for list mode
|
|
214
|
+
useInput((input, key) => {
|
|
215
|
+
if (!isFocused || mode !== "list")
|
|
216
|
+
return;
|
|
217
|
+
if (input === "j" || key.downArrow) {
|
|
218
|
+
handleNavigateDown();
|
|
219
|
+
}
|
|
220
|
+
else if (input === "k" || key.upArrow) {
|
|
221
|
+
handleNavigateUp();
|
|
222
|
+
}
|
|
223
|
+
else if (key.return || input === "e") {
|
|
224
|
+
handleStartEdit();
|
|
225
|
+
}
|
|
226
|
+
else if (input === "d" || key.delete) {
|
|
227
|
+
handleDelete();
|
|
228
|
+
}
|
|
229
|
+
else if (key.escape) {
|
|
230
|
+
onCancel();
|
|
231
|
+
}
|
|
232
|
+
else if (input === "s" || input === "S") {
|
|
233
|
+
onConfirm(editProviders);
|
|
234
|
+
}
|
|
235
|
+
}, { isActive: isFocused && mode === "list" });
|
|
236
|
+
// Handle keyboard input for type selection
|
|
237
|
+
useInput((input, key) => {
|
|
238
|
+
if (!isFocused || mode !== "select-type")
|
|
239
|
+
return;
|
|
240
|
+
if (input === "j" || key.downArrow) {
|
|
241
|
+
setTypeIndex((prev) => (prev < PROVIDER_TYPES.length - 1 ? prev + 1 : 0));
|
|
242
|
+
}
|
|
243
|
+
else if (input === "k" || key.upArrow) {
|
|
244
|
+
setTypeIndex((prev) => (prev > 0 ? prev - 1 : PROVIDER_TYPES.length - 1));
|
|
245
|
+
}
|
|
246
|
+
else if (key.return) {
|
|
247
|
+
handleTypeSelect();
|
|
248
|
+
}
|
|
249
|
+
else if (key.escape) {
|
|
250
|
+
handleCancel();
|
|
251
|
+
}
|
|
252
|
+
}, { isActive: isFocused && mode === "select-type" });
|
|
253
|
+
// Handle keyboard input for text editing modes
|
|
254
|
+
useInput((_input, key) => {
|
|
255
|
+
if (!isFocused ||
|
|
256
|
+
mode === "list" ||
|
|
257
|
+
mode === "select-type" ||
|
|
258
|
+
mode === "edit-provider")
|
|
259
|
+
return;
|
|
260
|
+
if (key.escape) {
|
|
261
|
+
handleCancel();
|
|
262
|
+
}
|
|
263
|
+
}, {
|
|
264
|
+
isActive: isFocused &&
|
|
265
|
+
mode !== "list" &&
|
|
266
|
+
mode !== "select-type" &&
|
|
267
|
+
mode !== "edit-provider",
|
|
268
|
+
});
|
|
269
|
+
// Handle keyboard input for edit-provider mode (viewing a provider)
|
|
270
|
+
useInput((input, key) => {
|
|
271
|
+
if (!isFocused || mode !== "edit-provider" || !editingProvider)
|
|
272
|
+
return;
|
|
273
|
+
if (key.escape) {
|
|
274
|
+
handleCancel();
|
|
275
|
+
}
|
|
276
|
+
else if (input === "t" || input === "T") {
|
|
277
|
+
// Edit type
|
|
278
|
+
setTypeIndex(PROVIDER_TYPES.indexOf(editingProvider.config.type));
|
|
279
|
+
setMode("select-type");
|
|
280
|
+
}
|
|
281
|
+
else if (input === "m" || input === "M") {
|
|
282
|
+
// Edit model
|
|
283
|
+
setEditText(editingProvider.config.model);
|
|
284
|
+
setMode("edit-model");
|
|
285
|
+
}
|
|
286
|
+
else if (input === "a" || input === "A") {
|
|
287
|
+
// Edit API key
|
|
288
|
+
setEditText(editingProvider.config.apiKey || "");
|
|
289
|
+
setMode("edit-apikey");
|
|
290
|
+
}
|
|
291
|
+
else if (input === "b" || input === "B") {
|
|
292
|
+
// Edit base URL
|
|
293
|
+
setEditText(editingProvider.config.baseUrl || "");
|
|
294
|
+
setMode("edit-baseurl");
|
|
295
|
+
}
|
|
296
|
+
else if (input === "s" || input === "S") {
|
|
297
|
+
// Save and close
|
|
298
|
+
const newProviders = {
|
|
299
|
+
...editProviders,
|
|
300
|
+
[editingProvider.name]: { ...editingProvider.config },
|
|
301
|
+
};
|
|
302
|
+
// Clean up undefined values
|
|
303
|
+
if (!newProviders[editingProvider.name].apiKey) {
|
|
304
|
+
delete newProviders[editingProvider.name].apiKey;
|
|
305
|
+
}
|
|
306
|
+
if (!newProviders[editingProvider.name].baseUrl) {
|
|
307
|
+
delete newProviders[editingProvider.name].baseUrl;
|
|
308
|
+
}
|
|
309
|
+
setEditProviders(newProviders);
|
|
310
|
+
setEditingProvider(null);
|
|
311
|
+
setMode("list");
|
|
312
|
+
}
|
|
313
|
+
}, { isActive: isFocused && mode === "edit-provider" });
|
|
314
|
+
// Render type selection mode
|
|
315
|
+
if (mode === "select-type") {
|
|
316
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Select Provider Type" }), editingProvider && _jsxs(Text, { dimColor: true, children: [" for \"", editingProvider.name, "\""] })] }), PROVIDER_TYPES.map((type, index) => {
|
|
317
|
+
const isHighlighted = index === typeIndex;
|
|
318
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: type }), _jsxs(Text, { dimColor: true, children: [" - ", type === "anthropic" ? "Claude models" : type === "openai" ? "GPT models" : "Local models"] })] }, type));
|
|
319
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "j/k: navigate | Enter: select | Esc: cancel" }) })] }));
|
|
320
|
+
}
|
|
321
|
+
// Render name input mode
|
|
322
|
+
if (mode === "add-name") {
|
|
323
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Add New LLM Provider" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Name: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleNameSubmit, focus: isFocused, placeholder: "e.g., claude, gpt4, local" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Common names: anthropic, openai, ollama, claude, gpt4, local" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
|
|
324
|
+
}
|
|
325
|
+
// Render model input mode
|
|
326
|
+
if (mode === "edit-model" && editingProvider) {
|
|
327
|
+
const suggestions = MODEL_SUGGESTIONS[editingProvider.config.type] || [];
|
|
328
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Enter Model Name" }), _jsxs(Text, { dimColor: true, children: [" for ", editingProvider.config.type] })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Model: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleModelSubmit, focus: isFocused })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Suggested models:" }), suggestions.slice(0, 4).map((model) => (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "gray", children: model }) }, model)))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
|
|
329
|
+
}
|
|
330
|
+
// Render API key input mode
|
|
331
|
+
if (mode === "edit-apikey" && editingProvider) {
|
|
332
|
+
const envVar = editingProvider.config.type === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
333
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Enter API Key" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Leave empty to use ", envVar, " environment variable"] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "API Key: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleApiKeySubmit, focus: isFocused, mask: "*" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
|
|
334
|
+
}
|
|
335
|
+
// Render base URL input mode
|
|
336
|
+
if (mode === "edit-baseurl" && editingProvider) {
|
|
337
|
+
const defaultUrl = editingProvider.config.type === "ollama" ? "http://localhost:11434" : "";
|
|
338
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Enter Base URL" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), editingProvider.config.type === "ollama" && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Default: ", defaultUrl] }) })), editingProvider.config.type === "openai" && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Use custom URL for OpenAI-compatible APIs" }) })), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Base URL: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleBaseUrlSubmit, focus: isFocused, placeholder: defaultUrl || "Leave empty for default" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save provider | Esc: cancel" }) })] }));
|
|
339
|
+
}
|
|
340
|
+
// Render edit-provider mode (viewing/editing a single provider)
|
|
341
|
+
if (mode === "edit-provider" && editingProvider) {
|
|
342
|
+
const config = editingProvider.config;
|
|
343
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: ["Edit Provider: ", editingProvider.name] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[T] Type: " }), _jsx(Text, { children: config.type })] }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[M] Model: " }), _jsx(Text, { children: config.model })] }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[A] API Key: " }), _jsx(Text, { dimColor: true, children: config.apiKey ? "********" : "(uses env var)" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[B] Base URL: " }), _jsx(Text, { dimColor: true, children: config.baseUrl || "(default)" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "T/M/A/B: edit field | S: save | Esc: cancel" }) })] }));
|
|
344
|
+
}
|
|
345
|
+
// Calculate scroll indicators
|
|
346
|
+
const canScrollUp = scrollOffset > 0;
|
|
347
|
+
const canScrollDown = scrollOffset + visibleCount < providerNames.length;
|
|
348
|
+
const hasOverflow = providerNames.length > visibleCount;
|
|
349
|
+
// Render list mode
|
|
350
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: label }), _jsxs(Text, { dimColor: true, children: [" (", providerNames.length, " providers)"] })] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), providerNames.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No LLM providers configured" }) })) : (visibleProviders.map((name) => {
|
|
351
|
+
const actualIndex = providerNames.indexOf(name);
|
|
352
|
+
const isHighlighted = actualIndex === highlightedIndex;
|
|
353
|
+
const config = editProviders[name];
|
|
354
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : "yellow", inverse: isHighlighted, children: name }), _jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { color: "magenta", children: config.type }), _jsx(Text, { dimColor: true, children: " / " }), _jsx(Text, { children: config.model })] }) }, name));
|
|
355
|
+
})), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollDown ? "cyan" : "gray", dimColor: !canScrollDown, children: canScrollDown ? " ▼ more" : "" }) })), _jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === providerNames.length ? "green" : undefined, children: highlightedIndex === providerNames.length ? "▸ " : " " }), _jsx(Text, { bold: highlightedIndex === providerNames.length, color: highlightedIndex === providerNames.length ? "green" : "gray", inverse: highlightedIndex === providerNames.length, children: "+ Add provider" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Enter/e: edit | d: delete" }), _jsx(Text, { dimColor: true, children: "s: save all | Esc: cancel" })] })] }));
|
|
356
|
+
}
|
|
357
|
+
export default LLMProvidersEditor;
|
|
@@ -209,7 +209,7 @@ export function ObjectEditor({ label, entries, onConfirm, onCancel, isFocused =
|
|
|
209
209
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add-value" ? "Add New Entry - Enter Value" : `Edit Value for "${newKey}"` }) }), mode === "add-value" && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(Text, { color: "yellow", children: newKey })] })), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Value: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleValueSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
|
|
210
210
|
}
|
|
211
211
|
// Render list mode
|
|
212
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" [", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), keys.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (visibleItems.map((key, visibleIndex) => {
|
|
212
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" ", "[", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), keys.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (visibleItems.map((key, visibleIndex) => {
|
|
213
213
|
// Check if this is the "Add entry" option
|
|
214
214
|
if (key === "__add_entry__") {
|
|
215
215
|
const actualIndex = keys.length;
|
|
@@ -185,6 +185,6 @@ export function Preview({ config, selectedSection, visible = true, maxHeight = 2
|
|
|
185
185
|
if (!config || !sectionData || Object.keys(sectionData).length === 0) {
|
|
186
186
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, width: 40, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "yellow", children: "JSON Preview" }) }), _jsx(Text, { dimColor: true, children: "No data to preview" })] }));
|
|
187
187
|
}
|
|
188
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, width: 40, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "yellow", children: "JSON Preview" }), _jsx(Text, { dimColor: true, children: currentSection?.label || selectedSection })] }), _jsxs(Box, { flexDirection: "column", children: [displayLines.map((line, index) => (_jsx(HighlightedLineComponent, { line: line }, index))), isOverflowing && (_jsxs(Text, { dimColor: true, children: ["
|
|
188
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, width: 40, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "yellow", children: "JSON Preview" }), _jsx(Text, { dimColor: true, children: currentSection?.label || selectedSection })] }), _jsxs(Box, { flexDirection: "column", children: [displayLines.map((line, index) => (_jsx(HighlightedLineComponent, { line: line }, index))), isOverflowing && (_jsxs(Text, { dimColor: true, children: [" ... (", highlightedLines.length - maxHeight + 1, " more lines)"] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Tab] to hide" }) })] }));
|
|
189
189
|
}
|
|
190
190
|
export default Preview;
|