design(desktop): unify Regenerate, Branch, and Edit-and-resend into one retry-from-turn model
- Dominant language
- TypeScript
- Stars
- 5.4k
- Forks
- 502
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 715
Description
## Problem
A settled turn exposes three ways to "go back and try again", and they do not share a model. Each one lands in a different place, keeps a different kind of history, and enforces a different set of rules. The seams between them are where users get stuck.
Current behaviour, verified against `main` (`8b3655a17`):
| Entry point | Host operation | Where the result lands | What the model sees |
|---|---|---|---|
| Assistant footer **Regenerate** | `turn.regenerate` ([interactive-turn-coordinator.ts:191](https://github.com/apache/maka/blob/main/packages/runtime-host/src/server/interactive-turn-coordinator.ts#L191)) | A **new turn appended to the same Session**, tagged `regeneratedFromTurnId` | The full linear history, including the answer being regenerated. `model-history.ts` has no lineage-aware exclusion. |
| Assistant footer **Branch** | `session.branch.create`, slice mode `through` ([session-revision-coordinator.ts:364](https://github.com/apache/maka/blob/main/packages/runtime-host/src/server/session-revision-coordinator.ts#L364)) | A **new Session** copied up to and including the turn | Clean copy |
| User message **Edit** (pencil) | `session.revision.create`, slice mode `before`, then a normal send ([app-shell-revision-actions.ts:75](https://github.com/apache/maka/blob/main/apps/desktop/src/renderer/app-shell-revision-actions.ts#L75)) | A **new Session** copied up to but excluding the turn; the TUI `/rewind` uses the same operation | Clean copy |
Concrete symptoms:
1. **Regenerate is not a fork, it is "ask the same thing again in the same conversation".** The old answer stays in the transcript and in the model context, so the model is told the user asked twice. The UI stacks both turns and links them with "regenerated from / regenerated to" badges. Users who expected a replacement or a branch read this as the app re-sending their message.
2. **Sending an unchanged edit is refused and redirected to a different feature.** [app-shell.tsx:1747](https://github.com/apache/maka/blob/main/apps/desktop/src/renderer/app-shell.tsx#L1747) blocks the send with "Nothing changed. Use Regenerate if you only want a new answer." The user is already in a flow the toast described as "rewound to before that message", and is now told to leave it and use a button with different semantics (same Session, old answer kept). #2372 recorded one shape of this dead end where Regenerate was not even rendered.
3. **Rewind has no entry point of its own; it is hidden inside Edit.** Clicking the pencil only refills the composer. The revision copy is created lazily at send time, which is why the desktop shell carries a `reserved / started / abandoning` copy-attempt state machine and a rollback path ([app-shell-revision-actions.ts:278-407](https://github.com/apache/maka/blob/main/apps/desktop/src/renderer/app-shell-revision-actions.ts#L278)) whose whole job is to make "nothing happened yet" survive cancel, navigation and send failures.
4. **Branch and Edit create two differently-named kinds of Session.** Branch records `branchOfTurnId`; Edit records a revision family (`revisionRootSessionId`, `revisionParentSessionId`, `revisionIndex`). The sidebar shows "branch" tasks and "version N" tasks for what the user experiences as one action: continue from an earlier point.
5. **Edit eligibility is a pile of independent guards** (attachments, directory references, quotes, transformed skill text, slash commands, another edit in progress), each surfaced as a separate toast ([chat-turn.tsx:583-601](https://github.com/apache/maka/blob/main/packages/ui/src/chat-turn.tsx#L583)). #5109 / #5118 are already peeling one of them off, which shows they are patches rather than one contract.
6. **Footer availability is derived from `TurnStatus` only** ([turn-footer-actions.ts](https://github.com/apache/maka/blob/main/apps/desktop/src/renderer/turn-footer-actions.ts)); the Host then rejects with `session_busy` when the Session has a live root turn. The user learns the action is unavailable only after clicking.
The root of all six is that **"go back to before turn T and submit again, with the text either changed or unchanged"** is one operation, but it is implemented as two entry points (Regenerate, Edit) with two storage semantics (append in place vs. revision copy) and two rule sets. The "unchanged text" boundary falls exactly in the gap between them.
## Desired outcome
One decision on the model, recorded here before any implementation:
- **One primitive for "retry from T".** Regenerate becomes the unchanged-text case of the same operation Edit uses, so both produce the same kind of history and the old answer is never in the model context of the new attempt. Whether that operation lands in a new Session (today's revision copy) or as a hidden sibling in the same Session is the design question to settle; either is acceptable, both at once is not.
- **The unchanged-text guard goes away.** Sending the same text from an edit draft is simply a regenerate.
- **Branch stays as the one "continue from here in a new Session, keep the answer" action**, and Branch and revision copies share one lineage vocabulary in the catalog and sidebar, or the two are explicitly documented as different products with different names.
- **Eligibility is one contract, stated once**, ideally by the Host (`operation_unavailable` / `session_busy` reasons) and reflected in the footer before the click, not a set of renderer-side toasts.
Non-goals for this issue: any change to what Branch copies, side conversations (`/side`), or the Agent Graph revision history in #2039.
## Alternatives or workarounds
- Keep three entry points but rename them honestly ("Ask again", "Branch", "Edit") and drop the unchanged-text redirect. This removes the worst confusion but leaves the double storage model and the duplicated answer in the model context.
- Keep Regenerate as an in-Session sibling but exclude the superseded turn from `model-history` and collapse it in the transcript. This fixes the context problem without touching the revision path, at the cost of a third lineage rule.
Related: #2372, #5109, #5118, #4994, #1287 (the PR that introduced edit-and-resend as a revision copy).
Contributor guide
Assessment
This issue has not been assessed yet.