feat(slash): /rename, plus tier-contrast and cursor follow-ups#72
Merged
feat(slash): /rename, plus tier-contrast and cursor follow-ups#72
Conversation
`ratatui_textarea`'s built-in placeholder paint offsets one column right of its cursor anchor, so the OS cursor sat in a gap to the left of `A` in `Ask anything...`. Hand-roll the placeholder at `textarea.x` so the cursor lands on top of the first character, matching the modal pickers' search row.
Five `draw_frame_*` snapshots that include the input panel still showed the old `❯ Ask` two-space gap; refresh them to match the placeholder-on-first-character layout.
…ctly `muted` was Catppuccin's Overlay0 (one step above `dim`'s Surface2), which left only ~17% luminance separation between secondary text and metadata. On many terminals the two tiers blurred into the same shade of grey. Pulling `muted` to Subtext0 (and the equivalent secondary-text hue in the non-Catppuccin palettes) widens the gap to a clear 4-step ladder while leaving `dim` where it is. Tool block left bars stay on the prior Overlay0 grey via their own `tool_border` slot — borders are chrome and want to stay subtle, so they decouple from the text tier. The /resume picker is the most visible beneficiary: unselected titles now read clearly above the metadata row. Status bar titles, tool labels, and grep file headers also gain readability for the same underlying reason.
Bare opens a modal pre-filled with the current title; `/rename <title>` applies directly. After a manual rename, the background AI title generator no-ops for the rest of the session so a slow Haiku response can't overwrite the user's pick. The manual-title flag is mirrored into both `SessionState` (read by the actor's `AppendAiTitle` gate) and `SharedState` (read by the title-gen pre-check via `SessionHandle`), so the suppression catches the race either side of the Haiku call.
Restate-the-code lines and multi-paragraph rationales that the function or field signature already implies — kept only the one-line summaries that explain the cross-module race coordination.
The slash-commands user guide gains a "Renaming a Session" section and a table row; the roadmap moves /rename out of Deferred into the working slash-command list (also tightening the bullet so the section stays balanced); the README adds /rename to the working-today bullet; the CLAUDE.md crate tree gains a `rename.rs` row.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Drops Autocomplete-Popup-as-its-own-section in favor of a two-line lede, folds /resume + /rename into one Sessions section, trims implementation detail (resolution tier walk, JSONL source tag, "load + sanitize path" parity note) that users don't need to use the commands.
Adds TestBackend render tests for `RenameModal` (layout, ellipsis truncation, cursor placement, short-area cursor bail, height pin) plus a `Tab`-key arm test for the catch-all branch. Extends the `acks_then_drops` integration test to route a `set_manual_title` call through the fixture so the `SetManualTitle` or-pattern arm in the test helper is exercised; adds two handle-level tests pinning the latched-flag contract on both healthy dispatch and dead-actor paths. Patch coverage on the /rename feature lifts from ~78% to ~97%.
Empty-buffer placeholder text (`Ask anything...`, `Type to queue a follow-up...`, `Esc edits last queued · Enter adds another`) added more chrome than signal — Claude Code's bare prompt is the simpler model. The arg-mode `[id]` ghost text stays, but is now painted one column right of the cursor so the OS-native cursor visually leads the dim hint, mirroring `ratatui_textarea`'s built-in placeholder layout. Drops `placeholder` and `has_queued` fields, the `PLACEHOLDER_*` constants, `refresh_placeholder` / `set_has_queued` / `is_buffer_empty`, and the App's `sync_input_queue_hint` plumbing — nothing else read those signals. Net 74 fewer lines in `input.rs`.
A `/rename` on a fresh session followed by quit used to write a
header+title-only JSONL with no actual messages, which then failed to
resume ("session has no messages"). Two fixes in one shape:
- Generalize `WriterStatus::Pending` to hold a `deferred: Vec<Entry>`
so a `/rename` before any record cmd queues the title there. The
queue dies with the actor when no message ever arrives — no file
on disk. Replaces the prior `ManualTitle` enum (Unset/Pending/
Persisted) and its invariant correlation with `WriterStatus`.
- Loosen the resume bail to `pre_sanitize_len > 0 && messages.is_empty()`
so a legitimately empty (header-only or header+title) file resumes
cleanly. Pre-fill `first_user_prompt_seen` with `data.title.is_some()`
so the next message doesn't push a duplicate FirstPrompt title.
The first cut of `WriterStatus::Pending { header, deferred: Vec<Entry> }`
plus a `defer_entry`/`is_pending` pair had two soft spots that PR review
flagged: `Vec<Entry>` permits any entry shape (today only titles are
ever deferred), and `defer_entry`'s `debug_assert!(false)` silently
drops the entry in release builds when the precondition fails.
- Narrow to `deferred_title: Option<String>`. Only titles are ever
queued, multiple `/rename`s collapse to last-wins for free, and the
`large_enum_variant` clippy lint stays quiet without boxing.
- Replace `is_pending` + `defer_entry` with a single
`try_defer_title(title) -> Result<(), String>` that returns the
rejected title for the caller to route into the live batch. Removes
the silent-drop hazard and the precondition-then-mutate dance.
- Reconstruct the `Entry::Title { source: UserProvided }` at flush
time (timestamp = flush instant, since the deferred slot only fires
on first promotion of an otherwise-empty file).
- Add tests for last-wins multi-rename and for the deferred slot
surviving a `Pending`-create rollback. Pin positional ordering of
header < title < message in the deferred-then-record path. Tighten
the unwritten-session assertion to `store.list().is_empty()`.
PR review flagged the dual `manual_title_set` (a `bool` on `SessionState` and an `AtomicBool` on `SharedState`) as correlated state — two writers storing the same logical fact. The dual was useful but expressed as if accidental. Drop the `SessionState` field. The actor reads / writes the `SharedState` atomic directly (passed through `absorb`), and `queue_message_entries` takes `manual_title_set: bool` as a parameter. The handle still flips the atomic eagerly before dispatch (closes the title-generator TOCTOU window) and the actor mirrors on absorb (covers direct-cmd tests + defends against a flip during a batch's `AppendAiTitle` arm). Single storage, two idempotent writers.
Two stale comments flagged in PR review: - `slash/rename.rs:289`: claimed empty `String::pop` "would panic in some std impls" — `String::pop` is documented to return `None` on empty in every impl. The test name already pins the behavior. - `slash/rename.rs:300`: referenced `MAX_TITLE_LEN`, but the local constant is `MAX_TITLE_CHARS`. Fully qualify to `session::state::MAX_TITLE_LEN` so the cross-module mirror is unambiguous. Also trim the title-only resume test comment to one line.
The popup help displays `description()` next to the `usage()` placeholder
(`[<level>]`, `[<title>]`, ...), so spelling the parameter inside the
description duplicates information the row already shows. The five
dual-mode commands also split between two prose templates ("Open the X
picker or switch directly with /cmd <param>" vs. "Verb the noun — /cmd
for X, /cmd <param> to Y"), which read as inconsistent.
Drop the parameter mention and the UI-mechanism framing. Land on the
same shape claude-code and codex use: a single short verb-first phrase.
Result<(), String> read like an error channel ("the title couldn't be
deferred") when the rejected title is just routed elsewhere — not an
error. Option<String> says it directly: Some(t) hands the title back for
the caller to queue normally, None means it's parked in the deferred
slot.
Drops the redundant "Pending: defer / Active: route" comment on the call
site — the new doc comment on try_defer_title plus the None / Some
branches read self-evidently. Adds a // ── try_defer_title ── unit
section covering the three branches (Pending overwrite, Active rejects,
Broken rejects) directly so a regression that drops the title on
Active / Broken would fail at the unit layer instead of riding only on
the actor integration tests.
- Drop "(likely corrupt)" from the resume bail. Sanitize also fires when every message was an unresolved tool block, which is legitimate pruning; the bail comment above already states the real reason (chaining onto a dropped last_message_uuid). - Reword `SessionState::resumed` doc as a precondition the caller must meet, not a description of what the one current caller does. - Drop the inline ` || data.title.is_some()` comment in handle.rs — the new resumed contract above carries the same WHY. - Tighten the AppendAiTitle re-check comment to name the actual race (rename flips the latch after the title generator queued the entry). - Reword the FirstPrompt latch comment so "before the push" is no longer ambiguous between the conditional title push and the unconditional message push.
The resume picker rendered titles on the muted tier so they could stay distinct from the metadata's dim tier without colliding with the cursor-row text+bold. After the recent muted brightening, muted and dim sit closer than they read on paper — the title still doesn't pop. Lift every title onto text and rely on bold + the gutter marker to call out the cursor row, matching how claude-code's session list reads. Add a trailing blank line per row (ROW_HEIGHT 2 → 3) so adjacent sessions don't visually run together.
Bold + the gutter `>` weren't a strong enough cue when the title color itself matched non-cursor rows. Lift the cursor row's title onto the theme accent so the whole row reads as one bright pop, matching the gutter marker that's already in accent.
The popup column read uneven: /config and /diff and /status had 90+ char descriptions that wrapped or stomped neighbors at typical widths, while /model and /theme were 16-17 chars. The path detail in /config and the git-command parenthetical in /diff belonged in the modal output, not the popup hint. Bring everything into the 15-40 char range. The state column already shows usage placeholders (`<id>`, `<title>`, ...) so descriptions can focus on a single verb-first phrase.
- handle.rs: move resume_title_only_session_succeeds_and_keeps_title_on_next_message ahead of resume_empty_session_errors so within-section order tracks happy → variants → error per CLAUDE.md. - state.rs: rename `_returns_title_verbatim` → `_rejects_with_title` to phrase the scenario instead of the return mechanism.
Seven test names were 60-88 chars long, padded with mechanism details that the assert lines already cover. Examples: "writes user provided title in lieu of first prompt" → "replaces first prompt title"; "keeps pending and preserves deferred title" → "preserves deferred title" (Pending is implied by the deferred slot surviving). Brings the long ones under 60 chars while keeping the scenario clear.
The trailing fragments were mechanism details the asserts already cover: "with system message no forward" → "locally", "with cch populated" → implicit, "within declared order" → implicit, "swaps id replays transcript and clears pending state" → "replays transcript and clears pending". Brings these from 75-88 chars down to 47-60.
5 tasks
hakula139
added a commit
that referenced
this pull request
May 9, 2026
<!-- markdownlint-disable-next-line first-line-heading --> ## Summary A doc-only sweep of `docs/` that fixes stale claims drifted from source, replaces rotting inline file links with backticks or concept-level cross-doc pointers, normalizes Sources sections, and reworks the prose tics that had crept into design and research docs. 19 files touched, `pnpm spellcheck` and `pnpm lint` clean. The sweep was deferred during the `/resume` PR after rewriting `slash/resume.md` exposed several stale claims of the same shape lurking in `commands.md` and `modals.md`. Folding the cross-doc audit into one PR reads cleaner than piecemeal edits attached to feature work. ## Design decisions - **Sources sections keep full `crates/oxide-code/src/...` paths, sorted alphabetically.** Full paths are clickable in editors and unambiguous about what level of the tree we're naming. Line numbers stay out so the entries don't rot under refactors. Annotations stay one short clause. - **Inline body references switch to backticks or concept-level cross-doc links.** `[X](../../../crates/.../x.rs)` in prose rotted on every file split. Backticked names like `` `SearchableList` `` and `[modals.md](modals.md)` survive renames. - **Trait-shape claims described semantically rather than as literal Rust.** The `modals.md` `ModalKey` block was already stale (the `Preview` variant added in PR #67 was missing). Rewriting it as four named outcomes with prose stays correct across future variant additions. - **Research docs keep their implementation depth.** Research docs document external systems (Claude Code, Codex, opencode, Anthropic API) and earn their detail. The sweep there is wording consistency only — antithesis trims, em-dash chain splits, stale counts. - **"X, not Y" rewritten in the body, sometimes preserved in titles.** Decision titles where the contrast IS the load-bearing rationale (`/resume`'s `roll_into` vs. process replacement; the `cch` body field vs. beta header) keep the antithesis. Body prose loses it: `xxh64, not SHA-256` becomes `xxh64 for change detection`, etc. - **Prose connectors over em-dashes and period fragmentation.** Em-dash is reserved for true parenthetical asides, not for stitching two independent clauses together. When trimming em-dash / semicolon overuse, transitions like `since`, `because`, `while`, and `where` carry the rewrite — defaulting to a period creates staccato fragmentation that reads worse than the original. ## Changes | File | Description | | ---- | ----------- | | `docs/design/slash/commands.md` | "Nine built-ins" → eleven; `/rename` and `/resume` added to the inline list and Per-Command notes; antithesis decision titles softened; Sources alphabetized with full paths. | | `docs/design/slash/modals.md` | `ModalKey` rewritten semantically (now-stale 3-of-4 Rust block dropped); Per-Modal notes added for `/rename` editor and `/resume` picker; Decisions 1, 7, 8 rewritten without antithesis; em-dash chains turned into transition-word sentences; Sources alphabetized. | | `docs/design/slash/resume.md` | `SearchableList` / `SessionRow` descriptions updated for the multi-line render and current field set (`message_count`, `git_branch`, `project`); decision titles softened; full repo paths in body links replaced with concept-level pointers; Sources alphabetized. | | `docs/design/session/file-tracking.md` | "xxh64, not SHA-256" rewritten in positive form; Sources alphabetized. | | `docs/design/session/persistence.md` | Em-dash chains in actor-batching and resume-sanitization paragraphs converted to conjunction-joined sentences; `WriterStatus::Pending` updated to mention the deferred-title field added in PR #72. | | `docs/design/tools/truncation.md` | `TRUNCATION_OVERHEAD` constant `50` → `80` (matches `tool.rs`); "Head-tail, not tail-only" rewritten in positive form; Sources alphabetized. | | `docs/design/tui/cancellation.md` | Status hints `Streaming . Esc` / `Running {tool} . Esc` → middot `·` matching `status.rs`; Decision 6's antithesis rewritten; em-dash-as-connector cases swapped for `since` / `because` clauses; Sources alphabetized. | | `docs/design/tui/overview.md` | Dropped fictional `trait Component` pseudocode (no such trait exists); "11 named color slots" replaced with a slot-family description (the actual count is 30+ accessors); the staccato streaming-markdown paragraph combined into one cohesive sentence. | | `docs/design/tui/welcome.md` | "9 entries" → 11 (full registry size); "8-entry STARTER_POOL / TIP_POOL" → 9-entry; antithesis decision titles softened; em-dash chain in the live-feeds Out-of-Scope item turned into a conjunction; Sources alphabetized. | | `docs/guide/configuration.md` | Default `model` cell `claude-opus-4-7` → `claude-opus-4-7[1m]` (matches `DEFAULT_MODEL`) in both the `[client]` table and the env-var table; "opt-in rather than automatic" rewritten as "you have to opt in explicitly"; OAuth-paragraph "matches Claude Code" implementation leak trimmed. | | `docs/guide/instructions.md` | "More specific locations override broader ones" softened to "files closer to your working directory appear later in the prompt and conventionally take precedence", which matches the actual concatenate-in-walk-order behaviour. | | `docs/guide/sessions.md` | Mid-session resume description loses the implementation-internal "load + sanitize pipeline" phrase; `/rename` interaction with the AI title generator added to the Titles section. | | `docs/guide/slash-commands.md` | Theme bullet's comma-spliced enumeration restructured; `/resume` description's em-dash run split; persistence-stance em-dash dropped. | | `docs/guide/theming.md` | `Color::Reset` (internal Rust type) → user-facing `reset`; relative-paths antithesis sentence rephrased; "same routing applies in both modes" empty restatement dropped. | | `docs/research/api/anthropic.md` | Per-model beta-set body trimmed of one staircase-narration sentence; em-dash chain on first-party-vs-3P fingerprint paragraph rewritten with conjunctions; "billing plumbing, not a security boundary" reworded; `prompt-caching-scope` paragraph reflows. | | `docs/research/api/extended-thinking.md` | `signature_delta` line restored to a clean parenthetical; credential-rotation em-dash chain split. | | `docs/research/api/system-prompt.md` | "absent → default (org-scoped) ephemeral cache. Universally accepted" tightened; org-default rationale's three-clause stack rewritten as a colon-introduced list. | | `docs/research/slash/commands.md` | Comparison table `oxide-code` `Variants: 9` → `11`. | | `docs/roadmap.md` | "and bash output" dropped from rich-tool-views (bash uses the fallback view); silent-merge "Rejects Claude Code's …" parenthetical inlined; status-bar follow-up sentence flow tightened. | ## Test plan - [x] `pnpm spellcheck` — clean - [x] `pnpm lint` — clean - [x] All Sources entries verified to exist under `crates/oxide-code/src/` - [x] All count-bearing claims verified against source (`BUILT_INS = 11`, `STARTER_POOL.len() = 9`, `TIP_POOL.len() = 9`, `TRUNCATION_OVERHEAD = 80`, `DEFAULT_MODEL = "claude-opus-4-7[1m]"`) - [x] All `[link](path)` cross-doc references resolve
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bundles the
/resumepicker follow-ups with a/renameslash command and a chat-input chrome simplification./renamelets you set a session title manually — bare opens a single-line modal pre-filled with the current title;/rename <title>applies directly. After a manual rename, the background AI title generator no-ops for the rest of the session so a slow Haiku response can't overwrite the user's pick. Renames before the first prompt defer the on-disk write: the JSONL file isn't created until a real message lands, so a session that's only renamed (then quit) leaves nothing behind, and one that does have a header-only file on disk can still be resumed.Two follow-ups land alongside it:
/resumepicker's secondary text (project / id / time) was nearly indistinguishable fromdim. Brighten themutedslot across all five built-in themes so the muted / dim / text triplet reads as three distinct steps.[id]ghost text stays but now sits one column after the cursor, mirroringratatui_textarea's built-in placeholder layout. Net 74 fewer lines ininput.rsplus thehas_queued/sync_input_queue_hintplumbing the placeholder used to drive.The slash-command popup descriptions for the five dual-mode commands also converge on a single short verb-first phrase, since the parameter placeholder is already shown by the row's
usage()next to the name.Design decisions
/renamebefore the first prompt produces a header +Titleentry but no message. Rather than writing those out and stripping them on next resume,WriterStatus::Pending { header, deferred_title }holds them in memory and only opens the JSONL on the first non-empty flush. Empty sessions die with the actor; non-empty sessions get a single atomic flush.manual_title_setlives onSharedStateonly. The actor reads / writes it through a&SharedStateborrow (passed intoabsorb); the title generator's pre-check goes through the same flag. This closes the race where Haiku finishes between/renamearriving and the actor draining its channel, and avoids the "two correlated bools" smell of a parallel field onSessionState.try_defer_titlereturnsOption<String>, notResult<(), String>. The rejected title is routed back to the caller for normal-path queueing, not surfaced as an error.Optionsays that directly:Some(t)hands the title back,Noneparks it in the deferred slot. Avoids both the inverted-Result-reading smell and theclippy::large_enum_varianthit from holding a 272-byteEntryinWriterStatus. Last-wins is preserved (a secondSomeoverwrites the first).pre_sanitize_len > 0 && messages.is_empty(). Header +Title-only sessions sanitize down to zero messages but should still resume — the title is meaningful state. Foldingdata.title.is_some()into thefirst_user_prompt_seencheck keeps the title alive across the next message.SlashContext::with_titleborrow overLiveSessionInfofield. The status-bar title isn't part of the persisted config snapshot — adding it toLiveSessionInfowould mean syncing two sources of truth acrossSessionRolled/Titleevents. A read-onlyOption<&str>parameter keeps the data flow obvious for the one command that needs it./rename <title>could in principle dispatch mid-turn, but routing it asMutatingmatches/clear,/init,/resume— a single deferral rule keeps the dispatcher simple and the user model consistent.description()andusage()are rendered side by side, so spelling the parameter inside the description duplicates information the row already shows. The five dual-mode commands also split between two prose templates ("Open the X picker or switch directly with/cmd <param>" vs. "Verb the noun —/cmdfor X,/cmd <param>to Y"). Land on the same shape claude-code and codex use: a single short verb-first phrase, no parameter mention, no UI-mechanism framing.muted, don't darkentext. Bumping Catppuccin fromOverlay0toSubtext0(and Material's equivalent) lifts the secondary tier without disturbing existing snapshots' text rendering.Paragraphto work aroundratatui_textareareserving col 0 for its cursor cell. Removing the placeholder altogether eliminates the hand-roll, and shifting the arg-mode hint by one column lets the OS-native cursor lead the dim text — the same layout the textarea's built-inset_placeholder_textproduces.Changes
agent/event.rsUserAction::Rename { title }.agent.rsRenamethrough the mid-turn warn-log catch-all.main.rsapply_renamehelper: persist viaset_manual_title, surface I/O failure, emitSessionTitleUpdated.session/state.rsWriterStatus::Pending { header, deferred_title } | Active(SessionWriter) | Brokenstate machine;try_defer_titlereturnsOption<String>(Some(t)routes the title to the normal queue,Noneparks it);flush_entriesreconstructsEntry::Title { source: UserProvided, updated_at: now() }at flush time whendeferred_titleis set. New// ── try_defer_title ──unit section covers Pending overwrite, Active reject, Broken reject. Resume contract onSessionState::resumedreworded as a precondition.session/handle.rsSharedState.manual_title_set: AtomicBoolis the single source of truth;SessionHandle::set_manual_titlelatches before dispatch and exposes amanual_title_setaccessor. Resume bail loosened topre_sanitize_len > 0 && data.messages.is_empty()("(likely corrupt)" parenthetical dropped — sanitize-prunes-everything is also legitimate);data.title.is_some()folds intofirst_user_prompt_seen. Tests cover the latch contract on healthy and dead-actor dispatch, plus title-only resume with title preservation across the next message.session/handle/testing.rsacks_then_dropscovers theSetManualTitleor-pattern arm.session/actor.rsabsorbnow takes&SharedState. TheSetManualTitlearm marks the shared flag, then either parks the title viatry_defer_title(returnsNone) or queues aTitle { source: UserProvided }entry (returnsSome);AppendAiTitleshort-circuits whenshared.manual_title_set();Recordpasses the same flag intoqueue_message_entries. New tests cover last-wins on twoSetManualTitles and positional ordering of header / title / message.session/title_generator.rsmanual_title_setafterparse_titleto skip the actor round-trip when the user has already renamed.slash.rsslash/context.rsSlashContext.title: Option<&str>pluswith_titleconstructor;new()delegates withNone.slash/registry.rs&RenameCmdtoBUILT_INS(alphabetical).slash/rename.rsRenameCmd(Mutating,[<title>]usage) +RenameModal(single-line input, 80-char cap, Enter / Backspace / Esc handling), plus TestBackend render tests for layout, ellipsis truncation, cursor placement, and short-area cursor bail.slash/{effort,model,theme,rename,resume}.rsusage().tui/app.rsUserAction::Renamethroughapply_action_locally; pass the live status-bar title intoSlashContext; dropsync_input_queue_hint.tui/components/status.rstitle()from#[cfg(test)]to production-visible.tui/components/input.rshas_queuedplumbing; arg-mode[id]ghost text now paints one column past the cursor.tui/theme.rs,tui/theme/loader.rs,themes/*.tomlmutedslot across the five built-in themes (CatppuccinSubtext0and Material equivalents).CLAUDE.md,README.md,docs/guide/slash-commands.md,docs/roadmap.md,docs/design/tui/cancellation.md/renameand trim the slash-commands guide; drop placeholder claims that no longer apply.Test plan
cargo buildcompiles cleanlycargo clippy --all-targets -- -D warnings— zero warningscargo test— 1811 tests passcargo llvm-cov --ignore-filename-regex 'main\.rs'— 98% line / 98% region coveragepnpm lint— Markdownpnpm spellcheck/rename,/rename foo, modal Enter on empty, Esc cancel, the AI-title race (rename mid-Haiku — manual wins, no flicker), and the empty-session round-trip (rename + quit leaves no file on disk; rename + message + quit + resume keeps the title)