mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-11 10:09:38 +00:00
feat: Update README to include Cursor CLI support and enhance chat message handling with streaming improvements
This commit is contained in:
23
README.md
23
README.md
@@ -4,7 +4,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for AI-assisted coding. You can use it locally or remotely to view your active projects and sessions in claude code and make changes to them the same way you would do it in claude code CLI. This gives you a proper interface that works everywhere.
|
A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), and [Cursor CLI](https://docs.cursor.com/en/cli/overview). You can use it locally or remotely to view your active projects and sessions in Claude Code or Cursor and make changes to them from everywhere (mobile or desktop). This gives you a proper interface that works everywhere. Supports models including **Claude Sonnet 4**, **Opus 4.1**, and **GPT-5**
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -25,6 +25,14 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
|
|||||||
<em>Responsive mobile design with touch navigation</em>
|
<em>Responsive mobile design with touch navigation</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" colspan="2">
|
||||||
|
<h3>CLI Selection</h3>
|
||||||
|
<img src="public/screenshots/cli-selection.png" alt="CLI Selection" width="400">
|
||||||
|
<br>
|
||||||
|
<em>Select between Claude Code and Cursor CLI</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
@@ -34,11 +42,12 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Claude Code from mobile
|
- **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Claude Code from mobile
|
||||||
- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code
|
- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code or Cursor
|
||||||
- **Integrated Shell Terminal** - Direct access to Claude Code CLI through built-in shell functionality
|
- **Integrated Shell Terminal** - Direct access to Claude Code or Cursor CLI through built-in shell functionality
|
||||||
- **File Explorer** - Interactive file tree with syntax highlighting and live editing
|
- **File Explorer** - Interactive file tree with syntax highlighting and live editing
|
||||||
- **Git Explorer** - View, stage and commit your changes. You can also switch branches
|
- **Git Explorer** - View, stage and commit your changes. You can also switch branches
|
||||||
- **Session Management** - Resume conversations, manage multiple sessions, and track history
|
- **Session Management** - Resume conversations, manage multiple sessions, and track history
|
||||||
|
- **Model Compatibility** - Works with Claude Sonnet 4, Opus 4.1, and GPT-5
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -46,7 +55,8 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) v20 or higher
|
- [Node.js](https://nodejs.org/) v20 or higher
|
||||||
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or
|
||||||
|
- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -108,9 +118,10 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a
|
|||||||
- **Visual Project Browser** - All available projects with metadata and session counts
|
- **Visual Project Browser** - All available projects with metadata and session counts
|
||||||
- **Project Actions** - Rename, delete, and organize projects
|
- **Project Actions** - Rename, delete, and organize projects
|
||||||
- **Smart Navigation** - Quick access to recent projects and sessions
|
- **Smart Navigation** - Quick access to recent projects and sessions
|
||||||
|
- **MCP support** - Add your own MCP servers through the UI
|
||||||
|
|
||||||
#### Chat Interface
|
#### Chat Interface
|
||||||
- **Use responsive chat or Claude Code CLI** - You can either use the adapted chat interface or use the shell button to connect to Claude Code CLI.
|
- **Use responsive chat or Claude Code/Cursor CLI** - You can either use the adapted chat interface or use the shell button to connect to your selected CLI.
|
||||||
- **Real-time Communication** - Stream responses from Claude with WebSocket connection
|
- **Real-time Communication** - Stream responses from Claude with WebSocket connection
|
||||||
- **Session Management** - Resume previous conversations or start fresh sessions
|
- **Session Management** - Resume previous conversations or start fresh sessions
|
||||||
- **Message History** - Complete conversation history with timestamps and metadata
|
- **Message History** - Complete conversation history with timestamps and metadata
|
||||||
@@ -152,7 +163,7 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a
|
|||||||
### Backend (Node.js + Express)
|
### Backend (Node.js + Express)
|
||||||
- **Express Server** - RESTful API with static file serving
|
- **Express Server** - RESTful API with static file serving
|
||||||
- **WebSocket Server** - Communication for chats and project refresh
|
- **WebSocket Server** - Communication for chats and project refresh
|
||||||
- **Claude CLI Integration** - Process spawning and management
|
- **CLI Integration (Claude Code / Cursor)** - Process spawning and management
|
||||||
- **Session Management** - JSONL parsing and conversation persistence
|
- **Session Management** - JSONL parsing and conversation persistence
|
||||||
- **File System API** - Exposing file browser for projects
|
- **File System API** - Exposing file browser for projects
|
||||||
|
|
||||||
|
|||||||
BIN
public/screenshots/cli-selection.png
Normal file
BIN
public/screenshots/cli-selection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
@@ -26,8 +26,8 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
const args = [];
|
const args = [];
|
||||||
|
|
||||||
// Build flags allowing both resume and prompt together (reply in existing session)
|
// Build flags allowing both resume and prompt together (reply in existing session)
|
||||||
if (resume && sessionId) {
|
// Treat presence of sessionId as intention to resume, regardless of resume flag
|
||||||
// Resume existing session
|
if (sessionId) {
|
||||||
args.push('--resume=' + sessionId);
|
args.push('--resume=' + sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ async function spawnCursor(command, options = {}, ws) {
|
|||||||
args.push('-p', command);
|
args.push('-p', command);
|
||||||
|
|
||||||
// Add model flag if specified (only meaningful for new sessions; harmless on resume)
|
// Add model flag if specified (only meaningful for new sessions; harmless on resume)
|
||||||
if (!resume && model) {
|
if (!sessionId && model) {
|
||||||
args.push('--model', model);
|
args.push('--model', model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1151,6 +1151,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
const scrollContainerRef = useRef(null);
|
const scrollContainerRef = useRef(null);
|
||||||
|
// Streaming throttle buffers
|
||||||
|
const streamBufferRef = useRef('');
|
||||||
|
const streamTimerRef = useRef(null);
|
||||||
const [debouncedInput, setDebouncedInput] = useState('');
|
const [debouncedInput, setDebouncedInput] = useState('');
|
||||||
const [showFileDropdown, setShowFileDropdown] = useState(false);
|
const [showFileDropdown, setShowFileDropdown] = useState(false);
|
||||||
const [fileList, setFileList] = useState([]);
|
const [fileList, setFileList] = useState([]);
|
||||||
@@ -1839,7 +1842,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadMessages();
|
loadMessages();
|
||||||
}, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange, isLoading]);
|
}, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]);
|
||||||
|
|
||||||
// Update chatMessages when convertedMessages changes
|
// Update chatMessages when convertedMessages changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1910,27 +1913,56 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Handle Cursor streaming format (content_block_delta / content_block_stop)
|
// Handle Cursor streaming format (content_block_delta / content_block_stop)
|
||||||
if (messageData && typeof messageData === 'object' && messageData.type) {
|
if (messageData && typeof messageData === 'object' && messageData.type) {
|
||||||
if (messageData.type === 'content_block_delta' && messageData.delta?.text) {
|
if (messageData.type === 'content_block_delta' && messageData.delta?.text) {
|
||||||
setChatMessages(prev => {
|
// Buffer deltas and flush periodically to reduce rerenders
|
||||||
// Check if the last message is an assistant message we can append to
|
streamBufferRef.current += messageData.delta.text;
|
||||||
if (prev.length > 0 && prev[prev.length - 1].type === 'assistant' && !prev[prev.length - 1].isToolUse) {
|
if (!streamTimerRef.current) {
|
||||||
// Append to the last assistant message
|
streamTimerRef.current = setTimeout(() => {
|
||||||
const updatedMessages = [...prev];
|
const chunk = streamBufferRef.current;
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
streamBufferRef.current = '';
|
||||||
lastMessage.content = (lastMessage.content || '') + messageData.delta.text;
|
streamTimerRef.current = null;
|
||||||
return updatedMessages;
|
if (!chunk) return;
|
||||||
} else {
|
setChatMessages(prev => {
|
||||||
// Create a new assistant message for the first delta
|
const updated = [...prev];
|
||||||
return [...prev, {
|
const last = updated[updated.length - 1];
|
||||||
type: 'assistant',
|
if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
|
||||||
content: messageData.delta.text,
|
last.content = (last.content || '') + chunk;
|
||||||
timestamp: new Date()
|
} else {
|
||||||
}];
|
updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
|
||||||
}
|
}
|
||||||
});
|
return updated;
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (messageData.type === 'content_block_stop') {
|
if (messageData.type === 'content_block_stop') {
|
||||||
// Nothing specific to do; leave as-is
|
// Flush any buffered text and mark streaming message complete
|
||||||
|
if (streamTimerRef.current) {
|
||||||
|
clearTimeout(streamTimerRef.current);
|
||||||
|
streamTimerRef.current = null;
|
||||||
|
}
|
||||||
|
const chunk = streamBufferRef.current;
|
||||||
|
streamBufferRef.current = '';
|
||||||
|
if (chunk) {
|
||||||
|
setChatMessages(prev => {
|
||||||
|
const updated = [...prev];
|
||||||
|
const last = updated[updated.length - 1];
|
||||||
|
if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
|
||||||
|
last.content = (last.content || '') + chunk;
|
||||||
|
} else {
|
||||||
|
updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setChatMessages(prev => {
|
||||||
|
const updated = [...prev];
|
||||||
|
const last = updated[updated.length - 1];
|
||||||
|
if (last && last.type === 'assistant' && last.isStreaming) {
|
||||||
|
last.isStreaming = false;
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2075,11 +2107,30 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'claude-output':
|
case 'claude-output':
|
||||||
setChatMessages(prev => [...prev, {
|
{
|
||||||
type: 'assistant',
|
const cleaned = String(latestMessage.data || '');
|
||||||
content: latestMessage.data,
|
if (cleaned.trim()) {
|
||||||
timestamp: new Date()
|
streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned);
|
||||||
}]);
|
if (!streamTimerRef.current) {
|
||||||
|
streamTimerRef.current = setTimeout(() => {
|
||||||
|
const chunk = streamBufferRef.current;
|
||||||
|
streamBufferRef.current = '';
|
||||||
|
streamTimerRef.current = null;
|
||||||
|
if (!chunk) return;
|
||||||
|
setChatMessages(prev => {
|
||||||
|
const updated = [...prev];
|
||||||
|
const last = updated[updated.length - 1];
|
||||||
|
if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
|
||||||
|
last.content = last.content ? `${last.content}\n${chunk}` : chunk;
|
||||||
|
} else {
|
||||||
|
updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'claude-interactive-prompt':
|
case 'claude-interactive-prompt':
|
||||||
// Handle interactive prompts from CLI
|
// Handle interactive prompts from CLI
|
||||||
@@ -2163,13 +2214,28 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
try {
|
try {
|
||||||
const r = latestMessage.data || {};
|
const r = latestMessage.data || {};
|
||||||
const textResult = typeof r.result === 'string' ? r.result : '';
|
const textResult = typeof r.result === 'string' ? r.result : '';
|
||||||
if (textResult && textResult.trim()) {
|
// Flush buffered deltas before finalizing
|
||||||
setChatMessages(prev => [...prev, {
|
if (streamTimerRef.current) {
|
||||||
type: r.is_error ? 'error' : 'assistant',
|
clearTimeout(streamTimerRef.current);
|
||||||
content: textResult,
|
streamTimerRef.current = null;
|
||||||
timestamp: new Date()
|
|
||||||
}]);
|
|
||||||
}
|
}
|
||||||
|
const pendingChunk = streamBufferRef.current;
|
||||||
|
streamBufferRef.current = '';
|
||||||
|
|
||||||
|
setChatMessages(prev => {
|
||||||
|
const updated = [...prev];
|
||||||
|
// Try to consolidate into the last streaming assistant message
|
||||||
|
const last = updated[updated.length - 1];
|
||||||
|
if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
|
||||||
|
// Replace streaming content with the final content so deltas don't remain
|
||||||
|
const finalContent = textResult && textResult.trim() ? textResult : (last.content || '') + (pendingChunk || '');
|
||||||
|
last.content = finalContent;
|
||||||
|
last.isStreaming = false;
|
||||||
|
} else if (textResult && textResult.trim()) {
|
||||||
|
updated.push({ type: r.is_error ? 'error' : 'assistant', content: textResult, timestamp: new Date(), isStreaming: false });
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Error handling cursor-result message:', e);
|
console.warn('Error handling cursor-result message:', e);
|
||||||
}
|
}
|
||||||
@@ -2198,23 +2264,25 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const raw = String(latestMessage.data ?? '');
|
const raw = String(latestMessage.data ?? '');
|
||||||
const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim();
|
const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim();
|
||||||
if (cleaned) {
|
if (cleaned) {
|
||||||
setChatMessages(prev => {
|
streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned);
|
||||||
// If the last message is from assistant and not a tool use, append to it
|
if (!streamTimerRef.current) {
|
||||||
if (prev.length > 0 && prev[prev.length - 1].type === 'assistant' && !prev[prev.length - 1].isToolUse) {
|
streamTimerRef.current = setTimeout(() => {
|
||||||
const updatedMessages = [...prev];
|
const chunk = streamBufferRef.current;
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1];
|
streamBufferRef.current = '';
|
||||||
// Append with a newline if there's already content
|
streamTimerRef.current = null;
|
||||||
lastMessage.content = lastMessage.content ? `${lastMessage.content}\n${cleaned}` : cleaned;
|
if (!chunk) return;
|
||||||
return updatedMessages;
|
setChatMessages(prev => {
|
||||||
} else {
|
const updated = [...prev];
|
||||||
// Otherwise create a new assistant message
|
const last = updated[updated.length - 1];
|
||||||
return [...prev, {
|
if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
|
||||||
type: 'assistant',
|
last.content = last.content ? `${last.content}\n${chunk}` : chunk;
|
||||||
content: cleaned,
|
} else {
|
||||||
timestamp: new Date()
|
updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
|
||||||
}];
|
}
|
||||||
}
|
return updated;
|
||||||
});
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Error handling cursor-output message:', e);
|
console.warn('Error handling cursor-output message:', e);
|
||||||
@@ -2636,12 +2704,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response
|
setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response
|
||||||
setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered
|
setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered
|
||||||
|
|
||||||
|
// Determine effective session id for replies to avoid race on state updates
|
||||||
|
const effectiveSessionId = currentSessionId || selectedSession?.id || sessionStorage.getItem('cursorSessionId');
|
||||||
|
|
||||||
// Session Protection: Mark session as active to prevent automatic project updates during conversation
|
// Session Protection: Mark session as active to prevent automatic project updates during conversation
|
||||||
// This is crucial for maintaining chat state integrity. We handle two cases:
|
// Use existing session if available; otherwise a temporary placeholder until backend provides real ID
|
||||||
// 1. Existing sessions: Use the real currentSessionId
|
const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`;
|
||||||
// 2. New sessions: Generate temporary identifier "new-session-{timestamp}" since real ID comes via WebSocket later
|
|
||||||
// This ensures no gap in protection between message send and session creation
|
|
||||||
const sessionToActivate = currentSessionId || `new-session-${Date.now()}`;
|
|
||||||
if (onSessionActive) {
|
if (onSessionActive) {
|
||||||
onSessionActive(sessionToActivate);
|
onSessionActive(sessionToActivate);
|
||||||
}
|
}
|
||||||
@@ -2672,13 +2740,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
sendMessage({
|
sendMessage({
|
||||||
type: 'cursor-command',
|
type: 'cursor-command',
|
||||||
command: input,
|
command: input,
|
||||||
sessionId: currentSessionId,
|
sessionId: effectiveSessionId,
|
||||||
options: {
|
options: {
|
||||||
// Prefer fullPath (actual cwd for project), fallback to path
|
// Prefer fullPath (actual cwd for project), fallback to path
|
||||||
cwd: selectedProject.fullPath || selectedProject.path,
|
cwd: selectedProject.fullPath || selectedProject.path,
|
||||||
projectPath: selectedProject.fullPath || selectedProject.path,
|
projectPath: selectedProject.fullPath || selectedProject.path,
|
||||||
sessionId: currentSessionId,
|
sessionId: effectiveSessionId,
|
||||||
resume: !!currentSessionId,
|
resume: !!effectiveSessionId,
|
||||||
model: cursorModel,
|
model: cursorModel,
|
||||||
skipPermissions: toolsSettings?.skipPermissions || false,
|
skipPermissions: toolsSettings?.skipPermissions || false,
|
||||||
toolsSettings: toolsSettings
|
toolsSettings: toolsSettings
|
||||||
@@ -3073,7 +3141,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
<div className="chat-message assistant">
|
<div className="chat-message assistant">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1 bg-gray-600">
|
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1 bg-transparent">
|
||||||
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? (
|
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? (
|
||||||
<CursorLogo className="w-full h-full" />
|
<CursorLogo className="w-full h-full" />
|
||||||
) : (
|
) : (
|
||||||
@@ -3104,7 +3172,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6'
|
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6'
|
||||||
}`}>
|
}`}>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<ClaudeStatus
|
||||||
|
status={claudeStatus}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onAbort={handleAbortSession}
|
||||||
|
provider={provider}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
|
{/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
|
||||||
<div className="max-w-4xl mx-auto mb-3">
|
<div className="max-w-4xl mx-auto mb-3">
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user