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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Registry
|
|
3
|
+
* Manages command registration and lookup
|
|
4
|
+
*/
|
|
5
|
+
const commands = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Register a command
|
|
8
|
+
*/
|
|
9
|
+
export function registerCommand(command) {
|
|
10
|
+
commands.set(command.name, command);
|
|
11
|
+
if (command.aliases) {
|
|
12
|
+
for (const alias of command.aliases) {
|
|
13
|
+
commands.set(alias, command);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get a command by name
|
|
19
|
+
*/
|
|
20
|
+
export function getCommand(name) {
|
|
21
|
+
return commands.get(name.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get all unique commands (excluding aliases)
|
|
25
|
+
*/
|
|
26
|
+
export function getAllCommands() {
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const cmd of commands.values()) {
|
|
30
|
+
if (!seen.has(cmd.name)) {
|
|
31
|
+
seen.add(cmd.name);
|
|
32
|
+
result.push(cmd);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if input is a command
|
|
39
|
+
*/
|
|
40
|
+
export function isCommand(input) {
|
|
41
|
+
return input.trim().startsWith('/');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse command input into name and arguments
|
|
45
|
+
*/
|
|
46
|
+
export function parseCommand(input) {
|
|
47
|
+
const trimmed = input.trim();
|
|
48
|
+
if (!trimmed.startsWith('/')) {
|
|
49
|
+
return { name: '', args: [] };
|
|
50
|
+
}
|
|
51
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
52
|
+
const name = parts[0]?.toLowerCase() || '';
|
|
53
|
+
const args = parts.slice(1);
|
|
54
|
+
return { name, args };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Execute a command
|
|
58
|
+
*/
|
|
59
|
+
export async function executeCommand(input, context) {
|
|
60
|
+
const { name, args } = parseCommand(input);
|
|
61
|
+
if (!name) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
message: 'Invalid command. Type /help for available commands.',
|
|
65
|
+
action: 'display',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const command = getCommand(name);
|
|
69
|
+
if (!command) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
message: `Unknown command: /${name}. Type /help for available commands.`,
|
|
73
|
+
action: 'display',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return await command.execute(args, context);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
message: `Command error: ${error.message || 'Unknown error'}`,
|
|
83
|
+
action: 'display',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerCommand } from './registry.js';
|
|
2
|
+
const toolLogCommand = {
|
|
3
|
+
name: 'tool_log',
|
|
4
|
+
aliases: ['tl', 'log'],
|
|
5
|
+
description: 'Show tool execution log for current session',
|
|
6
|
+
usage: '/tool_log',
|
|
7
|
+
execute: async () => {
|
|
8
|
+
// Tool history is now managed via SDK events in ToolpackContext.
|
|
9
|
+
// ChatScreen will display the popup using toolHistory from context.
|
|
10
|
+
return {
|
|
11
|
+
success: true,
|
|
12
|
+
action: 'popup',
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
registerCommand(toolLogCommand);
|
|
17
|
+
export default toolLogCommand;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { registerCommand } from './registry.js';
|
|
2
|
+
const toolSearchCommand = {
|
|
3
|
+
name: 'tool_search',
|
|
4
|
+
aliases: ['ts', 'search'],
|
|
5
|
+
description: 'Search for tools by query',
|
|
6
|
+
usage: '/tool_search <query>',
|
|
7
|
+
execute: (args, context) => {
|
|
8
|
+
if (args.length === 0) {
|
|
9
|
+
return {
|
|
10
|
+
success: false,
|
|
11
|
+
message: 'Usage: /tool_search <query>',
|
|
12
|
+
action: 'display',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (!context.toolpack) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
message: 'Toolpack not initialized',
|
|
19
|
+
action: 'display',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const query = args.join(' ');
|
|
23
|
+
const registry = context.toolpack.getToolRegistry?.();
|
|
24
|
+
if (!registry) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
message: 'Tool registry not available',
|
|
28
|
+
action: 'display',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Simple search through tool names and descriptions
|
|
32
|
+
const tools = registry.getAll();
|
|
33
|
+
const queryLower = query.toLowerCase();
|
|
34
|
+
const matches = tools.filter((t) => t.name.toLowerCase().includes(queryLower) ||
|
|
35
|
+
t.description?.toLowerCase().includes(queryLower) ||
|
|
36
|
+
t.displayName?.toLowerCase().includes(queryLower));
|
|
37
|
+
if (matches.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
message: `No tools found matching: "${query}"`,
|
|
41
|
+
action: 'display',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const lines = [
|
|
45
|
+
`**Tools matching "${query}":** ${matches.length} found`,
|
|
46
|
+
'',
|
|
47
|
+
...matches.map((t) => `- **${t.name}**: ${t.description || 'No description'}`),
|
|
48
|
+
];
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
message: lines.join('\n'),
|
|
52
|
+
action: 'display',
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
registerCommand(toolSearchCommand);
|
|
57
|
+
export default toolSearchCommand;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { registerCommand } from './registry.js';
|
|
2
|
+
const toolsCommand = {
|
|
3
|
+
name: 'tools',
|
|
4
|
+
aliases: ['t'],
|
|
5
|
+
description: 'List available tools',
|
|
6
|
+
execute: (_, context) => {
|
|
7
|
+
if (!context.toolpack) {
|
|
8
|
+
return {
|
|
9
|
+
success: false,
|
|
10
|
+
message: 'Toolpack not initialized',
|
|
11
|
+
action: 'display',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const registry = context.toolpack.getToolRegistry?.();
|
|
15
|
+
if (!registry) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
message: 'Tool registry not available',
|
|
19
|
+
action: 'display',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const tools = registry.getAll();
|
|
23
|
+
const byCategory = new Map();
|
|
24
|
+
for (const tool of tools) {
|
|
25
|
+
const cat = tool.category || 'uncategorized';
|
|
26
|
+
if (!byCategory.has(cat)) {
|
|
27
|
+
byCategory.set(cat, []);
|
|
28
|
+
}
|
|
29
|
+
byCategory.get(cat).push(tool.name);
|
|
30
|
+
}
|
|
31
|
+
const lines = [`**Available Tools:** ${tools.length} total`, ''];
|
|
32
|
+
for (const [category, toolNames] of byCategory) {
|
|
33
|
+
lines.push(`**${category}** (${toolNames.length}):`);
|
|
34
|
+
lines.push(toolNames.map(n => ` - ${n}`).join('\n'));
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
success: true,
|
|
39
|
+
message: lines.join('\n'),
|
|
40
|
+
action: 'display',
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
registerCommand(toolsCommand);
|
|
45
|
+
export default toolsCommand;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command types and interfaces
|
|
3
|
+
*/
|
|
4
|
+
export interface CommandContext {
|
|
5
|
+
toolpack: any;
|
|
6
|
+
activeMode: any;
|
|
7
|
+
activeModel: string;
|
|
8
|
+
activeConversation: any;
|
|
9
|
+
addMessage: (role: 'user' | 'assistant' | 'system', content: string) => void;
|
|
10
|
+
clearHistory: () => void;
|
|
11
|
+
setScreen: (screen: 'home' | 'chat') => void;
|
|
12
|
+
}
|
|
13
|
+
export interface CommandResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
message?: string;
|
|
16
|
+
action?: 'display' | 'navigate' | 'silent' | 'popup';
|
|
17
|
+
popupData?: any;
|
|
18
|
+
}
|
|
19
|
+
export interface Command {
|
|
20
|
+
name: string;
|
|
21
|
+
aliases?: string[];
|
|
22
|
+
description: string;
|
|
23
|
+
usage?: string;
|
|
24
|
+
execute: (args: string[], context: CommandContext) => Promise<CommandResult> | CommandResult;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { registerCommand } from './registry.js';
|
|
2
|
+
const versionCommand = {
|
|
3
|
+
name: 'version',
|
|
4
|
+
aliases: ['v'],
|
|
5
|
+
description: 'Show version information',
|
|
6
|
+
execute: async () => {
|
|
7
|
+
const { readFileSync } = await import('fs');
|
|
8
|
+
const { join } = await import('path');
|
|
9
|
+
let version = 'unknown';
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
|
|
12
|
+
version = pkg.version || 'unknown';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Ignore
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
message: `**Toolpack SDK CLI** v${version}`,
|
|
20
|
+
action: 'display',
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
registerCommand(versionCommand);
|
|
25
|
+
export default versionCommand;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function AppInfo(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useTheme } from '../theme/ThemeContext.js';
|
|
4
|
+
export function AppInfo() {
|
|
5
|
+
const { theme } = useTheme();
|
|
6
|
+
// Hardcoded for now, mimicking original behavior
|
|
7
|
+
const currentPath = process.cwd();
|
|
8
|
+
const version = 'v1.0.0';
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginBottom: 2, children: [_jsx(Text, { color: theme.colors.textSecondary, children: currentPath }), _jsx(Text, { color: theme.colors.textSecondary, children: version })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface HomeInputProps {
|
|
2
|
+
focusedIndex: number;
|
|
3
|
+
showModeSelect: boolean;
|
|
4
|
+
setShowModeSelect: (val: boolean) => void;
|
|
5
|
+
showModelSelect: boolean;
|
|
6
|
+
setShowModelSelect: (val: boolean) => void;
|
|
7
|
+
showHistorySelect: boolean;
|
|
8
|
+
setShowHistorySelect: (val: boolean) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function HomeInput({ focusedIndex, showModeSelect, setShowModeSelect, showModelSelect, setShowModelSelect, showHistorySelect, setShowHistorySelect, }: HomeInputProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useEffect, useRef } 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 { ProviderBar } from './ProviderBar.js';
|
|
8
|
+
import clipboardy from 'clipboardy';
|
|
9
|
+
// Bracketed paste mode sequences
|
|
10
|
+
const PASTE_START = '\x1b[200~';
|
|
11
|
+
const PASTE_END = '\x1b[201~';
|
|
12
|
+
/**
|
|
13
|
+
* Wraps text to fit within a given width, preserving existing newlines.
|
|
14
|
+
*/
|
|
15
|
+
function wrapText(text, maxWidth) {
|
|
16
|
+
if (maxWidth <= 0)
|
|
17
|
+
return text.split('\n');
|
|
18
|
+
const result = [];
|
|
19
|
+
const paragraphs = text.split('\n');
|
|
20
|
+
for (const paragraph of paragraphs) {
|
|
21
|
+
if (paragraph.length <= maxWidth) {
|
|
22
|
+
result.push(paragraph);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Word-wrap long lines
|
|
26
|
+
let remaining = paragraph;
|
|
27
|
+
while (remaining.length > maxWidth) {
|
|
28
|
+
// Try to break at a space
|
|
29
|
+
let breakPoint = remaining.lastIndexOf(' ', maxWidth);
|
|
30
|
+
if (breakPoint <= 0) {
|
|
31
|
+
// No space found, hard break
|
|
32
|
+
breakPoint = maxWidth;
|
|
33
|
+
}
|
|
34
|
+
result.push(remaining.slice(0, breakPoint));
|
|
35
|
+
remaining = remaining.slice(breakPoint).trimStart();
|
|
36
|
+
}
|
|
37
|
+
if (remaining) {
|
|
38
|
+
result.push(remaining);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function sanitizeText(input) {
|
|
45
|
+
// Remove ANSI escape sequences
|
|
46
|
+
let sanitized = input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
47
|
+
// Remove any remaining escape characters
|
|
48
|
+
sanitized = sanitized.replace(/\x1b/g, '');
|
|
49
|
+
// Normalize line endings
|
|
50
|
+
sanitized = sanitized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
51
|
+
// Convert tabs to spaces
|
|
52
|
+
sanitized = sanitized.replace(/\t/g, ' ');
|
|
53
|
+
return sanitized;
|
|
54
|
+
}
|
|
55
|
+
export function HomeInput({ focusedIndex, showModeSelect, setShowModeSelect, showModelSelect, setShowModelSelect, showHistorySelect, setShowHistorySelect, }) {
|
|
56
|
+
const { theme } = useTheme();
|
|
57
|
+
const { stdout } = useStdout();
|
|
58
|
+
const [query, setQuery] = useState('');
|
|
59
|
+
const [cursorPos, setCursorPos] = useState(0);
|
|
60
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
61
|
+
// Ref to track current cursor position for use in closures
|
|
62
|
+
const cursorPosRef = useRef(0);
|
|
63
|
+
cursorPosRef.current = cursorPos;
|
|
64
|
+
// Ref to track current query for use in closures
|
|
65
|
+
const queryRef = useRef('');
|
|
66
|
+
queryRef.current = query;
|
|
67
|
+
// Refs for bracketed paste handling
|
|
68
|
+
const isPastingRef = useRef(false);
|
|
69
|
+
const pasteBufferRef = useRef('');
|
|
70
|
+
// Hardcode the max visible lines the input block is allowed to consume
|
|
71
|
+
const MAX_VISIBLE_LINES = 4;
|
|
72
|
+
// Calculate available width for text wrapping
|
|
73
|
+
// The input box is 80% of terminal width, minus border (2) and padding (2)
|
|
74
|
+
const boxWidth = Math.floor((stdout.columns || 80) * 0.8);
|
|
75
|
+
const textWidth = Math.max(10, boxWidth - 6); // 2 for border + 2 for paddingX on each side
|
|
76
|
+
// Wrap text to fit within the available width
|
|
77
|
+
const wrappedLines = wrapText(query, textWidth);
|
|
78
|
+
// focusedIndex = 0 means the text input has focus
|
|
79
|
+
const isInputFocused = focusedIndex === 0 && !showModeSelect && !showModelSelect;
|
|
80
|
+
const { createConversation } = useConversation();
|
|
81
|
+
const { activeMode, activeModel } = useToolpack();
|
|
82
|
+
// Auto-scroll to the bottom when the user adds new lines
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (wrappedLines.length > MAX_VISIBLE_LINES) {
|
|
85
|
+
setScrollOffset(wrappedLines.length - MAX_VISIBLE_LINES);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
setScrollOffset(0);
|
|
89
|
+
}
|
|
90
|
+
}, [wrappedLines.length]);
|
|
91
|
+
// Ref to track if we handled an escape sequence (to prevent useInput from processing it)
|
|
92
|
+
const handledEscapeRef = useRef(false);
|
|
93
|
+
// Handle raw stdin for bracketed paste mode, mouse wheel, and Option+arrow keys
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!isInputFocused)
|
|
96
|
+
return;
|
|
97
|
+
const handleData = (data) => {
|
|
98
|
+
const str = data.toString();
|
|
99
|
+
// Option+Left (Esc+b) - move word backward
|
|
100
|
+
if (str === '\x1bb' || str === '\x1bB') {
|
|
101
|
+
handledEscapeRef.current = true;
|
|
102
|
+
const q = queryRef.current;
|
|
103
|
+
let pos = cursorPosRef.current - 1;
|
|
104
|
+
// Skip whitespace
|
|
105
|
+
while (pos > 0 && /\s/.test(q.charAt(pos)))
|
|
106
|
+
pos--;
|
|
107
|
+
// Find start of word
|
|
108
|
+
while (pos > 0 && !/\s/.test(q.charAt(pos - 1)))
|
|
109
|
+
pos--;
|
|
110
|
+
setCursorPos(Math.max(0, pos));
|
|
111
|
+
setTimeout(() => { handledEscapeRef.current = false; }, 10);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Option+Right (Esc+f) - move word forward
|
|
115
|
+
if (str === '\x1bf' || str === '\x1bF') {
|
|
116
|
+
handledEscapeRef.current = true;
|
|
117
|
+
const q = queryRef.current;
|
|
118
|
+
let pos = cursorPosRef.current;
|
|
119
|
+
// Skip current word
|
|
120
|
+
while (pos < q.length && !/\s/.test(q.charAt(pos)))
|
|
121
|
+
pos++;
|
|
122
|
+
// Skip whitespace
|
|
123
|
+
while (pos < q.length && /\s/.test(q.charAt(pos)))
|
|
124
|
+
pos++;
|
|
125
|
+
setCursorPos(pos);
|
|
126
|
+
setTimeout(() => { handledEscapeRef.current = false; }, 10);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Check for bracketed paste start
|
|
130
|
+
if (str.includes(PASTE_START)) {
|
|
131
|
+
isPastingRef.current = true;
|
|
132
|
+
// Extract content after paste start marker
|
|
133
|
+
const startIdx = str.indexOf(PASTE_START) + PASTE_START.length;
|
|
134
|
+
let content = str.slice(startIdx);
|
|
135
|
+
// Check if paste end is in the same chunk
|
|
136
|
+
if (content.includes(PASTE_END)) {
|
|
137
|
+
const endIdx = content.indexOf(PASTE_END);
|
|
138
|
+
content = content.slice(0, endIdx);
|
|
139
|
+
isPastingRef.current = false;
|
|
140
|
+
const sanitized = sanitizeText(content);
|
|
141
|
+
const pos = cursorPosRef.current;
|
|
142
|
+
setQuery(prev => prev.slice(0, pos) + sanitized + prev.slice(pos));
|
|
143
|
+
setCursorPos(pos + sanitized.length);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
pasteBufferRef.current = content;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// If we're in paste mode, accumulate content
|
|
151
|
+
if (isPastingRef.current) {
|
|
152
|
+
if (str.includes(PASTE_END)) {
|
|
153
|
+
const endIdx = str.indexOf(PASTE_END);
|
|
154
|
+
pasteBufferRef.current += str.slice(0, endIdx);
|
|
155
|
+
isPastingRef.current = false;
|
|
156
|
+
const sanitized = sanitizeText(pasteBufferRef.current);
|
|
157
|
+
const pos = cursorPosRef.current;
|
|
158
|
+
setQuery(prev => prev.slice(0, pos) + sanitized + prev.slice(pos));
|
|
159
|
+
setCursorPos(pos + sanitized.length);
|
|
160
|
+
pasteBufferRef.current = '';
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
pasteBufferRef.current += str;
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Handle mouse wheel events
|
|
168
|
+
const match = str.match(/\x1b\[<(64|65);\d+;\d+M/);
|
|
169
|
+
if (match && match[1]) {
|
|
170
|
+
const button = parseInt(match[1], 10);
|
|
171
|
+
if (button === 64) {
|
|
172
|
+
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
173
|
+
}
|
|
174
|
+
else if (button === 65) {
|
|
175
|
+
const maxOffset = Math.max(0, wrappedLines.length - MAX_VISIBLE_LINES);
|
|
176
|
+
setScrollOffset(prev => Math.min(maxOffset, prev + 1));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
process.stdin.on('data', handleData);
|
|
181
|
+
return () => {
|
|
182
|
+
process.stdin.off('data', handleData);
|
|
183
|
+
};
|
|
184
|
+
}, [isInputFocused, wrappedLines.length, MAX_VISIBLE_LINES]);
|
|
185
|
+
// Handle keyboard input (non-paste)
|
|
186
|
+
useInput((input, key) => {
|
|
187
|
+
if (!isInputFocused)
|
|
188
|
+
return;
|
|
189
|
+
// Skip if we're in paste mode (handled by raw stdin)
|
|
190
|
+
if (isPastingRef.current)
|
|
191
|
+
return;
|
|
192
|
+
if (key.ctrl && input === 'l') {
|
|
193
|
+
setQuery('');
|
|
194
|
+
setCursorPos(0);
|
|
195
|
+
setScrollOffset(0);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (key.ctrl && input === 'k') {
|
|
199
|
+
try {
|
|
200
|
+
clipboardy.writeSync(query);
|
|
201
|
+
}
|
|
202
|
+
catch (err) { }
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Ctrl+A to go to beginning of line
|
|
206
|
+
if (key.ctrl && input === 'a') {
|
|
207
|
+
setCursorPos(0);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Ctrl+E to go to end of line
|
|
211
|
+
if (key.ctrl && input === 'e') {
|
|
212
|
+
setCursorPos(queryRef.current.length);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Alt/Option + Left (sends Esc+b in terminal) - move word backward
|
|
216
|
+
if (input === 'b' && key.escape) {
|
|
217
|
+
const q = queryRef.current;
|
|
218
|
+
let pos = cursorPosRef.current - 1;
|
|
219
|
+
// Skip whitespace
|
|
220
|
+
while (pos > 0 && /\s/.test(q.charAt(pos)))
|
|
221
|
+
pos--;
|
|
222
|
+
// Find start of word
|
|
223
|
+
while (pos > 0 && !/\s/.test(q.charAt(pos - 1)))
|
|
224
|
+
pos--;
|
|
225
|
+
setCursorPos(Math.max(0, pos));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Alt/Option + Right (sends Esc+f in terminal) - move word forward
|
|
229
|
+
if (input === 'f' && key.escape) {
|
|
230
|
+
const q = queryRef.current;
|
|
231
|
+
let pos = cursorPosRef.current;
|
|
232
|
+
// Skip current word
|
|
233
|
+
while (pos < q.length && !/\s/.test(q.charAt(pos)))
|
|
234
|
+
pos++;
|
|
235
|
+
// Skip whitespace
|
|
236
|
+
while (pos < q.length && /\s/.test(q.charAt(pos)))
|
|
237
|
+
pos++;
|
|
238
|
+
setCursorPos(pos);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Left/Right arrow keys for cursor movement
|
|
242
|
+
if (key.leftArrow) {
|
|
243
|
+
setCursorPos(prev => Math.max(0, prev - 1));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (key.rightArrow) {
|
|
247
|
+
setCursorPos(prev => Math.min(queryRef.current.length, prev + 1));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Up/Down arrow keys for scrolling
|
|
251
|
+
if (key.upArrow) {
|
|
252
|
+
setScrollOffset(prev => Math.max(0, prev - 1));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (key.downArrow) {
|
|
256
|
+
const maxOffset = Math.max(0, wrappedLines.length - MAX_VISIBLE_LINES);
|
|
257
|
+
setScrollOffset(prev => Math.min(maxOffset, prev + 1));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (key.return) {
|
|
261
|
+
if (key.shift) {
|
|
262
|
+
setQuery(prev => prev.slice(0, cursorPos) + '\n' + prev.slice(cursorPos));
|
|
263
|
+
setCursorPos(prev => prev + 1);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (query.endsWith('\\')) {
|
|
267
|
+
setQuery(prev => prev.slice(0, -1) + '\n');
|
|
268
|
+
setCursorPos(query.length);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (query.trim()) {
|
|
272
|
+
createConversation(query.trim(), activeMode?.name, activeModel);
|
|
273
|
+
setQuery('');
|
|
274
|
+
setCursorPos(0);
|
|
275
|
+
setScrollOffset(0);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const handleBackspace = () => {
|
|
280
|
+
setCursorPos(prev => {
|
|
281
|
+
if (prev > 0) {
|
|
282
|
+
setQuery(q => q.slice(0, prev - 1) + q.slice(prev));
|
|
283
|
+
return prev - 1;
|
|
284
|
+
}
|
|
285
|
+
return prev;
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
const handleForwardDelete = () => {
|
|
289
|
+
const pos = cursorPosRef.current;
|
|
290
|
+
const len = queryRef.current.length;
|
|
291
|
+
if (pos < len) {
|
|
292
|
+
setQuery(prev => prev.slice(0, pos) + prev.slice(pos + 1));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const hasModifier = key.ctrl || key.meta || key.shift;
|
|
296
|
+
if (key.backspace || (key.delete && !hasModifier)) {
|
|
297
|
+
handleBackspace();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (key.delete && hasModifier) {
|
|
301
|
+
handleForwardDelete();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (input) {
|
|
305
|
+
// Skip if this was part of an escape sequence we already handled
|
|
306
|
+
if (handledEscapeRef.current) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Skip 'b' and 'f' if escape key is also pressed (Option+arrow on macOS)
|
|
310
|
+
if (key.escape && (input === 'b' || input === 'f')) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const sanitized = sanitizeText(input);
|
|
314
|
+
if (sanitized) {
|
|
315
|
+
const pos = cursorPosRef.current;
|
|
316
|
+
setQuery(prev => prev.slice(0, pos) + sanitized + prev.slice(pos));
|
|
317
|
+
setCursorPos(pos + sanitized.length);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}, { isActive: true });
|
|
321
|
+
// The border should be active (primary) if ANYTHING in this box is focused
|
|
322
|
+
const isActive = focusedIndex !== -1;
|
|
323
|
+
const borderColor = isActive ? theme.colors.border : theme.colors.secondary;
|
|
324
|
+
return (_jsxs(Box, { width: "100%", flexDirection: "column", children: [_jsxs(Box, { width: "100%", borderStyle: "single", borderColor: borderColor, flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Box, { height: MAX_VISIBLE_LINES, overflowY: "hidden", width: "100%", flexDirection: "column", children: _jsx(Box, { marginTop: -scrollOffset, flexDirection: "column", width: "100%", children: query ? (_jsxs(Text, { color: theme.colors.text, children: [query.slice(0, cursorPos), isInputFocused && _jsx(Text, { color: theme.colors.primary, children: "\u258E" }), query.slice(cursorPos)] })) : (_jsxs(Text, { color: theme.colors.textMuted, children: [isInputFocused && _jsx(Text, { color: theme.colors.primary, children: "\u258E" }), "Type a message or ask a question..."] })) }) }), _jsx(Box, { width: "100%", flexDirection: "column", marginTop: 1, children: _jsx(Box, { width: "100%", children: _jsx(ProviderBar, { isFocused: isActive, focusedIndex: focusedIndex, showModeSelect: showModeSelect, setShowModeSelect: setShowModeSelect, showModelSelect: showModelSelect, setShowModelSelect: setShowModelSelect, showHistorySelect: showHistorySelect, setShowHistorySelect: setShowHistorySelect }) }) })] }), _jsx(Hints, { theme: theme })] }));
|
|
325
|
+
}
|
|
326
|
+
const Hints = React.memo(({ theme }) => {
|
|
327
|
+
return (_jsxs(Box, { width: "100%", justifyContent: "flex-end", paddingRight: 1, marginTop: 1, gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "^L" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Clear" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "^K" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Copy" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "\u2191/\u2193" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Scroll" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "Tab" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Select" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "\\+Ent" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Line" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "Ent" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Send" })] })] }));
|
|
328
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Logo(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useTheme } from '../theme/ThemeContext.js';
|
|
4
|
+
export function Logo() {
|
|
5
|
+
const { theme } = useTheme();
|
|
6
|
+
const asciiArt = `
|
|
7
|
+
████████╗ ██████╗ ██████╗ ██╗ ██████╗ █████╗ ██████╗██╗ ██╗ ███████╗██████╗ ██╗ ██╗
|
|
8
|
+
╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔══██╗██╔══██╗██╔════╝██║ ██╔╝ ██╔════╝██╔══██╗██║ ██╔╝
|
|
9
|
+
██║ ██║ ██║██║ ██║██║ ██████╔╝███████║██║ █████╔╝ ███████╗██║ ██║█████╔╝
|
|
10
|
+
██║ ██║ ██║██║ ██║██║ ██╔═══╝ ██╔══██║██║ ██╔═██╗ ╚════██║██║ ██║██╔═██╗
|
|
11
|
+
██║ ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║╚██████╗██║ ██╗ ███████║██████╔╝██║ ██╗
|
|
12
|
+
╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝
|
|
13
|
+
`;
|
|
14
|
+
return (_jsx(Box, { justifyContent: "center", width: "100%", children: _jsx(Text, { color: theme.colors.text, bold: true, children: asciiArt }) }));
|
|
15
|
+
}
|