Files
CST-agent/archive/desktop-tauri/src/dsh/conversation/ConversationRoot.module.css
T
WHCandCursor 5538ed3412
ci / tcc-check (push) Successful in 1m8s
ci / linux (aarch64, 0) (push) Failing after 1h0m3s
ci / linux (x86, 1) (push) Failing after 1h0m3s
ci / linux (x86_64, 1) (push) Failing after 1h0m2s
ci / windows (push) Failing after 50s
refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus
libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 22:49:23 +08:00

637 lines
20 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
.root {
position: relative; /* positioning context for slot-owned absolute chrome */
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
background: var(--dsw-alias-bg-base);
}
/* 76px with its rule: the height of the Sidebar's tab strip and header row
(ui-dockkit `.tabStrip` 38px + the pane header's 38px), so the two rules
meet at the column edge. The rows add up to it — 10px top inset, the 30px
title row, the tab strip's 10px margin, and a 25px view row. When the views
and the More launcher do not fit on one row, the row wraps and the header
grows past 76px. */
.header {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
flex: none;
box-sizing: border-box;
min-height: 76px;
padding: 10px 28px 0 20px;
border-bottom: 0.5px solid var(--dsw-alias-border-l3);
}
.header:where(:not(:has(.tabs))) {
min-height: 0;
padding-bottom: 10px;
}
/* Blank Sessions retain navigation controls without conversation title or tabs. */
.headerBlank {
min-height: 0;
padding-bottom: 0;
border-bottom: none;
}
.headerBlank .headerCorner {
margin-left: auto;
}
/* Only the macOS titlebar needs reserved drag space before a Session exists. */
:global(html:not([data-platform='darwin'])) .headerSessionless {
padding-top: 0;
}
:global(html:not([data-platform='darwin'])) .headerSessionless .titleRow {
min-height: 0;
}
/* macOS hiddenInset titlebar: the header block owns the window drag over its
own box — title row plus the view tab strip, so the draggable geometry is the
header's geometry and no frame band has to match its height. The header marks
itself `data-window-drag` (ConversationHeader); below it the title-row clusters
subtract their whole boxes, and individual controls are covered by the base.css
interactive-element rule. A covering overlay lives beside #root
(ui-settings-general), which puts it after this row in document order. */
:global([data-platform='darwin']) .headerLeading,
:global([data-platform='darwin']) .headerActions,
:global([data-platform='darwin']) .headerUtilities,
:global([data-platform='darwin']) .headerCorner {
-webkit-app-region: no-drag;
}
.titleRow {
grid-column: 2;
/* Anonymous inline-size container (CSS modules hash container-names per
module, so occupants match the nearest container): header occupants such
as the preset label and slot actions shed their text at row-width
breakpoints. The minmax(0, 1fr) column sizes the row, so inline-size
containment cannot collapse it. */
container-type: inline-size;
display: flex;
align-items: center;
gap: 0;
min-height: 30px;
/* Clears the frame's window-chrome band (traffic lights + shell.leading
controls, ui-layout AppFrame) when it is published; the header already
pads 20px of it. Zero everywhere else (the fallback makes the max
negative). */
padding-inline-start: max(0px, calc(var(--dsh-frame-leading-clearance, 0px) - 20px));
}
/* Header actions do not shrink, so on a narrow row they overflow the cluster.
The cluster clips them, hit testing included, so the next control keeps
every click. The trailing padding spans the gap before that control and
holds the fade: content that fits ends before it, and only overflowing
content reaches it. The 4px block and start padding, offset by negative
margins, keeps focus rings inside the clip. `clip` rather than `hidden`
keeps the cluster from becoming a scroll container that focus could scroll. */
.titleCluster {
display: flex;
flex: 1;
align-items: center;
gap: 10px;
min-width: 0;
--title-cluster-fade: 20px;
margin: -4px calc(-1 * var(--title-cluster-fade)) -4px -4px;
padding: 4px var(--title-cluster-fade) 4px 4px;
overflow: clip;
mask-image: linear-gradient(to left, transparent calc(0.3 * var(--title-cluster-fade)), #000 var(--title-cluster-fade));
}
/* Without utilities the corner's 8px margin is the gap the fade spans. */
.titleCluster:has(+ .headerUtilities:empty) {
--title-cluster-fade: 8px;
}
/* The auto grid column collapses with an empty or CSS-hidden occupant.
The occupant owns its trailing gap so hidden controls reserve no space. */
.headerLeading {
grid-column: 1;
grid-row: 1;
display: flex;
flex: none;
align-items: center;
gap: 8px;
}
.crumbs {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
overflow: hidden;
white-space: nowrap;
}
.crumbSeg {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.crumbSep {
/* figma: "/" separators are 14px caption gray (75:7903), one tint lighter than crumb text. */
color: var(--dsw-alias-label-caption);
font-size: 14px;
line-height: 20px;
}
.crumb {
display: inline-block;
max-width: 220px;
overflow: hidden;
padding: 4px 8px;
border: none;
border-radius: var(--dsw-radius-md);
background: transparent;
font-size: 14px;
line-height: 20px;
color: var(--dsw-alias-label-tertiary);
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.crumbSubagent {
font-size: 12px;
line-height: 18px;
}
@media (hover: hover) {
button.crumb:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
}
.crumbCurrent {
font-weight: 500;
color: var(--dsw-alias-label-primary);
cursor: default;
}
.headerActions {
display: flex;
flex: none;
align-items: center;
gap: 8px;
}
.headerUtilities {
display: flex;
flex: none;
align-items: center;
gap: 8px;
margin-left: 20px;
}
.headerUtilities:empty {
display: none;
}
/* The far-right corner seat reaches 16px into the header's 28px right padding,
so its control sits past the utilities' edge; it is laid out only while its
occupant renders something. Its left margin equals the utilities' 8px gap so
the header's trailing controls space evenly. */
.headerCorner {
display: flex;
flex: none;
align-items: center;
margin-left: 8px;
margin-right: -16px;
}
.headerCorner:empty {
display: none;
}
/* The tab row sits above the header paint so the active bar covers its rule. */
.tabs {
grid-column: 1 / -1;
position: relative;
z-index: 1;
display: flex;
gap: 16px 20px;
width: 100%;
min-width: 0;
box-sizing: border-box;
align-items: flex-start;
flex-wrap: wrap;
margin-top: 10px;
padding-left: 8px;
}
@media (max-width: 767px) {
:global(html:not([data-platform])) .tabs {
padding-left: 0;
}
}
.viewTabs {
display: flex;
flex-wrap: wrap;
gap: 32px;
min-width: 0;
min-height: 25px;
}
/* The 16px text line retains 9px below it. The press target extends 10px
above the text into the strip's top margin and 10px to each side, matching
the inline padding of the Sidebar tab chips (ui-dockkit `.tab`); negative
margins keep the layout box at its 25px row and text width. The active bar
spans the text and reaches 1px past the tab box to sit over the header
rule. */
.tab {
--dsh-tab-hit-inline: 10px;
position: relative;
box-sizing: border-box;
min-height: 35px;
margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0;
padding: 10px var(--dsh-tab-hit-inline) 9px;
border: none;
background: transparent;
font-size: 13px;
line-height: 16px;
font-weight: 500;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.tab::after {
content: '';
position: absolute;
right: var(--dsh-tab-hit-inline);
bottom: -1px;
left: var(--dsh-tab-hit-inline);
height: 2px;
border-radius: 2px;
background: transparent;
}
/* Selected tab is blue, not ink: brand-primary resolves to neutral black in
this token sheet, so the selected state rides the business blue — the
nearest semantic token that stays blue in both themes. */
.tabActive {
color: var(--dsw-alias-state-business-primary);
}
.tabActive::after {
background: var(--dsw-alias-state-business-primary);
}
/* The More launcher centers its 22px box on the tab's 16px text line. */
.viewsMoreAnchor {
flex: none;
}
.viewsMore {
width: 22px;
height: 22px;
margin-top: -3px;
padding: 0;
color: var(--dsw-alias-label-secondary);
}
.viewsMoreActive { color: var(--dsw-alias-state-business-primary); }
.tabLabel { display: block; max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viewsMore[aria-expanded='true'] {
background: var(--dsw-alias-interactive-bg-hover);
}
.viewsMoreMenu {
width: 288px;
max-width: calc(100vw - 24px);
}
.viewArea {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
/* Width handles: 10px col-resize strips beside the transcript, absolute in
.body (NOT the scrollport: an absolute strip there would extend the
scrollable range; NOT .root: a strip from the column top would paint its
glow through the header's transparent background). The inner edge sits
24px outside the content column and the strip extends 10px outward; the
outer edge is clamped to keep a 24px safe zone against the
column edges (sidebar side and scrollbar side stay drag-free). When the
margin cannot fit inset + strip + safe zone the computed width goes
negative and the strip resolves to zero — no mispositioned hit area on
narrow columns. Known half-scrollbar (~4px) centering offset vs the
gutter-reserved content box is accepted: it consumes part of the 10px
strip but remains within it. */
.widthHandle {
position: absolute;
top: 0;
bottom: 0;
/* Concrete overflow content establishes z-index 1, while the sticky
composer remains above both layers. */
z-index: 0;
width: min(
10px,
calc((100% - var(--dsh-chat-content-width)) / 2 - 24px - 24px)
);
cursor: col-resize;
}
.widthHandle[data-side='left'] {
right: calc(50% + var(--dsh-chat-content-width) / 2 + 24px);
}
.widthHandle[data-side='right'] {
left: calc(50% + var(--dsh-chat-content-width) / 2 + 24px);
}
/* The glow: a short 2px bar following the captured pointer's Y, centered in the strip
(29px from the content column), not a full-height rule — a 16px solid core
centered on the pointer
(published during drag as --dsh-width-handle-pointer-y), fading to
transparent over 28px each side (72px total). A solid CORE, not a single
solid stop: a lone peak washes out over the fade. The resting scrollbar
tint keeps this secondary affordance quiet in both themes. Before the first
drag move the fallback centers the glow at 50% height; hover leaves its position unchanged. */
.widthHandle::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 2px;
border-radius: 2px;
background: linear-gradient(
to bottom,
transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),
var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),
var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),
transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)
);
opacity: 0;
pointer-events: none;
}
.widthHandle[data-side='left']::after {
right: 4px;
}
.widthHandle[data-side='right']::after {
left: 4px;
}
.widthHandle[data-dragging]::after {
opacity: 1;
}
@media (hover: hover) {
.widthHandle:hover::after {
opacity: 1;
}
}
/* A drag starts above the composer band. Pointer capture keeps it active when
crossing the footer; lift only the active handle so its indicator remains
visible without making the resting footer gutter draggable. */
.widthHandle[data-dragging] {
z-index: 8;
}
/* Views that elect a composer overlay (trajectory etc.) own their full-bleed
scrollers; the strips would sit over their content and steal clicks. */
.root:has([data-conversation-composer-overlay]) .widthHandle {
display: none;
}
/* Composer context stack (Figma 9:937): standalone dock cards share one
rhythm above the input card. */
.composerStack {
/* Horizontal geometry (card width, clearance, dock inset) rides the shared
.root variables above so takeover siblings match the stack. */
--dsh-composer-stack-gap: 6px;
display: flex;
flex-direction: column;
gap: var(--dsh-composer-stack-gap);
}
/* Common seat for the composer chain (fallback + elected overlay siblings). */
.composerSeat {
display: flex;
flex: none;
flex-direction: column;
/* One cap for every scrolling text region a composer seat can hold: the
InputBar draft (figma Input 75:8208 max 14 lines × 24px line) and the
takeover panels' bodies top out at the same height, so electing a
takeover never grows the footer past the card it replaces. Declared on
the seat because it is the chain's only shared ancestor — fallback and
elected overlay are siblings — and custom properties inherit down to
whichever entry is mounted. */
--dsh-composer-text-max-height: 336px;
}
/* Active phase: header is ordinary column chrome above the scrollport (not
sticky). The scroll body holds the transcript and the sticky composer seat
so wheel over the footer moves the flow. */
.root[data-phase='active'] {
overflow: hidden;
}
.root[data-phase='active'] .header {
flex: none;
}
/* Band below the header: the scrollport plus the width handles. Anchoring the
handles here keeps their full-height strips and glow under the header's
bottom edge at every header height. */
.body {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
/* Shared width axis for the transcript, dock cards, composer, and takeover
cards. Main width controls publish the live body width and optional user
preference; embedded occurrences override this adaptive axis below. */
--dsh-chat-content-width: var(
--dsh-chat-user-width,
clamp(680px, calc(var(--dsh-conversation-column-width, 0px) * 0.64), 920px)
);
--dsh-composer-card-max-width: calc(var(--dsh-chat-content-width) + 32px);
--dsh-composer-side-clearance: 16px;
--dsh-composer-dock-inset: 8px;
}
.embeddedBody {
overflow: hidden;
--dsh-chat-content-width: min(calc(100% - 32px), 920px);
--dsh-composer-card-max-width: min(calc(100% - 16px), 952px);
--dsh-composer-side-clearance: 8px;
--dsh-composer-dock-inset: 8px;
}
.scrollBody {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
/* The product-wide 2px scrollbar offset (ui-workspace WorkspaceBrowser
`.list`, ui-sidebar-files FilesBody `.body`): the bar sits 2px clear of
the column's edge instead of flush against it. */
margin-right: 2px;
overflow-y: auto;
/* Reserved unconditionally: the composer seat rides this box's content box in
Chat and its padding box under a view's composer overlay, so an `auto`
gutter moves the input card sideways by the bar's width whenever the two
differ ([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */
scrollbar-gutter: stable;
}
/* The bar also stops 2px short of the scroller's ends, matching its 2px edge
offset. WebKit-only: the Firefox path has no track to inset. */
.scrollBody::-webkit-scrollbar-track {
margin: 2px;
}
.root[data-phase='active'] .viewArea,
.embeddedBody[data-content-phase='active'] .viewArea {
flex: 1 0 auto;
min-height: auto;
}
.root[data-phase='active'] .composerSeat,
.embeddedBody[data-content-phase='active'] .composerSeat {
position: sticky;
bottom: 0;
/* Above markdown CodeBlock sticky banners (z-index 6) so the footer never
paints under a sticking code header while scrolling. The pinned compaction
header (MessageItem.module.css, ui-chat) and the turn-navigation rail slot
(TurnNavigator.module.css, ui-chat) also take 7 to clear the same banners.
Among elements of equal rank the later one in DOM order wins, so the
composer stays above the pinned header on the extreme small-viewport
overlap and the pinned header stays above the rail. */
z-index: 7;
/* Input mask (figma 1205:27463): transcript fades out under a FIXED 36px
band at the seat's top (the figma 24% of the resting ~150px composer),
solid below — px stops, not %, so a growing draft only widens the solid
region and the fade band never stretches. The 0px stop is bg-base at
zero alpha (not white, which the figma export hardcodes) so both themes
fade from their own base. */
background: linear-gradient(
180deg,
color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent) 0px,
var(--dsw-alias-bg-base) 36px
);
}
/* An open @/slash menu (data-trigger-menu, ui-input-trigger) renders inside
the input card, so the seat's stacking context caps its z-index; lift the
seat above the back-to-bottom control (z-index 8, ui-chat ChatView) while
the menu is open. The steady state stays 7 so the control keeps painting
over the card. */
.root[data-phase='active'] .composerSeat:has([data-trigger-menu]) {
z-index: 9;
}
/* Views may opt into a composer overlay while ConversationRoot retains
ownership of the seat geometry and its active-phase precedence. */
.scrollBody:has([data-conversation-composer-overlay]) {
position: relative;
/* A clipping box nothing scrolls out of, stated as a scroll container on both
axes rather than `overflow: hidden`: a single-axis scroller computes the
other axis to `auto`, and the seat's absolute positioning below needs the
box to stay a containing block in every engine. */
overflow-x: hidden;
overflow-y: auto;
/* The view owns its own scrollers (the trajectory ledger scrolls inside the
view), so this box never scrolls here; reserving a gutter would only
narrow the view's content by the bar's width. Chat keeps the reservation
on the same element, so the seat below compensates with the bar's width
([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */
scrollbar-gutter: auto;
}
.scrollBody:has([data-conversation-composer-overlay]) > :global([data-slot='conversation.session']) > .viewArea {
flex: 1 1 0;
min-height: 0;
overflow: hidden;
}
.scrollBody:has([data-conversation-composer-overlay]) > .composerSeat {
position: absolute;
/* Chat's seat rides this box's content box, which its own scrolling shortens
by the bar's width; the overlay seat is against the padding box, so it
must give back the same width to keep the input card in place across
tabs. Reads ui-theme's --dsh-scrollbar-width, which mirrors the WebKit
bar's layout width. Chat reserves the engine-resolved width, so on an
engine where the resolved width differs from this fixed 8px the card
drifts by half the difference — accepted residual risk, recorded in
([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md)). */
right: var(--dsh-scrollbar-width);
bottom: 0;
left: 0;
}
/* Hero phase: the composer stack (hero chrome + workspace row + card) is
flex-centered in the column; composer phase docks it at the bottom. Flex,
NOT absolute+transform: a transform would make this box the containing
block for position:fixed descendants (pickers/modals), shrinking them. */
.composerHero {
align-self: center;
/* figma 75:8208 drew 12 between all three rows; the workspace row now sits
8 above the card (its margin-top restores 12 under the hero chrome). */
gap: 8px;
/* Foot inside the centered box floats the stack a bit above true center. */
padding-bottom: 32px;
/* Card cap + both clearances: the hero input card lands at exactly the same
width as the docked composer at every viewport. */
width: min(calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance)), 100%);
z-index: 1;
}
.heroWorkspaceRow {
display: flex;
align-items: center;
gap: 2px;
min-width: 0;
margin-top: 4px;
/* figma drew px 8; left nudged +12 so the chip's folder glyph lines up
closer to the card's inner controls below; right keeps the trailing
preset seat off the card edge. */
padding: 0 16px 0 20px;
}
/* Plugin options share the row, pushed to its right edge. */
.heroOptions {
display: flex;
flex: none;
align-items: center;
gap: 2px;
min-width: 0;
margin-left: auto;
}
.heroOptions:empty {
display: none;
}
/* Hero: the composer sits inside the session scroll body; center there so
the tree seat matches active (sticky footer) without a Root remount. */
.root[data-phase='hero'] .scrollBody,
.embeddedBody[data-content-phase='hero'] .scrollBody {
justify-content: center;
overflow-y: auto;
}
/* Settling (session replaying, hero/docked unknown): keep the composer seat
mounted but invisible so no wrong layout flashes before the phase lands. */
.root[data-phase='settling'] .composerSeat,
.embeddedBody[data-content-phase='settling'] .composerSeat {
visibility: hidden;
}