diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index a4cffd5..bc1ab67 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -1,11 +1,12 @@ 'use client'; -import { useEffect, useState, use } from 'react'; +import { useEffect, useState, useRef, useCallback, use } from 'react'; import Link from 'next/link'; import type { Message, MessagesResponse, ChatSession } from '@/types'; import { ChatView } from '@/components/chat/ChatView'; import { HugeiconsIcon } from "@hugeicons/react"; -import { Loading02Icon } from "@hugeicons/core-free-icons"; +import { Loading02Icon, PencilEdit01Icon } from "@hugeicons/core-free-icons"; +import { Input } from '@/components/ui/input'; import { usePanel } from '@/hooks/usePanel'; interface ChatSessionPageProps { @@ -22,8 +23,54 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { const [sessionModel, setSessionModel] = useState(''); const [sessionMode, setSessionMode] = useState(''); const [projectName, setProjectName] = useState(''); + const [isEditingTitle, setIsEditingTitle] = useState(false); + const [editTitle, setEditTitle] = useState(''); + const titleInputRef = useRef(null); const { setWorkingDirectory, setSessionId, setSessionTitle: setPanelSessionTitle, setPanelOpen } = usePanel(); + const handleStartEditTitle = useCallback(() => { + setEditTitle(sessionTitle || 'New Conversation'); + setIsEditingTitle(true); + }, [sessionTitle]); + + const handleSaveTitle = useCallback(async () => { + const trimmed = editTitle.trim(); + if (!trimmed) { + setIsEditingTitle(false); + return; + } + try { + const res = await fetch(`/api/chat/sessions/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: trimmed }), + }); + if (res.ok) { + setSessionTitle(trimmed); + setPanelSessionTitle(trimmed); + window.dispatchEvent(new CustomEvent('session-updated', { detail: { id, title: trimmed } })); + } + } catch { + // silently fail + } + setIsEditingTitle(false); + }, [editTitle, id, setPanelSessionTitle]); + + const handleTitleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSaveTitle(); + } else if (e.key === 'Escape') { + setIsEditingTitle(false); + } + }, [handleSaveTitle]); + + useEffect(() => { + if (isEditingTitle && titleInputRef.current) { + titleInputRef.current.focus(); + titleInputRef.current.select(); + } + }, [isEditingTitle]); + // Load session info and set working directory useEffect(() => { async function loadSession() { @@ -115,7 +162,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { {/* Chat title bar */} {sessionTitle && (
{projectName && ( @@ -124,9 +171,33 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { / )} -

- {sessionTitle} -

+ {isEditingTitle ? ( +
+ setEditTitle(e.target.value)} + onKeyDown={handleTitleKeyDown} + onBlur={handleSaveTitle} + className="h-7 text-sm max-w-md text-center" + /> +
+ ) : ( +
+

+ {sessionTitle} +

+ +
+ )}
)} diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index f49733e..6dc671b 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -35,12 +35,6 @@ export default function NewChatPage() { const [streamingToolOutput, setStreamingToolOutput] = useState(''); const abortControllerRef = useRef(null); - const handleWorkingDirectoryChange = useCallback((dir: string) => { - setWorkingDir(dir); - setWorkingDirectory(dir); - setPanelOpen(true); - }, [setWorkingDirectory, setPanelOpen]); - const stopStreaming = useCallback(() => { abortControllerRef.current?.abort(); abortControllerRef.current = null; @@ -364,7 +358,6 @@ export default function NewChatPage() { modelName={currentModel} onModelChange={setCurrentModel} workingDirectory={workingDir} - onWorkingDirectoryChange={handleWorkingDirectoryChange} mode={mode} onModeChange={setMode} /> diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index c9a942d..9993405 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -59,19 +59,6 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal }, [sessionId]); const abortControllerRef = useRef(null); - const handleWorkingDirectoryChange = useCallback((dir: string) => { - setWorkingDirectory(dir); - setPanelOpen(true); - // Persist to database - if (sessionId) { - fetch(`/api/chat/sessions/${sessionId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ working_directory: dir }), - }).catch(() => { /* silent */ }); - } - }, [sessionId, setWorkingDirectory, setPanelOpen]); - // Ref to keep accumulated streaming content in sync regardless of React batching const accumulatedRef = useRef(''); // Ref for sendMessage to allow self-referencing in timeout auto-retry without circular deps @@ -484,7 +471,6 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal modelName={currentModel} onModelChange={setCurrentModel} workingDirectory={workingDirectory} - onWorkingDirectoryChange={handleWorkingDirectoryChange} mode={mode} onModeChange={handleModeChange} /> diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index 78cb76e..1e95b72 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -4,14 +4,13 @@ import { useRef, useState, useCallback, useEffect, type KeyboardEvent, type Form import { HugeiconsIcon } from "@hugeicons/react"; import { AtIcon, - FolderOpenIcon, Wrench01Icon, ClipboardIcon, HelpCircleIcon, ArrowDown01Icon, ArrowUp02Icon, CommandLineIcon, - Attachment01Icon, + PlusSignIcon, Cancel01Icon, Delete02Icon, Coins01Icon, @@ -23,7 +22,6 @@ import { GlobalIcon, } from "@hugeicons/core-free-icons"; import { cn } from '@/lib/utils'; -import { FolderPicker } from './FolderPicker'; import { PromptInput, PromptInputTextarea, @@ -61,7 +59,6 @@ interface MessageInputProps { modelName?: string; onModelChange?: (model: string) => void; workingDirectory?: string; - onWorkingDirectoryChange?: (dir: string) => void; mode?: string; onModeChange?: (mode: string) => void; } @@ -245,7 +242,7 @@ function AttachFileButton() { onClick={() => attachments.openFileDialog()} tooltip="Attach files" > - + ); } @@ -343,7 +340,6 @@ export function MessageInput({ modelName, onModelChange, workingDirectory, - onWorkingDirectoryChange, mode = 'code', onModeChange, }: MessageInputProps) { @@ -358,7 +354,6 @@ export function MessageInput({ const [popoverFilter, setPopoverFilter] = useState(''); const [selectedIndex, setSelectedIndex] = useState(0); const [triggerPos, setTriggerPos] = useState(null); - const [folderPickerOpen, setFolderPickerOpen] = useState(false); const [modeMenuOpen, setModeMenuOpen] = useState(false); const [modelMenuOpen, setModelMenuOpen] = useState(false); const [inputValue, setInputValue] = useState(''); @@ -742,10 +737,6 @@ export function MessageInput({ const currentModelOption = MODEL_OPTIONS.find((m) => m.value === currentModelValue) || MODEL_OPTIONS[0]; const currentMode = MODE_OPTIONS.find((m) => m.value === mode) || MODE_OPTIONS[0]; - const folderShortName = workingDirectory - ? workingDirectory.split('/').filter(Boolean).pop() || workingDirectory - : ''; - // Map isStreaming to ChatStatus for PromptInputSubmit const chatStatus: ChatStatus = isStreaming ? 'streaming' : 'ready'; @@ -921,23 +912,11 @@ export function MessageInput({ {/* Attach file button */} - {/* Folder picker button */} - setFolderPickerOpen(true)} - tooltip={workingDirectory || 'Select project folder'} - > - - - {folderShortName || 'Folder'} - - - {/* Mode selector */}
setModeMenuOpen((prev) => !prev)} > - {currentMode.label} @@ -974,9 +953,7 @@ export function MessageInput({
)} - -
{/* Model selector */}
{modelMenuOpen && ( -
+
{MODEL_OPTIONS.map((opt) => { const isActive = opt.value === currentModelValue; @@ -1011,29 +988,20 @@ export function MessageInput({
)}
+ - -
+
- {/* FolderPicker dialog */} - { - onWorkingDirectoryChange?.(dir); - }} - initialPath={workingDirectory || undefined} - /> ); } diff --git a/src/components/layout/ChatListPanel.tsx b/src/components/layout/ChatListPanel.tsx index bc356a9..4bc0b1d 100644 --- a/src/components/layout/ChatListPanel.tsx +++ b/src/components/layout/ChatListPanel.tsx @@ -126,6 +126,34 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { () => loadCollapsedProjects() ); const [hoveredFolder, setHoveredFolder] = useState(null); + const [creatingChat, setCreatingChat] = useState(false); + + const handleNewChat = useCallback(async () => { + const lastDir = localStorage.getItem("codepilot:last-working-directory"); + if (!lastDir) { + router.push("/chat"); + return; + } + setCreatingChat(true); + try { + const res = await fetch("/api/chat/sessions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ working_directory: lastDir }), + }); + if (!res.ok) { + router.push("/chat"); + return; + } + const data = await res.json(); + router.push(`/chat/${data.session.id}`); + window.dispatchEvent(new CustomEvent("session-created")); + } catch { + router.push("/chat"); + } finally { + setCreatingChat(false); + } + }, [router]); const toggleProject = useCallback((wd: string) => { setCollapsedProjects((prev) => { @@ -260,27 +288,39 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { Threads -
- - - - - Open project folder - - -
+ + + + {/* New Chat + New Project */} +
+ + + + + + Open project folder +
{/* Search */} -
+
- - - - - - Import conversations from Claude Code CLI - - +
{/* Session list grouped by project */} @@ -349,19 +382,23 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { /> {group.displayName} {/* New chat in project button (on hover) */} - {isFolderHovered && group.workingDirectory !== "" && ( + {group.workingDirectory !== "" && ( - - New Chat - - - {/* Divider */} -
- {/* Nav icons */}