Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ app/desktop/stats.html
**/__pycache__/
*.pyc

# TypeScript build artifacts (tsc -b)
*.tsbuildinfo

*.local

# L3 真实测试证据(scripts/e2e/run-real-e2e-lane.sh 输出);README.md 保留
Expand Down
2 changes: 1 addition & 1 deletion app/mobile-rn/src/theme/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export const agentHubThemes = {
warningSoft: 'rgba(212, 170, 76, 0.08)', // --warning-bg
danger: '#e87070', // --danger(--td-danger)
dangerSoft: 'rgba(232, 112, 112, 0.06)', // --danger-bg
onAccent: '#f4f5ff', // --primary-foreground(--td-ink-on-plum)
onAccent: '#0b1b26', // --primary-foreground(--td-ink-on-plum)
onDanger: '#ffffff', // --destructive-foreground
scrim: 'rgba(0, 0, 0, 0.48)', // 登记:mobile-only 遮蔽(CSS 无 --td-scrim)
},
Expand Down
27 changes: 13 additions & 14 deletions app/shared/src/chatview/components/AgentGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { roleInitial } from '../design/roles'
import MarkdownContent from '../../ui/Markdown'
import { MessageDisplayMeta } from './MessageDisplayMeta'
import { isAgentItemStreaming } from './streaming'
import { Message, MessageAvatar, MessageContent } from '../../shadcn/ui/message'

