# Attention State Feature Spec

## Overview

CodexBar currently shows Claude and Codex usage quotas on an always-on ESP32 display. This feature adds a second signal: whether a local Claude or Codex coding session needs the user's attention, and which project is waiting.

The target experience is simple: while working elsewhere, the user can glance at the device and see that Codex or Claude has stopped progressing and is waiting for input, approval, or review. If possible, the display should also show the project name, such as `CodexBar-ESP32`.

This feature should not require the ESP32 to inspect VS Code, terminals, or project folders directly. Agent sessions report lifecycle events to the Mac companion server, and the ESP32 receives a compact aggregated state during polling.

## Goals

- Show when Claude or Codex is waiting for the user.
- Show which project is waiting when there is enough screen space.
- Wake the dimmed ESP32 display when a provider needs user attention.
- Send an ntfy notification when a provider enters `needs_user`.
- Support multiple concurrent projects per provider.
- Keep the ESP32 UI glanceable and calm.
- Avoid brittle UI scraping of VS Code or terminal windows.
- Reuse the existing companion proxy and ESP32 polling architecture.

## Non-Goals

- Full project dashboard on the ESP32.
- Deep transcript display on the ESP32.
- VS Code UI automation or terminal scraping as the primary detection method.
- Perfect semantic detection of every natural-language question.
- Cross-machine synchronization.

## Architecture

```text
Claude/Codex session hook
        |
        | POST /attention
        v
Mac companion server
  - stores per-session attention state
  - aggregates provider-level display state
  - serves /usage or /status to ESP32
        |
        | HTTP poll
        v
ESP32 display
  - renders usage gauges
  - overlays attention/project indicators
```

The companion server remains the source of truth for display-ready state. Hooks are only event producers.

## Ingestion Rules

The companion should normalize and validate hook input before updating state.

- Normalize provider names case-insensitively to `Claude` or `Codex`.
- Reject unknown providers with `ok: false` rather than creating phantom providers.
- Derive `reason` from the hook event on the server.
- Use the server receive time for `updated_at`; do not trust client-supplied timestamps.
- Treat missing `session_id` as a stable fallback key derived from `provider + cwd`, if `cwd` is present.
- Ignore records without both a usable provider and project identity.

## Session State Model

The companion should track state per provider/session/project.

Example internal record:

```json
{
  "provider": "Codex",
  "session_id": "abc123",
  "cwd": "/Users/jameslopez/projects/CodexBar-ESP32",
  "project_name": "CodexBar-ESP32",
  "state": "needs_user",
  "reason": "permission_request",
  "updated_at": 1779820000,
  "last_running_at": 1779819900
}
```

### Fields

| Field | Meaning |
|---|---|
| `provider` | `Claude` or `Codex` |
| `session_id` | Stable session identifier from the hook payload |
| `cwd` | Working directory reported by the agent |
| `project_name` | Display-safe name derived from `cwd` |
| `state` | Current attention state |
| `reason` | Why the state changed |
| `updated_at` | Unix timestamp of latest event |
| `last_running_at` | Latest timestamp indicating active progress |

## Attention States

| State | Meaning | Display Priority |
|---|---|---|
| `needs_user` | Session is blocked on input, approval, or explicit prompt | 1 |
| `error` | Session failed or stopped unexpectedly | 2 |
| `done` | Session stopped after meaningful work and likely needs review | 3 |
| `running` | Session is actively working or recently emitted activity | 4 |
| `idle` | No active/recent session | 5 |
| `unknown` | State is stale or could not be classified | 6 |

Lower display priority number wins when aggregating multiple sessions.

## Expiration Rules

All session records must expire or decay so stale hooks cannot leave the ESP32 showing old attention forever.

Default timeouts:

| State | Timeout | Expiry Behavior |
|---|---:|---|
| `needs_user` | 60 minutes | Remove record |
| `error` | 30 minutes | Remove record |
| `done` | 5 minutes | Remove record |
| `running` | 10 minutes | Change to `unknown`; remove after 30 minutes total stale time |
| `unknown` | 20 minutes | Remove record |
| `idle` | Immediate or 5 minutes | Remove record |

Any new event for the same provider/session refreshes the record timestamp and recomputes its state.

When a new `SessionStart` arrives for a provider and `cwd`, the companion should clear prior non-waiting records for that same provider and `cwd`. This prevents an old `done` session from appearing beside a newly running session for the same project. Existing `needs_user` records for the same provider and `cwd` may also be replaced by the new session, because a fresh user prompt or restart usually means the user has acknowledged the old waiting state.

## Notifications

