feat: rename a plan from the sidebar, the toolbar title, and the plan menu
Summary
You can now rename a plan, end to end. #1831 left "Rename" out of the plan menu because there was no backend for it.
Server. PATCH /api/plans/{id} with { name } (plans-app.ts) goes through the existing one-plan route and rewrite, so no new route table entry is needed. The name is read by planRenameRequestSchema, which uses the same rule as creation (trimmedText(PLAN_BOUNDS.MAX_NAME_CHARS)): trimmed, non-empty, at most 200 characters. That matches the new-plan form's maxLength={200}. Unknown keys are refused, like every request on this wire.
plan-store.ts's renamePlan is owner-scoped. It takes the plan row under FOR UPDATE and writes the name in one transaction. Another account's plan answers 404 not-found, the same as a plan that doesn't exist. A rename is not a save: updatedAt and the list order stay as they were.
What else reads the plan's name:
- Document title (re-titled). The body opens with
# <name>. When the stored body is exactly the one formatted from its stored fields under the old name, the rename formats it again under the new name, inside the same lock. Two cases leave the body alone: a never-saved body (""), which already reads as the template under whatever name stands, and any other body. One more is left as saved: a body the longer heading would carry pastMAX_BODY_CHARS. The plan still decodes, and the next save of notes formats it. - Notetaker (now formats under the current name). A save of notes now formats the heading under the plan's name as it stands at the save. The read and the write happen under the row's lock (
readPlanForUpdate), not under the name the call began with, so a run already in flight when the rename lands can't put the old heading back.PlanDocumentBindingdrops itsheader. This came from review. - Planning model (sees it next turn).
planningStandingContext(Name:line) reads the plan fresh on every turn. - Voice seed (consciously left alone mid-call).
voice/opening.ts's plan seed is one message written when a call opens. A call already standing keeps the name it opened with, and the next call reads the new one. Re-seeding a live voice session for a rename isn't worth the cost to its prompt cache. The scribe's in-progress drafts are also formatted under the name its run read, but they are transient: the save that follows lands under the current name.
Wire and host.
HostedPlanClient.rename. A blank name never leaves the Mac.GATEWAY_METHOD.PLANNING_RENAMEincompose-planning.tstakes the answered plan into the list row and the open document in place, with no refetch.ACT_KIND.PLANNING_RENAME(planning.rename): payloadplanningRenameParamsSchema, a boolean answer guard, and one router row inplanning-acts.ts, refused outside a panel like its siblings.HTTP_METHOD.PATCHis added to@sidecar/wire.
Optimistic update. usePlansTab draws the new name at once, in the sidebar and on the open plan alike. It keeps that name until main's view carries it, so there is no flicker while the view catches up. On a refusal it puts the old name back, and the surface where you typed shows a quiet note: "The plan could not be renamed. Try again."
UI (plan-name-field.tsx, plan-actions.tsx, desktop-plans.tsx):
- Menu item. "Rename" with the pencil icon sits in the shared plan menu, right after Copy plan. For a plan that isn't open it comes first. No "…", following #1847.
- Sidebar. Right-click → Rename turns the row into an inline field with the whole name selected. The folder line stays beneath it.
- Toolbar title. The title is click-to-edit, Linear/Codex style. It has no chrome until hovered, then the field opens in the same place and type, with a ring rather than a border so nothing shifts. Rename from the toolbar ⋯ edits there.
- Same rules everywhere. Return or blur saves. Escape cancels and stops at the field, so it does not also leave the plan. An empty or unchanged name reverts. Ending with a key hands focus back to the row or title.
- No shortcut. There is no natural Mac binding (Finder's Return-to-rename would clash with opening the row), so the #1844 shortcut table is unchanged.
- Session replay. The field is a plain
<input>, which session replay masks like every input (session-replay.ts). Its only text attribute isaria-label="Plan name"; the plan's name appears in no attribute.
Evidence
- Platform-independent checks:
./scripts/check.shexits 0, after a rebase on #1853–#1855. - macOS Electron verification (
./scripts/verify.sh): not run. CI could not verify this change: it builds nothing for the Mac, and this PR was written on a Linux cloud VM. LUKE-159 is the manual Mac pass.
Visual evidence
- Fixture screenshots inspected:
not attached. I rendered the realDesktopShellover the built stylesheet in headless Chrome (2x) and inspected three states:- Sidebar. Right-click → Rename shows the row as a ringed field with "Billing export" selected and
~/ledgerbeneath it. - Toolbar title. The title as a ringed field spanning the heading, name selected.
- Refused rename. The old name is back, with the red note left of Copy plan.
- Sidebar. Right-click → Rename shows the row as a ringed field with "Billing export" selected and
- That render surfaced one CSS fix: the title's
overflow: hiddenclipped the ring's left edge, so the title now reserves the 6px.
Physical-device evidence
- Screenshot or screen recording:
not attached - Device/display configuration:
not recorded
Invariants
-
./scripts/check.shgreen../scripts/verify.shnot run (no Mac here; see above). - One new JSON Schema golden,
packages/hosted/fixtures/json-schema/plan-wire-planRenameRequestSchema.json, for the newplanRenameRequestSchema. Its bytes are the create request's:{ name }, a trimmed string,minLength: 1,maxLength: 200. No existing golden changed. - Every behavior change has a test that fails without it:
- Store (PGlite,
test:store). Renames own plan, heading included. A never-saved plan reads under the new name. Another account and an unknown id are refused, and nothing moves. Notes saved after a rename keep the new heading. A near-limit body keeps its body and still reads. - Route. PATCH trims and answers the plan, which then lists that way. Blank, whitespace-only, 201-character, and widened bodies answer 400. Another account answers 404.
- Client. Sends the trimmed name. A blank name never travels. 404 answers nothing.
- Host. List row and open document redraw with no read. A refusal leaves both.
- Act router. The new act reaches the host, trimmed.
- Hook. Optimistic name, then revert on refusal, then let go once the view carries it. A fixture renames nowhere.
- Components. Menu shows Rename. The inline sidebar field saves on Return, and Escape and an empty name keep the old one, without the window's Escape firing. The toolbar title edits on a press and from ⋯, and blur saves. A refused rename shows the note until the next edit, and the late refusal of an edit a newer one replaced shows none.
- Store (PGlite,
- Gateway envelope goldens unchanged.
- No new
astype assertion. - No new
Effect.runPromise/runSync/runFork. - No new raw async primitives.
- No discarded Effect.
- Test count +16: web +6 (
hosted-plans4,plans-app2),plan-client+2,compose-planning+2,use-plans-tab+2,plan-actions+4. Existing menu-label assertions now include "Rename", and the act-router test covers the new act. - No hand-rolled file replaced.
-
CLAUDE.md/AGENTS.mduntouched; no sentence there names these parts. -
PRIVACY.mdunchanged. - No dependency changes.
- No barrel or door change. The renderer reads
PLAN_BOUNDSthrough the@sidecar/hosted/plan-wiredoor it already used.
Notes
- The guide has no hand-written facts half any more (
packages/guide), so there was nothing to teach it.
🤖 Generated with Claude Code