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.
Files changed (99) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +15 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +29 -0
  7. package/dist/commands/clear.d.ts +3 -0
  8. package/dist/commands/clear.js +15 -0
  9. package/dist/commands/help.d.ts +3 -0
  10. package/dist/commands/help.js +29 -0
  11. package/dist/commands/index.d.ts +15 -0
  12. package/dist/commands/index.js +16 -0
  13. package/dist/commands/info.d.ts +3 -0
  14. package/dist/commands/info.js +24 -0
  15. package/dist/commands/mode.d.ts +3 -0
  16. package/dist/commands/mode.js +51 -0
  17. package/dist/commands/model.d.ts +3 -0
  18. package/dist/commands/model.js +14 -0
  19. package/dist/commands/registry.d.ts +32 -0
  20. package/dist/commands/registry.js +86 -0
  21. package/dist/commands/tool-log.d.ts +3 -0
  22. package/dist/commands/tool-log.js +17 -0
  23. package/dist/commands/tool-search.d.ts +3 -0
  24. package/dist/commands/tool-search.js +57 -0
  25. package/dist/commands/tools.d.ts +3 -0
  26. package/dist/commands/tools.js +45 -0
  27. package/dist/commands/types.d.ts +25 -0
  28. package/dist/commands/types.js +4 -0
  29. package/dist/commands/version.d.ts +3 -0
  30. package/dist/commands/version.js +25 -0
  31. package/dist/components/AppInfo.d.ts +1 -0
  32. package/dist/components/AppInfo.js +10 -0
  33. package/dist/components/HomeInput.d.ts +11 -0
  34. package/dist/components/HomeInput.js +328 -0
  35. package/dist/components/Logo.d.ts +1 -0
  36. package/dist/components/Logo.js +15 -0
  37. package/dist/components/Markdown.d.ts +5 -0
  38. package/dist/components/Markdown.js +121 -0
  39. package/dist/components/ProviderBar.d.ts +12 -0
  40. package/dist/components/ProviderBar.js +32 -0
  41. package/dist/components/ShimmerText.d.ts +8 -0
  42. package/dist/components/ShimmerText.js +20 -0
  43. package/dist/components/ToolLogPopup.d.ts +7 -0
  44. package/dist/components/ToolLogPopup.js +87 -0
  45. package/dist/components/common/HistorySelect.d.ts +6 -0
  46. package/dist/components/common/HistorySelect.js +57 -0
  47. package/dist/components/common/Modal.d.ts +10 -0
  48. package/dist/components/common/Modal.js +13 -0
  49. package/dist/components/common/ModeSelect.d.ts +6 -0
  50. package/dist/components/common/ModeSelect.js +13 -0
  51. package/dist/components/common/ModelSelect.d.ts +9 -0
  52. package/dist/components/common/ModelSelect.js +45 -0
  53. package/dist/context/ConversationContext.d.ts +44 -0
  54. package/dist/context/ConversationContext.js +113 -0
  55. package/dist/context/ToolpackContext.d.ts +55 -0
  56. package/dist/context/ToolpackContext.js +221 -0
  57. package/dist/custom-providers/AnthropicCustomAdapter.d.ts +49 -0
  58. package/dist/custom-providers/AnthropicCustomAdapter.js +297 -0
  59. package/dist/custom-providers/XAIAdapter.d.ts +40 -0
  60. package/dist/custom-providers/XAIAdapter.js +295 -0
  61. package/dist/custom-tools/skill-tools/index.d.ts +33 -0
  62. package/dist/custom-tools/skill-tools/index.js +63 -0
  63. package/dist/custom-tools/skill-tools/tools/create/index.d.ts +2 -0
  64. package/dist/custom-tools/skill-tools/tools/create/index.js +93 -0
  65. package/dist/custom-tools/skill-tools/tools/create/schema.d.ts +6 -0
  66. package/dist/custom-tools/skill-tools/tools/create/schema.js +41 -0
  67. package/dist/custom-tools/skill-tools/tools/list/index.d.ts +2 -0
  68. package/dist/custom-tools/skill-tools/tools/list/index.js +113 -0
  69. package/dist/custom-tools/skill-tools/tools/list/schema.d.ts +6 -0
  70. package/dist/custom-tools/skill-tools/tools/list/schema.js +19 -0
  71. package/dist/custom-tools/skill-tools/tools/read/index.d.ts +2 -0
  72. package/dist/custom-tools/skill-tools/tools/read/index.js +124 -0
  73. package/dist/custom-tools/skill-tools/tools/read/schema.d.ts +6 -0
  74. package/dist/custom-tools/skill-tools/tools/read/schema.js +27 -0
  75. package/dist/custom-tools/skill-tools/tools/search/bm25.d.ts +71 -0
  76. package/dist/custom-tools/skill-tools/tools/search/bm25.js +305 -0
  77. package/dist/custom-tools/skill-tools/tools/search/index.d.ts +8 -0
  78. package/dist/custom-tools/skill-tools/tools/search/index.js +63 -0
  79. package/dist/custom-tools/skill-tools/tools/search/schema.d.ts +6 -0
  80. package/dist/custom-tools/skill-tools/tools/search/schema.js +19 -0
  81. package/dist/custom-tools/skill-tools/tools/search/skill-index.d.ts +54 -0
  82. package/dist/custom-tools/skill-tools/tools/search/skill-index.js +251 -0
  83. package/dist/custom-tools/skill-tools/tools/update/index.d.ts +2 -0
  84. package/dist/custom-tools/skill-tools/tools/update/index.js +115 -0
  85. package/dist/custom-tools/skill-tools/tools/update/schema.d.ts +6 -0
  86. package/dist/custom-tools/skill-tools/tools/update/schema.js +41 -0
  87. package/dist/screens/ChatScreen.d.ts +1 -0
  88. package/dist/screens/ChatScreen.js +327 -0
  89. package/dist/screens/HomeScreen.d.ts +1 -0
  90. package/dist/screens/HomeScreen.js +68 -0
  91. package/dist/screens/SettingsScreen.d.ts +1 -0
  92. package/dist/screens/SettingsScreen.js +35 -0
  93. package/dist/services/db.d.ts +31 -0
  94. package/dist/services/db.js +108 -0
  95. package/dist/theme/ThemeContext.d.ts +11 -0
  96. package/dist/theme/ThemeContext.js +31 -0
  97. package/dist/theme/theme.d.ts +17 -0
  98. package/dist/theme/theme.js +82 -0
  99. 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>;