mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Keep all assistant-side transcript content on one shared content rail
The desktop transcript had drifted into multiple competing left edges. Assistant bubbles, thinking rows, tool cards, permissions, and standalone results were using different offsets, which made the timeline feel visually broken even when the underlying data was correct. This change removes the ad hoc assistant-side indentation and makes the assistant output lane follow the same content rail as the composer. The assistant message component still distinguishes short bubble replies from markdown-heavy document replies, but both now sit on the same shared left alignment. Supporting chat blocks were updated to use that same rail so the whole transcript reads as one coherent column. Constraint: Assistant transcript content must align with the composer rail, not with local per-block offsets Rejected: Keep tool/thinking blocks on a separate inset lane | creates multiple left edges and keeps the transcript visually inconsistent Rejected: Fix only final assistant replies | leaves the rest of the assistant-side timeline misaligned Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat the composer rail as the canonical left edge for all assistant-side transcript blocks unless the entire transcript layout is redesigned together Tested: bun run test src/components/chat/MessageList.test.tsx --run; bun run lint; bun run build Not-tested: Real Tauri runtime screenshot against a live session after this unified alignment change
This commit is contained in:
parent
70fb6c429e
commit
bef9596ed5
@ -135,7 +135,7 @@ export function AskUserQuestion({ toolUseId, input, result }: Props) {
|
||||
if (!activeQuestion) return null
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ml-10 rounded-[var(--radius-lg)] border overflow-hidden ${
|
||||
<div className={`mb-4 rounded-[var(--radius-lg)] border overflow-hidden ${
|
||||
submitted
|
||||
? 'border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-low)] opacity-70'
|
||||
: 'border-[var(--color-secondary)] bg-[var(--color-surface-container-lowest)]'
|
||||
|
||||
@ -8,14 +8,23 @@ type Props = {
|
||||
}
|
||||
|
||||
export function AssistantMessage({ content, isStreaming }: Props) {
|
||||
const documentLayout = shouldUseDocumentLayout(content)
|
||||
|
||||
return (
|
||||
<div className="group mb-5 flex justify-start">
|
||||
<div
|
||||
data-message-shell="assistant"
|
||||
className="flex min-w-0 w-full max-w-[84%] flex-col items-start gap-2 sm:max-w-[80%] lg:max-w-[74%]"
|
||||
data-layout={documentLayout ? 'document' : 'bubble'}
|
||||
className={`flex min-w-0 flex-col items-start gap-2 ${
|
||||
documentLayout
|
||||
? 'w-full max-w-full'
|
||||
: 'w-full max-w-[88%] sm:max-w-[80%] lg:max-w-[72%]'
|
||||
}`}
|
||||
>
|
||||
<div className="rounded-[20px] rounded-tl-[8px] border border-[var(--color-border)]/60 bg-[var(--color-surface)] px-4 py-3 text-sm text-[var(--color-text-primary)] shadow-sm">
|
||||
<MarkdownRenderer content={content} />
|
||||
<div className={`rounded-[20px] rounded-tl-[8px] border border-[var(--color-border)]/60 bg-[var(--color-surface)] px-4 py-3 text-sm text-[var(--color-text-primary)] shadow-sm ${
|
||||
documentLayout ? 'w-full' : 'max-w-full'
|
||||
}`}>
|
||||
<MarkdownRenderer content={content} variant={documentLayout ? 'document' : 'default'} />
|
||||
{!isStreaming && <InlineImageGallery text={content} />}
|
||||
{isStreaming && (
|
||||
<span className="ml-0.5 inline-block h-4 w-0.5 animate-shimmer bg-[var(--color-brand)] align-text-bottom" />
|
||||
@ -31,3 +40,18 @@ export function AssistantMessage({ content, isStreaming }: Props) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function shouldUseDocumentLayout(content: string) {
|
||||
const normalized = content.trim()
|
||||
if (!normalized) return false
|
||||
|
||||
if (/```/.test(normalized)) return true
|
||||
if (/^\s{0,3}(#{1,6}\s|[-*+]\s|\d+\.\s|>\s|\|.+\|)/m.test(normalized)) return true
|
||||
|
||||
const paragraphs = normalized
|
||||
.split(/\n\s*\n/)
|
||||
.map((chunk) => chunk.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
return paragraphs.length >= 2 || normalized.split('\n').filter((line) => line.trim()).length >= 8
|
||||
}
|
||||
|
||||
@ -371,10 +371,46 @@ describe('MessageList nested tool calls', () => {
|
||||
expect(userShell?.className).toContain('items-end')
|
||||
expect(assistantShell).toBeTruthy()
|
||||
expect(assistantShell?.className).toContain('items-start')
|
||||
expect(assistantShell?.className).not.toContain('ml-10')
|
||||
expect(userActions?.getAttribute('data-align')).toBe('end')
|
||||
expect(assistantActions?.getAttribute('data-align')).toBe('start')
|
||||
})
|
||||
|
||||
it('uses the document column for markdown-heavy assistant replies', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
messages: [
|
||||
{
|
||||
id: 'assistant-doc',
|
||||
type: 'assistant_text',
|
||||
content: [
|
||||
'## 交付结果',
|
||||
'',
|
||||
'已完成以下内容:',
|
||||
'',
|
||||
'- 添加任务',
|
||||
'- 删除任务',
|
||||
'',
|
||||
'```bash',
|
||||
'npm run build',
|
||||
'```',
|
||||
].join('\n'),
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
render(<MessageList />)
|
||||
|
||||
const assistantShell = screen.getByText('交付结果').closest('[data-message-shell="assistant"]')
|
||||
expect(assistantShell?.getAttribute('data-layout')).toBe('document')
|
||||
expect(assistantShell?.className).toContain('w-full')
|
||||
expect(assistantShell?.className).not.toContain('ml-10')
|
||||
})
|
||||
|
||||
it('opens a rewind preview modal for user messages', async () => {
|
||||
vi.spyOn(sessionsApi, 'rewind').mockResolvedValue({
|
||||
target: {
|
||||
|
||||
@ -127,7 +127,7 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr
|
||||
const allowRawToggle = !preview
|
||||
|
||||
return (
|
||||
<div className={`mb-4 ml-10 overflow-hidden rounded-[var(--radius-lg)] border ${
|
||||
<div className={`mb-4 overflow-hidden rounded-[var(--radius-lg)] border ${
|
||||
isPending
|
||||
? 'border-[var(--color-warning)] bg-[var(--color-surface-container-lowest)]'
|
||||
: 'border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-low)] opacity-70'
|
||||
|
||||
@ -23,7 +23,7 @@ export function StreamingIndicator() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2 ml-10 flex w-fit items-center gap-2 rounded-full border border-[var(--color-border)]/40 bg-[var(--color-surface-container-low)] px-3 py-1">
|
||||
<div className="mb-2 flex w-fit items-center gap-2 rounded-full border border-[var(--color-border)]/40 bg-[var(--color-surface-container-low)] px-3 py-1">
|
||||
<span className="text-[var(--color-brand)] animate-shimmer text-xs">✦</span>
|
||||
<span className="text-xs font-medium text-[var(--color-text-secondary)]">{verb}...</span>
|
||||
{elapsedSeconds > 0 && (
|
||||
|
||||
@ -18,7 +18,7 @@ export function ThinkingBlock({ content, isActive = false }: { content: string;
|
||||
const preview = firstLine.length > 80 ? firstLine.slice(0, 80) + '...' : firstLine
|
||||
|
||||
return (
|
||||
<div className="mb-1 ml-10">
|
||||
<div className="mb-1">
|
||||
<style>{thinkingStyles}</style>
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
|
||||
@ -51,7 +51,7 @@ export function ToolCallBlock({ toolName, input, result, compact = false }: Prop
|
||||
|
||||
return (
|
||||
<div className={`overflow-hidden rounded-lg border border-[var(--color-border)]/50 bg-[var(--color-surface-container-lowest)] ${
|
||||
compact ? 'mb-0' : 'mb-2 ml-10'
|
||||
compact ? 'mb-0' : 'mb-2'
|
||||
}`}>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -128,7 +128,7 @@ function AgentToolGroup({
|
||||
}, [isStreaming])
|
||||
|
||||
return (
|
||||
<div className="mb-2 ml-10">
|
||||
<div className="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
@ -201,7 +201,7 @@ function ToolCallGroupMulti({ toolCalls, resultMap, childToolCallsByParent, isSt
|
||||
}, [hasNestedToolCalls, isStreaming])
|
||||
|
||||
return (
|
||||
<div className="mb-2 ml-10">
|
||||
<div className="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
|
||||
@ -27,7 +27,7 @@ export function ToolResultBlock({ content, isError, toolName, standalone = true
|
||||
const hasMore = text.length > 200
|
||||
|
||||
return (
|
||||
<div className={`mb-2 ml-10 overflow-hidden rounded-xl border ${
|
||||
<div className={`mb-2 overflow-hidden rounded-xl border ${
|
||||
isError
|
||||
? 'border-[var(--color-error)]/20'
|
||||
: 'border-[var(--color-outline-variant)]/20'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user