Rename chat title

This commit is contained in:
Mark 2026-02-19 22:54:23 -08:00
parent f960ed3030
commit 5da7d46a49
3 changed files with 82 additions and 1 deletions

View File

@ -64,6 +64,30 @@ export async function GET(
return Response.json({ id, messages });
}
/** PATCH /api/web-sessions/[id] — update session metadata (e.g. rename). */
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
let body: { title?: string };
try {
body = await request.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const sessions = readIndex();
const session = sessions.find((s) => s.id === id);
if (!session) {
return Response.json({ error: "Session not found" }, { status: 404 });
}
if (typeof body.title === "string") {
session.title = body.title;
}
writeIndex(sessions);
return Response.json({ ok: true, session });
}
/** DELETE /api/web-sessions/[id] — remove a web chat session and its messages. */
export async function DELETE(
_request: Request,

View File

@ -49,6 +49,8 @@ type ChatSessionsSidebarProps = {
width?: number;
/** Called when the user deletes a session from the sidebar menu. */
onDeleteSession?: (sessionId: string) => void;
/** Called when the user renames a session from the sidebar menu. */
onRenameSession?: (sessionId: string, newTitle: string) => void;
/** When true, show a loader instead of empty state (e.g. initial sessions fetch). */
loading?: boolean;
};
@ -154,12 +156,15 @@ export function ChatSessionsSidebar({
onNewSession,
onSelectSubagent,
onDeleteSession,
onRenameSession,
mobile,
onClose,
width: widthProp,
loading = false,
}: ChatSessionsSidebarProps) {
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [renamingId, setRenamingId] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState("");
const handleSelect = useCallback(
(id: string) => {
@ -184,6 +189,19 @@ export function ChatSessionsSidebar({
[onDeleteSession],
);
const handleStartRename = useCallback((sessionId: string, currentTitle: string) => {
setRenamingId(sessionId);
setRenameValue(currentTitle || "");
}, []);
const handleCommitRename = useCallback(() => {
if (renamingId && renameValue.trim()) {
onRenameSession?.(renamingId, renameValue.trim());
}
setRenamingId(null);
setRenameValue("");
}, [renamingId, renameValue, onRenameSession]);
// Index subagents by parent session ID
const subagentsByParent = useMemo(() => {
const map = new Map<string, SidebarSubagentInfo[]>();
@ -288,6 +306,23 @@ export function ChatSessionsSidebar({
: "transparent",
}}
>
{renamingId === session.id ? (
<form
className="flex-1 min-w-0 px-2 py-1.5"
onSubmit={(e) => { e.preventDefault(); handleCommitRename(); }}
>
<input
type="text"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onBlur={handleCommitRename}
onKeyDown={(e) => { if (e.key === "Escape") { setRenamingId(null); setRenameValue(""); } }}
autoFocus
className="w-full text-xs font-medium px-1 py-0.5 rounded outline-none border"
style={{ color: "var(--color-text)", background: "var(--color-surface)", borderColor: "var(--color-border)" }}
/>
</form>
) : (
<button
type="button"
onClick={() => handleSelect(session.id)}
@ -313,7 +348,7 @@ export function ChatSessionsSidebar({
</div>
</div>
<div className="flex items-center gap-2 mt-0.5" style={{ paddingLeft: isStreamingSession ? "calc(0.375rem + 6px)" : undefined }}>
<span
className="text-[10px]"
style={{ color: "var(--color-text-muted)" }}
@ -330,6 +365,7 @@ export function ChatSessionsSidebar({
)}
</div>
</button>
)}
{onDeleteSession && (
<div className={`shrink-0 flex items-center pr-1 transition-opacity ${showMore ? "opacity-100" : "opacity-0"}`}>
<DropdownMenu>
@ -343,6 +379,12 @@ export function ChatSessionsSidebar({
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<DropdownMenuItem
onSelect={() => handleStartRename(session.id, session.title)}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /></svg>
Rename
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => handleDeleteSession(session.id)}

View File

@ -533,6 +533,18 @@ function WorkspacePageInner() {
[activeSessionId, sessions, fetchSessions],
);
const handleRenameSession = useCallback(
async (sessionId: string, newTitle: string) => {
await fetch(`/api/web-sessions/${sessionId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: newTitle }),
});
void fetchSessions();
},
[fetchSessions],
);
// Poll for active (streaming) agent runs so the sidebar can show indicators.
useEffect(() => {
let cancelled = false;
@ -1448,6 +1460,7 @@ function WorkspacePageInner() {
onSubagentClick={handleSubagentClickFromChat}
onFilePathClick={handleFilePathClickFromChat}
onDeleteSession={handleDeleteSession}
onRenameSession={handleRenameSession}
compact={isMobile}
/>
)}
@ -1477,6 +1490,7 @@ function WorkspacePageInner() {
}}
onSelectSubagent={handleSelectSubagent}
onDeleteSession={handleDeleteSession}
onRenameSession={handleRenameSession}
mobile
onClose={() => setChatSessionsOpen(false)}
/>
@ -1521,6 +1535,7 @@ function WorkspacePageInner() {
}}
onSelectSubagent={handleSelectSubagent}
onDeleteSession={handleDeleteSession}
onRenameSession={handleRenameSession}
width={rightSidebarWidth}
/>
)}