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.
Files changed (46) hide show
  1. package/dist/api/chat.d.ts +29 -0
  2. package/dist/api/chat.js +88 -0
  3. package/dist/api/models.d.ts +12 -0
  4. package/dist/api/models.js +40 -0
  5. package/dist/app.d.ts +6 -0
  6. package/dist/app.js +47 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +19 -0
  9. package/dist/constants/index.d.ts +18 -0
  10. package/dist/constants/index.js +18 -0
  11. package/dist/hooks/useGlobalExit.d.ts +5 -0
  12. package/dist/hooks/useGlobalExit.js +32 -0
  13. package/dist/types/index.d.ts +15 -0
  14. package/dist/types/index.js +1 -0
  15. package/dist/ui/components/ChatInput.d.ts +9 -0
  16. package/dist/ui/components/ChatInput.js +206 -0
  17. package/dist/ui/components/CommandPanel.d.ts +13 -0
  18. package/dist/ui/components/CommandPanel.js +22 -0
  19. package/dist/ui/components/Menu.d.ts +14 -0
  20. package/dist/ui/components/Menu.js +32 -0
  21. package/dist/ui/components/MessageList.d.ts +15 -0
  22. package/dist/ui/components/MessageList.js +16 -0
  23. package/dist/ui/components/PendingMessages.d.ts +6 -0
  24. package/dist/ui/components/PendingMessages.js +19 -0
  25. package/dist/ui/pages/ApiConfigScreen.d.ts +7 -0
  26. package/dist/ui/pages/ApiConfigScreen.js +126 -0
  27. package/dist/ui/pages/ChatScreen.d.ts +5 -0
  28. package/dist/ui/pages/ChatScreen.js +287 -0
  29. package/dist/ui/pages/ModelConfigScreen.d.ts +7 -0
  30. package/dist/ui/pages/ModelConfigScreen.js +239 -0
  31. package/dist/ui/pages/WelcomeScreen.d.ts +7 -0
  32. package/dist/ui/pages/WelcomeScreen.js +48 -0
  33. package/dist/utils/apiConfig.d.ts +17 -0
  34. package/dist/utils/apiConfig.js +86 -0
  35. package/dist/utils/commandExecutor.d.ts +11 -0
  36. package/dist/utils/commandExecutor.js +26 -0
  37. package/dist/utils/commands/clear.d.ts +2 -0
  38. package/dist/utils/commands/clear.js +12 -0
  39. package/dist/utils/index.d.ts +7 -0
  40. package/dist/utils/index.js +12 -0
  41. package/dist/utils/textBuffer.d.ts +52 -0
  42. package/dist/utils/textBuffer.js +310 -0
  43. package/dist/utils/textUtils.d.ts +33 -0
  44. package/dist/utils/textUtils.js +83 -0
  45. package/package.json +86 -0
  46. 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[];
@@ -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
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ version?: string;
4
+ };
5
+ export default function App({ version }: Props): React.JSX.Element;
6
+ export {};
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,5 @@
1
+ export interface ExitNotification {
2
+ show: boolean;
3
+ message: string;
4
+ }
5
+ export declare function useGlobalExit(onNotification?: (notification: ExitNotification) => void): void;
@@ -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 {};