ralph-cli-sandboxed 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/commands/action.d.ts +7 -0
  2. package/dist/commands/action.js +276 -0
  3. package/dist/commands/chat.js +95 -7
  4. package/dist/commands/config.js +6 -18
  5. package/dist/commands/fix-config.d.ts +4 -0
  6. package/dist/commands/fix-config.js +388 -0
  7. package/dist/commands/help.js +17 -0
  8. package/dist/commands/init.js +89 -2
  9. package/dist/commands/listen.js +50 -9
  10. package/dist/commands/prd.js +2 -2
  11. package/dist/config/languages.json +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/providers/telegram.d.ts +6 -2
  14. package/dist/providers/telegram.js +68 -2
  15. package/dist/templates/macos-scripts.d.ts +42 -0
  16. package/dist/templates/macos-scripts.js +448 -0
  17. package/dist/tui/ConfigEditor.d.ts +7 -0
  18. package/dist/tui/ConfigEditor.js +313 -0
  19. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  20. package/dist/tui/components/ArrayEditor.js +193 -0
  21. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  22. package/dist/tui/components/BooleanToggle.js +43 -0
  23. package/dist/tui/components/EditorPanel.d.ts +50 -0
  24. package/dist/tui/components/EditorPanel.js +232 -0
  25. package/dist/tui/components/HelpPanel.d.ts +13 -0
  26. package/dist/tui/components/HelpPanel.js +69 -0
  27. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  28. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  29. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  30. package/dist/tui/components/KeyValueEditor.js +261 -0
  31. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  32. package/dist/tui/components/ObjectEditor.js +227 -0
  33. package/dist/tui/components/PresetSelector.d.ts +23 -0
  34. package/dist/tui/components/PresetSelector.js +58 -0
  35. package/dist/tui/components/Preview.d.ts +18 -0
  36. package/dist/tui/components/Preview.js +190 -0
  37. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  38. package/dist/tui/components/ScrollableContainer.js +77 -0
  39. package/dist/tui/components/SectionNav.d.ts +31 -0
  40. package/dist/tui/components/SectionNav.js +130 -0
  41. package/dist/tui/components/StringEditor.d.ts +21 -0
  42. package/dist/tui/components/StringEditor.js +29 -0
  43. package/dist/tui/hooks/useConfig.d.ts +16 -0
  44. package/dist/tui/hooks/useConfig.js +89 -0
  45. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  46. package/dist/tui/hooks/useTerminalSize.js +48 -0
  47. package/dist/tui/utils/presets.d.ts +52 -0
  48. package/dist/tui/utils/presets.js +191 -0
  49. package/dist/tui/utils/validation.d.ts +49 -0
  50. package/dist/tui/utils/validation.js +198 -0
  51. package/dist/utils/chat-client.d.ts +31 -1
  52. package/dist/utils/chat-client.js +27 -1
  53. package/dist/utils/config.d.ts +7 -2
  54. package/docs/MACOS-DEVELOPMENT.md +435 -0
  55. package/package.json +1 -1
