Backend:
- service_task_environments: fail closed when checkAnySessionRunning
errors so a transient DB hiccup can't bypass the running-session
guard and tear down a live container/sandbox; teardown is now truly
best-effort with errors.Join across container/sandbox/worktree, so a
failed Docker call doesn't permanently orphan the worktree on retry.
Adds tests for the running-check error path and the multi-resource
best-effort path.
- executor_execute: propagate GetTaskEnvironmentByTaskID errors instead
of silently launching a fresh environment that orphans existing
containers/worktrees; gate environment reuse on matching ExecutorType
so a profile switch doesn't pass stale PreviousExecutionID/container
IDs to the wrong backend.
- task_environment_handlers: reject malformed JSON on POST
/tasks/:id/environment/reset (still allow empty body) so a destructive
endpoint doesn't silently proceed with a bad payload.
- worktree (PushEnvironmentBranch): bound git push with a 30s context
timeout and disable interactive prompts via GIT_TERMINAL_PROMPT=0
so a stalled remote can't hang the request thread.
- manager_environment: explicitly call ensureClient() before
ContainerMgr() in DestroyContainer/GetContainerLiveStatus so a missing
Docker daemon surfaces as "docker unavailable" rather than the
generic "container manager not initialized".
- container: WorkingDir always uses the in-container /workspace path
rather than the host workspace path (Docker would happily start in an
unrelated directory in host bind-mount mode); bootstrap now logs the
prepare-script exit code so failures show up in container logs.
- docker/client: CreateContainerInteractive honors ContainerConfig.Entrypoint
so the same config struct produces consistent behavior on both creation
paths.
Frontend:
- executor-settings-button: extract useTaskEnvironment hook (component
shrinks below the 100-line limit); only treat 404 as empty env (other
errors no longer wipe a valid environment); track "loaded once" via a
ref so loadEnv doesn't depend on env state — that dep was creating a
new function identity on every successful poll, cancelling the
3-second interval and turning the popover into a tight loop hammering
/environment/live; copy-shell command uses sh instead of bash so it
works on user-built minimal images.
- task-reset-env-confirm-dialog: AlertDialogAction onClick calls
e.preventDefault() so Radix's auto-close doesn't dismiss the dialog
before the async reset completes (loading spinner now visible; close
is parent-controlled via onOpenChange).
- remote-credentials-card: track env method in selectedIds the same way
files/gh_cli_token are tracked, and let initialChoice match either
selectedIds OR envSecretId. First-time env-credential setup for an
agent that exposes both files+env was broken: clicking the env radio
never updated state, choice re-derived to "none", and the secret
picker disappeared before the user could pick anything.
- e2e/executor-reuse: select the acknowledge checkbox by label, not by
positional last(), so adding another checkbox to the dialog can't
silently target the wrong control.
Format pass on prettier-flagged files; gofmt fixed two whitespace
nits in lifecycle files.
A new button on the task top bar opens a popover showing the active
TaskEnvironment — executor type, status, container/sprite/worktree
identifiers, and a copy-ready 'docker exec' command for local Docker.
The popover hosts the destructive Reset Environment action so users have
one obvious place to inspect and manage the environment without
cluttering the New Session dialog.
The reset confirmation dialog requires explicit acknowledgement that
uncommitted changes will be lost. When the environment has a host-side
worktree it offers an opt-in 'push branch before resetting' checkbox so
committed work can be pushed first; the backend aborts the reset on
push failure.
- task-environment-api.ts: fetchTaskEnvironment + resetTaskEnvironment
- executor-settings-button.tsx: popover trigger + identity panel
- task-reset-env-confirm-dialog.tsx: destructive confirm with push opt-in
- task-top-bar.tsx: wire the new button into the right-side controls
- e2e: replace fresh-env-toggle test with reset-from-popover flow
* fix(web): preserve primarySessionId across kanban.update broadcasts
The kanban.update WS handler rebuilt kanban.tasks without
primarySessionId / primarySessionState, wiping values set by
task.updated. After any board refresh the primary star vanished and
selectTaskWithLayout fell into the slower async fallback path. Move
task building inside setState so existing values are preserved in both
single and multi snapshots.
* fix(web): prevent deleted session panel from being re-created
handleDelete called removeTaskSession before containerApi.removePanel,
which let useAutoSessionTab re-run with effectiveSessionId still
pointing at the deleted session and re-create its panel. The
setupChatPanelSafetyNet had a matching gap — it re-added a panel for
activeSessionId without checking the session still existed.
Switch the active session (or clear it) before removeTaskSession so
effectiveSessionId updates first. Add guards to useAutoSessionTab and
the safety net so neither creates a panel for a session that is no
longer in the store.
* test(web): add session tab management regression suite
Cover the three bugs fixed in this branch end-to-end: deleting a
non-active session keeps it gone, deleting the active session switches
focus to the remainder, deleting then immediately switching tasks does
not resurrect the tab, and the primary star survives both a
kanban.update broadcast (triggered via apiClient.moveTask) and a
task-switch round trip.
* fix(web): hand off deletion to newest remaining session, document multi-snapshot fallback
- session-tab.tsx: sort remaining sessions by started_at DESC and
pick the newest, instead of relying on the unspecified store ordering.
- kanban.ts: comment the multi-snapshot primarySessionId fallback so
the double lookup does not look like redundancy.
Both per Claude review feedback on #800.
* fix(web): preserve explicit-null primary, abort delete on WS failure
- kanban.ts: switch the multi-snapshot fallback from `??` to an
explicit `=== undefined` check. An explicitly-cleared null primary
must NOT be replaced by a stale snapshot value, only an absent key.
- session-tab.tsx: wsAction now returns a boolean; handleDelete bails
out before mutating the store / dockview when the backend
session.delete request fails. Prevents client/server desync from a
transient WS failure.
- kanban.test.ts: add regression covering the null-vs-undefined merge.
Per CodeRabbit and Greptile review on #800.
* style(web): drop redundant .slice() in session deletion sort chain
filter() already returns a fresh array, so the subsequent sort() does
not mutate shared store state. Per Greptile review on #800.
* 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