feat: inline title editing, simplify right panel, polish input toolbar
- Add inline title editing in chat header (hover to reveal pencil icon, click to edit, Enter/Blur to save, Escape to cancel) - Simplify right panel: remove Name section, rename header to "Files" - Align top headers across all three columns (left/center/right) - Remove folder picker from message input (project already set via sidebar) - Change attachment icon from paperclip to plus sign - Remove icon from mode selector, move model selector next to mode selector - Fix FileTree search icon alignment and left padding consistency - Add background to chat list delete button to prevent text overlap Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
5a57098ffd
commit
ae8a905581
8 files changed
+187
-269
No files matched your search
@@ -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<string>('');
|
||||
const [sessionMode, setSessionMode] = useState<string>('');
|
||||
const [projectName, setProjectName] = useState<string>('');
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editTitle, setEditTitle] = useState('');
|
||||
const titleInputRef = useRef<HTMLInputElement>(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 && (
|
||||
<div
|
||||
className="flex items-center justify-center px-4 py-2 gap-1"
|
||||
className="flex items-center justify-center px-4 pb-2 gap-1"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
>
|
||||
{projectName && (
|
||||
@@ -124,9 +171,33 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
|
||||
<span className="text-xs text-muted-foreground shrink-0">/</span>
|
||||
</>
|
||||
)}
|
||||
<h2 className="text-sm font-medium text-foreground/80 truncate max-w-md">
|
||||
{sessionTitle}
|
||||
</h2>
|
||||
{isEditingTitle ? (
|
||||
<div style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
|
||||
<Input
|
||||
ref={titleInputRef}
|
||||
value={editTitle}
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
onBlur={handleSaveTitle}
|
||||
className="h-7 text-sm max-w-md text-center"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex items-center gap-1 group cursor-default max-w-md"
|
||||
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
|
||||
>
|
||||
<h2 className="text-sm font-medium text-foreground/80 truncate">
|
||||
{sessionTitle}
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleStartEditTitle}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0 p-0.5 rounded hover:bg-muted"
|
||||
>
|
||||
<HugeiconsIcon icon={PencilEdit01Icon} className="h-3 w-3 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ChatView key={id} sessionId={id} initialMessages={messages} initialHasMore={hasMore} modelName={sessionModel} initialMode={sessionMode} />
|
||||
|
||||
@@ -35,12 +35,6 @@ export default function NewChatPage() {
|
||||
const [streamingToolOutput, setStreamingToolOutput] = useState('');
|
||||
const abortControllerRef = useRef<AbortController | null>(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}
|
||||
/>
|
||||
|
||||
@@ -59,19 +59,6 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal
|
||||
}, [sessionId]);
|
||||
const abortControllerRef = useRef<AbortController | null>(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}
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<HugeiconsIcon icon={Attachment01Icon} className="h-3.5 w-3.5" />
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
</PromptInputButton>
|
||||
);
|
||||
}
|
||||
@@ -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<number | null>(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 */}
|
||||
<AttachFileButton />
|
||||
|
||||
{/* Folder picker button */}
|
||||
<PromptInputButton
|
||||
onClick={() => setFolderPickerOpen(true)}
|
||||
tooltip={workingDirectory || 'Select project folder'}
|
||||
>
|
||||
<HugeiconsIcon icon={FolderOpenIcon} className="h-3.5 w-3.5" />
|
||||
<span className="max-w-[120px] truncate text-xs">
|
||||
{folderShortName || 'Folder'}
|
||||
</span>
|
||||
</PromptInputButton>
|
||||
|
||||
{/* Mode selector */}
|
||||
<div className="relative" ref={modeMenuRef}>
|
||||
<PromptInputButton
|
||||
onClick={() => setModeMenuOpen((prev) => !prev)}
|
||||
>
|
||||
<HugeiconsIcon icon={currentMode.icon} className="h-3.5 w-3.5" />
|
||||
<span className="text-xs">{currentMode.label}</span>
|
||||
<HugeiconsIcon icon={ArrowDown01Icon} className={cn("h-2.5 w-2.5 transition-transform duration-200", modeMenuOpen && "rotate-180")} />
|
||||
</PromptInputButton>
|
||||
@@ -974,9 +953,7 @@ export function MessageInput({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PromptInputTools>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Model selector */}
|
||||
<div className="relative" ref={modelMenuRef}>
|
||||
<PromptInputButton
|
||||
@@ -987,7 +964,7 @@ export function MessageInput({
|
||||
</PromptInputButton>
|
||||
|
||||
{modelMenuOpen && (
|
||||
<div className="absolute bottom-full right-0 mb-1.5 w-48 rounded-lg border bg-popover shadow-lg overflow-hidden z-50">
|
||||
<div className="absolute bottom-full left-0 mb-1.5 w-48 rounded-lg border bg-popover shadow-lg overflow-hidden z-50">
|
||||
<div className="py-1">
|
||||
{MODEL_OPTIONS.map((opt) => {
|
||||
const isActive = opt.value === currentModelValue;
|
||||
@@ -1011,29 +988,20 @@ export function MessageInput({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PromptInputTools>
|
||||
|
||||
<FileAwareSubmitButton
|
||||
status={chatStatus}
|
||||
onStop={onStop}
|
||||
disabled={disabled}
|
||||
inputValue={inputValue}
|
||||
hasBadge={!!badge}
|
||||
/>
|
||||
</div>
|
||||
<FileAwareSubmitButton
|
||||
status={chatStatus}
|
||||
onStop={onStop}
|
||||
disabled={disabled}
|
||||
inputValue={inputValue}
|
||||
hasBadge={!!badge}
|
||||
/>
|
||||
</PromptInputFooter>
|
||||
</PromptInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FolderPicker dialog */}
|
||||
<FolderPicker
|
||||
open={folderPickerOpen}
|
||||
onOpenChange={setFolderPickerOpen}
|
||||
onSelect={(dir) => {
|
||||
onWorkingDirectoryChange?.(dir);
|
||||
}}
|
||||
initialPath={workingDirectory || undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -126,6 +126,34 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
() => loadCollapsedProjects()
|
||||
);
|
||||
const [hoveredFolder, setHoveredFolder] = useState<string | null>(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) {
|
||||
<span className="text-[13px] font-semibold tracking-tight text-sidebar-foreground">
|
||||
Threads
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setFolderPickerOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={FolderOpenIcon} className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Open project folder</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Open project folder</TooltipContent>
|
||||
</Tooltip>
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
|
||||
{/* New Chat + New Project */}
|
||||
<div className="flex items-center gap-2 px-3 pb-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 justify-center gap-1.5 h-8 text-xs"
|
||||
disabled={creatingChat}
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
|
||||
New Chat
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => setFolderPickerOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={FolderOpenIcon} className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Open project folder</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Open project folder</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="px-3 py-2">
|
||||
<div className="px-3 pb-2">
|
||||
<div className="relative">
|
||||
<HugeiconsIcon
|
||||
icon={Search01Icon}
|
||||
@@ -297,22 +337,15 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
|
||||
{/* Import CLI Session */}
|
||||
<div className="px-3 pb-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2 h-7 text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={FileImportIcon} className="h-3 w-3" />
|
||||
Import CLI Session
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
Import conversations from Claude Code CLI
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2 h-7 text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={FileImportIcon} className="h-3 w-3" />
|
||||
Import CLI Session
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Session list grouped by project */}
|
||||
@@ -349,19 +382,23 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
/>
|
||||
<HugeiconsIcon
|
||||
icon={Folder01Icon}
|
||||
className="h-3.5 w-3.5 shrink-0 text-blue-500"
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<span className="flex-1 truncate text-[12px] font-medium text-sidebar-foreground">
|
||||
{group.displayName}
|
||||
</span>
|
||||
{/* New chat in project button (on hover) */}
|
||||
{isFolderHovered && group.workingDirectory !== "" && (
|
||||
{group.workingDirectory !== "" && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="h-5 w-5 shrink-0 text-muted-foreground hover:text-foreground"
|
||||
className={cn(
|
||||
"h-5 w-5 shrink-0 text-muted-foreground hover:text-foreground transition-opacity",
|
||||
isFolderHovered ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
tabIndex={isFolderHovered ? 0 : -1}
|
||||
onClick={(e) =>
|
||||
handleCreateSessionInProject(
|
||||
e,
|
||||
@@ -459,7 +496,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="absolute right-1 top-1 text-muted-foreground/60 hover:text-destructive"
|
||||
className="absolute right-1 top-1 bg-sidebar text-muted-foreground/60 hover:text-destructive"
|
||||
onClick={(e) =>
|
||||
handleDeleteSession(e, session.id)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useState, useSyncExternalStore } from "react";
|
||||
import { useCallback, useSyncExternalStore } from "react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import {
|
||||
Message02Icon,
|
||||
PlusSignIcon,
|
||||
GridIcon,
|
||||
Settings02Icon,
|
||||
Moon02Icon,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SessionResponse } from "@/types";
|
||||
|
||||
|
||||
interface NavRailProps {
|
||||
chatListOpen: boolean;
|
||||
@@ -42,58 +41,9 @@ export function NavRail({ onToggleChatList, hasUpdate, skipPermissionsActive }:
|
||||
const emptySubscribe = useCallback(() => () => {}, []);
|
||||
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
|
||||
const isChatRoute = pathname === "/chat" || pathname.startsWith("/chat/");
|
||||
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: SessionResponse = await res.json();
|
||||
router.push(`/chat/${data.session.id}`);
|
||||
window.dispatchEvent(new CustomEvent("session-created"));
|
||||
} catch {
|
||||
router.push("/chat");
|
||||
} finally {
|
||||
setCreatingChat(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<aside className="flex w-14 shrink-0 flex-col items-center bg-sidebar pb-3 pt-10">
|
||||
{/* New Chat */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mb-2 h-9 w-9"
|
||||
disabled={creatingChat}
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
<HugeiconsIcon icon={PlusSignIcon} className="h-4 w-4" />
|
||||
<span className="sr-only">New Chat</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">New Chat</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mx-auto mb-2 h-px w-6 bg-border/50" />
|
||||
|
||||
{/* Nav icons */}
|
||||
<nav className="flex flex-1 flex-col items-center gap-1">
|
||||
{navItems.map((item) => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { StructureFolderIcon, PanelRightCloseIcon, PencilEdit01Icon, Tick01Icon } from "@hugeicons/core-free-icons";
|
||||
import { StructureFolderIcon, PanelRightCloseIcon } from "@hugeicons/core-free-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -18,53 +17,7 @@ interface RightPanelProps {
|
||||
}
|
||||
|
||||
export function RightPanel({ width }: RightPanelProps) {
|
||||
const { panelOpen, setPanelOpen, workingDirectory, sessionId, sessionTitle, setSessionTitle, previewFile, setPreviewFile } = usePanel();
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [editName, setEditName] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const displayTitle = sessionTitle || (sessionId ? `Session ${sessionId.slice(0, 8)}` : "Untitled Chat");
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditingName && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditingName]);
|
||||
|
||||
const handleStartEdit = useCallback(() => {
|
||||
setEditName(displayTitle);
|
||||
setIsEditingName(true);
|
||||
}, [displayTitle]);
|
||||
|
||||
const handleSaveName = useCallback(async () => {
|
||||
const trimmed = editName.trim();
|
||||
if (!trimmed || !sessionId) {
|
||||
setIsEditingName(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/chat/sessions/${sessionId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: trimmed }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSessionTitle(trimmed);
|
||||
}
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
setIsEditingName(false);
|
||||
}, [editName, sessionId, setSessionTitle]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSaveName();
|
||||
} else if (e.key === "Escape") {
|
||||
setIsEditingName(false);
|
||||
}
|
||||
}, [handleSaveName]);
|
||||
const { panelOpen, setPanelOpen, workingDirectory, previewFile, setPreviewFile } = usePanel();
|
||||
|
||||
const handleFileAdd = useCallback((path: string) => {
|
||||
window.dispatchEvent(new CustomEvent('attach-file-to-chat', { detail: { path } }));
|
||||
@@ -115,8 +68,8 @@ export function RightPanel({ width }: RightPanelProps) {
|
||||
return (
|
||||
<aside className="hidden h-full shrink-0 flex-col overflow-hidden bg-background lg:flex" style={{ width: width ?? 288 }}>
|
||||
{/* Header */}
|
||||
<div className="flex h-10 shrink-0 items-center justify-between px-4">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Chat Info</span>
|
||||
<div className="flex h-12 mt-5 shrink-0 items-center justify-between px-4">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Files</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -133,54 +86,12 @@ export function RightPanel({ width }: RightPanelProps) {
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden p-4 space-y-4">
|
||||
{/* Name - editable */}
|
||||
<div className="shrink-0">
|
||||
<label className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70 mb-1.5 block">Name</label>
|
||||
{isEditingName ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSaveName}
|
||||
className="h-7 text-sm"
|
||||
/>
|
||||
<Button variant="ghost" size="icon-xs" onClick={handleSaveName}>
|
||||
<HugeiconsIcon icon={Tick01Icon} className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 group">
|
||||
<p className="text-sm font-medium flex-1 truncate">{displayTitle}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
||||
onClick={handleStartEdit}
|
||||
>
|
||||
<HugeiconsIcon icon={PencilEdit01Icon} className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-px bg-border/50 shrink-0" />
|
||||
|
||||
{/* Files — always show FileTree, fill remaining space */}
|
||||
<div className="flex flex-1 flex-col min-h-0 overflow-hidden">
|
||||
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70 mb-1.5 block shrink-0">Files</span>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<FileTree
|
||||
workingDirectory={workingDirectory}
|
||||
onFileSelect={handleFileSelect}
|
||||
onFileAdd={handleFileAdd}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col min-h-0 overflow-hidden">
|
||||
<FileTree
|
||||
workingDirectory={workingDirectory}
|
||||
onFileSelect={handleFileSelect}
|
||||
onFileAdd={handleFileAdd}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -152,7 +152,7 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 p-2 border-b border-border/30 shrink-0">
|
||||
<div className="flex items-center gap-2 px-4 py-2 shrink-0">
|
||||
<p className="min-w-0 flex-1 truncate text-[11px] text-muted-foreground" title={workingDirectory}>
|
||||
{workingDirectory || 'No directory selected'}
|
||||
</p>
|
||||
@@ -169,14 +169,16 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative p-2 border-b border-border/30 shrink-0">
|
||||
<HugeiconsIcon icon={Search01Icon} className="absolute left-4 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Filter files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-7 pl-7 text-xs"
|
||||
/>
|
||||
<div className="px-4 pb-2 shrink-0">
|
||||
<div className="relative">
|
||||
<HugeiconsIcon icon={Search01Icon} className="absolute left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
placeholder="Filter files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-7 pl-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tree */}
|
||||
|
||||
Reference in new issue
Block a user