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,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,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,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,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,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
|
+
}
|