Names that fit the chat, and a way to change one

Two things about titles were wrong. Every chat spent a second completion on
its name, including an agent chat whose opening words are already a title --
somebody starting one states an objective, not a topic. An agent chat now
takes `fallback_title` from its first prompt and makes no request at all;
an ordinary chat, which opens with a question whose *answer* is what makes a
title worth asking for, is unchanged.

And renaming existed only as the `/title` slash command, which set the heading
and left the sidebar row showing the old name until the next reload -- a rename
that looks half-applied is one people do twice. There are pencil buttons on the
heading and on every sidebar row now, both PATCHing the route that was already
there, and `update_chat` answers a rename with the out-of-band pair the `done`
frame has always sent, so one response moves both. Only on a rename: sending it
for every PATCH would overwrite the heading from an unrelated save. `/title`
sets both spans itself, being a bare fetch rather than htmx.

The dialog is the `data-prompt` mechanism the folder work added, which is why
the heading keeps a button rather than becoming an inline field: it sits in a
flex row beside the badges and the connection chip, and swapping it for a text
box moves all of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-04 08:46:08 +02:00
parent 6fb260892f
commit 50270e13f7
6 changed files with 279 additions and 4 deletions
+12
View File
@@ -1463,12 +1463,14 @@ async def update_chat(request: Request, db: Db, user: RequiredUser, chat_id: str
allowed = permissions.resolve(db, user)
form = await request.form()
renamed = False
if "title" in form:
cleaned = str(form["title"]).strip()[:300]
if cleaned:
chat.title = cleaned
# An explicit rename must not be overwritten by auto-titling later.
chat.title_generated = True
renamed = True
if "folder_id" in form:
chat.folder_id = str(form["folder_id"]) or None
@@ -1576,6 +1578,16 @@ async def update_chat(request: Request, db: Db, user: RequiredUser, chat_id: str
chat.params_json = {**(chat.params_json or {}), "reasoning_effort": seeded}
db.commit()
if renamed:
# The two out-of-band spans the `done` frame already uses, so one
# response updates the heading *and* the sidebar row. Renaming used to
# be the `/title` command alone, which set the heading and left the
# sidebar showing the old name until the next reload -- a rename that
# looks half-applied is one people do twice.
return HTMLResponse(
templates.get_template("chat/_title_oob.html").render({"chat": chat})
)
return Response(status_code=status.HTTP_204_NO_CONTENT)