const evidenceIconMap: Record<string, typeof IconFile> = {
file: IconFile,
Expand Down Expand Up @@ -224,31 +225,29 @@ export const AgentGroup = memo(function AgentGroup({ item, chatMode, onAgentClic

if (chatMode === 'dm') {
return (
<div className={enter ? 'grp-row grp-enter' : 'grp-row'}>
<div
className="dm-avatar"
<Message className={enter ? 'agent-row grp-enter' : 'agent-row'}>
<MessageAvatar
className="dm-avatar self-start"
onClick={onAgentClick ? (e) => onAgentClick(item.agent, e.currentTarget) : undefined}
style={onAgentClick ? { cursor: 'pointer' } : undefined}
>
{avatar}
</div>
<div className="grp-content">{orderedBodyContent ? orderedBody : body}</div>
<div className="dm-spacer" aria-hidden="true"><div className="ag-av">&nbsp;</div></div>
</div>
</MessageAvatar>
<MessageContent className="grp-content">{orderedBodyContent ? orderedBody : body}</MessageContent>
</Message>
)
}

return (
<div className={enter ? 'grp-row grp-enter' : 'grp-row'}>
<div
className="dm-avatar"
<Message className={enter ? 'agent-row grp-enter' : 'agent-row'}>
<MessageAvatar
className="dm-avatar self-start"
onClick={onAgentClick ? (e) => onAgentClick(item.agent, e.currentTarget) : undefined}
style={onAgentClick ? { cursor: 'pointer' } : undefined}
>
{avatar}
</div>
<div className="grp-content">{orderedBodyContent ? orderedBody : body}</div>
<div className="dm-spacer" aria-hidden="true"><div className="ag-av">&nbsp;</div></div>
</div>
</MessageAvatar>
<MessageContent className="grp-content">{orderedBodyContent ? orderedBody : body}</MessageContent>
</Message>
)
})
29 changes: 15 additions & 14 deletions app/shared/src/chatview/components/RowItem.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

.row-item {
display:flex; flex-direction:column;
background:var(--td-surface); border:1px solid var(--td-line); margin:0;
border-radius:var(--chat-r-md);
background:var(--td-surface);
border:1px solid color-mix(in oklch, var(--td-ink) 12%, transparent);
margin:0;
border-radius:12px;
box-shadow:0 1px 2px rgb(0 0 0 / 0.03);
overflow:hidden;
}
.row-item.selected { outline: 2px solid var(--td-plum); outline-offset: -2px }
Expand All @@ -30,24 +33,22 @@

.card-stack > .row-item {
width:100%;
border-radius:0;
border-radius:12px;
}
.card-stack > .row-item + .row-item { border-top:none; margin-top:-1px }
.card-stack > .row-item:first-child { border-radius:var(--chat-r-md) var(--chat-r-md) 0 0 }
.card-stack > .row-item:last-child { border-radius:0 0 var(--chat-r-md) var(--chat-r-md) }
.card-stack > .row-item:only-child { border-radius:var(--chat-r-md) }
.card-stack > .row-item + .row-item { margin-top:0 }
.card-stack > .row-item:only-child { border-radius:12px }
.card-stack > .row-item.route { margin:0 }

.row-hd {
display:flex; align-items:center; gap:var(--chat-sp-xxs);
padding:var(--chat-sp-xs) var(--chat-sp-10); cursor:pointer; user-select:none;
font:var(--chat-label); min-height:28px; line-height:1.3;
padding:8px 12px; cursor:pointer; user-select:none;
font:500 0.78125rem/1.35 var(--td-font); min-height:34px;
transition:background var(--chat-dur) ease;
border-radius:inherit;
border:none; background:none; color:inherit; text-align:inherit;
width:100%;
}
.row-hd:hover { background:var(--td-surface-3) }
.row-hd:hover { background:color-mix(in oklch, var(--td-ink) 4%, transparent) }
.row-hd:focus-visible { outline:none; box-shadow:var(--focus-ring) }

/* Artifact rows are preview triggers, not expandable empty file rows. */
Expand All @@ -65,17 +66,17 @@
.row-item.running > .row-hd:hover,
.row-item:not(.collapsible) > .row-hd:hover { background:transparent }

.row-icon { color:var(--td-ink-muted); flex-shrink:0; width:var(--icon-size); text-align:center; font-size:0.875rem }
.row-icon { color:var(--td-ink-muted); flex-shrink:0; width:15px; text-align:center; font-size:0.875rem }
.row-label { font-weight:600; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.row-extra { color:var(--td-ink-subtle); font:var(--chat-label-xs); flex-shrink:0 }
.row-chevron { color:var(--td-ink-subtle); transition:transform var(--chat-dur) ease; flex-shrink:0; font-size:0.6875rem }
.row-item.open > .row-hd .row-chevron { transform:rotate(180deg) }
.row-item:not(.collapsible) > .row-hd .row-chevron { display:none }

.row-bd {
margin-left:19px; padding:var(--chat-sp-2) var(--chat-sp-md) var(--chat-sp-sm) var(--chat-sp-16);
font:var(--chat-body); color:var(--td-ink-muted); line-height:1.55;
border-left:1px solid var(--td-line);
margin-left:20px; padding:6px 14px 12px;
font:400 0.78125rem/1.6 var(--td-font); color:var(--td-ink-muted);
border-left:1px solid color-mix(in oklch, var(--td-ink) 10%, transparent);
overflow:hidden;
animation:rowBdExpand var(--chat-dur) var(--td-ease);
}
Expand Down
93 changes: 56 additions & 37 deletions app/shared/src/chatview/components/Transcript.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@
TRANSCRIPT — unified grp-row (DM + Group share same flex layout)
══════════════════════════════════════════════════════════════════════ */

.transcript { display:flex; flex-direction:column; gap:0; min-width:0; flex:1; min-height:0; overflow-y:auto; scroll-behavior:smooth; scrollbar-gutter:stable; scrollbar-width:thin; padding:0 0 0 10px }
.transcript {
display:flex;
flex-direction:column;
gap:0;
min-width:0;
flex:1;
min-height:0;
overflow-y:auto;
scroll-behavior:smooth;
scrollbar-gutter:stable;
scrollbar-width:thin;
padding:24px max(18px, calc((100% - 780px) / 2)) 12px;
}

/* ── Scroll-to-bottom button ────────────────────────────────────────── */
.scroll-to-bottom-btn {
Expand Down Expand Up @@ -226,41 +238,40 @@
font-weight:400;
}

/* ── Row — symmetric: [avatar-l] [content flex:1] [avatar-r] ────────
Density: row rhythm uses chat density tokens (12px step), not base 20/24. */
.grp-row {
display:flex; gap:var(--chat-sp-sm); align-items:flex-start;
margin-bottom:var(--chat-sp-md);
/* ── Rows ─────────────────────────────────────────────────────────────
One reading column, generous vertical rhythm, avatar-led alignment. */
.agent-row,
.user-row-right {
display:flex;
gap:12px;
align-items:flex-start;
margin-bottom:26px;
}
.grp-content { flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--chat-sp-2) }
.user-row-right { justify-content:flex-end }
.grp-content { flex:1; min-width:0; display:flex; flex-direction:column; gap:8px }

.card-stack {
align-self:flex-start;
display:flex;
flex-direction:column;
gap:0;
width:min(86%, 820px);
gap:8px;
width:100%;
max-width:100%;
min-width:0;
}

/* first child aligns flush to avatar top */
.grp-content > :first-child { margin-top:0 }

/* Bubbles sit on a stable reading column; user replies still shrink right. */
.grp-content > .agent-bubble { align-self:flex-start; max-width:72% }
.grp-content > .user-bubble { align-self:flex-end; max-width:68% }

.bubble-group {
align-self:flex-start;
display:flex;
flex-direction:column;
gap:var(--chat-sp-xxs);
width:min(86%, 820px);
gap:6px;
width:100%;
max-width:100%;
min-width:0;
}
.bubble-group > .agent-bubble { width:100% }

/* ── Message display metadata from Hub/Edge contracts ─────────────── */
.message-display-meta {
Expand Down Expand Up @@ -343,17 +354,27 @@
}

/* ── Bubbles ─────────────────────────────────────────────────────────
Density: tighter vertical padding, keep horizontal reading comfort. */
User speech is a quiet brand bubble; assistant output is plain prose on
the canvas (Vercel-style), with cards carrying the structured evidence. */
.user-bubble {
background:var(--td-plum); color:var(--td-ink-on-plum);
border-radius:var(--chat-r-lg); padding:var(--chat-sp-xxs) var(--chat-sp-sm);
font:var(--chat-bubble-font); line-height:1.55; word-break:break-word;
max-width:min(100%, 620px);
background:var(--td-plum);
color:var(--td-ink-on-plum);
border:0;
border-radius:18px 18px 6px 18px;
padding:10px 14px;
font:400 0.9375rem/1.6 var(--td-font);
word-break:break-word;
box-shadow:0 1px 1px rgb(0 0 0 / 0.04);
}

.agent-bubble {
background:var(--td-surface); border:1px solid var(--td-line);
border-radius:var(--chat-r-md); padding:var(--chat-sp-xxs) var(--chat-sp-sm);
font:var(--chat-body); color:var(--td-ink); line-height:1.5;
background:transparent;
border:0;
border-radius:0;
padding:0;
font:400 0.9375rem/1.68 var(--td-font);
color:var(--td-ink);
max-width:100%;
min-width:0;
}
Expand Down Expand Up @@ -402,39 +423,36 @@

/* ── Avatars ───────────────────────────────────────────────────────── */
.ag-av {
width:var(--avatar-size); height:var(--avatar-size); border-radius:50%;
width:26px; height:26px; border-radius:50%;
display:flex; align-items:center; justify-content:center;
font:600 0.75rem var(--td-font); color:var(--td-ink-on-plum); flex-shrink:0;
font:600 0.6875rem var(--td-font); color:var(--td-ink-on-plum); flex-shrink:0;
box-shadow:inset 0 0 0 1px rgb(255 255 255 / 0.12);
}
.ag-av.builder{background:var(--role-builder)} .ag-av.reviewer{background:var(--role-reviewer)}
.ag-av.deployer{background:var(--role-deployer)} .ag-av.researcher{background:var(--role-researcher)}
.ag-av.orch{background:var(--role-orch)} .ag-av.shield{background:var(--td-warning)}
.ag-av.agent{background:var(--info)} .ag-av.system{background:var(--td-ink-subtle)}
.ag-av.user-av{background:var(--td-plum)}

.ag-name { font:var(--chat-label); color:var(--td-ink-muted) }
.ag-time { font:var(--chat-label-xs); font-family:var(--td-mono); font-weight:400; color:var(--td-ink-subtle) }
.ag-name { font:600 0.8125rem/1.3 var(--td-font); color:var(--td-ink) }
.ag-time { font:400 0.6875rem/1.3 var(--td-mono); color:var(--td-ink-subtle) }

.user-meta { display:flex; align-items:center; gap:var(--chat-sp-sm); margin-bottom:var(--chat-sp-2) }
.user-meta { display:flex; align-items:center; gap:8px }

.user-row-right { justify-content:flex-end }
.user-content-right { align-items:flex-end }

.agent-meta { display:flex; align-items:center; gap:var(--chat-sp-sm) }
.agent-meta { display:flex; align-items:baseline; gap:8px }

.dm-avatar { flex-shrink:0 }

/* ── Spacer placeholder — preserves symmetric grp-row layout ──────── */
.dm-spacer { flex-shrink:0; visibility:hidden }
.dm-spacer .ag-av { visibility:hidden }

.user-avatar { width:26px; height:26px; min-width:26px; border-radius:9999px; background:transparent }

/* ── #1825: new-message entry + streaming activity ────────────────────
grp-enter is keyed to the newest same-session append (Transcript sets the
class once, clears it after the animation window) so virtualized remounts
while scrolling history never replay it. transform/opacity only — layout
height is untouched, keeping virtua measurements stable. */
.grp-row.grp-enter { animation:grpEnter var(--chat-dur-slow) var(--td-ease) both }
.agent-row.grp-enter,
.user-row-right.grp-enter { animation:grpEnter var(--chat-dur-slow) var(--td-ease) both }

@keyframes grpEnter {
from { opacity:0; transform:translateY(6px) }
Expand All @@ -456,6 +474,7 @@
@media (prefers-reduced-motion: reduce) {
.transcript { scroll-behavior:auto }

.grp-row.grp-enter,
.agent-row.grp-enter,
.user-row-right.grp-enter,
.bubble-group.streaming .agent-bubble::after { animation:none }
}
28 changes: 3 additions & 25 deletions app/shared/src/chatview/components/Transcript.css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,6 @@ describe('ChatViewTranscript CSS contract', () => {
}
});

it('keeps agent markdown replies on a stable readable column', () => {
const groupRule = cssRule(css, '.bubble-group');
const nestedBubbleRule = cssRule(css, '.bubble-group > .agent-bubble');

expect(groupRule).not.toMatch(/\bwidth\s*:\s*fit-content\b/);
expect(groupRule).toMatch(/\bwidth\s*:\s*min\(86%,\s*820px\)/);
expect(nestedBubbleRule).toMatch(/\bwidth\s*:\s*100%/);
});

it('keeps the ChatView root constrained so the transcript owns vertical scrolling', () => {
const chatviewRule = cssRule(css, '.chatview');

Expand All @@ -59,26 +50,13 @@ describe('ChatViewTranscript CSS contract', () => {
});

it('keeps Hub message metadata styled as compact non-card chrome', () => {
const metaRule = cssRule(css, '.message-display-meta');
const detailRule = cssRule(css, '.message-display-detail');

for (const className of messageDisplayMetaClassNames) {
expect(css, className).toMatch(new RegExp(`\\.${className}\\b`));
}
expect(metaRule).toMatch(/\bfont\s*:\s*var\(--chat-label-xs\)/);
expect(metaRule).not.toMatch(/\bbackground\s*:/);
expect(detailRule).toMatch(/\btext-overflow\s*:\s*ellipsis/);
});

it('uses dense row rhythm tokens for transcript messages (#1311)', () => {
const rowRule = cssRule(css, '.grp-row');
const contentRule = cssRule(css, '.grp-content');

expect(rowRule).toMatch(/margin-bottom\s*:\s*var\(--chat-sp-md\)/);
expect(rowRule).toMatch(/gap\s*:\s*var\(--chat-sp-sm\)/);
expect(contentRule).toMatch(/gap\s*:\s*var\(--chat-sp-2\)/);
/* Full-file match: cssRule hits first `.user-bubble` under `.grp-content >` (align only). */
expect(css).toMatch(/\.user-bubble\s*\{[^}]*padding\s*:\s*var\(--chat-sp-xxs\)\s+var\(--chat-sp-sm\)/);
expect(css).toMatch(/\.agent-bubble\s*\{[^}]*padding\s*:\s*var\(--chat-sp-xxs\)\s+var\(--chat-sp-sm\)/);
it('keeps both message roles on the shared conversation column', () => {
expect(css).toMatch(/\.agent-row\s*,\s*\.user-row-right\s*\{/);
expect(css).toMatch(/\.user-row-right\s*\{[^}]*justify-content\s*:\s*flex-end/);
});
});
Loading
Loading