Refine workspace reading and mobile layout

This commit is contained in:
caoqianming 2026-07-29 14:59:10 +08:00
parent 07eba5c402
commit 369c74483c
7 changed files with 300 additions and 59 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。 > 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.60.9 — 2026-07-29
- 网页工作台改用更温和的暖白配色,任务与文件列表的分隔更加清楚;助手回复按完整轮次形成统一阅读块,手机端输入区也改为更紧凑的图标操作布局。
## 0.60.8 — 2026-07-29 ## 0.60.8 — 2026-07-29
- 网页工作台完成一轮整体视觉优化:任务、对话和文件三栏层级更清晰,长回复阅读更舒适,按钮、输入框、弹窗、状态反馈及手机端触控尺寸也更加统一。 - 网页工作台完成一轮整体视觉优化:任务、对话和文件三栏层级更清晰,长回复阅读更舒适,按钮、输入框、弹窗、状态反馈及手机端触控尺寸也更加统一。

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-07-29(Web 工作台 Visual Polish v1,bump 0.60.8) 最后更新:2026-07-29(Web 工作台视觉复核修正,bump 0.60.9)
--- ---
@ -23,6 +23,7 @@
### 2026-07-29 ### 2026-07-29
- **07-29 / 0.60.9 / Web 工作台视觉复核修正(桌面 + 手机实图)**:据上线截图把冷蓝灰改为暖白/浅米灰;任务与文件行恢复明确分隔,助手消息收进居中 52rem 文档型回答块,工具与进度对齐同一阅读轨道;同时修掉 `task_progress`-only assistant 被过滤工具后仍留下空“助手”壳。手机端 composer 改为独立状态行 + 3 个 44px SVG 图标键 + 发送键,缩短 placeholder、加入安全区并压缩操作/模型区;录音与润色状态改只更新 label/ARIA不再覆盖 SVG DOM。相关 Python 21 项 + Node 5 项及 JS 语法、HTML/SVG 结构、CSS 括号检查通过;无 API、schema、migration 或依赖变化。
- **07-29 / 0.60.8 / Web 工作台 Visual Polish v1**:新增零构建 `web/static/ui.css` 作为内部轻量视觉基础层,集中颜色、间距、圆角、阴影与 focus/disabled 等控件状态;收敛三栏层级、任务/资源导航、对话正文与工具过程块、输入区、文件栏、modal/toast 及 ≤640px 触控尺寸,不改 DOM 契约、API 或业务交互。新增 CSS 文件接入、MIME 与 `no-cache` 回归测试;相关 Python 20 项 + Node 5 项通过,浏览器连接不可用故截图级视觉验收留真实页面复核。 - **07-29 / 0.60.8 / Web 工作台 Visual Polish v1**:新增零构建 `web/static/ui.css` 作为内部轻量视觉基础层,集中颜色、间距、圆角、阴影与 focus/disabled 等控件状态;收敛三栏层级、任务/资源导航、对话正文与工具过程块、输入区、文件栏、modal/toast 及 ≤640px 触控尺寸,不改 DOM 契约、API 或业务交互。新增 CSS 文件接入、MIME 与 `no-cache` 回归测试;相关 Python 20 项 + Node 5 项通过,浏览器连接不可用故截图级视觉验收留真实页面复核。
- **07-29 / 0.60.7 / 组合框内直接新建工作目录**:新对话目录 combobox 增加 allow-create 语义:输入时继续过滤已有目录,无完全同名项时在同一下拉内提供“创建工作目录「名称」”,明确选择后才进入新建状态,失焦搜索词不会误提交;删除外部新建按钮与二级输入框。纯前端改动,无 API、schema、migration 或目录绑定语义变化;全量 387 测试通过17 项按环境跳过)。 - **07-29 / 0.60.7 / 组合框内直接新建工作目录**:新对话目录 combobox 增加 allow-create 语义:输入时继续过滤已有目录,无完全同名项时在同一下拉内提供“创建工作目录「名称」”,明确选择后才进入新建状态,失焦搜索词不会误提交;删除外部新建按钮与二级输入框。纯前端改动,无 API、schema、migration 或目录绑定语义变化;全量 387 测试通过17 项按环境跳过)。
- **07-29 / 0.60.6 / 新对话目录选择改可搜索组合框**:将上一版独立搜索框 + 原生 select 合并为单一 combobox同一输入区完成已选目录展示、即时过滤和下拉选择并补方向键、Enter、Esc、失焦恢复与 ARIA 状态;最近目录置顶、明确选择 working_dir 和新建目录入口均保持不变。纯前端改动无依赖、API、schema 或 migration全量 387 测试通过17 项按环境跳过)。 - **07-29 / 0.60.6 / 新对话目录选择改可搜索组合框**:将上一版独立搜索框 + 原生 select 合并为单一 combobox同一输入区完成已选目录展示、即时过滤和下拉选择并补方向键、Enter、Esc、失焦恢复与 ARIA 状态;最近目录置顶、明确选择 working_dir 和新建目录入口均保持不变。纯前端改动无依赖、API、schema 或 migration全量 387 测试通过17 项按环境跳过)。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.60.8" __version__ = "0.60.9"

