Skip to content

feat(tui)!: replace /retitle with local /rename - #826

Open
anandh8x wants to merge 2 commits into
mainfrom
fix/tui-session-rename
Open

feat(tui)!: replace /retitle with local /rename#826
anandh8x wants to merge 2 commits into
mainfrom
fix/tui-session-rename

Conversation

@anandh8x

@anandh8x anandh8x commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Replace the bulk /retitle command with a local /rename [title] command.
  • Make bare /rename open a prefilled editor with Enter-to-save and Esc-to-cancel behavior.
  • Preserve names chosen before a fresh session's first prompt and prevent a late automatic-title response from overwriting a manual rename.
  • Polish /resume with aligned timestamp/title columns, hidden-but-searchable raw session IDs, and a visible position count.

Why

Renaming a session should be an immediate local metadata operation rather than a model-backed bulk action. The previous resume rows also gave raw IDs too much visual space, truncating the titles users actually scan for.

Impact

Users can rename the current session without a provider call or token usage, including before the first prompt. Resume rows are cleaner and easier to scan while session IDs remain available through search.

Validation

  • make fmt-check
  • go vet ./...
  • go test ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke
  • make lint-static
  • make vulncheck
  • git diff HEAD --check
  • Manual TUI verification of bare rename, inline rename, save, cancel, prefill, fresh-session naming, and the resume picker

Summary by CodeRabbit

  • New Features
    • Added /rename to rename the active session or set a title before the first prompt.
    • Added an interactive rename editor with save, cancel, validation, and paste support.
    • Session pickers now show cleaner titles, aligned timestamps, safer searchable entries, and “position / total” pagination.
  • Bug Fixes
    • Automatic titles no longer overwrite manual renames (even when an auto-title is in progress).
    • Empty rename pastes no longer trigger clipboard image handling.
    • /retitle is no longer available; rename editor state better blocks unrelated UI interactions.

BREAKING CHANGE: /retitle is removed; use /rename instead.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 36f16b7483c0
Changed files (16): internal/sessions/session_title_test.go, internal/sessions/store.go, internal/tui/btw.go, internal/tui/clipboard.go, internal/tui/commands.go, internal/tui/composer.go, internal/tui/model.go, internal/tui/model_test.go, internal/tui/session.go, internal/tui/session_rename.go, internal/tui/session_rename_test.go, internal/tui/session_title.go, and 4 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The TUI replaces /retitle with /rename, adds interactive and pending-session rename support, and conditionally persists automatic titles so stale generation cannot overwrite manual names. Session picker labels and overlay/sidebar behavior are also updated.

Changes

Session title flow

Layer / File(s) Summary
Conditional title storage
internal/sessions/store.go, internal/sessions/session_title_test.go
Adds locked UpdateTitleIfCurrent, applying trimmed non-empty titles only when the stored title matches the expected value, with matching and stale-update tests.
Manual rename command and editor
internal/tui/commands.go, internal/tui/model.go, internal/tui/session_rename.go, internal/tui/session.go, internal/tui/clipboard.go, internal/tui/composer.go, internal/tui/btw.go, internal/tui/session_rename_test.go
Adds /rename, interactive editing, pending titles, validation, persistence, paste handling, modal routing, and command-flow tests.
Automatic title consistency
internal/tui/session_title.go, internal/tui/session_title_test.go
Automatic title generation uses conditional updates and applies results only when the stored title remains unchanged.
Picker and overlay presentation
internal/tui/session.go, internal/tui/view.go, internal/tui/model_test.go, internal/tui/sidebar.go, internal/tui/transcript_selection.go
Session picker labels hide IDs while retaining searchability, add aligned timestamps and position counts, and suppress sidebar interactions for active MCP UI state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TUIModel
  participant SessionStore
  participant AutoTitle
  User->>TUIModel: Enter /rename
  TUIModel->>SessionStore: Persist manual title
  SessionStore-->>TUIModel: Updated metadata
  AutoTitle->>SessionStore: UpdateTitleIfCurrent(original title, generated title)
  SessionStore-->>AutoTitle: applied=false when manual title changed
  AutoTitle-->>TUIModel: Report title result
Loading

Possibly related PRs

  • Gitlawb/zero#224: Extends the same session title persistence functionality with conditional title updates.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing /retitle with a local /rename flow.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tui-session-rename

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/tui/session_title_test.go (1)

275-312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the missing branch assertion: stale auto-title must keep the retry gate set.

handleSessionTitleGenerated has three outcomes (apply / stale-skip-keep-gate / failure-release-gate); this test proves the store-level CAS outcome but doesn't drive result through handleSessionTitleGenerated to confirm titledSessions[session.SessionID] stays true after a stale (applied:false, err:nil) result — the one combination not covered by TestAutoTitleFailureReleasesRetryGate.

 	got, err := store.Get(session.SessionID)
 	if err != nil || got == nil {
 		t.Fatalf("get session: %v", err)
 	}
 	if got.Title != "Manual Name" {
 		t.Fatalf("late automatic title overwrote manual name: %q", got.Title)
 	}
