feat(workspace): VS Code-style split panes, edge-to-edge session view
The console splits into up to four panes: a binary split tree of tmux slots (client/src/lib/paneLayout.ts) drawn by PaneGrid.tsx, every pane an absolutely placed sibling keyed by slot so splitting, swapping, resizing or closing never remounts a live terminal. Ctrl/Cmd+click on a tab, a + item or a sidebar session row opens it beside the focused pane (router state, never the URL); a pane header drags onto another pane's edge to split there or onto its middle to swap, picks another live session, splits right/down (a not-shown session or a new terminal), or closes the pane only. Dividers drag or take arrow keys. ?run= is the focused pane: every layout change goes through change(), which pushes the URL with focusOnly instead of an effect watching the focus (that ping-ponged with open requests). The layout is kept per workspace in localStorage. Only the focused pane's terminal takes the keyboard when it appears, and the keyboard follows a focus change. A pane closes when its terminal reports the exit; the run list prunes only a remembered layout on a workspace's first read and a just-opened slot that never got listed, so one empty read cannot wipe a split. A run ending on the home or a replay closes its pane without leaving the page. The session view now fills the page edge to edge: no page padding, the tab bar and the right panel's activity bar one h-10 strip, and the tab bar, pane headers and right panel on the sidebar/status bar colour (surface-1) around terminals on the page colour.
This commit is contained in:
@@ -984,7 +984,7 @@ export function LaneChangesPanel({
|
||||
<div data-testid="lane-changes-browse">
|
||||
{/* Pinned: the finder is what you reach for after scrolling a
|
||||
long tree, not only from its top. */}
|
||||
<div className="sticky top-0 z-10 flex items-center gap-1.5 bg-surface-0 pb-2">
|
||||
<div className="sticky top-0 z-10 flex items-center gap-1.5 bg-surface-1 pb-2">
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
@@ -1320,7 +1320,7 @@ export function LaneChangesPanel({
|
||||
{(stagedCount > 0 || toStage.length > 0) && (
|
||||
<div
|
||||
data-testid="lane-changes-actions"
|
||||
className="sticky top-0 z-10 mb-1.5 flex items-center gap-1.5 border-b border-border-light bg-surface-0 pb-2"
|
||||
className="sticky top-0 z-10 mb-1.5 flex items-center gap-1.5 border-b border-border-light bg-surface-1 pb-2"
|
||||
>
|
||||
{/* ONE row: message, commit, then the two bulk index moves. */}
|
||||
{stagedCount > 0 && (
|
||||
@@ -1619,7 +1619,7 @@ export function LaneChangesPanel({
|
||||
: t("changes.collapseFile", { path: f.path })
|
||||
}
|
||||
data-sticky-top=""
|
||||
className={`sticky top-0 z-10 flex cursor-pointer items-baseline gap-1.5 rounded-t bg-surface-1 px-2 py-1 hover:bg-surface-2 ${
|
||||
className={`sticky top-0 z-10 flex cursor-pointer items-baseline gap-1.5 rounded-t bg-surface-2 px-2 py-1 hover:bg-surface-3 ${
|
||||
shut ? "" : "border-b border-border-light"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
* renames it and its hover button archives it (`PATCH {name}` / `{archived}`,
|
||||
* which deletes nothing). A session with a LIVE pane (a `GET /api/run` handle
|
||||
* carrying its session id) opens that tmux pane on the Workspace page
|
||||
* (`?lane=<id>&run=<runId>`, its tab in the session view) — attaching, never
|
||||
* starting or resuming anything;
|
||||
* (`?lane=<id>&run=<runId>`, its tab in the session view; Ctrl/Cmd+click opens
|
||||
* it in a split beside the focused pane) — attaching, never starting or
|
||||
* resuming anything;
|
||||
* any other session opens in its workspace too (`?lane=<id>&session=<id>`,
|
||||
* `SessionReplay`): its conversation with a prompt box, resumed only when a
|
||||
* message is SENT, because a resume re-reads the whole transcript into context.
|
||||
@@ -314,11 +315,14 @@ export function WorkspaceSessionList({
|
||||
type="button"
|
||||
data-testid={`workspace-session-${s.id}`}
|
||||
data-live={run ? "true" : undefined}
|
||||
onClick={() =>
|
||||
onClick={(e) =>
|
||||
navigate(
|
||||
run
|
||||
? `/workspace?lane=${lane.id}&run=${encodeURIComponent(run.id)}`
|
||||
: `/workspace?lane=${lane.id}&session=${encodeURIComponent(s.id)}`
|
||||
: `/workspace?lane=${lane.id}&session=${encodeURIComponent(s.id)}`,
|
||||
// Ctrl/Cmd+click a live one: open it in a split beside the
|
||||
// focused pane (Workspace reads this state, not the URL).
|
||||
run && (e.ctrlKey || e.metaKey) ? { state: { split: true } } : undefined
|
||||
)
|
||||
}
|
||||
onDoubleClick={() => {
|
||||
|
||||
Reference in New Issue
Block a user