From 1cc7f68c15c2656557b077b98fd0c71f19d67a41 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 10 Aug 2026 03:20:56 +0200 Subject: [PATCH] feat(readmymind): phase 3 PR 1: alternates, rethink steering, phone surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alternate suggestions render as tappable rows that swap into the editable field, with their rationale as a visible second line (phones have no hover for a title tip). The predictor already returned up to 3 kind-diverse suggestions; the modal showed only the first. - Rethink now records every displayed suggestion as rejected and carries an optional free-text steer note (the API accepted steer since phase 2; the UI never collected it). Both reset on each open. The steer field stays available in the error phase: steering a failed run's retry is exactly when a note helps. - The modal header names the target session: overview rows and the accessory key can open it for a session that is not the active tab. - Phone surfaces: a keyboard-accessory 🧠 key (hidden unless readMyMindEnabled is ON and the active session is claude mode, re-derived after innerHTML rebuilds, settings applies, and session switches) and a Suggest strip on the phone overview's yellow waiting rows (waiting only: on red rows a dialog is on screen and input text would land in its menu; answer routing is phase 3 PR 2) - The modal renders as a compact sheet on phones with 16px inputs (iOS zoom guard) and 44px tap targets - Send/Insert/Rethink freeze during the loading phase so a stale suggestion cannot be sent mid-rethink - test/readmymind-phase3-surfaces.test.ts pins the load-bearing facts in CI: key in BOTH bar layouts, [hidden] re-assertion over inline-flex, header button off phones, the waiting-only row gate, no terminal refocus from the readmymind action, steer outside the result div Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- docs/readmymind-plan.md | 11 +- docs/readmymind.md | 9 +- src/web/public/app.js | 4 + src/web/public/i18n.js | 2 + src/web/public/index.html | 10 +- src/web/public/keyboard-accessory.js | 30 ++++++ src/web/public/mobile-overview.js | 41 +++++++- src/web/public/mobile.css | 27 ++++- src/web/public/readmymind-ui.js | 132 ++++++++++++++++++++---- src/web/public/settings-ui.js | 3 + src/web/public/styles.css | 87 ++++++++++++++++ test/readmymind-phase3-surfaces.test.ts | 104 +++++++++++++++++++ 13 files changed, 424 insertions(+), 38 deletions(-) create mode 100644 test/readmymind-phase3-surfaces.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index e6792be6..451d4ac4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,7 +210,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **Approvals Inbox** (cross-session queue of prompts waiting on a human; `approvalsInboxEnabled`, SYNCED, default OFF: every surface is opt-in; only the store and answer endpoints run regardless, so flipping it ON shows anything already pending): `web/approval-inbox.ts` is a `sessionWaits`-style singleton fed by `/api/hook-event`, holding at most ONE item per session (a new prompt supersedes), claude-mode only, in-memory. Cards are answered via `POST /api/approvals/:id/answer`, which sends a digit / Esc / idle-prompt text through `writeViaMux` (menu answers never carry `\r`). ⚠️ `option` digits are accepted ONLY when they match options parsed from the captured pane frame, and the answer path RE-CAPTURES the pane first (a dialog that no longer parses on screen means the keystroke would land in the composer, so refuse with 409). ⚠️ Resolution on the heuristic `working` signal is restricted to `idle` items; permission/question items clear only on definitive signals (`stop`, `elicitation_complete`/`elicitation_response`, exit/delete, answer, supersede, 12h TTL). The frontend seeds from `GET /api/approvals` in `handleInit` (which is what makes tab alerts survive reloads), but only with the setting ON; push Approve/Deny buttons are also gated on it (`sendPushNotifications` strips `actions`/`approvalId` when OFF) and are answered from `sw.js` directly so they work with no tab open. Surfaces (all gated on the setting): header bell (marker-hidden until count > 0, phones never show it) + drawer (`approvals-ui.js`), phone overview NEEDS YOU answer strips (`mobile-overview.js`). Design: `docs/approvals-inbox-plan.md`. -**Read My Mind intent profiles** (phase 1 of `docs/readmymind-plan.md`; `readMyMindEnabled`, SYNCED, default OFF): per-CASE profiles (user-stated `goals` + the user's recent real prompts), keyed by owner + realpath(workingDir) so they survive `/clear`/respawns and multi-user scoping is structural. Capture rides the transcript (`transcript:user_prompt` from `transcript-watcher.ts`), NOT the input paths: `POST /input` sees only programmatic prompts and the WS channel is raw keystrokes. The listener lives inside `startTranscriptWatcher()`'s `if (!watcher)` block (outside it would duplicate per hook event) and is claude-only + gated on the setting per event. Store: `src/intent-store.ts` singleton, `intents.json` written 0600 tmp+rename (prompts can contain secrets; never fed to `/api/search`). Endpoints: GET/PUT/DELETE `/api/sessions/:id/intent` + POST `/api/sessions/:id/readmymind` (`readmymind-routes.ts`, ownership via `findSessionOrFail` WITH `req`; registrations stay the bare `app.('path')` shape, the endpoints.md drift scanner cannot see generics). **Phase 2 (predictor + 🧠 button)**: `readmymind-context.ts` is the PURE budgeted assembler (9 ranked sources, drop order siblings→away→workspace→tools, sections 1-4 truncate only); IO lives in `readmymind-collectors.ts` (transcript TAIL read — the live watcher keeps only a 500-char snippet — + git signals, skipped for remote-SSH cases) and the route; `readmymind-predictor.ts` reuses the AiCheckerBase spawn mechanics standalone (verdict-shaped base vs freeform JSON) as a mutable singleton routes call and tests stub. Claude-mode only (400), one in flight per session (409 CONFLICT), model = `readMyMindModel` setting defaulting to `AI_CHECK_MODEL` (opus, decided). Frontend `readmymind-ui.js`: header 🧠 marker-hidden (`btn-readmymind--hidden`) until the setting is ON, desktop-only (mobile.css hides it; phone key is phase 3); suggestions render via value/`textContent` ONLY and Send/Insert go through `POST /input` (server-side, so the sendEnterKey/local-echo trap does not apply) — nothing auto-sends, ever. User guide: `docs/readmymind.md`. +**Read My Mind intent profiles** (phase 1 of `docs/readmymind-plan.md`; `readMyMindEnabled`, SYNCED, default OFF): per-CASE profiles (user-stated `goals` + the user's recent real prompts), keyed by owner + realpath(workingDir) so they survive `/clear`/respawns and multi-user scoping is structural. Capture rides the transcript (`transcript:user_prompt` from `transcript-watcher.ts`), NOT the input paths: `POST /input` sees only programmatic prompts and the WS channel is raw keystrokes. The listener lives inside `startTranscriptWatcher()`'s `if (!watcher)` block (outside it would duplicate per hook event) and is claude-only + gated on the setting per event. Store: `src/intent-store.ts` singleton, `intents.json` written 0600 tmp+rename (prompts can contain secrets; never fed to `/api/search`). Endpoints: GET/PUT/DELETE `/api/sessions/:id/intent` + POST `/api/sessions/:id/readmymind` (`readmymind-routes.ts`, ownership via `findSessionOrFail` WITH `req`; registrations stay the bare `app.('path')` shape, the endpoints.md drift scanner cannot see generics). **Phase 2 (predictor + 🧠 button)**: `readmymind-context.ts` is the PURE budgeted assembler (9 ranked sources, drop order siblings→away→workspace→tools, sections 1-4 truncate only); IO lives in `readmymind-collectors.ts` (transcript TAIL read — the live watcher keeps only a 500-char snippet — + git signals, skipped for remote-SSH cases) and the route; `readmymind-predictor.ts` reuses the AiCheckerBase spawn mechanics standalone (verdict-shaped base vs freeform JSON) as a mutable singleton routes call and tests stub. Claude-mode only (400), one in flight per session (409 CONFLICT), model = `readMyMindModel` setting defaulting to `AI_CHECK_MODEL` (opus, decided). Frontend `readmymind-ui.js`: header 🧠 marker-hidden (`btn-readmymind--hidden`) until the setting is ON, desktop; phones get the keyboard-accessory 🧠 key (`refreshReadMyMind()` in keyboard-accessory.js, re-derived after every innerHTML rebuild + settings apply + the tab-render tail; `.accessory-btn[hidden]` must stay re-asserted, inline-flex beats the UA hidden rule) plus a `🧠 Suggest` strip on the overview's YELLOW waiting rows ONLY (on red rows a dialog is on screen and `POST /input` text would land in its menu; answer routing via the approvals endpoint is phase-3 PR 2). Up to 3 kind-diverse suggestions: alternates swap into the editable field, Rethink records everything displayed as rejected and carries the optional steer note (resets each open, like the rejections); suggestions render via value/`textContent` ONLY and Send/Insert go through `POST /input` (server-side, so the sendEnterKey/local-echo trap does not apply) — nothing auto-sends, ever. User guide: `docs/readmymind.md`. **Agent Teams**: `TeamWatcher` polls `~/.claude/teams/`, matches to sessions via `leadSessionId`. Teammates are in-process threads appearing as subagents. Enable: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. See `docs/agent-teams/`. diff --git a/docs/readmymind-plan.md b/docs/readmymind-plan.md index d263bf07..8e27edf8 100644 --- a/docs/readmymind-plan.md +++ b/docs/readmymind-plan.md @@ -122,14 +122,15 @@ Agent use cases this unlocks: a lead session records intentions as the user stat ## Phases -1. **Intent store + capture + intent endpoints + skill docs.** Immediately useful to agents even before any UI exists. -2. **Context assembler + predictor + predict endpoint + desktop button/modal.** The feature as pitched. The assembler ships with all collectors it can serve from day one (transcript, intent, git, run-summary, siblings); the approvals collector activates when PR #245 lands. -3. **Phone accessory key, rethink steering, alternates row.** -4. Explicitly later: proactive predict-on-idle (ghost suggestion chip), auto-compaction of `recentPrompts` into `goals` via a cheap model, codex/gemini capture, cross-case "global" intent. +1. **Intent store + capture + intent endpoints + skill docs.** Immediately useful to agents even before any UI exists. Shipped 1.16.1 (PR #253). +2. **Context assembler + predictor + predict endpoint + desktop button/modal.** The feature as pitched. The assembler ships with all collectors it can serve from day one (transcript, intent, git, run-summary, siblings); the approvals collector activates when PR #245 lands. Shipped 1.16.2 (PR #256). +3. **Phase 3, PR 1: alternates row, rethink steering, phone surfaces.** Alternate suggestions as tappable rows that swap into the field; Rethink records everything displayed as rejected and carries the optional steer note; keyboard-accessory 🧠 key (`refreshReadMyMind()`, re-derived after every innerHTML rebuild, settings apply, and session switch); `🧠 Suggest` strip on the phone overview's YELLOW waiting rows only. Red rows deliberately get no shortcut yet: a dialog is on screen there and text sent via `POST /input` would land in its menu. +4. **Phase 3, PR 2: the approvals fusion** (the Cloudflare OS learnings tie-in, items 1-2 of `cloudflare-os-learnings-plan.md`). `GET /api/sessions/:id/recap`: a deterministic "what was done, simplified" catch-up (last assistant tail + recent tool one-liners + git state) reusing the phase-2 collectors verbatim; no model call, effectively an observation-ledger v0 whose data source can later swap to a real ledger. Surfaces: collapsible "What happened" on approval cards (lazy-fetched), recap in the modal's loading phase (read while opus thinks), 🧠 on approval cards and red overview rows. Answer-aware Send: the predictor output gains an optional `answer` option number (validated against the pending dialog's parsed options, dropped when invalid) and the modal routes dialog answers through `POST /api/approvals/:id/answer` (option digits / idle text), keeping `POST /input` only for dialog-free sessions. +5. Explicitly later: proactive predict-on-idle (ghost suggestion chip), auto-compaction of `recentPrompts` into `goals` via a cheap model, codex/gemini capture, cross-case "global" intent, a model-written prose recap (deterministic-only in v1). ## Open questions -- Should Rethink's rejected-suggestion memory persist across modal closes, or reset each open? +- ~~Should Rethink's rejected-suggestion memory persist across modal closes, or reset each open?~~ Decided in phase 2 and kept: reset each open (a fresh open is a fresh question); the steer note resets with it. - Is a composer-adjacent placement (next to the toolbar Run controls) better than the header for discoverability? - Pending-dialog input (source #1) consumes the approvals-inbox store (PR #245, merged): the phase-2 collector reads pending items directly from `src/approval-inbox.ts`. diff --git a/docs/readmymind.md b/docs/readmymind.md index fee5d477..6ff32b4c 100644 --- a/docs/readmymind.md +++ b/docs/readmymind.md @@ -23,11 +23,11 @@ Add `-u user:password` if your install has `CODEMAN_PASSWORD` set, and drop `-k` ## The 🧠 button -On a Claude session, press the brain button in the header (desktop; the phone surface is a planned keyboard-accessory key). Codeman assembles everything it already knows: your goals, your recent prompts (with your voice: length, tone, shorthand), the tail of the last assistant reply, recent tool activity, git state (branch, dirty files, pending changesets), how long you have been away and what happened meanwhile, sibling sessions in the same case, and any dialog the session is currently waiting on. A one-shot model call (opus by default, `readMyMindModel` to override) turns that into 1-3 suggestions; the top one lands in an editable field with its rationale. +On a Claude session, press the brain button in the header (desktop). On phones the same modal opens from the 🧠 key on the keyboard accessory bar, or from the `🧠 Suggest` strip under a yellow waiting row on the phone overview home screen (steer a session without opening its tab). Codeman assembles everything it already knows: your goals, your recent prompts (with your voice: length, tone, shorthand), the tail of the last assistant reply, recent tool activity, git state (branch, dirty files, pending changesets), how long you have been away and what happened meanwhile, sibling sessions in the same case, and any dialog the session is currently waiting on. A one-shot model call (opus by default, `readMyMindModel` to override) turns that into 1-3 kind-diverse suggestions (continue / verify / redirect); the top one lands in an editable field with its rationale, and the others render as tappable alternate rows that swap into the field. - **Send** submits it to the session (with Enter). - **Insert** drops it on the CLI composer *without* Enter, so you can edit it in the terminal before sending. -- **Rethink** re-runs with the shown suggestion recorded as rejected. +- **Rethink** re-runs with everything currently displayed recorded as rejected. The optional steer field above the buttons ("no, I meant the mobile bug") tells the re-run what you actually meant; it is your own words and outranks everything the predictor observed. - **Dismiss** closes; nothing happens. A prediction takes 5-90 seconds and costs real tokens; one runs per session at a time. If the session is sitting on a permission/question dialog, the suggestion is usually an answer to that dialog: that is intentional. @@ -87,13 +87,14 @@ The `codeman` agent skill documents the same verbs (SKILL.md §3 plus `reference ## What comes next (phase 3+) -Phone keyboard-accessory 🧠 key, a steer-note input on Rethink, and tappable alternate suggestions. Explicitly later: proactive predict-on-idle, auto-compaction of the prompt history into goals, non-Claude capture. See the phases section of [`readmymind-plan.md`](readmymind-plan.md). +The approvals fusion: a per-session catch-up recap ("what was done, simplified", derived from the same collectors the predictor uses), shown on Approvals Inbox cards next to a 🧠 button, plus answer-aware Send that routes dialog answers through the approvals endpoint instead of typing into a menu. Explicitly later: proactive predict-on-idle, auto-compaction of the prompt history into goals, non-Claude capture. See the phases section of [`readmymind-plan.md`](readmymind-plan.md). ## Troubleshooting | Symptom | Cause / fix | | ------- | ----------- | -| No 🧠 button in the header | `readMyMindEnabled` is OFF (App Settings → Panels), you are on a phone (desktop-only in this phase), or the active session is not claude-mode | +| No 🧠 button in the header | `readMyMindEnabled` is OFF (App Settings → Panels), you are on a phone (the header button is desktop-only; phones use the keyboard-accessory 🧠 key and the overview waiting rows), or the active session is not claude-mode | +| No 🧠 key on the phone keyboard bar | `readMyMindEnabled` is OFF, or the active session is not claude-mode (the key hides itself for codex/gemini/opencode/antigravity/shell sessions) | | Prediction feels generic | The profile is thin: record goals (PUT or ask your agent to), and let capture accumulate a few real prompts first | | "A prediction is already running" (409) | One per session at a time; wait for the current one (up to 90 s) | | Prediction fails (502) | The model returned no usable JSON, or the CLI could not start; retry. Check `readMyMindModel` if you overrode it | diff --git a/src/web/public/app.js b/src/web/public/app.js index f475cb67..8b5fc085 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -3688,6 +3688,10 @@ class CodemanApp { this._refreshMobileOverviewIfVisible?.(); // Same deal for the desktop home screen's tab column. this._refreshHomeSessionsIfVisible?.(); + // The keyboard accessory bar's 🧠 key depends on the active session's mode + // (claude only) and the readMyMindEnabled setting; session switches and + // mode changes all funnel through this render. + if (typeof KeyboardAccessoryBar !== 'undefined') KeyboardAccessoryBar.refreshReadMyMind?.(); } // Auto-wrap desktop session tabs to a second row when they overflow one row, diff --git a/src/web/public/i18n.js b/src/web/public/i18n.js index a73c4c16..805f0137 100644 --- a/src/web/public/i18n.js +++ b/src/web/public/i18n.js @@ -257,6 +257,8 @@ 'Reading your mind…': '正在读取您的想法…', 'No suggestion this time. Rethink to try again.': '这次没有建议。点击「重想」再试一次。', Rethink: '重想', + 'Steer the rethink (optional): tell it what you actually meant': '引导重想(可选):告诉它您真正的意思', + 'Optional steer note for Rethink': '重想的可选引导说明', Insert: '插入', "Put the text on the session's composer without submitting it": '将文本放入会话输入框但不提交', 'Predicted prompt, editable': '预测的提示,可编辑', diff --git a/src/web/public/index.html b/src/web/public/index.html index 6ec66057..8e4d30e0 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -2802,7 +2802,7 @@ + + diff --git a/src/web/public/keyboard-accessory.js b/src/web/public/keyboard-accessory.js index a254b96c..89415e66 100644 --- a/src/web/public/keyboard-accessory.js +++ b/src/web/public/keyboard-accessory.js @@ -441,6 +441,7 @@ const KeyboardAccessoryBar = { + + @@ -528,6 +530,8 @@ const KeyboardAccessoryBar = { if (toolbar && toolbar.parentNode) { toolbar.parentNode.insertBefore(this.element, toolbar); } + + this.refreshReadMyMind(); }, /** Switch between 'simple' and 'extended' button layouts */ @@ -536,6 +540,27 @@ const KeyboardAccessoryBar = { this._mode = mode; this.clearConfirm(); this.element.innerHTML = mode === 'extended' ? this._extendedButtons : this._simpleButtons; + // innerHTML rebuilds ship the 🧠 key with its default `hidden` attribute; + // re-derive its visibility for the fresh element. + this.refreshReadMyMind(); + }, + + /** + * Show the 🧠 key only when Read My Mind is opted in (`readMyMindEnabled`, + * synced) AND the active session is claude mode (the predictor is a 400 on + * every other CLI). Called after every innerHTML rebuild, from + * applyHeaderVisibilitySettings (settings load/save), and from the tab + * renderer tail (session switches funnel through it). + */ + refreshReadMyMind() { + if (!this.element) return; + const btn = this.element.querySelector('[data-action="readmymind"]'); + if (!btn) return; + const hasApp = typeof app !== 'undefined' && app; + const session = hasApp && app.activeSessionId ? app.sessions?.get(app.activeSessionId) : null; + const enabled = hasApp && app.readMyMindEnabled?.() === true; + const claudeMode = !!session && (!session.mode || session.mode === 'claude'); + btn.hidden = !(enabled && claudeMode); }, _confirmTimer: null, @@ -613,6 +638,11 @@ const KeyboardAccessoryBar = { case 'pick-path': this.pickPath(); break; + case 'readmymind': + // Opens the shared prediction modal (readmymind-ui.js). Deliberately + // not in refocusActions: the modal takes over and the keyboard closes. + app.openReadMyMind?.(); + break; case 'clear-input': app.clearTerminalInput?.(); break; diff --git a/src/web/public/mobile-overview.js b/src/web/public/mobile-overview.js index 7c514f5b..9b6492eb 100644 --- a/src/web/public/mobile-overview.js +++ b/src/web/public/mobile-overview.js @@ -642,12 +642,16 @@ Object.assign(CodemanApp.prototype, { // Approvals Inbox: a pending dialog for this session gets an answer strip // BELOW the row (the row itself is a