View File

@ -53,6 +53,20 @@ class StaticVendorTests(unittest.TestCase):
self.assertIn(":focus-visible", css) self.assertIn(":focus-visible", css)
self.assertIn("@media (max-width: 640px)", css) self.assertIn("@media (max-width: 640px)", css)
def test_ui_polish_keeps_mobile_composer_and_reading_rail_contracts(self) -> None:
html = DEV_HTML.read_text(encoding="utf-8")
css = UI_CSS.read_text(encoding="utf-8")
frontend = _frontend_source()
self.assertIn("--list-divider:", css)
self.assertIn(".msg.assistant", css)
self.assertIn(".composer-tool .composer-label", css)
self.assertIn('aria-label="添加附件"', html)
self.assertIn('aria-label="语音输入"', html)
self.assertIn('aria-label="润色提示词"', html)
self.assertIn("hasVisibleAssistantContent", frontend)
self.assertIn('input.placeholder = mqPhone.matches', frontend)
def test_frontend_surfaces_context_and_cache_stats(self) -> None: def test_frontend_surfaces_context_and_cache_stats(self) -> None:
src = _frontend_source() src = _frontend_source()

View File

@ -1841,10 +1841,31 @@
<span id="chat-attach"></span> <span id="chat-attach"></span>
<div class="row"> <div class="row">
<span class="hint" id="chat-hint">就绪</span> <span class="hint" id="chat-hint">就绪</span>
<span style="flex:1;"></span> <span class="composer-spacer"></span>
<button type="button" class="small" id="chat-attach-btn" title="添加附件:手机可拍照 / 选照片,电脑直接开文件选择器(也可粘贴 / 拖拽)">📎 附件</button> <button type="button" class="small composer-tool" id="chat-attach-btn"
<button type="button" class="small" id="chat-mic" title="语音输入:点击开始录音,再点结束并转写填入输入框(最长 60 秒)">🎙 语音</button> aria-label="添加附件" title="添加附件:手机可拍照 / 选照片,电脑直接开文件选择器(也可粘贴 / 拖拽)">
<button type="button" class="small" id="chat-optimize" disabled title="用当前对话模型润色草稿(参考已选生图模型偏好)— 替换为更清晰可执行的 prompt,Ctrl+Z 可撤销">✨ 润色</button> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m21.4 11.6-8.9 8.9a6 6 0 0 1-8.5-8.5l9.6-9.6a4 4 0 0 1 5.7 5.7l-9.6 9.6a2 2 0 1 1-2.8-2.8l8.9-8.9"/>
</svg>
<span class="composer-label">附件</span>
</button>
<button type="button" class="small composer-tool" id="chat-mic"
aria-label="语音输入" title="语音输入:点击开始录音,再点结束并转写填入输入框(最长 60 秒)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5M8 22h8"/>
</svg>
<span class="composer-label">语音</span>
</button>
<button type="button" class="small composer-tool" id="chat-optimize" disabled
aria-label="润色提示词" title="用当前对话模型润色草稿(参考已选生图模型偏好)— 替换为更清晰可执行的 prompt,Ctrl+Z 可撤销">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m15 4 5 5L8 21H3v-5L15 4Z"/><path d="m13 6 5 5M5 3v4M3 5h4M19 16v5M16.5 18.5h5"/>
</svg>
<span class="composer-label">润色</span>
</button>
<button type="submit" class="primary" id="chat-action">发送</button> <button type="submit" class="primary" id="chat-action">发送</button>
</div> </div>
</form> </form>

View File

