ralph-cli-sandboxed 0.4.0 → 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 (80) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +47 -20
  3. package/dist/commands/chat.d.ts +1 -1
  4. package/dist/commands/chat.js +325 -62
  5. package/dist/commands/config.js +2 -1
  6. package/dist/commands/daemon.d.ts +2 -5
  7. package/dist/commands/daemon.js +118 -49
  8. package/dist/commands/docker.js +110 -73
  9. package/dist/commands/fix-config.js +2 -1
  10. package/dist/commands/fix-prd.js +2 -2
  11. package/dist/commands/help.js +19 -3
  12. package/dist/commands/init.js +78 -17
  13. package/dist/commands/listen.js +116 -5
  14. package/dist/commands/logo.d.ts +5 -0
  15. package/dist/commands/logo.js +41 -0
  16. package/dist/commands/notify.js +1 -1
  17. package/dist/commands/once.js +19 -9
  18. package/dist/commands/prd.js +20 -2
  19. package/dist/commands/run.js +111 -27
  20. package/dist/commands/slack.d.ts +10 -0
  21. package/dist/commands/slack.js +333 -0
  22. package/dist/config/responder-presets.json +69 -0
  23. package/dist/index.js +6 -1
  24. package/dist/providers/discord.d.ts +82 -0
  25. package/dist/providers/discord.js +697 -0
  26. package/dist/providers/slack.d.ts +79 -0
  27. package/dist/providers/slack.js +715 -0
  28. package/dist/providers/telegram.d.ts +30 -0
  29. package/dist/providers/telegram.js +190 -7
  30. package/dist/responders/claude-code-responder.d.ts +48 -0
  31. package/dist/responders/claude-code-responder.js +203 -0
  32. package/dist/responders/cli-responder.d.ts +62 -0
  33. package/dist/responders/cli-responder.js +298 -0
  34. package/dist/responders/llm-responder.d.ts +135 -0
  35. package/dist/responders/llm-responder.js +582 -0
  36. package/dist/templates/macos-scripts.js +2 -4
  37. package/dist/templates/prompts.js +4 -2
  38. package/dist/tui/ConfigEditor.js +42 -5
  39. package/dist/tui/components/ArrayEditor.js +1 -1
  40. package/dist/tui/components/EditorPanel.js +10 -6
  41. package/dist/tui/components/HelpPanel.d.ts +1 -1
  42. package/dist/tui/components/HelpPanel.js +1 -1
  43. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  44. package/dist/tui/components/KeyValueEditor.js +69 -5
  45. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  46. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  47. package/dist/tui/components/ObjectEditor.js +1 -1
  48. package/dist/tui/components/Preview.js +1 -1
  49. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  50. package/dist/tui/components/RespondersEditor.js +437 -0
  51. package/dist/tui/components/SectionNav.js +27 -3
  52. package/dist/tui/utils/presets.js +15 -2
  53. package/dist/utils/chat-client.d.ts +33 -4
  54. package/dist/utils/chat-client.js +20 -1
  55. package/dist/utils/config.d.ts +100 -1
  56. package/dist/utils/config.js +78 -1
  57. package/dist/utils/daemon-actions.d.ts +19 -0
  58. package/dist/utils/daemon-actions.js +111 -0
  59. package/dist/utils/daemon-client.d.ts +21 -0
  60. package/dist/utils/daemon-client.js +28 -1
  61. package/dist/utils/llm-client.d.ts +82 -0
  62. package/dist/utils/llm-client.js +185 -0
  63. package/dist/utils/message-queue.js +6 -6
  64. package/dist/utils/notification.d.ts +10 -2
  65. package/dist/utils/notification.js +111 -4
  66. package/dist/utils/prd-validator.js +60 -19
  67. package/dist/utils/prompt.js +22 -12
  68. package/dist/utils/responder-logger.d.ts +47 -0
  69. package/dist/utils/responder-logger.js +129 -0
  70. package/dist/utils/responder-presets.d.ts +92 -0
  71. package/dist/utils/responder-presets.js +156 -0
  72. package/dist/utils/responder.d.ts +88 -0
  73. package/dist/utils/responder.js +207 -0
  74. package/dist/utils/stream-json.js +6 -6
  75. package/docs/CHAT-CLIENTS.md +520 -0
  76. package/docs/CHAT-RESPONDERS.md +785 -0
  77. package/docs/DEVELOPMENT.md +25 -0
  78. package/docs/USEFUL_ACTIONS.md +815 -0
  79. package/docs/chat-architecture.md +251 -0
  80. package/package.json +14 -1
@@ -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;
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import type { RespondersConfig } from "../../utils/config.js";
3
+ export interface RespondersEditorProps {
4
+ /** The label to display for this field */
5
+ label: string;
6
+ /** The current responders config */
7
+ responders: RespondersConfig;
8
+ /** Called when the user confirms the edit */
9
+ onConfirm: (newResponders: RespondersConfig) => 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
+ * RespondersEditor component for editing chat responder configurations.
19
+ * Provides a user-friendly interface for adding, editing, and removing responders.
20
+ */
21
+ export declare function RespondersEditor({ label, responders, onConfirm, onCancel, isFocused, maxHeight, }: RespondersEditorProps): React.ReactElement;
22
+ export default RespondersEditor;