Skip to content

Drop in from an idle headless session, and take over after the turn from a working one #592

Closed

Description

@barelyworkingcode

User story

As the owner, I want a Drop in button on an idle headless Claude session, so that I can take it over and type to it from my phone or tablet.

Acceptance criteria

  • A headless Claude session in the Idle group shows the Drop in button, in the board and in the Chief of Staff agents list.
  • A headless session in the Working group shows a "Take over after this turn" button. It waits for the turn to end, as relay's handoff does.
  • A tap opens the terminal on the session with its history and read-write input.
  • When the terminal closes, the session returns to the board as idle and headless.
  • A refusal from relay (a tool is open, another terminal holds it, no conversation yet) shows its reason in one line.
  • The button never shows on a read-only outside row.

What I'd notice

Any idle agent I started through relay has a Drop in button, and the Chief of Staff list does too.

Context

AgentBoard.showsDropIn in public/agent-board.js allows the button only in the Needs you group (asking, errored, stalled). Relay's POST /api/sessions/{id}/drop-in already takes over any headless Claude session, including one with no live process (docs/session-host.md, Drop-in). No relay change is needed.
Outside sessions stay read-only by design. The next issue covers resuming an ended one.

Tasks

  • Planner: write the ## Contract.
  • Widen the rule; add the Take over after this turn state; show the refusal reason.
  • Playwright tests from the criteria; update the feature map row. Verification task, last.

Contract

Standalone story. Branch feat/592-drop-in-idle off main; the PR targets main with Closes #592. Three tasks: T1 dev, T2 tests and feature map, T3 verification (last).

Relay side: no change, no pin bump

The pinned fakerelay (test/e2e/relay-pin.json, relay b96e32d) already serves everything this story calls:

  • POST /api/sessions/{id}/drop-in on an idle headless Claude session, also one with no live process.
  • A running turn: the request waits on the session's own wake signal (no timer), then checks again.
  • Refusals dropped_in, tool_running (a pending permission), no_conversation, not_headless, not_claude, session_not_found, template_unavailable.
  • Hand-back: when the drop-in terminal ends, the hold clears and a session_state idle frame goes out with a new since.

Two limits, both accepted (questions 3 and 4): turn_timeout is not faked, and haiku/sonnet/opus always reply echo. So no hermetic spec can hold a Claude turn running. relay main adds nothing to drop-in since the pin, so a pin bump would gain nothing. Relay is not touched.

Interfaces

Wire (unchanged; docs/api.md row already correct, no edit):

  • Request: POST /api/sessions/:id/drop-in with {"cols":80,"rows":24}. Eve forwards it to relay through proxy() with no client timeout.
  • Success 201: {sessionId, claudeSessionId, host?, terminal: {terminalId, templateId, name, directory, host}}. terminal.name is <session name> (drop-in).
  • Refusal: {error, message} with relay's status. message is the reason text, and relay is its only source. Real relay codes: session_not_found 404; not_claude, not_headless, dropped_in, tool_running, turn_timeout, no_conversation, host_unavailable, attach_failed 409; a terminal-launch refusal such as template_unavailable 403.
  • Pinned fakerelay texts the specs may assert:
    • dropped_in: a terminal already has this session; close it first
    • tool_running: a tool is running (<tool>); wait for it to finish or stop the turn, then try again
    • no_conversation: the session has not run a turn yet; there is nothing to take over
  • Eve's own answers: an outside: id gets 403 {error:"outside_session_read_only", message:"Outside sessions are read-only."}; relay down gets 502 {"error":"Service unavailable"}.
  • "After this turn" is the same POST. Relay waits for the turn to end (up to 60 s in real relay), so eve adds no second call and no client timer.
  • Hand-back: eve makes no call. Closing the tab sends the ordinary terminal_close. Relay hands the session back, and the session_state idle frame moves the row to Idle. The row stays headless (headless: true on the list row).

public/agent-board.js (T1):