The companion should send an ntfy notification whenever a session transitions into `needs_user`.

Default ntfy topic:

```text
claudeCodeWaitingOnYou
```

Notification behavior:

- Send only on transition into `needs_user`, not on every refresh of the same waiting state.
- Include provider and project in the message, such as `Codex waiting: CodexBar-ESP32`.
- Include reason when useful, such as `permission_request` or `elicitation`.
- Deduplicate repeated notifications for the same provider/session/state.
- Allow future configuration of the topic, but default to the existing topic above.
- Notification failure must not block state ingestion or ESP32 updates.

## Event Mapping

Exact hook event names may differ between Claude and Codex versions, but the mapping should follow this shape.

| Hook Event | State | Reason |
|---|---|---|
| `SessionStart` | `running` | `session_start` |
| `PreToolUse` | `running` | `tool_use` |
| `PostToolUse` | `running` | `tool_use` |
| `PermissionRequest` | `needs_user` | `permission_request` |
| `Notification` | `needs_user` | `notification` |
| `Elicitation` | `needs_user` | `elicitation` |
| `Stop` | `done` | `stop` |
| `UserPromptSubmit` | `running` | `user_prompt` |
| `SessionEnd` | `idle` | `session_end` |

In Phases 1-4, `Stop` defaults to `done`. Phase 5 may optionally upgrade `Stop` to `needs_user` with reason `possible_question` when the hook-provided final assistant message looks like a likely question.

### Question Detection

Explicit events such as permission requests and elicitation should be treated as reliable `needs_user` signals.

Plain natural-language questions are less reliable. Phase 5 inspects `last_assistant_message` after `Stop` when the hook payload provides it, and classifies the stop as `possible_question` when it contains patterns such as:

- A direct question ending in `?`
- Numbered or lettered answer choices
- Phrases such as "which option", "should I", "do you want", or "please confirm"

This heuristic should be conservative and easy to disable.

## Project Name Derivation

Project names should come from hook-reported `cwd`, not from scanning the user's projects directory.

Default behavior:

```text
/Users/jameslopez/projects/CodexBar-ESP32 -> CodexBar-ESP32
/Users/jameslopez/projects/client/foo -> foo
```

Future optional behavior:

- Configurable project root prefixes.
- Friendly-name overrides.
- Duplicate basename disambiguation, such as `client/foo` vs `personal/foo`.

## Aggregated Provider State

The ESP32 should not receive every raw session by default. The companion should aggregate sessions into a compact provider-level payload.

Example provider payload:

```json
{
  "name": "Codex",
  "attention": {
    "state": "needs_user",
    "count": 2,
    "primary_project": "CodexBar-ESP32",
    "projects": ["CodexBar-ESP32", "BacklogRunner"],
    "reason": "permission_request",
    "updated_at": 1779820000
  },
  "windows": [
    { "label": "5h", "pct": 42, "resets_in_sec": 7200, "duration_sec": 18000 }
  ]
}
```

Aggregation rules:

1. Group active/recent records by provider.
2. Select the highest-priority state for each provider.
3. Within that state, choose the most recently updated session as `primary_project`.
4. Set `count` to the number of sessions in the selected state.
5. Include up to three project names in `projects`, ordered by recency.
6. Apply expiration rules before aggregation.
7. Deduplicate records with the same provider and `cwd`, keeping the highest-priority and newest record.

## HTTP API

### POST `/attention`

Hook endpoint for Claude/Codex lifecycle events.

Example request:

```json
{
  "provider": "Codex",
  "event": "PermissionRequest",
  "session_id": "abc123",
  "cwd": "/Users/jameslopez/projects/CodexBar-ESP32"
}
```

Response:

```json
{ "ok": true }
```

The request intentionally does not include `reason` or `timestamp`. The server derives `reason` from `event` and sets timestamps from its own clock.

### GET `/usage`

Existing ESP32 endpoint. Add `attention` to each provider object.

This is the simplest firmware integration because the device already polls `/usage`.

### Optional GET `/status`

Future lightweight endpoint for frequent attention polling without hitting upstream usage APIs.

This may be useful because quota usage can refresh every few minutes, while attention state should refresh within a few seconds.

Response shape:

```json
{
  "ok": true,
  "updated_at": 1779820000,
  "providers": [
    {
      "name": "Claude",
      "attention": {
        "state": "idle",
        "count": 0,
        "primary_project": "",
        "projects": [],
        "reason": "",
        "updated_at": 1779820000
      }
    },
    {
      "name": "Codex",
      "attention": {
        "state": "needs_user",
        "count": 2,
        "primary_project": "CodexBar-ESP32",
        "projects": ["CodexBar-ESP32", "BacklogRunner"],
        "reason": "permission_request",
        "updated_at": 1779820000
      }
    }
  ]
}
```

