diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx
index a347a43..42511a2 100644
--- a/src/components/Conversation.tsx
+++ b/src/components/Conversation.tsx
@@ -7,7 +7,8 @@ import type { Message, Part, HITLCard } from '../contracts'
import { UIRenderer, InlineConfirmCard, hasOpenConfirm } from '@ui'
import { docBridge } from './Workbench'
-marked.setOptions({ breaks: true })
+// marked 走标准 Markdown 语义(不开 breaks):模型输出按块级排版渲染,
+// 行内换行交给段落折叠,避免与 CSS 块间距叠加出松散排版。
// Part 渲染器注册表:按类型查表;未知类型静默忽略(前向兼容)。
function PartView({ part, live }: { part: Part; live?: boolean | null }) {
@@ -16,7 +17,13 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) {
return
发送消息开始对话
diff --git a/src/store.ts b/src/store.ts
index 24422e7..48cda25 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -308,10 +308,12 @@ export const store = {
return
}
if (out.run) {
- set({
- live: { runId: out.run.id, phase: 'thinking', thinking: '', text: '', toolCalls: [], agentNotes: {}, startedAt: Date.now() },
- messages: [...state.messages, out.message],
- })
+ // 竞态保护:POST 返回前 SSE 事件可能已先行到达并累积(同 run),
+ // 此时保留已累积的 live,只补挂用户消息;否则初始化空 live。
+ if (!state.live || state.live.runId !== out.run.id) {
+ set({ live: { runId: out.run.id, phase: 'thinking', thinking: '', text: '', toolCalls: [], agentNotes: {}, startedAt: Date.now() } })
+ }
+ set({ messages: [...state.messages, out.message] })
store.connectIfNeeded()
}
} catch (e) {
diff --git a/src/styles.css b/src/styles.css
index 6bea78a..00571b9 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -50,16 +50,35 @@ button { font-family: inherit; }
.msg-role { font-size: 12px; color: var(--text-faint); margin-bottom: 4px; display: flex; gap: 8px; align-items: center; }
.msg.user .bubble { background: var(--accent-soft); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; white-space: pre-wrap; }
.part { margin: 4px 0; }
-.part-thinking { color: var(--text-faint); font-size: 13px; border-left: 2px solid var(--border); padding-left: 10px; white-space: pre-wrap; }
+.part-thinking { color: var(--text-dim); font-size: 12.5px; border-left: 2px solid var(--border); padding-left: 10px; margin: 4px 0; }
+.part-thinking summary { cursor: pointer; color: var(--text-faint); user-select: none; }
+.part-thinking .part-thinking-body { white-space: pre-wrap; margin-top: 4px; }
.part-tool { font-size: 12.5px; }
.tool-chip { display: inline-flex; gap: 6px; align-items: center; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 4px 8px; margin: 2px 4px 2px 0; font-family: var(--mono); }
.tool-chip .status-ok { color: var(--ok); }
.tool-chip .status-error { color: var(--err); }
-.part-text { white-space: pre-wrap; line-height: 1.7; }
-.part-text :is(h1,h2,h3) { margin: 0.6em 0 0.3em; }
+.part-text { line-height: 1.65; overflow-wrap: break-word; }
+/* 块级间距只由标签自身控制:禁用 pre-wrap(marked 输出的块间换行会被渲染成空行,
+ 叠加出"列表逐项空一行"的松散排版);行内换行语义交给 Markdown 段落。 */
+.part-text > *:first-child { margin-top: 0; }
+.part-text > *:last-child { margin-bottom: 0; }
+.part-text p { margin: 0.5em 0; }
+.part-text :is(h1,h2,h3,h4) { margin: 0.8em 0 0.35em; line-height: 1.35; font-weight: 600; }
+.part-text h1 { font-size: 1.3em; }
+.part-text h2 { font-size: 1.18em; }
+.part-text h3 { font-size: 1.06em; }
+.part-text :is(ul,ol) { margin: 0.4em 0; padding-left: 1.6em; }
+.part-text li { margin: 0.15em 0; }
+.part-text li > p { margin: 0.2em 0; }
.part-text pre { background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; overflow-x: auto; font-family: var(--mono); font-size: 12.5px; }
.part-text code { font-family: var(--mono); background: var(--bg-raised); border-radius: 4px; padding: 1px 5px; font-size: 12.5px; }
-.part-text :is(p,ul,ol) { margin: 0.4em 0; }
+.part-text pre code { background: none; padding: 0; }
+.part-text blockquote { margin: 0.5em 0; padding: 0.1em 0 0.1em 10px; border-left: 3px solid var(--border); color: var(--text-dim); }
+.part-text table { border-collapse: collapse; margin: 0.6em 0; font-size: 0.95em; display: block; max-width: 100%; overflow-x: auto; }
+.part-text th, .part-text td { border: 1px solid var(--border); padding: 4px 10px; text-align: left; }
+.part-text th { background: var(--bg-raised); font-weight: 600; }
+.part-text hr { border: none; border-top: 1px solid var(--border); margin: 0.8em 0; }
+.part-text a { color: var(--accent); }
.part-error { color: var(--err); background: color-mix(in srgb, var(--err) 8%, transparent); border: 1px solid var(--err); border-radius: var(--radius-sm); padding: 8px 12px; }
.artifact-chip { display: inline-flex; align-items: center; gap: 6px; color: var(--accent); background: var(--accent-soft); border-radius: var(--radius-sm); padding: 4px 10px; cursor: pointer; font-size: 13px; }
.artifact-chip:hover { filter: brightness(1.15); }