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.
Files changed (69) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +9 -9
  3. package/dist/commands/chat.js +13 -12
  4. package/dist/commands/config.js +2 -1
  5. package/dist/commands/daemon.js +4 -3
  6. package/dist/commands/docker.js +102 -66
  7. package/dist/commands/fix-config.js +2 -1
  8. package/dist/commands/fix-prd.js +2 -2
  9. package/dist/commands/init.js +78 -17
  10. package/dist/commands/listen.js +3 -1
  11. package/dist/commands/notify.js +1 -1
  12. package/dist/commands/once.js +17 -9
  13. package/dist/commands/prd.js +4 -1
  14. package/dist/commands/run.js +40 -25
  15. package/dist/commands/slack.js +2 -2
  16. package/dist/config/responder-presets.json +69 -0
  17. package/dist/index.js +1 -1
  18. package/dist/providers/discord.d.ts +28 -0
  19. package/dist/providers/discord.js +227 -14
  20. package/dist/providers/slack.d.ts +41 -1
  21. package/dist/providers/slack.js +389 -8
  22. package/dist/providers/telegram.d.ts +30 -0
  23. package/dist/providers/telegram.js +185 -5
  24. package/dist/responders/claude-code-responder.d.ts +48 -0
  25. package/dist/responders/claude-code-responder.js +203 -0
  26. package/dist/responders/cli-responder.d.ts +62 -0
  27. package/dist/responders/cli-responder.js +298 -0
  28. package/dist/responders/llm-responder.d.ts +135 -0
  29. package/dist/responders/llm-responder.js +582 -0
  30. package/dist/templates/macos-scripts.js +2 -4
  31. package/dist/templates/prompts.js +4 -2
  32. package/dist/tui/ConfigEditor.js +19 -5
  33. package/dist/tui/components/ArrayEditor.js +1 -1
  34. package/dist/tui/components/EditorPanel.js +10 -6
  35. package/dist/tui/components/HelpPanel.d.ts +1 -1
  36. package/dist/tui/components/HelpPanel.js +1 -1
  37. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  38. package/dist/tui/components/KeyValueEditor.js +54 -9
  39. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  40. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  41. package/dist/tui/components/ObjectEditor.js +1 -1
  42. package/dist/tui/components/Preview.js +1 -1
  43. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  44. package/dist/tui/components/RespondersEditor.js +437 -0
  45. package/dist/tui/components/SectionNav.js +27 -3
  46. package/dist/utils/chat-client.d.ts +4 -0
  47. package/dist/utils/chat-client.js +12 -5
  48. package/dist/utils/config.d.ts +84 -0
  49. package/dist/utils/config.js +78 -1
  50. package/dist/utils/daemon-client.d.ts +21 -0
  51. package/dist/utils/daemon-client.js +28 -1
  52. package/dist/utils/llm-client.d.ts +82 -0
  53. package/dist/utils/llm-client.js +185 -0
  54. package/dist/utils/message-queue.js +6 -6
  55. package/dist/utils/notification.d.ts +6 -1
  56. package/dist/utils/notification.js +103 -2
  57. package/dist/utils/prd-validator.js +60 -19
  58. package/dist/utils/prompt.js +22 -12
  59. package/dist/utils/responder-logger.d.ts +47 -0
  60. package/dist/utils/responder-logger.js +129 -0
  61. package/dist/utils/responder-presets.d.ts +92 -0
  62. package/dist/utils/responder-presets.js +156 -0
  63. package/dist/utils/responder.d.ts +88 -0
  64. package/dist/utils/responder.js +207 -0
  65. package/dist/utils/stream-json.js +6 -6
  66. package/docs/CHAT-RESPONDERS.md +785 -0
  67. package/docs/DEVELOPMENT.md +25 -0
  68. package/docs/chat-architecture.md +251 -0
  69. 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 ? "red"
225
- : field.type === "array" ? "yellow"
226
- : field.type === "object" ? "magenta"
227
- : field.type === "boolean" ? "blue"
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") && (_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));
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, }: HelpPanelProps): React.ReactElement | null;
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 && (typeof serverConfig.env !== "object" || Array.isArray(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") && typeof value === "object" && !Array.isArray(value)) {
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: [" ... (", lines.length - maxLines, " more lines)"] }) }, "more"));
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 && (_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" })] })] }));
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
- { key: "botToken", description: "Slack Bot Token (xoxb-...) from OAuth & Permissions", required: true },
30
- { key: "appToken", description: "Slack App Token (xapp-...) from Basic Information > App-Level Tokens", required: true },
31
- { key: "signingSecret", description: "Slack Signing Secret from Basic Information > App Credentials", required: true },
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
- { key: "botToken", description: "Discord Bot Token from Developer Portal > Bot > Token", required: true },
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: [" ... (", highlightedLines.length - maxHeight + 1, " more lines)"] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "[Tab] to hide" }) })] }));
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;