# Claude login code-entry — design

## Problem

`claude auth login --claudeai` sometimes blocks on a terminal prompt (`Paste code here if prompted >`) after the browser step, expecting the user to paste an authorization code back into the CLI's stdin. `ClaudeAuthOperation._login()` (`claude_auth_operation.py:161`) runs this via `subprocess.run(capture_output=True, timeout=LOGIN_TIMEOUT_SECS)` — output is captured and discarded, stdin is never wired to anything. Both `add()` and `reauthenticate()` call `_login()`, so both "Add account" and "Re-authenticate" hang (now bounded by the 900s timeout added earlier) with no way for the user to supply the code through the tray app. Confirmed live: running the command directly reproduces the exact prompt.

## Goal

Both flows can complete an interactive Claude login end-to-end through the tray UI: show the browser URL, let the user paste the code into a tray dialog, feed it to the CLI's stdin, and finish the same success/collision/failure/rollback handling that already exists.

## Approach (single recommended approach)

Mirror the existing Codex `DeviceAuthFlow` shape (`device_auth.py`) but for Claude's plain-text prompt instead of Codex's structured device-code payload. Reuse `_login()` as the single shared call site for both `add()` and `reauthenticate()` — no duplication between the two flows.

### `claude_auth_operation.py`

- `ClaudeLoginPrompt(url: str, raw_text: str)` — dataclass, parsed from CLI stdout.
- `ClaudeLoginSession(process: subprocess.Popen, account_home: Path)` — dataclass, replaces the current fire-and-forget `subprocess.run` result.
- `_login(account_home, *, login_hint=None) -> Iterator[LoginEvent]` — new shape, still the single call site used by both `add()` and `reauthenticate()`:
  - `Popen([...], stdin=PIPE, stdout=PIPE, stderr=STDOUT, text=True, bufsize=1, env=...)`.
  - Background reader thread streams stdout lines into a `queue.Queue`; a monotonic deadline enforces the existing `LOGIN_TIMEOUT_SECS` (900.0) end-to-end (from process start to exit), not per-read.
  - Yields `LoginEvent(kind="url_ready", url=...)` as soon as the "visit: ..." line is parsed (regex on the existing literal prefix `If the browser didn't open, visit: `).
  - If the `Paste code here if prompted >` marker (existing literal, matched verbatim) appears before the process exits, yields `LoginEvent(kind="code_required", session=...)` and then **blocks the generator** on a `queue.Queue` fed by a new `submit_code(session, code)` method — i.e. the generator itself waits for external code delivery via the queue, not via `.send()`.
  - If the process exits before requesting a code (non-interactive completion), skip straight to completion — no behavior change for that path.
  - On code submission: write `code.strip() + "\n"` to `process.stdin`, flush, then `process.wait(timeout=remaining_deadline)`; `subprocess.TimeoutExpired` → kill (already-established pattern) → raise `ClaudeAuthOperationError` with the existing "timed out" message shape.
  - Non-zero exit after completion → `ClaudeAuthOperationError(f"claude auth login exited with code {result.returncode}")` (unchanged from today).
- `add()` / `reauthenticate()`: replace the direct `self._login(...)` call with iteration over `_login`'s events; on `code_required`, yield a new `LifecycleEvent(kind="code_required", account=pending_account, prompt=ClaudeLoginPrompt(...), session=session)` and stop consuming until `submit_code()` (called externally, from indicator.py's dialog) unblocks the generator. All existing collision/success/failure/rollback branches are unchanged — they sit downstream of whichever path `_login` takes.

### New dialog (add to `device_auth_dialog.py`)

- `ClaudeCodeEntryDialog` — new class alongside `DeviceAuthDialog`, reusing its existing GTK/fallback helper methods (`_new_button`, `_new_label`, `_pack`, `_clipboard`, the `_FallbackGtk` headless shim).
- Shows: the URL as a `LinkButton`, a `Gtk.Entry` for the pasted code, "Submit" and "Cancel" buttons.
- `on_submit: Callable[[str], None]` fires with the entry's text; `on_cancel: Callable[[], None]` — same contract shape as `DeviceAuthDialog`'s existing callbacks.
- No timer/countdown UI needed (unlike Codex's 15-minute code display) — the 900s deadline is enforced by `_login`, not the dialog.

### `indicator.py`

- `_run_add_account_flow` (`:996`) and `_run_repair_flow` (`:946`) both currently `continue` on `"browser-waiting"` and never handle a code step. Both gain one new branch:
  - `event.kind == "code_required"` → build `ClaudeCodeEntryDialog` (same `_create_device_auth_dialog`-style idle-add/threading.Event pattern used for `prompt_ready` today) with `on_submit=lambda code: provider.lifecycle.submit_code(event.session, code)` and `on_cancel=lambda: terminate_process(event.session.process)`.
  - `submit_code` is called directly on the session (a side-channel action), the same way `on_cancel` already calls `terminate_process(session.process)` directly today — no generator `.send()` gymnastics.
  - Existing collision/success/failure handling downstream is untouched.

## Error handling

- Unchanged failure/rollback plumbing (`staged.rollback()` for add, `_restore_files` for reauth) — `code_required` sits strictly before any account-mutating step, so no new rollback path is needed.
- Cancel during `code_required`: `terminate_process(session.process)` (existing helper) → process exit → `_login` generator's completion branch treats it as a normal non-zero-exit failure → existing rollback fires.
- Timeout during `code_required` (user never pastes a code): governed by the same end-to-end `LOGIN_TIMEOUT_SECS` deadline already added — no separate timer.

## Testing

- `claude_auth_operation.py`: extend the existing test harness (fake `Popen`-like runner) to cover: url parsed → code_required event → `submit_code` → success; url parsed → code_required → timeout; cancel during code_required → rollback. Mirror the existing `test_add_login_timeout_rolls_back_and_reports_timeout_message` shape.
- `device_auth_dialog.py`: headless-fallback test that `ClaudeCodeEntryDialog` builds without a real GTK display (already how `DeviceAuthDialog` is tested, per the existing `_FallbackGtk` layer) and that `on_submit`/`on_cancel` fire with the right arguments.
- `indicator.py`: extend whichever existing test drives `_run_add_account_flow`/`_run_repair_flow` with a fake `code_required` event and assert the dialog is shown and `submit_code` is called with the entered text.

## Architecture Decisions

- Single shared `_login()` generator for both `add()` and `reauthenticate()` (rejected: duplicating the Popen/parse/submit logic per flow) — same root cause, same fix, per user's explicit "code reusability" direction.
- One new dialog class in the existing `device_auth_dialog.py` file (rejected: a new file) — it's a small, closely related sibling to `DeviceAuthDialog` and reuses its GTK/fallback helpers; a new file would just re-import the same scaffolding.