`/status` should use the same `attention` object schema as `/usage`, but omit quota windows and never call upstream quota APIs.

## Persistence

The companion can begin with in-memory state only. For better behavior across companion restarts, persist to:

```text
~/.codexbar/attention-state.json
```

Persistence should be best-effort. If the file is missing or invalid, the companion should start with empty attention state.

## Display Behavior

The ESP32 should preserve the current usage gauges as the default view. Attention state should appear as an overlay or temporary enhancement.

Recommended behavior:

| Condition | Display |
|---|---|
| One project waiting | Pulse provider section and show project name |
| Multiple projects waiting | Show `2 waiting`; rotate project names every few seconds |
| Provider running | Optional subtle breathing provider dot |
| Provider done | Solid accent or `review` label for 5 minutes, no aggressive flashing |
| Both providers waiting | Pulse both provider sections independently |
| No attention state | Current usage display |

Suggested visual hierarchy:

1. Provider name remains visible.
2. Waiting project name appears under provider name or in the local status area.
3. Usage gauges remain visible unless the attention state is urgent.
4. Flashing should be restrained; pulsing accent color is preferred for an always-on desk device.

Initial animation parameters:

- `needs_user`: 1000 ms pulse cycle, accent brightness from 45% to 100%.
- `running`: 2000 ms breathing dot cycle, accent brightness from 35% to 75%.
- `done`: solid accent at 70% brightness with `review` or project name.
- Project names should be end-truncated with an ellipsis if they do not fit.
- `projects` rotation, when used, should advance every 3 seconds.

### Attention Wake and Dimming

When any provider enters `needs_user`, the ESP32 should restore the backlight to `TFT_BRIGHTNESS` immediately, even if the display had dimmed due to unchanged usage.

After waking for attention, the display should dim using the same stepped behavior as the existing idle dimming:

- Start at configured `TFT_BRIGHTNESS`.
- Dim across the same five steps used for unchanged usage polling.
- Keep the attention pulse visible at each dim level.
- Wake back to full brightness again if a new provider/project enters `needs_user`.
- Do not repeatedly reset brightness for the same unchanged waiting state.

This keeps the attention signal noticeable when it first appears without leaving the display bright forever.

## Polling Strategy

Phase 1 can include attention in `/usage` and rely on the existing poll interval.

For a useful real-time attention indicator, a later phase should separate polling:

- `/usage`: every 5 minutes, as today.
- `/status`: every 5-10 seconds for local attention state.

The `/status` endpoint should not call upstream quota APIs.

## Rollout Plan

### Phase 1: Companion State Broker

- Add in-memory attention state store.
- Add `POST /attention`.
- Add aggregated `attention` object to `/usage`.
- Add provider normalization, same-project deduplication, and session expiration.
- Add ntfy notification on transition into `needs_user`.
- Provide manual `curl` examples for testing.

### Phase 2: Hook Integration

- Add Claude hook examples that call `/attention`.
- Add Codex hook examples that call `/attention`.
- Map explicit permission/notification/session events.
- Confirm hook payload fields for `session_id` and `cwd`.

### Phase 3: ESP32 Rendering

- Parse optional `attention` object per provider.
- Pulse provider section when `state == needs_user`.
- Wake the dimmed display when a new `needs_user` state appears.
- Reuse the existing five-step dimming behavior after attention wake.
- Show `primary_project` when one project is waiting.
- Show `N waiting` when multiple projects are waiting.
- End-truncate long project names.
- Show `done`/review state for 5 minutes based on companion expiration.

### Phase 4: Faster Local Status

- Add `GET /status`.
- Poll `/status` every 5-10 seconds.
- Keep quota usage polling at the existing interval.
- Update only affected display regions when possible.

### Phase 5: Smarter Classification

- Add optional Stop-event final-message inspection.
- Classify likely questions as `needs_user` with reason `possible_question`.
- Add a config switch to disable the heuristic.

## Open Questions

1. Should multiple waiting projects rotate automatically or show only `N waiting`?
2. Should Phase 5 question detection become more or less aggressive after real-world usage?

## Feasibility

This feature is highly feasible because CodexBar already has the correct architecture: a Mac companion server that translates local machine state into a simple ESP32 HTTP payload.

The reliable portion is explicit lifecycle state, especially permission requests, notifications, and session stops. The less reliable portion is semantic question detection from natural-language output. That should be treated as a later best-effort enhancement rather than a blocker for the core feature.
