snow-ai 0.1.12
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/dist/api/chat.d.ts +29 -0
- package/dist/api/chat.js +88 -0
- package/dist/api/models.d.ts +12 -0
- package/dist/api/models.js +40 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +47 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +19 -0
- package/dist/constants/index.d.ts +18 -0
- package/dist/constants/index.js +18 -0
- package/dist/hooks/useGlobalExit.d.ts +5 -0
- package/dist/hooks/useGlobalExit.js +32 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.js +1 -0
- package/dist/ui/components/ChatInput.d.ts +9 -0
- package/dist/ui/components/ChatInput.js +206 -0
- package/dist/ui/components/CommandPanel.d.ts +13 -0
- package/dist/ui/components/CommandPanel.js +22 -0
- package/dist/ui/components/Menu.d.ts +14 -0
- package/dist/ui/components/Menu.js +32 -0
- package/dist/ui/components/MessageList.d.ts +15 -0
- package/dist/ui/components/MessageList.js +16 -0
- package/dist/ui/components/PendingMessages.d.ts +6 -0
- package/dist/ui/components/PendingMessages.js +19 -0
- package/dist/ui/pages/ApiConfigScreen.d.ts +7 -0
- package/dist/ui/pages/ApiConfigScreen.js +126 -0
- package/dist/ui/pages/ChatScreen.d.ts +5 -0
- package/dist/ui/pages/ChatScreen.js +287 -0
- package/dist/ui/pages/ModelConfigScreen.d.ts +7 -0
- package/dist/ui/pages/ModelConfigScreen.js +239 -0
- package/dist/ui/pages/WelcomeScreen.d.ts +7 -0
- package/dist/ui/pages/WelcomeScreen.js +48 -0
- package/dist/utils/apiConfig.d.ts +17 -0
- package/dist/utils/apiConfig.js +86 -0
- package/dist/utils/commandExecutor.d.ts +11 -0
- package/dist/utils/commandExecutor.js +26 -0
- package/dist/utils/commands/clear.d.ts +2 -0
- package/dist/utils/commands/clear.js +12 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/textBuffer.d.ts +52 -0
- package/dist/utils/textBuffer.js +310 -0
- package/dist/utils/textUtils.d.ts +33 -0
- package/dist/utils/textUtils.js +83 -0
- package/package.json +86 -0
- package/readme.md +9 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ChatMessage {
|
|
2
|
+
role: 'system' | 'user' | 'assistant';
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ChatCompletionOptions {
|
|
6
|
+
model: string;
|
|
7
|
+
messages: ChatMessage[];
|
|
8
|
+
stream?: boolean;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
max_tokens?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ChatCompletionChunk {
|
|
13
|
+
id: string;
|
|
14
|
+
object: 'chat.completion.chunk';
|
|
15
|
+
created: number;
|
|
16
|
+
model: string;
|
|
17
|
+
choices: Array<{
|
|
18
|
+
index: number;
|
|
19
|
+
delta: {
|
|
20
|
+
role?: string;
|
|
21
|
+
content?: string;
|
|
22
|
+
};
|
|
23
|
+
finish_reason?: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export declare function resetOpenAIClient(): void;
|
|
27
|
+
export declare function createChatCompletion(options: ChatCompletionOptions): Promise<string>;
|
|
28
|
+
export declare function createStreamingChatCompletion(options: ChatCompletionOptions, abortSignal?: AbortSignal): AsyncGenerator<string, void, unknown>;
|
|
29
|
+
export declare function validateChatOptions(options: ChatCompletionOptions): string[];
|
package/dist/api/chat.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { getOpenAiConfig } from '../utils/apiConfig.js';
|
|
3
|
+
let openaiClient = null;
|
|
4
|
+
function getOpenAIClient() {
|
|
5
|
+
if (!openaiClient) {
|
|
6
|
+
const config = getOpenAiConfig();
|
|
7
|
+
if (!config.apiKey || !config.baseUrl) {
|
|
8
|
+
throw new Error('OpenAI API configuration is incomplete. Please configure API settings first.');
|
|
9
|
+
}
|
|
10
|
+
openaiClient = new OpenAI({
|
|
11
|
+
apiKey: config.apiKey,
|
|
12
|
+
baseURL: config.baseUrl,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return openaiClient;
|
|
16
|
+
}
|
|
17
|
+
export function resetOpenAIClient() {
|
|
18
|
+
openaiClient = null;
|
|
19
|
+
}
|
|
20
|
+
export async function createChatCompletion(options) {
|
|
21
|
+
const client = getOpenAIClient();
|
|
22
|
+
try {
|
|
23
|
+
const response = await client.chat.completions.create({
|
|
24
|
+
model: options.model,
|
|
25
|
+
messages: options.messages,
|
|
26
|
+
stream: false,
|
|
27
|
+
temperature: options.temperature || 0.7,
|
|
28
|
+
max_tokens: options.max_tokens,
|
|
29
|
+
});
|
|
30
|
+
return response.choices[0]?.message?.content || '';
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof Error) {
|
|
34
|
+
throw new Error(`Chat completion failed: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error('Chat completion failed: Unknown error');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function* createStreamingChatCompletion(options, abortSignal) {
|
|
40
|
+
const client = getOpenAIClient();
|
|
41
|
+
try {
|
|
42
|
+
const stream = await client.chat.completions.create({
|
|
43
|
+
model: options.model,
|
|
44
|
+
messages: options.messages,
|
|
45
|
+
stream: true,
|
|
46
|
+
temperature: options.temperature || 0.7,
|
|
47
|
+
max_tokens: options.max_tokens,
|
|
48
|
+
}, {
|
|
49
|
+
signal: abortSignal,
|
|
50
|
+
});
|
|
51
|
+
for await (const chunk of stream) {
|
|
52
|
+
if (abortSignal?.aborted) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
56
|
+
if (content) {
|
|
57
|
+
yield content;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
63
|
+
return; // Silently handle abort
|
|
64
|
+
}
|
|
65
|
+
if (error instanceof Error) {
|
|
66
|
+
throw new Error(`Streaming chat completion failed: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
throw new Error('Streaming chat completion failed: Unknown error');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function validateChatOptions(options) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
if (!options.model || options.model.trim().length === 0) {
|
|
74
|
+
errors.push('Model is required');
|
|
75
|
+
}
|
|
76
|
+
if (!options.messages || options.messages.length === 0) {
|
|
77
|
+
errors.push('At least one message is required');
|
|
78
|
+
}
|
|
79
|
+
for (const message of options.messages || []) {
|
|
80
|
+
if (!message.role || !['system', 'user', 'assistant'].includes(message.role)) {
|
|
81
|
+
errors.push('Invalid message role');
|
|
82
|
+
}
|
|
83
|
+
if (!message.content || message.content.trim().length === 0) {
|
|
84
|
+
errors.push('Message content cannot be empty');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface Model {
|
|
2
|
+
id: string;
|
|
3
|
+
object: string;
|
|
4
|
+
created: number;
|
|
5
|
+
owned_by: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ModelsResponse {
|
|
8
|
+
object: string;
|
|
9
|
+
data: Model[];
|
|
10
|
+
}
|
|
11
|
+
export declare function fetchAvailableModels(): Promise<Model[]>;
|
|
12
|
+
export declare function filterModels(models: Model[], searchTerm: string): Model[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getOpenAiConfig } from '../utils/apiConfig.js';
|
|
2
|
+
export async function fetchAvailableModels() {
|
|
3
|
+
const config = getOpenAiConfig();
|
|
4
|
+
if (!config.baseUrl) {
|
|
5
|
+
throw new Error('Base URL not configured. Please configure API settings first.');
|
|
6
|
+
}
|
|
7
|
+
const url = `${config.baseUrl.replace(/\/$/, '')}/models`;
|
|
8
|
+
const headers = {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
};
|
|
11
|
+
// Add Authorization header only if API key is provided
|
|
12
|
+
if (config.apiKey) {
|
|
13
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
headers,
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
// Sort models alphabetically by id for better UX
|
|
25
|
+
return (data.data || []).sort((a, b) => a.id.localeCompare(b.id));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error) {
|
|
29
|
+
throw new Error(`Error fetching models: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
throw new Error('Unknown error occurred while fetching models');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function filterModels(models, searchTerm) {
|
|
35
|
+
if (!searchTerm.trim()) {
|
|
36
|
+
return models;
|
|
37
|
+
}
|
|
38
|
+
const lowerSearchTerm = searchTerm.toLowerCase();
|
|
39
|
+
return models.filter(model => model.id.toLowerCase().includes(lowerSearchTerm));
|
|
40
|
+
}
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Alert } from '@inkjs/ui';
|
|
4
|
+
import WelcomeScreen from './ui/pages/WelcomeScreen.js';
|
|
5
|
+
import ApiConfigScreen from './ui/pages/ApiConfigScreen.js';
|
|
6
|
+
import ModelConfigScreen from './ui/pages/ModelConfigScreen.js';
|
|
7
|
+
import ChatScreen from './ui/pages/ChatScreen.js';
|
|
8
|
+
import { useGlobalExit } from './hooks/useGlobalExit.js';
|
|
9
|
+
export default function App({ version }) {
|
|
10
|
+
const [currentView, setCurrentView] = useState('welcome');
|
|
11
|
+
const [exitNotification, setExitNotification] = useState({
|
|
12
|
+
show: false,
|
|
13
|
+
message: ''
|
|
14
|
+
});
|
|
15
|
+
// Global exit handler
|
|
16
|
+
useGlobalExit(setExitNotification);
|
|
17
|
+
const handleMenuSelect = (value) => {
|
|
18
|
+
if (value === 'chat' || value === 'settings' || value === 'config' || value === 'models') {
|
|
19
|
+
setCurrentView(value);
|
|
20
|
+
}
|
|
21
|
+
else if (value === 'exit') {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const renderView = () => {
|
|
26
|
+
switch (currentView) {
|
|
27
|
+
case 'welcome':
|
|
28
|
+
return (React.createElement(WelcomeScreen, { version: version, onMenuSelect: handleMenuSelect }));
|
|
29
|
+
case 'chat':
|
|
30
|
+
return (React.createElement(ChatScreen, null));
|
|
31
|
+
case 'settings':
|
|
32
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
33
|
+
React.createElement(Text, { color: "blue" }, "Settings"),
|
|
34
|
+
React.createElement(Text, { color: "gray" }, "Settings interface would be implemented here")));
|
|
35
|
+
case 'config':
|
|
36
|
+
return (React.createElement(ApiConfigScreen, { onBack: () => setCurrentView('welcome'), onSave: () => setCurrentView('welcome') }));
|
|
37
|
+
case 'models':
|
|
38
|
+
return (React.createElement(ModelConfigScreen, { onBack: () => setCurrentView('welcome'), onSave: () => setCurrentView('welcome') }));
|
|
39
|
+
default:
|
|
40
|
+
return (React.createElement(WelcomeScreen, { version: version, onMenuSelect: handleMenuSelect }));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
44
|
+
renderView(),
|
|
45
|
+
exitNotification.show && (React.createElement(Box, { padding: 1 },
|
|
46
|
+
React.createElement(Alert, { variant: "warning" }, exitNotification.message)))));
|
|
47
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import meow from 'meow';
|
|
5
|
+
import App from './app.js';
|
|
6
|
+
const cli = meow(`
|
|
7
|
+
Usage
|
|
8
|
+
$ aibotpro
|
|
9
|
+
|
|
10
|
+
Options
|
|
11
|
+
--help Show help
|
|
12
|
+
--version Show version
|
|
13
|
+
`, {
|
|
14
|
+
importMeta: import.meta,
|
|
15
|
+
flags: {},
|
|
16
|
+
});
|
|
17
|
+
render(React.createElement(App, { version: cli.pkg.version }), {
|
|
18
|
+
exitOnCtrlC: false,
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const APP_NAME = "AI Bot CLI";
|
|
2
|
+
export declare const APP_VERSION = "1.0.0";
|
|
3
|
+
export declare const APP_DESCRIPTION = "Intelligent Command Line Assistant";
|
|
4
|
+
export declare const COLORS: {
|
|
5
|
+
readonly primary: "cyan";
|
|
6
|
+
readonly secondary: "magenta";
|
|
7
|
+
readonly success: "green";
|
|
8
|
+
readonly warning: "yellow";
|
|
9
|
+
readonly error: "red";
|
|
10
|
+
readonly info: "blue";
|
|
11
|
+
readonly muted: "gray";
|
|
12
|
+
};
|
|
13
|
+
export declare const COMMANDS: {
|
|
14
|
+
readonly HELP: "help";
|
|
15
|
+
readonly VERSION: "version";
|
|
16
|
+
readonly EXIT: "exit";
|
|
17
|
+
readonly CLEAR: "clear";
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const APP_NAME = 'AI Bot CLI';
|
|
2
|
+
export const APP_VERSION = '1.0.0';
|
|
3
|
+
export const APP_DESCRIPTION = 'Intelligent Command Line Assistant';
|
|
4
|
+
export const COLORS = {
|
|
5
|
+
primary: 'cyan',
|
|
6
|
+
secondary: 'magenta',
|
|
7
|
+
success: 'green',
|
|
8
|
+
warning: 'yellow',
|
|
9
|
+
error: 'red',
|
|
10
|
+
info: 'blue',
|
|
11
|
+
muted: 'gray',
|
|
12
|
+
};
|
|
13
|
+
export const COMMANDS = {
|
|
14
|
+
HELP: 'help',
|
|
15
|
+
VERSION: 'version',
|
|
16
|
+
EXIT: 'exit',
|
|
17
|
+
CLEAR: 'clear',
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useInput } from 'ink';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
export function useGlobalExit(onNotification) {
|
|
4
|
+
const [lastCtrlCTime, setLastCtrlCTime] = useState(0);
|
|
5
|
+
const ctrlCTimeout = 1000; // 1 second timeout for double Ctrl+C
|
|
6
|
+
useInput((input, key) => {
|
|
7
|
+
if (key.ctrl && input === 'c') {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
if (now - lastCtrlCTime < ctrlCTimeout) {
|
|
10
|
+
// Second Ctrl+C within timeout - exit
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
// First Ctrl+C - show notification
|
|
15
|
+
setLastCtrlCTime(now);
|
|
16
|
+
if (onNotification) {
|
|
17
|
+
onNotification({
|
|
18
|
+
show: true,
|
|
19
|
+
message: 'Press Ctrl+C again to exit'
|
|
20
|
+
});
|
|
21
|
+
// Hide notification after timeout
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
onNotification({
|
|
24
|
+
show: false,
|
|
25
|
+
message: ''
|
|
26
|
+
});
|
|
27
|
+
}, ctrlCTimeout);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AIBotConfig {
|
|
2
|
+
model?: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
maxTokens?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Command {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
handler: (args: string[]) => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface AppState {
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
currentCommand?: string;
|
|
14
|
+
history: string[];
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type Props = {
|
|
3
|
+
onSubmit: (message: string) => void;
|
|
4
|
+
onCommand?: (commandName: string, result: any) => void;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export default function ChatInput({ onSubmit, onCommand, placeholder, disabled }: Props): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { Box, Text, useStdout, useInput } from 'ink';
|
|
3
|
+
import { TextBuffer } from '../../utils/textBuffer.js';
|
|
4
|
+
import { cpSlice } from '../../utils/textUtils.js';
|
|
5
|
+
import CommandPanel from './CommandPanel.js';
|
|
6
|
+
import { executeCommand } from '../../utils/commandExecutor.js';
|
|
7
|
+
// Command Definition
|
|
8
|
+
const commands = [
|
|
9
|
+
{ name: 'clear', description: 'Clear chat context and conversation history' },
|
|
10
|
+
{ name: 'agents', description: 'Manage agent configurations' }
|
|
11
|
+
];
|
|
12
|
+
export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false }) {
|
|
13
|
+
const { stdout } = useStdout();
|
|
14
|
+
const terminalWidth = stdout?.columns || 80;
|
|
15
|
+
const uiOverhead = 8;
|
|
16
|
+
const viewport = {
|
|
17
|
+
width: Math.max(40, terminalWidth - uiOverhead),
|
|
18
|
+
height: 1
|
|
19
|
+
};
|
|
20
|
+
const [buffer] = useState(() => new TextBuffer(viewport));
|
|
21
|
+
const [, forceUpdate] = useState({});
|
|
22
|
+
const lastUpdateTime = useRef(0);
|
|
23
|
+
// Command panel state
|
|
24
|
+
const [showCommands, setShowCommands] = useState(false);
|
|
25
|
+
const [commandSelectedIndex, setCommandSelectedIndex] = useState(0);
|
|
26
|
+
// Get filtered commands based on current input
|
|
27
|
+
const getFilteredCommands = useCallback(() => {
|
|
28
|
+
const text = buffer.getFullText();
|
|
29
|
+
if (!text.startsWith('/'))
|
|
30
|
+
return [];
|
|
31
|
+
const query = text.slice(1).toLowerCase();
|
|
32
|
+
return commands.filter(command => command.name.toLowerCase().includes(query) ||
|
|
33
|
+
command.description.toLowerCase().includes(query));
|
|
34
|
+
}, [buffer]);
|
|
35
|
+
// Update command panel state
|
|
36
|
+
const updateCommandPanelState = useCallback((text) => {
|
|
37
|
+
if (text.startsWith('/') && text.length > 0) {
|
|
38
|
+
setShowCommands(true);
|
|
39
|
+
setCommandSelectedIndex(0);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
setShowCommands(false);
|
|
43
|
+
setCommandSelectedIndex(0);
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
// Force re-render when buffer changes
|
|
47
|
+
const triggerUpdate = useCallback(() => {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
// Avoid too frequent updates
|
|
50
|
+
if (now - lastUpdateTime.current > 16) { // ~60fps limit
|
|
51
|
+
lastUpdateTime.current = now;
|
|
52
|
+
forceUpdate({});
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
// Update buffer viewport when terminal width changes
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const newViewport = {
|
|
58
|
+
width: Math.max(40, terminalWidth - uiOverhead),
|
|
59
|
+
height: 1
|
|
60
|
+
};
|
|
61
|
+
buffer.updateViewport(newViewport);
|
|
62
|
+
triggerUpdate();
|
|
63
|
+
}, [terminalWidth, buffer, triggerUpdate]);
|
|
64
|
+
// Handle input using useInput hook instead of raw stdin
|
|
65
|
+
useInput((input, key) => {
|
|
66
|
+
if (disabled)
|
|
67
|
+
return;
|
|
68
|
+
// Backspace
|
|
69
|
+
if (key.backspace || key.delete) {
|
|
70
|
+
buffer.backspace();
|
|
71
|
+
const text = buffer.getFullText();
|
|
72
|
+
updateCommandPanelState(text);
|
|
73
|
+
triggerUpdate();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Handle command panel navigation
|
|
77
|
+
if (showCommands) {
|
|
78
|
+
const filteredCommands = getFilteredCommands();
|
|
79
|
+
// Up arrow in command panel
|
|
80
|
+
if (key.upArrow) {
|
|
81
|
+
setCommandSelectedIndex(prev => Math.max(0, prev - 1));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Down arrow in command panel
|
|
85
|
+
if (key.downArrow) {
|
|
86
|
+
const maxIndex = Math.max(0, filteredCommands.length - 1);
|
|
87
|
+
setCommandSelectedIndex(prev => Math.min(maxIndex, prev + 1));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Enter - select command
|
|
91
|
+
if (key.return) {
|
|
92
|
+
if (filteredCommands.length > 0 && commandSelectedIndex < filteredCommands.length) {
|
|
93
|
+
const selectedCommand = filteredCommands[commandSelectedIndex];
|
|
94
|
+
if (selectedCommand) {
|
|
95
|
+
// Execute command instead of inserting text
|
|
96
|
+
executeCommand(selectedCommand.name).then(result => {
|
|
97
|
+
if (onCommand) {
|
|
98
|
+
onCommand(selectedCommand.name, result);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
buffer.setText('');
|
|
102
|
+
setShowCommands(false);
|
|
103
|
+
setCommandSelectedIndex(0);
|
|
104
|
+
triggerUpdate();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// If no commands available, fall through to normal Enter handling
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Enter - submit message
|
|
112
|
+
if (key.return) {
|
|
113
|
+
const message = buffer.getFullText().trim();
|
|
114
|
+
if (message) {
|
|
115
|
+
buffer.setText('');
|
|
116
|
+
forceUpdate({});
|
|
117
|
+
onSubmit(message);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Arrow keys for cursor movement
|
|
122
|
+
if (key.leftArrow) {
|
|
123
|
+
buffer.moveLeft();
|
|
124
|
+
triggerUpdate();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (key.rightArrow) {
|
|
128
|
+
buffer.moveRight();
|
|
129
|
+
triggerUpdate();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (key.upArrow && !showCommands) {
|
|
133
|
+
buffer.moveUp();
|
|
134
|
+
triggerUpdate();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (key.downArrow && !showCommands) {
|
|
138
|
+
buffer.moveDown();
|
|
139
|
+
triggerUpdate();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Regular character input
|
|
143
|
+
if (input && !key.ctrl && !key.meta && !key.escape) {
|
|
144
|
+
buffer.insert(input);
|
|
145
|
+
const text = buffer.getFullText();
|
|
146
|
+
updateCommandPanelState(text);
|
|
147
|
+
triggerUpdate();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// Handle paste events - useInput should handle paste automatically
|
|
151
|
+
// but we may need to handle large pastes specially
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const handlePaste = (event) => {
|
|
154
|
+
if (event.clipboardData) {
|
|
155
|
+
const pastedText = event.clipboardData.getData('text');
|
|
156
|
+
if (pastedText && pastedText.length > 0) {
|
|
157
|
+
// Let TextBuffer handle the paste processing
|
|
158
|
+
buffer.insert(pastedText);
|
|
159
|
+
const text = buffer.getFullText();
|
|
160
|
+
updateCommandPanelState(text);
|
|
161
|
+
triggerUpdate();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
// Note: This might not work in all terminal environments
|
|
166
|
+
// but the useInput hook should handle most paste scenarios
|
|
167
|
+
if (typeof window !== 'undefined') {
|
|
168
|
+
window.addEventListener('paste', handlePaste);
|
|
169
|
+
return () => window.removeEventListener('paste', handlePaste);
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}, [buffer, updateCommandPanelState, triggerUpdate]);
|
|
173
|
+
const visualLines = buffer.viewportVisualLines;
|
|
174
|
+
const [cursorRow, cursorCol] = buffer.visualCursor;
|
|
175
|
+
// Render content with cursor and paste placeholders
|
|
176
|
+
const renderContent = useCallback(() => {
|
|
177
|
+
if (buffer.text.length > 0) {
|
|
178
|
+
return visualLines.map((line, index) => (React.createElement(Box, { key: `line-${index}` },
|
|
179
|
+
React.createElement(Text, null, index === cursorRow ? (React.createElement(React.Fragment, null,
|
|
180
|
+
cpSlice(line, 0, cursorCol),
|
|
181
|
+
React.createElement(Text, { backgroundColor: "white", color: "black" }, (() => {
|
|
182
|
+
const charInfo = buffer.getCharAtCursor();
|
|
183
|
+
return charInfo.char === '\n' ? ' ' : charInfo.char;
|
|
184
|
+
})()),
|
|
185
|
+
cpSlice(line, cursorCol + 1))) : (
|
|
186
|
+
// Check for paste placeholders and highlight them
|
|
187
|
+
line.includes('[Paste ') && line.includes(' line #') ? (React.createElement(Text, null, line.split(/(\[Paste \d+ line #\d+\])/).map((part, partIndex) => part.match(/^\[Paste \d+ line #\d+\]$/) ? (React.createElement(Text, { key: partIndex, color: "cyan", dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part))))) : (line || ' '))))));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
return (React.createElement(Box, null,
|
|
191
|
+
React.createElement(Text, { backgroundColor: disabled ? "gray" : "white", color: disabled ? "darkGray" : "black" }, ' '),
|
|
192
|
+
React.createElement(Text, { color: disabled ? "darkGray" : "gray", dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
|
|
193
|
+
}
|
|
194
|
+
}, [visualLines, cursorRow, cursorCol, buffer, placeholder]);
|
|
195
|
+
return (React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
196
|
+
React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, width: "100%" },
|
|
197
|
+
React.createElement(Text, { color: "cyan", bold: true },
|
|
198
|
+
"\u27A3",
|
|
199
|
+
' '),
|
|
200
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, renderContent())),
|
|
201
|
+
React.createElement(CommandPanel, { commands: getFilteredCommands(), selectedIndex: commandSelectedIndex, query: buffer.getFullText().slice(1), visible: showCommands }),
|
|
202
|
+
React.createElement(Box, { marginTop: 1 },
|
|
203
|
+
React.createElement(Text, { color: "gray", dimColor: true }, showCommands && getFilteredCommands().length > 0
|
|
204
|
+
? "Type to filter commands"
|
|
205
|
+
: "Press Ctrl+C twice to exit"))));
|
|
206
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface Command {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
commands: Command[];
|
|
8
|
+
selectedIndex: number;
|
|
9
|
+
query: string;
|
|
10
|
+
visible: boolean;
|
|
11
|
+
}
|
|
12
|
+
export default function CommandPanel({ commands, selectedIndex, query, visible }: Props): React.JSX.Element | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function CommandPanel({ commands, selectedIndex, query, visible }) {
|
|
4
|
+
// Don't show panel if not visible or no commands found
|
|
5
|
+
if (!visible || commands.length === 0) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
9
|
+
React.createElement(Box, { width: "100%" },
|
|
10
|
+
React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
11
|
+
React.createElement(Box, null,
|
|
12
|
+
React.createElement(Text, { color: "yellow", bold: true },
|
|
13
|
+
"Available Commands ",
|
|
14
|
+
query && `(${commands.length} matches)`)),
|
|
15
|
+
commands.map((command, index) => (React.createElement(Box, { key: command.name, flexDirection: "row", width: "100%" },
|
|
16
|
+
React.createElement(Text, { color: index === selectedIndex ? "green" : "gray" },
|
|
17
|
+
index === selectedIndex ? "➣ " : " ",
|
|
18
|
+
"/",
|
|
19
|
+
command.name),
|
|
20
|
+
React.createElement(Box, { marginLeft: 2 },
|
|
21
|
+
React.createElement(Text, { color: index === selectedIndex ? "green" : "gray", dimColor: true }, command.description)))))))));
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type MenuOption = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
infoText?: string;
|
|
7
|
+
};
|
|
8
|
+
type Props = {
|
|
9
|
+
options: MenuOption[];
|
|
10
|
+
onSelect: (value: string) => void;
|
|
11
|
+
onSelectionChange?: (infoText: string) => void;
|
|
12
|
+
};
|
|
13
|
+
export default function Menu({ options, onSelect, onSelectionChange }: Props): React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export default function Menu({ options, onSelect, onSelectionChange }) {
|
|
4
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
5
|
+
React.useEffect(() => {
|
|
6
|
+
const currentOption = options[selectedIndex];
|
|
7
|
+
if (onSelectionChange && currentOption?.infoText) {
|
|
8
|
+
onSelectionChange(currentOption.infoText);
|
|
9
|
+
}
|
|
10
|
+
}, [selectedIndex, options, onSelectionChange]);
|
|
11
|
+
useInput((_, key) => {
|
|
12
|
+
if (key.upArrow) {
|
|
13
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : options.length - 1));
|
|
14
|
+
}
|
|
15
|
+
else if (key.downArrow) {
|
|
16
|
+
setSelectedIndex(prev => (prev < options.length - 1 ? prev + 1 : 0));
|
|
17
|
+
}
|
|
18
|
+
else if (key.return) {
|
|
19
|
+
const selectedOption = options[selectedIndex];
|
|
20
|
+
if (selectedOption) {
|
|
21
|
+
onSelect(selectedOption.value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return (React.createElement(Box, { flexDirection: "column", width: '100%', borderStyle: 'round', borderColor: "#A9C13E", padding: 1 },
|
|
26
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
27
|
+
React.createElement(Text, { color: "cyan" }, "Use \u2191\u2193 keys to navigate, press Enter to select:")),
|
|
28
|
+
options.map((option, index) => (React.createElement(Box, { key: option.value },
|
|
29
|
+
React.createElement(Text, { color: index === selectedIndex ? 'green' : option.color || 'white', bold: true },
|
|
30
|
+
index === selectedIndex ? '➣ ' : ' ',
|
|
31
|
+
option.label))))));
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface Message {
|
|
3
|
+
role: 'user' | 'assistant' | 'command';
|
|
4
|
+
content: string;
|
|
5
|
+
streaming?: boolean;
|
|
6
|
+
discontinued?: boolean;
|
|
7
|
+
commandName?: string;
|
|
8
|
+
}
|
|
9
|
+
interface Props {
|
|
10
|
+
messages: Message[];
|
|
11
|
+
animationFrame: number;
|
|
12
|
+
maxMessages?: number;
|
|
13
|
+
}
|
|
14
|
+
export default function MessageList({ messages, animationFrame, maxMessages }: Props): React.JSX.Element | null;
|
|
15
|
+
export {};
|