Skip to content

bug(dispatch): terminals created from Dispatch pin to the top of the session list instead of the natural flow #671

Closed

Description

@Juliusolsson05

Problem

In Dispatch Mode — most visibly in Tiled Dispatch — a terminal created from Dispatch always appears at the top of its project group in the session index, above every agent, no matter when it was created. Agents created from the same surface land in creation order at the bottom of the group, where you expect a new thing to appear.

The result is that the one row class you did not just create is the one that grabs the top slot, and the index stops reading as a chronological list of what you have open.

Expected behavior

A terminal created from Dispatch takes the same position an agent created from Dispatch would take: appended in creation order, in the natural flow of the list. Terminal and agent creation from Dispatch should be one flow, not two.

Actual behavior

Terminal rows sort above all agent rows in their project group, permanently.

Reproduction

  1. Enter Dispatch Mode (classic or Tiled) on a project with one grid agent.
  2. Create two or three Dispatch agents (⌥D / the project header "+"). They append below, in creation order.
  3. Create a terminal (⌥T, or the New Agent overlay → Terminal).
  4. The terminal row renders at/near the top of the project group, above every agent — including agents created before it.

Root cause

splitFocused (src/renderer/src/workspace/hook/actions/pane.ts) has two separate Dispatch branches:

  • dispatchMode && kind !== 'terminal' (line ~374) — creates a detached dispatch session (detachedSessions[id], detachedAt: Date.now()).
  • dispatchMode && kind === 'terminal' (line ~450) — inserts a grid leaf via splitLeaf into the owning tab's tile tree.

buildDispatchGroups (src/renderer/src/workspace/dispatch/dispatchSelectors.ts:72-75) builds each project group as [...gridSessionIds, ...detachedSessionIds] — grid leaves unconditionally before detached rows. Since every Dispatch-created agent is detached and every Dispatch-created terminal is a grid leaf, terminals are structurally guaranteed to sort above agents.

The position is not even stable within the grid: resolveDispatchTerminalSplitTarget picks targetLeafId ?? focusedLeafId ?? leafIds[0] as the split anchor. The focused Dispatch row is normally a detached agent with no grid leaf, so the anchor degrades to leafIds[0] — the terminal is spliced beside the first grid leaf, which is what makes it read as "pinned to the top".

The WHY terminals still enter the grid from Dispatch comment at pane.ts:~488 justifies this with tmux name, resize lifecycle, undo/close history, and persistence. Each of those is session-scoped, not grid-scoped, and detached terminals are already a supported state elsewhere:

  • detachFocusedToDispatch has never excluded terminals.
  • renderWorkspaceLeaf renders kind === 'terminal' in a Dispatch lane already.
  • TerminalLeaf wakes its own PTY on mount via ensureSessionLive('terminal-leaf.mount'), and ensureSessionLive passes recoverTmuxName for terminals.
  • undoClose already gates recoverTmuxName on kind for detached entries, with a comment anticipating exactly this change.
  • findTerminalSessionInTab's own comment states the project terminal "is allowed to be detached into Dispatch now".

Impact

User-visible ordering defect on the primary multi-agent scanning surface. It also makes the A1/A2/A3 dispatch coordinates shift under the user whenever a terminal is created, because globalIndex is assigned in final render order.

Adjacent dead code found while tracing

  • findTerminalSessionInTab (dispatchSelectors.ts:224) — only caller is the function below.
  • findTerminalInLatestTab (hook/actions/dispatch.ts:389) — defined, never called.

Both are leftovers from the removed Dispatch project-terminal column.

Acceptance criteria

  • A terminal created from Dispatch is a detached dispatch session filed under the same resolved project as an agent would be.
  • It renders in creation order in the session index, not above the agents.
  • The Dispatch: new project terminal uses stale active tab instead of focused lane #366 invariant holds: project tab and cwd both come from the focused lane / focused Dispatch row, never from a stale activeTabId.
  • Tiled Dispatch places the new terminal in the focused lane, exactly as a new agent does.
  • The two Dispatch creation branches in splitFocused are merged into one.
  • Regression coverage asserts the ordering, not just the target resolution.

Related: #152, #202, #248, #266, #366.

Activity

  1. Juliusolsson05 commented on Aug 28, 2026

    @Juliusolsson05
    OwnerAuthor

    Correction to the root-cause section above, found during review of #672.

    The original analysis claimed the terminal was pinned to the top because resolveDispatchTerminalSplitTarget's anchor degraded to leafIds[0]. That is wrong. The anchor is targetLeafId ?? focusedLeafId ?? leafIds[0], and focusedLeafId is tab.focusedSessionId — which focusDispatchSession deliberately never writes (its own comment says so). So it stays a real grid leaf and the leafIds[0] fallback is effectively unreachable. splitLeaf also inserts after its anchor, so a Dispatch terminal could not occupy the first leaf position even if it were reached.

    Verified by executing the pre-fix path: with grid [g1,g2,g3] and Dispatch focus on a detached agent, the terminal lands at the end of the grid slice, not the start.

    The real cause is the [...gridSessionIds, ...detachedSessionIds] concatenation in buildDispatchGroups, on its own and entirely. The terminal's position within the grid slice never mattered, because the whole grid slice precedes the detached rows every Dispatch agent lives in. The reported symptom is the same either way; the mechanism named in the issue was not.

    Two further corrections:

    • Repro step 4 is not a real route. "New Agent overlay → Terminal" cannot happen: kindOnly is true whenever Dispatch is active, which filters the kind picker down to agent providers, so Terminal is never offered there. The live routes are ⌥T / ⇧⌥T and the two palette rows.
    • Acceptance criteria addition. Closing a Dispatch terminal must remain undoable. Moving terminals onto the detached path removed the undo entry that the grid path used to capture, and because a closed terminal's tmux session is reaped as an orphan on the next launch, losing that entry means losing the scrollback permanently. Tracked and fixed in fix(dispatch): file Dispatch terminals as detached rows in the natural flow #672.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions