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,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Text, Box } from 'ink';
4
+ import { marked } from 'marked';
5
+ export function Markdown({ children }) {
6
+ let tokens;
7
+ try {
8
+ tokens = marked.lexer(children);
9
+ }
10
+ catch {
11
+ // If lexer fails, fall back to plain text
12
+ return _jsx(Text, { children: children });
13
+ }
14
+ return _jsx(Box, { flexDirection: "column", children: renderTokens(tokens) });
15
+ }
16
+ function renderTokens(tokens) {
17
+ return tokens.map((token, i) => renderToken(token, i));
18
+ }
19
+ function renderToken(token, key) {
20
+ try {
21
+ switch (token.type) {
22
+ case 'paragraph':
23
+ return (_jsx(Box, { marginBottom: 1, flexDirection: "row", flexWrap: "wrap", children: renderInlineTokens(token.tokens ?? []) }, key));
24
+ case 'heading':
25
+ return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: extractPlainText(token.tokens ?? [], token.text) }) }, key));
26
+ case 'code':
27
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingX: 1, borderStyle: "round", borderColor: "gray", children: [token.lang ? (_jsx(Text, { color: "gray", dimColor: true, children: token.lang })) : null, _jsx(Text, { color: "yellowBright", children: token.text })] }, key));
28
+ case 'list':
29
+ return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: (token.items ?? []).map((item, j) => {
30
+ const itemTokens = item.tokens ?? [];
31
+ // Separate inline (text/paragraph) from block-level (nested lists, code, etc.)
32
+ const inlineTokens = itemTokens.filter((t) => t.type === 'text' || t.type === 'paragraph');
33
+ const blockTokens = itemTokens.filter((t) => t.type !== 'text' && t.type !== 'paragraph');
34
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: "gray", children: token.ordered ? `${j + 1}. ` : '• ' }), renderListItemContent(inlineTokens)] }), blockTokens.length > 0 && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: blockTokens.map((bt, k) => renderToken(bt, k)) }))] }, j));
35
+ }) }, key));
36
+ case 'blockquote':
37
+ return (_jsxs(Box, { marginBottom: 1, flexDirection: "row", paddingLeft: 1, children: [_jsx(Text, { color: "gray", children: "\u2502 " }), _jsx(Box, { flexDirection: "column", children: renderTokens(token.tokens ?? []) })] }, key));
38
+ case 'hr':
39
+ return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(40) }) }, key));
40
+ case 'space':
41
+ return null;
42
+ default:
43
+ // Fallback: try inline tokens if available
44
+ if (token.tokens && token.tokens.length > 0) {
45
+ return (_jsx(Box, { marginBottom: 1, flexDirection: "row", flexWrap: "wrap", children: renderInlineTokens(token.tokens) }, key));
46
+ }
47
+ return (_jsx(Box, { children: _jsx(Text, { children: token.text || token.raw }) }, key));
48
+ }
49
+ }
50
+ catch {
51
+ // If any token fails to render, show raw text as fallback
52
+ return (_jsx(Box, { children: _jsx(Text, { children: token.text || token.raw }) }, key));
53
+ }
54
+ }
55
+ /**
56
+ * List items contain nested tokens with type "text" (NOT "paragraph").
57
+ * These text tokens can themselves have nested inline tokens (bold, code, etc).
58
+ * We need to unwrap them so the bullet stays aligned with the formatted content.
59
+ */
60
+ function renderListItemContent(tokens) {
61
+ return tokens.map((token, i) => {
62
+ // List items wrap their content in either "text" or "paragraph" tokens
63
+ if (token.type === 'paragraph' || token.type === 'text') {
64
+ if (token.tokens && token.tokens.length > 0) {
65
+ return (_jsx(React.Fragment, { children: renderInlineTokens(token.tokens) }, i));
66
+ }
67
+ return _jsx(Text, { children: token.text }, i);
68
+ }
69
+ // Nested lists, code blocks, etc.
70
+ return renderToken(token, i);
71
+ });
72
+ }
73
+ /**
74
+ * Renders inline tokens: text, bold, italic, code, links, etc.
75
+ * Uses token.text (parsed content) instead of token.raw (includes markdown syntax).
76
+ */
77
+ function renderInlineTokens(tokens) {
78
+ return tokens.map((token, i) => {
79
+ try {
80
+ switch (token.type) {
81
+ case 'text':
82
+ // Text tokens can contain nested children (e.g. bold inside text)
83
+ if (token.tokens && token.tokens.length > 0) {
84
+ return _jsx(Text, { children: renderInlineTokens(token.tokens) }, i);
85
+ }
86
+ return _jsx(Text, { children: token.text }, i);
87
+ case 'strong':
88
+ if (token.tokens && token.tokens.length > 0) {
89
+ return (_jsx(Text, { bold: true, color: "whiteBright", children: renderInlineTokens(token.tokens) }, i));
90
+ }
91
+ return (_jsx(Text, { bold: true, color: "whiteBright", children: token.text }, i));
92
+ case 'em':
93
+ if (token.tokens && token.tokens.length > 0) {
94
+ return (_jsx(Text, { italic: true, color: "white", children: renderInlineTokens(token.tokens) }, i));
95
+ }
96
+ return (_jsx(Text, { italic: true, color: "white", children: token.text }, i));
97
+ case 'codespan':
98
+ return (_jsx(Text, { color: "yellow", backgroundColor: "#333333", children: ` ${token.text} ` }, i));
99
+ case 'link':
100
+ return (_jsx(Text, { color: "blueBright", underline: true, children: token.text }, i));
101
+ case 'escape':
102
+ return _jsx(Text, { children: token.text }, i);
103
+ case 'br':
104
+ return _jsx(Text, { children: '\n' }, i);
105
+ default:
106
+ return _jsx(Text, { children: token.text || token.raw }, i);
107
+ }
108
+ }
109
+ catch {
110
+ return _jsx(Text, { children: token.text || token.raw }, i);
111
+ }
112
+ });
113
+ }
114
+ /**
115
+ * Extract plain text from inline tokens for headings.
116
+ */
117
+ function extractPlainText(tokens, fallback) {
118
+ if (tokens.length === 0)
119
+ return fallback;
120
+ return tokens.map(t => t.text || t.raw).join('');
121
+ }
@@ -0,0 +1,12 @@
1
+ interface ProviderBarProps {
2
+ isFocused: boolean;
3
+ focusedIndex: number;
4
+ showModeSelect: boolean;
5
+ setShowModeSelect: (val: boolean) => void;
6
+ showModelSelect: boolean;
7
+ setShowModelSelect: (val: boolean) => void;
8
+ showHistorySelect: boolean;
9
+ setShowHistorySelect: (val: boolean) => void;
10
+ }
11
+ export declare function ProviderBar({ isFocused, focusedIndex, showModeSelect, setShowModeSelect, showModelSelect, setShowModelSelect, showHistorySelect, setShowHistorySelect, }: ProviderBarProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useTheme } from '../theme/ThemeContext.js';
4
+ import { useToolpack } from '../context/ToolpackContext.js';
5
+ export function ProviderBar({ isFocused, focusedIndex, showModeSelect, setShowModeSelect, showModelSelect, setShowModelSelect, showHistorySelect, setShowHistorySelect, }) {
6
+ const { theme } = useTheme();
7
+ const { activeMode, activeModel, models } = useToolpack();
8
+ useInput((_, key) => {
9
+ if (!isFocused)
10
+ return;
11
+ if (key.return && focusedIndex === 1) {
12
+ setShowModeSelect(!showModeSelect);
13
+ }
14
+ else if (key.return && focusedIndex === 2) {
15
+ setShowModelSelect(!showModelSelect);
16
+ }
17
+ else if (key.return && focusedIndex === 3) {
18
+ setShowHistorySelect(!showHistorySelect);
19
+ }
20
+ }, {
21
+ isActive: isFocused &&
22
+ (focusedIndex === 1 || focusedIndex === 2 || focusedIndex === 3),
23
+ });
24
+ const modeLabel = activeMode ? activeMode.displayName : 'All';
25
+ // Find the matching label for the activeModel key
26
+ const currentModelObj = models.find(m => m.value === activeModel);
27
+ const modelLabel = currentModelObj ? currentModelObj.label : activeModel;
28
+ const modeBgColor = isFocused && focusedIndex === 1 ? theme.colors.highlight : undefined;
29
+ const modelBgColor = isFocused && focusedIndex === 2 ? theme.colors.highlight : undefined;
30
+ const historyBgColor = isFocused && focusedIndex === 3 ? theme.colors.highlight : undefined;
31
+ return (_jsxs(Box, { width: "100%", paddingLeft: 1, paddingRight: 1, marginTop: 1, children: [_jsxs(Box, { gap: 4, flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.secondary, children: "Mode: " }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: modeBgColor, color: theme.colors.primary, children: modeLabel }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.secondary, children: "Model: " }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: modelBgColor, color: theme.colors.primary, children: modelLabel }) })] })] }), _jsx(Box, { children: _jsx(Box, { paddingX: 1, children: _jsx(Text, { backgroundColor: historyBgColor, color: theme.colors.primary, children: "History" }) }) })] }));
32
+ }
@@ -0,0 +1,8 @@
1
+ interface ShimmerTextProps {
2
+ text: string;
3
+ baseColor?: string;
4
+ brightColor?: string;
5
+ speed?: number;
6
+ }
7
+ export declare function ShimmerText({ text, baseColor, brightColor, speed, }: ShimmerTextProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Text } from 'ink';
4
+ export function ShimmerText({ text, baseColor = '#6b7280', brightColor = '#3b82f6', speed = 150, }) {
5
+ const [activeIndex, setActiveIndex] = useState(0);
6
+ useEffect(() => {
7
+ const interval = setInterval(() => {
8
+ setActiveIndex(prev => (prev + 1) % text.length);
9
+ }, speed);
10
+ return () => clearInterval(interval);
11
+ }, [text.length, speed]);
12
+ return (_jsx(Text, { children: text.split('').map((char, idx) => {
13
+ // Calculate distance from active index (circular)
14
+ const distance = Math.min(Math.abs(idx - activeIndex), text.length - Math.abs(idx - activeIndex));
15
+ // Bright if within 3 characters of active index
16
+ const isBright = distance <= 1;
17
+ const color = isBright ? brightColor : baseColor;
18
+ return (_jsx(Text, { color: color, children: char }, idx));
19
+ }) }));
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { ToolCallEntry } from '../context/ConversationContext.js';
2
+ interface ToolLogPopupProps {
3
+ toolCalls: ToolCallEntry[];
4
+ onClose: () => void;
5
+ }
6
+ export declare function ToolLogPopup({ toolCalls, onClose }: ToolLogPopupProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,87 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { Modal } from './common/Modal.js';
5
+ import { useTheme } from '../theme/ThemeContext.js';
6
+ export function ToolLogPopup({ toolCalls, onClose }) {
7
+ const { theme } = useTheme();
8
+ const [scrollOffset, setScrollOffset] = useState(0);
9
+ // Calculate total lines for all tool calls
10
+ const calculateTotalLines = () => {
11
+ let total = 0;
12
+ toolCalls.forEach(toolCall => {
13
+ total += 3; // Header + spacing
14
+ total += 2; // Arguments section
15
+ if (toolCall.result)
16
+ total += 2; // Result section
17
+ if (toolCall.error)
18
+ total += 2; // Error section
19
+ total += 2; // Divider + spacing
20
+ });
21
+ return total;
22
+ };
23
+ const totalLines = calculateTotalLines();
24
+ const visibleLines = 20; // Visible content area height
25
+ const maxScroll = Math.max(0, totalLines - visibleLines);
26
+ // Scroll handlers
27
+ useInput((_, key) => {
28
+ if (key.downArrow || key.pageDown) {
29
+ setScrollOffset(prev => Math.min(maxScroll, prev + (key.pageDown ? 5 : 1)));
30
+ }
31
+ else if (key.upArrow || key.pageUp) {
32
+ setScrollOffset(prev => Math.max(0, prev - (key.pageUp ? 5 : 1)));
33
+ }
34
+ }, { isActive: true });
35
+ const formatTimestamp = (timestamp) => {
36
+ const now = Date.now();
37
+ const diff = now - timestamp;
38
+ const seconds = Math.floor(diff / 1000);
39
+ const minutes = Math.floor(seconds / 60);
40
+ const hours = Math.floor(minutes / 60);
41
+ if (seconds < 60)
42
+ return `${seconds}s ago`;
43
+ if (minutes < 60)
44
+ return `${minutes}m ago`;
45
+ if (hours < 24)
46
+ return `${hours}h ago`;
47
+ return new Date(timestamp).toLocaleString();
48
+ };
49
+ // Render tool calls as lines
50
+ const renderLines = [];
51
+ toolCalls.forEach((toolCall, idx) => {
52
+ const truncateText = (text, maxLen) => {
53
+ if (text.length <= maxLen)
54
+ return text;
55
+ return text.slice(0, maxLen) + '...';
56
+ };
57
+ // Header line
58
+ renderLines.push(`#${idx + 1} ${toolCall.name} • ${formatTimestamp(toolCall.timestamp)} • ${toolCall.status}`);
59
+ renderLines.push('');
60
+ // Arguments
61
+ renderLines.push(' Arguments:');
62
+ renderLines.push(` ${truncateText(JSON.stringify(toolCall.arguments), 200)}`);
63
+ // Result
64
+ if (toolCall.result) {
65
+ renderLines.push(' Result:');
66
+ const resultLines = truncateText(toolCall.result, 500).split('\n');
67
+ resultLines.forEach(line => {
68
+ renderLines.push(` ${line}`);
69
+ });
70
+ }
71
+ // Error
72
+ if (toolCall.error) {
73
+ renderLines.push(' Error:');
74
+ renderLines.push(` ${truncateText(toolCall.error, 200)}`);
75
+ }
76
+ // Divider
77
+ if (idx < toolCalls.length - 1) {
78
+ renderLines.push('');
79
+ renderLines.push('─'.repeat(80));
80
+ renderLines.push('');
81
+ }
82
+ });
83
+ const visibleContent = renderLines.slice(scrollOffset, scrollOffset + visibleLines);
84
+ const canScrollUp = scrollOffset > 0;
85
+ const canScrollDown = scrollOffset < maxScroll;
86
+ return (_jsx(Modal, { title: "Tool Execution Log", width: 90, height: 30, onClose: onClose, children: _jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Box, { marginBottom: 1, paddingX: 2, children: _jsxs(Text, { color: theme.colors.textSecondary, children: ["Total tool calls: ", toolCalls.length, totalLines > visibleLines && (_jsxs(Text, { color: theme.colors.textSecondary, children: [' • ', "Line ", scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, totalLines), " of", ' ', totalLines] }))] }) }), canScrollUp && (_jsx(Box, { paddingX: 2, marginBottom: 1, children: _jsx(Text, { color: "#fbbf24", children: "\u25B2 More above \u25B2" }) })), _jsx(Box, { flexDirection: "column", paddingX: 2, height: visibleLines, children: visibleContent.map((line, idx) => (_jsx(Box, { children: _jsx(Text, { color: theme.colors.text, children: line }) }, idx))) }), canScrollDown && (_jsx(Box, { paddingX: 2, marginTop: 1, children: _jsx(Text, { color: "#fbbf24", children: "\u25BC More below \u25BC" }) })), _jsx(Box, { marginTop: 1, paddingTop: 1, borderTop: true, borderColor: theme.colors.border, paddingX: 2, children: _jsxs(Text, { color: theme.colors.textSecondary, children: [_jsx(Text, { color: "#fbbf24", bold: true, children: "\u2191/\u2193 PgUp/PgDn" }), _jsx(Text, { children: " :Scroll | " }), _jsx(Text, { color: "#fbbf24", bold: true, children: "Esc" }), _jsx(Text, { children: " :Close" })] }) })] }) }));
87
+ }
@@ -0,0 +1,6 @@
1
+ interface HistorySelectProps {
2
+ onSelect: (id: string, label: string) => void;
3
+ onClose: () => void;
4
+ }
5
+ export declare function HistorySelect({ onSelect, onClose }: HistorySelectProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } 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 { Modal } from './Modal.js';
8
+ export function HistorySelect({ onSelect, onClose }) {
9
+ const { theme } = useTheme();
10
+ const { history } = useConversation();
11
+ const [search, setSearch] = useState('');
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ const [isSearchFocused, setIsSearchFocused] = useState(true);
14
+ // Filter history entries by search query
15
+ const filtered = history.filter(entry => entry.label.toLowerCase().includes(search.toLowerCase()));
16
+ // Height calculations:
17
+ // Modal chrome (borders + padding + title space) = 6 lines
18
+ // Search Box (borders + padding + margin) = 4 lines
19
+ // List = visibleItems lines
20
+ const visibleItems = Math.max(1, Math.min(filtered.length, 6)); // At least 1 for the empty state
21
+ const modalHeight = 6 + 4 + visibleItems;
22
+ useInput((_, key) => {
23
+ if (isSearchFocused) {
24
+ if (key.downArrow || key.tab) {
25
+ setIsSearchFocused(false);
26
+ setSelectedIndex(0);
27
+ }
28
+ return;
29
+ }
30
+ // List navigation
31
+ if (key.upArrow) {
32
+ if (selectedIndex === 0) {
33
+ setIsSearchFocused(true);
34
+ }
35
+ else {
36
+ setSelectedIndex(prev => Math.max(0, prev - 1));
37
+ }
38
+ }
39
+ else if (key.downArrow) {
40
+ setSelectedIndex(prev => Math.min(filtered.length - 1, prev + 1));
41
+ }
42
+ else if (key.return) {
43
+ const entry = filtered[selectedIndex];
44
+ if (entry)
45
+ onSelect(entry.id, entry.label);
46
+ }
47
+ }, { isActive: true });
48
+ return (_jsxs(Modal, { title: "History", height: modalHeight, width: 60, onClose: onClose, children: [_jsxs(Box, { marginBottom: 1, borderStyle: "single", borderColor: isSearchFocused ? theme.colors.primary : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.textMuted, children: '🔍 ' }), _jsx(TextInput, { value: search, onChange: val => {
49
+ setSearch(val);
50
+ setSelectedIndex(0);
51
+ }, placeholder: "Search history...", focus: isSearchFocused })] }), _jsx(Box, { flexDirection: "column", children: filtered.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, children: " No entries found." })) : (filtered.slice(0, 6).map((entry, index) => {
52
+ const isSelected = !isSearchFocused && index === selectedIndex;
53
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { backgroundColor: isSelected ? theme.colors.highlight : undefined, color: isSelected ? theme.colors.text : theme.colors.textSecondary, children: [isSelected ? '❯ ' : ' ', entry.label.length > 40
54
+ ? entry.label.slice(0, 37) + '...'
55
+ : entry.label] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", entry.timestamp] })] }, entry.id));
56
+ })) })] }));
57
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ interface ModalProps {
3
+ title: string;
4
+ width?: number;
5
+ height: number;
6
+ children: React.ReactNode;
7
+ onClose?: () => void;
8
+ }
9
+ export declare function Modal({ title, width, height, children, onClose, }: ModalProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout, useInput } from 'ink';
3
+ import { useTheme } from '../../theme/ThemeContext.js';
4
+ export function Modal({ title, width = 50, height, children, onClose, }) {
5
+ const { stdout } = useStdout();
6
+ const { theme } = useTheme();
7
+ useInput((_, key) => {
8
+ if (key.escape && onClose) {
9
+ onClose();
10
+ }
11
+ }, { isActive: true });
12
+ return (_jsx(Box, { position: "absolute", width: stdout.columns, height: stdout.rows, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { position: "relative", width: width, height: height, children: [_jsx(Box, { position: "absolute", width: width, height: height, flexDirection: "column", children: Array.from({ length: height }).map((_, i) => (_jsx(Text, { backgroundColor: theme.colors.bg, children: ' '.repeat(width) }, i))) }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 2, paddingY: 1, width: width, height: height, children: [_jsxs(Box, { justifyContent: "space-between", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.text, children: title }), _jsx(Box, { gap: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: "esc [x]" }) })] }), children] })] }) }));
13
+ }
@@ -0,0 +1,6 @@
1
+ interface ModeSelectProps {
2
+ onSelect: (mode: string) => void;
3
+ onClose: () => void;
4
+ }
5
+ export declare function ModeSelect({ onSelect, onClose }: ModeSelectProps): import("react/jsx-runtime").JSX.Element;
6
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import SelectInput from 'ink-select-input';
3
+ import { useToolpack } from '../../context/ToolpackContext.js';
4
+ import { Modal } from './Modal.js';
5
+ export function ModeSelect({ onSelect, onClose }) {
6
+ const { modes } = useToolpack();
7
+ const items = modes.map(m => ({
8
+ label: m.displayName || m.name,
9
+ value: m.name,
10
+ }));
11
+ const height = 6 + items.length;
12
+ return (_jsx(Modal, { title: "Select Mode", height: height, onClose: onClose, children: _jsx(SelectInput, { items: items, onSelect: item => onSelect(item.value) }) }));
13
+ }
@@ -0,0 +1,9 @@
1
+ interface ModelSelectProps {
2
+ onSelect: (model: {
3
+ value: string;
4
+ provider?: string;
5
+ }) => void;
6
+ onClose: () => void;
7
+ }
8
+ export declare function ModelSelect({ onSelect, onClose }: ModelSelectProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import { useToolpack } from '../../context/ToolpackContext.js';
5
+ import { Modal } from './Modal.js';
6
+ export function ModelSelect({ onSelect, onClose }) {
7
+ const { setModel, modelCategories, loadingModels } = useToolpack();
8
+ if (loadingModels) {
9
+ return (_jsx(Modal, { title: "Select Model", height: 6, onClose: onClose, children: _jsx(Text, { color: "yellow", children: "Loading available models..." }) }));
10
+ }
11
+ if (modelCategories.length === 0) {
12
+ return (_jsx(Modal, { title: "Select Model", height: 6, onClose: onClose, children: _jsx(Text, { color: "red", children: "No models available. Please check provider configuration." }) }));
13
+ }
14
+ const items = [];
15
+ modelCategories.forEach(c => {
16
+ items.push({
17
+ label: `--- ${c.displayName} ---`,
18
+ value: `__category_${c.provider}`,
19
+ key: `cat_${c.provider}`,
20
+ });
21
+ c.models.forEach((m, idx) => {
22
+ // Add indicator if model doesn't support tools
23
+ const toolIndicator = m.capabilities?.toolCalling ? '' : ' [no tools]';
24
+ items.push({
25
+ label: ` ${m.label}${toolIndicator}`,
26
+ value: m.value,
27
+ provider: c.provider,
28
+ capabilities: m.capabilities,
29
+ key: `${c.provider}_${m.value}_${idx}`,
30
+ });
31
+ });
32
+ });
33
+ const height = 6 + Math.min(items.length, 20); // cap height to 20 items max
34
+ return (_jsx(Modal, { title: "Select Model", height: height, onClose: onClose, children: _jsx(SelectInput, { items: items, limit: 18, onSelect: (item) => {
35
+ if (item.value.startsWith('__category_'))
36
+ return; // Ignore headers
37
+ const modelObj = {
38
+ value: item.value,
39
+ provider: item.provider,
40
+ capabilities: item.capabilities,
41
+ };
42
+ setModel(modelObj);
43
+ onSelect(modelObj);
44
+ } }) }));
45
+ }
@@ -0,0 +1,44 @@
1
+ import { ReactNode } from 'react';
2
+ import type { ToolCallEntry } from '../services/db.js';
3
+ export interface HistoryEntry {
4
+ id: string;
5
+ label: string;
6
+ timestamp: string;
7
+ }
8
+ export interface Message {
9
+ id: string;
10
+ role: 'user' | 'assistant' | 'system';
11
+ content: string;
12
+ timestamp: Date;
13
+ toolCalls?: ToolCallEntry[];
14
+ }
15
+ export type { ToolCallEntry };
16
+ export interface Conversation {
17
+ id: string;
18
+ title: string;
19
+ mode?: string;
20
+ model?: string;
21
+ messages: Message[];
22
+ createdAt: Date;
23
+ updatedAt: Date;
24
+ }
25
+ interface ConversationContextType {
26
+ history: HistoryEntry[];
27
+ activeConversation: Conversation | null;
28
+ createConversation: (title: string, mode?: string, model?: string) => void;
29
+ loadConversation: (id: string) => {
30
+ mode?: string;
31
+ model?: string;
32
+ };
33
+ addMessage: (role: 'user' | 'assistant' | 'system', content: string, toolCalls?: ToolCallEntry[]) => void;
34
+ clearHistory: () => void;
35
+ reloadHistory: () => void;
36
+ screen: 'home' | 'chat' | 'settings';
37
+ setScreen: (screen: 'home' | 'chat' | 'settings') => void;
38
+ pendingPrompt: string | null;
39
+ setPendingPrompt: (val: string | null) => void;
40
+ }
41
+ export declare function ConversationProvider({ children }: {
42
+ children: ReactNode;
43
+ }): import("react/jsx-runtime").JSX.Element;
44
+ export declare function useConversation(): ConversationContextType;
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useEffect } from 'react';
3
+ import * as dbManager from '../services/db.js';
4
+ const ConversationContext = createContext(undefined);
5
+ export function ConversationProvider({ children }) {
6
+ const [history, setHistory] = useState([]);
7
+ const [activeConversation, setActiveConversation] = useState(null);
8
+ const [screen, setScreen] = useState('home');
9
+ const [pendingPrompt, setPendingPrompt] = useState(null);
10
+ const reloadHistory = () => {
11
+ try {
12
+ const dbConvs = dbManager.getConversations();
13
+ setHistory(dbConvs.map(c => ({
14
+ id: c.id,
15
+ label: c.title,
16
+ timestamp: new Date(c.createdAt).toLocaleString(),
17
+ })));
18
+ }
19
+ catch (e) {
20
+ console.error('Failed to load history', e);
21
+ }
22
+ };
23
+ useEffect(() => {
24
+ reloadHistory();
25
+ }, []);
26
+ const createConversation = (title, mode, model) => {
27
+ const dbConv = dbManager.createConversation(title, mode, model);
28
+ const newConversation = {
29
+ id: dbConv.id,
30
+ title: dbConv.title,
31
+ mode: dbConv.mode,
32
+ model: dbConv.model,
33
+ messages: [],
34
+ createdAt: new Date(dbConv.createdAt),
35
+ updatedAt: new Date(dbConv.createdAt),
36
+ };
37
+ setActiveConversation(newConversation);
38
+ reloadHistory();
39
+ setPendingPrompt(title);
40
+ setScreen('chat');
41
+ };
42
+ const loadConversation = (id) => {
43
+ const dbMsgs = dbManager.getMessages(id);
44
+ const dbConvs = dbManager.getConversations();
45
+ const info = dbConvs.find(c => c.id === id);
46
+ if (!info)
47
+ return {};
48
+ const messages = dbMsgs.map(m => ({
49
+ id: m.id,
50
+ role: m.role,
51
+ content: m.content,
52
+ timestamp: new Date(m.createdAt),
53
+ toolCalls: m.toolCalls,
54
+ }));
55
+ setActiveConversation({
56
+ id: info.id,
57
+ title: info.title,
58
+ mode: info.mode,
59
+ model: info.model,
60
+ createdAt: new Date(info.createdAt),
61
+ updatedAt: new Date(),
62
+ messages,
63
+ });
64
+ setScreen('chat');
65
+ // Return mode and model so caller can restore them
66
+ return { mode: info.mode, model: info.model };
67
+ };
68
+ const addMessage = (role, content, toolCalls) => {
69
+ if (!activeConversation)
70
+ return;
71
+ const dbMsg = dbManager.saveMessage(activeConversation.id, role, content, toolCalls);
72
+ const newMessage = {
73
+ id: dbMsg.id,
74
+ role: dbMsg.role,
75
+ content: dbMsg.content,
76
+ timestamp: new Date(dbMsg.createdAt),
77
+ toolCalls: dbMsg.toolCalls,
78
+ };
79
+ setActiveConversation(prev => {
80
+ if (!prev)
81
+ return prev;
82
+ return {
83
+ ...prev,
84
+ messages: [...prev.messages, newMessage],
85
+ updatedAt: new Date(dbMsg.createdAt),
86
+ };
87
+ });
88
+ };
89
+ const clearHistory = () => {
90
+ setActiveConversation(null);
91
+ setScreen('home');
92
+ };
93
+ return (_jsx(ConversationContext.Provider, { value: {
94
+ history,
95
+ activeConversation,
96
+ createConversation,
97
+ loadConversation,
98
+ addMessage,
99
+ clearHistory,
100
+ reloadHistory,
101
+ screen,
102
+ setScreen,
103
+ pendingPrompt,
104
+ setPendingPrompt,
105
+ }, children: children }));
106
+ }
107
+ export function useConversation() {
108
+ const context = useContext(ConversationContext);
109
+ if (!context) {
110
+ throw new Error('useConversation must be used within a ConversationProvider');
111
+ }
112
+ return context;
113
+ }