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.
- package/dist/commands/action.d.ts +7 -0
- package/dist/commands/action.js +276 -0
- package/dist/commands/chat.js +95 -7
- package/dist/commands/config.js +6 -18
- package/dist/commands/fix-config.d.ts +4 -0
- package/dist/commands/fix-config.js +388 -0
- package/dist/commands/help.js +17 -0
- package/dist/commands/init.js +89 -2
- package/dist/commands/listen.js +50 -9
- package/dist/commands/prd.js +2 -2
- package/dist/config/languages.json +4 -0
- package/dist/index.js +4 -0
- package/dist/providers/telegram.d.ts +6 -2
- package/dist/providers/telegram.js +68 -2
- package/dist/templates/macos-scripts.d.ts +42 -0
- package/dist/templates/macos-scripts.js +448 -0
- package/dist/tui/ConfigEditor.d.ts +7 -0
- package/dist/tui/ConfigEditor.js +313 -0
- package/dist/tui/components/ArrayEditor.d.ts +22 -0
- package/dist/tui/components/ArrayEditor.js +193 -0
- package/dist/tui/components/BooleanToggle.d.ts +19 -0
- package/dist/tui/components/BooleanToggle.js +43 -0
- package/dist/tui/components/EditorPanel.d.ts +50 -0
- package/dist/tui/components/EditorPanel.js +232 -0
- package/dist/tui/components/HelpPanel.d.ts +13 -0
- package/dist/tui/components/HelpPanel.js +69 -0
- package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
- package/dist/tui/components/JsonSnippetEditor.js +380 -0
- package/dist/tui/components/KeyValueEditor.d.ts +34 -0
- package/dist/tui/components/KeyValueEditor.js +261 -0
- package/dist/tui/components/ObjectEditor.d.ts +23 -0
- package/dist/tui/components/ObjectEditor.js +227 -0
- package/dist/tui/components/PresetSelector.d.ts +23 -0
- package/dist/tui/components/PresetSelector.js +58 -0
- package/dist/tui/components/Preview.d.ts +18 -0
- package/dist/tui/components/Preview.js +190 -0
- package/dist/tui/components/ScrollableContainer.d.ts +38 -0
- package/dist/tui/components/ScrollableContainer.js +77 -0
- package/dist/tui/components/SectionNav.d.ts +31 -0
- package/dist/tui/components/SectionNav.js +130 -0
- package/dist/tui/components/StringEditor.d.ts +21 -0
- package/dist/tui/components/StringEditor.js +29 -0
- package/dist/tui/hooks/useConfig.d.ts +16 -0
- package/dist/tui/hooks/useConfig.js +89 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
- package/dist/tui/hooks/useTerminalSize.js +48 -0
- package/dist/tui/utils/presets.d.ts +52 -0
- package/dist/tui/utils/presets.js +191 -0
- package/dist/tui/utils/validation.d.ts +49 -0
- package/dist/tui/utils/validation.js +198 -0
- package/dist/utils/chat-client.d.ts +31 -1
- package/dist/utils/chat-client.js +27 -1
- package/dist/utils/config.d.ts +7 -2
- package/docs/MACOS-DEVELOPMENT.md +435 -0
- package/package.json +1 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } 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
|
+
* Parse JSON with detailed error information including line and column.
|
|
7
|
+
*/
|
|
8
|
+
function parseJsonWithLineInfo(jsonStr) {
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(jsonStr);
|
|
11
|
+
return { value };
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
const message = err instanceof Error ? err.message : "Invalid JSON";
|
|
15
|
+
// Try to extract line/column from error message
|
|
16
|
+
// Format: "... at position N" or "... at line X column Y"
|
|
17
|
+
const posMatch = message.match(/at position (\d+)/);
|
|
18
|
+
const lineColMatch = message.match(/at line (\d+) column (\d+)/);
|
|
19
|
+
if (lineColMatch) {
|
|
20
|
+
return {
|
|
21
|
+
error: {
|
|
22
|
+
message: message.replace(/at line \d+ column \d+/, "").trim(),
|
|
23
|
+
line: parseInt(lineColMatch[1], 10),
|
|
24
|
+
column: parseInt(lineColMatch[2], 10),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (posMatch) {
|
|
29
|
+
const position = parseInt(posMatch[1], 10);
|
|
30
|
+
// Calculate line and column from position
|
|
31
|
+
const lines = jsonStr.substring(0, position).split("\n");
|
|
32
|
+
const line = lines.length;
|
|
33
|
+
const column = lines[lines.length - 1].length + 1;
|
|
34
|
+
return {
|
|
35
|
+
error: {
|
|
36
|
+
message: message.replace(/at position \d+/, "").trim(),
|
|
37
|
+
line,
|
|
38
|
+
column,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { error: { message } };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Format JSON with indentation for display.
|
|
47
|
+
*/
|
|
48
|
+
function formatJson(value) {
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
return "null";
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value, null, 2);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate JSON structure against known config schemas.
|
|
56
|
+
* Returns warnings for common issues.
|
|
57
|
+
*/
|
|
58
|
+
function validateJsonStructure(value, label) {
|
|
59
|
+
const warnings = [];
|
|
60
|
+
if (value === null || value === undefined) {
|
|
61
|
+
return warnings;
|
|
62
|
+
}
|
|
63
|
+
// MCP Servers validation
|
|
64
|
+
if (label.toLowerCase().includes("mcp") && typeof value === "object" && !Array.isArray(value)) {
|
|
65
|
+
const servers = value;
|
|
66
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
67
|
+
if (typeof config !== "object" || config === null) {
|
|
68
|
+
warnings.push(`Server "${name}": expected object with command field`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const serverConfig = config;
|
|
72
|
+
if (!serverConfig.command || typeof serverConfig.command !== "string") {
|
|
73
|
+
warnings.push(`Server "${name}": missing or invalid "command" field`);
|
|
74
|
+
}
|
|
75
|
+
if (serverConfig.args && !Array.isArray(serverConfig.args)) {
|
|
76
|
+
warnings.push(`Server "${name}": "args" should be an array`);
|
|
77
|
+
}
|
|
78
|
+
if (serverConfig.env && (typeof serverConfig.env !== "object" || Array.isArray(serverConfig.env))) {
|
|
79
|
+
warnings.push(`Server "${name}": "env" should be an object`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Daemon Actions validation
|
|
84
|
+
if (label.toLowerCase().includes("action") && typeof value === "object" && !Array.isArray(value)) {
|
|
85
|
+
const actions = value;
|
|
86
|
+
for (const [name, config] of Object.entries(actions)) {
|
|
87
|
+
if (typeof config !== "object" || config === null) {
|
|
88
|
+
warnings.push(`Action "${name}": expected object with command field`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const actionConfig = config;
|
|
92
|
+
if (!actionConfig.command || typeof actionConfig.command !== "string") {
|
|
93
|
+
warnings.push(`Action "${name}": missing or invalid "command" field`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Skills validation
|
|
98
|
+
if (label.toLowerCase().includes("skill") && Array.isArray(value)) {
|
|
99
|
+
for (let i = 0; i < value.length; i++) {
|
|
100
|
+
const skill = value[i];
|
|
101
|
+
if (typeof skill !== "object" || skill === null) {
|
|
102
|
+
warnings.push(`Skill [${i}]: expected object`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const skillConfig = skill;
|
|
106
|
+
if (!skillConfig.name || typeof skillConfig.name !== "string") {
|
|
107
|
+
warnings.push(`Skill [${i}]: missing or invalid "name" field`);
|
|
108
|
+
}
|
|
109
|
+
if (!skillConfig.description || typeof skillConfig.description !== "string") {
|
|
110
|
+
warnings.push(`Skill [${i}]: missing or invalid "description" field`);
|
|
111
|
+
}
|
|
112
|
+
if (!skillConfig.instructions || typeof skillConfig.instructions !== "string") {
|
|
113
|
+
warnings.push(`Skill [${i}]: missing or invalid "instructions" field`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Daemon Events validation
|
|
118
|
+
if (label.toLowerCase().includes("event") && typeof value === "object" && !Array.isArray(value)) {
|
|
119
|
+
const validEventTypes = ["task_complete", "ralph_complete", "iteration_complete", "error"];
|
|
120
|
+
const events = value;
|
|
121
|
+
for (const [eventType, handlers] of Object.entries(events)) {
|
|
122
|
+
if (!validEventTypes.includes(eventType)) {
|
|
123
|
+
warnings.push(`Unknown event type: "${eventType}". Valid: ${validEventTypes.join(", ")}`);
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(handlers)) {
|
|
126
|
+
warnings.push(`Event "${eventType}": handlers should be an array`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < handlers.length; i++) {
|
|
130
|
+
const handler = handlers[i];
|
|
131
|
+
if (typeof handler !== "object" || handler === null) {
|
|
132
|
+
warnings.push(`Event "${eventType}"[${i}]: expected object`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const eventConfig = handler;
|
|
136
|
+
if (!eventConfig.action || typeof eventConfig.action !== "string") {
|
|
137
|
+
warnings.push(`Event "${eventType}"[${i}]: missing or invalid "action" field`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return warnings;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Truncate a JSON string value if it's too long.
|
|
146
|
+
* Keeps the quotes and adds "..." before the closing quote.
|
|
147
|
+
*/
|
|
148
|
+
function truncateJsonString(str, maxLen) {
|
|
149
|
+
// str includes the surrounding quotes, so we need to account for that
|
|
150
|
+
if (str.length <= maxLen) {
|
|
151
|
+
return str;
|
|
152
|
+
}
|
|
153
|
+
// Truncate the content (excluding quotes) and add ellipsis
|
|
154
|
+
// Leave room for the ellipsis (3 chars) and closing quote
|
|
155
|
+
const truncated = str.slice(0, maxLen - 4) + '..."';
|
|
156
|
+
return truncated;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Simple syntax highlighting for JSON preview.
|
|
160
|
+
*/
|
|
161
|
+
function highlightJson(json, maxLines, maxLineWidth = 60) {
|
|
162
|
+
const lines = json.split("\n");
|
|
163
|
+
const displayLines = lines.slice(0, maxLines);
|
|
164
|
+
const hasMore = lines.length > maxLines;
|
|
165
|
+
const elements = [];
|
|
166
|
+
// Calculate max string length based on available width (account for line number, indentation)
|
|
167
|
+
const maxStringLen = Math.max(20, Math.min(50, maxLineWidth - 15));
|
|
168
|
+
for (let i = 0; i < displayLines.length; i++) {
|
|
169
|
+
const line = displayLines[i];
|
|
170
|
+
const lineNum = String(i + 1).padStart(3, " ");
|
|
171
|
+
// Simple tokenization for highlighting
|
|
172
|
+
const tokens = [];
|
|
173
|
+
let remaining = line;
|
|
174
|
+
let tokenKey = 0;
|
|
175
|
+
let lineLength = 0;
|
|
176
|
+
const effectiveMaxWidth = maxLineWidth - 6; // Account for line number prefix "123 "
|
|
177
|
+
while (remaining.length > 0 && lineLength < effectiveMaxWidth) {
|
|
178
|
+
// Match string (key or value)
|
|
179
|
+
const stringMatch = remaining.match(/^("(?:[^"\\]|\\.)*")/);
|
|
180
|
+
if (stringMatch) {
|
|
181
|
+
let str = stringMatch[1];
|
|
182
|
+
// Check if this is a key (followed by :)
|
|
183
|
+
const afterStr = remaining.slice(str.length).trim();
|
|
184
|
+
const isKey = afterStr.startsWith(":");
|
|
185
|
+
// Truncate long string values (not keys)
|
|
186
|
+
if (!isKey && str.length > maxStringLen) {
|
|
187
|
+
str = truncateJsonString(str, maxStringLen);
|
|
188
|
+
}
|
|
189
|
+
tokens.push(_jsx(Text, { color: isKey ? "cyan" : "green", children: str }, tokenKey++));
|
|
190
|
+
lineLength += str.length;
|
|
191
|
+
remaining = remaining.slice(stringMatch[1].length);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// Match number
|
|
195
|
+
const numMatch = remaining.match(/^(-?\d+\.?\d*(?:[eE][+-]?\d+)?)/);
|
|
196
|
+
if (numMatch) {
|
|
197
|
+
tokens.push(_jsx(Text, { color: "yellow", children: numMatch[1] }, tokenKey++));
|
|
198
|
+
lineLength += numMatch[1].length;
|
|
199
|
+
remaining = remaining.slice(numMatch[1].length);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Match boolean or null
|
|
203
|
+
const boolNullMatch = remaining.match(/^(true|false|null)/);
|
|
204
|
+
if (boolNullMatch) {
|
|
205
|
+
tokens.push(_jsx(Text, { color: "magenta", children: boolNullMatch[1] }, tokenKey++));
|
|
206
|
+
lineLength += boolNullMatch[1].length;
|
|
207
|
+
remaining = remaining.slice(boolNullMatch[1].length);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Match whitespace or punctuation
|
|
211
|
+
const otherMatch = remaining.match(/^([\s{}[\]:,]+)/);
|
|
212
|
+
if (otherMatch) {
|
|
213
|
+
tokens.push(_jsx(Text, { dimColor: true, children: otherMatch[1] }, tokenKey++));
|
|
214
|
+
lineLength += otherMatch[1].length;
|
|
215
|
+
remaining = remaining.slice(otherMatch[1].length);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Fallback: single character
|
|
219
|
+
tokens.push(_jsx(Text, { children: remaining[0] }, tokenKey++));
|
|
220
|
+
lineLength += 1;
|
|
221
|
+
remaining = remaining.slice(1);
|
|
222
|
+
}
|
|
223
|
+
// If line was truncated due to length
|
|
224
|
+
if (remaining.length > 0) {
|
|
225
|
+
tokens.push(_jsx(Text, { dimColor: true, children: "..." }, tokenKey++));
|
|
226
|
+
}
|
|
227
|
+
elements.push(_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [lineNum, " "] }), tokens] }, i));
|
|
228
|
+
}
|
|
229
|
+
if (hasMore) {
|
|
230
|
+
elements.push(_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" ... (", lines.length - maxLines, " more lines)"] }) }, "more"));
|
|
231
|
+
}
|
|
232
|
+
return elements;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* JsonSnippetEditor component for editing complex nested JSON sections.
|
|
236
|
+
* Supports copy/paste and live JSON validation with syntax highlighting.
|
|
237
|
+
* Used for MCP servers, actions, skills, and other complex configs.
|
|
238
|
+
*/
|
|
239
|
+
export function JsonSnippetEditor({ label, value, onConfirm, onCancel, isFocused = true, maxHeight = 15, maxWidth = 80, }) {
|
|
240
|
+
// Format the value as JSON for editing
|
|
241
|
+
const initialJson = useMemo(() => formatJson(value), [value]);
|
|
242
|
+
const [mode, setMode] = useState("view");
|
|
243
|
+
const [editText, setEditText] = useState(initialJson);
|
|
244
|
+
const [parseError, setParseError] = useState(null);
|
|
245
|
+
const [warnings, setWarnings] = useState([]);
|
|
246
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
247
|
+
const [copied, setCopied] = useState(false);
|
|
248
|
+
// Validate JSON as user types
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const result = parseJsonWithLineInfo(editText);
|
|
251
|
+
if (result.error) {
|
|
252
|
+
setParseError(result.error);
|
|
253
|
+
setWarnings([]);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
setParseError(null);
|
|
257
|
+
const structureWarnings = validateJsonStructure(result.value, label);
|
|
258
|
+
setWarnings(structureWarnings);
|
|
259
|
+
}
|
|
260
|
+
}, [editText, label]);
|
|
261
|
+
// Reset edit text when value changes externally
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
const newJson = formatJson(value);
|
|
264
|
+
if (mode === "view") {
|
|
265
|
+
setEditText(newJson);
|
|
266
|
+
}
|
|
267
|
+
}, [value, mode]);
|
|
268
|
+
// Calculate content lines for scrolling
|
|
269
|
+
const contentLines = useMemo(() => {
|
|
270
|
+
return editText.split("\n").length;
|
|
271
|
+
}, [editText]);
|
|
272
|
+
// Max visible lines for preview (accounting for header, errors, footer)
|
|
273
|
+
const previewMaxLines = Math.max(3, maxHeight - 6);
|
|
274
|
+
// Handle saving
|
|
275
|
+
const handleSave = useCallback(() => {
|
|
276
|
+
const result = parseJsonWithLineInfo(editText);
|
|
277
|
+
if (result.error) {
|
|
278
|
+
// Cannot save with parse errors
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
onConfirm(result.value);
|
|
282
|
+
}, [editText, onConfirm]);
|
|
283
|
+
// Handle entering edit mode
|
|
284
|
+
const handleEdit = useCallback(() => {
|
|
285
|
+
setMode("edit");
|
|
286
|
+
}, []);
|
|
287
|
+
// Handle canceling edit
|
|
288
|
+
const handleCancelEdit = useCallback(() => {
|
|
289
|
+
setEditText(initialJson);
|
|
290
|
+
setMode("view");
|
|
291
|
+
setParseError(null);
|
|
292
|
+
setWarnings([]);
|
|
293
|
+
}, [initialJson]);
|
|
294
|
+
// Handle copying to clipboard
|
|
295
|
+
const handleCopy = useCallback(() => {
|
|
296
|
+
// Use process.stdout.write with OSC 52 escape sequence for clipboard
|
|
297
|
+
// This works in most modern terminals
|
|
298
|
+
const base64 = Buffer.from(editText).toString("base64");
|
|
299
|
+
process.stdout.write(`\x1b]52;c;${base64}\x07`);
|
|
300
|
+
setCopied(true);
|
|
301
|
+
setTimeout(() => setCopied(false), 1500);
|
|
302
|
+
}, [editText]);
|
|
303
|
+
// Handle formatting JSON
|
|
304
|
+
const handleFormat = useCallback(() => {
|
|
305
|
+
const result = parseJsonWithLineInfo(editText);
|
|
306
|
+
if (!result.error && result.value !== undefined) {
|
|
307
|
+
setEditText(formatJson(result.value));
|
|
308
|
+
}
|
|
309
|
+
}, [editText]);
|
|
310
|
+
// Scroll handlers
|
|
311
|
+
const handleScrollUp = useCallback(() => {
|
|
312
|
+
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
313
|
+
}, []);
|
|
314
|
+
const handleScrollDown = useCallback(() => {
|
|
315
|
+
setScrollOffset((prev) => Math.min(contentLines - previewMaxLines, prev + 1));
|
|
316
|
+
}, [contentLines, previewMaxLines]);
|
|
317
|
+
const handlePageUp = useCallback(() => {
|
|
318
|
+
setScrollOffset((prev) => Math.max(0, prev - previewMaxLines));
|
|
319
|
+
}, [previewMaxLines]);
|
|
320
|
+
const handlePageDown = useCallback(() => {
|
|
321
|
+
setScrollOffset((prev) => Math.min(contentLines - previewMaxLines, prev + previewMaxLines));
|
|
322
|
+
}, [contentLines, previewMaxLines]);
|
|
323
|
+
// Handle keyboard input for view mode
|
|
324
|
+
useInput((input, key) => {
|
|
325
|
+
if (!isFocused || mode !== "view")
|
|
326
|
+
return;
|
|
327
|
+
if (key.return || input === "e") {
|
|
328
|
+
handleEdit();
|
|
329
|
+
}
|
|
330
|
+
else if (input === "s" || input === "S") {
|
|
331
|
+
handleSave();
|
|
332
|
+
}
|
|
333
|
+
else if (key.escape) {
|
|
334
|
+
onCancel();
|
|
335
|
+
}
|
|
336
|
+
else if (input === "c" || input === "C") {
|
|
337
|
+
handleCopy();
|
|
338
|
+
}
|
|
339
|
+
else if (input === "f" || input === "F") {
|
|
340
|
+
handleFormat();
|
|
341
|
+
}
|
|
342
|
+
else if (input === "j" || key.downArrow) {
|
|
343
|
+
handleScrollDown();
|
|
344
|
+
}
|
|
345
|
+
else if (input === "k" || key.upArrow) {
|
|
346
|
+
handleScrollUp();
|
|
347
|
+
}
|
|
348
|
+
else if (key.pageUp) {
|
|
349
|
+
handlePageUp();
|
|
350
|
+
}
|
|
351
|
+
else if (key.pageDown) {
|
|
352
|
+
handlePageDown();
|
|
353
|
+
}
|
|
354
|
+
}, { isActive: isFocused && mode === "view" });
|
|
355
|
+
// Handle keyboard input for edit mode
|
|
356
|
+
useInput((_input, key) => {
|
|
357
|
+
if (!isFocused || mode !== "edit")
|
|
358
|
+
return;
|
|
359
|
+
if (key.escape) {
|
|
360
|
+
handleCancelEdit();
|
|
361
|
+
}
|
|
362
|
+
}, { isActive: isFocused && mode === "edit" });
|
|
363
|
+
// Count errors and warnings for status bar
|
|
364
|
+
const errorCount = parseError ? 1 : 0;
|
|
365
|
+
const warningCount = warnings.length;
|
|
366
|
+
// Render edit mode with text input
|
|
367
|
+
if (mode === "edit") {
|
|
368
|
+
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 JSON: ", label] }) }), _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [">", " "] }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleSave, focus: isFocused })] }), parseError && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", bold: true, children: "Syntax Error:" }), _jsxs(Text, { color: "red", children: [parseError.line && parseError.column
|
|
369
|
+
? `Line ${parseError.line}, Column ${parseError.column}: `
|
|
370
|
+
: "", parseError.message] })] })), _jsx(Box, { marginTop: 1, children: errorCount > 0 ? (_jsxs(Text, { color: "red", children: [errorCount, " error", errorCount > 1 ? "s" : ""] })) : warningCount > 0 ? (_jsxs(Text, { color: "yellow", children: [warningCount, " warning", warningCount > 1 ? "s" : ""] })) : (_jsx(Text, { color: "green", children: "Valid JSON" })) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save (if valid) | Esc: cancel" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Tip: Paste multi-line JSON, then press Enter" }) })] }));
|
|
371
|
+
}
|
|
372
|
+
// Render view mode with syntax-highlighted preview
|
|
373
|
+
// Account for border (2) and padding (2) when calculating preview width
|
|
374
|
+
const previewWidth = Math.max(40, maxWidth - 4);
|
|
375
|
+
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
|
|
377
|
+
? `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" })] })] }));
|
|
379
|
+
}
|
|
380
|
+
export default JsonSnippetEditor;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Provider-specific hint configuration.
|
|
4
|
+
* Shows common keys and their descriptions based on the notification provider.
|
|
5
|
+
*/
|
|
6
|
+
export interface ProviderHint {
|
|
7
|
+
key: string;
|
|
8
|
+
description: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Built-in hints for known notification providers.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROVIDER_HINTS: Record<string, ProviderHint[]>;
|
|
15
|
+
export interface KeyValueEditorProps {
|
|
16
|
+
/** The label to display for this object field */
|
|
17
|
+
label: string;
|
|
18
|
+
/** The current object entries as key-value pairs */
|
|
19
|
+
entries: Record<string, string>;
|
|
20
|
+
/** Called when the user confirms the edit */
|
|
21
|
+
onConfirm: (newEntries: Record<string, string>) => void;
|
|
22
|
+
/** Called when the user cancels the edit (Esc) */
|
|
23
|
+
onCancel: () => void;
|
|
24
|
+
/** Whether this editor has focus */
|
|
25
|
+
isFocused?: boolean;
|
|
26
|
+
/** Provider name for showing hints (e.g., "ntfy", "pushover", "gotify") */
|
|
27
|
+
providerName?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* KeyValueEditor component for editing key-value pairs with provider-specific hints.
|
|
31
|
+
* Enhanced version of ObjectEditor with support for common key suggestions.
|
|
32
|
+
*/
|
|
33
|
+
export declare function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused, providerName, }: KeyValueEditorProps): React.ReactElement;
|
|
34
|
+
export default KeyValueEditor;
|