static dropping = new Map();   // sessionId -> 'drop' | 'wait'; was a Set. Kind is fixed at tap.
static held = new Map();       // sessionId -> terminalId of a drop-in this page opened
static showsDropIn(r)          // the one rule, see below
static async dropIn(container, sessionId, { wait = false, onAnswer = null } = {})  // never rejects
  • Rule. showsDropIn(r) is true when all of these hold:
    • r.kind === 'session', isClaude(r.item) and r.item.headless === true;
    • and one of these: dropping.has(r.id); group needs or idle; or group working and !held.has(r.id).
      Outside rows (kind 'outside') never pass. _withActions keeps sending them to _withView only. Done and Ended never pass.
  • held.
    • dropIn adds sessionId -> terminal.terminalId on a 201.
    • collect() (or a render step in the same file) deletes an entry once that terminal is missing from mgr.allTerminals or has state === 'stopped'.
    • Effect: this page never offers "Take over" on a session its own open terminal holds. Another page or device still sees the button and gets relay's dropped_in refusal.
  • dropIn flow.
    1. Set dropping to wait ? 'wait' : 'drop' and re-render every live board.
    2. POST.
    3. On 201 with a terminalId: set held, call onAnswer?.(), then openDropIn(terminal).
    4. Otherwise: call onAnswer?.() and show the toast.
    5. Finally: delete from dropping and re-render.
      The CoS alert-card call AgentBoard.dropIn(container, id) keeps working unchanged (kind drop).
  • onOpen timing (changed). A row's Drop in or Take over calls onOpen(r) through onAnswer, when relay answers (terminal or refusal), not at the tap. Only the phone sheet passes onOpen today, so only the sheet changes: it stays open and shows the waiting state, then closes when relay answers. A row tap is unchanged.
  • Toast (refusal, unchanged id drop-in-<id>, type error, 8 s). The text is one line, made by collapsing whitespace runs to one space and trimming:
    • Can't drop in: <message>, else Can't drop in: <error>, else Can't drop in: relay isn't reachable;
    • with no terminal back: Can't drop in: relay did not return a terminal.

UI states (sibling .agent-row__action under the row, as today):

Row Visible text Accessible name testid Busy text (disabled, aria-busy="true")
Needs you, Idle Drop in Drop in to <label> <p>-drop-in-<id> Dropping in…
Working (running, starting) Take over after this turn Take over <label> after this turn <p>-take-over-<id> Waiting for the turn to end…
  • The busy text follows the kind fixed at tap, not the row's current group. A Take over whose turn ends keeps reading Waiting for the turn to end… under Idle until relay answers.
  • Prefixes are today, the project page's, rail and sheet. All four come from AgentBoard, so the Chief of Staff list gets the button with no edit to cos-agent-rail.js or chief-of-staff-page.js.
  • Touch. The existing (pointer: coarse) rule gives .agent-row__action a 44x44 px minimum. The longer label may wrap; it must not overflow the rail or sheet.

Paths

  • Simple: Today's Agents board, a project page, or Chief of Staff > Agents (the rail on wide; on a phone or tablet the strip opens the sheet). Tap Drop in on an idle agent, or Take over after this turn on a working one. A terminal tab opens on the conversation.
  • Power: relay drop-in <session-id> (relay CLI, existing; same wait and refusals). Eve adds no config or flag.

File ownership