@@ -0,0 +1,261 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useCallback, useMemo } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ /**
6
+ * Built-in hints for known notification providers.
7
+ */
8
+ export const PROVIDER_HINTS = {
9
+ ntfy: [
10
+ { key: "topic", description: "ntfy topic name", required: true },
11
+ { key: "server", description: "ntfy server URL (default: https://ntfy.sh)" },
12
+ { key: "priority", description: "Message priority (1-5)" },
13
+ { key: "tags", description: "Comma-separated tags/emojis" },
14
+ ],
15
+ pushover: [
16
+ { key: "user", description: "Pushover user key", required: true },
17
+ { key: "token", description: "Pushover app token", required: true },
18
+ { key: "device", description: "Target device name" },
19
+ { key: "priority", description: "Message priority (-2 to 2)" },
20
+ { key: "sound", description: "Notification sound name" },
21
+ ],
22
+ gotify: [
23
+ { key: "server", description: "Gotify server URL", required: true },
24
+ { key: "token", description: "Gotify app token", required: true },
25
+ { key: "priority", description: "Message priority (0-10)" },
26
+ ],
27
+ };
28
+ /**
29
+ * KeyValueEditor component for editing key-value pairs with provider-specific hints.
30
+ * Enhanced version of ObjectEditor with support for common key suggestions.
31
+ */
32
+ export function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused = true, providerName, }) {
33
+ const [editEntries, setEditEntries] = useState({ ...entries });
34
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
35
+ const [mode, setMode] = useState("list");
36
+ const [editText, setEditText] = useState("");
37
+ const [newKey, setNewKey] = useState("");
38
+ const [expandedKeys, setExpandedKeys] = useState(new Set());
39
+ const [hintIndex, setHintIndex] = useState(0);
40
+ // Get hints for the current provider
41
+ const providerHints = useMemo(() => {
42
+ if (!providerName)
43
+ return [];
44
+ return PROVIDER_HINTS[providerName] || [];
45
+ }, [providerName]);
46
+ // Get hints that are not already in entries
47
+ const availableHints = useMemo(() => {
48
+ return providerHints.filter(hint => !(hint.key in editEntries));
49
+ }, [providerHints, editEntries]);
50
+ // Get sorted keys for consistent ordering
51
+ const keys = Object.keys(editEntries).sort();
52
+ // Total options includes all keys plus "+ Add entry" option (and "+ Add from hints" if available)
53
+ const hasHints = availableHints.length > 0;
54
+ const totalOptions = keys.length + (hasHints ? 2 : 1);
55
+ // Navigation handlers
56
+ const handleNavigateUp = useCallback(() => {
57
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
58
+ }, [totalOptions]);
59
+ const handleNavigateDown = useCallback(() => {
60
+ setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
61
+ }, [totalOptions]);
62
+ // Toggle expansion of a key to show its value
63
+ const handleToggleExpand = useCallback(() => {
64
+ if (highlightedIndex < keys.length) {
65
+ const key = keys[highlightedIndex];
66
+ setExpandedKeys((prev) => {
67
+ const newSet = new Set(prev);
68
+ if (newSet.has(key)) {
69
+ newSet.delete(key);
70
+ }
71
+ else {
72
+ newSet.add(key);
73
+ }
74
+ return newSet;
75
+ });
76
+ }
77
+ }, [highlightedIndex, keys]);
78
+ // Delete the highlighted entry
79
+ const handleDelete = useCallback(() => {
80
+ if (highlightedIndex < keys.length) {
81
+ const keyToDelete = keys[highlightedIndex];
82
+ const newEntries = { ...editEntries };
83
+ delete newEntries[keyToDelete];
84
+ setEditEntries(newEntries);
85
+ // Remove from expanded set if present
86
+ setExpandedKeys((prev) => {
87
+ const newSet = new Set(prev);
88
+ newSet.delete(keyToDelete);
89
+ return newSet;
90
+ });
91
+ // Adjust highlighted index if needed
92
+ const newKeys = Object.keys(newEntries);
93
+ if (highlightedIndex >= newKeys.length && newKeys.length > 0) {
94
+ setHighlightedIndex(newKeys.length - 1);
95
+ }
96
+ else if (newKeys.length === 0) {
97
+ setHighlightedIndex(0);
98
+ }
99
+ }
100
+ }, [highlightedIndex, keys, editEntries]);
101
+ // Start editing a value
102
+ const handleStartEdit = useCallback(() => {
103
+ if (highlightedIndex < keys.length) {
104
+ const key = keys[highlightedIndex];
105
+ setEditText(editEntries[key] || "");
106
+ setNewKey(key);
107
+ setMode("edit-value");
108
+ }
109
+ else if (hasHints && highlightedIndex === keys.length) {
110
+ // "+ Add from hints" option
111
+ setHintIndex(0);
112
+ setMode("select-hint");
113
+ }
114
+ else {
115
+ // "+ Add entry" option - start with key input
116
+ setEditText("");
117
+ setNewKey("");
118
+ setMode("add-key");
119
+ }
120
+ }, [highlightedIndex, keys, editEntries, hasHints]);
121
+ // Confirm key input when adding
122
+ const handleKeySubmit = useCallback(() => {
123
+ const trimmedKey = editText.trim();
124
+ if (trimmedKey) {
125
+ // Check if key already exists
126
+ if (editEntries[trimmedKey] !== undefined) {
127
+ // Edit existing key instead
128
+ setNewKey(trimmedKey);
129
+ setEditText(editEntries[trimmedKey]);
130
+ setMode("edit-value");
131
+ }
132
+ else {
133
+ setNewKey(trimmedKey);
134
+ setEditText("");
135
+ setMode("add-value");
136
+ }
137
+ }
138
+ else {
139
+ // Empty key - cancel
140
+ setMode("list");
141
+ setEditText("");
142
+ }
143
+ }, [editText, editEntries]);
144
+ // Confirm value input
145
+ const handleValueSubmit = useCallback(() => {
146
+ const trimmedValue = editText.trim();
147
+ if (newKey) {
148
+ const newEntries = { ...editEntries };
149
+ newEntries[newKey] = trimmedValue;
150
+ setEditEntries(newEntries);
151
+ // Update highlighted index to the new/edited key
152
+ const sortedKeys = Object.keys(newEntries).sort();
153
+ const newIndex = sortedKeys.indexOf(newKey);
154
+ if (newIndex >= 0) {
155
+ setHighlightedIndex(newIndex);
156
+ }
157
+ }
158
+ setMode("list");
159
+ setEditText("");
160
+ setNewKey("");
161
+ }, [editText, newKey, editEntries]);
162
+ // Cancel text input
163
+ const handleTextCancel = useCallback(() => {
164
+ setMode("list");
165
+ setEditText("");
166
+ setNewKey("");
167
+ }, []);
168
+ // Handle hint selection
169
+ const handleSelectHint = useCallback(() => {
170
+ if (availableHints.length > 0 && hintIndex < availableHints.length) {
171
+ const hint = availableHints[hintIndex];
172
+ setNewKey(hint.key);
173
+ setEditText("");
174
+ setMode("add-value");
175
+ }
176
+ }, [availableHints, hintIndex]);
177
+ // Handle keyboard input for list mode
178
+ useInput((input, key) => {
179
+ if (!isFocused || mode !== "list")
180
+ return;
181
+ // j/k or arrow keys for navigation
182
+ if (input === "j" || key.downArrow) {
183
+ handleNavigateDown();
184
+ }
185
+ else if (input === "k" || key.upArrow) {
186
+ handleNavigateUp();
187
+ }
188
+ else if (key.return || input === "e") {
189
+ // Enter or 'e' to edit/add
190
+ handleStartEdit();
191
+ }
192
+ else if (input === "d" || key.delete) {
193
+ // 'd' or Delete to remove
194
+ handleDelete();
195
+ }
196
+ else if (key.tab || input === " ") {
197
+ // Tab or Space to expand/collapse value
198
+ handleToggleExpand();
199
+ }
200
+ else if (key.escape) {
201
+ onCancel();
202
+ }
203
+ else if (input === "s" || input === "S") {
204
+ // 's' to save and confirm
205
+ onConfirm(editEntries);
206
+ }
207
+ }, { isActive: isFocused && mode === "list" });
208
+ // Handle keyboard input for hint selection mode
209
+ useInput((input, key) => {
210
+ if (!isFocused || mode !== "select-hint")
211
+ return;
212
+ if (input === "j" || key.downArrow) {
213
+ setHintIndex((prev) => (prev < availableHints.length - 1 ? prev + 1 : 0));
214
+ }
215
+ else if (input === "k" || key.upArrow) {
216
+ setHintIndex((prev) => (prev > 0 ? prev - 1 : availableHints.length - 1));
217
+ }
218
+ else if (key.return) {
219
+ handleSelectHint();
220
+ }
221
+ else if (key.escape) {
222
+ setMode("list");
223
+ }
224
+ }, { isActive: isFocused && mode === "select-hint" });
225
+ // Handle keyboard input for text editing modes
226
+ useInput((_input, key) => {
227
+ if (!isFocused || mode === "list" || mode === "select-hint")
228
+ return;
229
+ if (key.escape) {
230
+ handleTextCancel();
231
+ }
232
+ }, { isActive: isFocused && mode !== "list" && mode !== "select-hint" });
233
+ // Render hint selection mode
234
+ if (mode === "select-hint") {
235
+ 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 a Common Key" }), providerName && _jsxs(Text, { dimColor: true, children: [" (", providerName, ")"] })] }), availableHints.map((hint, index) => {
236
+ const isHighlighted = index === hintIndex;
237
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: hint.required ? "yellow" : isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: hint.key }), hint.required && _jsx(Text, { color: "red", children: " *" }), _jsxs(Text, { dimColor: true, children: [" - ", hint.description] })] }, hint.key));
238
+ }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "j/k: navigate | Enter: select | Esc: cancel" }) })] }));
239
+ }
240
+ // Render key input mode
241
+ if (mode === "add-key") {
242
+ 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 Entry - Enter Key" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleKeySubmit, focus: isFocused })] }), availableHints.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Common keys for ", providerName, ":"] }), availableHints.slice(0, 4).map((hint) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: hint.required ? "yellow" : "gray", children: [hint.key, hint.required && " *"] }) }, hint.key)))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
243
+ }
244
+ // Render value input mode (for add or edit)
245
+ if (mode === "add-value" || mode === "edit-value") {
246
+ // Find hint description for the current key
247
+ const currentHint = providerHints.find(h => h.key === newKey);
248
+ 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" }) })] }));
249
+ }
250
+ // Check for missing required keys
251
+ const missingRequired = providerHints.filter(h => h.required && !(h.key in editEntries));
252
+ // Render list mode
253
+ 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) => {
254
+ const isHighlighted = index === highlightedIndex;
255
+ const isExpanded = expandedKeys.has(key);
256
+ const value = editEntries[key];
257
+ const hint = providerHints.find(h => h.key === key);
258
+ 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}`));
259
+ })), 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" })] })] }));
260
+ }
261
+ export default KeyValueEditor;
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ export interface ObjectEditorProps {
3
+ /** The label to display for this object field */
4
+ label: string;
5
+ /** The current object entries as key-value pairs */
6
+ entries: Record<string, string>;
7
+ /** Called when the user confirms the edit */
8
+ onConfirm: (newEntries: Record<string, string>) => void;
9
+ /** Called when the user cancels the edit (Esc) */
10
+ onCancel: () => void;
11
+ /** Whether this editor has focus */
12
+ isFocused?: boolean;
13
+ /** Maximum height for the entries list (for scrolling) */
14
+ maxHeight?: number;
15
+ }
16
+ /**
17
+ * ObjectEditor component for editing key-value pairs.
18
+ * Used for environment variables, mcpServers, actions, and similar objects.
19
+ * Shows keys with expandable values, supports adding and deleting entries.
20
+ * Supports scrolling for long lists with Page Up/Down.
21
+ */
22
+ export declare function ObjectEditor({ label, entries, onConfirm, onCancel, isFocused, maxHeight, }: ObjectEditorProps): React.ReactElement;
23
+ export default ObjectEditor;
@@ -0,0 +1,227 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect, useMemo } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ /**
6
+ * ObjectEditor component for editing key-value pairs.
7
+ * Used for environment variables, mcpServers, actions, and similar objects.
8
+ * Shows keys with expandable values, supports adding and deleting entries.
9
+ * Supports scrolling for long lists with Page Up/Down.
10
+ */
11
+ export function ObjectEditor({ label, entries, onConfirm, onCancel, isFocused = true, maxHeight = 10, }) {
12
+ const [editEntries, setEditEntries] = useState({ ...entries });
13
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
14
+ const [scrollOffset, setScrollOffset] = useState(0);
15
+ const [mode, setMode] = useState("list");
16
+ const [editText, setEditText] = useState("");
17
+ const [newKey, setNewKey] = useState("");
18
+ const [expandedKeys, setExpandedKeys] = useState(new Set());
19
+ // Get sorted keys for consistent ordering
20
+ const keys = Object.keys(editEntries).sort();
21
+ // Total options includes all keys plus "+ Add entry" option
22
+ const totalOptions = keys.length + 1;
23
+ // Auto-scroll to keep highlighted item visible
24
+ useEffect(() => {
25
+ if (highlightedIndex < scrollOffset) {
26
+ setScrollOffset(highlightedIndex);
27
+ }
28
+ else if (highlightedIndex >= scrollOffset + maxHeight) {
29
+ setScrollOffset(highlightedIndex - maxHeight + 1);
30
+ }
31
+ }, [highlightedIndex, scrollOffset, maxHeight]);
32
+ // Navigation handlers
33
+ const handleNavigateUp = useCallback(() => {
34
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
35
+ }, [totalOptions]);
36
+ const handleNavigateDown = useCallback(() => {
37
+ setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
38
+ }, [totalOptions]);
39
+ const handlePageUp = useCallback(() => {
40
+ const newIndex = Math.max(0, highlightedIndex - maxHeight);
41
+ setHighlightedIndex(newIndex);
42
+ }, [highlightedIndex, maxHeight]);
43
+ const handlePageDown = useCallback(() => {
44
+ const newIndex = Math.min(totalOptions - 1, highlightedIndex + maxHeight);
45
+ setHighlightedIndex(newIndex);
46
+ }, [highlightedIndex, maxHeight, totalOptions]);
47
+ // Toggle expansion of a key to show its value
48
+ const handleToggleExpand = useCallback(() => {
49
+ if (highlightedIndex < keys.length) {
50
+ const key = keys[highlightedIndex];
51
+ setExpandedKeys((prev) => {
52
+ const newSet = new Set(prev);
53
+ if (newSet.has(key)) {
54
+ newSet.delete(key);
55
+ }
56
+ else {
57
+ newSet.add(key);
58
+ }
59
+ return newSet;
60
+ });
61
+ }
62
+ }, [highlightedIndex, keys]);
63
+ // Delete the highlighted entry
64
+ const handleDelete = useCallback(() => {
65
+ if (highlightedIndex < keys.length) {
66
+ const keyToDelete = keys[highlightedIndex];
67
+ const newEntries = { ...editEntries };
68
+ delete newEntries[keyToDelete];
69
+ setEditEntries(newEntries);
70
+ // Remove from expanded set if present
71
+ setExpandedKeys((prev) => {
72
+ const newSet = new Set(prev);
73
+ newSet.delete(keyToDelete);
74
+ return newSet;
75
+ });
76
+ // Adjust highlighted index if needed
77
+ const newKeys = Object.keys(newEntries);
78
+ if (highlightedIndex >= newKeys.length && newKeys.length > 0) {
79
+ setHighlightedIndex(newKeys.length - 1);
80
+ }
81
+ else if (newKeys.length === 0) {
82
+ setHighlightedIndex(0);
83
+ }
84
+ }
85
+ }, [highlightedIndex, keys, editEntries]);
86
+ // Start editing a value
87
+ const handleStartEdit = useCallback(() => {
88
+ if (highlightedIndex < keys.length) {
89
+ const key = keys[highlightedIndex];
90
+ setEditText(editEntries[key] || "");
91
+ setNewKey(key);
92
+ setMode("edit-value");
93
+ }
94
+ else {
95
+ // Add new entry - start with key input
96
+ setEditText("");
97
+ setNewKey("");
98
+ setMode("add-key");
99
+ }
100
+ }, [highlightedIndex, keys, editEntries]);
101
+ // Confirm key input when adding
102
+ const handleKeySubmit = useCallback(() => {
103
+ const trimmedKey = editText.trim();
104
+ if (trimmedKey) {
105
+ // Check if key already exists
106
+ if (editEntries[trimmedKey] !== undefined) {
107
+ // Edit existing key instead
108
+ setNewKey(trimmedKey);
109
+ setEditText(editEntries[trimmedKey]);
110
+ setMode("edit-value");
111
+ }
112
+ else {
113
+ setNewKey(trimmedKey);
114
+ setEditText("");
115
+ setMode("add-value");
116
+ }
117
+ }
118
+ else {
119
+ // Empty key - cancel
120
+ setMode("list");
121
+ setEditText("");
122
+ }
123
+ }, [editText, editEntries]);
124
+ // Confirm value input
125
+ const handleValueSubmit = useCallback(() => {
126
+ const trimmedValue = editText.trim();
127
+ if (newKey) {
128
+ const newEntries = { ...editEntries };
129
+ newEntries[newKey] = trimmedValue;
130
+ setEditEntries(newEntries);
131
+ // Update highlighted index to the new/edited key
132
+ const sortedKeys = Object.keys(newEntries).sort();
133
+ const newIndex = sortedKeys.indexOf(newKey);
134
+ if (newIndex >= 0) {
135
+ setHighlightedIndex(newIndex);
136
+ }
137
+ }
138
+ setMode("list");
139
+ setEditText("");
140
+ setNewKey("");
141
+ }, [editText, newKey, editEntries]);
142
+ // Cancel text input
143
+ const handleTextCancel = useCallback(() => {
144
+ setMode("list");
145
+ setEditText("");
146
+ setNewKey("");
147
+ }, []);
148
+ // Handle keyboard input for list mode
149
+ useInput((input, key) => {
150
+ if (!isFocused || mode !== "list")
151
+ return;
152
+ // j/k or arrow keys for navigation
153
+ if (input === "j" || key.downArrow) {
154
+ handleNavigateDown();
155
+ }
156
+ else if (input === "k" || key.upArrow) {
157
+ handleNavigateUp();
158
+ }
159
+ else if (key.pageUp) {
160
+ handlePageUp();
161
+ }
162
+ else if (key.pageDown) {
163
+ handlePageDown();
164
+ }
165
+ else if (key.return || input === "e") {
166
+ // Enter or 'e' to edit/add
167
+ handleStartEdit();
168
+ }
169
+ else if (input === "d" || key.delete) {
170
+ // 'd' or Delete to remove
171
+ handleDelete();
172
+ }
173
+ else if (key.tab || input === " ") {
174
+ // Tab or Space to expand/collapse value
175
+ handleToggleExpand();
176
+ }
177
+ else if (key.escape) {
178
+ onCancel();
179
+ }
180
+ else if (input === "s" || input === "S") {
181
+ // 's' to save and confirm
182
+ onConfirm(editEntries);
183
+ }
184
+ }, { isActive: isFocused && mode === "list" });
185
+ // Handle keyboard input for text editing modes
186
+ useInput((_input, key) => {
187
+ if (!isFocused || mode === "list")
188
+ return;
189
+ if (key.escape) {
190
+ handleTextCancel();
191
+ }
192
+ }, { isActive: isFocused && mode !== "list" });
193
+ // Calculate visible items based on scroll offset
194
+ const visibleItems = useMemo(() => {
195
+ const allKeys = [...keys, "__add_entry__"];
196
+ const endIndex = Math.min(scrollOffset + maxHeight, allKeys.length);
197
+ return allKeys.slice(scrollOffset, endIndex);
198
+ }, [keys, scrollOffset, maxHeight]);
199
+ // Check if we have overflow
200
+ const canScrollUp = scrollOffset > 0;
201
+ const canScrollDown = scrollOffset + maxHeight < totalOptions;
202
+ const hasOverflow = totalOptions > maxHeight;
203
+ // Render key input mode
204
+ if (mode === "add-key") {
205
+ 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 Entry - Enter Key" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleKeySubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
206
+ }
207
+ // Render value input mode (for add or edit)
208
+ if (mode === "add-value" || mode === "edit-value") {
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
+ }
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) => {
213
+ // Check if this is the "Add entry" option
214
+ if (key === "__add_entry__") {
215
+ const actualIndex = keys.length;
216
+ const isHighlighted = actualIndex === highlightedIndex;
217
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "green" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "green" : "gray", inverse: isHighlighted, children: "+ Add entry" })] }, "add-entry"));
218
+ }
219
+ // Regular entry
220
+ const actualIndex = scrollOffset + visibleIndex;
221
+ const isHighlighted = actualIndex === highlightedIndex;
222
+ const isExpanded = expandedKeys.has(key);
223
+ const value = editEntries[key];
224
+ 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" : "yellow", inverse: isHighlighted, children: key }), !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}`));
225
+ })), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollDown ? "cyan" : "gray", dimColor: !canScrollDown, children: canScrollDown ? " ▼ more" : "" }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Tab/Space: expand | Enter/e: edit" }), _jsxs(Text, { dimColor: true, children: ["d: delete | s: save | Esc: cancel", hasOverflow && " | PgUp/Dn: scroll"] })] })] }));
226
+ }
227
+ export default ObjectEditor;
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import type { ConfigPreset } from "../utils/presets.js";
3
+ import type { RalphConfig } from "../../utils/config.js";
4
+ export interface PresetSelectorProps {
5
+ /** The section ID to show presets for */
6
+ sectionId: string;
7
+ /** Current configuration (to detect active preset) */
8
+ config: RalphConfig;
9
+ /** Called when a preset is selected */
10
+ onSelectPreset: (preset: ConfigPreset) => void;
11
+ /** Called when user skips preset selection */
12
+ onSkip: () => void;
13
+ /** Called when user cancels */
14
+ onCancel: () => void;
15
+ /** Whether this component has focus */
16
+ isFocused?: boolean;
17
+ }
18
+ /**
19
+ * PresetSelector component displays available presets for a config section.
20
+ * Allows the user to quickly apply a preset template or skip to manual editing.
21
+ */
22
+ export declare function PresetSelector({ sectionId, config, onSelectPreset, onSkip, onCancel, isFocused, }: PresetSelectorProps): React.ReactElement;
23
+ export default PresetSelector;
@@ -0,0 +1,58 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useCallback, useMemo } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { getPresetsForSection, detectActivePreset } from "../utils/presets.js";
5
+ /**
6
+ * PresetSelector component displays available presets for a config section.
7
+ * Allows the user to quickly apply a preset template or skip to manual editing.
8
+ */
9
+ export function PresetSelector({ sectionId, config, onSelectPreset, onSkip, onCancel, isFocused = true, }) {
10
+ const presets = useMemo(() => getPresetsForSection(sectionId), [sectionId]);
11
+ const activePresetId = useMemo(() => detectActivePreset(config, sectionId), [config, sectionId]);
12
+ // Include "Skip" option at the end
13
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
14
+ const totalOptions = presets.length + 1; // +1 for "Skip" option
15
+ // Navigation handlers
16
+ const handleNavigateUp = useCallback(() => {
17
+ setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
18
+ }, [totalOptions]);
19
+ const handleNavigateDown = useCallback(() => {
20
+ setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
21
+ }, [totalOptions]);
22
+ const handleSelect = useCallback(() => {
23
+ if (highlightedIndex < presets.length) {
24
+ const preset = presets[highlightedIndex];
25
+ onSelectPreset(preset);
26
+ }
27
+ else {
28
+ // Skip option selected
29
+ onSkip();
30
+ }
31
+ }, [highlightedIndex, presets, onSelectPreset, onSkip]);
32
+ // Handle keyboard input
33
+ useInput((input, key) => {
34
+ if (!isFocused)
35
+ return;
36
+ // j/k or arrow keys for navigation
37
+ if (input === "j" || key.downArrow) {
38
+ handleNavigateDown();
39
+ }
40
+ else if (input === "k" || key.upArrow) {
41
+ handleNavigateUp();
42
+ }
43
+ else if (key.return) {
44
+ handleSelect();
45
+ }
46
+ else if (key.escape) {
47
+ onCancel();
48
+ }
49
+ }, { isActive: isFocused });
50
+ // Get section title for display
51
+ const sectionTitle = sectionId.charAt(0).toUpperCase() + sectionId.slice(1);
52
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: ["Use Preset: ", sectionTitle] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select a preset to auto-fill common settings, or skip to configure manually." }) }), presets.map((preset, index) => {
53
+ const isHighlighted = index === highlightedIndex;
54
+ const isActive = preset.id === activePresetId;
55
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : isActive ? "green" : undefined, inverse: isHighlighted, children: preset.name }), isActive && _jsx(Text, { color: "green", children: " (active)" })] }), _jsx(Box, { marginLeft: 4, children: _jsx(Text, { dimColor: true, children: preset.description }) })] }, preset.id));
56
+ }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: highlightedIndex === presets.length ? "cyan" : undefined, children: highlightedIndex === presets.length ? "▸ " : " " }), _jsx(Text, { bold: highlightedIndex === presets.length, color: highlightedIndex === presets.length ? "yellow" : "gray", inverse: highlightedIndex === presets.length, children: "Skip - Configure manually" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "j/k: navigate | Enter: select | Esc: back" }) })] }));
57
+ }
58
+ export default PresetSelector;
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import type { RalphConfig } from "../../utils/config.js";
3
+ export interface PreviewProps {
4
+ /** The current configuration */
5
+ config: RalphConfig | null;
6
+ /** Currently selected section ID */
7
+ selectedSection: string;
8
+ /** Whether the preview panel is visible */
9
+ visible?: boolean;
10
+ /** Maximum height for the preview (lines) */
11
+ maxHeight?: number;
12
+ }
13
+ /**
14
+ * Preview component displays the current section's config as syntax-highlighted JSON.
15
+ * Updates live as edits are made.
16
+ */
17
+ export declare function Preview({ config, selectedSection, visible, maxHeight, }: PreviewProps): React.ReactElement | null;
18
+ export default Preview;