|
| 1 | +# t3x — Per-thread auto-resume UI (design + implementation plan) |
| 2 | + |
| 3 | +**Date:** 2026-07-24 |
| 4 | +**Fork:** `radroid/t3code` |
| 5 | +**Parent feature:** `2026-07-23-usage-limit-auto-resume-design.md` (the headless server feature this adds UI to) |
| 6 | +**For:** a fresh implementation agent. Self-contained. Branch: `t3x/autoresume-ui` off `main` (merge `t3x/setup-and-auto-resume` into `main` first). |
| 7 | + |
| 8 | +## Goal |
| 9 | + |
| 10 | +Give each thread a small, visible control for the existing (headless) auto-resume feature: |
| 11 | + |
| 12 | +1. **Status** — is this loop going to be auto-resumed, and when ("next attempt ~3:47 PM"). |
| 13 | +2. **On/off toggle** — enable/disable auto-resume for _this thread_ (default on). |
| 14 | +3. **Resume-message textbox** — the text sent on resume for this thread (default `"continue"`; already stored as `overridePrompt`). |
| 15 | + |
| 16 | +Placement (decided): a **floating card** overlaying the thread view — lowest merge-conflict, self-positioning, no upstream layout cooperation. |
| 17 | + |
| 18 | +## Guiding constraint (this is a fork) |
| 19 | + |
| 20 | +Keep the upstream footprint at a **fixed, tiny seam** and touch **zero** `@t3tools/contracts` code and no hot chat files' logic. Target total upstream footprint: |
| 21 | + |
| 22 | +- **+1 line** (+1 import) in `apps/server/src/server.ts` (mount the t3x HTTP route). |
| 23 | +- **+1 line** (+1 import) in `apps/web/src/routes/_chat.$environmentId.$threadId.tsx` (mount the overlay). |
| 24 | +- Everything else is **new files** under `apps/server/src/t3x/` and a new `apps/web/src/t3x/`. |
| 25 | + |
| 26 | +> Every concrete file path / line number / symbol below came from a codebase exploration and is a strong lead, **but the implementing agent MUST re-confirm each against the current code before editing** (upstream churn moves line numbers). |
| 27 | +
|
| 28 | +## Architecture (3 pieces) |
| 29 | + |
| 30 | +### A. Status — mostly free |
| 31 | + |
| 32 | +The reactor already appends per-thread timeline activities (`t3x.auto-resume.scheduled|resumed|cancelled|capped` via `appendActivity` → `thread.activity.append` in `apps/server/src/t3x/autoResume/Reactor.ts`), and the web already renders arbitrary activity kinds in the work-log (per exploration: `apps/web/src/.../session-logic.ts` `deriveWorkLogEntries` uses `activity.summary` and does not filter our kinds — **verify**). So a baseline status already surfaces in the timeline with no change. |
| 33 | +The floating card additionally shows a **live** status ("auto-resume: on · next attempt ~3:47 PM") read from the GET endpoint below (returns the current pending, if any). Enrich wording only in our own `Reactor.ts` if desired — no seam. |
| 34 | + |
| 35 | +### B. Persistence — a fork-owned raw HTTP route (NOT an RPC/contracts change) |
| 36 | + |
| 37 | +Adding a WS-RPC method would force edits to `@t3tools/contracts` (`WsRpcGroup`, `WS_METHODS`), `apps/server/src/ws.ts` (churn 36/60d) and its scope map — high conflict. Instead: |
| 38 | + |
| 39 | +- **New file `apps/server/src/t3x/autoResume/http.ts`** exposing a raw `HttpRouter` with: |
| 40 | + - `GET /api/t3x/auto-resume?threadId=…` → `{ enabled, overridePrompt, pending: { resumeAtMs, reason } | null }` |
| 41 | + - `POST /api/t3x/auto-resume` body `{ threadId, enabled?, overridePrompt? }` → writes to `AutoResumeStore`. |
| 42 | +- **Auth:** copy the existing authenticated-raw-route pattern — mirror `otlpTracesProxyRouteLayer` / `authenticateRawRouteWithScope` in `server.ts` (same Bearer token + scope the web already holds). **Do not** create an unauthenticated endpoint. Confirm the exact helper name and required scope from the otlp route and reuse it verbatim. |
| 43 | +- **Mount:** one additive line in `server.ts`'s `makeRoutesLayer` `mergeAll`, beside `otlpTracesProxyRouteLayer` (+1 import). This is the only server seam. |
| 44 | +- **Sharing the store:** the route handler and the reactor must use the **same** `AutoResumeStore` instance. Wire `AutoResumeStoreLive` so it's visible to both the routes layer and the reactor (exploration flagged flipping `provide`→`provideMerge` for the store in `apps/server/src/t3x/index.ts` — confirm the layer graph and make the store a shared dependency, not two instances). |
| 45 | + |
| 46 | +### C. The control UI — new `apps/web/src/t3x/` |
| 47 | + |
| 48 | +- **New component** `apps/web/src/t3x/AutoResumeOverlay.tsx` — a floating/absolute-positioned card. Reuses existing primitives: `ui/switch` (toggle) and `ui/textarea` (resume message). Shows the live status line. |
| 49 | +- **Data:** an authenticated `fetch` to the route in (B), using the connection's `{ httpBaseUrl, httpAuthorization }` (find how the web already resolves these — likely an atom/context used by existing HTTP calls / `LocalApi`; reuse it). Debounce the textbox write; optimistic toggle. |
| 50 | +- **Mount (the one web seam):** in `apps/web/src/routes/_chat.$environmentId.$threadId.tsx`, add one import and render `<AutoResumeOverlay threadRef={threadRef} />` as a sibling of `<ChatView>` inside `<SidebarInset>` (~line 66; `threadRef` is already resolved there). This route is the server-thread view; the draft route is correctly excluded. |
| 51 | + |
| 52 | +### D. State + gating (fork-owned, zero seam) |
| 53 | + |
| 54 | +- **`apps/server/src/t3x/autoResume/state.ts`:** add `enabled` to `ThreadRecord` as **optional with default true** (`Schema.optionalWith(Schema.Boolean, { default: () => true })` or equivalent) so pre-existing state files still decode. Add a `setEnabled(threadId, enabled)` mutation and a `setOverridePrompt(threadId, text)` mutation (the store already carries `overridePrompt`; add the setter). |
| 55 | +- **`apps/server/src/t3x/autoResume/Reactor.ts`:** gate on `enabled`. In detection (`onRuntimeEvent`) skip scheduling when the thread is disabled; in `fireOne` treat disabled as a cancel reason (clear pending, optional activity note). Simplest: check `record.enabled !== false` in detection, and re-check in `fireOne` against a fresh read. |
| 56 | + |
| 57 | +## Data flow |
| 58 | + |
| 59 | +``` |
| 60 | +toggle/textbox → fetch POST /api/t3x/auto-resume → AutoResumeStore.setEnabled / setOverridePrompt (durable JSON) |
| 61 | +reactor detection/fireOne → reads enabled + overridePrompt from the same store |
| 62 | +card status ← fetch GET /api/t3x/auto-resume ← AutoResumeStore (enabled, overridePrompt, current pending) |
| 63 | +timeline status ← existing activity notes (no change) |
| 64 | +``` |
| 65 | + |
| 66 | +## Implementation phases |
| 67 | + |
| 68 | +**Phase 1 — server state + gating (no UI yet).** |
| 69 | + |
| 70 | +- Add `enabled` (optional, default true) + `setEnabled`/`setOverridePrompt` to `state.ts`. |
| 71 | +- Gate detection + `fireOne` on `enabled` in `Reactor.ts`. |
| 72 | +- Tests: extend `state.test.ts` (enabled round-trips, defaults true, old file without the field decodes to enabled=true); extend `Reactor.test.ts` (disabled thread neither schedules nor fires). |
| 73 | +- Verify: `pnpm run test src/t3x` green; `pnpm run typecheck` clean. |
| 74 | + |
| 75 | +**Phase 2 — the HTTP route.** |
| 76 | + |
| 77 | +- `http.ts` GET/POST over the shared `AutoResumeStore`, authenticated like `otlpTracesProxyRouteLayer`. |
| 78 | +- Share the store instance; mount via the single `server.ts` line (+import). |
| 79 | +- Test: a route-level test (auth required; GET/POST round-trip through a real store). Confirm an unauthenticated request is rejected. |
| 80 | +- Verify: typecheck + tests; `curl` the route against a locally booted server with the bearer token (see the parent conversation's headless-boot recipe) and confirm read/write. |
| 81 | + |
| 82 | +**Phase 3 — the web overlay.** |
| 83 | + |
| 84 | +- `apps/web/src/t3x/AutoResumeOverlay.tsx` + the single mount line in the chat route file. |
| 85 | +- Resolve `{ httpBaseUrl, httpAuthorization }` the same way existing web HTTP calls do; debounce textbox; optimistic toggle; render live status. |
| 86 | +- Verify: `pnpm run typecheck`; web lint; manual check in a locally built app — toggle persists across reload, textbox persists, status reflects a scheduled resume. |
| 87 | + |
| 88 | +**Phase 4 — docs + seam ledger.** |
| 89 | + |
| 90 | +- Update `docs/t3x/SEAMS.md`: record the 2 new seams (server.ts route mount; web route-file overlay mount) and confirm contracts untouched. |
| 91 | +- Update the parent auto-resume spec's B7 ("zero UI") to point here. |
| 92 | + |
| 93 | +## Guardrails (do not violate) |
| 94 | + |
| 95 | +- **Zero edits to `@t3tools/contracts`.** No new RPC method, no settings-schema field. |
| 96 | +- **No logic edits to hot chat files** (`ChatView.tsx` 51/60d, `ChatComposer` 30, `ws.ts` 36). The only upstream edits are the two additive mount lines named above. |
| 97 | +- New code lives under `apps/server/src/t3x/` and `apps/web/src/t3x/` only. |
| 98 | +- Follow Effect v4 conventions already used in `t3x/autoResume/*` (`Effect.catch`/`catchCause`, `Context.Service`, no `new Date()`/`Math.random`). |
| 99 | + |
| 100 | +## Out of scope (v1) |
| 101 | + |
| 102 | +- Global defaults UI (a `/settings` section). Low-conflict to add later (new route file + one nav line) but only holds global defaults, not per-thread state — skip unless wanted. |
| 103 | +- Manual "resume now" / "cancel scheduled resume" buttons — easy follow-ups on the same route; not required for this ask. |
0 commit comments