aegiscode 3.1.8 → 3.1.10
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/README.md +23 -15
- package/dist/main.js +549 -552
- package/package.json +4 -2
- package/src/agent/Agent.ts +903 -0
- package/src/agent/SimpleAgent.ts +48 -0
- package/src/agent/index.ts +54 -0
- package/src/agent/orchestrator/AppBuilder.ts +443 -0
- package/src/agent/orchestrator/CouncilAgent.ts +310 -0
- package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
- package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
- package/src/agent/orchestrator/index.ts +38 -0
- package/src/agent/orchestrator/utils.ts +397 -0
- package/src/agent/pricing.ts +115 -0
- package/src/agent/router.ts +74 -0
- package/src/agent/routerStats.ts +121 -0
- package/src/agent/types.ts +318 -0
- package/src/auth/login.ts +383 -0
- package/src/cli/config.ts +189 -0
- package/src/cli/index.ts +17 -0
- package/src/cli/middleware.ts +119 -0
- package/src/cli/types.ts +75 -0
- package/src/config/ConfigManager.ts +587 -0
- package/src/config/index.ts +7 -0
- package/src/config/types.ts +584 -0
- package/src/context/CompactionService.ts +300 -0
- package/src/context/ContextManager.ts +450 -0
- package/src/context/FileAnalyzer.ts +267 -0
- package/src/context/TokenCounter.ts +265 -0
- package/src/context/index.ts +27 -0
- package/src/context/storage/CacheStore.ts +176 -0
- package/src/context/storage/JSONLStore.ts +201 -0
- package/src/context/storage/MemoryStore.ts +205 -0
- package/src/context/storage/PersistentStore.ts +327 -0
- package/src/context/storage/index.ts +9 -0
- package/src/context/storage/pathUtils.ts +114 -0
- package/src/context/test.ts +309 -0
- package/src/context/types.ts +268 -0
- package/src/hooks/HookExecutor.ts +434 -0
- package/src/hooks/HookManager.ts +596 -0
- package/src/hooks/HookService.ts +269 -0
- package/src/hooks/Matcher.ts +157 -0
- package/src/hooks/index.ts +63 -0
- package/src/hooks/types.ts +424 -0
- package/src/main.tsx +596 -0
- package/src/mcp/HealthMonitor.ts +150 -0
- package/src/mcp/McpClient.ts +491 -0
- package/src/mcp/McpRegistry.ts +321 -0
- package/src/mcp/createMcpTool.ts +251 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/server.ts +334 -0
- package/src/mcp/test-server.ts +88 -0
- package/src/mcp/test.ts +372 -0
- package/src/mcp/types.ts +247 -0
- package/src/memory/AgentMemoryBus.ts +432 -0
- package/src/memory/CloudSync.ts +99 -0
- package/src/memory/DriveSync.ts +106 -0
- package/src/memory/SharedMemory.ts +951 -0
- package/src/memory/index.ts +14 -0
- package/src/memory/machineFingerprint.ts +40 -0
- package/src/orchestrator/SubAgentMetadata.ts +136 -0
- package/src/prompts/builder.ts +213 -0
- package/src/prompts/default.ts +144 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/plan.ts +64 -0
- package/src/prompts/test.ts +78 -0
- package/src/services/AnthropicChatService.ts +341 -0
- package/src/services/ChatService.ts +347 -0
- package/src/services/ClaudeCliChatService.ts +256 -0
- package/src/services/CloudSync.ts +168 -0
- package/src/services/CostLedger.ts +211 -0
- package/src/services/Heartbeat.ts +135 -0
- package/src/services/LearningCollector.ts +291 -0
- package/src/services/OllamaInstaller.ts +342 -0
- package/src/services/VersionChecker.ts +445 -0
- package/src/services/index.ts +57 -0
- package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
- package/src/services/streaming/RenderingProfile.ts +90 -0
- package/src/services/streaming/StreamEventParser.ts +181 -0
- package/src/services/streaming/ThrottledRenderer.ts +139 -0
- package/src/services/streaming/TranscriptBuffer.ts +574 -0
- package/src/services/streaming/eventStatusMap.ts +52 -0
- package/src/services/streaming/index.ts +46 -0
- package/src/services/streaming/renderFormatting.ts +79 -0
- package/src/services/streaming/types.ts +234 -0
- package/src/skills/SkillLoader.ts +126 -0
- package/src/skills/SkillRegistry.ts +366 -0
- package/src/skills/index.ts +48 -0
- package/src/skills/types.ts +146 -0
- package/src/slash-commands/billing.ts +70 -0
- package/src/slash-commands/build.ts +413 -0
- package/src/slash-commands/builtinCommands.ts +2733 -0
- package/src/slash-commands/clone.ts +242 -0
- package/src/slash-commands/council.ts +125 -0
- package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
- package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
- package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
- package/src/slash-commands/custom/index.ts +7 -0
- package/src/slash-commands/debate.ts +254 -0
- package/src/slash-commands/gmail.ts +105 -0
- package/src/slash-commands/index.ts +388 -0
- package/src/slash-commands/mcpCommand.ts +205 -0
- package/src/slash-commands/types.ts +201 -0
- package/src/store/index.ts +76 -0
- package/src/store/selectors.ts +246 -0
- package/src/store/slices/appSlice.ts +205 -0
- package/src/store/slices/commandSlice.ts +115 -0
- package/src/store/slices/configSlice.ts +46 -0
- package/src/store/slices/focusSlice.ts +64 -0
- package/src/store/slices/index.ts +9 -0
- package/src/store/slices/sessionSlice.ts +424 -0
- package/src/store/streaming-buffer.ts +425 -0
- package/src/store/test.ts +296 -0
- package/src/store/types.ts +274 -0
- package/src/store/vanilla.ts +186 -0
- package/src/tools/builtin/bash.ts +236 -0
- package/src/tools/builtin/council.ts +105 -0
- package/src/tools/builtin/edit.ts +213 -0
- package/src/tools/builtin/glob.ts +136 -0
- package/src/tools/builtin/grep.ts +263 -0
- package/src/tools/builtin/index.ts +61 -0
- package/src/tools/builtin/memory.ts +66 -0
- package/src/tools/builtin/read.ts +168 -0
- package/src/tools/builtin/skill.ts +97 -0
- package/src/tools/builtin/snapshot.ts +40 -0
- package/src/tools/builtin/task.ts +106 -0
- package/src/tools/builtin/write.ts +134 -0
- package/src/tools/createTool.ts +221 -0
- package/src/tools/execution/ExecutionPipeline.ts +263 -0
- package/src/tools/execution/index.ts +40 -0
- package/src/tools/execution/stages/CacheStage.ts +131 -0
- package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
- package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
- package/src/tools/execution/stages/ExecutionStage.ts +48 -0
- package/src/tools/execution/stages/FormattingStage.ts +44 -0
- package/src/tools/execution/stages/HookStage.ts +72 -0
- package/src/tools/execution/stages/PermissionStage.ts +287 -0
- package/src/tools/execution/stages/PostHookStage.ts +71 -0
- package/src/tools/execution/stages/index.ts +12 -0
- package/src/tools/execution/test.ts +266 -0
- package/src/tools/execution/types.ts +273 -0
- package/src/tools/index.ts +81 -0
- package/src/tools/registry.ts +304 -0
- package/src/tools/schemas.ts +109 -0
- package/src/tools/test.ts +220 -0
- package/src/tools/types.ts +175 -0
- package/src/tools/validation/PermissionChecker.ts +242 -0
- package/src/tools/validation/SensitiveFileDetector.ts +210 -0
- package/src/tools/validation/index.ts +11 -0
- package/src/ui/App.tsx +166 -0
- package/src/ui/components/AegisInterface.tsx +484 -0
- package/src/ui/components/common/ChatSearch.tsx +150 -0
- package/src/ui/components/common/ErrorBoundary.tsx +82 -0
- package/src/ui/components/common/ExitMessage.tsx +120 -0
- package/src/ui/components/common/LoadingIndicator.tsx +49 -0
- package/src/ui/components/common/index.ts +6 -0
- package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
- package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
- package/src/ui/components/dialog/SetupWizard.tsx +297 -0
- package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
- package/src/ui/components/dialog/index.ts +8 -0
- package/src/ui/components/index.ts +28 -0
- package/src/ui/components/input/CommandSuggestions.tsx +139 -0
- package/src/ui/components/input/CustomTextInput.tsx +220 -0
- package/src/ui/components/input/InputArea.tsx +361 -0
- package/src/ui/components/input/PromptSuggestions.tsx +66 -0
- package/src/ui/components/input/index.ts +6 -0
- package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
- package/src/ui/components/layout/ContextBar.tsx +79 -0
- package/src/ui/components/layout/MessageArea.tsx +96 -0
- package/src/ui/components/layout/MessageList.tsx +647 -0
- package/src/ui/components/layout/MessageSeparator.tsx +26 -0
- package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
- package/src/ui/components/layout/index.ts +7 -0
- package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
- package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
- package/src/ui/components/markdown/index.ts +8 -0
- package/src/ui/components/markdown/parser.ts +336 -0
- package/src/ui/components/markdown/types.ts +66 -0
- package/src/ui/focus/FocusManager.ts +137 -0
- package/src/ui/focus/index.ts +13 -0
- package/src/ui/focus/types.ts +54 -0
- package/src/ui/focus/useFocus.ts +75 -0
- package/src/ui/hooks/index.ts +11 -0
- package/src/ui/hooks/useAgent.ts +284 -0
- package/src/ui/hooks/useCommandHistory.ts +87 -0
- package/src/ui/hooks/useCommandProcessor.ts +443 -0
- package/src/ui/hooks/useConfirmation.ts +99 -0
- package/src/ui/hooks/useCtrlCHandler.ts +100 -0
- package/src/ui/hooks/useInputBuffer.ts +122 -0
- package/src/ui/hooks/useTerminalSize.ts +68 -0
- package/src/ui/hooks/useTerminalWidth.ts +5 -0
- package/src/ui/hooks/useWindowedList.ts +118 -0
- package/src/ui/render-debugger.ts +621 -0
- package/src/ui/test.ts +189 -0
- package/src/ui/themes/ThemeManager.ts +332 -0
- package/src/ui/themes/aegisTheme.ts +87 -0
- package/src/ui/themes/darkTheme.ts +87 -0
- package/src/ui/themes/defaultTheme.ts +85 -0
- package/src/ui/themes/index.ts +10 -0
- package/src/ui/themes/lightTheme.ts +89 -0
- package/src/ui/themes/popularThemes.ts +187 -0
- package/src/ui/themes/types.ts +130 -0
- package/src/utils/clipboard.ts +48 -0
- package/src/utils/debug.ts +43 -0
- package/src/utils/environment.ts +68 -0
- package/src/utils/index.ts +10 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React, { useMemo, useCallback, useRef, memo } from 'react';
|
|
2
|
+
import { Text, useInput } from 'ink';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { useIsFocused, FocusId, focusManager } from '../../focus/index.js';
|
|
5
|
+
|
|
6
|
+
interface CustomTextInputProps {
|
|
7
|
+
value: string;
|
|
8
|
+
cursorPosition: number;
|
|
9
|
+
/** Cursor visibility — computed by parent so this component doesn't re-render every tick */
|
|
10
|
+
cursorOn?: boolean;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
onChangeCursorPosition: (pos: number) => void;
|
|
13
|
+
onSubmit?: (value: string) => void;
|
|
14
|
+
onPaste?: (text: string) => { prompt?: string } | void;
|
|
15
|
+
onArrowUp?: () => void;
|
|
16
|
+
onArrowDown?: () => void;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
focusId?: FocusId;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PASTE_CONFIG = {
|
|
23
|
+
TIMEOUT_MS: 100,
|
|
24
|
+
RAPID_INPUT_THRESHOLD_MS: 400,
|
|
25
|
+
LARGE_INPUT_THRESHOLD: 100,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const CustomTextInput: React.FC<CustomTextInputProps> = memo(
|
|
29
|
+
({
|
|
30
|
+
value,
|
|
31
|
+
cursorPosition,
|
|
32
|
+
onChange,
|
|
33
|
+
onChangeCursorPosition,
|
|
34
|
+
onSubmit,
|
|
35
|
+
onPaste,
|
|
36
|
+
onArrowUp,
|
|
37
|
+
onArrowDown,
|
|
38
|
+
placeholder = '',
|
|
39
|
+
focusId = FocusId.MAIN_INPUT,
|
|
40
|
+
disabled = false,
|
|
41
|
+
cursorOn = true,
|
|
42
|
+
}) => {
|
|
43
|
+
const isFocused = useIsFocused(focusId);
|
|
44
|
+
const isActive = isFocused && !disabled;
|
|
45
|
+
|
|
46
|
+
// Refs for all values accessed inside the stable useInput handler.
|
|
47
|
+
// Updated on every render — never trigger re-renders themselves.
|
|
48
|
+
const valueRef = useRef(value);
|
|
49
|
+
const cursorPositionRef = useRef(cursorPosition);
|
|
50
|
+
const onChangeRef = useRef(onChange);
|
|
51
|
+
const onChangeCursorPositionRef = useRef(onChangeCursorPosition);
|
|
52
|
+
const onSubmitRef = useRef(onSubmit);
|
|
53
|
+
const onPasteRef = useRef(onPaste);
|
|
54
|
+
const onArrowUpRef = useRef(onArrowUp);
|
|
55
|
+
const onArrowDownRef = useRef(onArrowDown);
|
|
56
|
+
const isActiveRef = useRef(isActive);
|
|
57
|
+
|
|
58
|
+
valueRef.current = value;
|
|
59
|
+
cursorPositionRef.current = cursorPosition;
|
|
60
|
+
onChangeRef.current = onChange;
|
|
61
|
+
onChangeCursorPositionRef.current = onChangeCursorPosition;
|
|
62
|
+
onSubmitRef.current = onSubmit;
|
|
63
|
+
onPasteRef.current = onPaste;
|
|
64
|
+
onArrowUpRef.current = onArrowUp;
|
|
65
|
+
onArrowDownRef.current = onArrowDown;
|
|
66
|
+
isActiveRef.current = isActive;
|
|
67
|
+
|
|
68
|
+
const renderedValue = useMemo(() => {
|
|
69
|
+
if (!isActive) return value || chalk.dim(placeholder);
|
|
70
|
+
|
|
71
|
+
if (value.length === 0) {
|
|
72
|
+
const block = cursorOn
|
|
73
|
+
? chalk.bgHex('#00c8b4').hex('#000000')(' ')
|
|
74
|
+
: ' ';
|
|
75
|
+
return block + chalk.dim(placeholder);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const before = value.slice(0, cursorPosition);
|
|
79
|
+
const charUnder = value[cursorPosition] ?? ' ';
|
|
80
|
+
const after = value.slice(cursorPosition + 1);
|
|
81
|
+
|
|
82
|
+
const cursorChar = cursorOn
|
|
83
|
+
? chalk.bgHex('#00c8b4').hex('#000000')(charUnder)
|
|
84
|
+
: charUnder;
|
|
85
|
+
|
|
86
|
+
return before + cursorChar + after;
|
|
87
|
+
}, [value, cursorPosition, isActive, placeholder, cursorOn]);
|
|
88
|
+
|
|
89
|
+
const pasteStateRef = useRef({
|
|
90
|
+
chunks: [] as string[],
|
|
91
|
+
timeoutId: null as NodeJS.Timeout | null,
|
|
92
|
+
firstInputTime: null as number | null,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Stable — accesses all mutable values via refs
|
|
96
|
+
const handlePasteStable = useCallback((text: string) => {
|
|
97
|
+
const val = valueRef.current;
|
|
98
|
+
const pos = cursorPositionRef.current;
|
|
99
|
+
// Claude Code behavior: trim trailing whitespace from pasted text
|
|
100
|
+
const cleanText = text.trimEnd();
|
|
101
|
+
|
|
102
|
+
if (onPasteRef.current) {
|
|
103
|
+
const result = onPasteRef.current(text);
|
|
104
|
+
if (result?.prompt) {
|
|
105
|
+
onChangeRef.current(val + result.prompt);
|
|
106
|
+
onChangeCursorPositionRef.current(val.length + result.prompt.length);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const newValue = val.slice(0, pos) + cleanText + val.slice(pos);
|
|
111
|
+
onChangeRef.current(newValue);
|
|
112
|
+
onChangeCursorPositionRef.current(pos + cleanText.length);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
// Stable — Ink registers this once and never re-registers unless focusId changes.
|
|
116
|
+
// All mutable state is read from refs at call time.
|
|
117
|
+
const inputHandler = useCallback(
|
|
118
|
+
(input: string, key: Parameters<Parameters<typeof useInput>[0]>[1]) => {
|
|
119
|
+
if (focusManager.getCurrentFocus() !== focusId || !isActiveRef.current) return;
|
|
120
|
+
|
|
121
|
+
const val = valueRef.current;
|
|
122
|
+
const pos = cursorPositionRef.current;
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const pasteState = pasteStateRef.current;
|
|
125
|
+
const timeSinceFirst = pasteState.firstInputTime
|
|
126
|
+
? now - pasteState.firstInputTime
|
|
127
|
+
: 0;
|
|
128
|
+
|
|
129
|
+
const isPaste =
|
|
130
|
+
input.length > PASTE_CONFIG.LARGE_INPUT_THRESHOLD ||
|
|
131
|
+
input.includes('\n') ||
|
|
132
|
+
(timeSinceFirst < PASTE_CONFIG.RAPID_INPUT_THRESHOLD_MS &&
|
|
133
|
+
pasteState.chunks.length > 0);
|
|
134
|
+
|
|
135
|
+
if (isPaste && input.length > 1) {
|
|
136
|
+
pasteState.chunks.push(input);
|
|
137
|
+
if (!pasteState.firstInputTime) pasteState.firstInputTime = now;
|
|
138
|
+
if (pasteState.timeoutId) clearTimeout(pasteState.timeoutId);
|
|
139
|
+
pasteState.timeoutId = setTimeout(() => {
|
|
140
|
+
const mergedText = pasteState.chunks.join('');
|
|
141
|
+
handlePasteStable(mergedText);
|
|
142
|
+
pasteState.chunks = [];
|
|
143
|
+
pasteState.timeoutId = null;
|
|
144
|
+
pasteState.firstInputTime = null;
|
|
145
|
+
}, PASTE_CONFIG.TIMEOUT_MS);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Multi-agent commands (/multi, /multiyolo, /research, /build, /forge, /council):
|
|
150
|
+
// Enter inserts newline, Shift+Enter submits — enables multi-line prompts.
|
|
151
|
+
// All other commands/messages: Enter submits immediately (as before).
|
|
152
|
+
const MULTI_AGENT_CMDS = ['/multi', '/multiyolo', '/research', '/build', '/forge', '/council'];
|
|
153
|
+
const isMultiAgent = MULTI_AGENT_CMDS.some(cmd => val.trimStart().startsWith(cmd));
|
|
154
|
+
if (key.return) {
|
|
155
|
+
if (isMultiAgent && !key.shift) {
|
|
156
|
+
// Insert newline at cursor
|
|
157
|
+
const newValue = val.slice(0, pos) + '\n' + val.slice(pos);
|
|
158
|
+
onChangeRef.current(newValue);
|
|
159
|
+
onChangeCursorPositionRef.current(pos + 1);
|
|
160
|
+
} else {
|
|
161
|
+
onSubmitRef.current?.(val);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (key.leftArrow) { if (pos > 0) onChangeCursorPositionRef.current(pos - 1); return; }
|
|
166
|
+
if (key.rightArrow) { if (pos < val.length) onChangeCursorPositionRef.current(pos + 1); return; }
|
|
167
|
+
if (key.upArrow) { onArrowUpRef.current?.(); return; }
|
|
168
|
+
if (key.downArrow) { onArrowDownRef.current?.(); return; }
|
|
169
|
+
if (key.backspace || key.delete) {
|
|
170
|
+
if (pos > 0) {
|
|
171
|
+
const newValue = val.slice(0, pos - 1) + val.slice(pos);
|
|
172
|
+
onChangeRef.current(newValue);
|
|
173
|
+
onChangeCursorPositionRef.current(pos - 1);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (key.ctrl && input === 'a') { onChangeCursorPositionRef.current(0); return; }
|
|
178
|
+
if (key.ctrl && input === 'e') { onChangeCursorPositionRef.current(val.length); return; }
|
|
179
|
+
if (key.ctrl && input === 'k') { onChangeRef.current(val.slice(0, pos)); return; }
|
|
180
|
+
if (key.ctrl && input === 'u') {
|
|
181
|
+
onChangeRef.current(val.slice(pos));
|
|
182
|
+
onChangeCursorPositionRef.current(0);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (input && !key.ctrl && !key.meta) {
|
|
186
|
+
const newValue = val.slice(0, pos) + input + val.slice(pos);
|
|
187
|
+
onChangeRef.current(newValue);
|
|
188
|
+
onChangeCursorPositionRef.current(pos + input.length);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
[focusId, handlePasteStable] // both stable — focusId is a constant, handlePasteStable has empty deps
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
useInput(inputHandler, { isActive });
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Text>
|
|
198
|
+
{!isActive && value.length === 0 ? (
|
|
199
|
+
<Text dimColor>{placeholder}</Text>
|
|
200
|
+
) : (
|
|
201
|
+
renderedValue
|
|
202
|
+
)}
|
|
203
|
+
</Text>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
// Only re-render when the displayed output would actually change.
|
|
207
|
+
// Callbacks are accessed via refs so they never need to cause a re-render.
|
|
208
|
+
// focus changes trigger re-renders via useIsFocused internally (bypasses this comparator).
|
|
209
|
+
(prev, next) =>
|
|
210
|
+
prev.value === next.value &&
|
|
211
|
+
prev.cursorPosition === next.cursorPosition &&
|
|
212
|
+
prev.cursorOn === next.cursorOn &&
|
|
213
|
+
prev.placeholder === next.placeholder &&
|
|
214
|
+
prev.disabled === next.disabled &&
|
|
215
|
+
prev.focusId === next.focusId
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
CustomTextInput.displayName = 'CustomTextInput';
|
|
219
|
+
|
|
220
|
+
export default CustomTextInput;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputArea - 输入区域组件
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useCallback, useState, useMemo, useEffect, useRef, memo } from 'react';
|
|
9
|
+
import { Box, Text, useInput } from 'ink';
|
|
10
|
+
|
|
11
|
+
const PromptGlyph: React.FC<{ isProcessing: boolean; lockedIn: boolean; glowPhase: number; color: string; idleColor: string }> = memo(({ isProcessing, lockedIn, glowPhase, color, idleColor }) => {
|
|
12
|
+
const displayColor = (isProcessing && lockedIn)
|
|
13
|
+
? ELECTRIC_COLORS[glowPhase % ELECTRIC_COLORS.length]
|
|
14
|
+
: idleColor;
|
|
15
|
+
return <Text color={displayColor} bold>□</Text>;
|
|
16
|
+
});
|
|
17
|
+
PromptGlyph.displayName = 'PromptGlyph';
|
|
18
|
+
import { CustomTextInput } from './CustomTextInput.js';
|
|
19
|
+
import { CommandSuggestions } from './CommandSuggestions.js';
|
|
20
|
+
|
|
21
|
+
import { themeManager } from '../../themes/index.js';
|
|
22
|
+
import { FocusId, focusManager } from '../../focus/index.js';
|
|
23
|
+
import { getState, useIsThinking, usePendingCommands, useCurrentCommand } from '../../../store/index.js';
|
|
24
|
+
import { vanillaStore } from '../../../store/vanilla.js';
|
|
25
|
+
import { getStreamingContent } from '../../../store/streaming-buffer.js';
|
|
26
|
+
|
|
27
|
+
// Electric blue color ramp — cycles fast to simulate voltage/energy
|
|
28
|
+
const ELECTRIC_COLORS = [
|
|
29
|
+
'#0055FF', '#0077FF', '#0099FF', '#00AAFF',
|
|
30
|
+
'#00CCFF', '#00EEFF', '#00FFFF', '#00EEFF',
|
|
31
|
+
'#00CCFF', '#00AAFF', '#0099FF', '#0077FF',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
interface InputAreaProps {
|
|
35
|
+
/** 提交回调 */
|
|
36
|
+
onSubmit?: (value: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
*
|
|
42
|
+
*/
|
|
43
|
+
export const InputArea: React.FC<InputAreaProps> = React.memo(
|
|
44
|
+
({
|
|
45
|
+
onSubmit,
|
|
46
|
+
}) => {
|
|
47
|
+
// 使用 ref 保持回调引用稳
|
|
48
|
+
const onSubmitRef = useRef(onSubmit);
|
|
49
|
+
|
|
50
|
+
// 更新 ref(不触发重新渲
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
onSubmitRef.current = onSubmit;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 自管理命令历史(使用 ref 避免状态变化导致重新渲染传播到父组
|
|
56
|
+
const historyRef = useRef<string[]>([]);
|
|
57
|
+
const historyIndexRef = useRef(-1);
|
|
58
|
+
|
|
59
|
+
const addToHistory = useCallback((command: string) => {
|
|
60
|
+
if (command.trim()) {
|
|
61
|
+
const history = historyRef.current;
|
|
62
|
+
if (history[history.length - 1] !== command) {
|
|
63
|
+
history.push(command);
|
|
64
|
+
if (history.length > 100) history.shift();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
historyIndexRef.current = -1;
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const getPreviousCommand = useCallback(() => {
|
|
71
|
+
const history = historyRef.current;
|
|
72
|
+
if (history.length === 0) return null;
|
|
73
|
+
if (historyIndexRef.current < history.length - 1) {
|
|
74
|
+
historyIndexRef.current++;
|
|
75
|
+
return history[history.length - 1 - historyIndexRef.current];
|
|
76
|
+
}
|
|
77
|
+
return history[0];
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const getNextCommand = useCallback(() => {
|
|
81
|
+
if (historyIndexRef.current > 0) {
|
|
82
|
+
historyIndexRef.current--;
|
|
83
|
+
return historyRef.current[historyRef.current.length - 1 - historyIndexRef.current];
|
|
84
|
+
}
|
|
85
|
+
historyIndexRef.current = -1;
|
|
86
|
+
return '';
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const theme = themeManager.getTheme();
|
|
90
|
+
|
|
91
|
+
// 自己订阅需要的状
|
|
92
|
+
const isProcessing = useIsThinking();
|
|
93
|
+
const pendingCommands = usePendingCommands();
|
|
94
|
+
const currentCommand = useCurrentCommand();
|
|
95
|
+
|
|
96
|
+
const [pasteNotification, setPasteNotification] = useState<string | null>(null);
|
|
97
|
+
const pasteNotifRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
98
|
+
|
|
99
|
+
const [thinkingSeconds, setThinkingSeconds] = useState(0);
|
|
100
|
+
const [streamingTokens, setStreamingTokens] = useState(0);
|
|
101
|
+
const thinkingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (isProcessing) {
|
|
104
|
+
setThinkingSeconds(0);
|
|
105
|
+
setStreamingTokens(0);
|
|
106
|
+
thinkingTimerRef.current = setInterval(() => {
|
|
107
|
+
setThinkingSeconds(s => s + 1);
|
|
108
|
+
const buf = getStreamingContent();
|
|
109
|
+
if (buf) setStreamingTokens(Math.ceil(buf.content.length / 4));
|
|
110
|
+
}, 1000);
|
|
111
|
+
} else {
|
|
112
|
+
if (thinkingTimerRef.current) { clearInterval(thinkingTimerRef.current); thinkingTimerRef.current = null; }
|
|
113
|
+
setThinkingSeconds(0);
|
|
114
|
+
setStreamingTokens(0);
|
|
115
|
+
}
|
|
116
|
+
return () => { if (thinkingTimerRef.current) { clearInterval(thinkingTimerRef.current); thinkingTimerRef.current = null; } };
|
|
117
|
+
}, [isProcessing]);
|
|
118
|
+
|
|
119
|
+
const [hasStreamingMessage, setHasStreamingMessage] = useState(
|
|
120
|
+
() => getState().session.messages.some(m => m.isStreaming)
|
|
121
|
+
);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
let prev = hasStreamingMessage;
|
|
124
|
+
const unsubscribe = vanillaStore.subscribe((state) => {
|
|
125
|
+
const msgs = state.session.messages;
|
|
126
|
+
const newVal = msgs[msgs.length - 1]?.isStreaming ?? false;
|
|
127
|
+
if (newVal !== prev) {
|
|
128
|
+
prev = newVal;
|
|
129
|
+
setHasStreamingMessage(newVal);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return unsubscribe;
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
// Electric glow animation — 200ms ticks, only active while processing
|
|
136
|
+
const [glowPhase, setGlowPhase] = useState(0);
|
|
137
|
+
const glowTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (isProcessing) {
|
|
140
|
+
setGlowPhase(0);
|
|
141
|
+
glowTimerRef.current = setInterval(() => setGlowPhase(p => p + 1), 150);
|
|
142
|
+
} else {
|
|
143
|
+
if (glowTimerRef.current) { clearInterval(glowTimerRef.current); glowTimerRef.current = null; }
|
|
144
|
+
setGlowPhase(0);
|
|
145
|
+
}
|
|
146
|
+
return () => { if (glowTimerRef.current) { clearInterval(glowTimerRef.current); glowTimerRef.current = null; } };
|
|
147
|
+
}, [isProcessing]);
|
|
148
|
+
|
|
149
|
+
// Cursor blink at idle (530 ms on/off), piggybacks on glowPhase during generation
|
|
150
|
+
const [idleBlink, setIdleBlink] = useState(true);
|
|
151
|
+
const idleBlinkRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!isProcessing) {
|
|
154
|
+
setIdleBlink(true);
|
|
155
|
+
idleBlinkRef.current = setInterval(() => setIdleBlink(v => !v), 530);
|
|
156
|
+
} else {
|
|
157
|
+
if (idleBlinkRef.current) { clearInterval(idleBlinkRef.current); idleBlinkRef.current = null; }
|
|
158
|
+
setIdleBlink(true);
|
|
159
|
+
}
|
|
160
|
+
return () => { if (idleBlinkRef.current) { clearInterval(idleBlinkRef.current); idleBlinkRef.current = null; } };
|
|
161
|
+
}, [isProcessing]);
|
|
162
|
+
const cursorOn = isProcessing ? Math.floor(glowPhase / 5) % 2 === 0 : idleBlink;
|
|
163
|
+
|
|
164
|
+
const thinkingLabel = useMemo(() => {
|
|
165
|
+
if (!isProcessing) return null;
|
|
166
|
+
const cmd = currentCommand?.slice(0, 80);
|
|
167
|
+
const cmdStr = cmd ? ` · locked: ${cmd}` : '';
|
|
168
|
+
const elapsed = thinkingSeconds >= 2 ? ` · ${thinkingSeconds}s` : '';
|
|
169
|
+
const tokens = streamingTokens > 0 ? ` · ~${streamingTokens}t` : '';
|
|
170
|
+
if (hasStreamingMessage) return `generating${tokens}${elapsed}${cmdStr}`;
|
|
171
|
+
return `thinking${elapsed}${cmdStr}`;
|
|
172
|
+
}, [isProcessing, hasStreamingMessage, thinkingSeconds, streamingTokens, currentCommand]);
|
|
173
|
+
|
|
174
|
+
// 计
|
|
175
|
+
const placeholder = useMemo(() => {
|
|
176
|
+
if (isProcessing) {
|
|
177
|
+
return pendingCommands.length > 0
|
|
178
|
+
? `Queued: ${pendingCommands.length} command(s). Type to add more...`
|
|
179
|
+
: 'Processing... Type to queue next command';
|
|
180
|
+
}
|
|
181
|
+
return 'Type a message...';
|
|
182
|
+
}, [isProcessing, pendingCommands.length]);
|
|
183
|
+
|
|
184
|
+
// 自管理的输入状
|
|
185
|
+
const [input, setInput] = useState('');
|
|
186
|
+
const [cursorPosition, setCursorPosition] = useState(0);
|
|
187
|
+
const inputRef = useRef({ value: '', cursorPosition: 0 });
|
|
188
|
+
|
|
189
|
+
// 更新 ref 保持同
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
inputRef.current = { value: input, cursorPosition };
|
|
192
|
+
}, [input, cursorPosition]);
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// 设置输入
|
|
197
|
+
const handleChange = useCallback((newValue: string) => {
|
|
198
|
+
inputRef.current.value = newValue;
|
|
199
|
+
setInput(newValue);
|
|
200
|
+
// cursor is always set immediately after via onChangeCursorPosition — no guard needed
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
// 设置光标位
|
|
204
|
+
const handleChangeCursorPosition = useCallback((pos: number) => {
|
|
205
|
+
const newPos = Math.max(0, Math.min(pos, inputRef.current.value.length));
|
|
206
|
+
inputRef.current.cursorPosition = newPos;
|
|
207
|
+
setCursorPosition(newPos);
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
const showSuggestions = input.startsWith('/') && input.length > 0 && !isProcessing;
|
|
211
|
+
const showPromptSuggestions = false; // Suggestions disabled
|
|
212
|
+
|
|
213
|
+
// 选择建议回
|
|
214
|
+
const handleSelectSuggestion = useCallback((newValue: string) => {
|
|
215
|
+
handleChange(newValue);
|
|
216
|
+
handleChangeCursorPosition(newValue.length);
|
|
217
|
+
}, [handleChange, handleChangeCursorPosition]);
|
|
218
|
+
|
|
219
|
+
// 清空输
|
|
220
|
+
const clearInput = useCallback(() => {
|
|
221
|
+
setInput('');
|
|
222
|
+
setCursorPosition(0);
|
|
223
|
+
inputRef.current = { value: '', cursorPosition: 0 };
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
// Paste — mimic Claude Code: show paste size notification
|
|
229
|
+
// Trimming is handled in CustomTextInput.tsx's handlePasteStable
|
|
230
|
+
const handlePaste = useCallback((text: string) => {
|
|
231
|
+
const lines = text.split('\n').length;
|
|
232
|
+
const chars = text.length;
|
|
233
|
+
|
|
234
|
+
if (lines > 1 || chars > 200) {
|
|
235
|
+
const msg = lines > 1 ? `pasted ${lines} lines (${chars} chars)` : `pasted ${chars} chars`;
|
|
236
|
+
setPasteNotification(msg);
|
|
237
|
+
if (pasteNotifRef.current) clearTimeout(pasteNotifRef.current);
|
|
238
|
+
pasteNotifRef.current = setTimeout(() => setPasteNotification(null), 2000);
|
|
239
|
+
}
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
// 提交处
|
|
243
|
+
const handleSubmit = useCallback(
|
|
244
|
+
(_value: string) => {
|
|
245
|
+
// Read from ref, not prop: the prop lags until React re-renders, so holding
|
|
246
|
+
// Enter fires multiple submits with the same stale value. The ref is
|
|
247
|
+
// pre-cleared here so any queued handler calls before re-render bail out.
|
|
248
|
+
const current = inputRef.current.value;
|
|
249
|
+
if (current.trim() && onSubmitRef.current) {
|
|
250
|
+
inputRef.current = { value: '', cursorPosition: 0 }; // guard before any async
|
|
251
|
+
addToHistory(current);
|
|
252
|
+
clearInput(); // queues setInput('') for React
|
|
253
|
+
onSubmitRef.current(current);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
[clearInput, addToHistory]
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// 处理上下箭
|
|
260
|
+
const handleArrowUpInternal = useCallback(() => {
|
|
261
|
+
const prevCmd = getPreviousCommand();
|
|
262
|
+
if (prevCmd !== null && prevCmd !== undefined) {
|
|
263
|
+
handleChange(prevCmd);
|
|
264
|
+
handleChangeCursorPosition(prevCmd.length);
|
|
265
|
+
}
|
|
266
|
+
}, [handleChange, handleChangeCursorPosition, getPreviousCommand]);
|
|
267
|
+
|
|
268
|
+
const handleArrowDownInternal = useCallback(() => {
|
|
269
|
+
const nextCmd = getNextCommand();
|
|
270
|
+
if (nextCmd !== null && nextCmd !== undefined) {
|
|
271
|
+
handleChange(nextCmd);
|
|
272
|
+
handleChangeCursorPosition(nextCmd.length);
|
|
273
|
+
}
|
|
274
|
+
}, [handleChange, handleChangeCursorPosition, getNextCommand]);
|
|
275
|
+
|
|
276
|
+
// 处理 Tab — 由 CommandSuggestions 接管
|
|
277
|
+
useInput((_char, key) => {
|
|
278
|
+
if (focusManager.getCurrentFocus() !== FocusId.MAIN_INPUT) return;
|
|
279
|
+
// Tab is handled by CommandSuggestions; just swallow it here
|
|
280
|
+
// to prevent default browser-like behavior
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<Box flexDirection="column">
|
|
286
|
+
{/* Paste notification — Claude Code style: minimal, brief */}
|
|
287
|
+
{pasteNotification && (
|
|
288
|
+
<Box paddingX={0} marginBottom={0}>
|
|
289
|
+
<Text color={theme.colors.text.muted} dimColor>
|
|
290
|
+
{pasteNotification}
|
|
291
|
+
</Text>
|
|
292
|
+
</Box>
|
|
293
|
+
)}
|
|
294
|
+
{/* Thinking indicator — Claude Code style: minimal */}
|
|
295
|
+
<Box paddingX={0} marginBottom={0}>
|
|
296
|
+
{thinkingLabel ? (
|
|
297
|
+
<Text color={theme.colors.text.muted} dimColor>
|
|
298
|
+
{thinkingLabel}
|
|
299
|
+
{pendingCommands.length > 0 && <Text dimColor> · queued: {pendingCommands.length}</Text>}
|
|
300
|
+
</Text>
|
|
301
|
+
) : null}
|
|
302
|
+
</Box>
|
|
303
|
+
|
|
304
|
+
{/* 输入框 — Claude Code style: no border, just prompt + input */}
|
|
305
|
+
<Box
|
|
306
|
+
flexDirection="row"
|
|
307
|
+
paddingX={0}
|
|
308
|
+
paddingY={0}
|
|
309
|
+
>
|
|
310
|
+
{/* Spinner while AI works, □ when idle */}
|
|
311
|
+
<Box marginRight={1}>
|
|
312
|
+
<PromptGlyph
|
|
313
|
+
isProcessing={isProcessing}
|
|
314
|
+
lockedIn={currentCommand !== null}
|
|
315
|
+
glowPhase={glowPhase}
|
|
316
|
+
color={theme.colors.primary}
|
|
317
|
+
idleColor={theme.colors.primary}
|
|
318
|
+
/>
|
|
319
|
+
</Box>
|
|
320
|
+
|
|
321
|
+
{/* 输入框 - 始终启用,支持命令队列 */}
|
|
322
|
+
<Box flexGrow={1}>
|
|
323
|
+
<CustomTextInput
|
|
324
|
+
value={input}
|
|
325
|
+
cursorPosition={cursorPosition}
|
|
326
|
+
onChange={handleChange}
|
|
327
|
+
onChangeCursorPosition={handleChangeCursorPosition}
|
|
328
|
+
onSubmit={handleSubmit}
|
|
329
|
+
onPaste={handlePaste}
|
|
330
|
+
onArrowUp={handleArrowUpInternal}
|
|
331
|
+
onArrowDown={handleArrowDownInternal}
|
|
332
|
+
placeholder={placeholder}
|
|
333
|
+
focusId={FocusId.MAIN_INPUT}
|
|
334
|
+
disabled={false}
|
|
335
|
+
cursorOn={cursorOn}
|
|
336
|
+
/>
|
|
337
|
+
</Box>
|
|
338
|
+
</Box>
|
|
339
|
+
|
|
340
|
+
{/* Command suggestions — below the input bar */}
|
|
341
|
+
{showSuggestions && (
|
|
342
|
+
<CommandSuggestions
|
|
343
|
+
input={input}
|
|
344
|
+
cursorPosition={cursorPosition}
|
|
345
|
+
onSelectSuggestion={handleSelectSuggestion}
|
|
346
|
+
visible={showSuggestions}
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{/* Prompt suggestions — disabled */}
|
|
351
|
+
</Box>
|
|
352
|
+
);
|
|
353
|
+
},
|
|
354
|
+
// 自定义比较函数:始终返回 true,因为回调通过 ref 访
|
|
355
|
+
// 这样 props 变化不会触发重新渲
|
|
356
|
+
() => false
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
InputArea.displayName = 'InputArea';
|
|
360
|
+
|
|
361
|
+
export default InputArea;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { themeManager } from '../../themes/index.js';
|
|
4
|
+
import { FocusId, focusManager } from '../../focus/index.js';
|
|
5
|
+
|
|
6
|
+
const SUGGESTIONS = [
|
|
7
|
+
{ label: 'Explain codebase', text: 'Explain this codebase — what does it do and how is it structured?' },
|
|
8
|
+
{ label: 'Fix a bug', text: 'Find and fix the bug: ' },
|
|
9
|
+
{ label: 'Write tests', text: 'Write tests for ' },
|
|
10
|
+
{ label: 'Refactor', text: 'Refactor this to be cleaner and more maintainable: ' },
|
|
11
|
+
{ label: 'Review changes', text: 'Review my recent git changes and flag any issues' },
|
|
12
|
+
{ label: 'Add feature', text: 'Implement a new feature: ' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
interface PromptSuggestionsProps {
|
|
16
|
+
onSelect: (text: string) => void;
|
|
17
|
+
visible: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PromptSuggestions: React.FC<PromptSuggestionsProps> = ({ onSelect, visible }) => {
|
|
21
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
22
|
+
const theme = themeManager.getTheme();
|
|
23
|
+
|
|
24
|
+
useInput(
|
|
25
|
+
(_, key) => {
|
|
26
|
+
if (!visible) return;
|
|
27
|
+
if (focusManager.getCurrentFocus() !== FocusId.MAIN_INPUT) return;
|
|
28
|
+
|
|
29
|
+
if (key.tab && !key.shift) {
|
|
30
|
+
setSelectedIndex(i => (i + 1) % SUGGESTIONS.length);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (key.tab && key.shift) {
|
|
34
|
+
setSelectedIndex(i => (i <= 0 ? SUGGESTIONS.length - 1 : i - 1));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (key.return) {
|
|
38
|
+
onSelect(SUGGESTIONS[selectedIndex].text);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{ isActive: visible }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!visible) return null;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Box flexDirection="row" paddingX={2} marginTop={0} flexWrap="wrap">
|
|
48
|
+
{SUGGESTIONS.map((s, i) => {
|
|
49
|
+
const isSelected = i === selectedIndex;
|
|
50
|
+
return (
|
|
51
|
+
<Box key={s.label} marginRight={2}>
|
|
52
|
+
<Text
|
|
53
|
+
color={isSelected ? theme.colors.primary : theme.colors.text.muted}
|
|
54
|
+
bold={isSelected}
|
|
55
|
+
dimColor={!isSelected}
|
|
56
|
+
>
|
|
57
|
+
{isSelected ? '▸ ' : ' '}{s.label}
|
|
58
|
+
</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default PromptSuggestions;
|