mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat(readmymind): phase 3 PR 1: alternates, rethink steering, phone surfaces
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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': '预测的提示,可编辑',
|
||||
|
||||
@@ -2802,7 +2802,7 @@
|
||||
<div class="modal-backdrop" onclick="app.closeReadMyMind()"></div>
|
||||
<div class="modal-content readmymind-modal">
|
||||
<div class="modal-header">
|
||||
<h3><span aria-hidden="true">🧠</span> Read My Mind</h3>
|
||||
<h3><span aria-hidden="true">🧠</span> Read My Mind<span class="readmymind-session" id="readMyMindSession" data-i18n-skip></span></h3>
|
||||
<button class="modal-close" onclick="app.closeReadMyMind()" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -2813,14 +2813,18 @@
|
||||
<input type="text" id="readMyMindPrompt" class="readmymind-prompt-input" data-i18n-skip aria-label="Predicted prompt, editable" onkeydown="if(event.key==='Enter')app.sendReadMyMind(true)">
|
||||
</div>
|
||||
<div class="readmymind-why" id="readMyMindWhy" data-i18n-skip></div>
|
||||
<div class="readmymind-alternates" id="readMyMindAlternates" data-i18n-skip></div>
|
||||
</div>
|
||||
<div class="readmymind-error" style="display:none">No suggestion this time. Rethink to try again.</div>
|
||||
<!-- Outside the result div on purpose: steering matters in the error
|
||||
phase too (Rethink retries with the note); hidden only while loading. -->
|
||||
<input type="text" id="readMyMindSteer" class="readmymind-steer-input" style="display:none" placeholder="Steer the rethink (optional): tell it what you actually meant" aria-label="Optional steer note for Rethink" onkeydown="if(event.key==='Enter')app.rethinkReadMyMind()">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="app.closeReadMyMind()">Dismiss</button>
|
||||
<button class="btn btn-secondary" id="readMyMindRethink" onclick="app.rethinkReadMyMind()">Rethink</button>
|
||||
<button class="btn btn-secondary" onclick="app.sendReadMyMind(false)" title="Put the text on the session's composer without submitting it">Insert</button>
|
||||
<button class="btn btn-primary" onclick="app.sendReadMyMind(true)">Send</button>
|
||||
<button class="btn btn-secondary" id="readMyMindInsert" onclick="app.sendReadMyMind(false)" title="Put the text on the session's composer without submitting it">Insert</button>
|
||||
<button class="btn btn-primary" id="readMyMindSend" onclick="app.sendReadMyMind(true)">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -441,6 +441,7 @@ const KeyboardAccessoryBar = {
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="accessory-btn accessory-btn-readmymind" data-action="readmymind" title="Read My Mind: predict your next prompt" hidden>🧠</button>
|
||||
<button class="accessory-btn" data-action="esc" title="Escape">Esc</button>
|
||||
<button class="accessory-btn accessory-btn-dismiss" data-action="dismiss" title="Dismiss keyboard">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||
@@ -477,6 +478,7 @@ const KeyboardAccessoryBar = {
|
||||
</svg>
|
||||
</button>
|
||||
<button class="accessory-btn" data-action="pick-path" title="Insert a file or folder path">📁 Path</button>
|
||||
<button class="accessory-btn accessory-btn-readmymind" data-action="readmymind" title="Read My Mind: predict your next prompt" hidden>🧠</button>
|
||||
<button class="accessory-btn" data-action="clear-input" title="Clear the current unsent input">⌫ All</button>
|
||||
<button class="accessory-btn" data-action="tab" title="Tab">Tab</button>
|
||||
<button class="accessory-btn" data-action="shift-tab" title="Shift+Tab">⇧Tab</button>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <button>, so actions cannot nest
|
||||
// inside it). Tapping the row still opens the session, unchanged.
|
||||
// Waiting rows get the Read My Mind shortcut strip the same way.
|
||||
const approval = this._pendingApprovalForSession(row.id);
|
||||
if (approval) {
|
||||
const rmmShortcut = !approval && this._readMyMindRowShortcut(row);
|
||||
if (approval || rmmShortcut) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'mobile-overview-row-wrap';
|
||||
wrap.appendChild(item);
|
||||
wrap.appendChild(this._buildMobileOverviewApprovalStrip(approval));
|
||||
wrap.appendChild(
|
||||
approval ? this._buildMobileOverviewApprovalStrip(approval) : this._buildMobileOverviewRmmStrip(row.id)
|
||||
);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -691,6 +695,39 @@ Object.assign(CodemanApp.prototype, {
|
||||
return strip;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a NEEDS YOU row gets the Read My Mind shortcut: opt-in setting,
|
||||
* live claude-mode session, and only the `waiting` state. A yellow row is a
|
||||
* bare composer waiting for a prompt; on red rows a dialog is on screen and
|
||||
* sent text would land in its menu, so those keep the approval strip (or the
|
||||
* open-the-session path) until the predictor can answer dialogs through the
|
||||
* approvals endpoint (phase 3 PR 2, docs/readmymind-plan.md).
|
||||
*/
|
||||
_readMyMindRowShortcut(row) {
|
||||
if (row.state !== 'waiting') return false;
|
||||
if (!this.readMyMindEnabled || !this.readMyMindEnabled()) return false;
|
||||
const session = this.sessions.get(row.id);
|
||||
return !!session && (!session.mode || session.mode === 'claude');
|
||||
},
|
||||
|
||||
/** A one-button strip opening the prediction modal for this row's session. */
|
||||
_buildMobileOverviewRmmStrip(sessionId) {
|
||||
const strip = document.createElement('div');
|
||||
strip.className = 'mobile-overview-approval-strip';
|
||||
strip.setAttribute('data-i18n-skip', '');
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'mobile-overview-approval-btn rmm';
|
||||
btn.textContent = '🧠 Suggest';
|
||||
btn.setAttribute('aria-label', 'Predict my next prompt');
|
||||
btn.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
this.openReadMyMind(sessionId);
|
||||
});
|
||||
strip.appendChild(btn);
|
||||
return strip;
|
||||
},
|
||||
|
||||
/** A past conversation. Tapping it resumes, which creates a fresh session. */
|
||||
_buildMobileOverviewPastRow(row) {
|
||||
const item = document.createElement('button');
|
||||
|
||||
@@ -530,8 +530,9 @@ html.mobile-init .file-browser-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Read My Mind 🧠 button: desktop header only in phase 2; the phone surface
|
||||
is a planned keyboard-accessory key (docs/readmymind-plan.md phase 3). */
|
||||
/* Read My Mind 🧠 button: desktop header only. The phone surfaces are the
|
||||
keyboard-accessory 🧠 key and the overview's waiting-row Suggest strip
|
||||
(docs/readmymind-plan.md phase 3), both opening the same modal. */
|
||||
.btn-icon-header.btn-readmymind {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1290,8 +1291,11 @@ html.mobile-init .file-browser-panel {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Small modals (confirmations, dialogs) — auto-height, not full-screen */
|
||||
.modal-content.modal-sm {
|
||||
/* Small modals (confirmations, dialogs) — auto-height, not full-screen.
|
||||
Read My Mind is a decision dialog, not a workspace, so it gets the same
|
||||
compact-sheet treatment instead of the full-screen default. */
|
||||
.modal-content.modal-sm,
|
||||
.modal-content.readmymind-modal {
|
||||
height: auto;
|
||||
max-height: 85vh;
|
||||
border-radius: 12px;
|
||||
@@ -1299,6 +1303,16 @@ html.mobile-init .file-browser-panel {
|
||||
width: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
/* Read My Mind phone polish: iOS zooms the page when focusing sub-16px
|
||||
inputs, and alternate rows are tap targets. */
|
||||
.readmymind-prompt-input,
|
||||
.readmymind-steer-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
.readmymind-alt {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Modal safe area padding - all sides for full-screen modals */
|
||||
.ios-device .modal-content {
|
||||
padding-top: var(--safe-area-top);
|
||||
@@ -2600,6 +2614,11 @@ html.mobile-init .file-browser-panel {
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
/* Read My Mind shortcut on waiting rows (mobile-overview.js). */
|
||||
.mobile-overview-approval-btn.rmm {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Attention states mirror the session tabs exactly: red blink when the agent
|
||||
asked something (permission / question), yellow blink when it is waiting for
|
||||
|
||||
+113
-19
@@ -5,9 +5,16 @@
|
||||
* setting is ON) opens a modal that asks the server for the user's most likely
|
||||
* next prompt (`POST /api/sessions/:id/readmymind`, one-shot predictor over the
|
||||
* case's intent profile + live session signals). The top suggestion lands in an
|
||||
* editable single-line field with its rationale below; buttons are Send (with
|
||||
* Enter), Insert (drop on the CLI composer WITHOUT Enter, for editing), Rethink
|
||||
* (re-run with the shown suggestion recorded as rejected), Dismiss.
|
||||
* editable single-line field with its rationale below; the predictor returns up
|
||||
* to 3 kind-diverse suggestions (continue / verify / redirect) and the rest
|
||||
* render as tappable alternate rows that swap into the field. Buttons are Send
|
||||
* (with Enter), Insert (drop on the CLI composer WITHOUT Enter, for editing),
|
||||
* Rethink (re-run with every displayed suggestion recorded as rejected, plus an
|
||||
* optional free-text steer note from the field above the buttons), Dismiss.
|
||||
*
|
||||
* Phone surfaces reuse this same modal: the keyboard-accessory 🧠 key
|
||||
* (keyboard-accessory.js) and the phone overview's waiting-row shortcut
|
||||
* (mobile-overview.js) both call openReadMyMind(sessionId).
|
||||
*
|
||||
* Suggestions are NEVER auto-sent: the explicit click here is the security
|
||||
* boundary for observed/injectable predictor inputs, so suggestion text is
|
||||
@@ -31,10 +38,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
return this.loadAppSettingsFromStorage().readMyMindEnabled === true;
|
||||
},
|
||||
|
||||
/** Open the modal for the active session and start a prediction. */
|
||||
openReadMyMind() {
|
||||
const sessionId = this.activeSessionId;
|
||||
const session = sessionId ? this.sessions.get(sessionId) : null;
|
||||
/**
|
||||
* Open the modal and start a prediction. Defaults to the active session;
|
||||
* phone surfaces (accessory key, overview rows) pass an explicit id so a
|
||||
* prediction can start without switching tabs first.
|
||||
*/
|
||||
openReadMyMind(sessionId) {
|
||||
const id = sessionId || this.activeSessionId;
|
||||
const session = id ? this.sessions.get(id) : null;
|
||||
if (!session) {
|
||||
this.showToast('Select a session first', 'warning');
|
||||
return;
|
||||
@@ -43,8 +54,15 @@ 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).
|
||||
this._rmm = { sessionId, shown: null, rejected: [], busy: false };
|
||||
// Rethink memory resets on each open (a fresh open is a fresh question),
|
||||
// and so does the steer note (it belongs to the question it steered).
|
||||
this._rmm = { sessionId: id, shown: null, alternates: [], rejected: [], busy: false };
|
||||
const steer = document.getElementById('readMyMindSteer');
|
||||
if (steer) steer.value = '';
|
||||
// Name the target in the header: overview rows and the accessory key can
|
||||
// open this for a session that is not the active tab.
|
||||
const sessionLabel = document.getElementById('readMyMindSession');
|
||||
if (sessionLabel) sessionLabel.textContent = this.getSessionName?.(session) || session.name || '';
|
||||
document.getElementById('readMyMindModal')?.classList.add('active');
|
||||
this._readMyMindPredict();
|
||||
},
|
||||
@@ -54,14 +72,25 @@ Object.assign(CodemanApp.prototype, {
|
||||
this._rmm = null;
|
||||
},
|
||||
|
||||
/** Run (or re-run) the prediction and render the top suggestion. */
|
||||
/** Run (or re-run) the prediction and render the suggestions. */
|
||||
async _readMyMindPredict() {
|
||||
const state = this._rmm;
|
||||
if (!state || state.busy) return;
|
||||
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 is the user's own words; server-side it rides in the
|
||||
// highest-authority context tier. Bounds mirror ReadMyMindPredictSchema.
|
||||
const steerEl = document.getElementById('readMyMindSteer');
|
||||
const steer = steerEl
|
||||
? steerEl.value
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 2000)
|
||||
: '';
|
||||
if (steer) body.steer = steer;
|
||||
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
|
||||
@@ -69,14 +98,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (this._rmm !== state) return;
|
||||
state.busy = false;
|
||||
|
||||
const suggestion = data && data.suggestions && data.suggestions[0];
|
||||
if (!suggestion) {
|
||||
const suggestions = (data && data.suggestions) || [];
|
||||
if (suggestions.length === 0) {
|
||||
this._rmmSetPhase('error');
|
||||
return;
|
||||
}
|
||||
state.shown = suggestion;
|
||||
state.shown = suggestions[0];
|
||||
state.alternates = suggestions.slice(1);
|
||||
this._rmmSetPhase('ready');
|
||||
this._rmmShowSuggestion(state.shown);
|
||||
this._rmmRenderAlternates();
|
||||
document.getElementById('readMyMindPrompt')?.focus();
|
||||
},
|
||||
|
||||
/** Paint one suggestion into the editable field, kind chip, and rationale. */
|
||||
_rmmShowSuggestion(suggestion) {
|
||||
const input = document.getElementById('readMyMindPrompt');
|
||||
const why = document.getElementById('readMyMindWhy');
|
||||
const kind = document.getElementById('readMyMindKind');
|
||||
@@ -88,7 +124,49 @@ Object.assign(CodemanApp.prototype, {
|
||||
kind.textContent = suggestion.kind || 'continue';
|
||||
kind.className = `readmymind-kind readmymind-kind-${suggestion.kind || 'continue'}`;
|
||||
}
|
||||
input?.focus();
|
||||
},
|
||||
|
||||
/** The non-primary suggestions as tappable rows below the rationale. */
|
||||
_rmmRenderAlternates() {
|
||||
const state = this._rmm;
|
||||
const box = document.getElementById('readMyMindAlternates');
|
||||
if (!box) return;
|
||||
box.textContent = '';
|
||||
if (!state) return;
|
||||
state.alternates.forEach((alt, i) => {
|
||||
const row = document.createElement('button');
|
||||
row.type = 'button';
|
||||
row.className = 'readmymind-alt';
|
||||
const kind = document.createElement('span');
|
||||
kind.className = `readmymind-kind readmymind-kind-${alt.kind || 'continue'}`;
|
||||
kind.textContent = alt.kind || 'continue';
|
||||
const col = document.createElement('span');
|
||||
col.className = 'readmymind-alt-col';
|
||||
const text = document.createElement('span');
|
||||
text.className = 'readmymind-alt-text';
|
||||
text.textContent = alt.prompt;
|
||||
col.appendChild(text);
|
||||
// The rationale as a visible second line: phones have no hover, and the
|
||||
// why is what separates two plausible-looking prompts.
|
||||
if (alt.why) {
|
||||
const why = document.createElement('span');
|
||||
why.className = 'readmymind-alt-why';
|
||||
why.textContent = alt.why;
|
||||
col.appendChild(why);
|
||||
}
|
||||
row.appendChild(kind);
|
||||
row.appendChild(col);
|
||||
row.addEventListener('click', () => {
|
||||
// Swap: the tapped alternate becomes the shown suggestion and the
|
||||
// previously shown one takes its row, so browsing loses nothing.
|
||||
const prev = state.shown;
|
||||
state.shown = alt;
|
||||
state.alternates[i] = prev;
|
||||
this._rmmShowSuggestion(alt);
|
||||
this._rmmRenderAlternates();
|
||||
});
|
||||
box.appendChild(row);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -114,11 +192,19 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.showToast(withEnter ? 'Prompt sent' : 'Inserted, press Enter in the terminal to send', 'success');
|
||||
},
|
||||
|
||||
/** Re-run with the shown suggestion recorded as a rejection. */
|
||||
/**
|
||||
* Re-run with every displayed suggestion recorded as a rejection (the user
|
||||
* saw them all and wanted none: strong negative signal) plus the optional
|
||||
* steer note, read by _readMyMindPredict from its field.
|
||||
*/
|
||||
rethinkReadMyMind() {
|
||||
const state = this._rmm;
|
||||
if (!state || state.busy) return;
|
||||
if (state.shown && state.shown.prompt) state.rejected.push(state.shown.prompt);
|
||||
for (const s of [state.shown, ...state.alternates]) {
|
||||
if (!s || !s.prompt) continue;
|
||||
const prompt = s.prompt.slice(0, 1000);
|
||||
if (!state.rejected.includes(prompt)) state.rejected.push(prompt);
|
||||
}
|
||||
this._readMyMindPredict();
|
||||
},
|
||||
|
||||
@@ -129,7 +215,15 @@ 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';
|
||||
const rethinkBtn = document.getElementById('readMyMindRethink');
|
||||
if (rethinkBtn) rethinkBtn.disabled = phase === 'loading';
|
||||
// The steer field shows in ready AND error phases: steering a failed run's
|
||||
// retry is exactly when a note helps. Hidden only while loading.
|
||||
const steer = document.getElementById('readMyMindSteer');
|
||||
if (steer) steer.style.display = phase === 'loading' ? 'none' : '';
|
||||
// While the predictor runs the hidden field still holds the previous text;
|
||||
// freeze every action so a stale suggestion cannot be sent mid-rethink.
|
||||
for (const btnId of ['readMyMindRethink', 'readMyMindInsert', 'readMyMindSend']) {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (btn) btn.disabled = phase === 'loading';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2117,6 +2117,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (readMyMindBtn) {
|
||||
readMyMindBtn.classList.toggle('btn-readmymind--hidden', settings.readMyMindEnabled !== true);
|
||||
}
|
||||
// The phone accessory bar's 🧠 key follows the same setting (plus the
|
||||
// active session's mode); keep it in step on every settings apply.
|
||||
if (typeof KeyboardAccessoryBar !== 'undefined') KeyboardAccessoryBar.refreshReadMyMind?.();
|
||||
|
||||
// Plan-usage chip — shown by default on desktop, OFF on handhelds (App
|
||||
// Settings → Display → "Plan Usage Limits"). The template always ships it
|
||||
|
||||
@@ -10697,6 +10697,17 @@ kbd {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Which session the prediction targets: overview rows and the accessory key
|
||||
can open the modal for a session that is not the active tab. */
|
||||
.readmymind-session {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.readmymind-session:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Read My Mind modal: top suggestion in an editable single-line field with the
|
||||
kind badge beside it and the rationale below. */
|
||||
@@ -10753,6 +10764,75 @@ kbd {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
/* Alternates: the predictor's other kind-diverse suggestions (verify /
|
||||
redirect) as tappable rows; tapping swaps one into the editable field.
|
||||
Text is set via textContent only (predictor output is observed/injectable
|
||||
content, same rule as the field above). */
|
||||
.readmymind-alternates {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.readmymind-alternates:empty {
|
||||
display: none;
|
||||
}
|
||||
.readmymind-alt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 7px 9px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--control-border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.readmymind-alt:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.readmymind-alt-col {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.readmymind-alt-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* Rationale as a visible second line: phones have no hover for a title tip,
|
||||
and the why is what separates two plausible-looking prompts. */
|
||||
.readmymind-alt-why {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
/* Steer note: optional free-text guidance for Rethink ("no, I meant the
|
||||
mobile bug"); sent as the highest-authority tier of the re-run's context. */
|
||||
.readmymind-steer-input {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
padding: 7px 9px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px dashed var(--control-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.readmymind-steer-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
border-style: solid;
|
||||
}
|
||||
.readmymind-error {
|
||||
padding: 12px 4px;
|
||||
color: var(--text-dim);
|
||||
@@ -12075,6 +12155,13 @@ body.touch-device.cjk-input-visible .main {
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
/* The 🧠 key ships `hidden` until refreshReadMyMind() reveals it; the
|
||||
inline-flex rules above (and mobile.css's) beat the UA [hidden] rule, so
|
||||
hiding must be re-asserted, same trap as the home-sessions column. */
|
||||
.accessory-btn[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.accessory-btn.confirming {
|
||||
background: color-mix(in srgb, var(--yellow) 18%, var(--bg-input));
|
||||
border-color: var(--yellow);
|
||||
|
||||
Reference in New Issue
Block a user