Task Owns Others may
T1 dev public/agent-board.js; docs/design-workbench.md (append ## Amended by eve#592: rule, labels, held, onOpen timing) —
T1 dev, shared public/apple/agents.css: only .agent-row__action wrapping/max-width if the long label overflows at 220 px rail or 412 px phone nothing else in the file
T2 tests test/e2e/g6-drop-in.spec.js (new); docs/FEATURES.md: rows G6.23, G6.24, G6.41 and a new row G6.105 —
T3 verify no files; posts the Verified: line —
  • docs/areas.jsonc needs no edit: test/** and docs/** are quiet, and T1 adds no file.
  • docs/api.md needs no edit: no route or frame changes.
  • test/e2e/support/** is not owned. A needed fixture change comes back as a question.

FEATURES.md edits (T2):

  • G6.23: Action becomes Drop in to an agent that is waiting on me or idle. Add idle to the screen proof. r3 becomes not a headless Claude session, or an outside row: no button. Spec becomes g6-drop-in.spec.js; keep the devbox journey.
  • G6.24: Screen proof becomes The tab closes and the agent is back under "Idle" with its Drop in button. Spec becomes g6-drop-in.spec.js.
  • G6.41: The simple door adds or the phone dialog "Agents". Spec becomes g6-drop-in.spec.js, replacing none yet: #369.
  • New G6.105, Take over a working agent after its turn:
    • Simple door: Today or Chief of Staff > button "Take over <agent name> after this turn".
    • Power door: relay drop-in <session-id>.
    • Screen proof: the button reads "Waiting for the turn to end…" until relay answers, then a terminal tab opens on the same conversation.
    • Relay proof: relay logs --event session.drop_in ok.
    • Refusals: r1 relay refuses (a tool is running, a terminal already has it, the turn did not end): toast "Can't drop in: <reason>"; r2 this page's own drop-in terminal holds it: no button; r3 outside row: no button.
    • Journey: none yet. Spec: g6-drop-in.spec.js.

Acceptance mapping

# Criterion Task · interface Spec
1 Idle headless Claude shows Drop in, board and CoS list T1 · showsDropIn idle branch idle row on Today and in the rail: button Drop in to <name>; a non-headless idle thread has none
2 Working shows "Take over after this turn", waits for the turn T1 · working branch, dropping kind wait, dropIn({wait:true}) Page B sees the row under Working while page A holds it; fault --mode slow on the drop-in route; tap shows Waiting for the turn to end…, disabled; page A closes its tab; release; page B's terminal opens
3 Tap opens the terminal with history and read-write input T1 · openDropIn(terminal) (existing) tab <name> (drop-in) selected; type into Terminal input and the echo shows; no Read-only badge. History: T3 real-app only (fake drop-in is an echo terminal)
4 Terminal closes, session returns idle and headless relay hand-back + existing AgentAttention button Close <name> (drop-in); row under Idle with Drop in to <name> again
5 Refusal reason in one line T1 · toast text rule (a) page B taps Take over while page A holds it: Can't drop in: a terminal already has this session; close it first; (b) a --mode error --status 409 fault with the tool_running body; (c) the same with no_conversation. Each toast is one line
6 No button on a read-only outside row T1 · kind 'outside' never passes world outside_sessions with an idle and a working row: no Drop in and no Take over button in rail or sheet
— Touch, phone and tablet CSS (existing) test.use(profiles.phone) in the CoS sheet: tap() Drop in, the sheet closes, the tab opens, and the button box is at least 44 px high; one Take over in the sheet with the slow fault: the sheet stays open with the waiting text. One profiles.tablet Today-board tap

How to get rows: an idle headless Claude session comes from a Chief of Staff Start card with an entry (a chat template or Claude agent on model haiku). A model-only start is not tracked in the pinned fakerelay, so it never reaches the board. Working is reached only by another page's (eve.openAnother) drop-in hold (question 4).

Test surface

  • Playwright (g6-drop-in.spec.js): criteria 1, 2, 3 (input), 4, 5 and 6, plus the touch rows. Unit and integration: none (the repo has no unit tier).
  • Real-app pass (T3): criterion 3's history, and relay's real turn wait for criterion 2.

Waits

  • T1: none needed. The waiting state ends on the POST's own promise (completion handle). T1 adds no timer.
  • T2: each wait uses one of these signals, never a duration:
    • eve boot: eve.open() (<html data-ready="1">);
    • an agent's turn ended: button Drop in to <name> visible (driven by the session_state idle frame);
    • the slow fault holding: relay.waitForEvent('fakerelay.fault', { match: l => l.action === 'held' });
    • drop-in done: relay.waitForEvent('session.drop_in', { match: l => l.status === 'ok' }), then the tab selected;
    • refusal: session.drop_in with status denied, then the toast text;
    • hand-back: session.exited for the terminal id, then the Idle button visible;
    • echo: the typed text visible in the terminal.
  • T3: stack-up prints STACK up e592 <stack.json>; each harness run waits on its own process exit; the real-app check waits on the screen (button, tab, typed echo).

Verification task (T3, last; takes no devlock)

On the PR head, after main is merged in:

  1. Unlock the devid keychain. Run npm ci in the worktree.
  2. Run npx playwright test (repo suite).
  3. ./stack-up.sh e592 --relay <relay main checkout> --eve <this worktree>.
  4. node devboxverify/main.js --checkout <this worktree> --stack <stack.json>.
  5. Real-app check, before stack-down, on the stack's eve at a touch viewport (tablet or phone):
    • Start a real headless Claude agent on Haiku with a one-line prompt and wait for it under Idle.
    • Tap Drop in: the terminal opens on the same conversation with its earlier turn, and a typed line reaches Claude.
    • Close the tab: the row is back under Idle with Drop in.
    • Then one attempt at Take over after this turn during a turn. Record which one shows: the waiting text, then the terminal; or a one-line refusal.
  6. ./stack-down.sh e592.
  7. Post Verified: <head sha> · repo-suite: <result> · devboxverify: <result> · real-app: <result> · log: <path>. A push after it reruns this task.

Budget

  • T1: 2 files (3 if the CSS wrap is needed), about 50 to 90 changed lines.
  • T2: 2 files, about 220 to 300 lines of spec and 6 row edits.
  • T3: no diff.
    Going past either is a question for the coordinator.

Open questions

  1. INSIDE. Recommend hiding "Take over" on a session this page's own drop-in terminal holds (held). Relay reads a held session as running, so without this my own board would offer a take-over that relay refuses.
  2. INSIDE. Recommend keeping the existing error toast for the refusal, collapsed to one line, and closing the sheet when relay answers so the toast is not under the modal. Not a new inline row line.
  3. INSIDE. Recommend proving "with its history" by the T3 real-app check only. The fakerelay drop-in is an echo terminal with no transcript.
  4. INSIDE. Recommend accepting that the hermetic Working path is a page-B-while-page-A-holds setup plus a slow fault. The pinned fakerelay cannot hold a haiku/sonnet/opus turn, and adding that would be a relay change, which the issue rules out. Relay's real turn wait is tried in T3.
  5. INSIDE. Recommend no devboxverify journey change. agent-drop-in stays G6.23's journey (Needs you path), and T3's real-app check covers the idle path. A journey edit would be a harness change outside the task list.
  6. INSIDE. Recommend showing "Take over after this turn" for the whole Working group, starting included. Relay answers a session with no turn yet with no_conversation, shown as the one-line refusal.

Activity

  1. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Coordinator approval: owner asked, 2026-10-19 (the owner's number one priority: the Chief of Staff must be highly usable)

  2. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Planning done: the ## Contract is in this issue. Work stops here and waits for the owner review (owner instruction, 2026-10-19).

  3. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Linked as a child of #596 (session adoption epic, eve track). Under the epic its PR targets epic/596-session-adoption, not main; the planner amends the contract for that when the epic opens. Nothing else changes.

  4. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Coordinator approval: owner asked, 2026-10-20. Child of the approved epic eve#596; the PR targets epic/596-session-adoption.

  5. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Contract amendment (coordinator, 2026-10-20): under epic eve#596, branch feat/592-drop-in-idle is cut from epic/596-session-adoption, not main. The PR targets epic/596-session-adoption with Closes #592. The verification task (T3) moves to the epic's close child; a PR into the epic branch needs none. Nothing else in the contract changes.

  6. added a commit that references this issue on Oct 20, 2026
  7. barelyworkingcode commented on Oct 20, 2026

    @barelyworkingcode
    OwnerAuthor

    Merged into epic/596-session-adoption by #599. Left open: the epic's close child closes its children by hand. Real-app check of history and relay's real turn wait goes to the epic's verification task (contract T3).

  8. barelyworkingcode commented on Oct 21, 2026

    @barelyworkingcode
    OwnerAuthor

    Done by #599, merged into epic/596-session-adoption. Verified on the epic head 1399a79 (see #598).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions