diff --git a/README.md b/README.md
index 2a0ab72..926f57a 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,9 @@ https://github.com/ZgDaniel/cc-web 给我装!
## 更新记录
+- **v1.2.1**
+ - 修复 `AskUserQuestion` 交互选项在 Web 端不显示的问题:后端保留完整结构化参数并前端按问题/选项渲染。
+ - 新增交互选项快捷填充:点击选项即可把对应答案插入输入框,便于快速确认并发送。
- **v1.2**
- 修复消息中包含代码块时可能触发的页面横向溢出问题:窗口不再被长代码撑宽,代码块可在块内横向滚动。
- 优化移动端输入体验:手机浏览器回车键默认换行,不再直接发送消息;消息发送改为手动点击发送按钮。
diff --git a/public/app.js b/public/app.js
index 81f3fb0..acac859 100644
--- a/public/app.js
+++ b/public/app.js
@@ -410,9 +410,16 @@
for (const tc of m.toolCalls) {
const details = document.createElement('details');
details.className = 'tool-call';
- const contentText = tc.result || (typeof tc.input === 'string' ? tc.input : (tc.input ? JSON.stringify(tc.input, null, 2) : ''));
- details.innerHTML = ` ${escapeHtml(tc.name)}
-
${escapeHtml(contentText)}
`;
+ details.dataset.toolName = tc.name || '';
+ if (tc.name === 'AskUserQuestion') details.open = true;
+
+ const summary = document.createElement('summary');
+ summary.innerHTML = ` ${escapeHtml(tc.name)}`;
+ details.appendChild(summary);
+
+ const displayInput = tc.name === 'AskUserQuestion' ? tc.input : (tc.result || tc.input);
+ details.appendChild(buildToolContentElement(tc.name, displayInput));
+
bubble.insertBefore(details, bubble.firstChild);
}
}
@@ -421,6 +428,99 @@
scrollToBottom();
}
+ function normalizeAskUserInput(input) {
+ if (input === null || input === undefined) return null;
+ if (typeof input === 'string') {
+ const trimmed = input.trim();
+ if (!trimmed) return null;
+ try {
+ return JSON.parse(trimmed);
+ } catch {
+ return null;
+ }
+ }
+ return input;
+ }
+
+ function extractAskUserQuestions(input) {
+ const parsed = normalizeAskUserInput(input);
+ if (!parsed || !Array.isArray(parsed.questions)) return [];
+ return parsed.questions;
+ }
+
+ function appendAskOptionToInput(question, option) {
+ const header = (question?.header || '').trim() || '问题';
+ const line = `【${header}】${option?.label || ''}`;
+ const current = msgInput.value.trim();
+ msgInput.value = current ? `${current}\n${line}` : line;
+ autoResize();
+ msgInput.focus();
+ }
+
+ function createAskUserQuestionView(questions) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'ask-user-question';
+
+ questions.forEach((q, idx) => {
+ const card = document.createElement('div');
+ card.className = 'ask-question-card';
+
+ const header = document.createElement('div');
+ header.className = 'ask-question-header';
+ header.textContent = `${idx + 1}. ${q.header || '问题'}`;
+ card.appendChild(header);
+
+ const body = document.createElement('div');
+ body.className = 'ask-question-text';
+ body.textContent = q.question || '';
+ card.appendChild(body);
+
+ if (Array.isArray(q.options) && q.options.length > 0) {
+ const opts = document.createElement('div');
+ opts.className = 'ask-question-options';
+ q.options.forEach((opt, i) => {
+ const item = document.createElement('button');
+ item.type = 'button';
+ item.className = 'ask-option-item';
+ item.addEventListener('click', () => appendAskOptionToInput(q, opt));
+
+ const title = document.createElement('div');
+ title.className = 'ask-option-label';
+ title.textContent = `${i + 1}. ${opt.label || ''}`;
+ item.appendChild(title);
+
+ if (opt.description) {
+ const desc = document.createElement('div');
+ desc.className = 'ask-option-desc';
+ desc.textContent = opt.description;
+ item.appendChild(desc);
+ }
+
+ opts.appendChild(item);
+ });
+ card.appendChild(opts);
+ }
+
+ wrapper.appendChild(card);
+ });
+
+ return wrapper;
+ }
+
+ function buildToolContentElement(name, input) {
+ if (name === 'AskUserQuestion') {
+ const questions = extractAskUserQuestions(input);
+ if (questions.length > 0) {
+ return createAskUserQuestionView(questions);
+ }
+ }
+ const inputStr = typeof input === 'string' ? input : (input ? JSON.stringify(input, null, 2) : '');
+ const content = document.createElement('div');
+ content.className = 'tool-call-content';
+ content.textContent = inputStr;
+ return content;
+ }
+
function appendToolCall(toolUseId, name, input, done) {
const streamEl = document.getElementById('streaming-msg');
if (!streamEl) return;
@@ -430,11 +530,14 @@
const details = document.createElement('details');
details.className = 'tool-call';
details.id = `tool-${toolUseId}`;
- const inputStr = typeof input === 'string' ? input : (input ? JSON.stringify(input, null, 2) : '');
- details.innerHTML = `
- ${escapeHtml(name)}
- ${escapeHtml(inputStr)}
- `;
+ details.dataset.toolName = name || '';
+ if (name === 'AskUserQuestion') details.open = true;
+
+ const summary = document.createElement('summary');
+ summary.innerHTML = ` ${escapeHtml(name)}`;
+ details.appendChild(summary);
+ details.appendChild(buildToolContentElement(name, input));
+
bubble.appendChild(details);
scrollToBottom();
}
@@ -445,6 +548,9 @@
const icon = el.querySelector('.tool-call-icon');
if (icon) { icon.classList.remove('running'); icon.classList.add('done'); }
if (result) {
+ if (el.dataset.toolName === 'AskUserQuestion') {
+ return;
+ }
const content = el.querySelector('.tool-call-content');
if (content) content.textContent = result;
}
diff --git a/public/style.css b/public/style.css
index 4b1edc6..3b5fb83 100644
--- a/public/style.css
+++ b/public/style.css
@@ -571,6 +571,63 @@ body {
font-family: 'SF Mono', monospace;
}
+/* AskUserQuestion preview */
+.ask-user-question {
+ padding: 10px 12px;
+ background: var(--bg-primary);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.ask-question-card {
+ border: 1px solid var(--border-color);
+ border-radius: 10px;
+ background: #fff;
+ padding: 10px;
+}
+.ask-question-header {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--accent);
+ margin-bottom: 6px;
+}
+.ask-question-text {
+ font-size: 13px;
+ line-height: 1.5;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+}
+.ask-question-options {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.ask-option-item {
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 8px;
+ background: var(--bg-secondary);
+ width: 100%;
+ text-align: left;
+ cursor: pointer;
+ transition: background 0.12s, border-color 0.12s;
+}
+.ask-option-item:hover {
+ background: var(--accent-light);
+ border-color: var(--accent);
+}
+.ask-option-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+.ask-option-desc {
+ font-size: 12px;
+ color: var(--text-secondary);
+ line-height: 1.45;
+ margin-top: 4px;
+}
+
/* Typing indicator */
.typing-indicator {
display: inline-flex;
diff --git a/server.js b/server.js
index 7a1a090..2797d87 100644
--- a/server.js
+++ b/server.js
@@ -1155,7 +1155,8 @@ function processClaudeEvent(entry, event, sessionId) {
entry.fullText += block.text;
wsSend(entry.ws, { type: 'text_delta', text: block.text });
} else if (block.type === 'tool_use') {
- const tc = { name: block.name, id: block.id, input: truncateObj(block.input, 500), done: false };
+ const toolInput = sanitizeToolInput(block.name, block.input);
+ const tc = { name: block.name, id: block.id, input: toolInput, done: false };
entry.toolCalls.push(tc);
wsSend(entry.ws, { type: 'tool_start', name: block.name, toolUseId: block.id, input: tc.input });
} else if (block.type === 'tool_result') {
@@ -1202,6 +1203,29 @@ function truncateObj(obj, maxLen) {
return s.slice(0, maxLen) + '...';
}
+function safeJsonParse(input) {
+ if (input === null || input === undefined) return input;
+ if (typeof input !== 'string') return input;
+ const trimmed = input.trim();
+ if (!trimmed) return input;
+ if (!((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')))) {
+ return input;
+ }
+ try {
+ return JSON.parse(trimmed);
+ } catch {
+ return input;
+ }
+}
+
+function sanitizeToolInput(toolName, input) {
+ const parsed = safeJsonParse(input);
+ if (toolName === 'AskUserQuestion') {
+ return parsed;
+ }
+ return truncateObj(parsed, 500);
+}
+
// === Startup ===
recoverProcesses();