snow-ai 0.3.23 → 0.3.25
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/cli.d.ts +1 -1
- package/dist/cli.js +3 -0
- package/dist/hooks/useConversation.d.ts +0 -5
- package/dist/hooks/useConversation.js +91 -44
- package/dist/hooks/useFilePicker.d.ts +1 -1
- package/dist/hooks/useFilePicker.js +13 -7
- package/dist/hooks/useInputBuffer.d.ts +1 -1
- package/dist/hooks/useInputBuffer.js +24 -9
- package/dist/hooks/useStreamingState.js +2 -2
- package/dist/hooks/useVSCodeState.js +23 -6
- package/dist/mcp/filesystem.js +1 -1
- package/dist/ui/components/ChatInput.js +14 -9
- package/dist/ui/components/MarkdownRenderer.js +2 -14
- package/dist/ui/components/MessageList.d.ts +0 -1
- package/dist/ui/components/MessageList.js +1 -2
- package/dist/ui/components/ToolConfirmation.d.ts +1 -1
- package/dist/ui/components/ToolConfirmation.js +63 -22
- package/dist/ui/pages/ChatScreen.js +1 -5
- package/dist/ui/pages/ConfigScreen.js +8 -6
- package/dist/ui/pages/HeadlessModeScreen.js +0 -1
- package/dist/ui/pages/ProxyConfigScreen.d.ts +1 -1
- package/dist/ui/pages/ProxyConfigScreen.js +6 -6
- package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +7 -0
- package/dist/ui/pages/SensitiveCommandConfigScreen.js +262 -0
- package/dist/ui/pages/SubAgentConfigScreen.js +1 -1
- package/dist/ui/pages/WelcomeScreen.js +14 -3
- package/dist/utils/patch-highlight.d.ts +5 -0
- package/dist/utils/patch-highlight.js +23 -0
- package/dist/utils/sensitiveCommandManager.d.ts +53 -0
- package/dist/utils/sensitiveCommandManager.js +308 -0
- package/package.json +4 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { isSensitiveCommand } from '../../utils/sensitiveCommandManager.js';
|
|
4
5
|
// Helper function to format argument values with truncation
|
|
5
6
|
function formatArgumentValue(value, maxLength = 100) {
|
|
6
7
|
if (value === null || value === undefined) {
|
|
@@ -35,11 +36,28 @@ function formatArgumentsAsTree(args, toolName) {
|
|
|
35
36
|
return keys.map((key, index) => ({
|
|
36
37
|
key,
|
|
37
38
|
value: formatArgumentValue(args[key]),
|
|
38
|
-
isLast: index === keys.length - 1
|
|
39
|
+
isLast: index === keys.length - 1,
|
|
39
40
|
}));
|
|
40
41
|
}
|
|
41
|
-
export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm }) {
|
|
42
|
+
export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm, }) {
|
|
42
43
|
const [hasSelected, setHasSelected] = useState(false);
|
|
44
|
+
// Check if this is a sensitive command (for terminal-execute)
|
|
45
|
+
const sensitiveCommandCheck = useMemo(() => {
|
|
46
|
+
if (toolName !== 'terminal-execute' || !toolArguments) {
|
|
47
|
+
return { isSensitive: false };
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(toolArguments);
|
|
51
|
+
const command = parsed.command;
|
|
52
|
+
if (command && typeof command === 'string') {
|
|
53
|
+
return isSensitiveCommand(command);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore parse errors
|
|
58
|
+
}
|
|
59
|
+
return { isSensitive: false };
|
|
60
|
+
}, [toolName, toolArguments]);
|
|
43
61
|
// Parse and format tool arguments for display (single tool)
|
|
44
62
|
const formattedArgs = useMemo(() => {
|
|
45
63
|
if (!toolArguments)
|
|
@@ -61,31 +79,38 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
61
79
|
const parsed = JSON.parse(tool.function.arguments);
|
|
62
80
|
return {
|
|
63
81
|
name: tool.function.name,
|
|
64
|
-
args: formatArgumentsAsTree(parsed, tool.function.name)
|
|
82
|
+
args: formatArgumentsAsTree(parsed, tool.function.name),
|
|
65
83
|
};
|
|
66
84
|
}
|
|
67
85
|
catch {
|
|
68
86
|
return {
|
|
69
87
|
name: tool.function.name,
|
|
70
|
-
args: []
|
|
88
|
+
args: [],
|
|
71
89
|
};
|
|
72
90
|
}
|
|
73
91
|
});
|
|
74
92
|
}, [allTools]);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
// Conditionally show "Always approve" based on sensitive command check
|
|
94
|
+
const items = useMemo(() => {
|
|
95
|
+
const baseItems = [
|
|
96
|
+
{
|
|
97
|
+
label: 'Approve (once)',
|
|
98
|
+
value: 'approve',
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
// Only show "Always approve" if NOT a sensitive command
|
|
102
|
+
if (!sensitiveCommandCheck.isSensitive) {
|
|
103
|
+
baseItems.push({
|
|
104
|
+
label: 'Always approve this tool',
|
|
105
|
+
value: 'approve_always',
|
|
106
|
+
});
|
|
87
107
|
}
|
|
88
|
-
|
|
108
|
+
baseItems.push({
|
|
109
|
+
label: 'Reject (end session)',
|
|
110
|
+
value: 'reject',
|
|
111
|
+
});
|
|
112
|
+
return baseItems;
|
|
113
|
+
}, [sensitiveCommandCheck.isSensitive]);
|
|
89
114
|
const handleSelect = (item) => {
|
|
90
115
|
if (!hasSelected) {
|
|
91
116
|
setHasSelected(true);
|
|
@@ -98,8 +123,21 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
98
123
|
!formattedAllTools && (React.createElement(React.Fragment, null,
|
|
99
124
|
React.createElement(Box, { marginBottom: 1 },
|
|
100
125
|
React.createElement(Text, null,
|
|
101
|
-
"Tool:
|
|
126
|
+
"Tool:",
|
|
127
|
+
' ',
|
|
102
128
|
React.createElement(Text, { bold: true, color: "cyan" }, toolName))),
|
|
129
|
+
sensitiveCommandCheck.isSensitive && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
130
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
131
|
+
React.createElement(Text, { bold: true, color: "red" }, "SENSITIVE COMMAND DETECTED")),
|
|
132
|
+
React.createElement(Box, { flexDirection: "column", gap: 0 },
|
|
133
|
+
React.createElement(Box, null,
|
|
134
|
+
React.createElement(Text, { dimColor: true }, "Pattern: "),
|
|
135
|
+
React.createElement(Text, { color: "magenta", bold: true }, sensitiveCommandCheck.matchedCommand?.pattern)),
|
|
136
|
+
React.createElement(Box, { marginTop: 0 },
|
|
137
|
+
React.createElement(Text, { dimColor: true }, "Reason: "),
|
|
138
|
+
React.createElement(Text, { color: "white" }, sensitiveCommandCheck.matchedCommand?.description))),
|
|
139
|
+
React.createElement(Box, { marginTop: 1, paddingX: 1, paddingY: 0 },
|
|
140
|
+
React.createElement(Text, { color: "yellow", italic: true }, "This command requires confirmation even in YOLO/Always-Approved mode")))),
|
|
103
141
|
formattedArgs && formattedArgs.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
104
142
|
React.createElement(Text, { dimColor: true }, "Arguments:"),
|
|
105
143
|
formattedArgs.map((arg, index) => (React.createElement(Box, { key: index, flexDirection: "column" },
|
|
@@ -107,12 +145,14 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
107
145
|
arg.isLast ? '└─' : '├─',
|
|
108
146
|
" ",
|
|
109
147
|
arg.key,
|
|
110
|
-
":
|
|
148
|
+
":",
|
|
149
|
+
' ',
|
|
111
150
|
React.createElement(Text, { color: "white" }, arg.value))))))))),
|
|
112
151
|
formattedAllTools && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
113
152
|
React.createElement(Box, { marginBottom: 1 },
|
|
114
153
|
React.createElement(Text, null,
|
|
115
|
-
"Tools:
|
|
154
|
+
"Tools:",
|
|
155
|
+
' ',
|
|
116
156
|
React.createElement(Text, { bold: true, color: "cyan" },
|
|
117
157
|
formattedAllTools.length,
|
|
118
158
|
" tools in parallel"))),
|
|
@@ -125,11 +165,12 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
125
165
|
arg.isLast ? '└─' : '├─',
|
|
126
166
|
" ",
|
|
127
167
|
arg.key,
|
|
128
|
-
":
|
|
168
|
+
":",
|
|
169
|
+
' ',
|
|
129
170
|
React.createElement(Text, { color: "white" }, arg.value))))))))))),
|
|
130
171
|
React.createElement(Box, { marginBottom: 1 },
|
|
131
172
|
React.createElement(Text, { dimColor: true }, "Select action:")),
|
|
132
|
-
!hasSelected &&
|
|
173
|
+
!hasSelected && React.createElement(SelectInput, { items: items, onSelect: handleSelect }),
|
|
133
174
|
hasSelected && (React.createElement(Box, null,
|
|
134
175
|
React.createElement(Text, { color: "green" }, "Confirmed")))));
|
|
135
176
|
}
|
|
@@ -13,7 +13,6 @@ import MarkdownRenderer from '../components/MarkdownRenderer.js';
|
|
|
13
13
|
import ToolConfirmation from '../components/ToolConfirmation.js';
|
|
14
14
|
import DiffViewer from '../components/DiffViewer.js';
|
|
15
15
|
import ToolResultPreview from '../components/ToolResultPreview.js';
|
|
16
|
-
import TodoTree from '../components/TodoTree.js';
|
|
17
16
|
import FileRollbackConfirmation from '../components/FileRollbackConfirmation.js';
|
|
18
17
|
import ShimmerText from '../components/ShimmerText.js';
|
|
19
18
|
import { getOpenAiConfig } from '../../utils/apiConfig.js';
|
|
@@ -50,7 +49,6 @@ import '../../utils/commands/todoPicker.js';
|
|
|
50
49
|
export default function ChatScreen({ skipWelcome }) {
|
|
51
50
|
const [messages, setMessages] = useState([]);
|
|
52
51
|
const [isSaving] = useState(false);
|
|
53
|
-
const [currentTodos, setCurrentTodos] = useState([]);
|
|
54
52
|
const [pendingMessages, setPendingMessages] = useState([]);
|
|
55
53
|
const pendingMessagesRef = useRef([]);
|
|
56
54
|
const hasAttemptedAutoVscodeConnect = useRef(false);
|
|
@@ -570,7 +568,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
570
568
|
saveMessage,
|
|
571
569
|
setMessages,
|
|
572
570
|
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
573
|
-
setCurrentTodos,
|
|
574
571
|
requestToolConfirmation,
|
|
575
572
|
isToolAutoApproved,
|
|
576
573
|
addMultipleToAlwaysApproved,
|
|
@@ -754,7 +751,6 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
754
751
|
saveMessage,
|
|
755
752
|
setMessages,
|
|
756
753
|
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
757
|
-
setCurrentTodos,
|
|
758
754
|
requestToolConfirmation,
|
|
759
755
|
isToolAutoApproved,
|
|
760
756
|
addMultipleToAlwaysApproved,
|
|
@@ -939,7 +935,7 @@ export default function ChatScreen({ skipWelcome }) {
|
|
|
939
935
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
940
936
|
"\u2514\u2500 ",
|
|
941
937
|
message.commandName),
|
|
942
|
-
message.content && (React.createElement(Text, { color: "white" }, message.content)))) :
|
|
938
|
+
message.content && (React.createElement(Text, { color: "white" }, message.content)))) : (React.createElement(React.Fragment, null,
|
|
943
939
|
message.role === 'user' || isToolMessage ? (React.createElement(Text, { color: message.role === 'user'
|
|
944
940
|
? 'gray'
|
|
945
941
|
: message.content.startsWith('⚡')
|
|
@@ -455,7 +455,7 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
|
|
|
455
455
|
"Anthropic Beta:"),
|
|
456
456
|
React.createElement(Box, { marginLeft: 3 },
|
|
457
457
|
React.createElement(Text, { color: "gray" },
|
|
458
|
-
anthropicBeta ? '
|
|
458
|
+
anthropicBeta ? '[✓] Enabled' : '[ ] Disabled',
|
|
459
459
|
" (Press Enter to toggle)"))));
|
|
460
460
|
case 'thinkingEnabled':
|
|
461
461
|
return (React.createElement(Box, { key: field, flexDirection: "column" },
|
|
@@ -464,7 +464,7 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
|
|
|
464
464
|
"Thinking Enabled:"),
|
|
465
465
|
React.createElement(Box, { marginLeft: 3 },
|
|
466
466
|
React.createElement(Text, { color: "gray" },
|
|
467
|
-
thinkingEnabled ? '
|
|
467
|
+
thinkingEnabled ? '[✓] Enabled' : '[ ] Disabled',
|
|
468
468
|
" (Press Enter to toggle)"))));
|
|
469
469
|
case 'thinkingBudgetTokens':
|
|
470
470
|
return (React.createElement(Box, { key: field, flexDirection: "column" },
|
|
@@ -484,7 +484,7 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
|
|
|
484
484
|
"Gemini Thinking Enabled:"),
|
|
485
485
|
React.createElement(Box, { marginLeft: 3 },
|
|
486
486
|
React.createElement(Text, { color: "gray" },
|
|
487
|
-
geminiThinkingEnabled ? '
|
|
487
|
+
geminiThinkingEnabled ? '[✓] Enabled' : '[ ] Disabled',
|
|
488
488
|
" (Press Enter to toggle)"))));
|
|
489
489
|
case 'geminiThinkingBudget':
|
|
490
490
|
return (React.createElement(Box, { key: field, flexDirection: "column" },
|
|
@@ -504,8 +504,9 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
|
|
|
504
504
|
"Responses Reasoning Enabled:"),
|
|
505
505
|
React.createElement(Box, { marginLeft: 3 },
|
|
506
506
|
React.createElement(Text, { color: "gray" },
|
|
507
|
-
responsesReasoningEnabled ? '
|
|
508
|
-
|
|
507
|
+
responsesReasoningEnabled ? '[✓] Enabled' : '[ ] Disabled',
|
|
508
|
+
' ',
|
|
509
|
+
"(Press Enter to toggle)"))));
|
|
509
510
|
case 'responsesReasoningEffort':
|
|
510
511
|
return (React.createElement(Box, { key: field, flexDirection: "column" },
|
|
511
512
|
React.createElement(Text, { color: isActive ? 'green' : 'white' },
|
|
@@ -964,7 +965,8 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
|
|
|
964
965
|
currentField === 'advancedModel' && 'Advanced Model',
|
|
965
966
|
currentField === 'basicModel' && 'Basic Model',
|
|
966
967
|
currentField === 'compactModelName' && 'Compact Model',
|
|
967
|
-
currentField === 'responsesReasoningEffort' &&
|
|
968
|
+
currentField === 'responsesReasoningEffort' &&
|
|
969
|
+
'Responses Reasoning Effort',
|
|
968
970
|
":"),
|
|
969
971
|
React.createElement(Box, { marginLeft: 3, marginTop: 1 },
|
|
970
972
|
currentField === 'profile' && (React.createElement(Box, { flexDirection: "column" },
|
|
@@ -339,7 +339,6 @@ export default function HeadlessModeScreen({ prompt, onComplete }) {
|
|
|
339
339
|
saveMessage,
|
|
340
340
|
setMessages,
|
|
341
341
|
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
342
|
-
setCurrentTodos: () => { }, // No-op in headless mode
|
|
343
342
|
requestToolConfirmation,
|
|
344
343
|
isToolAutoApproved,
|
|
345
344
|
addMultipleToAlwaysApproved,
|
|
@@ -4,5 +4,5 @@ type Props = {
|
|
|
4
4
|
onSave: () => void;
|
|
5
5
|
inlineMode?: boolean;
|
|
6
6
|
};
|
|
7
|
-
export default function ProxyConfigScreen({ onBack, onSave, inlineMode }: Props): React.JSX.Element;
|
|
7
|
+
export default function ProxyConfigScreen({ onBack, onSave, inlineMode, }: Props): React.JSX.Element;
|
|
8
8
|
export {};
|
|
@@ -4,7 +4,7 @@ import Gradient from 'ink-gradient';
|
|
|
4
4
|
import { Alert } from '@inkjs/ui';
|
|
5
5
|
import TextInput from 'ink-text-input';
|
|
6
6
|
import { getProxyConfig, updateProxyConfig, } from '../../utils/apiConfig.js';
|
|
7
|
-
export default function ProxyConfigScreen({ onBack, onSave, inlineMode = false }) {
|
|
7
|
+
export default function ProxyConfigScreen({ onBack, onSave, inlineMode = false, }) {
|
|
8
8
|
const [enabled, setEnabled] = useState(false);
|
|
9
9
|
const [port, setPort] = useState('7890');
|
|
10
10
|
const [browserPath, setBrowserPath] = useState('');
|
|
@@ -86,7 +86,7 @@ export default function ProxyConfigScreen({ onBack, onSave, inlineMode = false }
|
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
88
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
89
|
-
!inlineMode && (React.createElement(Box, { marginBottom: 1, borderStyle: "double", borderColor:
|
|
89
|
+
!inlineMode && (React.createElement(Box, { marginBottom: 1, borderStyle: "double", borderColor: 'cyan', paddingX: 2, paddingY: 1 },
|
|
90
90
|
React.createElement(Box, { flexDirection: "column" },
|
|
91
91
|
React.createElement(Gradient, { name: "rainbow" }, "Proxy Configuration"),
|
|
92
92
|
React.createElement(Text, { color: "gray", dimColor: true }, "Configure system proxy for web search and fetch")))),
|
|
@@ -98,7 +98,7 @@ export default function ProxyConfigScreen({ onBack, onSave, inlineMode = false }
|
|
|
98
98
|
"Enable Proxy:"),
|
|
99
99
|
React.createElement(Box, { marginLeft: 3 },
|
|
100
100
|
React.createElement(Text, { color: "gray" },
|
|
101
|
-
enabled ? '
|
|
101
|
+
enabled ? '[✓] Enabled' : '[ ] Disabled',
|
|
102
102
|
" (Press Enter to toggle)")))),
|
|
103
103
|
React.createElement(Box, { marginBottom: 1 },
|
|
104
104
|
React.createElement(Box, { flexDirection: "column" },
|
|
@@ -131,13 +131,13 @@ export default function ProxyConfigScreen({ onBack, onSave, inlineMode = false }
|
|
|
131
131
|
"Browser Path Examples: ",
|
|
132
132
|
React.createElement(Newline, null),
|
|
133
133
|
React.createElement(Text, { color: 'blue' }, "\u2022 Windows: C:\\Program Files(x86)\\Microsoft\\Edge\\Application\\msedge.exe"),
|
|
134
|
-
|
|
134
|
+
' ',
|
|
135
135
|
React.createElement(Newline, null),
|
|
136
136
|
React.createElement(Text, { color: 'green' }, "\u2022 macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
|
137
|
-
|
|
137
|
+
' ',
|
|
138
138
|
React.createElement(Newline, null),
|
|
139
139
|
React.createElement(Text, { color: 'yellow' }, "\u2022 Linux: /usr/bin/chromium-browser"),
|
|
140
|
-
|
|
140
|
+
' ',
|
|
141
141
|
React.createElement(Newline, null),
|
|
142
142
|
"Leave empty to auto-detect system browser (Edge/Chrome)"))));
|
|
143
143
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import { Alert } from '@inkjs/ui';
|
|
5
|
+
import { getAllSensitiveCommands, toggleSensitiveCommand, addSensitiveCommand, removeSensitiveCommand, resetToDefaults, } from '../../utils/sensitiveCommandManager.js';
|
|
6
|
+
// Focus event handling
|
|
7
|
+
const focusEventTokenRegex = /(?:\x1b)?\[[0-9;]*[IO]/g;
|
|
8
|
+
const isFocusEventInput = (value) => {
|
|
9
|
+
if (!value)
|
|
10
|
+
return false;
|
|
11
|
+
if (value === '\x1b[I' ||
|
|
12
|
+
value === '\x1b[O' ||
|
|
13
|
+
value === '[I' ||
|
|
14
|
+
value === '[O') {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return false;
|
|
20
|
+
const tokens = trimmed.match(focusEventTokenRegex);
|
|
21
|
+
if (!tokens)
|
|
22
|
+
return false;
|
|
23
|
+
const normalized = trimmed.replace(/\s+/g, '');
|
|
24
|
+
const tokensCombined = tokens.join('');
|
|
25
|
+
return tokensCombined === normalized;
|
|
26
|
+
};
|
|
27
|
+
const stripFocusArtifacts = (value) => {
|
|
28
|
+
if (!value)
|
|
29
|
+
return '';
|
|
30
|
+
return value
|
|
31
|
+
.replace(/\x1b\[[0-9;]*[IO]/g, '')
|
|
32
|
+
.replace(/\[[0-9;]*[IO]/g, '')
|
|
33
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
34
|
+
};
|
|
35
|
+
export default function SensitiveCommandConfigScreen({ onBack, inlineMode = false, }) {
|
|
36
|
+
const [commands, setCommands] = useState([]);
|
|
37
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
38
|
+
const [viewMode, setViewMode] = useState('list');
|
|
39
|
+
const [showSuccess, setShowSuccess] = useState(false);
|
|
40
|
+
const [successMessage, setSuccessMessage] = useState('');
|
|
41
|
+
// Confirmation states
|
|
42
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
43
|
+
const [confirmReset, setConfirmReset] = useState(false);
|
|
44
|
+
// Add custom command fields
|
|
45
|
+
const [customPattern, setCustomPattern] = useState('');
|
|
46
|
+
const [customDescription, setCustomDescription] = useState('');
|
|
47
|
+
const [addField, setAddField] = useState('pattern');
|
|
48
|
+
// Load commands
|
|
49
|
+
const loadCommands = useCallback(() => {
|
|
50
|
+
const allCommands = getAllSensitiveCommands();
|
|
51
|
+
setCommands(allCommands);
|
|
52
|
+
}, []);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
loadCommands();
|
|
55
|
+
}, [loadCommands]);
|
|
56
|
+
// Handle list view input
|
|
57
|
+
const handleListInput = useCallback((input, key) => {
|
|
58
|
+
if (key.escape) {
|
|
59
|
+
// Cancel any pending confirmations
|
|
60
|
+
if (confirmDelete || confirmReset) {
|
|
61
|
+
setConfirmDelete(false);
|
|
62
|
+
setConfirmReset(false);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
onBack();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.upArrow) {
|
|
69
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
70
|
+
// Clear confirmations when navigating
|
|
71
|
+
setConfirmDelete(false);
|
|
72
|
+
setConfirmReset(false);
|
|
73
|
+
}
|
|
74
|
+
else if (key.downArrow) {
|
|
75
|
+
setSelectedIndex(prev => Math.min(commands.length - 1, prev + 1));
|
|
76
|
+
// Clear confirmations when navigating
|
|
77
|
+
setConfirmDelete(false);
|
|
78
|
+
setConfirmReset(false);
|
|
79
|
+
}
|
|
80
|
+
else if (input === ' ') {
|
|
81
|
+
// Toggle command
|
|
82
|
+
const cmd = commands[selectedIndex];
|
|
83
|
+
if (cmd) {
|
|
84
|
+
toggleSensitiveCommand(cmd.id);
|
|
85
|
+
loadCommands();
|
|
86
|
+
setSuccessMessage(cmd.enabled
|
|
87
|
+
? `Disabled: ${cmd.pattern}`
|
|
88
|
+
: `Enabled: ${cmd.pattern}`);
|
|
89
|
+
setShowSuccess(true);
|
|
90
|
+
setTimeout(() => setShowSuccess(false), 2000);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (input === 'a' || input === 'A') {
|
|
94
|
+
// Add custom command
|
|
95
|
+
setViewMode('add');
|
|
96
|
+
setCustomPattern('');
|
|
97
|
+
setCustomDescription('');
|
|
98
|
+
setAddField('pattern');
|
|
99
|
+
}
|
|
100
|
+
else if (input === 'd' || input === 'D') {
|
|
101
|
+
// Delete custom command - requires confirmation
|
|
102
|
+
const cmd = commands[selectedIndex];
|
|
103
|
+
if (cmd && !cmd.isPreset) {
|
|
104
|
+
if (!confirmDelete) {
|
|
105
|
+
// First press - ask for confirmation
|
|
106
|
+
setConfirmDelete(true);
|
|
107
|
+
setConfirmReset(false);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Second press - execute delete
|
|
111
|
+
removeSensitiveCommand(cmd.id);
|
|
112
|
+
loadCommands();
|
|
113
|
+
setSelectedIndex(prev => Math.min(prev, commands.length - 2));
|
|
114
|
+
setSuccessMessage(`Deleted: ${cmd.pattern}`);
|
|
115
|
+
setShowSuccess(true);
|
|
116
|
+
setTimeout(() => setShowSuccess(false), 2000);
|
|
117
|
+
setConfirmDelete(false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (input === 'r' || input === 'R') {
|
|
122
|
+
// Reset to defaults - requires confirmation
|
|
123
|
+
if (!confirmReset) {
|
|
124
|
+
// First press - ask for confirmation
|
|
125
|
+
setConfirmReset(true);
|
|
126
|
+
setConfirmDelete(false);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Second press - execute reset
|
|
130
|
+
resetToDefaults();
|
|
131
|
+
loadCommands();
|
|
132
|
+
setSelectedIndex(0);
|
|
133
|
+
setSuccessMessage('Reset to default commands');
|
|
134
|
+
setShowSuccess(true);
|
|
135
|
+
setTimeout(() => setShowSuccess(false), 2000);
|
|
136
|
+
setConfirmReset(false);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}, [
|
|
140
|
+
commands,
|
|
141
|
+
selectedIndex,
|
|
142
|
+
onBack,
|
|
143
|
+
loadCommands,
|
|
144
|
+
confirmDelete,
|
|
145
|
+
confirmReset,
|
|
146
|
+
]);
|
|
147
|
+
// Handle add view input
|
|
148
|
+
const handleAddInput = useCallback((_input, key) => {
|
|
149
|
+
if (key.escape) {
|
|
150
|
+
setViewMode('list');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (key.tab) {
|
|
154
|
+
setAddField(prev => (prev === 'pattern' ? 'description' : 'pattern'));
|
|
155
|
+
}
|
|
156
|
+
}, []);
|
|
157
|
+
// Use input hook
|
|
158
|
+
useInput((input, key) => {
|
|
159
|
+
if (viewMode === 'list') {
|
|
160
|
+
handleListInput(input, key);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
handleAddInput(input, key);
|
|
164
|
+
}
|
|
165
|
+
}, { isActive: true });
|
|
166
|
+
// Handle pattern input change
|
|
167
|
+
const handlePatternChange = useCallback((value) => {
|
|
168
|
+
if (!isFocusEventInput(value)) {
|
|
169
|
+
setCustomPattern(stripFocusArtifacts(value));
|
|
170
|
+
}
|
|
171
|
+
}, []);
|
|
172
|
+
// Handle description input change
|
|
173
|
+
const handleDescriptionChange = useCallback((value) => {
|
|
174
|
+
if (!isFocusEventInput(value)) {
|
|
175
|
+
setCustomDescription(stripFocusArtifacts(value));
|
|
176
|
+
}
|
|
177
|
+
}, []);
|
|
178
|
+
// Handle add submit
|
|
179
|
+
const handleAddSubmit = useCallback(() => {
|
|
180
|
+
if (addField === 'pattern') {
|
|
181
|
+
setAddField('description');
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Submit
|
|
185
|
+
if (customPattern.trim() && customDescription.trim()) {
|
|
186
|
+
try {
|
|
187
|
+
addSensitiveCommand(customPattern.trim(), customDescription.trim());
|
|
188
|
+
loadCommands();
|
|
189
|
+
setViewMode('list');
|
|
190
|
+
setSuccessMessage(`Added: ${customPattern}`);
|
|
191
|
+
setShowSuccess(true);
|
|
192
|
+
setTimeout(() => setShowSuccess(false), 2000);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Handle error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, [addField, customPattern, customDescription, loadCommands]);
|
|
200
|
+
if (viewMode === 'add') {
|
|
201
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: inlineMode ? 0 : 2, paddingY: 1 },
|
|
202
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Add Custom Sensitive Command"),
|
|
203
|
+
React.createElement(Box, { marginTop: 1 }),
|
|
204
|
+
React.createElement(Text, { dimColor: true }, "Pattern (supports wildcards, e.g., \"rm*\"):"),
|
|
205
|
+
React.createElement(Box, null,
|
|
206
|
+
React.createElement(Text, { color: addField === 'pattern' ? 'cyan' : 'gray' }, "\u276F "),
|
|
207
|
+
React.createElement(TextInput, { value: customPattern, onChange: handlePatternChange, onSubmit: handleAddSubmit, focus: addField === 'pattern' })),
|
|
208
|
+
React.createElement(Box, { marginTop: 1 }),
|
|
209
|
+
React.createElement(Text, { dimColor: true }, "Description:"),
|
|
210
|
+
React.createElement(Box, null,
|
|
211
|
+
React.createElement(Text, { color: addField === 'description' ? 'cyan' : 'gray' }, "\u276F "),
|
|
212
|
+
React.createElement(TextInput, { value: customDescription, onChange: handleDescriptionChange, onSubmit: handleAddSubmit, focus: addField === 'description' })),
|
|
213
|
+
React.createElement(Box, { marginTop: 1 }),
|
|
214
|
+
React.createElement(Text, { dimColor: true }, "Tab: Switch \u2022 Enter: Submit \u2022 Esc: Cancel")));
|
|
215
|
+
}
|
|
216
|
+
// Calculate visible range for scrolling
|
|
217
|
+
const viewportHeight = 13;
|
|
218
|
+
const startIndex = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2));
|
|
219
|
+
const endIndex = Math.min(commands.length, startIndex + viewportHeight);
|
|
220
|
+
const adjustedStart = Math.max(0, endIndex - viewportHeight);
|
|
221
|
+
const selectedCmd = commands[selectedIndex];
|
|
222
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: inlineMode ? 0 : 2, paddingY: 1 },
|
|
223
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Sensitive Command Protection"),
|
|
224
|
+
React.createElement(Text, { dimColor: true }, "Configure commands that require confirmation even in YOLO/Always-Approved mode"),
|
|
225
|
+
showSuccess && (React.createElement(Box, { marginTop: 1 },
|
|
226
|
+
React.createElement(Alert, { variant: "success" }, successMessage))),
|
|
227
|
+
React.createElement(Box, { marginTop: 1 }),
|
|
228
|
+
commands.length === 0 ? (React.createElement(Text, { dimColor: true }, "No commands configured")) : (commands.map((cmd, index) => {
|
|
229
|
+
// Only render items in the visible viewport
|
|
230
|
+
if (index < adjustedStart || index >= endIndex) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return (React.createElement(Text, { key: cmd.id, color: selectedIndex === index
|
|
234
|
+
? 'cyan'
|
|
235
|
+
: cmd.enabled
|
|
236
|
+
? 'white'
|
|
237
|
+
: 'gray', bold: selectedIndex === index, dimColor: !cmd.enabled },
|
|
238
|
+
selectedIndex === index ? '❯ ' : ' ',
|
|
239
|
+
"[",
|
|
240
|
+
cmd.enabled ? '✓' : ' ',
|
|
241
|
+
"]",
|
|
242
|
+
' ',
|
|
243
|
+
cmd.pattern,
|
|
244
|
+
!cmd.isPreset && React.createElement(Text, { color: "yellow" }, " (custom)")));
|
|
245
|
+
})),
|
|
246
|
+
React.createElement(Box, { marginTop: 1 }),
|
|
247
|
+
selectedCmd && !confirmDelete && !confirmReset && (React.createElement(Text, { dimColor: true },
|
|
248
|
+
selectedCmd.description,
|
|
249
|
+
" (",
|
|
250
|
+
selectedCmd.enabled ? 'Enabled' : 'Disabled',
|
|
251
|
+
")",
|
|
252
|
+
!selectedCmd.isPreset && ' [Custom]')),
|
|
253
|
+
confirmDelete && selectedCmd && (React.createElement(Text, { bold: true, color: "yellow" },
|
|
254
|
+
"\u26A0\uFE0F Press D again to confirm deletion of \"",
|
|
255
|
+
selectedCmd.pattern,
|
|
256
|
+
"\"")),
|
|
257
|
+
confirmReset && (React.createElement(Text, { bold: true, color: "yellow" }, "\u26A0\uFE0F Press R again to confirm reset to default commands")),
|
|
258
|
+
React.createElement(Box, { marginTop: 1 }),
|
|
259
|
+
React.createElement(Text, { dimColor: true }, confirmDelete || confirmReset
|
|
260
|
+
? 'Press the same key again to confirm • Esc: Cancel'
|
|
261
|
+
: '↑↓: Navigate • Space: Toggle • A: Add • D: Delete • R: Reset • Esc: Back')));
|
|
262
|
+
}
|
|
@@ -353,7 +353,7 @@ export default function SubAgentConfigScreen({ onBack, onSave, inlineMode = fals
|
|
|
353
353
|
return (React.createElement(Box, { key: tool },
|
|
354
354
|
React.createElement(Text, { color: isCurrentTool ? 'cyan' : 'white', bold: isCurrentTool },
|
|
355
355
|
isCurrentTool ? '❯ ' : ' ',
|
|
356
|
-
selectedTools.has(tool) ? '
|
|
356
|
+
selectedTools.has(tool) ? '[✓]' : '[ ]',
|
|
357
357
|
" ",
|
|
358
358
|
tool)));
|
|
359
359
|
})))));
|
|
@@ -9,6 +9,7 @@ import ConfigScreen from './ConfigScreen.js';
|
|
|
9
9
|
import ProxyConfigScreen from './ProxyConfigScreen.js';
|
|
10
10
|
import SubAgentConfigScreen from './SubAgentConfigScreen.js';
|
|
11
11
|
import SubAgentListScreen from './SubAgentListScreen.js';
|
|
12
|
+
import SensitiveCommandConfigScreen from './SensitiveCommandConfigScreen.js';
|
|
12
13
|
export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
13
14
|
const [infoText, setInfoText] = useState('Start a new chat conversation');
|
|
14
15
|
const [inlineView, setInlineView] = useState('menu');
|
|
@@ -54,6 +55,11 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
|
54
55
|
value: 'subagent',
|
|
55
56
|
infoText: 'Configure sub-agents with custom tool permissions',
|
|
56
57
|
},
|
|
58
|
+
{
|
|
59
|
+
label: 'Sensitive Commands',
|
|
60
|
+
value: 'sensitive-commands',
|
|
61
|
+
infoText: 'Configure commands that require confirmation even in YOLO mode',
|
|
62
|
+
},
|
|
57
63
|
{
|
|
58
64
|
label: 'Exit',
|
|
59
65
|
value: 'exit',
|
|
@@ -75,6 +81,9 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
|
75
81
|
else if (value === 'subagent') {
|
|
76
82
|
setInlineView('subagent-list');
|
|
77
83
|
}
|
|
84
|
+
else if (value === 'sensitive-commands') {
|
|
85
|
+
setInlineView('sensitive-commands');
|
|
86
|
+
}
|
|
78
87
|
else {
|
|
79
88
|
// Pass through to parent for other actions (chat, exit, etc.)
|
|
80
89
|
onMenuSelect?.(value);
|
|
@@ -107,11 +116,11 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
|
107
116
|
const handler = setTimeout(() => {
|
|
108
117
|
stdout.write(ansiEscapes.clearTerminal);
|
|
109
118
|
setRemountKey(prev => prev + 1); // Force re-render
|
|
110
|
-
},
|
|
119
|
+
}, 200); // Add debounce delay to avoid rapid re-renders
|
|
111
120
|
return () => {
|
|
112
121
|
clearTimeout(handler);
|
|
113
122
|
};
|
|
114
|
-
}, [terminalWidth
|
|
123
|
+
}, [terminalWidth]); // Remove stdout from dependencies to avoid loops
|
|
115
124
|
return (React.createElement(Box, { flexDirection: "column", width: terminalWidth },
|
|
116
125
|
React.createElement(Static, { key: remountKey, items: [
|
|
117
126
|
React.createElement(Box, { key: "welcome-header", flexDirection: "row", paddingLeft: 2, paddingTop: 1, paddingBottom: 0, width: terminalWidth },
|
|
@@ -137,5 +146,7 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
|
137
146
|
inlineView === 'subagent-add' && (React.createElement(Box, { paddingX: 1 },
|
|
138
147
|
React.createElement(SubAgentConfigScreen, { onBack: () => setInlineView('subagent-list'), onSave: handleSubAgentSave, inlineMode: true }))),
|
|
139
148
|
inlineView === 'subagent-edit' && (React.createElement(Box, { paddingX: 1 },
|
|
140
|
-
React.createElement(SubAgentConfigScreen, { onBack: () => setInlineView('subagent-list'), onSave: handleSubAgentSave, agentId: editingAgentId, inlineMode: true })))
|
|
149
|
+
React.createElement(SubAgentConfigScreen, { onBack: () => setInlineView('subagent-list'), onSave: handleSubAgentSave, agentId: editingAgentId, inlineMode: true }))),
|
|
150
|
+
inlineView === 'sensitive-commands' && (React.createElement(Box, { paddingX: 1 },
|
|
151
|
+
React.createElement(SensitiveCommandConfigScreen, { onBack: handleBackToMenu, inlineMode: true })))));
|
|
141
152
|
}
|