Skip to content

feat(web): close a session from its tab, with a two-click guard - #158

Open
defangdevs wants to merge 1 commit into
masterfrom
feat/workspace-tab-close
Open

feat(web): close a session from its tab, with a two-click guard#158
defangdevs wants to merge 1 commit into
masterfrom
feat/workspace-tab-close

Conversation

@defangdevs

Copy link
Copy Markdown
Owner

Motivation

Closing a session meant leaving the workspace for the settings page. This puts a small × on every tab — but closing kills a live agent, and the button lands a few pixels from the session name, so a plain one-click × would be an accident waiting to happen.

What it does

The first click arms the button: it becomes a red Close? pill and the tab itself tints red, so it is unmistakable which session is at stake. The second click actually closes.

Tab close: resting and armed

The armed state gives way to anything that suggests the user didn't mean it:

  • a 4s timeout, Escape, tabbing away, arming another tab, or a click anywhere else;
  • a double-click is swallowed rather than taken as a confirmation (a 350ms settle window), so a stray double-tap on the × arms and stops;
  • a tab-bar re-render (the 2.5s session-state poll swaps #tab-bar wholesale) also drops the armed state — it fails safe.

Keyboard works the same way: Enter arms, Enter confirms.

Implementation notes

  • No confirm() dialog. An inline onsubmit confirm is itself JavaScript, so it protects nobody when scripting is off — the arming replaces it, and the no-JS fallback stays what it always was for the settings-page delete: a plain one-click POST to the same /sessions/delete route.
  • The button is a sibling of the tab link, not a child. A <form> inside an <a> is invalid markup, and keeping them apart also stops the tab-select click delegation from swallowing the close click. It is absolutely positioned over the tab's extra right padding so it still reads as part of the tab.
  • Nothing moves between the two clicks. The armed tab only changes the colour of an already-present 1px border, so the second click can't miss.
  • The × is always visible (dim until hovered) rather than hover-only — touch has no hover.

User-visible and security effects

User-visible: the new × per tab, and the arm/confirm interaction above. No security effect — it posts to the existing auth-gated, same-origin-checked /sessions/delete route with the same payload the settings page sends, and adds no new route, state, or privilege.

Checks run

  • python3 bin/assemble-module.py --check — passes (modules/agent-box.nix regenerated and committed).
  • node --check modules/src/settings.js, ast.parse on the daemon.
  • Rendered the real assembled daemon locally against a scratch tmux socket and drove the × through its actual code path to produce the screenshot above.
  • Not run here: the nix VM checks — this flake only exposes x86_64-linux checks and the box I worked on is aarch64-linux. tests/sessions.nix gains assertions for the server-rendered close form/button; tests/e2e/root-sessions.spec.ts gains a case covering all four legs of the guard (first click inert, disarm on outside click, double-click swallowed, second click closes) — both need CI or a deployed box.

Note for the reviewer

docs/workspace-tabs.png predates this change and still shows tabs without the ×; worth refreshing from a live box at some point. Separately (pre-existing on master, not touched here): tests/e2e/root-sessions.spec.ts fills #session-editor input[name="name"], but the add form has no name field any more (names are auto-derived), and settings.js's matching body.get("name") is dead — so adding a session no longer focuses its new tab under JS.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sxoeoon2SQcbxnaG9tExNY

Every workspace tab gains a small × posting to the existing
/sessions/delete route, so closing a session no longer means a trip to
the settings page.

Closing kills a live agent and the button sits a few pixels from the
session name, so a single click can't do it: the first click arms the
button (it becomes a red "Close?" pill and the tab tints red), the
second one submits. The armed state gives way to a 4s timeout, Escape,
tabbing away, arming another tab, or any click elsewhere, and a
double-click is swallowed rather than treated as a confirmation.

No confirm() dialog: an inline onsubmit confirm is itself JavaScript,
so it guards nobody when scripting is off — the arming replaces it, and
the no-JS fallback stays a plain one-click POST.

The button is a sibling of the tab link rather than a child: a <form>
inside an <a> is invalid markup, and keeping them apart also stops the
tab-select click delegation from swallowing the close click.

Covered by tests/sessions.nix (server-rendered markup and route) and a
new tests/e2e case for all four legs of the guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sxoeoon2SQcbxnaG9tExNY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants