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;