toolpack-cli 0.1.0-SNAPSHOT
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/LICENSE +201 -0
- package/README.md +131 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +29 -0
- package/dist/commands/clear.d.ts +3 -0
- package/dist/commands/clear.js +15 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.js +29 -0
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.js +16 -0
- package/dist/commands/info.d.ts +3 -0
- package/dist/commands/info.js +24 -0
- package/dist/commands/mode.d.ts +3 -0
- package/dist/commands/mode.js +51 -0
- package/dist/commands/model.d.ts +3 -0
- package/dist/commands/model.js +14 -0
- package/dist/commands/registry.d.ts +32 -0
- package/dist/commands/registry.js +86 -0
- package/dist/commands/tool-log.d.ts +3 -0
- package/dist/commands/tool-log.js +17 -0
- package/dist/commands/tool-search.d.ts +3 -0
- package/dist/commands/tool-search.js +57 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +45 -0
- package/dist/commands/types.d.ts +25 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.js +25 -0
- package/dist/components/AppInfo.d.ts +1 -0
- package/dist/components/AppInfo.js +10 -0
- package/dist/components/HomeInput.d.ts +11 -0
- package/dist/components/HomeInput.js +328 -0
- package/dist/components/Logo.d.ts +1 -0
- package/dist/components/Logo.js +15 -0
- package/dist/components/Markdown.d.ts +5 -0
- package/dist/components/Markdown.js +121 -0
- package/dist/components/ProviderBar.d.ts +12 -0
- package/dist/components/ProviderBar.js +32 -0
- package/dist/components/ShimmerText.d.ts +8 -0
- package/dist/components/ShimmerText.js +20 -0
- package/dist/components/ToolLogPopup.d.ts +7 -0
- package/dist/components/ToolLogPopup.js +87 -0
- package/dist/components/common/HistorySelect.d.ts +6 -0
- package/dist/components/common/HistorySelect.js +57 -0
- package/dist/components/common/Modal.d.ts +10 -0
- package/dist/components/common/Modal.js +13 -0
- package/dist/components/common/ModeSelect.d.ts +6 -0
- package/dist/components/common/ModeSelect.js +13 -0
- package/dist/components/common/ModelSelect.d.ts +9 -0
- package/dist/components/common/ModelSelect.js +45 -0
- package/dist/context/ConversationContext.d.ts +44 -0
- package/dist/context/ConversationContext.js +113 -0
- package/dist/context/ToolpackContext.d.ts +55 -0
- package/dist/context/ToolpackContext.js +221 -0
- package/dist/custom-providers/AnthropicCustomAdapter.d.ts +49 -0
- package/dist/custom-providers/AnthropicCustomAdapter.js +297 -0
- package/dist/custom-providers/XAIAdapter.d.ts +40 -0
- package/dist/custom-providers/XAIAdapter.js +295 -0
- package/dist/custom-tools/skill-tools/index.d.ts +33 -0
- package/dist/custom-tools/skill-tools/index.js +63 -0
- package/dist/custom-tools/skill-tools/tools/create/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/create/index.js +93 -0
- package/dist/custom-tools/skill-tools/tools/create/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/create/schema.js +41 -0
- package/dist/custom-tools/skill-tools/tools/list/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/list/index.js +113 -0
- package/dist/custom-tools/skill-tools/tools/list/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/list/schema.js +19 -0
- package/dist/custom-tools/skill-tools/tools/read/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/read/index.js +124 -0
- package/dist/custom-tools/skill-tools/tools/read/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/read/schema.js +27 -0
- package/dist/custom-tools/skill-tools/tools/search/bm25.d.ts +71 -0
- package/dist/custom-tools/skill-tools/tools/search/bm25.js +305 -0
- package/dist/custom-tools/skill-tools/tools/search/index.d.ts +8 -0
- package/dist/custom-tools/skill-tools/tools/search/index.js +63 -0
- package/dist/custom-tools/skill-tools/tools/search/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/search/schema.js +19 -0
- package/dist/custom-tools/skill-tools/tools/search/skill-index.d.ts +54 -0
- package/dist/custom-tools/skill-tools/tools/search/skill-index.js +251 -0
- package/dist/custom-tools/skill-tools/tools/update/index.d.ts +2 -0
- package/dist/custom-tools/skill-tools/tools/update/index.js +115 -0
- package/dist/custom-tools/skill-tools/tools/update/schema.d.ts +6 -0
- package/dist/custom-tools/skill-tools/tools/update/schema.js +41 -0
- package/dist/screens/ChatScreen.d.ts +1 -0
- package/dist/screens/ChatScreen.js +327 -0
- package/dist/screens/HomeScreen.d.ts +1 -0
- package/dist/screens/HomeScreen.js +68 -0
- package/dist/screens/SettingsScreen.d.ts +1 -0
- package/dist/screens/SettingsScreen.js +35 -0
- package/dist/services/db.d.ts +31 -0
- package/dist/services/db.js +108 -0
- package/dist/theme/ThemeContext.d.ts +11 -0
- package/dist/theme/ThemeContext.js +31 -0
- package/dist/theme/theme.d.ts +17 -0
- package/dist/theme/theme.js +82 -0
- package/package.json +101 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useStdout, useInput, measureElement } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { useTheme } from '../theme/ThemeContext.js';
|
|
6
|
+
import { useConversation } from '../context/ConversationContext.js';
|
|
7
|
+
import { useToolpack } from '../context/ToolpackContext.js';
|
|
8
|
+
import { Markdown } from '../components/Markdown.js';
|
|
9
|
+
import { isCommand, executeCommand } from '../commands/index.js';
|
|
10
|
+
import * as dbManager from '../services/db.js';
|
|
11
|
+
import { ToolLogPopup } from '../components/ToolLogPopup.js';
|
|
12
|
+
import { ShimmerText } from '../components/ShimmerText.js';
|
|
13
|
+
export function ChatScreen() {
|
|
14
|
+
const { theme } = useTheme();
|
|
15
|
+
const { stdout } = useStdout();
|
|
16
|
+
const { activeConversation, addMessage, clearHistory, pendingPrompt, setPendingPrompt, setScreen, } = useConversation();
|
|
17
|
+
const { toolpack, activeMode, activeModel, activeModelCapabilities, workflowProgress, toolHistory, clearToolHistory, clearWorkflowProgress, toolsUnsupportedWarning, setToolsUnsupportedWarning, clearToolsUnsupportedWarning, } = useToolpack();
|
|
18
|
+
const [input, setInput] = useState('');
|
|
19
|
+
const [streamingContent, setStreamingContent] = useState('');
|
|
20
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
const [showToolLogPopup, setShowToolLogPopup] = useState(false);
|
|
23
|
+
const [wasInterrupted, setWasInterrupted] = useState(false);
|
|
24
|
+
const abortControllerRef = useRef(null);
|
|
25
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
26
|
+
const [isUserScrolled, setIsUserScrolled] = useState(false);
|
|
27
|
+
const prevMessageCountRef = useRef(0);
|
|
28
|
+
// Keep a ref to the latest messages so handleSubmit always has the freshest state
|
|
29
|
+
const messagesRef = useRef([]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
messagesRef.current = activeConversation?.messages || [];
|
|
32
|
+
}, [activeConversation?.messages]);
|
|
33
|
+
// The outer Box has height={stdout.rows - 1}.
|
|
34
|
+
// Fixed rows consumed above the scroll area: header(1) + separator(1) + paddingY(2) = 4
|
|
35
|
+
// Fixed rows consumed below: input(3) + footer(1) = 4. Total fixed = 8.
|
|
36
|
+
// We do NOT use measureElement for visibleLines to avoid feedback loops.
|
|
37
|
+
// viewport paddingY={1} adds 2 more rows consumed. Total = 10.
|
|
38
|
+
const visibleLines = Math.max(1, stdout.rows - 11);
|
|
39
|
+
// Measure the actual content height from the DOM after every render.
|
|
40
|
+
// Use a ref to avoid triggering state updates (which would cause infinite loops).
|
|
41
|
+
const contentRef = useRef(null);
|
|
42
|
+
const viewportRef = useRef(null); // unused for measurement, just for ref
|
|
43
|
+
const totalLinesRef = useRef(0);
|
|
44
|
+
const [totalLines, setTotalLines] = useState(0);
|
|
45
|
+
// Measure content height synchronously before paint using useLayoutEffect.
|
|
46
|
+
// This prevents the blank screen flash that occurs when measurement happens after render.
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
if (contentRef.current) {
|
|
49
|
+
const { height } = measureElement(contentRef.current);
|
|
50
|
+
if (height !== totalLinesRef.current) {
|
|
51
|
+
totalLinesRef.current = height;
|
|
52
|
+
setTotalLines(height);
|
|
53
|
+
// Immediately update scroll offset if auto-scrolling to prevent blank screen
|
|
54
|
+
if (!isUserScrolled) {
|
|
55
|
+
const newOffset = height > visibleLines ? Math.max(0, height - visibleLines) : 0;
|
|
56
|
+
setScrollOffset(newOffset);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, [activeConversation?.messages, streamingContent, isStreaming, visibleLines, isUserScrolled]);
|
|
61
|
+
// Auto-scroll: when the user hasn't manually scrolled, keep the view at the bottom.
|
|
62
|
+
// This handles both streaming content growth and history load.
|
|
63
|
+
// Also clamp scrollOffset to prevent showing blank space beyond content
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
66
|
+
if (!isUserScrolled) {
|
|
67
|
+
// Auto-scroll to bottom
|
|
68
|
+
setScrollOffset(maxOffset);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// User scrolled manually - clamp to valid range to prevent blank screens
|
|
72
|
+
setScrollOffset(prev => {
|
|
73
|
+
const clamped = Math.min(prev, maxOffset);
|
|
74
|
+
// If clamped value is at max, consider user back at bottom
|
|
75
|
+
if (clamped >= maxOffset - 1) {
|
|
76
|
+
setIsUserScrolled(false);
|
|
77
|
+
}
|
|
78
|
+
return clamped;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}, [totalLines, visibleLines, isUserScrolled]);
|
|
82
|
+
// Additional safeguard: always clamp scroll offset to prevent blank screens
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
85
|
+
setScrollOffset(prev => {
|
|
86
|
+
// If scroll offset is beyond valid range, reset to bottom
|
|
87
|
+
if (prev > maxOffset) {
|
|
88
|
+
return maxOffset;
|
|
89
|
+
}
|
|
90
|
+
return prev;
|
|
91
|
+
});
|
|
92
|
+
}, [totalLines, visibleLines]);
|
|
93
|
+
// When a conversation is loaded from history (bulk message load),
|
|
94
|
+
// ensure we auto-scroll to the bottom.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const msgCount = activeConversation?.messages.length ?? 0;
|
|
97
|
+
const prevCount = prevMessageCountRef.current;
|
|
98
|
+
prevMessageCountRef.current = msgCount;
|
|
99
|
+
// Bulk load detected: ensure auto-follow is on
|
|
100
|
+
if (msgCount - prevCount > 2) {
|
|
101
|
+
setIsUserScrolled(false);
|
|
102
|
+
}
|
|
103
|
+
}, [activeConversation?.messages.length]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const handleData = (data) => {
|
|
106
|
+
const str = data.toString();
|
|
107
|
+
const match = str.match(/\x1b\[<(64|65);\d+;\d+M/);
|
|
108
|
+
if (match && match[1]) {
|
|
109
|
+
const button = parseInt(match[1], 10);
|
|
110
|
+
if (button === 64) {
|
|
111
|
+
setIsUserScrolled(true);
|
|
112
|
+
setScrollOffset(prev => Math.max(0, prev - 3));
|
|
113
|
+
}
|
|
114
|
+
else if (button === 65) {
|
|
115
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
116
|
+
setScrollOffset(prev => {
|
|
117
|
+
const next = Math.min(maxOffset, prev + 3);
|
|
118
|
+
if (next >= maxOffset)
|
|
119
|
+
setIsUserScrolled(false);
|
|
120
|
+
else
|
|
121
|
+
setIsUserScrolled(true);
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
process.stdin.on('data', handleData);
|
|
128
|
+
return () => {
|
|
129
|
+
process.stdin.off('data', handleData);
|
|
130
|
+
};
|
|
131
|
+
}, [totalLines, visibleLines]);
|
|
132
|
+
// Escape listener to go back home, and PageUp/PageDown/Arrows for scrolling
|
|
133
|
+
useInput((input, key) => {
|
|
134
|
+
if (input === 'q' && isStreaming && abortControllerRef.current) {
|
|
135
|
+
abortControllerRef.current.abort();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.escape && !isStreaming && !showToolLogPopup) {
|
|
139
|
+
clearHistory();
|
|
140
|
+
}
|
|
141
|
+
else if (key.pageDown) {
|
|
142
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
143
|
+
setScrollOffset(prev => {
|
|
144
|
+
const next = Math.min(maxOffset, prev + visibleLines - 1);
|
|
145
|
+
if (next >= maxOffset)
|
|
146
|
+
setIsUserScrolled(false);
|
|
147
|
+
else
|
|
148
|
+
setIsUserScrolled(true);
|
|
149
|
+
return next;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else if (key.pageUp) {
|
|
153
|
+
setIsUserScrolled(true);
|
|
154
|
+
setScrollOffset(prev => Math.max(0, prev - visibleLines + 1));
|
|
155
|
+
}
|
|
156
|
+
else if (key.upArrow) {
|
|
157
|
+
setIsUserScrolled(true);
|
|
158
|
+
setScrollOffset(prev => Math.max(0, prev - 3));
|
|
159
|
+
}
|
|
160
|
+
else if (key.downArrow) {
|
|
161
|
+
setIsUserScrolled(true);
|
|
162
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
163
|
+
setScrollOffset(prev => Math.min(maxOffset, prev + 3));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (pendingPrompt && toolpack && !isStreaming) {
|
|
168
|
+
const promptToSubmit = pendingPrompt;
|
|
169
|
+
setPendingPrompt(null); // Clear it immediately so it doesn't double-fire
|
|
170
|
+
handleSubmit(promptToSubmit);
|
|
171
|
+
}
|
|
172
|
+
}, [pendingPrompt, toolpack]);
|
|
173
|
+
const handleSubmit = async (value) => {
|
|
174
|
+
if (!value.trim() || isStreaming)
|
|
175
|
+
return;
|
|
176
|
+
// Strip newlines from submission just in case
|
|
177
|
+
const prompt = value.trim();
|
|
178
|
+
setInput('');
|
|
179
|
+
// Handle slash commands
|
|
180
|
+
if (isCommand(prompt)) {
|
|
181
|
+
const context = {
|
|
182
|
+
toolpack,
|
|
183
|
+
activeMode,
|
|
184
|
+
activeModel,
|
|
185
|
+
activeConversation,
|
|
186
|
+
addMessage,
|
|
187
|
+
clearHistory,
|
|
188
|
+
setScreen,
|
|
189
|
+
};
|
|
190
|
+
const result = await executeCommand(prompt, context);
|
|
191
|
+
if (result.action === 'navigate') {
|
|
192
|
+
return; // Navigation handled by command
|
|
193
|
+
}
|
|
194
|
+
if (result.action === 'popup') {
|
|
195
|
+
// Show popup with tool history from context
|
|
196
|
+
setShowToolLogPopup(true);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (result.message) {
|
|
200
|
+
// Display command output as system message
|
|
201
|
+
addMessage('system', result.message);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Regular chat message - requires toolpack
|
|
206
|
+
if (!toolpack)
|
|
207
|
+
return;
|
|
208
|
+
// Save user message immediately
|
|
209
|
+
addMessage('user', prompt);
|
|
210
|
+
setIsStreaming(true);
|
|
211
|
+
setError(null);
|
|
212
|
+
setStreamingContent('');
|
|
213
|
+
setWasInterrupted(false);
|
|
214
|
+
clearToolHistory(); // Clear tool history for new request
|
|
215
|
+
clearWorkflowProgress(); // Clear previous workflow status
|
|
216
|
+
clearToolsUnsupportedWarning(); // Clear any previous tool warning
|
|
217
|
+
// Create abort controller for interrupt capability
|
|
218
|
+
const abortController = new AbortController();
|
|
219
|
+
abortControllerRef.current = abortController;
|
|
220
|
+
let fullResponse = '';
|
|
221
|
+
try {
|
|
222
|
+
// Build message array from the database (synchronous, always fresh)
|
|
223
|
+
// This ensures we have the complete conversation history including the just-added user message
|
|
224
|
+
// System messages are CLI-only display messages and should not be sent to the AI
|
|
225
|
+
const dbMessages = activeConversation
|
|
226
|
+
? dbManager.getMessages(activeConversation.id)
|
|
227
|
+
: [];
|
|
228
|
+
const historyMessages = dbMessages
|
|
229
|
+
.filter((m) => m.role !== 'system')
|
|
230
|
+
.map((m) => ({
|
|
231
|
+
role: m.role,
|
|
232
|
+
content: m.content,
|
|
233
|
+
}));
|
|
234
|
+
// Check if model supports tools - if not and mode uses tools, show warning after response
|
|
235
|
+
const modeUsesTools = activeMode?.allowedToolCategories &&
|
|
236
|
+
activeMode.allowedToolCategories.length > 0;
|
|
237
|
+
const modelSupportsTools = activeModelCapabilities?.toolCalling !== false;
|
|
238
|
+
const shouldWarnAboutTools = modeUsesTools && !modelSupportsTools;
|
|
239
|
+
const streamOptions = {
|
|
240
|
+
// historyMessages already includes the current user message (saved to DB above)
|
|
241
|
+
messages: historyMessages,
|
|
242
|
+
model: activeModel,
|
|
243
|
+
};
|
|
244
|
+
// If model doesn't support tools, explicitly set tool_choice to 'none' to skip sending tools
|
|
245
|
+
if (shouldWarnAboutTools) {
|
|
246
|
+
streamOptions.tool_choice = 'none';
|
|
247
|
+
}
|
|
248
|
+
const stream = toolpack.stream({
|
|
249
|
+
...streamOptions,
|
|
250
|
+
signal: abortController.signal,
|
|
251
|
+
});
|
|
252
|
+
for await (const chunk of stream) {
|
|
253
|
+
if (abortController.signal.aborted) {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
if (chunk.delta) {
|
|
257
|
+
fullResponse += chunk.delta;
|
|
258
|
+
setStreamingContent(fullResponse);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Save final assistant message (tool history comes from context)
|
|
262
|
+
// IMPORTANT: Clear streaming content BEFORE adding message to prevent
|
|
263
|
+
// a brief state where both exist, causing measurement issues
|
|
264
|
+
setStreamingContent('');
|
|
265
|
+
if (abortController.signal.aborted) {
|
|
266
|
+
setWasInterrupted(true);
|
|
267
|
+
if (fullResponse.trim()) {
|
|
268
|
+
addMessage('assistant', fullResponse + '\n\n*[Interrupted]*');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
addMessage('assistant', fullResponse);
|
|
273
|
+
}
|
|
274
|
+
// Force scroll to bottom after message is added
|
|
275
|
+
setIsUserScrolled(false);
|
|
276
|
+
// Show warning if model doesn't support tools but mode uses them
|
|
277
|
+
if (shouldWarnAboutTools) {
|
|
278
|
+
setToolsUnsupportedWarning(`Model "${activeModel}" does not support tool/function calls. Running in chat-only mode.`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
// Don't show error for intentional abort
|
|
283
|
+
if (err.name === 'AbortError' || abortController.signal.aborted) {
|
|
284
|
+
setWasInterrupted(true);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
console.error('[ChatScreen] Streaming error:', err);
|
|
288
|
+
// Save partial response so it's not lost from conversation history
|
|
289
|
+
if (fullResponse.trim()) {
|
|
290
|
+
addMessage('assistant', fullResponse +
|
|
291
|
+
'\n\n• **Step failed:** ' +
|
|
292
|
+
(err.message || String(err)) +
|
|
293
|
+
'*Workflow aborted.*');
|
|
294
|
+
}
|
|
295
|
+
setError(err.message || String(err));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
setIsStreaming(false);
|
|
300
|
+
abortControllerRef.current = null;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const handleInputChange = (val) => {
|
|
304
|
+
let sanitized = val.replace(/\x1b?\[<\d+;\d+;\d+[mM]/g, '');
|
|
305
|
+
sanitized = sanitized.replace(/\x1b/g, '');
|
|
306
|
+
setInput(sanitized);
|
|
307
|
+
};
|
|
308
|
+
return (_jsxs(Box, { width: stdout.columns, height: stdout.rows - 1, flexDirection: "column", children: [_jsx(Box, { flexDirection: "row", borderBottom: false, paddingY: 0, paddingX: 1, flexShrink: 0, children: _jsxs(Text, { color: theme.colors.secondary, children: ["Conversation:", ' ', _jsx(Text, { color: theme.colors.primary, bold: true, children: activeConversation?.title }), ' ', "| Mode: ", activeMode?.displayName || 'All', " | Model: ", activeModel] }) }), _jsx(Box, { width: "100%", height: 1, flexShrink: 0, paddingX: 1, children: _jsx(Text, { color: theme.colors.border, children: '─'.repeat(stdout.columns - 2) }) }), _jsx(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingY: 1, overflowY: "hidden", ref: viewportRef, children: _jsx(Box, { marginTop: -scrollOffset, flexDirection: "column", flexShrink: 0, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: contentRef, children: [activeConversation?.messages.map((msg, index) => {
|
|
309
|
+
const isUser = msg.role === 'user';
|
|
310
|
+
const isSystem = msg.role === 'system';
|
|
311
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, flexShrink: 0, width: "100%", borderStyle: isUser ? 'round' : isSystem ? 'single' : undefined, borderColor: isUser
|
|
312
|
+
? theme.colors.highlight
|
|
313
|
+
: isSystem
|
|
314
|
+
? theme.colors.border
|
|
315
|
+
: undefined, paddingX: isUser || isSystem ? 1 : 0, paddingY: isUser || isSystem ? 1 : 0, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 2, flexShrink: 0, children: isUser ? (_jsx(Text, { color: theme.colors.secondary, bold: true, children: '> ' })) : isSystem ? (_jsx(Text, { color: theme.colors.highlight, bold: true, children: '⚡' })) : (_jsx(Text, { color: theme.colors.primary, bold: true, children: '• ' })) }), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: isUser ? (_jsx(Text, { children: msg.content })) : (_jsx(Markdown, { children: msg.content })) })] }) }, index));
|
|
316
|
+
}), isStreaming && (_jsx(Box, { flexDirection: "column", marginBottom: 1, flexShrink: 0, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: theme.colors.primary, bold: true, children: '• ' }) }), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: streamingContent ? (_jsx(Markdown, { children: streamingContent })) : !workflowProgress ? (_jsx(Text, { color: theme.colors.textSecondary, italic: true, children: "Thinking..." })) : null })] }) })), error && (_jsx(Box, { flexDirection: "column", marginBottom: 1, flexShrink: 0, children: _jsxs(Text, { color: "#ef4444", children: ["Error: ", error] }) }))] }) }) }), workflowProgress && (_jsx(Box, { paddingX: 1, marginTop: 1, flexShrink: 0, children: workflowProgress.status === 'executing' ? (_jsx(ShimmerText, { text: `Step ${workflowProgress.currentStep}/${workflowProgress.totalSteps}: ${workflowProgress.currentStepDescription}`, baseColor: theme.colors.textSecondary, brightColor: theme.colors.primary, speed: 100 })) : workflowProgress.status === 'completed' ? (_jsxs(Text, { color: theme.colors.textSecondary, dimColor: true, children: ["Workflow completed. (", workflowProgress.currentStep, "/", workflowProgress.totalSteps, " steps)"] })) : null })), !isStreaming && toolsUnsupportedWarning && (_jsx(Box, { paddingX: 1, marginTop: 1, flexShrink: 0, children: _jsxs(Text, { color: "#f59e0b", children: ["\u26A0 ", toolsUnsupportedWarning] }) })), _jsxs(Box, { paddingX: 1, marginTop: 1, marginBottom: 1, flexShrink: 0, flexDirection: "row", children: [_jsx(Text, { color: theme.colors.text, bold: true, children: '> ' }), isStreaming ? (_jsx(Text, { color: theme.colors.textSecondary, children: "Thinking..." })) : (_jsx(TextInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, placeholder: activeConversation?.messages.length === 0
|
|
317
|
+
? 'Ask anything...'
|
|
318
|
+
: 'Send a message...' }))] }), _jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: theme.colors.textSecondary, children: isStreaming ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "#ef4444", bold: true, children: "q" }), _jsx(Text, { children: " :Interrupt | " }), _jsx(Text, { color: "#fbbf24", bold: true, children: "\u2191/\u2193/PgUp/PgDn" }), _jsx(Text, { children: " :Scroll" })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "#fbbf24", bold: true, children: "Esc" }), _jsx(Text, { children: " :Home | " }), _jsx(Text, { color: "#fbbf24", bold: true, children: "\u2191/\u2193/PgUp/PgDn" }), _jsx(Text, { children: " :Scroll | " }), _jsx(Text, { color: "#fbbf24", bold: true, children: "/help" }), _jsx(Text, { children: " :Commands" }), wasInterrupted && (_jsx(Text, { color: "#f59e0b", children: " | Last response interrupted" }))] })) }) }), showToolLogPopup && (_jsx(ToolLogPopup, { toolCalls: toolHistory.map(t => ({
|
|
319
|
+
id: t.id,
|
|
320
|
+
name: t.name,
|
|
321
|
+
arguments: t.arguments,
|
|
322
|
+
result: t.result,
|
|
323
|
+
timestamp: t.timestamp,
|
|
324
|
+
duration: t.duration,
|
|
325
|
+
status: t.status === 'success' ? 'success' : 'error',
|
|
326
|
+
})), onClose: () => setShowToolLogPopup(false) }))] }));
|
|
327
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function HomeScreen(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, useStdout, useInput, Text } from 'ink';
|
|
4
|
+
import { useTheme } from '../theme/ThemeContext.js';
|
|
5
|
+
import { useToolpack } from '../context/ToolpackContext.js';
|
|
6
|
+
import { useConversation } from '../context/ConversationContext.js';
|
|
7
|
+
import { Logo } from '../components/Logo.js';
|
|
8
|
+
import { AppInfo } from '../components/AppInfo.js';
|
|
9
|
+
import { HomeInput } from '../components/HomeInput.js';
|
|
10
|
+
import { ModeSelect } from '../components/common/ModeSelect.js';
|
|
11
|
+
import { ModelSelect } from '../components/common/ModelSelect.js';
|
|
12
|
+
import { HistorySelect } from '../components/common/HistorySelect.js';
|
|
13
|
+
export function HomeScreen() {
|
|
14
|
+
const { theme } = useTheme();
|
|
15
|
+
const { stdout } = useStdout();
|
|
16
|
+
const { setMode, setModel, models } = useToolpack();
|
|
17
|
+
const { loadConversation, setScreen } = useConversation();
|
|
18
|
+
// 0 = TextInput, 1 = Mode, 2 = Model, 3 = History
|
|
19
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
20
|
+
const [showModeSelect, setShowModeSelect] = useState(false);
|
|
21
|
+
const [showModelSelect, setShowModelSelect] = useState(false);
|
|
22
|
+
const [showHistorySelect, setShowHistorySelect] = useState(false);
|
|
23
|
+
const anyModalOpen = showModeSelect || showModelSelect || showHistorySelect;
|
|
24
|
+
// Catch Tab keys globally at this screen level
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
if (anyModalOpen)
|
|
27
|
+
return; // Prevent tab cycling if modal is open
|
|
28
|
+
if (key.tab) {
|
|
29
|
+
if (key.shift) {
|
|
30
|
+
setFocusedIndex(prev => (prev > 0 ? prev - 1 : 3));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
setFocusedIndex(prev => (prev < 3 ? prev + 1 : 0));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (key.ctrl && input === 's') {
|
|
37
|
+
setScreen('settings');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return (_jsxs(Box, { width: stdout.columns, height: stdout.rows - 1, flexDirection: "column", position: "relative", children: [_jsxs(Box, { width: "100%", alignItems: "center", flexDirection: "column", paddingTop: 6, flexShrink: 0, children: [_jsx(Logo, {}), _jsx(AppInfo, {})] }), _jsx(Box, { flexGrow: 1, width: "100%", justifyContent: "center", alignItems: "center", flexDirection: "column", children: _jsx(Box, { width: "80%", children: _jsx(HomeInput, { focusedIndex: focusedIndex, showModeSelect: showModeSelect, setShowModeSelect: setShowModeSelect, showModelSelect: showModelSelect, setShowModelSelect: setShowModelSelect, showHistorySelect: showHistorySelect, setShowHistorySelect: setShowHistorySelect }) }) }), _jsx(Box, { width: "100%", flexDirection: "column", alignItems: "center", paddingBottom: 1, flexShrink: 0, children: _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.textSecondary, children: [_jsx(Text, { color: "#eab308", bold: true, children: "\u2022 Note" }), ' ', "If you are using local models, make sure to have them downloaded and available."] }) }) }), showModeSelect && (_jsx(ModeSelect, { onSelect: val => {
|
|
41
|
+
setMode(val);
|
|
42
|
+
setShowModeSelect(false);
|
|
43
|
+
setFocusedIndex(0); // Return focus to input
|
|
44
|
+
}, onClose: () => {
|
|
45
|
+
setShowModeSelect(false);
|
|
46
|
+
setFocusedIndex(0); // Return focus to input
|
|
47
|
+
} })), showModelSelect && (_jsx(ModelSelect, { onSelect: () => {
|
|
48
|
+
setShowModelSelect(false);
|
|
49
|
+
setFocusedIndex(0); // Return focus to input
|
|
50
|
+
}, onClose: () => {
|
|
51
|
+
setShowModelSelect(false);
|
|
52
|
+
setFocusedIndex(0); // Return focus to input
|
|
53
|
+
} })), showHistorySelect && (_jsx(HistorySelect, { onSelect: (id, _label) => {
|
|
54
|
+
const { mode, model } = loadConversation(id);
|
|
55
|
+
// Restore the mode and model from the conversation
|
|
56
|
+
if (mode)
|
|
57
|
+
setMode(mode);
|
|
58
|
+
if (model) {
|
|
59
|
+
const full = models.find(m => m.value === model);
|
|
60
|
+
setModel({ value: model, provider: full?.provider });
|
|
61
|
+
}
|
|
62
|
+
setShowHistorySelect(false);
|
|
63
|
+
setFocusedIndex(0); // Return focus to input
|
|
64
|
+
}, onClose: () => {
|
|
65
|
+
setShowHistorySelect(false);
|
|
66
|
+
setFocusedIndex(0); // Return focus to input
|
|
67
|
+
} }))] }));
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function SettingsScreen(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
+
import { useTheme } from '../theme/ThemeContext.js';
|
|
5
|
+
import { useConversation } from '../context/ConversationContext.js';
|
|
6
|
+
import { useToolpack } from '../context/ToolpackContext.js';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
export function SettingsScreen() {
|
|
11
|
+
const { theme } = useTheme();
|
|
12
|
+
const { stdout } = useStdout();
|
|
13
|
+
const { setScreen } = useConversation();
|
|
14
|
+
const { configSource, activeConfigPath } = useToolpack();
|
|
15
|
+
const [statusMsg, setStatusMsg] = useState(null);
|
|
16
|
+
useInput((input, key) => {
|
|
17
|
+
if (key.escape || input === 'q') {
|
|
18
|
+
setScreen('home');
|
|
19
|
+
}
|
|
20
|
+
else if (input === 'o' && activeConfigPath) {
|
|
21
|
+
open(activeConfigPath).catch(err => {
|
|
22
|
+
setStatusMsg(`Failed to open config: ${err.message}`);
|
|
23
|
+
});
|
|
24
|
+
setStatusMsg(`Opening ${activeConfigPath}...`);
|
|
25
|
+
setTimeout(() => setStatusMsg(null), 3000);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return (_jsx(Box, { width: stdout.columns, height: stdout.rows - 1, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsxs(Box, { borderStyle: "single", borderColor: theme.colors.border, paddingX: 2, paddingY: 1, flexDirection: "column", width: "100%", children: [_jsx(Text, { color: theme.colors.primary, bold: true, children: "\u2699 Configuration Settings" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { bold: true, children: "Active Configuration Source:" }), ' ', configSource === 'local'
|
|
29
|
+
? 'Workspace Local'
|
|
30
|
+
: configSource === 'global'
|
|
31
|
+
? 'Global Default'
|
|
32
|
+
: configSource === 'base'
|
|
33
|
+
? 'SDK Base'
|
|
34
|
+
: 'Built-in Defaults'] }), activeConfigPath ? (_jsxs(Text, { color: theme.colors.textSecondary, children: [_jsx(Text, { bold: true, children: "Path:" }), " ", activeConfigPath] })) : (_jsx(Text, { color: theme.colors.textSecondary, children: "No configuration file found. Using defaults." }))] }), _jsxs(Box, { marginTop: 2, flexDirection: "column", children: [_jsx(Text, { color: theme.colors.textSecondary, children: _jsx(Text, { bold: true, children: "Hierarchy Info:" }) }), _jsxs(Text, { color: theme.colors.textSecondary, children: ["1. Workspace Local:", ' ', _jsx(Text, { color: theme.colors.text, children: path.join(process.cwd(), '.toolpack/config/toolpack.config.json') })] }), _jsxs(Text, { color: theme.colors.textSecondary, children: ["2. Global Default:", ' ', _jsx(Text, { color: theme.colors.text, children: path.join(os.homedir(), '.toolpack/config/toolpack.config.json') })] }), _jsxs(Text, { color: theme.colors.textSecondary, children: ["3. SDK Base:", ' ', _jsx(Text, { color: theme.colors.text, children: path.join(process.cwd(), 'toolpack.config.json') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textSecondary, children: "Local settings override global, which override base settings." }) })] }), statusMsg && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.success, children: statusMsg }) })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: theme.colors.secondary, children: "[o] Open active config file \u2022 [q] or [esc] Back to home" }) })] }) }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
export interface Conversation {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
mode?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
createdAt: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolCallEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
arguments: Record<string, any>;
|
|
13
|
+
result?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
duration?: number;
|
|
17
|
+
status: 'pending' | 'success' | 'error';
|
|
18
|
+
}
|
|
19
|
+
export interface Message {
|
|
20
|
+
id: string;
|
|
21
|
+
conversationId: string;
|
|
22
|
+
role: 'user' | 'assistant' | 'system';
|
|
23
|
+
content: string;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
toolCalls?: ToolCallEntry[];
|
|
26
|
+
}
|
|
27
|
+
export declare function initDb(): Database.Database;
|
|
28
|
+
export declare function createConversation(initialPrompt: string, mode?: string, model?: string): Conversation;
|
|
29
|
+
export declare function getConversations(): Conversation[];
|
|
30
|
+
export declare function getMessages(conversationId: string): Message[];
|
|
31
|
+
export declare function saveMessage(conversationId: string, role: string, content: string, toolCalls?: ToolCallEntry[]): Message;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
const DB_DIR = path.join(os.homedir(), '.toolpack', 'cli', 'db');
|
|
7
|
+
const DB_PATH = path.join(DB_DIR, 'history.db');
|
|
8
|
+
let db = null;
|
|
9
|
+
export function initDb() {
|
|
10
|
+
if (db)
|
|
11
|
+
return db;
|
|
12
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
13
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
db = new Database(DB_PATH);
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
title TEXT NOT NULL,
|
|
20
|
+
created_at INTEGER NOT NULL
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
conversation_id TEXT NOT NULL,
|
|
26
|
+
role TEXT NOT NULL,
|
|
27
|
+
content TEXT NOT NULL,
|
|
28
|
+
created_at INTEGER NOT NULL,
|
|
29
|
+
tool_calls TEXT,
|
|
30
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
31
|
+
);
|
|
32
|
+
`);
|
|
33
|
+
// Migration: Add tool_calls column if it doesn't exist
|
|
34
|
+
try {
|
|
35
|
+
db.exec('ALTER TABLE messages ADD COLUMN tool_calls TEXT');
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// Column already exists, ignore
|
|
39
|
+
}
|
|
40
|
+
// Migration: Add mode and model columns to conversations
|
|
41
|
+
try {
|
|
42
|
+
db.exec('ALTER TABLE conversations ADD COLUMN mode TEXT');
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
// Column already exists, ignore
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
db.exec('ALTER TABLE conversations ADD COLUMN model TEXT');
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
// Column already exists, ignore
|
|
52
|
+
}
|
|
53
|
+
// Enable WAL mode for better performance
|
|
54
|
+
db.pragma('journal_mode = WAL');
|
|
55
|
+
return db;
|
|
56
|
+
}
|
|
57
|
+
export function createConversation(initialPrompt, mode, model) {
|
|
58
|
+
const database = initDb();
|
|
59
|
+
const id = uuidv4();
|
|
60
|
+
// Use first 15 chars for the title
|
|
61
|
+
let title = initialPrompt.trim();
|
|
62
|
+
if (title.length > 15) {
|
|
63
|
+
title = title.substring(0, 15) + '...';
|
|
64
|
+
}
|
|
65
|
+
const conversation = {
|
|
66
|
+
id,
|
|
67
|
+
title,
|
|
68
|
+
mode,
|
|
69
|
+
model,
|
|
70
|
+
createdAt: Date.now(),
|
|
71
|
+
};
|
|
72
|
+
const stmt = database.prepare('INSERT INTO conversations (id, title, mode, model, created_at) VALUES (?, ?, ?, ?, ?)');
|
|
73
|
+
stmt.run(conversation.id, conversation.title, conversation.mode || null, conversation.model || null, conversation.createdAt);
|
|
74
|
+
return conversation;
|
|
75
|
+
}
|
|
76
|
+
export function getConversations() {
|
|
77
|
+
const database = initDb();
|
|
78
|
+
const stmt = database.prepare('SELECT id, title, mode, model, created_at as createdAt FROM conversations ORDER BY created_at DESC');
|
|
79
|
+
return stmt.all();
|
|
80
|
+
}
|
|
81
|
+
export function getMessages(conversationId) {
|
|
82
|
+
const database = initDb();
|
|
83
|
+
const stmt = database.prepare('SELECT id, conversation_id as conversationId, role, content, created_at as createdAt, tool_calls as toolCallsJson FROM messages WHERE conversation_id = ? ORDER BY created_at ASC');
|
|
84
|
+
const rows = stmt.all(conversationId);
|
|
85
|
+
return rows.map(row => ({
|
|
86
|
+
id: row.id,
|
|
87
|
+
conversationId: row.conversationId,
|
|
88
|
+
role: row.role,
|
|
89
|
+
content: row.content,
|
|
90
|
+
createdAt: row.createdAt,
|
|
91
|
+
toolCalls: row.toolCallsJson ? JSON.parse(row.toolCallsJson) : undefined,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
export function saveMessage(conversationId, role, content, toolCalls) {
|
|
95
|
+
const database = initDb();
|
|
96
|
+
const message = {
|
|
97
|
+
id: uuidv4(),
|
|
98
|
+
conversationId,
|
|
99
|
+
role: role,
|
|
100
|
+
content,
|
|
101
|
+
createdAt: Date.now(),
|
|
102
|
+
toolCalls,
|
|
103
|
+
};
|
|
104
|
+
const toolCallsJson = toolCalls ? JSON.stringify(toolCalls) : null;
|
|
105
|
+
const stmt = database.prepare('INSERT INTO messages (id, conversation_id, role, content, created_at, tool_calls) VALUES (?, ?, ?, ?, ?, ?)');
|
|
106
|
+
stmt.run(message.id, message.conversationId, message.role, message.content, message.createdAt, toolCallsJson);
|
|
107
|
+
return message;
|
|
108
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Theme } from './theme.js';
|
|
3
|
+
interface ThemeContextType {
|
|
4
|
+
theme: Theme;
|
|
5
|
+
setTheme: (name: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function ThemeProvider({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function useTheme(): ThemeContextType;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
3
|
+
import { themes } from './theme.js';
|
|
4
|
+
const ThemeContext = createContext(undefined);
|
|
5
|
+
export function ThemeProvider({ children }) {
|
|
6
|
+
const [activeTheme, setActiveTheme] = useState(themes['modern']);
|
|
7
|
+
// Update the native terminal background globally to match the theme
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (activeTheme?.colors?.bg) {
|
|
10
|
+
// OSC 11 sets the terminal background color
|
|
11
|
+
process.stdout.write(`\x1b]11;${activeTheme.colors.bg}\x07`);
|
|
12
|
+
}
|
|
13
|
+
return () => {
|
|
14
|
+
// OSC 111 resets the terminal background color
|
|
15
|
+
process.stdout.write('\x1b]111\x07');
|
|
16
|
+
};
|
|
17
|
+
}, [activeTheme]);
|
|
18
|
+
const setTheme = (name) => {
|
|
19
|
+
if (themes[name]) {
|
|
20
|
+
setActiveTheme(themes[name]);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return (_jsx(ThemeContext.Provider, { value: { theme: activeTheme, setTheme }, children: children }));
|
|
24
|
+
}
|
|
25
|
+
export function useTheme() {
|
|
26
|
+
const context = useContext(ThemeContext);
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
29
|
+
}
|
|
30
|
+
return context;
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Theme {
|
|
2
|
+
name: string;
|
|
3
|
+
colors: {
|
|
4
|
+
bg: string;
|
|
5
|
+
border: string;
|
|
6
|
+
primary: string;
|
|
7
|
+
secondary: string;
|
|
8
|
+
text: string;
|
|
9
|
+
textSecondary: string;
|
|
10
|
+
textMuted: string;
|
|
11
|
+
highlight: string;
|
|
12
|
+
success: string;
|
|
13
|
+
error: string;
|
|
14
|
+
inputBg: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare const themes: Record<string, Theme>;
|