Skip to content

Commit ae8a905

Browse files
7418claude
andcommitted
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>
1 parent 5a57098 commit ae8a905

File tree

8 files changed

+187
-269
lines changed

8 files changed

+187
-269
lines changed

src/app/chat/[id]/page.tsx

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use client';
22

3-
import { useEffect, useState, use } from 'react';
3+
import { useEffect, useState, useRef, useCallback, use } from 'react';
44
import Link from 'next/link';
55
import type { Message, MessagesResponse, ChatSession } from '@/types';
66
import { ChatView } from '@/components/chat/ChatView';
77
import { HugeiconsIcon } from "@hugeicons/react";
8-
import { Loading02Icon } from "@hugeicons/core-free-icons";
8+
import { Loading02Icon, PencilEdit01Icon } from "@hugeicons/core-free-icons";
9+
import { Input } from '@/components/ui/input';
910
import { usePanel } from '@/hooks/usePanel';
1011

1112
interface ChatSessionPageProps {
@@ -22,8 +23,54 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
2223
const [sessionModel, setSessionModel] = useState<string>('');
2324
const [sessionMode, setSessionMode] = useState<string>('');
2425
const [projectName, setProjectName] = useState<string>('');
26+
const [isEditingTitle, setIsEditingTitle] = useState(false);
27+
const [editTitle, setEditTitle] = useState('');
28+
const titleInputRef = useRef<HTMLInputElement>(null);
2529
const { setWorkingDirectory, setSessionId, setSessionTitle: setPanelSessionTitle, setPanelOpen } = usePanel();
2630

31+
const handleStartEditTitle = useCallback(() => {
32+
setEditTitle(sessionTitle || 'New Conversation');
33+
setIsEditingTitle(true);
34+
}, [sessionTitle]);
35+
36+
const handleSaveTitle = useCallback(async () => {
37+
const trimmed = editTitle.trim();
38+
if (!trimmed) {
39+
setIsEditingTitle(false);
40+
return;
41+
}
42+
try {
43+
const res = await fetch(`/api/chat/sessions/${id}`, {
44+
method: 'PATCH',
45+
headers: { 'Content-Type': 'application/json' },
46+
body: JSON.stringify({ title: trimmed }),
47+
});
48+
if (res.ok) {
49+
setSessionTitle(trimmed);
50+
setPanelSessionTitle(trimmed);
51+
window.dispatchEvent(new CustomEvent('session-updated', { detail: { id, title: trimmed } }));
52+
}
53+
} catch {
54+
// silently fail
55+
}
56+
setIsEditingTitle(false);
57+
}, [editTitle, id, setPanelSessionTitle]);
58+
59+
const handleTitleKeyDown = useCallback((e: React.KeyboardEvent) => {
60+
if (e.key === 'Enter') {
61+
handleSaveTitle();
62+
} else if (e.key === 'Escape') {
63+
setIsEditingTitle(false);
64+
}
65+
}, [handleSaveTitle]);
66+
67+
useEffect(() => {
68+
if (isEditingTitle && titleInputRef.current) {
69+
titleInputRef.current.focus();
70+
titleInputRef.current.select();
71+
}
72+
}, [isEditingTitle]);
73+
2774
// Load session info and set working directory
2875
useEffect(() => {
2976
async function loadSession() {
@@ -115,7 +162,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
115162
{/* Chat title bar */}
116163
{sessionTitle && (
117164
<div
118-
className="flex items-center justify-center px-4 py-2 gap-1"
165+
className="flex items-center justify-center px-4 pb-2 gap-1"
119166
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
120167
>
121168
{projectName && (
@@ -124,9 +171,33 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) {
124171
<span className="text-xs text-muted-foreground shrink-0">/</span>
125172
</>
126173
)}
127-
<h2 className="text-sm font-medium text-foreground/80 truncate max-w-md">
128-
{sessionTitle}
129-
</h2>
174+
{isEditingTitle ? (
175+
<div style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
176+
<Input
177+
ref={titleInputRef}
178+
value={editTitle}
179+
onChange={(e) => setEditTitle(e.target.value)}
180+
onKeyDown={handleTitleKeyDown}
181+
onBlur={handleSaveTitle}
182+
className="h-7 text-sm max-w-md text-center"
183+
/>
184+
</div>
185+
) : (
186+
<div
187+
className="flex items-center gap-1 group cursor-default max-w-md"
188+
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
189+
>
190+
<h2 className="text-sm font-medium text-foreground/80 truncate">
191+
{sessionTitle}
192+
</h2>
193+
<button
194+
onClick={handleStartEditTitle}
195+
className="opacity-0 group-hover:opacity-100 transition-opacity shrink-0 p-0.5 rounded hover:bg-muted"
196+
>
197+
<HugeiconsIcon icon={PencilEdit01Icon} className="h-3 w-3 text-muted-foreground" />
198+
</button>
199+
</div>
200+
)}
130201
</div>
131202
)}
132203
<ChatView key={id} sessionId={id} initialMessages={messages} initialHasMore={hasMore} modelName={sessionModel} initialMode={sessionMode} />

src/app/chat/page.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,6 @@ export default function NewChatPage() {
3535
const [streamingToolOutput, setStreamingToolOutput] = useState('');
3636
const abortControllerRef = useRef<AbortController | null>(null);
3737

38-
const handleWorkingDirectoryChange = useCallback((dir: string) => {
39-
setWorkingDir(dir);
40-
setWorkingDirectory(dir);
41-
setPanelOpen(true);
42-
}, [setWorkingDirectory, setPanelOpen]);
43-
4438
const stopStreaming = useCallback(() => {
4539
abortControllerRef.current?.abort();
4640
abortControllerRef.current = null;
@@ -364,7 +358,6 @@ export default function NewChatPage() {
364358
modelName={currentModel}
365359
onModelChange={setCurrentModel}
366360
workingDirectory={workingDir}
367-
onWorkingDirectoryChange={handleWorkingDirectoryChange}
368361
mode={mode}
369362
onModeChange={setMode}
370363
/>

src/components/chat/ChatView.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,6 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal
5959
}, [sessionId]);
6060
const abortControllerRef = useRef<AbortController | null>(null);
6161

62-
const handleWorkingDirectoryChange = useCallback((dir: string) => {
63-
setWorkingDirectory(dir);
64-
setPanelOpen(true);
65-
// Persist to database
66-
if (sessionId) {
67-
fetch(`/api/chat/sessions/${sessionId}`, {
68-
method: 'PATCH',
69-
headers: { 'Content-Type': 'application/json' },
70-
body: JSON.stringify({ working_directory: dir }),
71-
}).catch(() => { /* silent */ });
72-
}
73-
}, [sessionId, setWorkingDirectory, setPanelOpen]);
74-
7562
// Ref to keep accumulated streaming content in sync regardless of React batching
7663
const accumulatedRef = useRef('');
7764
// 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
484471
modelName={currentModel}
485472
onModelChange={setCurrentModel}
486473
workingDirectory={workingDirectory}
487-
onWorkingDirectoryChange={handleWorkingDirectoryChange}
488474
mode={mode}
489475
onModeChange={handleModeChange}
490476
/>

src/components/chat/MessageInput.tsx

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import { useRef, useState, useCallback, useEffect, type KeyboardEvent, type Form
44
import { HugeiconsIcon } from "@hugeicons/react";
55
import {
66
AtIcon,
7-
FolderOpenIcon,
87
Wrench01Icon,
98
ClipboardIcon,
109
HelpCircleIcon,
1110
ArrowDown01Icon,
1211
ArrowUp02Icon,
1312
CommandLineIcon,
14-
Attachment01Icon,
13+
PlusSignIcon,
1514
Cancel01Icon,
1615
Delete02Icon,
1716
Coins01Icon,
@@ -23,7 +22,6 @@ import {
2322
GlobalIcon,
2423
} from "@hugeicons/core-free-icons";
2524
import { cn } from '@/lib/utils';
26-
import { FolderPicker } from './FolderPicker';
2725
import {
2826
PromptInput,
2927
PromptInputTextarea,
@@ -61,7 +59,6 @@ interface MessageInputProps {
6159
modelName?: string;
6260
onModelChange?: (model: string) => void;
6361
workingDirectory?: string;
64-
onWorkingDirectoryChange?: (dir: string) => void;
6562
mode?: string;
6663
onModeChange?: (mode: string) => void;
6764
}
@@ -245,7 +242,7 @@ function AttachFileButton() {
245242
onClick={() => attachments.openFileDialog()}
246243
tooltip="Attach files"
247244
>
248-
<HugeiconsIcon icon={Attachment01Icon} className="h-3.5 w-3.5" />
245+
<HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" />
249246
</PromptInputButton>
250247
);
251248
}
@@ -343,7 +340,6 @@ export function MessageInput({
343340
modelName,
344341
onModelChange,
345342
workingDirectory,
346-
onWorkingDirectoryChange,
347343
mode = 'code',
348344
onModeChange,
349345
}: MessageInputProps) {
@@ -358,7 +354,6 @@ export function MessageInput({
358354
const [popoverFilter, setPopoverFilter] = useState('');
359355
const [selectedIndex, setSelectedIndex] = useState(0);
360356
const [triggerPos, setTriggerPos] = useState<number | null>(null);
361-
const [folderPickerOpen, setFolderPickerOpen] = useState(false);
362357
const [modeMenuOpen, setModeMenuOpen] = useState(false);
363358
const [modelMenuOpen, setModelMenuOpen] = useState(false);
364359
const [inputValue, setInputValue] = useState('');
@@ -742,10 +737,6 @@ export function MessageInput({
742737
const currentModelOption = MODEL_OPTIONS.find((m) => m.value === currentModelValue) || MODEL_OPTIONS[0];
743738
const currentMode = MODE_OPTIONS.find((m) => m.value === mode) || MODE_OPTIONS[0];
744739

745-
const folderShortName = workingDirectory
746-
? workingDirectory.split('/').filter(Boolean).pop() || workingDirectory
747-
: '';
748-
749740
// Map isStreaming to ChatStatus for PromptInputSubmit
750741
const chatStatus: ChatStatus = isStreaming ? 'streaming' : 'ready';
751742

@@ -921,23 +912,11 @@ export function MessageInput({
921912
{/* Attach file button */}
922913
<AttachFileButton />
923914

924-
{/* Folder picker button */}
925-
<PromptInputButton
926-
onClick={() => setFolderPickerOpen(true)}
927-
tooltip={workingDirectory || 'Select project folder'}
928-
>
929-
<HugeiconsIcon icon={FolderOpenIcon} className="h-3.5 w-3.5" />
930-
<span className="max-w-[120px] truncate text-xs">
931-
{folderShortName || 'Folder'}
932-
</span>
933-
</PromptInputButton>
934-
935915
{/* Mode selector */}
936916
<div className="relative" ref={modeMenuRef}>
937917
<PromptInputButton
938918
onClick={() => setModeMenuOpen((prev) => !prev)}
939919
>
940-
<HugeiconsIcon icon={currentMode.icon} className="h-3.5 w-3.5" />
941920
<span className="text-xs">{currentMode.label}</span>
942921
<HugeiconsIcon icon={ArrowDown01Icon} className={cn("h-2.5 w-2.5 transition-transform duration-200", modeMenuOpen && "rotate-180")} />
943922
</PromptInputButton>
@@ -974,9 +953,7 @@ export function MessageInput({
974953
</div>
975954
)}
976955
</div>
977-
</PromptInputTools>
978956

979-
<div className="flex items-center gap-1.5">
980957
{/* Model selector */}
981958
<div className="relative" ref={modelMenuRef}>
982959
<PromptInputButton
@@ -987,7 +964,7 @@ export function MessageInput({
987964
</PromptInputButton>
988965

989966
{modelMenuOpen && (
990-
<div className="absolute bottom-full right-0 mb-1.5 w-48 rounded-lg border bg-popover shadow-lg overflow-hidden z-50">
967+
<div className="absolute bottom-full left-0 mb-1.5 w-48 rounded-lg border bg-popover shadow-lg overflow-hidden z-50">
991968
<div className="py-1">
992969
{MODEL_OPTIONS.map((opt) => {
993970
const isActive = opt.value === currentModelValue;
@@ -1011,29 +988,20 @@ export function MessageInput({
1011988
</div>
1012989
)}
1013990
</div>
991+
</PromptInputTools>
1014992

1015-
<FileAwareSubmitButton
1016-
status={chatStatus}
1017-
onStop={onStop}
1018-
disabled={disabled}
1019-
inputValue={inputValue}
1020-
hasBadge={!!badge}
1021-
/>
1022-
</div>
993+
<FileAwareSubmitButton
994+
status={chatStatus}
995+
onStop={onStop}
996+
disabled={disabled}
997+
inputValue={inputValue}
998+
hasBadge={!!badge}
999+
/>
10231000
</PromptInputFooter>
10241001
</PromptInput>
10251002
</div>
10261003
</div>
10271004

1028-
{/* FolderPicker dialog */}
1029-
<FolderPicker
1030-
open={folderPickerOpen}
1031-
onOpenChange={setFolderPickerOpen}
1032-
onSelect={(dir) => {
1033-
onWorkingDirectoryChange?.(dir);
1034-
}}
1035-
initialPath={workingDirectory || undefined}
1036-
/>
10371005
</div>
10381006
);
10391007
}

0 commit comments

Comments
 (0)