feat(dispatch): remove a chosen lane from Tiled Dispatch - #625
Merged
Conversation
Tiled Dispatch's size is a single count, and shrinking by count always drops the tail (lanes.slice(0, next)). With seven lanes open and the finished agent in lane three, 7 -> 6 removes lane seven and leaves the user re-selecting the rest by hand. Closing that agent instead does not shrink anything either: the lane empties and buildAutoLanes' auto-fill re-homes another agent into it, so the count stays put. There was no way at all to reclaim a slot at a position of the user's choosing. Two commands rather than one with a flag. The default is destructive, and a command that sometimes ends a session and sometimes does not is the kind of thing that surprises someone moving fast: Close Agent and Remove Lane — closes the agent, then removes its lane Remove Lane — lane goes, agent keeps running The titles carry the difference. `Close` is this catalog's established verb for ending a session (Close Focused Session, Close Tab, Close Old Agents), so the destructive one leads with it; putting "Remove Lane" first would bury the irreversible half. "Remove Agent" was rejected outright — it reads non-destructive and is not. The destructive command closes FIRST and only splices if the close actually happened. closeSession runs its own confirmation for irreversible closes, so splicing first would shrink the grid while the user was still deciding, and a declined confirm would leave the layout changed with the agent alive. That required closeSession to report whether it closed, so it now returns Promise<boolean>; the two orchestration call sites adapt to their Promise<void> contract locally rather than widening a cross-process type for one caller. Lane 0 is a real agent lane that simply has no mini-list of its own — it is selected from the full index — so removing it promotes lane 1 into that role. No special-casing, but the descriptions say so rather than pretending the grid is homogeneous. The splice/clamp/ratio rules live in a pure removeLaneFromTiled so they are testable without a hook. Note the focus rule is not a plain clamp: a lane removed BEFORE the focused one shifts indices down, so holding focusedLane constant would silently move focus to the next agent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both returned NON-BLOCKING, and the mechanics came back clean: every closeSession exit path reports correctly, the orchestration wrappers preserve rejection/options/timing, all six focus combinations are right, and the cross-await race I most suspected does not exist — clearTiledLaneSessions only maps selectedSessionId and never touches lanes.length or order, so the captured laneIndex stays valid. Four real findings fixed. ratios was being dropped wholesale, and index 0 is not a lane boundary — it is the index-SIDEBAR fraction, with only ratios.slice(1) as lane weights. So removing a lane also snapped a deliberately-dragged sidebar back to its default. Copying setTiledLaneCount was wrong here: a count increase has no honest answer because it would have to invent a weight for a new lane, but a removal does. Now keeps ratios[0], drops the removed lane's weight, and yields exactly lanes.length weights so normalizedLaneWeights does not discard them. removeLaneFromTiled had been inserted between buildAutoLanes' doc block and buildAutoLanes itself, orphaning a load-bearing comment onto the wrong symbol. Moved below. The destructive command's guard tested that a lane's session id was SET, not that it resolved. A lane holds a dead id for the render between a session disappearing and the heal effect clearing it, so the command could be admitted and then do neither of the two things its title promises. Now tests liveness. "remove agent" matched neither command — the fuzzy matcher works over title and keywords only, and the subsequence fails against both titles while no keyword carried both tokens. That is the phrase a user types for the destructive one. Added as a keyword. Rejecting "Remove Agent" as a title was right; leaving it unreachable was not. Also: this PR had introduced "grid" as a third noun for one concept alongside "tiles" and "lanes", in a Dispatch command, where the style doc says Dispatch is deliberately not a grid. Now "layout". Two focus combinations were untested — including the most common real invocation, where neither the adjust nor the clamp fires — plus a test that never asserted the lane list. catalog.test.ts's header still claimed a 102-id after-state. NOT fixed, deliberately: an orchestrating agent whose close is declined is told the child closed. requestCloseConfirmation resolves false rather than throwing, so the result builders report success unconditionally and skippedSessionIds stays empty. The Promise<boolean> added here is the missing half of that fix, but threading it changes what a cross-process caller is told and deserves its own review. The comment claiming the MCP layer has no use for the signal was false and now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Adds two commands so a Tiled Dispatch slot can be reclaimed at a position of your choosing.
The problem
Tiled Dispatch's size is a single count, and shrinking by count always drops the tail:
With seven lanes open and the finished agent in lane 3, going 7 → 6 removes lane 7. You then re-select several lanes by hand to get back to the arrangement you wanted.
The obvious workaround doesn't work either. Closing the agent in lane 3 doesn't shrink the grid —
clearTiledLaneSessionsempties that lane and auto-fill re-homes another agent into it. Count stays 7.So there was no way to shrink the tiled grid at a chosen position.
Two commands, not one flag
The default is destructive, and a command that sometimes ends a session is the kind of thing that surprises you at speed. The titles carry the difference:
Closeis this catalog's established verb for ending a session (Close Focused Session,Close Tab,Close Old Agents), so the destructive one leads with it. Putting "Remove Lane" first would bury the irreversible half, and "Remove Agent" was rejected outright — it reads non-destructive and isn't.Close first, splice second
closeSessionruns its own confirmation for irreversible closes. Splicing the lane first would shrink the grid while you were still deciding, and a declined confirm would leave the layout changed with the agent alive — the worst of both.That required
closeSessionto report whether it actually closed, so it now returnsPromise<boolean>. The two orchestration call sites adapt locally to theirPromise<void>contract rather than widening a cross-process type for one caller.Details worth a look
focusedLaneconstant would silently move focus to the next agent. Adjusted explicitly, then clamped.ratiosare dropped on removal, matchingsetTiledLaneCount— they're positional and a removal invalidates them.Tests
tiledLaneRemoval.test.ts— 8 cases over the pureremoveLaneFromTiled: middle removal keeps neighbours, focus follows its agent when an earlier lane goes, clamps when the last lane was focused, refuses at the floor and on bad indices, always drops ratios, never mutates its input.Verification
npx tsc -b→ exit 0NODE_ENV=test npx vitest run→ 255 files, 1758 tests passingnpm run check:keybindings→ OKnpm run test:contract→ satisfiedNot verified: appearance and the live close-confirm flow — I haven't run the app.
🤖 Generated with Claude Code