@ -620,7 +620,7 @@ export async function showNewConversationDraft() {
$("chat-form").style.display = "flex"; $("chat-form").style.display = "flex";
$("chat-input").readOnly = false; $("chat-input").readOnly = false;
$("chat-input").classList.remove("readonly-locked"); $("chat-input").classList.remove("readonly-locked");
$("chat-input").placeholder = "输入消息…(Enter 发送Shift+Enter 换行)"; setComposerPlaceholder(null);
$("chat-attach-btn").disabled = true; // task_dir 尚不存在,首发建 task 后开放 $("chat-attach-btn").disabled = true; // task_dir 尚不存在,首发建 task 后开放
$("chat-mic").disabled = false; $("chat-mic").disabled = false;
$("btn-done").disabled = true; $("btn-done").disabled = true;
@ -1275,7 +1275,7 @@ function finishRunningTool(run) {
function createLiveAssistantCard(run) { function createLiveAssistantCard(run) {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "msg assistant live-run"; card.className = "msg assistant live-run";
card.innerHTML = `<div class="role">助手</div>`; card.innerHTML = `<div class="role">科研助手</div>`;
run.card = card; run.card = card;
run.curSeg = null; run.curSeg = null;
if (run.acc) { if (run.acc) {
@ -1509,18 +1509,21 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "msg " + role; card.className = "msg " + role;
card.dataset.idx = m.idx; card.dataset.idx = m.idx;
const roleLabel = { user: "我", assistant: "助手", error: "错误" }[role] || role; const roleLabel = { user: "我", assistant: "科研助手", error: "错误" }[role] || role;
let html = `<div class="role">${roleLabel}</div>`; let html = `<div class="role">${roleLabel}</div>`;
let hasVisibleAssistantContent = false;
// 思考过程(thinking 模型持久化的 reasoning_content):默认折叠,与直播态同构 // 思考过程(thinking 模型持久化的 reasoning_content):默认折叠,与直播态同构
if (role === "assistant") { if (role === "assistant") {
const rc = (typeof p.reasoning_content === "string" && p.reasoning_content) const rc = (typeof p.reasoning_content === "string" && p.reasoning_content)
|| ((p.provider_specific_fields || {}).reasoning_content || ""); || ((p.provider_specific_fields || {}).reasoning_content || "");
if (rc) { if (rc) {
html += `<details class="reasoning"><summary>思考过程</summary><div class="reason-body">${escapeHtml(rc)}</div></details>`; html += `<details class="reasoning"><summary>思考过程</summary><div class="reason-body">${escapeHtml(rc)}</div></details>`;
hasVisibleAssistantContent = true;
} }
} }
if (typeof p.content === "string" && p.content) { if (typeof p.content === "string" && p.content) {
html += `<div class="body">${renderMd(p.content)}</div>`; html += `<div class="body">${renderMd(p.content)}</div>`;
if (role === "assistant") hasVisibleAssistantContent = true;
// assistant 正文里 echo 的 <wd>/... 路径**永远**挂 chip(绕开 seenRels —— 上面 // assistant 正文里 echo 的 <wd>/... 路径**永远**挂 chip(绕开 seenRels —— 上面
// tool 结果可能 inline 过同图,但 chip 是小按钮无视觉污染,助手回复里有可 // tool 结果可能 inline 过同图,但 chip 是小按钮无视觉污染,助手回复里有可
// 点的"产物锚点"比没有好);强制 allowInlineMedia=false 防止大图被重复 inline。 // 点的"产物锚点"比没有好);强制 allowInlineMedia=false 防止大图被重复 inline。
@ -1555,6 +1558,7 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
} }
} }
html += buildAskUserCard(argsObj, { interactive: !answered, chosenLabel: chosen }).outerHTML; html += buildAskUserCard(argsObj, { interactive: !answered, chosenLabel: chosen }).outerHTML;
hasVisibleAssistantContent = true;
continue; continue;
} }
const label = toolActivityLabel(fn, argsObj); const label = toolActivityLabel(fn, argsObj);
@ -1564,6 +1568,7 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
<details class="tool-call"><summary>${escapeHtml(label)}</summary><pre>${escapeHtml(args)}</pre></details> <details class="tool-call"><summary>${escapeHtml(label)}</summary><pre>${escapeHtml(args)}</pre></details>
${renderArtifactBarHtml(rels, isProducer)} ${renderArtifactBarHtml(rels, isProducer)}
`; `;
hasVisibleAssistantContent = true;
} }
// 进度不再内联进消息卡 —— 累积值在循环末统一喂顶部 dock(见 setTaskProgress) // 进度不再内联进消息卡 —— 累积值在循环末统一喂顶部 dock(见 setTaskProgress)
} }
@ -1571,6 +1576,9 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
const hasCopy = role === "user" const hasCopy = role === "user"
|| (role === "assistant" && typeof p.content === "string" && p.content.trim()); || (role === "assistant" && typeof p.content === "string" && p.content.trim());
if (hasCopy) html += msgActionsHtml(); if (hasCopy) html += msgActionsHtml();
// task_progress-only assistant rows feed the persistent progress dock.
// Once that tool call is filtered, do not leave an empty assistant shell.
if (role === "assistant" && !hasVisibleAssistantContent) continue;
card.innerHTML = html; card.innerHTML = html;
highlightIn(card); highlightIn(card);
wrap.appendChild(card); wrap.appendChild(card);
@ -1828,9 +1836,7 @@ function applyChannelComposerLock(meta) {
const cfg = meta && channelCfg(meta.channel); // 微信 / 企业微信镜像 → 只读 const cfg = meta && channelCfg(meta.channel); // 微信 / 企业微信镜像 → 只读
input.readOnly = !!cfg; input.readOnly = !!cfg;
input.classList.toggle("readonly-locked", !!cfg); input.classList.toggle("readonly-locked", !!cfg);
input.placeholder = cfg setComposerPlaceholder(cfg);
? `${cfg.label}对话请在${cfg.label}里进行 — web 端为只读镜像,可查看历史`
: "输入消息…(Enter 发送,Shift+Enter 换行,可粘贴 / 拖拽文件)";
if (cfg) { if (cfg) {
const opt = $("chat-optimize"); const opt = $("chat-optimize");
if (opt) opt.disabled = true; if (opt) opt.disabled = true;
@ -1841,6 +1847,24 @@ function applyChannelComposerLock(meta) {
if (attach) attach.disabled = !!cfg; // 附件按钮同口径(与拖拽/粘贴的 _composerLocked 一致) if (attach) attach.disabled = !!cfg; // 附件按钮同口径(与拖拽/粘贴的 _composerLocked 一致)
} }
function setComposerPlaceholder(cfg) {
const input = $("chat-input");
if (!input) return;
if (cfg) {
input.placeholder = mqPhone.matches
? `${cfg.label}对话请在${cfg.label}中进行`
: `${cfg.label}对话请在${cfg.label}里进行 — web 端为只读镜像,可查看历史`;
return;
}
input.placeholder = mqPhone.matches
? "输入消息…"
: "输入消息…(Enter 发送,Shift+Enter 换行,可粘贴 / 拖拽文件)";
}
mqPhone.addEventListener("change", () => {
setComposerPlaceholder(channelCfg(state.taskMeta && state.taskMeta.channel));
});
// 润色:同步调后端,把 textarea 内容替成优化后文本。用 execCommand('insertText') // 润色:同步调后端,把 textarea 内容替成优化后文本。用 execCommand('insertText')
// 接 textarea 原生 undo 栈 — Ctrl+Z 一次回到原文。streaming 期间允许并行(后端 // 接 textarea 原生 undo 栈 — Ctrl+Z 一次回到原文。streaming 期间允许并行(后端
// 不与主对话 run 互斥,各跑各的 LLM)。 // 不与主对话 run 互斥,各跑各的 LLM)。
@ -1861,8 +1885,11 @@ async function optimizePrompt() {
const btn = $("chat-optimize"); const btn = $("chat-optimize");
state.optimizing = true; state.optimizing = true;
btn.disabled = true; btn.disabled = true;
const oldLabel = btn.textContent; const label = btn.querySelector(".composer-label");
btn.textContent = "润色中…"; const oldLabel = label ? label.textContent : "润色";
const oldAriaLabel = btn.getAttribute("aria-label") || "润色提示词";
if (label) label.textContent = "润色中";
btn.setAttribute("aria-label", "正在润色");
const oldHint = $("chat-hint").textContent; const oldHint = $("chat-hint").textContent;
$("chat-hint").textContent = "润色中…"; $("chat-hint").textContent = "润色中…";
try { try {
@ -1891,7 +1918,8 @@ async function optimizePrompt() {
$("chat-hint").textContent = `润色失败:${e.message}`; $("chat-hint").textContent = `润色失败:${e.message}`;
} finally { } finally {
state.optimizing = false; state.optimizing = false;
btn.textContent = oldLabel; if (label) label.textContent = oldLabel;
btn.setAttribute("aria-label", oldAriaLabel);
syncOptimizeBtn(); syncOptimizeBtn();
} }
} }
@ -2064,7 +2092,11 @@ async function startVoiceInput(mode, startY) {
cv.height = Math.max(1, cv.clientHeight * dpr); cv.height = Math.max(1, cv.clientHeight * dpr);
_vrHint(mode === "hold" ? "松开转写 · 上滑取消" : "说完按 Enter 结束 · Esc 取消"); _vrHint(mode === "hold" ? "松开转写 · 上滑取消" : "说完按 Enter 结束 · Esc 取消");
btn.classList.add("recording"); btn.classList.add("recording");
if (mode === "click") btn.textContent = "■ 停止"; if (mode === "click") {
const label = btn.querySelector(".composer-label");
if (label) label.textContent = "停止";
btn.setAttribute("aria-label", "停止录音");
}
const tick = () => { const tick = () => {
if (!_voice) return; if (!_voice) return;
const sec = Math.floor((Date.now() - _voice.t0) / 1000); const sec = Math.floor((Date.now() - _voice.t0) / 1000);
@ -2080,7 +2112,9 @@ async function startVoiceInput(mode, startY) {
if (mode === "hold" && _holdReleased) { if (mode === "hold" && _holdReleased) {
if (Date.now() - _pressT0 < HOLD_TOGGLE_MS && !_voice.cancelArmed) { if (Date.now() - _pressT0 < HOLD_TOGGLE_MS && !_voice.cancelArmed) {
_voice.mode = "click"; _voice.mode = "click";
btn.textContent = "■ 停止"; const label = btn.querySelector(".composer-label");
if (label) label.textContent = "停止";
btn.setAttribute("aria-label", "停止录音");
_vrHint("说完按 Enter 结束 · Esc 取消"); _vrHint("说完按 Enter 结束 · Esc 取消");
} else { } else {
stopVoiceInput(_voice.cancelArmed); stopVoiceInput(_voice.cancelArmed);
@ -2096,7 +2130,9 @@ async function stopVoiceInput(discard) {
cancelAnimationFrame(v.raf); cancelAnimationFrame(v.raf);
const btn = $("chat-mic"); const btn = $("chat-mic");
btn.classList.remove("recording"); btn.classList.remove("recording");
btn.textContent = "🎙 语音"; const label = btn.querySelector(".composer-label");
if (label) label.textContent = "语音";
btn.setAttribute("aria-label", "语音输入");
$("chat-form").classList.remove("recording"); $("chat-form").classList.remove("recording");
$("voice-rec-panel").classList.remove("cancel-armed"); $("voice-rec-panel").classList.remove("cancel-armed");
$("vr-time").textContent = "0:00"; $("vr-time").textContent = "0:00";
@ -2382,7 +2418,7 @@ async function sendMessage(overrideText) {
// assistant 流式占位卡 // assistant 流式占位卡
const asstCard = document.createElement("div"); const asstCard = document.createElement("div");
asstCard.className = "msg assistant live-run"; asstCard.className = "msg assistant live-run";
asstCard.innerHTML = `<div class="role">助手</div><div class="body streaming"></div>`; asstCard.innerHTML = `<div class="role">科研助手</div><div class="body streaming"></div>`;
wrap.appendChild(asstCard); wrap.appendChild(asstCard);
wrap.scrollTop = wrap.scrollHeight; wrap.scrollTop = wrap.scrollHeight;

View File

@ -11,15 +11,16 @@
color-scheme: light; color-scheme: light;
/* Neutral workspace palette */ /* Neutral workspace palette */
--bg: #f3f5f7; --bg: #f6f4f1;
--panel: #ffffff; --panel: #ffffff;
--panel-subtle: #f8f9fb; --panel-subtle: #fbfaf8;
--panel-muted: #f4f6f8; --panel-muted: #f7f4f0;
--border: #dde2e7; --border: #e2ddd7;
--border-soft: #e9edf1; --border-soft: #ede8e2;
--text: #20242a; --list-divider: #ddd7d0;
--text-strong: #15181d; --text: #292623;
--muted: #6f7782; --text-strong: #1c1a18;
--muted: #756f69;
/* Institute red is deliberately reserved for primary emphasis. */ /* Institute red is deliberately reserved for primary emphasis. */
--accent: #b83a31; --accent: #b83a31;
@ -27,9 +28,9 @@
--accent-soft: #fbecea; --accent-soft: #fbecea;
--accent-ring: rgba(184, 58, 49, 0.16); --accent-ring: rgba(184, 58, 49, 0.16);
--hover: #f0f3f5; --hover: #f5f1ed;
--code-bg: #f3f5f7; --code-bg: #f6f3ef;
--user-bg: #f1f5fa; --user-bg: #f5f1eb;
--asst-bg: #ffffff; --asst-bg: #ffffff;
--c-green: #238653; --c-green: #238653;
@ -97,7 +98,7 @@ button.small {
} }
button:hover:not(:disabled) { button:hover:not(:disabled) {
border-color: #cbd2d9; border-color: #cdc5bd;
} }
button.primary { button.primary {
@ -136,7 +137,7 @@ select {
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover,
textarea:hover, textarea:hover,
select:hover { select:hover {
border-color: #c7ced5; border-color: #c9c1b9;
} }
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus,
@ -148,7 +149,7 @@ select:focus {
} }
::placeholder { ::placeholder {
color: #9aa1aa; color: #9b958f;
} }
/* Application shell */ /* Application shell */
@ -202,7 +203,7 @@ header .who {
} }
.splitter::before { .splitter::before {
background: #d7dde3; background: #d9d3cc;
} }
.pane-head { .pane-head {
@ -224,7 +225,7 @@ header .who {
#task-scroll, #task-scroll,
#file-list { #file-list {
scrollbar-color: #cbd1d7 transparent; scrollbar-color: #cec7bf transparent;
scrollbar-width: thin; scrollbar-width: thin;
} }
@ -240,27 +241,28 @@ header .who {
} }
#task-list { #task-list {
padding: 4px 8px 8px; padding: 0;
} }
.task-row { .task-row {
margin: 3px 0; margin: 0;
padding: 9px 10px; padding: 10px 12px 10px 9px;
border: 1px solid transparent; border: 0;
border-radius: var(--r-md); border-left: 3px solid transparent;
border-bottom: 1px solid var(--list-divider);
border-radius: 0;
} }
.task-row:hover { .task-row:hover {
background: var(--panel); background: var(--panel);
border-color: var(--border-soft); box-shadow: none;
box-shadow: 0 1px 3px rgba(24, 32, 41, 0.04);
} }
.task-row.active { .task-row.active {
padding-left: 8px; padding-left: 9px;
background: var(--accent-soft); background: var(--accent-soft);
border-color: #f0d2cf;
border-left: 3px solid var(--accent); border-left: 3px solid var(--accent);
border-bottom-color: #e8c6c3;
box-shadow: none; box-shadow: none;
} }
@ -335,7 +337,8 @@ header .who {
line-height: 1.7; line-height: 1.7;
} }
/* Conversation: assistant content reads like a document, not a bubble stack. */ /* Conversation: one centred reading rail, with each assistant turn grouped as
a document block rather than a floating chat label. */
#chat-meta { #chat-meta {
min-height: 40px; min-height: 40px;
@ -345,7 +348,7 @@ header .who {
} }
#chat-stream { #chat-stream {
gap: 14px; gap: 16px;
padding: 20px clamp(16px, 3vw, 36px) 24px; padding: 20px clamp(16px, 3vw, 36px) 24px;
background: var(--panel); background: var(--panel);
scrollbar-color: #cbd1d7 transparent; scrollbar-color: #cbd1d7 transparent;
@ -364,14 +367,21 @@ header .who {
} }
.msg.assistant { .msg.assistant {
padding: 8px 0; width: min(100%, 52rem);
border-color: transparent; max-width: min(100%, 52rem);
border-radius: 0; align-self: center;
padding: 16px 18px;
border: 1px solid var(--border-soft);
border-left: 3px solid #cbc3bb;
border-radius: var(--r-lg);
background: #fffdfa;
} }
.msg.assistant.live-run { .msg.assistant.live-run {
padding: 12px 14px; padding: 16px 18px;
border-color: rgba(184, 58, 49, 0.2); border-color: rgba(184, 58, 49, 0.2);
border-left-color: var(--accent);
border-left-width: 3px;
border-radius: var(--r-lg); border-radius: var(--r-lg);
box-shadow: box-shadow:
0 0 0 1px rgba(184, 58, 49, 0.05), 0 0 0 1px rgba(184, 58, 49, 0.05),
@ -387,6 +397,42 @@ header .who {
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
.msg.assistant > .role {
display: flex;
align-items: center;
gap: 7px;
color: #6d665f;
}
.msg.assistant > .role::before {
content: "";
width: 7px;
height: 7px;
flex: none;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.msg.tool {
width: min(100%, 52rem);
max-width: min(100%, 52rem);
align-self: center;
padding: 0 18px;
border: 0;
background: transparent;
}
.msg.tool > .role {
margin-bottom: 5px;
}
#task-progress-dock .task-progress {
width: min(100%, 52rem);
max-width: 52rem;
margin: 0 auto;
}
.msg .body { .msg .body {
color: var(--text); color: var(--text);
font-size: 15px; font-size: 15px;
@ -410,7 +456,7 @@ header .who {
.tool-call, .tool-call,
.reasoning { .reasoning {
color: #4f5863; color: #5f5851;
} }
.tool-call summary, .tool-call summary,
@ -444,13 +490,30 @@ header .who {
padding: 10px 12px; padding: 10px 12px;
line-height: 1.6; line-height: 1.6;
border-radius: var(--r-lg); border-radius: var(--r-lg);
background: #fcfcfd; background: #fffdfa;
} }
#chat-form .row { #chat-form .row {
align-items: center; align-items: center;
} }
.composer-spacer {
flex: 1;
}
.composer-tool {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.composer-tool svg {
width: 15px;
height: 15px;
flex: none;
}
#chat-action { #chat-action {
min-width: 64px; min-width: 64px;
} }
@ -467,7 +530,7 @@ header .who {
.file-row { .file-row {
min-height: 38px; min-height: 38px;
padding: 7px 12px; padding: 7px 12px;
border-bottom-color: var(--border-soft); border-bottom: 1px solid var(--list-divider);
} }
.file-row:hover { .file-row:hover {
@ -475,7 +538,7 @@ header .who {
} }
.file-row .name { .file-row .name {
color: #30363d; color: #332f2b;
} }
.storage-foot { .storage-foot {
@ -486,7 +549,7 @@ header .who {
.storage-foot .bar { .storage-foot .bar {
height: 5px; height: 5px;
background: #e5e9ed; background: #e8e3de;
} }
/* Dialogs and notifications */ /* Dialogs and notifications */
@ -557,13 +620,69 @@ header .who {
min-height: 42px; min-height: 42px;
} }
.mobile-tabs {
gap: 6px;
}
.mobile-tabs button {
min-height: 38px;
padding: 6px 8px;
}
#pane-mid > .pane-head {
min-height: 38px;
padding: 5px 10px;
}
#pane-mid > .pane-head > button {
min-height: 30px;
}
#chat-meta {
min-height: auto;
padding: 7px 10px;
column-gap: 7px;
row-gap: 7px;
}
#chat-meta > .spacer {
flex: 1 0 100%;
height: 0;
}
#chat-meta .mdl-wrap {
flex: 1 1 auto;
min-width: 0;
}
#chat-meta select {
width: 100% !important;
max-width: none;
}
#task-progress-dock {
padding: 6px 10px;
}
#chat-stream { #chat-stream {
gap: 12px; gap: 14px;
padding: 14px 12px 18px; padding: 14px 12px 18px;
} }
.msg { .msg {
max-width: 97%; max-width: 100%;
}
.msg.assistant {
width: 100%;
max-width: 100%;
padding: 13px 14px;
}
.msg.tool {
width: 100%;
max-width: 100%;
padding: 0 14px;
} }
.msg .body { .msg .body {
@ -573,14 +692,60 @@ header .who {
#chat-form { #chat-form {
padding: 10px; padding: 10px;
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
} }
#chat-input { #chat-input {
min-height: 64px; min-height: 64px;
} }
#chat-form .row button { #chat-form .row {
min-height: 36px; flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
#chat-hint {
order: -1;
flex: 1 0 100%;
min-height: 18px;
line-height: 18px;
}
.composer-spacer {
display: none;
}
#chat-form .row .composer-tool {
width: 44px;
min-width: 44px;
height: 44px;
min-height: 44px;
padding: 0;
}
.composer-tool svg {
width: 19px;
height: 19px;
}
.composer-tool .composer-label {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
#chat-action {
width: 72px;
min-width: 72px;
height: 44px;
min-height: 44px;
} }
.modal { .modal {