feat(ui): add prompt queuing system and improve session management
- Add prompt queue to handle multiple prompts when Claude is processing - Improve session reconnection with better event listener management - Fix race conditions in session initialization and cleanup - Replace Loader2 with rotating symbol for consistent loading states - Remove TokenCounter integration and loading-disabled input restrictions - Enhance cancellation logic with proper state cleanup - Update thinking mode phrase formatting in FloatingPromptInput - Improve UI layout with better spacing and error positioning This enables users to queue multiple prompts without waiting for the current one to complete, providing a smoother interaction experience.
This commit is contained in:
@@ -3,13 +3,15 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Terminal,
|
Terminal,
|
||||||
Loader2,
|
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Copy,
|
Copy,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Settings,
|
Settings,
|
||||||
Globe
|
Globe,
|
||||||
|
ChevronUp,
|
||||||
|
X,
|
||||||
|
Hash
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -22,7 +24,6 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|||||||
import { StreamMessage } from "./StreamMessage";
|
import { StreamMessage } from "./StreamMessage";
|
||||||
import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingPromptInput";
|
import { FloatingPromptInput, type FloatingPromptInputRef } from "./FloatingPromptInput";
|
||||||
import { ErrorBoundary } from "./ErrorBoundary";
|
import { ErrorBoundary } from "./ErrorBoundary";
|
||||||
import { TokenCounter } from "./TokenCounter";
|
|
||||||
import { TimelineNavigator } from "./TimelineNavigator";
|
import { TimelineNavigator } from "./TimelineNavigator";
|
||||||
import { CheckpointSettings } from "./CheckpointSettings";
|
import { CheckpointSettings } from "./CheckpointSettings";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||||
@@ -84,7 +85,9 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
const [showForkDialog, setShowForkDialog] = useState(false);
|
const [showForkDialog, setShowForkDialog] = useState(false);
|
||||||
const [forkCheckpointId, setForkCheckpointId] = useState<string | null>(null);
|
const [forkCheckpointId, setForkCheckpointId] = useState<string | null>(null);
|
||||||
const [forkSessionName, setForkSessionName] = useState("");
|
const [forkSessionName, setForkSessionName] = useState("");
|
||||||
const [isCancelling, setIsCancelling] = useState(false);
|
|
||||||
|
// Queued prompts state
|
||||||
|
const [queuedPrompts, setQueuedPrompts] = useState<Array<{ id: string; prompt: string; model: "sonnet" | "opus" }>>([]);
|
||||||
|
|
||||||
// New state for preview feature
|
// New state for preview feature
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
@@ -93,10 +96,21 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
const [splitPosition, setSplitPosition] = useState(50);
|
const [splitPosition, setSplitPosition] = useState(50);
|
||||||
const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);
|
const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);
|
||||||
|
|
||||||
|
// Add collapsed state for queued prompts
|
||||||
|
const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false);
|
||||||
|
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const unlistenRefs = useRef<UnlistenFn[]>([]);
|
const unlistenRefs = useRef<UnlistenFn[]>([]);
|
||||||
const hasActiveSessionRef = useRef(false);
|
const hasActiveSessionRef = useRef(false);
|
||||||
const floatingPromptRef = useRef<FloatingPromptInputRef>(null);
|
const floatingPromptRef = useRef<FloatingPromptInputRef>(null);
|
||||||
|
const queuedPromptsRef = useRef<Array<{ id: string; prompt: string; model: "sonnet" | "opus" }>>([]);
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
const isListeningRef = useRef(false);
|
||||||
|
|
||||||
|
// Keep ref in sync with state
|
||||||
|
useEffect(() => {
|
||||||
|
queuedPromptsRef.current = queuedPrompts;
|
||||||
|
}, [queuedPrompts]);
|
||||||
|
|
||||||
// Get effective session info (from prop or extracted) - use useMemo to ensure it updates
|
// Get effective session info (from prop or extracted) - use useMemo to ensure it updates
|
||||||
const effectiveSession = useMemo(() => {
|
const effectiveSession = useMemo(() => {
|
||||||
@@ -197,21 +211,27 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
// Load session history if resuming
|
// Load session history if resuming
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (session) {
|
||||||
loadSessionHistory();
|
// Set the claudeSessionId immediately when we have a session
|
||||||
|
setClaudeSessionId(session.id);
|
||||||
|
|
||||||
|
// Load session history first, then check for active session
|
||||||
|
const initializeSession = async () => {
|
||||||
|
await loadSessionHistory();
|
||||||
|
// After loading history, check if the session is still active
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
await checkForActiveSession();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeSession();
|
||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]); // Remove hasLoadedSession dependency to ensure it runs on mount
|
||||||
|
|
||||||
// Report streaming state changes
|
// Report streaming state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onStreamingChange?.(isLoading, claudeSessionId);
|
onStreamingChange?.(isLoading, claudeSessionId);
|
||||||
}, [isLoading, claudeSessionId, onStreamingChange]);
|
}, [isLoading, claudeSessionId, onStreamingChange]);
|
||||||
|
|
||||||
// Check for active Claude sessions on mount
|
|
||||||
useEffect(() => {
|
|
||||||
checkForActiveSession();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// Auto-scroll to bottom when new messages arrive
|
// Auto-scroll to bottom when new messages arrive
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (displayableMessages.length > 0) {
|
if (displayableMessages.length > 0) {
|
||||||
@@ -276,23 +296,11 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
if (activeSession) {
|
if (activeSession) {
|
||||||
// Session is still active, reconnect to its stream
|
// Session is still active, reconnect to its stream
|
||||||
console.log('[ClaudeCodeSession] Found active session, reconnecting:', session.id);
|
console.log('[ClaudeCodeSession] Found active session, reconnecting:', session.id);
|
||||||
|
// IMPORTANT: Set claudeSessionId before reconnecting
|
||||||
setClaudeSessionId(session.id);
|
setClaudeSessionId(session.id);
|
||||||
|
|
||||||
// Get any buffered output
|
// Don't add buffered messages here - they've already been loaded by loadSessionHistory
|
||||||
const bufferedOutput = await api.getClaudeSessionOutput(session.id);
|
// Just set up listeners for new messages
|
||||||
if (bufferedOutput) {
|
|
||||||
// Parse and add buffered messages
|
|
||||||
const lines = bufferedOutput.split('\n').filter((line: string) => line.trim());
|
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(line) as ClaudeStreamMessage;
|
|
||||||
setMessages(prev => [...prev, message]);
|
|
||||||
setRawJsonlOutput(prev => [...prev, line]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to parse buffered message:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up listeners for the active session
|
// Set up listeners for the active session
|
||||||
reconnectToSession(session.id);
|
reconnectToSession(session.id);
|
||||||
@@ -306,15 +314,29 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
const reconnectToSession = async (sessionId: string) => {
|
const reconnectToSession = async (sessionId: string) => {
|
||||||
console.log('[ClaudeCodeSession] Reconnecting to session:', sessionId);
|
console.log('[ClaudeCodeSession] Reconnecting to session:', sessionId);
|
||||||
|
|
||||||
|
// Prevent duplicate listeners
|
||||||
|
if (isListeningRef.current) {
|
||||||
|
console.log('[ClaudeCodeSession] Already listening to session, skipping reconnect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up previous listeners
|
// Clean up previous listeners
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
unlistenRefs.current = [];
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
|
// IMPORTANT: Set the session ID before setting up listeners
|
||||||
|
setClaudeSessionId(sessionId);
|
||||||
|
|
||||||
|
// Mark as listening
|
||||||
|
isListeningRef.current = true;
|
||||||
|
|
||||||
// Set up session-specific listeners
|
// Set up session-specific listeners
|
||||||
const outputUnlisten = await listen<string>(`claude-output:${sessionId}`, async (event) => {
|
const outputUnlisten = await listen<string>(`claude-output:${sessionId}`, async (event) => {
|
||||||
try {
|
try {
|
||||||
console.log('[ClaudeCodeSession] Received claude-output on reconnect:', event.payload);
|
console.log('[ClaudeCodeSession] Received claude-output on reconnect:', event.payload);
|
||||||
|
|
||||||
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
// Store raw JSONL
|
// Store raw JSONL
|
||||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
setRawJsonlOutput(prev => [...prev, event.payload]);
|
||||||
|
|
||||||
@@ -328,20 +350,26 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
|
|
||||||
const errorUnlisten = await listen<string>(`claude-error:${sessionId}`, (event) => {
|
const errorUnlisten = await listen<string>(`claude-error:${sessionId}`, (event) => {
|
||||||
console.error("Claude error:", event.payload);
|
console.error("Claude error:", event.payload);
|
||||||
setError(event.payload);
|
if (isMountedRef.current) {
|
||||||
|
setError(event.payload);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeUnlisten = await listen<boolean>(`claude-complete:${sessionId}`, async (event) => {
|
const completeUnlisten = await listen<boolean>(`claude-complete:${sessionId}`, async (event) => {
|
||||||
console.log('[ClaudeCodeSession] Received claude-complete on reconnect:', event.payload);
|
console.log('[ClaudeCodeSession] Received claude-complete on reconnect:', event.payload);
|
||||||
setIsLoading(false);
|
if (isMountedRef.current) {
|
||||||
hasActiveSessionRef.current = false;
|
setIsLoading(false);
|
||||||
|
hasActiveSessionRef.current = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
||||||
|
|
||||||
// Mark as loading to show the session is active
|
// Mark as loading to show the session is active
|
||||||
setIsLoading(true);
|
if (isMountedRef.current) {
|
||||||
hasActiveSessionRef.current = true;
|
setIsLoading(true);
|
||||||
|
hasActiveSessionRef.current = true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectPath = async () => {
|
const handleSelectPath = async () => {
|
||||||
@@ -364,126 +392,208 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => {
|
const handleSendPrompt = async (prompt: string, model: "sonnet" | "opus") => {
|
||||||
console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath });
|
console.log('[ClaudeCodeSession] handleSendPrompt called with:', { prompt, model, projectPath, claudeSessionId, effectiveSession });
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
setError("Please select a project directory first");
|
setError("Please select a project directory first");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If already loading, queue the prompt
|
||||||
|
if (isLoading) {
|
||||||
|
const newPrompt = {
|
||||||
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
prompt,
|
||||||
|
model
|
||||||
|
};
|
||||||
|
setQueuedPrompts(prev => [...prev, newPrompt]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
hasActiveSessionRef.current = true;
|
hasActiveSessionRef.current = true;
|
||||||
|
|
||||||
// Clean up previous listeners
|
// For resuming sessions, ensure we have the session ID
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
if (effectiveSession && !claudeSessionId) {
|
||||||
unlistenRefs.current = [];
|
setClaudeSessionId(effectiveSession.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Set up event listeners before executing
|
// Only clean up and set up new listeners if not already listening
|
||||||
console.log('[ClaudeCodeSession] Setting up event listeners...');
|
if (!isListeningRef.current) {
|
||||||
|
// Clean up previous listeners
|
||||||
// If we already have a Claude session ID, use isolated listeners
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
const eventSuffix = claudeSessionId ? `:${claudeSessionId}` : '';
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
const outputUnlisten = await listen<string>(`claude-output${eventSuffix}`, async (event) => {
|
|
||||||
try {
|
|
||||||
console.log('[ClaudeCodeSession] Received claude-output:', event.payload);
|
|
||||||
|
|
||||||
// Store raw JSONL
|
|
||||||
setRawJsonlOutput(prev => [...prev, event.payload]);
|
|
||||||
|
|
||||||
// Parse and display
|
|
||||||
const message = JSON.parse(event.payload) as ClaudeStreamMessage;
|
|
||||||
console.log('[ClaudeCodeSession] Parsed message:', message);
|
|
||||||
|
|
||||||
setMessages(prev => {
|
|
||||||
console.log('[ClaudeCodeSession] Adding message to state. Previous count:', prev.length);
|
|
||||||
return [...prev, message];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract session info from system init message
|
|
||||||
if (message.type === "system" && message.subtype === "init" && message.session_id) {
|
|
||||||
console.log('[ClaudeCodeSession] Extracting session info from init message');
|
|
||||||
// Extract project ID from the project path
|
|
||||||
const projectId = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
||||||
|
|
||||||
// Set both claudeSessionId and extractedSessionInfo
|
|
||||||
if (!claudeSessionId) {
|
|
||||||
setClaudeSessionId(message.session_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!extractedSessionInfo) {
|
|
||||||
setExtractedSessionInfo({
|
|
||||||
sessionId: message.session_id,
|
|
||||||
projectId: projectId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to parse message:", err, event.payload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorUnlisten = await listen<string>(`claude-error${eventSuffix}`, (event) => {
|
|
||||||
console.error("Claude error:", event.payload);
|
|
||||||
setError(event.payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const completeUnlisten = await listen<boolean>(`claude-complete${eventSuffix}`, async (event) => {
|
|
||||||
console.log('[ClaudeCodeSession] Received claude-complete:', event.payload);
|
|
||||||
setIsLoading(false);
|
|
||||||
hasActiveSessionRef.current = false;
|
|
||||||
|
|
||||||
// Check if we should create an auto checkpoint after completion
|
// Mark as setting up listeners
|
||||||
if (effectiveSession && event.payload) {
|
isListeningRef.current = true;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// 1️⃣ Event Listener Setup Strategy
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// Claude Code may emit a *new* session_id even when we pass --resume. If
|
||||||
|
// we listen only on the old session-scoped channel we will miss the
|
||||||
|
// stream until the user navigates away & back. To avoid this we:
|
||||||
|
// • Always start with GENERIC listeners (no suffix) so we catch the
|
||||||
|
// very first "system:init" message regardless of the session id.
|
||||||
|
// • Once that init message provides the *actual* session_id, we
|
||||||
|
// dynamically switch to session-scoped listeners and stop the
|
||||||
|
// generic ones to prevent duplicate handling.
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
console.log('[ClaudeCodeSession] Setting up generic event listeners first');
|
||||||
|
|
||||||
|
let currentSessionId: string | null = claudeSessionId || effectiveSession?.id || null;
|
||||||
|
|
||||||
|
// Helper to attach session-specific listeners **once we are sure**
|
||||||
|
const attachSessionSpecificListeners = async (sid: string) => {
|
||||||
|
console.log('[ClaudeCodeSession] Attaching session-specific listeners for', sid);
|
||||||
|
|
||||||
|
const specificOutputUnlisten = await listen<string>(`claude-output:${sid}`, (evt) => {
|
||||||
|
handleStreamMessage(evt.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
const specificErrorUnlisten = await listen<string>(`claude-error:${sid}`, (evt) => {
|
||||||
|
console.error('Claude error (scoped):', evt.payload);
|
||||||
|
setError(evt.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
const specificCompleteUnlisten = await listen<boolean>(`claude-complete:${sid}`, (evt) => {
|
||||||
|
console.log('[ClaudeCodeSession] Received claude-complete (scoped):', evt.payload);
|
||||||
|
processComplete(evt.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace existing unlisten refs with these new ones (after cleaning up)
|
||||||
|
unlistenRefs.current.forEach((u) => u());
|
||||||
|
unlistenRefs.current = [specificOutputUnlisten, specificErrorUnlisten, specificCompleteUnlisten];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generic listeners (catch-all)
|
||||||
|
const genericOutputUnlisten = await listen<string>('claude-output', async (event) => {
|
||||||
|
handleStreamMessage(event.payload);
|
||||||
|
|
||||||
|
// Attempt to extract session_id on the fly (for the very first init)
|
||||||
try {
|
try {
|
||||||
const settings = await api.getCheckpointSettings(
|
const msg = JSON.parse(event.payload) as ClaudeStreamMessage;
|
||||||
effectiveSession.id,
|
if (msg.type === 'system' && msg.subtype === 'init' && msg.session_id) {
|
||||||
effectiveSession.project_id,
|
if (!currentSessionId || currentSessionId !== msg.session_id) {
|
||||||
projectPath
|
console.log('[ClaudeCodeSession] Detected new session_id from generic listener:', msg.session_id);
|
||||||
);
|
currentSessionId = msg.session_id;
|
||||||
|
setClaudeSessionId(msg.session_id);
|
||||||
|
|
||||||
|
// If we haven't extracted session info before, do it now
|
||||||
|
if (!extractedSessionInfo) {
|
||||||
|
const projectId = projectPath.replace(/[^a-zA-Z0-9]/g, '-');
|
||||||
|
setExtractedSessionInfo({ sessionId: msg.session_id, projectId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to session-specific listeners
|
||||||
|
await attachSessionSpecificListeners(msg.session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore parse errors */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to process any JSONL stream message string
|
||||||
|
function handleStreamMessage(payload: string) {
|
||||||
|
try {
|
||||||
|
// Don't process if component unmounted
|
||||||
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
if (settings.auto_checkpoint_enabled) {
|
// Store raw JSONL
|
||||||
await api.checkAutoCheckpoint(
|
setRawJsonlOutput((prev) => [...prev, payload]);
|
||||||
|
|
||||||
|
const message = JSON.parse(payload) as ClaudeStreamMessage;
|
||||||
|
setMessages((prev) => [...prev, message]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to parse message:', err, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to handle completion events (both generic and scoped)
|
||||||
|
const processComplete = async (success: boolean) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
hasActiveSessionRef.current = false;
|
||||||
|
isListeningRef.current = false; // Reset listening state
|
||||||
|
|
||||||
|
if (effectiveSession && success) {
|
||||||
|
try {
|
||||||
|
const settings = await api.getCheckpointSettings(
|
||||||
effectiveSession.id,
|
effectiveSession.id,
|
||||||
effectiveSession.project_id,
|
effectiveSession.project_id,
|
||||||
projectPath,
|
projectPath
|
||||||
prompt
|
|
||||||
);
|
);
|
||||||
// Reload timeline to show new checkpoint
|
|
||||||
setTimelineVersion((v) => v + 1);
|
if (settings.auto_checkpoint_enabled) {
|
||||||
|
await api.checkAutoCheckpoint(
|
||||||
|
effectiveSession.id,
|
||||||
|
effectiveSession.project_id,
|
||||||
|
projectPath,
|
||||||
|
prompt
|
||||||
|
);
|
||||||
|
// Reload timeline to show new checkpoint
|
||||||
|
setTimelineVersion((v) => v + 1);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to check auto checkpoint:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to check auto checkpoint:', err);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
unlistenRefs.current = [outputUnlisten, errorUnlisten, completeUnlisten];
|
// Process queued prompts after completion
|
||||||
|
if (queuedPromptsRef.current.length > 0) {
|
||||||
// Add the user message immediately to the UI (after setting up listeners)
|
const [nextPrompt, ...remainingPrompts] = queuedPromptsRef.current;
|
||||||
const userMessage: ClaudeStreamMessage = {
|
setQueuedPrompts(remainingPrompts);
|
||||||
type: "user",
|
|
||||||
message: {
|
// Small delay to ensure UI updates
|
||||||
content: [
|
setTimeout(() => {
|
||||||
{
|
handleSendPrompt(nextPrompt.prompt, nextPrompt.model);
|
||||||
type: "text",
|
}, 100);
|
||||||
text: prompt
|
}
|
||||||
}
|
};
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, userMessage]);
|
|
||||||
|
|
||||||
// Execute the appropriate command
|
const genericErrorUnlisten = await listen<string>('claude-error', (evt) => {
|
||||||
if (effectiveSession && !isFirstPrompt) {
|
console.error('Claude error:', evt.payload);
|
||||||
console.log('[ClaudeCodeSession] Resuming session:', effectiveSession.id);
|
setError(evt.payload);
|
||||||
await api.resumeClaudeCode(projectPath, effectiveSession.id, prompt, model);
|
});
|
||||||
} else {
|
|
||||||
console.log('[ClaudeCodeSession] Starting new session');
|
const genericCompleteUnlisten = await listen<boolean>('claude-complete', (evt) => {
|
||||||
setIsFirstPrompt(false);
|
console.log('[ClaudeCodeSession] Received claude-complete (generic):', evt.payload);
|
||||||
await api.executeClaudeCode(projectPath, prompt, model);
|
processComplete(evt.payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the generic unlisteners for now; they may be replaced later.
|
||||||
|
unlistenRefs.current = [genericOutputUnlisten, genericErrorUnlisten, genericCompleteUnlisten];
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// 2️⃣ Auto-checkpoint logic moved after listener setup (unchanged)
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Add the user message immediately to the UI (after setting up listeners)
|
||||||
|
const userMessage: ClaudeStreamMessage = {
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: prompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
|
||||||
|
// Execute the appropriate command
|
||||||
|
if (effectiveSession && !isFirstPrompt) {
|
||||||
|
console.log('[ClaudeCodeSession] Resuming session:', effectiveSession.id);
|
||||||
|
await api.resumeClaudeCode(projectPath, effectiveSession.id, prompt, model);
|
||||||
|
} else {
|
||||||
|
console.log('[ClaudeCodeSession] Starting new session');
|
||||||
|
setIsFirstPrompt(false);
|
||||||
|
await api.executeClaudeCode(projectPath, prompt, model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to send prompt:", err);
|
console.error("Failed to send prompt:", err);
|
||||||
@@ -579,31 +689,32 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelExecution = async () => {
|
const handleCancelExecution = async () => {
|
||||||
if (!isLoading || isCancelling) return;
|
if (!claudeSessionId || !isLoading) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsCancelling(true);
|
await api.cancelClaudeExecution(claudeSessionId);
|
||||||
|
|
||||||
// Cancel the Claude execution with session ID if available
|
|
||||||
await api.cancelClaudeExecution(claudeSessionId || undefined);
|
|
||||||
|
|
||||||
// Clean up listeners
|
// Clean up listeners
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
unlistenRefs.current = [];
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
// Add a system message indicating cancellation
|
|
||||||
const cancelMessage: ClaudeStreamMessage = {
|
|
||||||
type: "system",
|
|
||||||
subtype: "cancelled",
|
|
||||||
result: "Execution cancelled by user",
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, cancelMessage]);
|
|
||||||
|
|
||||||
// Reset states
|
// Reset states
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
hasActiveSessionRef.current = false;
|
hasActiveSessionRef.current = false;
|
||||||
|
isListeningRef.current = false;
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Clear queued prompts
|
||||||
|
setQueuedPrompts([]);
|
||||||
|
|
||||||
|
// Add a message indicating the session was cancelled
|
||||||
|
const cancelMessage: ClaudeStreamMessage = {
|
||||||
|
type: "system",
|
||||||
|
subtype: "info",
|
||||||
|
result: "Session cancelled by user",
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, cancelMessage]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to cancel execution:", err);
|
console.error("Failed to cancel execution:", err);
|
||||||
|
|
||||||
@@ -624,9 +735,8 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
// Reset states to allow user to continue
|
// Reset states to allow user to continue
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
hasActiveSessionRef.current = false;
|
hasActiveSessionRef.current = false;
|
||||||
|
isListeningRef.current = false;
|
||||||
setError(null);
|
setError(null);
|
||||||
} finally {
|
|
||||||
setIsCancelling(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -707,10 +817,19 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clean up listeners on component unmount
|
// Cleanup event listeners and track mount state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
console.log('[ClaudeCodeSession] Component unmounting, cleaning up listeners');
|
||||||
|
isMountedRef.current = false;
|
||||||
|
isListeningRef.current = false;
|
||||||
|
|
||||||
|
// Clean up listeners
|
||||||
unlistenRefs.current.forEach(unlisten => unlisten());
|
unlistenRefs.current.forEach(unlisten => unlisten());
|
||||||
|
unlistenRefs.current = [];
|
||||||
|
|
||||||
// Clear checkpoint manager when session ends
|
// Clear checkpoint manager when session ends
|
||||||
if (effectiveSession) {
|
if (effectiveSession) {
|
||||||
api.clearCheckpointManager(effectiveSession.id).catch(err => {
|
api.clearCheckpointManager(effectiveSession.id).catch(err => {
|
||||||
@@ -718,20 +837,21 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [effectiveSession]);
|
||||||
|
|
||||||
const messagesList = (
|
const messagesList = (
|
||||||
<div
|
<div
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className="flex-1 overflow-y-auto relative"
|
className="flex-1 overflow-y-auto relative pb-40"
|
||||||
style={{
|
style={{
|
||||||
contain: 'strict',
|
contain: 'strict',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-5xl mx-auto px-4 py-4"
|
className="relative w-full max-w-5xl mx-auto px-4 pt-8 pb-4"
|
||||||
style={{
|
style={{
|
||||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
height: `${Math.max(rowVirtualizer.getTotalSize(), 100)}px`,
|
||||||
|
minHeight: '100px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -762,28 +882,27 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading and Error indicators positioned relative to the scroll container */}
|
{/* Loading indicator under the latest message */}
|
||||||
<div className="sticky bottom-0 w-full flex flex-col items-center pb-40">
|
{isLoading && (
|
||||||
{isLoading && (
|
<motion.div
|
||||||
<motion.div
|
initial={{ opacity: 0 }}
|
||||||
initial={{ opacity: 0 }}
|
animate={{ opacity: 1 }}
|
||||||
animate={{ opacity: 1 }}
|
className="flex items-center justify-center py-4 mb-40"
|
||||||
className="flex items-center justify-center py-4 mt-4"
|
>
|
||||||
>
|
<div className="rotating-symbol text-primary text-2xl" />
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
</motion.div>
|
||||||
</motion.div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* Error indicator */}
|
||||||
{error && (
|
{error && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive mt-4 w-full max-w-5xl mx-auto"
|
className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive mb-40 w-full max-w-5xl mx-auto"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -859,7 +978,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
disabled={isLoading}
|
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -965,8 +1083,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
onOpenChange={setCopyPopoverOpen}
|
onOpenChange={setCopyPopoverOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TokenCounter tokens={totalTokens} />
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -1002,23 +1118,141 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
<div className="h-full flex flex-col max-w-5xl mx-auto">
|
<div className="h-full flex flex-col max-w-5xl mx-auto">
|
||||||
{projectPathInput}
|
{projectPathInput}
|
||||||
{messagesList}
|
{messagesList}
|
||||||
</div>
|
|
||||||
)}
|
{isLoading && messages.length === 0 && (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
{isLoading && messages.length === 0 && (
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="rotating-symbol text-primary text-2xl" />
|
||||||
<div className="flex items-center gap-3">
|
<span className="text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
{session ? "Loading session history..." : "Initializing Claude Code..."}
|
||||||
<span className="text-sm text-muted-foreground">
|
</span>
|
||||||
{session ? "Loading session history..." : "Initializing Claude Code..."}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Prompt Input - Always visible */}
|
{/* Floating Prompt Input - Always visible */}
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
{/* Queued Prompts Display */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{queuedPrompts.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
className="fixed bottom-24 left-1/2 -translate-x-1/2 z-30 w-full max-w-3xl px-4"
|
||||||
|
>
|
||||||
|
<div className="bg-background/95 backdrop-blur-md border rounded-lg shadow-lg p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-1">
|
||||||
|
Queued Prompts ({queuedPrompts.length})
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setQueuedPromptsCollapsed(prev => !prev)}>
|
||||||
|
{queuedPromptsCollapsed ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={queuedPrompt.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 20 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="flex items-start gap-2 bg-muted/50 rounded-md p-2"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">#{index + 1}</span>
|
||||||
|
<span className="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">
|
||||||
|
{queuedPrompt.model === "opus" ? "Opus" : "Sonnet"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm line-clamp-2 break-words">{queuedPrompt.prompt}</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 flex-shrink-0"
|
||||||
|
onClick={() => setQueuedPrompts(prev => prev.filter(p => p.id !== queuedPrompt.id))}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Navigation Arrows - positioned above prompt bar with spacing */}
|
||||||
|
{displayableMessages.length > 5 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
className="fixed bottom-32 right-6 z-50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center bg-background/95 backdrop-blur-md border rounded-full shadow-lg overflow-hidden">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// Use virtualizer to scroll to the first item
|
||||||
|
if (displayableMessages.length > 0) {
|
||||||
|
// Scroll to top of the container
|
||||||
|
parentRef.current?.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
|
||||||
|
// After smooth scroll completes, trigger a small scroll to ensure rendering
|
||||||
|
setTimeout(() => {
|
||||||
|
if (parentRef.current) {
|
||||||
|
// Scroll down 1px then back to 0 to trigger virtualizer update
|
||||||
|
parentRef.current.scrollTop = 1;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (parentRef.current) {
|
||||||
|
parentRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500); // Wait for smooth scroll to complete
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-3 py-2 hover:bg-accent rounded-none"
|
||||||
|
title="Scroll to top"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div className="w-px h-4 bg-border" />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// Use virtualizer to scroll to the last item
|
||||||
|
if (displayableMessages.length > 0) {
|
||||||
|
// Scroll to bottom of the container
|
||||||
|
const scrollElement = parentRef.current;
|
||||||
|
if (scrollElement) {
|
||||||
|
scrollElement.scrollTo({
|
||||||
|
top: scrollElement.scrollHeight,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-3 py-2 hover:bg-accent rounded-none"
|
||||||
|
title="Scroll to bottom"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FloatingPromptInput
|
<FloatingPromptInput
|
||||||
ref={floatingPromptRef}
|
ref={floatingPromptRef}
|
||||||
onSend={handleSendPrompt}
|
onSend={handleSendPrompt}
|
||||||
@@ -1027,6 +1261,28 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
|||||||
disabled={!projectPath}
|
disabled={!projectPath}
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Token Counter - positioned under the Send button */}
|
||||||
|
{totalTokens > 0 && (
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-30 pointer-events-none">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<div className="flex justify-end px-4 pb-2">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
|
className="bg-background/95 backdrop-blur-md border rounded-full px-3 py-1 shadow-lg pointer-events-auto"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs">
|
||||||
|
<Hash className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="font-mono">{totalTokens.toLocaleString()}</span>
|
||||||
|
<span className="text-muted-foreground">tokens</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
||||||
{/* Timeline */}
|
{/* Timeline */}
|
||||||
|
@@ -335,13 +335,13 @@ const FloatingPromptInputInner = (
|
|||||||
}, [isExpanded]);
|
}, [isExpanded]);
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (prompt.trim() && !isLoading && !disabled) {
|
if (prompt.trim() && !disabled) {
|
||||||
let finalPrompt = prompt.trim();
|
let finalPrompt = prompt.trim();
|
||||||
|
|
||||||
// Append thinking phrase if not auto mode
|
// Append thinking phrase if not auto mode
|
||||||
const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode);
|
const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode);
|
||||||
if (thinkingMode && thinkingMode.phrase) {
|
if (thinkingMode && thinkingMode.phrase) {
|
||||||
finalPrompt = `${finalPrompt}\n\n${thinkingMode.phrase}.`;
|
finalPrompt = `${finalPrompt}.\n\n${thinkingMode.phrase}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSend(finalPrompt, selectedModel);
|
onSend(finalPrompt, selectedModel);
|
||||||
@@ -516,7 +516,7 @@ const FloatingPromptInputInner = (
|
|||||||
onChange={handleTextChange}
|
onChange={handleTextChange}
|
||||||
placeholder="Type your prompt here..."
|
placeholder="Type your prompt here..."
|
||||||
className="min-h-[200px] resize-none"
|
className="min-h-[200px] resize-none"
|
||||||
disabled={isLoading || disabled}
|
disabled={disabled}
|
||||||
onDragEnter={handleDrag}
|
onDragEnter={handleDrag}
|
||||||
onDragLeave={handleDrag}
|
onDragLeave={handleDrag}
|
||||||
onDragOver={handleDrag}
|
onDragOver={handleDrag}
|
||||||
@@ -603,7 +603,7 @@ const FloatingPromptInputInner = (
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!prompt.trim() || isLoading || disabled}
|
disabled={!prompt.trim() || disabled}
|
||||||
size="default"
|
size="default"
|
||||||
className="min-w-[60px]"
|
className="min-w-[60px]"
|
||||||
>
|
>
|
||||||
@@ -649,7 +649,7 @@ const FloatingPromptInputInner = (
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="default"
|
size="default"
|
||||||
disabled={isLoading || disabled}
|
disabled={disabled}
|
||||||
className="gap-2 min-w-[180px] justify-start"
|
className="gap-2 min-w-[180px] justify-start"
|
||||||
>
|
>
|
||||||
{selectedModelData.icon}
|
{selectedModelData.icon}
|
||||||
@@ -698,7 +698,7 @@ const FloatingPromptInputInner = (
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="default"
|
size="default"
|
||||||
disabled={isLoading || disabled}
|
disabled={disabled}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Brain className="h-4 w-4" />
|
<Brain className="h-4 w-4" />
|
||||||
@@ -757,7 +757,7 @@ const FloatingPromptInputInner = (
|
|||||||
onChange={handleTextChange}
|
onChange={handleTextChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={dragActive ? "Drop images here..." : "Ask Claude anything..."}
|
placeholder={dragActive ? "Drop images here..." : "Ask Claude anything..."}
|
||||||
disabled={isLoading || disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-[44px] max-h-[120px] resize-none pr-10",
|
"min-h-[44px] max-h-[120px] resize-none pr-10",
|
||||||
dragActive && "border-primary"
|
dragActive && "border-primary"
|
||||||
@@ -769,7 +769,7 @@ const FloatingPromptInputInner = (
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setIsExpanded(true)}
|
onClick={() => setIsExpanded(true)}
|
||||||
disabled={isLoading || disabled}
|
disabled={disabled}
|
||||||
className="absolute right-1 bottom-1 h-8 w-8"
|
className="absolute right-1 bottom-1 h-8 w-8"
|
||||||
>
|
>
|
||||||
<Maximize2 className="h-4 w-4" />
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
Reference in New Issue
Block a user