+
+	next, _ := m.handleSessionTitleGenerated(result)
+	if !next.titledSessions[session.SessionID] {
+		t.Fatal("a stale auto-title result must not release the retry gate (manual rename already won)")
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/session_title_test.go` around lines 275 - 312, Extend
TestManualRenameWinsOverInFlightAutoTitle to pass the stale successful result
through handleSessionTitleGenerated after execCmd, then assert
titledSessions[session.SessionID] remains true. Preserve the existing
store-level assertions and verify the applied:false, err:nil branch keeps the
retry gate set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/tui/model_test.go`:
- Around line 942-949: Update TestSessionPickerLabelAlignsTitles to assert that
todayColumn and olderColumn are non-negative before comparing them, so missing
titles fail the test rather than matching at -1; retain the existing alignment
assertion for present titles.

In `@internal/tui/session_rename.go`:
- Around line 46-53: Update renameActiveSession to truncate the trimmed title to
tuiSessionTitleLimit before validating and storing it, preserving the existing
empty-title and unavailable-storage handling.

---

Nitpick comments:
In `@internal/tui/session_title_test.go`:
- Around line 275-312: Extend TestManualRenameWinsOverInFlightAutoTitle to pass
the stale successful result through handleSessionTitleGenerated after execCmd,
then assert titledSessions[session.SessionID] remains true. Preserve the
existing store-level assertions and verify the applied:false, err:nil branch
keeps the retry gate set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fab9a4d9-a0fa-42a8-a3e4-3632c1506cfc

📥 Commits

Reviewing files that changed from the base of the PR and between 097c265 and daee6c1.

📒 Files selected for processing (16)
  • internal/sessions/session_title_test.go
  • internal/sessions/store.go
  • internal/tui/btw.go
  • internal/tui/clipboard.go
  • internal/tui/commands.go
  • internal/tui/composer.go
  • internal/tui/model.go
  • internal/tui/model_test.go
  • internal/tui/session.go
  • internal/tui/session_rename.go
  • internal/tui/session_rename_test.go
  • internal/tui/session_title.go
  • internal/tui/session_title_test.go
  • internal/tui/sidebar.go
  • internal/tui/transcript_selection.go
  • internal/tui/view.go

Comment thread internal/tui/model_test.go
Comment thread internal/tui/session_rename.go

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Changes requested — for one thing that is not a code change. The engineering here is sound and I could not break it; the problem is that as titled, this PR will silently drop a user-facing breaking change from the changelog.

Reviewed at 36f16b7, base 097c265 (level with origin/main, zero commits behind). Author has write access, so this is the internal cycle rather than the issue-approved gate.

What I verified

  • The compare-and-swap actually does the work it claims. I gutted it — removed the session.Title != strings.TrimSpace(expected) guard at internal/sessions/store.go:775 so a late automatic title always overwrites — and two independent tests failed, one at each layer:

    --- FAIL: TestUpdateTitleIfCurrent
        stale update should preserve manual title: Title:"Late generated title" ... true
    --- FAIL: TestManualRenameWinsOverInFlightAutoTitle
        late automatic title should be skipped, got {title:"Generated Automatic Title", applied:true}
    
  • The pre-first-prompt path is guarded, and the guard is tested. Removing the m.titledSessions[session.SessionID] = true block at internal/tui/session.go:48-53 fails TestRenameNamesFreshSessionBeforeFirstPrompt ("a manually named fresh session must skip automatic naming"). Belt and braces here — sessionTitleIsAuto at session_title.go:213 would also refuse a manual title — but the explicit gate is the one under test.

  • "Hidden but searchable" holds. Dropping Meta: meta.SessionID from the picker item does not cost searchability: scorePickerItem builds its haystack from Group, Label, Value, Meta (internal/tui/picker.go:168) and Value is still the session ID.

  • The n / total footer is consistent with the filtered view. applyPickerFilter replaces p.items wholesale (picker.go:158) and resets selected to 0, so the counter and the visible list always agree. I went looking for an off-by-one against an unfiltered total and there isn't one.

  • /retitle is fully excised — no leftover retitleQueue/retitleActive/retitleTotal/retitleDone/retitleOK fields, no stale docs. The only surviving reference is the negative test at session_rename_test.go:182.

  • Gauntlet, darwin/arm64, non-/tmp checkout: make fmt-check clean, go vet ./... clean, go test ./... fully green, go test -race ./internal/tui/ ./internal/sessions/ -count=3 green, go run ./cmd/zero-release build and smoke both pass, git diff HEAD --check clean.

  • Passes executed: 0–5, 7–12. Pass 6 was run narrowly rather than in full — this diff has no auth, exec, parsing or network surface; I checked the two boundaries it does touch (the new store write path and the paste route) and nothing else applied.

Blocking

  1. The PR title is not a conventional commit, and this repo derives its changelog from the squash subject. — PR title / release-please-config.json

    The chain, each hop checked:

    • release-please-config.json and .github/workflows/release-please.yml mean versioning and CHANGELOG.md are generated from conventional-commit subjects.
    • gh api repos/Gitlawb/zero reports squash_merge_commit_title=COMMIT_OR_PR_TITLE. This PR has two commits (daee6c15, 36f16b74), so the squash subject will be the PR title, not a commit subject.
    • That title is Improve session naming and resume picker — no type, no scope. release-please parses no type, so the change contributes no changelog entry and no version bump.
    • Every recently merged PR does follow the convention: fix(sandbox):, test(tools):, fix(config):.

    That would be a minor annoyance on an ordinary change. It is not one here, because this PR removes /retitle, which shipped in #224 and has been present in every release from v0.3.0 through v0.6.0. A command that existed in the last release disappears, and under the current title the changelog will not say so.

    Retitle to something like feat(tui)!: replace /retitle with a local /rename and add a BREAKING CHANGE: /retitle is removed; use /rename footer. No code change required — this is the whole blocker.

Non-blocking

  • && msg.applied currently has no observable effect.internal/tui/session_title.go:255. I mutated it out and the entire internal/tui suite stayed green, so I traced why rather than reporting it as untested-and-therefore-broken. When the store rejects the update, UpdateTitleIfCurrent returns the freshly-read metadata (store.go:777), so msg.title is already the manual title — assigning it to m.activeSession.Title is a no-op, and the titledSessions gate is retained on both branches either way. The flag is correct defensive code that becomes load-bearing the moment UpdateTitleIfCurrent returns anything else on rejection. Worth a one-line comment saying that, so a future reader does not delete it as dead.
  • Four separable changes in one PR. — diffstat. The /retitle/rename swap, the rename editor, the pre-prompt naming plus compare-and-swap, and the /resume picker relayout (session.go:406-436, view.go:816-825) are independent; the picker work does not depend on any of the rename work. Team PR through the internal cycle, so I am not treating this as a scope close — but the picker change is the one a bisect would most want isolated.
  • Merge-conflict exposure with two long-lived branches. #643 and #570 both touch internal/tui/session.go, commands.go and model.go. Neither is close to merging, and this branch is current, so the right outcome is probably that they rebase onto this — worth saying so on those PRs rather than holding this one.
  • /rename is missing from the README command table.README.md:214. /resume, /rewind are listed as the session commands; /retitle never was, so nothing is stale, but a new user-facing command belongs on that line.
  • Blank submit closes the editor instead of holding it open.internal/tui/session_rename.go:32-37. m.renamePrompt = nil is set before validation, so Enter on whitespace closes the editor and prints an error. TestRenameEditorRejectsBlankAndEscCancels:157 asserts this deliberately, so I read it as intended rather than an oversight — flagging only because the user loses their editing context to recover from a typo.

Questions for the author

  • /retitle refused to run mid-turn (Cannot retitle sessions while a run is active); /rename has no m.pending guard. I believe that is correct — the old guard existed because /retitle made provider calls and /rename is pure local metadata, and the in-flight case is exactly what the compare-and-swap covers. Confirming that was deliberate rather than dropped with the surrounding code.

Not covered by this PR

  • A migration path for /retitle. It now resolves to nothing, so a user who typed it last week gets a generic unknown-command error. A one-release alias that prints "renamed to /rename" would be kinder than a changelog line alone, though the changelog line is the part I am blocking on.
  • Bulk retitling of old sessions is simply gone as a capability. That looks intentional given the "no provider call, no token usage" rationale, but nothing in the PR body says the bulk case was considered and dropped rather than overlooked.

Not verified

  • Windows and Linux — everything above is darwin/arm64 only. The diff is platform-neutral TUI and store code, and internal/sessions locking is unchanged by this PR, so I did not expect divergence and did not look for it.
  • The manual TUI walkthrough listed in the PR description. I exercised the rename paths through the model's Update loop and the store, not through a real terminal, so the editor's rendering and the picker's new column alignment are unconfirmed by eye.
  • Cross-process races. UpdateTitleIfCurrent takes the same per-session lock as UpdateTitle and AppendEvent, and I read that as correct, but I did not run two zero processes against one session to prove it.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, with two small things you can take or leave.

renameActiveSession (session_rename.go:47) does TrimSpace then cutRunes, which strips the ends but keeps interior newlines. /rename foo<newline>bar persists a multi-line title into metadata, and the pickers and listings all assume one line. The other title paths collapse whitespace, so this one is the odd one out.

The empty-paste guard in routePaste (clipboard.go:79-81) looks unreachable: the right-click path already returns earlier, so nothing gets there with an empty payload. The clipboard change is also the one part of this PR with no test.

Neither is worth holding it for. The store change is backward compatible, sessions without the new fields still load, and I could not get the picker to point at a deleted session.

@anandh8x anandh8x changed the title Improve session naming and resume picker feat(tui)!: replace /retitle with local /rename Jul 29, 2026
@anandh8x

Copy link
Copy Markdown
Collaborator Author

@gnanam1990 Updated the PR title to the conventional breaking-change format and added the BREAKING CHANGE note. Could you please re-review and clear the changes-requested status if this resolves the blocker?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants