Skip to content
Closed
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
50 changes: 45 additions & 5 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -933,18 +933,58 @@ pre, code {
margin: 6px 0;
}

/* Unified collapse bars for every collapsible chat section (thinking,
process details, compaction summary). The top bar toggles expansion;
the bottom bar is only visible while expanded and collapses. */
.collapse-bar {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-height: 24px;
padding: 2px 0;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font: inherit;
font-size: 12px;
text-align: left;
}
.collapse-bar:hover {
color: var(--text);
}
.collapse-bar-static {
cursor: default;
}
.collapse-bar-static:hover {
color: var(--text-muted);
}
.collapse-bar-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.collapse-bar-meta {
flex-shrink: 0;
color: var(--text-dim);
display: inline-flex;
align-items: center;
gap: 6px;
}
.collapse-bar-bottom {
justify-content: center;
cursor: pointer;
}
.compaction-file-details {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 12px;
}
.compaction-file-details summary {
cursor: pointer;
font-weight: 600;
line-height: 1.4;
}
.compaction-file-section {
margin-top: 8px;
}
Expand Down
105 changes: 105 additions & 0 deletions components/CollapseBars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { CSSProperties, ReactNode } from "react";

/**
* Shared collapse-bar primitives. Every collapsible section in the chat
* (thinking blocks, process details, compaction summaries) uses the same
* pattern: a top bar that toggles expansion and, while expanded, a bottom
* bar that collapses. The top bar's chevron sits on the right (like the
* tool-call headers) and rotates with the state; the bottom bar's chevron
* points up, signalling that it collapses the section upward.
*/

export function CollapseChevron({ expanded, size = 12 }: { expanded: boolean; size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0, transform: expanded ? "rotate(90deg)" : "none", transition: "transform 0.15s" }}
>
<polyline points="4 2.5 7.5 6 4 9.5" />
</svg>
);
}

export function CollapseUpChevron({ size = 12 }: { size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0 }}
>
<polyline points="3 8 6 4.5 9 8" />
</svg>
);
}

export function CollapseTopBar({ expanded, onToggle, label, meta, ariaLabel, title, style }: {
expanded: boolean;
/** Omit for sections with nothing to expand — rendered as a static header. */
onToggle?: () => void;
label: ReactNode;
meta?: ReactNode;
ariaLabel?: string;
title?: string;
style?: CSSProperties;
}) {
const inner = (
<>
<span className="collapse-bar-label">{label}</span>
{meta != null && <span className="collapse-bar-meta">{meta}</span>}
<CollapseChevron expanded={expanded} />
</>
);
if (!onToggle) {
return (
<div className="collapse-bar collapse-bar-static" style={style}>
{inner}
</div>
);
}
return (
<button
type="button"
className="collapse-bar"
aria-expanded={expanded}
aria-label={ariaLabel}
title={title}
onClick={onToggle}
style={style}
>
{inner}
</button>
);
}

export function CollapseBottomBar({ onCollapse, label, title, style }: {
onCollapse: () => void;
label?: ReactNode;
title?: string;
style?: CSSProperties;
}) {
return (
<button
type="button"
className="collapse-bar collapse-bar-bottom"
onClick={onCollapse}
title={title}
style={style}
>
<CollapseUpChevron />
{label != null && <span className="collapse-bar-label">{label}</span>}
</button>
);
}
2 changes: 1 addition & 1 deletion components/MessageView.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ test("previews the first thinking line and reveals the full text with the saved
assert.match(html, new RegExp(`aria-expanded="${expanded}"`));
assert.equal((html.match(/>[^<]*Independent reasoning[^<]*</g) ?? []).length, 1);
assert.equal(html.includes("Detailed second line."), expanded);
assert.match(html, /aria-label="Thinking: /);
assert.match(html, /aria-label="Thinking"/);
assert.match(html, /3s/);
}
} finally {
Expand Down
Loading