mirror of https://github.com/buster-so/buster.git
chat cli endpoint and base cli agent
This commit is contained in:
parent
0febe2bd0a
commit
68ccbe1ba7
|
@ -1,5 +1,5 @@
|
|||
import { Box, Text, useApp, useInput } from 'ink';
|
||||
import { useRef, useState } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ChatFooter,
|
||||
ChatHistory,
|
||||
|
@ -10,8 +10,9 @@ import {
|
|||
ChatVersionTagline,
|
||||
VimStatus,
|
||||
} from '../components/chat-layout';
|
||||
import { Diff } from '../components/diff';
|
||||
import { SettingsForm } from '../components/settings-form';
|
||||
import { TypedMessage, type MessageType } from '../components/typed-message';
|
||||
import { type MessageType, TypedMessage } from '../components/typed-message';
|
||||
import { getSetting } from '../utils/settings';
|
||||
import type { SlashCommand } from '../utils/slash-commands';
|
||||
import type { VimMode } from '../utils/vim-mode';
|
||||
|
@ -24,6 +25,12 @@ interface Message {
|
|||
content: string;
|
||||
messageType?: MessageType;
|
||||
metadata?: string;
|
||||
diffLines?: Array<{
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'add' | 'remove' | 'context';
|
||||
}>;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export function Main() {
|
||||
|
@ -43,7 +50,7 @@ export function Main() {
|
|||
if (key.ctrl && value === 'c') {
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
// Cycle through modes with shift+tab
|
||||
if (key.shift && key.tab) {
|
||||
setAppMode((current) => {
|
||||
|
@ -61,67 +68,167 @@ export function Main() {
|
|||
|
||||
const getMockResponse = (userInput: string): Message[] => {
|
||||
const responses: Message[] = [];
|
||||
|
||||
// Always send all message types for demo
|
||||
responses.push(
|
||||
{
|
||||
|
||||
if (userInput.toLowerCase().includes('plan')) {
|
||||
responses.push({
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Creating a comprehensive plan for your request.',
|
||||
content: `I'll create a detailed plan for your request. Let me analyze the requirements and break down the implementation steps.`,
|
||||
messageType: 'PLAN',
|
||||
metadata: 'Updated: 3 total (3 pending, 0 in progress, 0 completed)'
|
||||
},
|
||||
{
|
||||
metadata: 'Creating a comprehensive plan for your request.',
|
||||
});
|
||||
} else if (userInput.toLowerCase().includes('search')) {
|
||||
responses.push({
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Executing command: npm install',
|
||||
content: 'Searching for relevant information across the web...',
|
||||
messageType: 'EXECUTE',
|
||||
metadata: 'cd /project && npm install, impact: low'
|
||||
},
|
||||
{
|
||||
metadata: 'Executing command: npm install',
|
||||
});
|
||||
} else if (userInput.toLowerCase().includes('run')) {
|
||||
responses.push({
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Searching for documentation and best practices...',
|
||||
messageType: 'WEB_SEARCH',
|
||||
metadata: '"React hooks useState useEffect"'
|
||||
},
|
||||
{
|
||||
content: `cd /Users/safzan/Development/insideim/jobai-backend && node --env-file=.env --input-type=module --import=axios`,
|
||||
messageType: 'EXECUTE',
|
||||
metadata: 'React hooks useState useEffect',
|
||||
});
|
||||
} else if (userInput.toLowerCase().includes('error')) {
|
||||
responses.push({
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Here is some general information about your request.',
|
||||
messageType: 'INFO'
|
||||
},
|
||||
{
|
||||
content: 'Writing new configuration file...',
|
||||
messageType: 'WRITE',
|
||||
metadata:
|
||||
'/Users/safzan/Development/insideim/cheating-daddy/node_modules/@google/genai, impact: medium',
|
||||
});
|
||||
} else {
|
||||
// Default response with EDIT and diff visualization
|
||||
responses.push({
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Successfully completed all operations!',
|
||||
messageType: 'SUCCESS'
|
||||
},
|
||||
{
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Warning: This operation may take longer than expected.',
|
||||
messageType: 'WARNING'
|
||||
},
|
||||
{
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Error: Failed to connect to the database.',
|
||||
messageType: 'ERROR'
|
||||
},
|
||||
{
|
||||
id: ++messageCounter.current,
|
||||
type: 'assistant',
|
||||
content: 'Debug: Variable state = { isLoading: true, data: null }',
|
||||
messageType: 'DEBUG',
|
||||
metadata: 'Line 42 in main.tsx'
|
||||
}
|
||||
);
|
||||
|
||||
content: 'Editing the file with the requested changes',
|
||||
messageType: 'EDIT',
|
||||
fileName: '/src/components/typed-message.tsx',
|
||||
diffLines: [
|
||||
{
|
||||
lineNumber: 62,
|
||||
content: 'const getMockResponse = (userInput: string): Message[] => {',
|
||||
type: 'context',
|
||||
},
|
||||
{ lineNumber: 63, content: ' const responses: Message[] = [];', type: 'context' },
|
||||
{ lineNumber: 64, content: '', type: 'context' },
|
||||
{
|
||||
lineNumber: 65,
|
||||
content: " if (userInput.toLowerCase().includes('plan')) {",
|
||||
type: 'remove',
|
||||
},
|
||||
{ lineNumber: 66, content: ' responses.push({', type: 'remove' },
|
||||
{ lineNumber: 65, content: ' // Always send all message types for demo', type: 'add' },
|
||||
{ lineNumber: 66, content: ' responses.push(', type: 'add' },
|
||||
{ lineNumber: 67, content: ' {', type: 'add' },
|
||||
{ lineNumber: 68, content: ' id: ++messageCounter.current,', type: 'context' },
|
||||
{ lineNumber: 69, content: " type: 'assistant',", type: 'context' },
|
||||
{
|
||||
lineNumber: 70,
|
||||
content:
|
||||
" content: 'I\\'ll create a detailed plan for your request. Let me analyze the requirements and break down the implementation steps.',",
|
||||
type: 'remove',
|
||||
},
|
||||
{
|
||||
lineNumber: 70,
|
||||
content: " content: 'Creating a comprehensive plan for your request.',",
|
||||
type: 'add',
|
||||
},
|
||||
{ lineNumber: 71, content: " messageType: 'PLAN',", type: 'context' },
|
||||
{
|
||||
lineNumber: 72,
|
||||
content: " metadata: 'Updated: 3 total (3 pending, 0 in progress, 0 completed)'",
|
||||
type: 'context',
|
||||
},
|
||||
{ lineNumber: 73, content: ' })', type: 'remove' },
|
||||
{ lineNumber: 73, content: ' },', type: 'add' },
|
||||
{
|
||||
lineNumber: 74,
|
||||
content: " } else if (userInput.toLowerCase().includes('search')) {",
|
||||
type: 'remove',
|
||||
},
|
||||
{ lineNumber: 75, content: ' responses.push({', type: 'remove' },
|
||||
{ lineNumber: 74, content: ' {', type: 'add' },
|
||||
{ lineNumber: 75, content: ' id: ++messageCounter.current,', type: 'add' },
|
||||
{ lineNumber: 76, content: " type: 'assistant',", type: 'add' },
|
||||
{
|
||||
lineNumber: 77,
|
||||
content: " content: 'Searching for relevant information across the web...',",
|
||||
type: 'remove',
|
||||
},
|
||||
{
|
||||
lineNumber: 77,
|
||||
content: " content: 'Executing command: npm install',",
|
||||
type: 'add',
|
||||
},
|
||||
{ lineNumber: 78, content: " messageType: 'EXECUTE',", type: 'add' },
|
||||
{
|
||||
lineNumber: 79,
|
||||
content: " metadata: 'cd /project && npm install, impact: low'",
|
||||
type: 'add',
|
||||
},
|
||||
{ lineNumber: 80, content: ' },', type: 'add' },
|
||||
{ lineNumber: 81, content: ' {', type: 'add' },
|
||||
{ lineNumber: 82, content: ' id: ++messageCounter.current,', type: 'add' },
|
||||
{ lineNumber: 83, content: " type: 'assistant',", type: 'add' },
|
||||
{
|
||||
lineNumber: 84,
|
||||
content: " content: 'Searching for documentation and best practices...',",
|
||||
type: 'add',
|
||||
},
|
||||
{ lineNumber: 85, content: " messageType: 'WEB_SEARCH',", type: 'remove' },
|
||||
{
|
||||
lineNumber: 86,
|
||||
content: ' metadata: \'"site:github.com generativelanguage auth_tokens.proto"\'',
|
||||
type: 'remove',
|
||||
},
|
||||
{ lineNumber: 87, content: ' });', type: 'remove' },
|
||||
{
|
||||
lineNumber: 88,
|
||||
content: " } else if (userInput.toLowerCase().includes('run')) {",
|
||||
type: 'remove',
|
||||
},
|
||||
{ lineNumber: 88, content: ' } else {', type: 'add' },
|
||||
{ lineNumber: 89, content: '', type: 'context' },
|
||||
{ lineNumber: 90, content: '', type: 'context' },
|
||||
{
|
||||
lineNumber: 91,
|
||||
content: " metadata: 'React hooks useState useEffect'",
|
||||
type: 'remove',
|
||||
},
|
||||
{
|
||||
lineNumber: 91,
|
||||
content:
|
||||
" content: 'cd /Users/safzan/Development/insideim/jobai-backend && node --env-file=.env --input-type=module --import=axios',",
|
||||
type: 'add',
|
||||
},
|
||||
{ lineNumber: 92, content: " messageType: 'EXECUTE',", type: 'remove' },
|
||||
{
|
||||
lineNumber: 93,
|
||||
content:
|
||||
" metadata: 'ls /Users/safzan/Development/insideim/cheating-daddy/node_modules/@google/genai, impact: medium'",
|
||||
type: 'remove',
|
||||
},
|
||||
{ lineNumber: 94, content: ' }));', type: 'remove' },
|
||||
{
|
||||
lineNumber: 95,
|
||||
content: " } else if (userInput.toLowerCase().includes('error')) {",
|
||||
type: 'remove',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return responses;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleSubmit = useCallback(() => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
setInput('');
|
||||
|
@ -136,78 +243,92 @@ export function Main() {
|
|||
};
|
||||
|
||||
const mockResponses = getMockResponse(trimmed);
|
||||
|
||||
|
||||
setMessages((prev) => [...prev, userMessage, ...mockResponses]);
|
||||
setInput('');
|
||||
};
|
||||
}, [input]);
|
||||
|
||||
const handleCommandExecute = (command: SlashCommand) => {
|
||||
switch (command.action) {
|
||||
case 'settings':
|
||||
setShowSettings(true);
|
||||
break;
|
||||
case 'clear':
|
||||
setHistory([]);
|
||||
break;
|
||||
case 'exit':
|
||||
exit();
|
||||
break;
|
||||
case 'help': {
|
||||
historyCounter.current += 1;
|
||||
const helpEntry: ChatHistoryEntry = {
|
||||
id: historyCounter.current,
|
||||
value:
|
||||
'Available commands:\n/settings - Configure app settings\n/clear - Clear chat history\n/exit - Exit the app\n/help - Show this help',
|
||||
};
|
||||
setHistory((prev) => [...prev, helpEntry].slice(-5));
|
||||
break;
|
||||
const handleCommandExecute = useCallback(
|
||||
(command: SlashCommand) => {
|
||||
switch (command.action) {
|
||||
case 'settings':
|
||||
setShowSettings(true);
|
||||
break;
|
||||
case 'clear':
|
||||
setHistory([]);
|
||||
break;
|
||||
case 'exit':
|
||||
exit();
|
||||
break;
|
||||
case 'help': {
|
||||
historyCounter.current += 1;
|
||||
const helpEntry: ChatHistoryEntry = {
|
||||
id: historyCounter.current,
|
||||
value:
|
||||
'Available commands:\n/settings - Configure app settings\n/clear - Clear chat history\n/exit - Exit the app\n/help - Show this help',
|
||||
};
|
||||
setHistory((prev) => [...prev, helpEntry].slice(-5));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[exit]
|
||||
);
|
||||
|
||||
if (showSettings) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} paddingY={2}>
|
||||
<SettingsForm onClose={() => {
|
||||
setShowSettings(false);
|
||||
// Refresh vim enabled setting after settings close
|
||||
setVimEnabled(getSetting('vimMode'));
|
||||
}} />
|
||||
<SettingsForm
|
||||
onClose={() => {
|
||||
setShowSettings(false);
|
||||
// Refresh vim enabled setting after settings close
|
||||
setVimEnabled(getSetting('vimMode'));
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Memoize message list to prevent re-renders from cursor blinking
|
||||
const messageList = useMemo(() => {
|
||||
return messages.map((message) => {
|
||||
if (message.type === 'user') {
|
||||
return (
|
||||
<Box key={message.id} marginBottom={1}>
|
||||
<Text color="#a855f7" bold>
|
||||
❯{' '}
|
||||
</Text>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (message.messageType) {
|
||||
return (
|
||||
<Box key={message.id} flexDirection="column">
|
||||
<TypedMessage
|
||||
type={message.messageType}
|
||||
content={message.content}
|
||||
metadata={message.metadata}
|
||||
/>
|
||||
{message.diffLines && <Diff lines={message.diffLines} fileName={message.fileName} />}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box key={message.id} marginBottom={1}>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} paddingY={2} gap={1}>
|
||||
<ChatTitle />
|
||||
<ChatVersionTagline />
|
||||
<ChatIntroText />
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{messages.map((message) => {
|
||||
if (message.type === 'user') {
|
||||
return (
|
||||
<Box key={message.id} marginBottom={1}>
|
||||
<Text color="#a855f7" bold>❯ </Text>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (message.messageType) {
|
||||
return (
|
||||
<TypedMessage
|
||||
key={message.id}
|
||||
type={message.messageType}
|
||||
content={message.content}
|
||||
metadata={message.metadata}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box key={message.id} marginBottom={1}>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{messageList}
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Box height={1}>
|
||||
|
@ -227,9 +348,9 @@ export function Main() {
|
|||
onAutocompleteStateChange={setIsAutocompleteOpen}
|
||||
/>
|
||||
<Box justifyContent="space-between">
|
||||
<VimStatus
|
||||
vimMode={currentVimMode}
|
||||
vimEnabled={vimEnabled}
|
||||
<VimStatus
|
||||
vimMode={currentVimMode}
|
||||
vimEnabled={vimEnabled}
|
||||
hideWhenAutocomplete={isAutocompleteOpen}
|
||||
/>
|
||||
<ChatFooter />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Box, Text } from 'ink';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { type FileSearchResult, searchFiles } from '../utils/file-search';
|
||||
import { type SlashCommand, searchCommands } from '../utils/slash-commands';
|
||||
import { CommandAutocomplete } from './command-autocomplete';
|
||||
|
@ -8,15 +8,15 @@ import { MultiLineTextInput, replaceMention } from './multi-line-text-input';
|
|||
import { SettingsForm } from './settings-form';
|
||||
import { SimpleBigText } from './simple-big-text';
|
||||
|
||||
export function ChatTitle() {
|
||||
export const ChatTitle = memo(function ChatTitle() {
|
||||
return (
|
||||
<Box justifyContent="center">
|
||||
<SimpleBigText text="Buster" color="#f5f3ff" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export function ChatVersionTagline() {
|
||||
export const ChatVersionTagline = memo(function ChatVersionTagline() {
|
||||
return (
|
||||
<Box justifyContent="center" marginTop={1}>
|
||||
<Text>
|
||||
|
@ -25,9 +25,9 @@ export function ChatVersionTagline() {
|
|||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export function ChatIntroText() {
|
||||
export const ChatIntroText = memo(function ChatIntroText() {
|
||||
const lines = useMemo(
|
||||
() => [
|
||||
'You are standing in an open terminal. An AI awaits your commands.',
|
||||
|
@ -45,7 +45,7 @@ export function ChatIntroText() {
|
|||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export function ChatStatusBar() {
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { Box, Text } from 'ink';
|
||||
import React from 'react';
|
||||
|
||||
interface DiffLine {
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'add' | 'remove' | 'context';
|
||||
}
|
||||
|
||||
interface DiffProps {
|
||||
lines: DiffLine[];
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export function Diff({ lines, fileName }: DiffProps) {
|
||||
return (
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
{fileName && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="#64748b">{fileName}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="column">
|
||||
{lines.map((line, index) => (
|
||||
<Box key={`${line.lineNumber}-${index}`} gap={1}>
|
||||
<Text color="#475569" dimColor>
|
||||
{String(line.lineNumber).padStart(3, ' ')}
|
||||
</Text>
|
||||
{line.type === 'add' && <Text color="#22c55e">+</Text>}
|
||||
{line.type === 'remove' && <Text color="#ef4444">-</Text>}
|
||||
{line.type === 'context' && <Text> </Text>}
|
||||
<Text
|
||||
backgroundColor={
|
||||
line.type === 'add' ? '#166534' : line.type === 'remove' ? '#7f1d1d' : undefined
|
||||
}
|
||||
color={line.type === 'add' || line.type === 'remove' ? '#ffffff' : '#e2e8f0'}
|
||||
>
|
||||
{line.content}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -30,9 +30,9 @@ export function MultiLineTextInput({
|
|||
onVimModeChange,
|
||||
}: MultiLineTextInputProps) {
|
||||
const [cursorPosition, setCursorPosition] = useState(value.length);
|
||||
const [showCursor, setShowCursor] = useState(true);
|
||||
const [expectingNewline, setExpectingNewline] = useState(false);
|
||||
const cursorBlinkTimer = useRef<NodeJS.Timeout>();
|
||||
// Always show cursor - no blinking to prevent re-renders
|
||||
const showCursor = true;
|
||||
|
||||
// Vim mode state
|
||||
const [vimEnabled] = useState(() => getSetting('vimMode'));
|
||||
|
@ -48,96 +48,87 @@ export function MultiLineTextInput({
|
|||
}
|
||||
}, [vimState.mode, vimEnabled, onVimModeChange]);
|
||||
|
||||
// Cursor blinking effect
|
||||
useEffect(() => {
|
||||
if (focus) {
|
||||
// In vim normal mode, cursor should be solid
|
||||
if (vimEnabled && vimState.mode === 'normal') {
|
||||
setShowCursor(true);
|
||||
} else {
|
||||
cursorBlinkTimer.current = setInterval(() => {
|
||||
setShowCursor((prev) => !prev);
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
setShowCursor(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (cursorBlinkTimer.current) {
|
||||
clearInterval(cursorBlinkTimer.current);
|
||||
}
|
||||
};
|
||||
}, [focus, vimEnabled, vimState.mode]);
|
||||
|
||||
// Update cursor position when value changes externally (e.g., when cleared after submit)
|
||||
useEffect(() => {
|
||||
setCursorPosition(value.length);
|
||||
}, [value]);
|
||||
|
||||
// Detect slash commands
|
||||
// Detect slash commands with debounce
|
||||
useEffect(() => {
|
||||
if (!onSlashChange) return;
|
||||
|
||||
// Check if we're at the beginning or after a newline
|
||||
let slashStart = -1;
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Check if we're at the beginning or after a newline
|
||||
let slashStart = -1;
|
||||
|
||||
// Look for a slash at the start of the current line
|
||||
const lines = value.substring(0, cursorPosition).split('\n');
|
||||
const currentLine = lines[lines.length - 1];
|
||||
const currentLineStart = cursorPosition - currentLine.length;
|
||||
// Look for a slash at the start of the current line
|
||||
const lines = value.substring(0, cursorPosition).split('\n');
|
||||
const currentLine = lines[lines.length - 1];
|
||||
const currentLineStart = cursorPosition - currentLine.length;
|
||||
|
||||
if (currentLine.startsWith('/')) {
|
||||
slashStart = currentLineStart;
|
||||
const slashEnd = cursorPosition;
|
||||
const slashQuery = value.substring(slashStart + 1, slashEnd);
|
||||
if (currentLine.startsWith('/')) {
|
||||
slashStart = currentLineStart;
|
||||
const slashEnd = cursorPosition;
|
||||
const slashQuery = value.substring(slashStart + 1, slashEnd);
|
||||
|
||||
// Only trigger if we're still in the command (no spaces)
|
||||
if (!slashQuery.includes(' ') && !slashQuery.includes('\n')) {
|
||||
onSlashChange(slashQuery, slashStart);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No active slash command
|
||||
onSlashChange(null, -1);
|
||||
}, [value, cursorPosition, onSlashChange]);
|
||||
|
||||
// Detect @ mentions
|
||||
useEffect(() => {
|
||||
if (!onMentionChange) return;
|
||||
|
||||
// Find the last @ before cursor position
|
||||
let mentionStart = -1;
|
||||
for (let i = cursorPosition - 1; i >= 0; i--) {
|
||||
if (value[i] === '@') {
|
||||
mentionStart = i;
|
||||
break;
|
||||
}
|
||||
// Stop if we hit whitespace or newline (mention ended)
|
||||
if (value[i] === ' ' || value[i] === '\n' || value[i] === '\t') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionStart !== -1) {
|
||||
// Check if there's a space or newline before @ (or it's at the start)
|
||||
const charBefore = mentionStart > 0 ? value[mentionStart - 1] : ' ';
|
||||
if (charBefore === ' ' || charBefore === '\n' || charBefore === '\t' || mentionStart === 0) {
|
||||
// Extract the mention query (text after @)
|
||||
const mentionEnd = cursorPosition;
|
||||
const mentionQuery = value.substring(mentionStart + 1, mentionEnd);
|
||||
|
||||
// Only trigger if we're still in the mention (no spaces)
|
||||
if (!mentionQuery.includes(' ') && !mentionQuery.includes('\n')) {
|
||||
onMentionChange(mentionQuery, mentionStart);
|
||||
// Only trigger if we're still in the command (no spaces)
|
||||
if (!slashQuery.includes(' ') && !slashQuery.includes('\n')) {
|
||||
onSlashChange(slashQuery, slashStart);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No active mention
|
||||
onMentionChange(null, -1);
|
||||
// No active slash command
|
||||
onSlashChange(null, -1);
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [value, cursorPosition, onSlashChange]);
|
||||
|
||||
// Detect @ mentions with debounce
|
||||
useEffect(() => {
|
||||
if (!onMentionChange) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Find the last @ before cursor position
|
||||
let mentionStart = -1;
|
||||
for (let i = cursorPosition - 1; i >= 0; i--) {
|
||||
if (value[i] === '@') {
|
||||
mentionStart = i;
|
||||
break;
|
||||
}
|
||||
// Stop if we hit whitespace or newline (mention ended)
|
||||
if (value[i] === ' ' || value[i] === '\n' || value[i] === '\t') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionStart !== -1) {
|
||||
// Check if there's a space or newline before @ (or it's at the start)
|
||||
const charBefore = mentionStart > 0 ? value[mentionStart - 1] : ' ';
|
||||
if (
|
||||
charBefore === ' ' ||
|
||||
charBefore === '\n' ||
|
||||
charBefore === '\t' ||
|
||||
mentionStart === 0
|
||||
) {
|
||||
// Extract the mention query (text after @)
|
||||
const mentionEnd = cursorPosition;
|
||||
const mentionQuery = value.substring(mentionStart + 1, mentionEnd);
|
||||
|
||||
// Only trigger if we're still in the mention (no spaces)
|
||||
if (!mentionQuery.includes(' ') && !mentionQuery.includes('\n')) {
|
||||
onMentionChange(mentionQuery, mentionStart);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No active mention
|
||||
onMentionChange(null, -1);
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [value, cursorPosition, onMentionChange]);
|
||||
|
||||
useInput(
|
||||
|
@ -409,14 +400,8 @@ export function MultiLineTextInput({
|
|||
// Always reserve space for cursor to prevent shifting
|
||||
let cursorChar = '█';
|
||||
if (vimEnabled) {
|
||||
// Different cursor styles for vim modes
|
||||
if (vimState.mode === 'normal') {
|
||||
cursorChar = '▮'; // Block cursor for normal mode
|
||||
} else if (vimState.mode === 'insert') {
|
||||
cursorChar = '│'; // Line cursor for insert mode
|
||||
} else if (vimState.mode === 'visual') {
|
||||
cursorChar = '▬'; // Underscore cursor for visual mode
|
||||
}
|
||||
// Use block cursor for all vim modes
|
||||
cursorChar = '█';
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -429,17 +414,12 @@ export function MultiLineTextInput({
|
|||
|
||||
const beforeCursor = value.slice(0, cursorPosition);
|
||||
const afterCursor = value.slice(cursorPosition);
|
||||
// Always use block cursor
|
||||
let cursorChar = showCursor && focus ? '█' : ' ';
|
||||
|
||||
// Show different cursor styles for vim modes
|
||||
// Keep block cursor for all vim modes
|
||||
if (vimEnabled && focus && showCursor) {
|
||||
if (vimState.mode === 'normal') {
|
||||
cursorChar = '▮'; // Block cursor for normal mode
|
||||
} else if (vimState.mode === 'insert') {
|
||||
cursorChar = '│'; // Line cursor for insert mode
|
||||
} else if (vimState.mode === 'visual') {
|
||||
cursorChar = '▬'; // Underscore cursor for visual mode
|
||||
}
|
||||
cursorChar = '█';
|
||||
}
|
||||
|
||||
// Show special cursor when expecting newline
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import { Box, Text } from 'ink';
|
||||
import React from 'react';
|
||||
|
||||
export type MessageType =
|
||||
| 'PLAN'
|
||||
| 'EXECUTE'
|
||||
| 'WEB_SEARCH'
|
||||
| 'INFO'
|
||||
| 'ERROR'
|
||||
| 'SUCCESS'
|
||||
| 'WARNING'
|
||||
| 'DEBUG';
|
||||
export type MessageType = 'PLAN' | 'EXECUTE' | 'WRITE' | 'EDIT';
|
||||
|
||||
interface TypedMessageProps {
|
||||
type: MessageType;
|
||||
|
@ -20,30 +12,24 @@ interface TypedMessageProps {
|
|||
const typeStyles: Record<MessageType, { bg: string; fg: string; label: string }> = {
|
||||
PLAN: { bg: '#fb923c', fg: '#000000', label: 'PLAN' },
|
||||
EXECUTE: { bg: '#fb923c', fg: '#000000', label: 'EXECUTE' },
|
||||
WEB_SEARCH: { bg: '#fb923c', fg: '#000000', label: 'WEB SEARCH' },
|
||||
INFO: { bg: '#3b82f6', fg: '#ffffff', label: 'INFO' },
|
||||
ERROR: { bg: '#ef4444', fg: '#ffffff', label: 'ERROR' },
|
||||
SUCCESS: { bg: '#22c55e', fg: '#ffffff', label: 'SUCCESS' },
|
||||
WARNING: { bg: '#eab308', fg: '#000000', label: 'WARNING' },
|
||||
DEBUG: { bg: '#8b5cf6', fg: '#ffffff', label: 'DEBUG' },
|
||||
WRITE: { bg: '#3b82f6', fg: '#ffffff', label: 'WRITE' },
|
||||
EDIT: { bg: '#22c55e', fg: '#ffffff', label: 'EDIT' },
|
||||
};
|
||||
|
||||
export function TypedMessage({ type, content, metadata }: TypedMessageProps) {
|
||||
const style = typeStyles[type];
|
||||
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box gap={1}>
|
||||
<Text backgroundColor={style.bg} color={style.fg} bold>
|
||||
{` ${style.label} `}
|
||||
</Text>
|
||||
{metadata && (
|
||||
<Text dimColor>{metadata}</Text>
|
||||
)}
|
||||
{metadata && <Text dimColor>{metadata}</Text>}
|
||||
</Box>
|
||||
<Box marginLeft={2} marginTop={0}>
|
||||
<Text>{content}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import {
|
||||
CliChatCreateRequestSchema,
|
||||
type CliChatCreateResponse,
|
||||
} from '@buster/server-shared/chats';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { createCliChatHandler } from './handler';
|
||||
|
||||
export const POST = new Hono().post(
|
||||
'/',
|
||||
zValidator('json', CliChatCreateRequestSchema),
|
||||
async (c) => {
|
||||
const request = c.req.valid('json');
|
||||
const user = c.get('busterUser');
|
||||
|
||||
const response: CliChatCreateResponse = await createCliChatHandler(request, user);
|
||||
|
||||
return c.json(response);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,41 @@
|
|||
import type { User } from '@buster/database/queries';
|
||||
import type { CliChatCreateRequest, CliChatCreateResponse } from '@buster/server-shared/chats';
|
||||
|
||||
/**
|
||||
* Handler function for creating a CLI chat.
|
||||
*
|
||||
* TODO: Implement CLI-specific chat logic
|
||||
*/
|
||||
export async function createCliChatHandler(
|
||||
request: CliChatCreateRequest,
|
||||
user: User
|
||||
): Promise<CliChatCreateResponse> {
|
||||
// Placeholder - just acknowledge the request
|
||||
console.log('CLI chat request received:', {
|
||||
userId: user.id,
|
||||
prompt: request.prompt,
|
||||
chatId: request.chat_id,
|
||||
});
|
||||
// Return a minimal response
|
||||
return {
|
||||
id: 'temp-id',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
title: 'CLI Chat',
|
||||
messages: {},
|
||||
created_by: user.id,
|
||||
created_by_id: user.id,
|
||||
created_by_name: user.email || 'Unknown',
|
||||
created_by_avatar: null,
|
||||
individual_permissions: [],
|
||||
publicly_accessible: false,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
workspace_sharing: 'none',
|
||||
workspace_member_count: 0,
|
||||
message_ids: [],
|
||||
is_favorited: false,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { ChatError } from '@buster/server-shared/chats';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { requireAuth } from '../../../../middleware/auth';
|
||||
import { POST } from './POST';
|
||||
|
||||
const app = new Hono()
|
||||
// Apply authentication middleware
|
||||
.use('*', requireAuth)
|
||||
.route('/', POST)
|
||||
.onError((e, c) => {
|
||||
if (e instanceof ChatError) {
|
||||
return c.json(e.toResponse(), e.statusCode);
|
||||
}
|
||||
if (e instanceof HTTPException) {
|
||||
return e.getResponse();
|
||||
}
|
||||
|
||||
throw new HTTPException(500, {
|
||||
message: 'Internal server error',
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -14,6 +14,7 @@ import { z } from 'zod';
|
|||
import GET from './GET';
|
||||
import chatById from './[id]';
|
||||
import { cancelChatHandler } from './cancel-chat';
|
||||
import cliChat from './cli';
|
||||
import { createChatHandler } from './handler';
|
||||
|
||||
const app = new Hono()
|
||||
|
@ -21,6 +22,7 @@ const app = new Hono()
|
|||
.use('*', requireAuth)
|
||||
.route('/', GET)
|
||||
.route('/:id', chatById)
|
||||
.route('/cli', cliChat)
|
||||
// POST /chats - Create a new chat
|
||||
.post('/', zValidator('json', ChatCreateRequestSchema), async (c) => {
|
||||
const request = c.req.valid('json');
|
||||
|
|
|
@ -59,3 +59,12 @@ export type DuplicateChatRequest = z.infer<typeof DuplicateChatRequestSchema>;
|
|||
// Logs list request (same as chats list)
|
||||
export const GetLogsListRequestSchema = GetChatsListRequestSchema;
|
||||
export type GetLogsListRequest = z.infer<typeof GetLogsListRequestSchema>;
|
||||
|
||||
// Request for creating a CLI chat
|
||||
export const CliChatCreateRequestSchema = z.object({
|
||||
prompt: z.string().describe('User prompt for the CLI chat'),
|
||||
chat_id: z.string().optional().describe('Optional existing chat ID to continue'),
|
||||
message_id: z.string().optional().describe('Optional message ID to continue from'),
|
||||
});
|
||||
|
||||
export type CliChatCreateRequest = z.infer<typeof CliChatCreateRequestSchema>;
|
||||
|
|
|
@ -39,3 +39,7 @@ export type DeleteChatsResponse = z.infer<typeof DeleteChatsResponseSchema>;
|
|||
|
||||
export const ShareChatResponseSchema = ChatWithMessagesSchema;
|
||||
export type ShareChatResponse = z.infer<typeof ShareChatResponseSchema>;
|
||||
|
||||
// Response for creating a CLI chat
|
||||
export const CliChatCreateResponseSchema = ChatWithMessagesSchema;
|
||||
export type CliChatCreateResponse = z.infer<typeof CliChatCreateResponseSchema>;
|
||||
|
|
Loading…
Reference in New Issue