f4be0d279cc0e81eb06b48d79a4e010563126691
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4be0d279c | fix: dockview, sprite, auth, and CLI db backup improvements (#1075) | ||
|
|
3bc092bf20 |
feat(terminal): first-class user terminals — stable seq, rename, park/resume (#1009)
* feat(terminal): persist user terminals with stable seq + park state
Ordinary user shells now have a DB-backed `user_terminals` row keyed by
the agentctl PTY id, carrying a per-task monotonic seq (gaps preserved on
delete), custom_name override, and open/parked state. The terminal
service owns the create/list/rename/park/resume/destroy surface and
guards reject `bottom-panel` (cmd+J) + `script-*` ids so passthrough
flows are untouched. Closing a tab now parks (PTY keeps running);
destroy kills the PTY and removes the row.
* feat(web): expose terminal seq badge, rename, and parked-terminals menu
Wires the right-panel strip and SSR page state to the new task-keyed
terminals endpoint and discriminated UserShell union, adds the `#N`
badge for ordinary terminals, plumbs context-menu / double-click rename,
and renders a "Parked" submenu so closed-but-running terminals can be
resumed. Closing a managed tab now parks instead of destroying so the
PTY (and its buffer) survives accidental closes.
* test(terminal): e2e coverage for first-class persistent entities
Covers the persistence guarantees the user explicitly called out:
monotonic seqs, refresh-survives-rename-and-park, gap-preserving destroy,
park+resume cycle, and bottom-panel/script guards. Exercises the new
user_shell.{create,list,rename,park,resume,destroy} WS actions through
the shared ApiClient.wsRequest helper.
* fix(terminal): atomic seq allocation + propagate destroy errors
- Repository.Create folds MAX(seq) read + INSERT into a single
`INSERT … SELECT` statement so concurrent creates can no longer pick
the same seq. Locked in with TestCreate_ConcurrentSeqAllocation (25
parallel creates, all unique).
- wsUserShellStop now propagates the service-Destroy error so the
frontend stops optimistically removing a row the backend failed to
delete.
- Service.Destroy / CleanupTask log PTY-stop failures at Warn (instead
of silently swallowing) while keeping the proceed-on-failure semantics
intentional — the DB row is the source of truth for "user expects
this terminal gone".
- IsUserShellAlive copies ProcessID under the read lock before calling
r.Get(), removing the post-unlock pointer-deref race.
- Get-not-found test asserts errors.Is(err, ErrNotFound) so it locks to
the repository contract.
* fix(web): close-tab waits for backend ack + className undefined guard
- handleCloseTab defers `removeTerminal` into the `.then()` of the
park/destroy promise. A failed backend call leaves the tab visible
rather than vanishing it optimistically; the next `user_shell.list`
poll reconciles state.
- session-tabs guards `tab.className` with `?? ""` so an undefined value
no longer produces "undefined group relative..." in the rendered
class string.
- user-shells.test simplification: `createSessionRuntimeSlice` only
consumes `set`, so the test passes the slice creator straight to
`immer()` instead of wrapping it in an adapter with unused args.
* fix(terminal): task-scoped mutations + legacy-shell list + UI hardening
Round-2 review feedback (cubic P1/P2, claude, coderabbit):
Backend:
- Rename/Park/Resume/Destroy now require a task_id and verify the
terminal belongs to that task via a shared requireOwnership helper.
Cross-task mutations return ErrTaskMismatch instead of silently
succeeding by raw terminal id.
- wsUserShellList no longer drops managed-shaped agentctl shells when
the terminal service is disabled or task_id is missing — legacy
passthrough callers see their existing shells again. `managedCovered`
flag scopes the orphan filter to the DB-backed path.
- service_test.go uses the shared-cache in-memory SQLite + MaxOpenConns(1)
setup, matching repository_test.go and removing the flake-one-race-away
warning.
- New cross-task guard tests: TestRename_RejectsCrossTask,
TestPark_RejectsCrossTask, TestDestroy_RejectsCrossTask, plus a
TestRename_EmptyTaskIDSkipsOwnership for internal callers.
Frontend:
- updateUserShell patch type narrowed to Partial<Omit<UserShellInfo,
"terminalId">> so the row key can't be silently mutated.
- destroyUserShell now throws on a missing WS client instead of silently
resolving — handleCloseTab's .then() no longer fires prematurely.
- rename/park/resume/destroy API client + useTerminals hook plumb the
active task id through every mutating call.
E2E:
- New "cross-task mutating ops are rejected" spec creates a terminal
under task A and verifies task B can't rename/park/destroy it by raw
id.
- Existing specs updated to send task_id with their mutating calls.
* fix(terminal): require task_id on all mutators + wire taskID into hooks
Round-3 review feedback (cubic P1, claude blocker, coderabbit):
Backend:
- requireOwnership no longer skips the ownership check when taskID is
empty. The previous carve-out was reachable from the WS layer (where
task_id is user-controlled), so an attacker who guessed a terminal id
could omit task_id and mutate any row. CleanupTask walks the repo
directly and never touched the bypass.
- TestRename_EmptyTaskIDRejected pins the new contract.
Frontend (the real bug claude caught):
- useUserShells now accepts a taskId parameter and forwards it on the
user_shell.list WS call. Without it the backend's DB-backed ordinary-
shell path was skipped and the Zustand store wiped the SSR-hydrated
panel strip on the first WS fetch.
- Response mapping switched to a discriminated union: id (new field)
falls back to terminal_id (legacy), and kind/seq/state/pty_status/
display_name/custom_name flow into the store so the parked-terminals
submenu actually populates.
- useTerminals + useMobileTerminals read the active task id from the
store and pass it through to useUserShells.
- destroyUserShell now retries with user_shell.stop when the server
returns an unknown-action error — keeps the rollout-window
compatibility intact for older backends that only know the legacy
action name.
* fix(terminal): list parked, rollout-safe kind default, helper extraction
Round-4 review feedback (claude + greptile P1):
- useUserShells now sends include_parked: true so the "Parked terminals"
submenu populates after a page reload. Previously the WS list returned
only state=open rows; parked terminals were invisible until the user
parked something in the same session. The main strip still hides
parked entries via buildTerminalsFromShells.
- useAddTerminal stops blindly defaulting kind to "ordinary" — that
misclassified legacy passthrough payloads (terminal_id without seq)
during a rollout where the frontend is newer than the backend. The
managed-tab path then called parkUserShell on a server that didn't
know the action. Now only tags as ordinary when kind is explicit or
the row carries a seq.
- handleCloseTab extracted into a standalone useCloseTab + helpers
hoisted into use-terminals-build.ts / use-terminals-types.ts. Brings
use-terminals.ts back under the 600-line file limit and the
useTerminalActions wrapper inside the 100-line function limit.
* fix(terminal): scope-keyed shell cache + null-task guard + tighter rollout default
Round-5 review feedback (cubic P1, coderabbit x4):
- useUserShells now keys its fetch cache on env+task (`${env}:${task}`),
not just env. An earlier load without a task id (e.g. before the
active task hydrated) no longer pins the store on a legacy-only shell
list and blocks the task-scoped refetch.
- useMobileTerminals delays auto-create until a real taskID is
available. Previously a null taskID could bootstrap a legacy
non-persistent shell, mark the env as auto-created, and starve the
real task of its first-class terminal. Guard key is now env:task, and
releaseAutoCreatedEnvironment clears every scope on that env.
- useAddTerminal tightens the rollout check: a row is "ordinary" only
when `seq !== undefined`. A bare `kind: "ordinary"` without seq from
an older backend stays legacy so close → destroy on the first click.
- useRemoveTerminal reads the current activeTab from a ref instead of
closing over the value captured when handleCloseTab first ran. Async
park/destroy resolves after a user tab-switch no longer clobber the
newer selection with the stale fallback shift.
Tests:
- New "waits for taskID before auto-creating — null taskID is a no-op"
case in use-mobile-terminals.test.ts confirms the null-task guard.
- All existing mobile-terminal + slice + E2E tests pass.
* fix(terminal): live activeTab lookup + selectable-tab guard
Round-6 review feedback (cubic P2 x2):
- getActiveTab now reads the active tab from the Zustand store via
storeApi.getState() instead of a useRef/useEffect pair. Closes the
post-render race where an async close handler could resolve between
the new render's commit and the effect-driven ref sync.
- computeTerminalTabValue requires activeTab to be selectable
(present in `terminals` or === "commands") before honoring it. A
stale id left over from a parked/destroyed terminal no longer pins
the panel on a nonexistent tab and blocks the fallback shift to a
real shell.
* chore(terminal): inline savedTabFromStorage to drop file under line cap
Round-7 review feedback (claude suggestion): the useMemo around the
synchronous getSessionStorage call was zero-value memoisation that
pushed use-terminals.ts three lines over the 600-line max-lines limit.
Inlined; file now at 600 lines.
* chore(terminal): drop savedTabExists local + comment to land under 600 lines
Round-8 follow-up (claude): the previous commit's two-line comment block
held the file at 603 lines (still over the 600-line cap). Inline
savedTabExists at the only call site and remove the redundant comment —
the round-7 PR thread already explains why the useMemo was removed. File
now at 599 lines.
* fix(terminal): plumb task_id through dockview + tolerate legacy destroys
Manual-test feedback: clicking X on a dockview terminal panel surfaced
"destroy terminal: terminal does not belong to the supplied task" because
the dockview path was creating + destroying terminals without sending
task_id at all. The new requireOwnership guard then correctly rejected
the empty task_id against a non-empty term.TaskID.
Frontend:
- addTerminalPanel grows task_id + title parameters; stamps both into
panel params at creation time so the destroy cleanup can pass them
back to user_shell.destroy / .stop.
- Every dockview-side createUserShell call site now passes the active
task_id, so the backend creates a DB-backed ordinary terminal (with
seq + displayName) instead of a legacy passthrough shell.
- destroyUserShell + stopUserShell already accepted an optional task_id;
dockview-layout-setup.onDidRemovePanel now reads the stamped task id
from panel params and forwards it.
Backend:
- wsUserShellStop now requires both terminalSvc and a non-empty
req.TaskID before routing through the managed service. An empty
task_id falls through to the legacy passthrough StopUserShell so older
clients (and dockview panels persisted before task_id was stamped)
keep working.
Known follow-ups (separate task): the #N badge, rename context menu,
and parked-terminals dropdown still only show on the mobile/tablet
right-panel strip. Desktop dockview shows the seq inside the panel
title text but doesn't expose the badge/rename/parked-list UI yet.
* feat(terminal): dockview tab badges + rename context menu + existing-terminals list
Manual-test feedback round 2:
- Custom dockview tab component (TerminalTab) renders the #N pill
only when the active env has more than one ordinary terminal —
mirrors the session-tab badge semantics. Right-click on any ordinary
terminal tab now exposes Rename / Park / Destroy.
- Dockview + menu picks up a new "Terminals" section listing every
ordinary terminal for the active task (open and parked). Clicking a
row focuses the existing dockview panel if one is mounted, otherwise
re-opens it; parked rows are auto-resumed via user_shell.resume
before the panel is added. The base "Terminal" action was renamed
to "New Terminal" so the existing-terminals list is distinguishable
in the menu.
- Reopen menu uses a stable EMPTY_SHELLS sentinel for its store
selector to avoid Zustand infinite re-renders when the env has no
shells.
E2E:
- terminal-new-tab.spec uses getByRole("menuitem", { name: "New
Terminal", exact: true }) to disambiguate from the new "Terminals"
submenu label.
- Full terminal suite still passes (26/26).
* fix(terminal): tab title shows "Terminal" with seq as badge, populate store on session page
Re-reading the user feedback: the title text was rendering as
"Terminal 1" (backend's displayName), and the dockview "+" menu's
Terminals section was empty because nothing on desktop was calling
useUserShells to populate the store.
- TerminalTab.pickDisplayName now returns the literal "Terminal" for
ordinary terminals (custom names still override). The seq lives only
in the sibling badge — same split as session-tab where the agent
name is the title and the seq is a pill before it. Reading "Terminal
1" out of "[1] Terminal" instead of "[1] Terminal 1".
- LeftHeaderActions (dockview "+") now calls useUserShells on mount.
Without this the desktop layout never triggered the WS list fetch,
so the Terminals submenu and the badge "count > 1" check both saw
an empty store and silently disappeared.
- TerminalReopenMenuItems also calls useUserShells defensively (cheap
thanks to scope-keyed cache) so it works when mounted in isolation.
* feat(terminal): "New X" rows live inside their section labels
User feedback: move "New Agent" under the Agents section header and
"New Terminal" under the Terminals section header so the dockview "+"
menu reads top-to-bottom as Agents → New Agent → existing sessions →
Terminals → New Terminal → existing terminals → other panels.
- SessionReopenMenuItems accepts an optional onNewSession prop and
renders the "New Agent" row as the first item under the "Agents"
label. Render gate now keeps the section visible whenever the create
handler is supplied, even with zero existing sessions.
- TerminalReopenMenuItems gets a matching onNewTerminal prop with
the same behaviour ("New Terminal" row under the "Terminals" label).
- dockview-add-panel-items drops its standalone "New Agent" and "New
Terminal" rows since both now live inside their sections.
* feat(terminal): auto-migrate default panel to DB-backed ordinary terminal
The legacy `terminal-default` dockview panel was hard-wired to a
non-managed `shell-default` PTY so it never picked up an ordinary
seq/customName/parked state — the new "Terminal N" badge UX could
only show a badge once the user had explicitly created a second
terminal. With useEnsureDefaultTerminalOrdinary the default panel is
upgraded into a real `shell-<uuid>` row on session-page mount, so
every visible tab is now a first-class ordinary terminal.
Plus a UI-level e2e spec asserting the multi-terminal title + badge
contract, and an expectation bump on the existing rename+park spec
since visiting the session page now mints 1 extra ordinary terminal.
* fix(terminal): park ordinary terminals on tab close so they survive refresh
Dockview's tab X close button routed straight to stopUserShell (an
alias for destroyUserShell), wiping the DB row on close — after a
refresh the user had no way to resume a still-running PTY they had
just tucked away.
The X is now a "hide for now" affordance: ordinary terminals get
parked (PTY survives, row stays in DB, reappears in the "+" →
Terminals menu with a `parked` pill), while scripts / bottom-panel /
legacy passthrough shells stay on the destroy path because they have
no resume story.
Permanent delete still lives on the right-click Destroy menu item
and (in the next commit) on each reopen-menu row.
* fix(terminal): preserve DB-backed terminal panel ids across layout restore
The serializer's normalizePanel rewrote every user-created terminal
panel id to `terminal-saved-N` and dropped environmentId/taskID from
params, so after a reload that panel couldn't be matched back to its
live PTY or its store record — the tab rendered with no badge and a
stale fallback title.
Treat `shell-<uuid>` panel ids the same way we already treat
`terminal-default`: preserve id and params on restore. Legacy
ephemeral `terminal-saved-*` ids still mint a fresh PTY on restore.
Also force tabComponent=terminalTab and title="Terminal" for any
restored terminal panel so layouts persisted before the badge UX
land on the new presentation.
* feat(terminal): destroy button on reopen-menu rows for permanent delete
Destroy used to live only on the tab's right-click menu — useless once
the tab was already closed. Each ordinary terminal row in the dockview
"+" → Terminals submenu now ends with an × button that confirms-then
calls destroyUserShell, killing the PTY and dropping the DB row so the
terminal does not reappear after reload.
Pairs with the previous close-to-park change: closing the tab parks
(soft, resumable), the × button on the menu row destroys (hard,
permanent).
* fix(terminal): reopen-menu focuses existing tab, drops parked pill, says Terminate
Three small fixes to the dockview "+" → Terminals submenu:
1. Clicking a row whose terminal is already mounted now FOCUSES the
existing panel instead of duplicating it. The default-migrated
panel keeps its registry id `terminal-default` even after the
migration to a `shell-<uuid>` PTY, so a direct getPanel(uuid) miss
would fall through to addTerminalPanel and create a second tab for
the same PTY. Fall back to scanning `params.terminalId` so the
lookup catches that case.
2. Drop the "parked" pill from the row — parking is now an internal
detail (close = park) that the user doesn't need to read off the
menu. Clicking the row brings the terminal back into view either
way, so distinguishing it visually is noise.
3. Rename the row × tooltip + confirm copy from "Destroy" / "Permanently
delete" to "Terminate" to match the new tab-context-menu label.
Spec: add a test that asserts no duplicate tab when reopening an
already-open terminal, and drop the parked-pill assertion from the
existing close+reload test.
* feat(terminal): empty-group watermark uses the rich + menu
When a user splits a group (split-right / split-down) the new half is
empty and dockview renders the watermark. Before this, the watermark
showed a hard-coded Agent/Plan/Terminal/Browser row and clicking
"Terminal" called createUserShell without a taskId — so it spawned a
legacy passthrough shell that never appeared in the user-shell list,
and clicking "Agent" only ever focused the singleton chat panel
(invisible if the task had multiple sessions).
The watermark now renders the same AddPanelMenuItems content the
header "+" uses, so the user gets:
- The Agents list (existing sessions, greyed when already open)
- The Terminals list (existing terminals, greyed when already open)
- The Browser/Plan/VS Code/Changes/Files/PR options
…served behind a single "Add panel" dropdown trigger centered on the
empty group.
* feat(terminal): inline rename on the tab; drop Park, rename Destroy → Terminate
The tab title is now editable in place — picking Rename from the
right-click context menu swaps the title for an input. Enter commits,
Escape / blur-with-no-change cancels, and the new name is pushed
through renameUserShell + the user-shell store so the API and badge
list stay in sync. Replaces the previous window.prompt which felt
out of place in the dockview chrome.
While here:
- Drop the "Park (hide, keep PTY)" context-menu item. Tab close
already parks ordinary terminals, so the explicit Park action was
redundant and broke the "one obvious affordance per outcome" rule.
- Rename "Destroy" → "Terminate" in the context menu. Matches the
reopen-menu × tooltip and reads more like a system action than a
destructive UI primitive.
Spec adds an inline-rename test asserting the input appears, accepts
typed text, and commits the new title to the tab.
* fix(terminal): right-click Terminate closes the tab; drop displayName DOM warning
Two unrelated fixes both landing in terminal-tab.tsx:
- Terminate used to fire the WS destroy and update the store, but left
the dockview panel mounted with a dead PTY underneath. Now it
optimistically drops the shell row from the store, closes the panel
via api.close(), then sends the destroy — same effect as the X close
but routed to the hard-destroy path (the onDidRemovePanel cleanup
sees no shell entry and skips its park branch).
- TerminalTabBody typed `displayName` as a prop but never destructured
it, so {...props} spread it into DockviewDefaultTab and on into a DOM
div, triggering "React does not recognize the `displayName` prop on
a DOM element". Drop it from the spread.
* feat(terminal): polish + menu — badge-style indices, flush-left New rows, terminate closes panel
Polish pass on the dockview "+" → Agents / Terminals submenus:
- Replace the plain `#N` text with the same pill-style badge the tabs
use (small bg, rounded, 11px). Agents and terminals both pick this
up — so the menu row reads "[N] label" matching the tab style.
- Drop the empty index-column placeholder span on the "New X" rows so
the icon + label sit flush-left under the section header instead of
awkwardly indented.
- Strip the backend "Terminal {seq}" suffix from row labels — the
badge already carries the number, so showing "Terminal 1" next to
the "1" pill duplicated it. Custom-renamed terminals still show
their name.
- The row × button now also closes the dockview panel if the terminal
is currently open, mirroring the right-click → Terminate flow.
Spec: switch the row-by-seq lookups from `hasText: "#N"` (now stale
since the `#` prefix is gone) to a `has:` filter against the new
`reopen-terminal-seq-N` badge testid, and add a regression test for
right-click → Terminate closing the tab and removing the row in one
gesture.
* fix: address terminal review feedback
* fix: resolve terminal review suggestions
* fix: catch terminal close stop failures
* fix: harden terminal terminate failures
* fix: use pinned pnpm docker arg
* fix: satisfy backend lint after main merge
* fix: stabilize e2e after layout merge
|
||
|
|
f1bdc56892 |
feat(layout): roomier pane caps + remember user-set widths (#1005)
* refactor(layout): swap pane width caps for viewport-proportional runtime cap Replace the hard 350px sidebar / 450px right caps with max(800, viewport*0.7) so wide displays get a roomy file/terminal panel and the sidebar can grow with the screen. Pinned minimum is raised from 50px to 180px so panels don't collapse into unusable slivers. Caps now live in lib/state/layout-manager/caps.ts; constants module no longer exports per-column max-px constants. * feat(layout): persist preferred pane widths, flush on unload, roomier tablet split - Mirror sidebar / right widths into a sessionStorage pinned-defaults slot on every resize; seed brand-new task envs with those widths so the user's last-set sizes follow them across tasks. - Re-apply the runtime cap to every pinned group on each ResizeObserver tick so a panel pinned to the old cap grows when the user widens the window (and re-clamps when they shrink it). - Flush the debounced layout save on `beforeunload`, so a reload less than 300ms after the last drag doesn't lose the new width. - Drop the tablet right-panel inner split minSize from 30/20 to 15/15 so the files vs. terminal proportions match the loosened desktop feel. * test(e2e): cover pane resize, persistence, and edge cases Four spec files under apps/web/e2e/tests/layout/ exercise the runtime cap and pinned-defaults persistence end-to-end via dockview's `.dv-sash` drag handles and `window.__dockviewApi__`: - pane-resize-right: drag past the old 450px cap, viewport-shrink re-clamp, reload + cross-env propagation of preferred width. - pane-resize-sidebar: same coverage for the sidebar including toggle-off/on width retention. - pane-persistence-tablet: localStorage round-trip for the tablet layout keys (`task-layout-tablet-v1`, `task-layout-right-v2`). - pane-resize-edge-cases: double-click smoke, rapid drags + beforeunload flush, maximize round-trip, sash off-viewport, resize with sidebar hidden. Adds e2e/helpers/dockview-resize.ts for the shared drag + width read-out helpers. * fix(layout): cap sidebar at 30% screen width, preserve legacy initial widths - Split the runtime cap into a sidebar-specific function (max(350, vw*0.3)) and the right-pane function (max(800, vw*0.7)). File-tree / task-list content rarely benefits from more than ~30% of the screen. - Preserve legacy initial widths: getPinnedWidth now clamps the ratio-based default to the old hard limits (350 sidebar, 450 right) so a fresh task env opens at exactly the same width as before. Users can still drag past these via setConstraints — only the *initial* default is capped tightly. * fix(layout): address PR review — narrow viewport, hidden sidebar, e2e flake Bot-review fixups for #1005: - caps: bound runtime caps by `viewport - VIEWPORT_RESERVE_PX` so the 800px right-cap floor can't collapse the center column on narrow screens (Cubic P1). - applier: respect `column.maxWidth` on the sidebar too, so the `compact` preset's 260px cap is honored at runtime (Cubic P2). - dockview-store: skip the pinned-defaults mirror when the column is hidden — auto-sync infers sidebar from grid index 0, which is the center when sidebar is hidden, so an unguarded mirror would persist the center's width as the sidebar's (Greptile/Cubic P1). - trackPinnedWidths: gate the auto-sync on `sidebarVisible` / `rightPanelsVisible` for the same reason. - sizing: use `LAYOUT_RIGHT_RATIO` for non-sidebar columns instead of silently reusing the sidebar ratio (Greptile P2). - e2e: tablet viewport bumped from 1024 → 700 (≥1024 is desktop, not tablet — the old viewport never rendered the tablet layout and every assertion timed out). Tests now also verify rendered panel proportions, not just the localStorage echo (CodeRabbit #2/#3). - e2e helpers: throw on missing `.dv-sash` instead of returning `-1` (CodeRabbit #1); shared `openWideTask` extracted from three spec files (CodeRabbit #5); silent `return` in evaluate blocks replaced with `throw` for fail-fast (CodeRabbit #4). * fix(layout): swap drag-mouse e2e for programmatic resize + claude/cr followups Round 2 of PR review fixups: - e2e: convert pane-resize-right/sidebar/edge-cases specs from `page.mouse` drag-by-delta to a programmatic `splitview.resizeView()` call. The mouse-drag approach was flaky in headless CI (5 chromium failures across shards 2-4); the programmatic path tests the same cap-enforcement and persistence contract without depending on pointer-events targeting through a 1px .dv-sash overlay. - dockview-layout-setup: remove an unreachable guard in trackPinnedWidths — `rightIdx === 0 && sidebarVisible` cannot occur given the `sv.length < 2` early return (Claude). - dockview-layout-setup: cancel any in-flight debounce timer in the setupLayoutPersistence disposer so a fire can't race with teardown and persist stale layout (CodeRabbit). - constants: document why LAYOUT_SIDEBAR_RATIO and LAYOUT_RIGHT_RATIO are both 0.25 today — separate constants stay decoupled so a future tweak to one doesn't silently change the other (Claude). * fix(layout): drop cross-env width propagation — defaults stay unchanged User feedback: the initial sidebar width on a brand-new task should be the same as it was before this PR (350px on typical viewports). The plan's "propagate preferred widths across envs" feature changed that in practice: after the first resize, every new task opened at the resized width instead of the legacy default. - Remove `getPinnedDefaults` / `setPinnedDefaults` helpers and the sessionStorage `kandev.dockview.pinned-defaults` slot. - Drop the write-through in `setPinnedWidth` and the seed in `performBuildDefault`. `freshPinned` is back to `new Map()`, so a brand-new env always renders at the ratio-based default (same as before this PR, since `getPinnedWidth` already clamps the initial ratio to the legacy 350/450 hard caps). - Per-env layout persistence still works via `setEnvLayout` — when a user resizes inside a task and switches back later, the width is restored. Only the cross-env *default* is no longer carried over. - Remove e2e tests + helpers that exercised the propagation contract. - Drop the flaky `rapid resizes persist across reload` edge test — too brittle around the debounce/beforeunload race. - `LAYOUT_INITIAL_RATIO` introduced as the canonical name; `LAYOUT_SIDEBAR_RATIO` / `LAYOUT_RIGHT_RATIO` kept as deprecated aliases for backwards compatibility while sizing.ts dispatches. * fix(layout): bump saved-layout storage keys to v2 Layouts saved by intermediate builds of this PR captured sidebar/right widths beyond the legacy initial caps (when the larger runtime cap was the only thing constraining serialized widths). Restoring those layouts on the current build replays the over-wide widths, defeating the "defaults stay the same" guarantee. Bumping the storage key prefixes (`env-layout` → `env-layout-v2`, `env-maximize` → `env-maximize-v2`, `dockview-layout-v1` → `dockview-layout-v2`) invalidates legacy saves once on first open; subsequent opens persist + restore under v2 normally. Existing users lose stored column widths once and re-customize from defaults — a one-time cost in exchange for the upgrade being clean. * fix(layout): apply target env's widths after fast-path switch Fast-path env switch (`tryFastEnvSwitch`) skips `fromJSON` for speed, which means column widths carry over from the *outgoing* env. If a user resized the sidebar to 480px in task A and then opens task B, B inherits A's 480px sidebar — even with empty sessionStorage on different dev-server ports, because the widths live in dockview's in-memory grid state, not in storage. Explicitly resize sidebar/right after the panel swap: - saved layout exists for target env → use its serialized sizes - no saved layout (brand-new env) → compute defaults via `getPinnedWidth` (legacy initial caps: 350 sidebar / 450 right) * fix(layout): pass measured dims to fromJSON so widths don't grow on rescale Even on a fresh dev instance with no prior storage, brand-new tasks were opening with oversized sidebar/right widths. Root cause: applyLayout fed `api.width` into toSerializedDockview, but `api.width` lags the live container during the dockview mount sequence. fromJSON wrote a layout sized for the stale width; the subsequent `api.layout(safeWidth, ...)` in performBuildDefault's rAF rescaled the grid proportionally — and without the old hard cap (350/450) clamping it, the sidebar grew past its legacy default. Pass the measured container dimensions through applyLayout into toSerializedDockview so fromJSON's grid.width already matches the live container. api.layout in the rAF is then a no-op, no rescale, and sidebar/right open at exactly their legacy defaults. Also applies to applyBuiltInPreset and applyCustomLayout for the same flow. * fix(layout): lock pinned column max width to current size, unlock on drag The real reason brand-new tasks opened with oversized sidebar/right: dockview's splitview rebalances pinned columns proportionally on every `api.layout` call. After we relaxed setConstraints from the legacy 350/450 hard caps to viewport-proportional runtime caps (576/1344), nothing clamped the rebalance — sidebar grew from 350 → ~450, right from 450 → ~530 on the first ResizeObserver tick. New invariant: pinned-column `maximumWidth` is locked to the column's *current* width whenever the user is not actively dragging a sash. The rebalance has zero room to grow. - `applyLayout` / `applyLayoutFixups` set the lock as soon as fromJSON finishes — sidebar/right open at exactly their just-computed widths. - `setupContainerResizeSync` re-applies the lock on every viewport ResizeObserver tick before and after `api.layout`. - `setupSashDragCapToggle` listens for mousedown/mouseup on `.dv-sash`: on mousedown, widen the cap to the runtime viewport-proportional cap so the user can drag past the legacy default; on mouseup, snap the cap back to the new current width. Also addresses Claude's latest review: - caps.test.ts: rename "hits the 350px floor on roomy viewports" → "… when vw*0.3 falls below the minimum" (test name now matches assertion). - dockview-resize.ts: drop the stale "pinned-defaults mirror" comment; guard the sidebar splitview index against the hidden-sidebar footgun. - pane-resize-right.spec.ts: rename the reload test to mention the correct localStorage key (`dockview-layout-v2`). - new `dockview-env-switch-pinned.test.ts` unit-tests applyPinned- ColumnSizes with a spy splitview — verifies sidebar/right are explicitly resized after a fast-path env switch. * fix(layout): constrain all groups, loosen e2e resize cap, drop stale docs Round of PR-review fixes: - applier: `applyConstraintsToAllPanelGroups` replaces the previous first-only helper. The default right column has separate top + bottom groups; constraining only the first left the bottom group unbounded and let the column grow on rebalance via that group (cubic P2 + Claude regression). - sizing: collapse `LAYOUT_SIDEBAR_RATIO` / `LAYOUT_RIGHT_RATIO` dispatch — they alias the same `LAYOUT_INITIAL_RATIO`, so the ternary was a silent no-op (Claude). - sizing: doc — drop "widths seeded from pinned-defaults sessionStorage" reference; that feature was removed in |
||
|
|
0d3fce25b4 | fix(web): stop dockview layout corruption when switching between maximized / sessionless tasks (#838) | ||
|
|
6fa38cd62c |
fix(terminal): re-key user-shell RPCs to task_environment_id (#770)
* refactor(backend): re-key user-shell RPCs to task_environment_id user_shell.list/create/stop now take task_environment_id directly instead of session_id with a session→env translation. Sessions in the same task share one shell list, so the env is the natural scope. HTTP route /api/v1/sessions/:id/terminals → /api/v1/environments/:id/terminals. shell.status/subscribe/input (agent passthrough) stay session-keyed. * fix(backend): heal task_sessions.task_environment_id on boot Sessions created via paths that didn't write the FK left shell ops broken — every env-keyed user-shell RPC failed to resolve the env and the frontend silently swallowed the error. The boot heal links each orphan session to its task's existing env (idempotent, runs after backfillTaskEnvironments + healDuplicateTaskEnvironments). * refactor(web): drop session_id from user-shell API, hooks, and slice createUserShell/stopUserShell/fetchTerminals take environmentId. useUserShells takes envId directly (no environmentIdBySessionId translation). useTerminals takes both (env for shell ops, session for tab UX persistence). Slice actions key writes by env id directly, removing the silent no-op when the session→env mapping wasn't set yet. * refactor(web): wire env id through dockview, right-panel, and SSR call sites Dockview "+" menu, terminal scripts dropdown, dev-preview button, right-panel "+" button, Ctrl+\` toggle, watermark, and panel-close cleanup all source the env id from useEnvironmentId() / store and pass it to createUserShell/stopUserShell. Shell-mode passthrough terminal no longer blocks on session id — env id is sufficient. SSR resolves the active session's task_environment_id from the session list and calls fetchTerminals with the env id. * test(e2e): cover env-keyed terminal new-tab flows + WS payload guard New spec exercises the right-panel "+" button and the dockview "+ → Terminal" menu, asserts the new tab connects, and intercepts outgoing user_shell.create WS frames to verify the payload carries task_environment_id and never session_id. A heal-coverage assertion checks every listed session has task_environment_id populated post-boot. * fix(e2e): drop right-panel '+' test, dockview is the only '+' path on desktop The mobile TaskRightPanel SessionTabs aren't rendered at desktop viewports — the dockview '+' menu is the sole new-tab affordance, so testing the mobile path on chromium just timed out clicking a button that never mounted. Consolidate into a single dockview '+ → Terminal' test that asserts the new panel gets the backend-assigned 'Terminal 2' label (proving env-shared shell list) plus the WS-payload regression guard. * fix(e2e): assert dockview terminal-tab count, not 'Terminal 2' label Dockview hardcodes the panel title to 'Terminal' for every tab — only the mobile right-panel SessionTabs surface the backend's 'Terminal N' label. Switch the assertion to count tabs to keep the e2e green. * test(backend): cover SQL NULL branch of healSessionTaskEnvironmentIDs Adds insertSessionWithNullEnvID and TestHealSessionTaskEnvironmentIDs_LinksNullOrphans so the WHERE-clause IS NULL predicate is exercised — the existing tests only seeded empty-string FKs. Also adds LIMIT 1 to the heal subquery as a defensive measure: the unique index already guarantees ≤1 row per task at runtime, but the SQL now reads as deterministic in isolation. * refactor(web): drop dead taskGateOk on agent terminal WS gate Agent passthrough routes by sessionId, not taskId — taskId was only used for logging. Gating the WS effect on taskId forced agent terminals to wait for useSession() to hydrate, adding a refresh-race window that didn't guard any real precondition. connectionKey + canConnect already cover the meaningful preconditions. * fix(web): stamp env id on terminal panels for accurate cleanup addTerminalPanel now bakes environmentId into the panel's params at creation time. The dockview-layout-setup onDidRemovePanel handler prefers the stamped env over re-deriving from the active session, so closing a terminal that was opened under a different task no longer calls stopUserShell with the wrong env (which silently no-ops on the backend and leaks the shell process until session teardown). Falls back to the active session's env for legacy panels persisted before the param existed. * fix(web): keep task id logging out of terminal reconnect deps |
||
|
|
45ee2ff48f |
feat: key terminals + dockview layout to TaskEnvironment (#755)
* feat(backend): key user shells by TaskEnvironmentID Sessions in the same task share a TaskEnvironment. Re-key the user shell runner so terminals are scoped per env, not per session — switching between sessions of the same task now sees the same shell list. - Add TaskEnvironmentID to AgentExecution / LaunchRequest / ExecutorCreateRequest / executor.LaunchAgentRequest, propagated through ToAgentExecution and the orchestrator launch path. - Add Manager.ResolveScopeKey(sessionID) — returns env when execution carries it, falls back to sessionID for legacy paths. - Rename runner param sessionID -> scopeID in interactive_shells.go; map keys are now scopeID:terminalID. - Shell handlers + WS terminal handler resolve sid -> scopeID via ResolveScopeKey before calling the runner. * feat(web): key dockview layout + portals to TaskEnvironmentID Switching between sessions of the same task no longer rebuilds the dockview layout — terminals and panels persist because layouts are env-scoped, not session-scoped. The bottom terminal also reuses a stable sessionId across same-env switches so it doesn't reconnect to a different shell. - Rename layout persistence: setSessionLayout/getSessionLayout/*SessionMaximizeState -> setEnvLayout/getEnvLayout/*EnvMaximizeState (kandev.dockview.env-layout. prefix). - Rename dockview-session-switch.ts -> dockview-env-switch.ts; performSessionSwitch -> performEnvSwitch with oldEnvId/newEnvId/activeSessionId. - dockview-store.ts: currentLayoutSessionId -> currentLayoutEnvId; switchSessionLayout -> switchEnvLayout(oldEnvId, newEnvId, activeSessionId). Same-env switches no-op. Drop _skipLayoutSwitchForSession + consumeSkipFlag — env-keyed action handles it structurally. - panel-portal-manager: PortalEntry.envId; releaseBySession -> releaseByEnv. ENV_SCOPED_COMPONENTS in dockview-desktop-layout (renamed from SESSION_SCOPED). - useEnvSwitchCleanup replaces useSessionSwitchCleanup; tracks env changes only. - All performLayoutSwitch callers (sidebar, dropdown, header, dialog) resolve sid -> envId via environmentIdBySessionId before switching. - bottom-terminal-panel: drop sessionId prop, use useEnvironmentSessionId(). - cleanupTaskStorage now takes envIds[] alongside sessionIds[] for env-scoped layout/maximize cleanup. * test: add coverage for env-keyed terminals + layout Playwright e2e (terminal-env-keyed.spec.ts) — switching between sessions of the same task keeps terminal scrollback intact and asserts both sessions share one task_environment_id. Update the comment in sessionless-task-switch.spec.ts to reference performEnvSwitch. * fix: address env layout PR feedback * test: fix flaky handshake auth check * fix: address env scope review findings |
||
|
|
1fc3544625 | fix: subtask sessions inherit agent profile from parent task (#692) | ||
|
|
c08d68c0fe |
refactor: re-key dockview panel state by environmentId instead of sessionId (#491)
* refactor: re-key dockview panel state by environmentId instead of sessionId - Remove terminal from SESSION_SCOPED_COMPONENTS (uses useEnvironmentSessionId internally) - Convert ChangesContent and file-editor-panel to useEnvironmentSessionId() - Re-key sessionCommits store from bySessionId to byEnvironmentId - Re-key useCumulativeDiff cache by environmentId - Update WS handler to resolve envKey before invalidating cumulative diff cache - Update comments across portal manager, session switch, and layout files * fix: revert useEnvironmentSessionId in file-editor-panel file-editor is session-scoped and ephemeral — portal is destroyed on session switch, so useEnvironmentSessionId() has no practical effect. * fix: address CodeRabbit review comments - Pass appStore to setupPortalCleanup so terminal tab close still calls stopUserShell via activeSessionId fallback (entry.sessionId is now undefined for global panels) - Add request version guard to useCumulativeDiff to discard stale responses after environment switches * fix: scope activeSessionId fallback to terminal only in portal cleanup vscode is session-scoped and always has entry.sessionId — the activeSessionId fallback should only apply to terminal (now global). * fix: migrate stranded env-keyed data on registerSessionEnvironment When registerSessionEnvironment(sessionId, environmentId) is called, any data previously stored under the fallback sessionId key is now migrated to the proper environmentId key. This prevents data from "vanishing" when selectors start reading by environmentId after the mapping is registered. Applies to: sessionCommits, gitStatus, shell outputs/statuses, userShells. * fix: extract migrateEnvKeyedData helper to fix lint max-lines * fix: address CodeRabbit review comments - Always delete orphaned sessionId key in migrateEnvKeyedData even when environmentId key already exists (prevents stale accumulation) - Reset loading state when switching to an envKey with warm cache to avoid brief spinner flash * fix: always reset loading on envKey switch in useCumulativeDiff Prevents stuck loading state on rapid A→B→A env switches where the stale in-flight response is discarded by version guard but loading is never reset. * fix: move sync effect before fetch effect in useCumulativeDiff React 18 batches effects in declaration order. With the sync effect after the fetch effect, setLoading(false) from the sync overrode setLoading(true) from the fetch on cold-cache env switches. Moving the sync effect first ensures the fetch effect's setLoading(true) wins the batch. * test: add migrateEnvKeyedData coverage for registerSessionEnvironment Tests 4 cases: normal migration, don't-clobber existing data, no-op when sessionId equals environmentId, and partial sub-key migration. * fix: close stale-diff gaps in useCumulativeDiff sync effect - Bump requestVersionRef on envKey change so in-flight fetches for the previous envKey are discarded - Clear loadingState[envKey] so the fetch effect isn't blocked on re-entry (A→B→A scenario) |
||
|
|
d1a028fdb6 | feat: introduce subtasks, allow sessions to task reuse executor (#419) |