mecatui: no-op model/effort re-selection still recreates the session; switch predecessors pile up indistinguishably in the sessions browser
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Bug description
Two related problems around model/effort switching in mecatui, one a plain
defect, one a UX consequence of designed behavior:
1. Re-selecting the CURRENT model (a no-op pick) still destroys and recreates
the session. cmd/mecatui/ui/models.go (chooseModel) applies every picker
choice unconditionally:
func (m Model) chooseModel(sel client.ModelSelection, label string) (tea.Model, tea.Cmd, bool) {
...
if m.sessionID != "" {
return m.restartOnModelWithCarryover(sel)
}
return m.restartOnModel(sel)
}
There is no comparison against the session's current resolved selection, so
picking the model you are already on ends any in-flight run (endRun), drives
phaseConnecting, runs the whole CreateSessionWithCarryover handoff, rebinds
to a NEW session id, and leaves the closed predecessor behind in the stored
sessions list — churn with zero benefit. The same applies to confirming the
current effort.
2. Every switch leaves the predecessor as an indistinguishable sibling in
mecatui sessions. A model switch (CreateSessionWithCarryover) and an
effort switch (ForkSession per ADR 0068) both mint a new session id and
close the source — but closing keeps the snapshot (deliberately, for
resumability), so the browser accumulates one row per switch: same title, same
conversation prefix, no marker that a row was superseded by a carryover/fork
successor, and no lineage linking them. After a session with a few model/effort
changes, the list reads as a pile of duplicate conversations and the operator
cannot tell which row is the live continuation (this is how the behavior was
discovered: "changing the model — or even selecting the same model — creates a
new session with a new session id, visible in mecatui sessions; same when
changing effort").
For scoping honesty: the new-id-per-switch itself is DESIGNED — provider/model
are fixed per session (ADR 0016, "Provider is FIXED per session") and effort
change is deliberately fork-resume (ADR 0068) — so this issue does NOT ask for
in-place mutation. Point 1 is a defect (a no-op should not churn); point 2 is
the missing supersession/lineage affordance that would make the designed
behavior legible.
Steps to reproduce
- Start mecatui, run a prompt or two.
- Open
/models, select the model the session already uses (or/effortand
confirm the current effort). - Observe: "switching model — adopting server transcript…", a new session id
(header/tab handle changes), and the old id now listed in
mecatui sessionsalongside the new one with the same title. - Repeat a few times; the sessions list grows by one same-titled row per pick.
Expected behavior
- Selecting the already-active model+effort is a no-op with a status note
("already on "), preserving the session id and any queued state. - Superseded predecessors are distinguishable in the sessions browser (e.g. a
"superseded by " annotation, grouping, or a filter), so a
conversation's carryover/fork chain reads as one lineage rather than
duplicates.
Actual behavior
- A no-op pick destroys a working session, creates a new one, and changes the
session identity (tab title handle,/sessiondetails, ask-id prefix). - The sessions browser shows every generation as an unrelated same-titled row.
Environment
- mecatl @
ebb14c78, mecatui embedded engine, jsonlstore, macOS 26.5.1
Additional context
- Fix-shape for (1): short-circuit in
chooseModel/switchEffortwhensel
matches the session's currentresolvedSessionModel(provider id + model id- reasoning effort — compare the RESOLVED echo, not the raw selector, so an
alias resolving to the current model also no-ops). Guarded by a reducer test
asserting noCreateSessionWithCarryover/ForkSessioncall and an unchanged
session id.
- reasoning effort — compare the RESOLVED echo, not the raw selector, so an
- For (2), the pieces mostly exist server-side: the carryover/fork RPCs know
the source session at creation time, so asuperseded_by/forked_from
annotation (or even client-side: the switch reducer knows both ids at
modelSwitchReadyMsgtime and could stamp the source row) would let the
sessions surface group or badge the chain. No position on which side owns it. - Related detail:
restartOnModelCmd's comment already acknowledges the
orphaning trade ("orphaning a server-side session is preferable to blocking
the re-create") — (2) is about making that orphan population legible, not
about preventing it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in cmd/mecatui/ui/models.go at chooseModel, switchEffort, restartOnModelCmd, and modelSwitchReadyMsg; trace the resolved session selection and switch reducer tests. Verify the no-op path preserves the session id and avoids carryover or fork calls. Then inspect the sessions browser and the existing switch RPC flow to determine how predecessor and successor lineage can be surfaced, with tests for both behaviors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100