feat: harden clarify dialog flow and refresh recovery

This commit is contained in:
Frank Song
2026-04-15 13:10:50 +08:00
parent 45d3dc0f68
commit ccba2f5c01
11 changed files with 1066 additions and 9 deletions

View File

@@ -44,6 +44,12 @@ const LOCALES = {
approval_btn_deny: 'Deny',
approval_btn_deny_title: 'Deny — do not run this command',
approval_responding: 'Responding\u2026',
clarify_heading: 'Clarification needed',
clarify_hint: 'Pick a choice, or type your own answer below.',
clarify_other: 'Other',
clarify_send: 'Send',
clarify_input_placeholder: 'Type your response…',
clarify_responding: 'Responding\u2026',
untitled: 'Untitled',
n_messages: (n) => `${n} messages`,
model_unavailable: ' (unavailable)',
@@ -452,6 +458,12 @@ const LOCALES = {
approval_btn_deny: 'Denegar',
approval_btn_deny_title: 'Denegar — no ejecutar este comando',
approval_responding: 'Respondiendo…',
clarify_heading: 'Se necesita aclaración',
clarify_hint: 'Elige una opción o escribe tu propia respuesta abajo.',
clarify_other: 'Otra',
clarify_send: 'Enviar',
clarify_input_placeholder: 'Escribe tu respuesta…',
clarify_responding: 'Respondiendo…',
untitled: 'Sin título',
n_messages: (n) => `${n} mensajes`,
model_unavailable: ' (no disponible)',
@@ -852,6 +864,12 @@ const LOCALES = {
approval_btn_deny: 'Ablehnen',
approval_btn_deny_title: 'Ablehnen \u2014 diesen Befehl nicht ausführen',
approval_responding: 'Antwortet\u2026',
clarify_heading: 'Klärung erforderlich',
clarify_hint: 'Wähle eine Option oder schreibe deine eigene Antwort unten.',
clarify_other: 'Andere',
clarify_send: 'Senden',
clarify_input_placeholder: 'Gib deine Antwort ein…',
clarify_responding: 'Antwortet\u2026',
untitled: 'Unbenannt',
n_messages: (n) => `${n} Nachrichten`,
model_unavailable: ' (nicht verfügbar)',
@@ -1056,6 +1074,12 @@ const LOCALES = {
approval_btn_deny: '拒绝',
approval_btn_deny_title: '拒绝 — 不执行此命令',
approval_responding: '处理中…',
clarify_heading: '需要澄清',
clarify_hint: '请选择一个选项,或在下方输入你自己的回答。',
clarify_other: '其他',
clarify_send: '发送',
clarify_input_placeholder: '请输入你的回答…',
clarify_responding: '处理中…',
untitled: '\u672a\u547d\u540d',
n_messages: (n) => `${n} \u6761\u6d88\u606f`,
model_unavailable: '\uff08\u4e0d\u53ef\u7528\uff09',
@@ -1369,7 +1393,7 @@ const LOCALES = {
workspace_paths_validated_hint: '保存前会校验路径是否为已存在目录。',
workspace_added: '工作区已添加',
workspace_remove_confirm_title: '移除工作区',
workspace_remove_confirm_message: (path) => `要移除${path}吗?`,
workspace_remove_confirm_message: (path) => `要移除"${path}"吗?`,
workspace_removed: '工作区已移除',
workspace_switch_prompt_title: '切换工作区',
workspace_switch_prompt_message: '输入绝对路径以添加并切换当前会话的工作区。',
@@ -1455,6 +1479,12 @@ const LOCALES = {
approval_btn_deny: '\u62d2\u7edd',
approval_btn_deny_title: '\u62d2\u7edd — \u4e0d\u57f7\u884c\u6b64\u547d\u4ee4',
approval_responding: '\u8655\u7406\u4e2d\u2026',
clarify_heading: '\u9700\u8981\u91cb\u6e05',
clarify_hint: '\u8acb\u9078\u64c7\u4e00\u500b\u9078\u9805\uff0c\u6216\u5728\u4e0b\u65b9\u8f38\u5165\u4f60\u81ea\u5df1\u7684\u56de\u7b54\u3002',
clarify_other: '\u5176\u4ed6',
clarify_send: '\u9001\u51fa',
clarify_input_placeholder: '\u8f38\u5165\u4f60\u7684\u56de\u7b54\u2026',
clarify_responding: '\u8655\u7406\u4e2d\u2026',
untitled: '\u672a\u547d\u540d',
n_messages: (n) => `${n} \u689d\u8a0a\u606f`,
model_unavailable: '\uff08\u4e0d\u53ef\u7528\uff09',

View File

@@ -246,6 +246,21 @@
</div>
</div>
</div>
<div class="clarify-card" id="clarifyCard" role="dialog" aria-labelledby="clarifyHeading" aria-describedby="clarifyQuestion clarifyHint">
<div class="clarify-inner">
<div class="clarify-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 17h.01"/><path d="M9.09 9a3 3 0 1 1 5.82 1c0 2-3 2-3 4"/><circle cx="12" cy="12" r="10"/></svg>
<span id="clarifyHeading" data-i18n="clarify_heading">Clarification needed</span>
</div>
<div class="clarify-question" id="clarifyQuestion"></div>
<div class="clarify-choices" id="clarifyChoices"></div>
<div class="clarify-response">
<input class="clarify-input" id="clarifyInput" type="text" data-i18n-placeholder="clarify_input_placeholder" placeholder="Type your response…">
<button class="clarify-submit" id="clarifySubmit" onclick="respondClarify()" data-i18n="clarify_send">Send</button>
</div>
<div class="clarify-hint" id="clarifyHint" data-i18n="clarify_hint">Pick a choice, or type your own answer below.</div>
</div>
</div>
<div class="composer-wrap" id="composerWrap">
<div class="cmd-dropdown" id="cmdDropdown"></div>
<div class="composer-box" id="composerBox">

View File

@@ -44,6 +44,7 @@ async function send(){
saveInflightState(activeSid,{streamId:null,messages:INFLIGHT[activeSid].messages,uploaded,toolCalls:[]});
}
startApprovalPolling(activeSid);
startClarifyPolling(activeSid);
S.activeStreamId = null; // will be set after stream starts
// Set provisional title from user message immediately so session appears
@@ -79,12 +80,34 @@ async function send(){
const cancelBtn=$('btnCancel');
if(cancelBtn) cancelBtn.style.display='inline-flex';
}catch(e){
const errMsg=String((e&&e.message)||'');
const conflictActiveStream=/session already has an active stream/i.test(errMsg);
if(conflictActiveStream){
delete INFLIGHT[activeSid];
if(typeof clearInflightState==='function') clearInflightState(activeSid);
stopApprovalPolling();
stopClarifyPolling();
// Keep the user's attempted turn by queueing it for after the current run.
queueSessionMessage(activeSid,{text:msgText,files:[]});
updateQueueBadge(activeSid);
showToast('Current session is still running. Reconnected and queued your message.',2600);
try{
await loadSession(activeSid);
setComposerStatus('');
return;
}catch(_){
// Fall through to standard error handling if session reload fails.
}
}
delete INFLIGHT[activeSid];
stopApprovalPolling();
stopClarifyPolling();
// Only hide approval card if it belongs to the session that just finished
if(!_approvalSessionId || _approvalSessionId===activeSid) hideApprovalCard(true);removeThinking();
S.messages.push({role:'assistant',content:`**Error:** ${e.message}`});
renderMessages();setBusy(false);setComposerStatus(`Error: ${e.message}`);
if(!_clarifySessionId || _clarifySessionId===activeSid) hideClarifyCard(true);
S.messages.push({role:'assistant',content:`**Error:** ${errMsg}`});
renderMessages();setBusy(false);setComposerStatus(`Error: ${errMsg}`);
return;
}
@@ -290,6 +313,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
source.addEventListener('tool',e=>{
const d=JSON.parse(e.data);
if(d.name==='clarify') return;
const tc={name:d.name, preview:d.preview||'', args:d.args||{}, snippet:'', done:false, tid:d.tid||`live-${Date.now()}-${Math.random().toString(36).slice(2,8)}`};
if(!Array.isArray(INFLIGHT[activeSid].toolCalls)) INFLIGHT[activeSid].toolCalls=[];
INFLIGHT[activeSid].toolCalls.push(tc);
@@ -305,6 +329,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
source.addEventListener('tool_complete',e=>{
const d=JSON.parse(e.data);
if(d.name==='clarify') return;
const inflight=INFLIGHT[activeSid];
if(!inflight) return;
if(!Array.isArray(inflight.toolCalls)) inflight.toolCalls=[];
@@ -340,13 +365,23 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
sendBrowserNotification('Approval required',d.description||'Tool approval needed');
});
source.addEventListener('clarify',e=>{
const d=JSON.parse(e.data);
d._session_id=activeSid;
showClarifyCard(d);
playNotificationSound();
sendBrowserNotification('Clarification needed',d.question||'Tool clarification needed');
});
source.addEventListener('done',e=>{
source.close();
const d=JSON.parse(e.data);
delete INFLIGHT[activeSid];
clearInflight();clearInflightState(activeSid);
stopApprovalPolling();
stopClarifyPolling();
if(!_approvalSessionId || _approvalSessionId===activeSid) hideApprovalCard(true);
if(!_clarifySessionId || _clarifySessionId===activeSid) hideClarifyCard(true);
if(S.session&&S.session.session_id===activeSid){
S.activeStreamId=null;
const _cb=$('btnCancel');if(_cb)_cb.style.display='none';
@@ -396,8 +431,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
// Application-level error sent explicitly by the server (rate limit, crash, etc.)
// This is distinct from the SSE network 'error' event below.
source.close();
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
if(!_approvalSessionId||_approvalSessionId===activeSid) hideApprovalCard(true);
if(!_clarifySessionId||_clarifySessionId===activeSid) hideClarifyCard(true);
if(S.session&&S.session.session_id===activeSid){
S.activeStreamId=null;const _cbe=$('btnCancel');if(_cbe)_cbe.style.display='none';
clearLiveToolCards();if(!assistantText)removeThinking();
@@ -457,8 +493,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
source.addEventListener('cancel',e=>{
source.close();
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
if(!_approvalSessionId||_approvalSessionId===activeSid) hideApprovalCard(true);
if(!_clarifySessionId||_clarifySessionId===activeSid) hideClarifyCard(true);
if(S.session&&S.session.session_id===activeSid){
S.activeStreamId=null;const _cbc=$('btnCancel');if(_cbc)_cbc.style.display='none';
}
@@ -472,9 +509,10 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
}
function _handleStreamError(){
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();
delete INFLIGHT[activeSid];clearInflight();clearInflightState(activeSid);stopApprovalPolling();stopClarifyPolling();
_closeSource();
if(!_approvalSessionId||_approvalSessionId===activeSid) hideApprovalCard(true);
if(!_clarifySessionId||_clarifySessionId===activeSid) hideClarifyCard(true);
if(S.session&&S.session.session_id===activeSid){
S.activeStreamId=null;const _cbe=$('btnCancel');if(_cbe)_cbe.style.display='none';
clearLiveToolCards();if(!assistantText)removeThinking();
@@ -622,6 +660,255 @@ function stopApprovalPolling() {
if (_approvalPollTimer) { clearInterval(_approvalPollTimer); _approvalPollTimer = null; }
}
// ── Clarify polling ──
let _clarifyPollTimer = null;
let _clarifyHideTimer = null;
let _clarifyVisibleSince = 0;
let _clarifySignature = '';
let _clarifySessionId = null;
let _clarifyMissingEndpointWarned = false;
const CLARIFY_MIN_VISIBLE_MS = 30000;
function _ensureClarifyCardDom() {
let card = $("clarifyCard");
if (card) return card;
const host = $("msgInner") || $("messages");
if (!host) return null;
card = document.createElement("div");
card.className = "clarify-card";
card.id = "clarifyCard";
card.setAttribute("role", "dialog");
card.setAttribute("aria-labelledby", "clarifyHeading");
card.setAttribute("aria-describedby", "clarifyQuestion clarifyHint");
card.innerHTML = `
<div class="clarify-inner">
<div class="clarify-header">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 17h.01"/><path d="M9.09 9a3 3 0 1 1 5.82 1c0 2-3 2-3 4"/><circle cx="12" cy="12" r="10"/></svg>
<span id="clarifyHeading" data-i18n="clarify_heading">Clarification needed</span>
</div>
<div class="clarify-question" id="clarifyQuestion"></div>
<div class="clarify-choices" id="clarifyChoices"></div>
<div class="clarify-response">
<input class="clarify-input" id="clarifyInput" type="text" data-i18n-placeholder="clarify_input_placeholder" placeholder="Type your response…">
<button class="clarify-submit" id="clarifySubmit" data-i18n="clarify_send">Send</button>
</div>
<div class="clarify-hint" id="clarifyHint" data-i18n="clarify_hint">Please choose one option, or type your own response below.</div>
</div>
`;
host.appendChild(card);
const submit = $("clarifySubmit");
if (submit) submit.onclick = () => respondClarify();
if (typeof applyLocaleToDOM === "function") applyLocaleToDOM();
return card;
}
function _clearClarifyHideTimer() {
if (_clarifyHideTimer) {
clearTimeout(_clarifyHideTimer);
_clarifyHideTimer = null;
}
}
function _resetClarifyCardState() {
_clearClarifyHideTimer();
_clarifyVisibleSince = 0;
_clarifySignature = '';
}
function hideClarifyCard(force=false) {
const card = $("clarifyCard");
if (!card) {
_clarifySessionId = null;
_resetClarifyCardState();
if (typeof unlockComposerForClarify === "function") unlockComposerForClarify();
return;
}
if (!force && _clarifyVisibleSince) {
const remaining = CLARIFY_MIN_VISIBLE_MS - (Date.now() - _clarifyVisibleSince);
if (remaining > 0) {
const scheduledSignature = _clarifySignature;
_clearClarifyHideTimer();
_clarifyHideTimer = setTimeout(() => {
_clarifyHideTimer = null;
if (_clarifySignature !== scheduledSignature) return;
hideClarifyCard(true);
}, remaining);
return;
}
}
_clarifySessionId = null;
_resetClarifyCardState();
card.classList.remove("visible");
if (typeof unlockComposerForClarify === "function") unlockComposerForClarify();
$("clarifyQuestion").textContent = "";
$("clarifyChoices").innerHTML = "";
$("clarifyInput").value = "";
$("clarifyInput").disabled = false;
$("clarifyInput").onkeydown = null;
const submit = $("clarifySubmit");
if (submit) { submit.disabled = false; submit.classList.remove("loading"); }
}
function _clarifySetControlsDisabled(disabled, loading=false) {
const input = $("clarifyInput");
const submit = $("clarifySubmit");
if (input) input.disabled = disabled;
if (submit) {
submit.disabled = disabled;
submit.classList.toggle("loading", !!loading);
}
const choices = $("clarifyChoices");
if (choices) {
choices.querySelectorAll("button").forEach(btn => {
btn.disabled = disabled;
if (loading && btn.dataset && btn.dataset.choice === "other") {
btn.classList.toggle("loading", false);
}
});
}
}
function showClarifyCard(pending) {
const question = pending.question || pending.description || '';
const choices = Array.isArray(pending.choices_offered)
? pending.choices_offered
: (Array.isArray(pending.choices) ? pending.choices : []);
const sig = JSON.stringify({
question,
choices,
sid: pending._session_id || (S.session && S.session.session_id) || null,
});
const card = _ensureClarifyCardDom();
if (!card) return;
const questionEl = $("clarifyQuestion");
const choicesEl = $("clarifyChoices");
const input = $("clarifyInput");
const sameClarify = card.classList.contains("visible") && _clarifySignature === sig;
_clarifySessionId = pending._session_id || (S.session && S.session.session_id) || null;
_clarifySignature = sig;
if (!sameClarify) {
_clarifyVisibleSince = Date.now();
_clearClarifyHideTimer();
}
if (questionEl) questionEl.textContent = question;
if (choicesEl) {
choicesEl.innerHTML = '';
choicesEl.style.display = choices.length ? '' : 'none';
if (choices.length) {
choices.forEach((choice, idx) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'clarify-choice';
btn.dataset.choice = choice;
btn.onclick = () => respondClarify(choice);
const badge = document.createElement('span');
badge.className = 'clarify-choice-badge';
badge.textContent = String(idx + 1);
const text = document.createElement('span');
text.className = 'clarify-choice-text';
text.textContent = choice;
btn.appendChild(badge);
btn.appendChild(text);
choicesEl.appendChild(btn);
});
const other = document.createElement('button');
other.type = 'button';
other.className = 'clarify-choice other';
other.dataset.choice = 'other';
other.setAttribute('data-i18n', 'clarify_other');
const otherBadge = document.createElement('span');
otherBadge.className = 'clarify-choice-badge other';
otherBadge.textContent = '•';
const otherText = document.createElement('span');
otherText.className = 'clarify-choice-text';
otherText.textContent = t('clarify_other') || 'Other';
other.appendChild(otherBadge);
other.appendChild(otherText);
other.onclick = () => {
const el = $("clarifyInput");
if (el) {
el.focus();
if (typeof el.select === 'function') el.select();
}
};
choicesEl.appendChild(other);
}
}
if (input) {
if (!sameClarify) input.value = '';
input.disabled = false;
input.onkeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
respondClarify();
}
};
}
if (typeof lockComposerForClarify === "function") {
lockComposerForClarify(question ? `Clarification needed: ${question}` : "Clarification needed");
}
_clarifySetControlsDisabled(false, false);
const msgInner = $("msgInner");
if (msgInner && card.parentElement !== msgInner) {
msgInner.appendChild(card);
}
card.classList.add("visible");
if (!sameClarify) card.scrollIntoView({block:"nearest", behavior:"smooth"});
if (typeof applyLocaleToDOM === "function") applyLocaleToDOM();
if (input && !sameClarify) setTimeout(() => input.focus(), 50);
}
async function respondClarify(response) {
const sid = _clarifySessionId || (S.session && S.session.session_id);
if (!sid) return;
const input = $("clarifyInput");
let value = typeof response === 'string' ? response : (input ? input.value : '');
value = String(value || '').trim();
if (!value) {
if (input) input.focus();
return;
}
_clarifySessionId = null;
_clarifySetControlsDisabled(true, true);
hideClarifyCard(true);
try {
await api("/api/clarify/respond", {
method: "POST",
body: JSON.stringify({ session_id: sid, response: value })
});
} catch(e) { setStatus(t("clarify_responding") + " " + e.message); }
}
function startClarifyPolling(sid) {
stopClarifyPolling();
_clarifyMissingEndpointWarned = false;
_clarifyPollTimer = setInterval(async () => {
if (!S.session || S.session.session_id !== sid) {
stopClarifyPolling(); hideClarifyCard(true); return;
}
try {
const data = await api("/api/clarify/pending?session_id=" + encodeURIComponent(sid));
if (data.pending) { data.pending._session_id=sid; showClarifyCard(data.pending); }
else { hideClarifyCard(); }
} catch(e) {
const msg = String((e && e.message) || "");
if (!_clarifyMissingEndpointWarned && /(^|\b)(404|not found)(\b|$)/i.test(msg)) {
_clarifyMissingEndpointWarned = true;
setComposerStatus("Clarify unavailable on current server build. Restart server.");
if (typeof showToast === "function") {
showToast("Clarify endpoint unavailable. Please restart server.", 5000);
}
stopClarifyPolling();
}
// Ignore transient poll errors; SSE clarify event still provides a fast path.
}
}, 1500);
}
function stopClarifyPolling() {
if (_clarifyPollTimer) { clearInterval(_clarifyPollTimer); _clarifyPollTimer = null; }
}
// ── Notifications and Sound ──────────────────────────────────────────────────
function playNotificationSound(){

View File

@@ -38,6 +38,8 @@ async function newSession(flash){
async function loadSession(sid){
stopApprovalPolling();hideApprovalCard();
if(typeof stopClarifyPolling==='function') stopClarifyPolling();
if(typeof hideClarifyCard==='function') hideClarifyCard();
const data=await api(`/api/session?session_id=${encodeURIComponent(sid)}`);
S.session=data.session;
S.lastUsage={...(data.session.last_usage||{})};
@@ -95,6 +97,7 @@ async function loadSession(sid){
}
setBusy(true);setComposerStatus('');
startApprovalPolling(sid);
if(typeof startClarifyPolling==='function') startClarifyPolling(sid);
S.activeStreamId=activeStreamId;
const _cb=$('btnCancel');if(_cb&&activeStreamId)_cb.style.display='inline-flex';
if(INFLIGHT[sid].reattach&&activeStreamId&&typeof attachLiveStream==='function'){
@@ -122,6 +125,7 @@ async function loadSession(sid){
syncTopbar();renderMessages();appendThinking();loadDir('.');
updateQueueBadge(sid);
startApprovalPolling(sid);
if(typeof startClarifyPolling==='function') startClarifyPolling(sid);
if(typeof attachLiveStream==='function') attachLiveStream(sid, activeStreamId, data.session.pending_attachments||[], {reconnecting:true});
else if(typeof watchInflightSession==='function') watchInflightSession(sid, activeStreamId);
}else{

View File

@@ -304,6 +304,30 @@
.approval-btn.deny{border-color:rgba(233,69,96,0.5);color:var(--accent);}
.approval-btn.deny:hover{background:rgba(233,69,96,0.12);border-color:rgba(233,69,96,0.7);}
.approval-btn.loading{opacity:.7;cursor:wait;}
/* ── Clarify card ── */
.clarify-card{display:none;max-width:680px;margin:4px 0 2px 40px;padding:0;}
.clarify-card.visible{display:block;}
.clarify-inner{background:rgba(255,255,255,.03);backdrop-filter:blur(8px);border:1px solid rgba(124,185,255,0.16);border-radius:12px;padding:12px 14px 13px;box-shadow:0 1px 0 rgba(255,255,255,.02) inset;}
.clarify-header{display:flex;align-items:center;gap:8px;margin-bottom:10px;font-size:12px;font-weight:700;color:var(--blue);letter-spacing:.01em;}
.clarify-question{font-size:14px;color:var(--text);line-height:1.7;white-space:pre-wrap;margin-bottom:12px;}
.clarify-choices{display:flex;flex-direction:column;gap:8px;margin-bottom:12px;}
.clarify-choice{display:flex;align-items:flex-start;gap:10px;width:100%;padding:11px 14px;border-radius:12px;font-size:13px;font-weight:600;border:1px solid rgba(124,185,255,0.3);background:rgba(124,185,255,0.08);color:var(--blue);cursor:pointer;transition:all .15s;white-space:normal;text-align:left;box-shadow:0 1px 0 rgba(255,255,255,.03) inset;}
.clarify-choice:hover{background:rgba(124,185,255,0.16);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,0.18);}
.clarify-choice:focus-visible{outline:2px solid rgba(124,185,255,.75);outline-offset:2px;}
.clarify-choice-badge{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;border-radius:999px;background:rgba(124,185,255,0.16);border:1px solid rgba(124,185,255,0.3);color:var(--blue);font-size:11px;font-weight:800;flex-shrink:0;line-height:1;}
.clarify-choice-badge.other{background:rgba(201,168,76,0.12);border-color:rgba(201,168,76,0.32);color:var(--gold);}
.clarify-choice-text{flex:1;line-height:1.45;min-width:0;}
.clarify-choice.other{border-color:rgba(201,168,76,0.35);color:var(--gold);background:rgba(201,168,76,0.08);}
.clarify-choice.other:hover{background:rgba(201,168,76,0.14);border-color:rgba(201,168,76,0.55);}
.clarify-response{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
.clarify-input{flex:1;min-width:220px;padding:10px 12px;border-radius:8px;border:1px solid var(--border2);background:var(--input-bg);color:var(--text);font:inherit;outline:none;transition:all .15s;}
.clarify-input:focus{border-color:rgba(124,185,255,.5);box-shadow:0 0 0 3px rgba(124,185,255,.08);background:var(--hover-bg);}
.clarify-submit{display:inline-flex;align-items:center;justify-content:center;min-width:92px;padding:10px 14px;border-radius:8px;border:1px solid rgba(124,185,255,0.35);background:rgba(124,185,255,0.14);color:var(--blue);font-size:12px;font-weight:700;cursor:pointer;transition:all .15s;white-space:nowrap;}
.clarify-submit:hover{background:rgba(124,185,255,0.22);transform:translateY(-1px);}
.clarify-submit:disabled{opacity:.6;cursor:not-allowed;transform:none;}
.clarify-submit.loading{opacity:.75;cursor:wait;}
.clarify-hint{margin-top:8px;font-size:11px;line-height:1.45;color:var(--muted);}
.clarify-card.visible .clarify-question{padding-left:1px;}
/* Sidebar navigation tabs */
.sidebar-nav{display:flex;border-bottom:1px solid var(--border);flex-shrink:0;padding:6px 8px 0;gap:2px;}
.nav-tab{flex:1;padding:10px 4px 8px;font-size:20px;text-align:center;cursor:pointer;color:var(--muted);border:none;background:none;transition:color .15s;border-bottom:2px solid transparent;white-space:nowrap;overflow:hidden;position:relative;}
@@ -691,6 +715,13 @@
.approval-btns{gap:6px;}
.approval-btn{padding:8px 12px;font-size:12px;min-height:44px;}
.approval-kbd{display:none;}
/* Clarify card */
.clarify-card{margin:6px 0 4px 0;max-width:100%;}
.clarify-inner{padding:12px 12px 13px;}
.clarify-response{flex-direction:column;align-items:stretch;}
.clarify-input,.clarify-submit{width:100%;min-height:44px;}
.clarify-choice{min-height:44px;}
.clarify-choice-badge{min-width:22px;height:22px;}
.app-dialog-overlay{padding:12px;}
.app-dialog{width:100%;padding:16px 16px 14px;border-radius:16px;}
.app-dialog-actions{flex-direction:column-reverse;align-items:stretch;}
@@ -825,6 +856,7 @@
/* Approval buttons: tab stops */
.approval-btn:focus{outline:2px solid var(--blue);outline-offset:2px;}
.clarify-choice:focus,.clarify-submit:focus,.clarify-input:focus{outline:2px solid var(--blue);outline-offset:2px;}
/* Message role: breathing room between icon and name */
.msg-role > span{line-height:1;}

View File

@@ -611,11 +611,43 @@ function setComposerStatus(t){
el.style.display='';
}
let _composerLockState=null;
function lockComposerForClarify(placeholderText){
const input=$('msg');
if(!input) return;
if(!_composerLockState){
_composerLockState={
disabled: input.disabled,
placeholder: input.placeholder,
};
}
input.disabled=true;
if(placeholderText) input.placeholder=placeholderText;
updateSendBtn();
}
function unlockComposerForClarify(){
const input=$('msg');
if(!input) return;
if(_composerLockState){
input.disabled=!!_composerLockState.disabled;
if(typeof _composerLockState.placeholder==='string'){
input.placeholder=_composerLockState.placeholder;
}
_composerLockState=null;
}else{
input.disabled=false;
}
updateSendBtn();
}
function updateSendBtn(){
const btn=$('btnSend');
if(!btn) return;
const hasContent=$('msg').value.trim().length>0||S.pendingFiles.length>0;
const canSend=hasContent&&!S.busy;
const msg=$('msg');
const hasContent=msg&&msg.value.trim().length>0||S.pendingFiles.length>0;
const canSend=hasContent&&!S.busy&&!(msg&&msg.disabled);
// Hide while busy (cancel button takes its place); show otherwise
btn.style.display=S.busy?'none':'';
btn.disabled=!canSend;
@@ -1830,4 +1862,3 @@ async function uploadPendingFiles(){
if(failures===total&&total>0)throw new Error(t('all_uploads_failed',total));
return names;
}