diff --git a/apps/cli/src/commands/main.tsx b/apps/cli/src/commands/main.tsx index 32307cd66..7a0b49839 100644 --- a/apps/cli/src/commands/main.tsx +++ b/apps/cli/src/commands/main.tsx @@ -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 ( - { - setShowSettings(false); - // Refresh vim enabled setting after settings close - setVimEnabled(getSetting('vimMode')); - }} /> + { + setShowSettings(false); + // Refresh vim enabled setting after settings close + setVimEnabled(getSetting('vimMode')); + }} + /> ); } + // Memoize message list to prevent re-renders from cursor blinking + const messageList = useMemo(() => { + return messages.map((message) => { + if (message.type === 'user') { + return ( + + + ❯{' '} + + {message.content} + + ); + } else if (message.messageType) { + return ( + + + {message.diffLines && } + + ); + } else { + return ( + + {message.content} + + ); + } + }); + }, [messages]); + return ( - {messages.map((message) => { - if (message.type === 'user') { - return ( - - - {message.content} - - ); - } else if (message.messageType) { - return ( - - ); - } else { - return ( - - {message.content} - - ); - } - })} + {messageList} @@ -227,9 +348,9 @@ export function Main() { onAutocompleteStateChange={setIsAutocompleteOpen} /> - diff --git a/apps/cli/src/components/chat-layout.tsx b/apps/cli/src/components/chat-layout.tsx index 9fecee2d6..415a0f0d5 100644 --- a/apps/cli/src/components/chat-layout.tsx +++ b/apps/cli/src/components/chat-layout.tsx @@ -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 ( ); -} +}); -export function ChatVersionTagline() { +export const ChatVersionTagline = memo(function ChatVersionTagline() { return ( @@ -25,9 +25,9 @@ export function ChatVersionTagline() { ); -} +}); -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() { ))} ); -} +}); export function ChatStatusBar() { return ( diff --git a/apps/cli/src/components/diff.tsx b/apps/cli/src/components/diff.tsx new file mode 100644 index 000000000..5bba39700 --- /dev/null +++ b/apps/cli/src/components/diff.tsx @@ -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 ( + + {fileName && ( + + {fileName} + + )} + + {lines.map((line, index) => ( + + + {String(line.lineNumber).padStart(3, ' ')} + + {line.type === 'add' && +} + {line.type === 'remove' && -} + {line.type === 'context' && } + + {line.content} + + + ))} + + + ); +} diff --git a/apps/cli/src/components/multi-line-text-input.tsx b/apps/cli/src/components/multi-line-text-input.tsx index a5063db01..d056772fb 100644 --- a/apps/cli/src/components/multi-line-text-input.tsx +++ b/apps/cli/src/components/multi-line-text-input.tsx @@ -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(); + // 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 diff --git a/apps/cli/src/components/typed-message.tsx b/apps/cli/src/components/typed-message.tsx index 4838ce632..d15607312 100644 --- a/apps/cli/src/components/typed-message.tsx +++ b/apps/cli/src/components/typed-message.tsx @@ -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 = { 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 ( {` ${style.label} `} - {metadata && ( - {metadata} - )} + {metadata && {metadata}} {content} ); -} \ No newline at end of file +} diff --git a/apps/server/src/api/v2/chats/cli/POST.ts b/apps/server/src/api/v2/chats/cli/POST.ts new file mode 100644 index 000000000..986eac806 --- /dev/null +++ b/apps/server/src/api/v2/chats/cli/POST.ts @@ -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); + } +); diff --git a/apps/server/src/api/v2/chats/cli/handler.ts b/apps/server/src/api/v2/chats/cli/handler.ts new file mode 100644 index 000000000..9e22887d4 --- /dev/null +++ b/apps/server/src/api/v2/chats/cli/handler.ts @@ -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 { + // 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, + }; +} diff --git a/apps/server/src/api/v2/chats/cli/index.ts b/apps/server/src/api/v2/chats/cli/index.ts new file mode 100644 index 000000000..dda90f198 --- /dev/null +++ b/apps/server/src/api/v2/chats/cli/index.ts @@ -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; diff --git a/apps/server/src/api/v2/chats/index.ts b/apps/server/src/api/v2/chats/index.ts index 37c44ca8c..3b361ac69 100644 --- a/apps/server/src/api/v2/chats/index.ts +++ b/apps/server/src/api/v2/chats/index.ts @@ -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'); diff --git a/packages/server-shared/src/chats/requests.ts b/packages/server-shared/src/chats/requests.ts index 6ec75bb02..a21b4191d 100644 --- a/packages/server-shared/src/chats/requests.ts +++ b/packages/server-shared/src/chats/requests.ts @@ -59,3 +59,12 @@ export type DuplicateChatRequest = z.infer; // Logs list request (same as chats list) export const GetLogsListRequestSchema = GetChatsListRequestSchema; export type GetLogsListRequest = z.infer; + +// 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; diff --git a/packages/server-shared/src/chats/responses.ts b/packages/server-shared/src/chats/responses.ts index 9102144dc..e16ffce50 100644 --- a/packages/server-shared/src/chats/responses.ts +++ b/packages/server-shared/src/chats/responses.ts @@ -39,3 +39,7 @@ export type DeleteChatsResponse = z.infer; export const ShareChatResponseSchema = ChatWithMessagesSchema; export type ShareChatResponse = z.infer; + +// Response for creating a CLI chat +export const CliChatCreateResponseSchema = ChatWithMessagesSchema; +export type CliChatCreateResponse = z.infer;