feat(readmymind): rethink steer note (phase 3 part 2)

Adds the optional free-text steer note to the Read My Mind modal: a
dashed input under the suggestions ("no, I meant the mobile bug") that
rides along as `steer` on every Rethink. The API already accepted it;
this wires the frontend end of the contract.

- Shown whenever Rethink is live (ready AND empty-result phases),
  hidden only while a prediction runs; typed text survives re-runs.
- Enter in the field triggers Rethink, mirroring the prompt field's
  Enter-to-send; a fresh open clears it with the rethink memory.
- Trimmed and capped to the schema's 2000 chars on the way out; a
  plain open still sends an empty body (neither steer nor rejected).
- zh-CN strings for the placeholder and aria-label, phone-sized
  touch target in mobile.css, static guards in the phase-3 test.

Verified with a browser E2E against a live dev server (stubbed predict
endpoint): payload contents, phase visibility, Enter wiring, and
reset-on-reopen all asserted with real keystrokes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-10 10:41:39 +02:00
parent 752374abc7
commit 831af88579
9 changed files with 78 additions and 8 deletions
+2
View File
@@ -261,6 +261,8 @@
"Put the text on the session's composer without submitting it": '将文本放入会话输入框但不提交',
'Predicted prompt, editable': '预测的提示,可编辑',
'Use this suggestion instead': '改用此建议',
"Steer the rethink, e.g. 'no, I meant the mobile bug'": '引导重想,例如:"不,我是指移动端的问题"',
'Steer note for Rethink': '重想的引导备注',
'Select a session first': '请先选择一个会话',
'Read My Mind works on Claude sessions only': '读心术仅适用于 Claude 会话',
'Prompt sent': '提示已发送',
+7
View File
@@ -2895,6 +2895,13 @@
<div class="readmymind-alternates" id="readMyMindAlternates" data-i18n-skip style="display:none"></div>
</div>
<div class="readmymind-error" style="display:none">No suggestion this time. Rethink to try again.</div>
<!-- Rethink steer note: the user's own words about what they actually
meant, sent as `steer` with the next Rethink. Shown whenever
Rethink is live (ready AND empty-result phases), hidden while a
prediction runs. -->
<div class="readmymind-steer-row" id="readMyMindSteerRow" style="display:none">
<input type="text" id="readMyMindSteer" class="readmymind-steer-input" maxlength="2000" placeholder="Steer the rethink, e.g. 'no, I meant the mobile bug'" aria-label="Steer note for Rethink" onkeydown="if(event.key==='Enter')app.rethinkReadMyMind()">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="app.closeReadMyMind()">Dismiss</button>
+3
View File
@@ -1350,6 +1350,9 @@ html.mobile-init .file-browser-panel {
.readmymind-alt {
min-height: 38px;
}
.readmymind-steer-input {
min-height: 38px;
}
/* Modal safe area padding - all sides for full-screen modals */
.ios-device .modal-content {
+20 -4
View File
@@ -10,7 +10,8 @@
* suggestions render as tappable alternate rows that swap into the field
* without losing edits. Buttons are Send (with Enter), Insert (drop on the CLI
* composer WITHOUT Enter, for editing), Rethink (re-run with the whole shown
* set, main + alternates, recorded as rejected), Dismiss.
* set, main + alternates, recorded as rejected, plus the optional free-text
* steer note, e.g. "no, I meant the mobile bug", sent as `steer`), Dismiss.
*
* Suggestions are NEVER auto-sent: the explicit click here is the security
* boundary for observed/injectable predictor inputs, so suggestion text is
@@ -47,8 +48,11 @@ Object.assign(CodemanApp.prototype, {
this.showToast('Read My Mind works on Claude sessions only', 'warning');
return;
}
// Rethink memory resets on each open (a fresh open is a fresh question).
// Rethink memory resets on each open (a fresh open is a fresh question),
// and the steer note resets with it.
this._rmm = { sessionId, suggestions: [], selected: 0, rejected: [], busy: false };
const steer = document.getElementById('readMyMindSteer');
if (steer) steer.value = '';
document.getElementById('readMyMindModal')?.classList.add('active');
this._readMyMindPredict();
},
@@ -65,7 +69,13 @@ Object.assign(CodemanApp.prototype, {
state.busy = true;
this._rmmSetPhase('loading');
const body = state.rejected.length > 0 ? { rejected: state.rejected.slice(-10) } : {};
const body = {};
if (state.rejected.length > 0) body.rejected = state.rejected.slice(-10);
// The steer note rides every re-run while it stays in the field: what the
// user sees in the box is what the predictor gets. Empty on first open
// (openReadMyMind clears it), so a plain predict sends neither key.
const steer = document.getElementById('readMyMindSteer')?.value.trim() ?? '';
if (steer) body.steer = steer.slice(0, 2000);
const data = await this._apiJson(`/api/sessions/${state.sessionId}/readmymind`, { method: 'POST', body });
// The modal may have been dismissed (or reopened for another session) while
@@ -176,7 +186,8 @@ Object.assign(CodemanApp.prototype, {
},
/** Re-run with the whole shown set (main + alternates) recorded as rejected:
* the user saw every row and asked for something else. */
* the user saw every row and asked for something else. The steer note (if
* any) is read from the field by _readMyMindPredict itself. */
rethinkReadMyMind() {
const state = this._rmm;
if (!state || state.busy) return;
@@ -193,6 +204,11 @@ Object.assign(CodemanApp.prototype, {
modal.querySelector('.readmymind-loading').style.display = phase === 'loading' ? '' : 'none';
modal.querySelector('.readmymind-result').style.display = phase === 'ready' ? '' : 'none';
modal.querySelector('.readmymind-error').style.display = phase === 'error' ? '' : 'none';
// The steer note belongs to Rethink, so it shows wherever Rethink is live:
// the ready phase AND the empty-result phase (typed text survives the
// loading round-trip, only the row's visibility toggles).
const steerRow = document.getElementById('readMyMindSteerRow');
if (steerRow) steerRow.style.display = phase === 'loading' ? 'none' : '';
const rethinkBtn = document.getElementById('readMyMindRethink');
if (rethinkBtn) rethinkBtn.disabled = phase === 'loading';
},
+26
View File
@@ -10901,6 +10901,32 @@ kbd {
white-space: nowrap;
font-family: var(--mono-font, monospace);
}
/* Rethink steer note: the user's own words about what they actually meant,
sent as `steer` on the next re-run (readmymind-ui.js _readMyMindPredict).
Dashed border marks it as the optional side channel, distinct from the
primary suggestion field above; it solidifies on focus. */
.readmymind-steer-row {
margin-top: 10px;
}
.readmymind-steer-input {
width: 100%;
min-width: 0;
font-size: 12px;
padding: 7px 10px;
background: transparent;
color: var(--text);
border: 1px dashed var(--control-border);
border-radius: 8px;
}
.readmymind-steer-input:focus {
outline: none;
border-style: solid;
border-color: var(--accent);
background: var(--bg-dark);
}
.readmymind-steer-input::placeholder {
color: var(--text-dim);
}
/* Keyboard-accessory 🧠 key: the phone surface for the same opt-in setting
(the header 🧠 button stays phone-hidden in mobile.css). The key ships in