Files
CST-agent/archive/desktop-tauri/src/dsh/chat/ChatView.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

410 lines
14 KiB
CSS

.frame {
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
flex: 1 1 auto;
/* Floating controls query this frame outside the clipped transcript. */
container-type: inline-size;
}
.root {
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
flex: 1 1 auto;
overflow-x: visible;
overflow-y: clip;
}
.scroll {
flex: 1 1 auto;
min-height: 0;
/* Never expose horizontal scrollable overflow: the absolutely positioned
text-interaction surface adds it transiently, and the changing scroll range
makes the transcript jitter while the tail is pinned. */
overflow-x: clip;
overflow-y: auto;
/* Sides = composer clearance + 16px: on narrow viewports the transcript
stays exactly 32px narrower than the input card (the shared width rule). */
padding: 16px calc(var(--dsh-composer-side-clearance) + 16px);
/* Inline-size query container: wide markdown tables size their breakout
against this box's content width (100cqw in AssistantMarkdown's
md-table-wide rule) — the CSS stand-in for deepsuite chat's JS-measured
--dsl-virtual-list-width. */
container-type: inline-size;
}
.root[data-chat-following-tail] .scroll,
:global([data-conversation-scroll]):has(.root[data-chat-following-tail]) {
overflow-anchor: none;
}
:global([data-conversation-scroll]) .frame,
:global([data-conversation-scroll]) .root {
flex: 0 0 auto;
min-height: auto;
height: auto;
}
:global([data-conversation-scroll]) .scroll {
/* A host scrollport still owns vertical overflow; horizontal overflow is
clipped for the same reason as the self-scrolled case above. */
overflow-x: clip;
overflow-y: visible;
flex: 0 0 auto;
min-height: auto;
}
/* Message column: shared chat width (ConversationRoot --dsh-chat-content-width),
centered on the same axis as the input box (which caps at chat + 16px); the
scroller itself stays full-bleed. */
.column {
position: relative;
max-width: var(--dsh-chat-content-width);
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
}
/* The flow slot's display:contents anchor preserves layout but not selector ancestry.
`hidden="until-found"` retains a searchable zero-height box; hidden and empty
Seats do not contribute spacing. The older-history control precedes the anchor;
an ancestor :has() here also invalidates the history on unrelated descendant edits. */
:is(.column, :global([data-slot="conversation.chat.flow"])) > :not([hidden]):not(.textInteraction):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))
~ :not([hidden]):not(.textInteraction):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))),
.column > .older + :global([data-slot="conversation.chat.flow"])
> :not([hidden]):not(.textInteraction):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))) {
margin-top: var(--dsh-chat-flow-gap, 4px);
}
/* Process rows sit 4px apart; an Assistant response is separated from its
neighbours by 12px. */
.flowItem[data-chat-group-part="response"] {
--dsh-chat-flow-gap: 12px;
}
:is(.column, :global([data-slot="conversation.chat.flow"])) > [data-chat-group-part="response"]:not([hidden]):not(:empty)
+ :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))):not(.running):not(:has(> [data-slot="conversation.chat.node"] > [data-chat-steering-batch])) {
--dsh-chat-flow-gap: 12px;
}
/* Settled-flow identity boundary. It is neutral until it becomes the natural
measurement/mount unit for a virtualizer without changing the column gap. */
.flowItem {
min-width: 0;
}
/* Bottom room: sized by the viewport owner while rows above the reader close,
given back only as later content grows into it. */
.turnSpacer {
height: 0;
}
/* While a Turn folds, spacing changes share the closing rows' timing. */
.column[data-chat-motion] > :not([data-chat-motion]),
.column[data-chat-motion] > :global([data-slot="conversation.chat.flow"]) > :not([data-chat-motion]) {
transition: margin-top 160ms cubic-bezier(0.4, 0, 0.2, 1);
}
.column[data-input-fold] > :not([data-chat-motion]),
.column[data-input-fold] > :global([data-slot="conversation.chat.flow"]) > :not([data-chat-motion]) {
transition: none;
}
/* A Turn's duration control renders empty until the Turn closes; its gap appears with it in the same
frame, so the layout below changes once instead of sliding after the control's own height. */
.column[data-chat-motion] > [data-chat-flow-kind="turn-process"],
.column[data-chat-motion] > :global([data-slot="conversation.chat.flow"]) > [data-chat-flow-kind="turn-process"] {
transition: none;
}
@media (prefers-reduced-motion: reduce) {
.column[data-chat-motion] > :not([data-chat-motion]),
.column[data-chat-motion] > :global([data-slot="conversation.chat.flow"]) > :not([data-chat-motion]) {
transition: none;
}
}
/* The Turn header has 16px clearance from its input and following content. A
running Turn's control renders empty until the Turn closes; an echo after it
is still an opening input of that Turn, so it keeps the input gap. */
.flowItem[data-chat-flow-kind="turn-process"],
:is(.column, :global([data-slot="conversation.chat.flow"])) > [data-chat-flow-kind="turn-process"]:not(:empty, :has(> [data-slot="conversation.chat.node"]:empty)) + :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))):not(.running):not(:has(> [data-slot="conversation.chat.node"] > [data-chat-steering-batch])),
:is(.column, :global([data-slot="conversation.chat.flow"])) > [data-chat-flow-kind="turn-process"] + :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty))):not(.running):not([data-submission-echo], [data-pending-steering]):not(:has(> [data-slot="conversation.chat.node"] > [data-chat-steering-batch])) {
--dsh-chat-flow-gap: 16px;
}
/* Tail slot: the running status and a completed Turn's actions row both take
a 16px offset plus a 28px row. The steering renderer marks the first
steering row of each claim that Chat orders after the Turn's process
control; that row keeps the slot above it, the way a completed Turn's
actions row precedes the next user message, while rows claimed together
stay 4px apart and opening inputs keep ordinary spacing. The first waiting
echo sits directly below the running status, so admission swaps the two
without moving any bubble. Neighbour gap rules exclude the batch start so
this slot always applies. */
:is(.column, :global([data-slot="conversation.chat.flow"])) > .flowItem:has(> [data-slot="conversation.chat.node"] > [data-chat-steering-batch="start"]) {
--dsh-chat-flow-gap: calc(44px + var(--dsh-content-font-delta, 0px));
}
.running + [data-pending-steering] {
--dsh-chat-flow-gap: 0px;
}
/* A completed Turn's actions row owns the tail slot; the status can trail it
for one frame before the Session stops running, or between queued Turns. */
.flowItem[data-chat-flow-kind="turn-tail"] + .running {
display: none;
}
/* Steering rows keep their full layout from insertion. Only the status moves:
it enters its new slot below them from one slot higher, independent of how
many admitted rows it passes or how tall they are. The start therefore sits
below the status's previous position by the admitted rows' height; the
fade-in covers that difference, so this entrance approximates the move. */
@media (prefers-reduced-motion: no-preference) {
:where(.column > :global([data-slot="conversation.chat.flow"]) > .flowItem[data-chat-flow-kind="steering"]) + .running {
animation: running-enters-slot 140ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* An input fold runs the entrance at zero duration. The switch only changes
the duration of an entrance its structural change already created, as if
the entrance had it from its start: lifting the switch never replays an
entrance older than its duration, and setting it completes a running one.
The structural rule is wrapped in :where() so this rule outranks it. */
.column[data-input-fold] .running {
animation-duration: 0s;
}
}
@keyframes running-enters-slot {
from {
opacity: 0;
translate: 0 calc(-44px - var(--dsh-content-font-delta, 0px));
}
40% {
opacity: 1;
}
}
/* Empty Seats, including retained empty slot anchors, preserve Turn lookup
without contributing height or the sibling gap. */
.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)) {
height: 0;
}
.callRow {
border-radius: var(--dsw-radius-sm);
}
/* Selection still sets data-selected for details linkage; no outline —
tool rows match Think chrome (no selected ring). */
.hint {
color: var(--dsw-alias-label-tertiary);
font-size: var(--dsh-content-font-size-secondary, 13px);
line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
}
.running {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
--dsh-chat-flow-gap: 12px;
--dsw-alias-label-shimmer: var(--dsw-alias-label-deep-diving-shimmer);
color: var(--dsw-alias-label-deep-diving);
font-size: calc(var(--dsh-content-font-size, 14px) - 2px);
line-height: calc(22px + var(--dsh-content-font-delta, 0px));
}
/* After the Session stops running and before the Turn's actions row arrives, the status keeps its slot
unseen so the tail does not shrink for that interval. */
.running[data-holding] {
visibility: hidden;
opacity: 0;
}
/* The last visible row before the status must contain output, not an input.
Hidden and empty seats do not separate a user message from the status, and
the steering/Inbox echoes after the status are outside the count. */
:is(.column, :global([data-slot="conversation.chat.flow"])) > :nth-last-child(2 of :not([hidden]):not([data-pending-steering]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))):is(
.flowItem:not([data-chat-flow-kind="user"]):not([data-chat-flow-kind="steering"]):not([data-chat-flow-kind="turn-trigger"]),
[data-chat-group-key]
) ~ .running > .runningDivider {
opacity: 1;
}
/* Drawn inside the 16px offset above the status row, so showing or hiding it
never changes the tail slot's height. */
.runningDivider {
position: absolute;
top: -4px;
left: 0;
right: 0;
height: 0.5px;
background: color-mix(in srgb, var(--dsw-alias-border-l1) 75%, var(--dsw-alias-border-l2));
opacity: 0;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.runningDivider {
transition: opacity 160ms ease;
}
}
/* Same offset and height as the completed Turn's tail actions row. */
.runningContent {
display: inline-flex;
align-items: center;
min-width: 0;
gap: 6px;
margin-top: 4px;
height: calc(28px + var(--dsh-content-font-delta, 0px));
}
.runningText {
min-width: 0;
font-variant-numeric: tabular-nums;
}
.runningIcon {
position: relative;
display: inline-flex;
flex: none;
width: calc(14px + var(--dsh-content-font-delta, 0px));
height: calc(14px + var(--dsh-content-font-delta, 0px));
/* The fixed footprint contains the animation's layout and paint. */
contain: strict;
overflow: hidden;
}
.runningWhaleAnimated {
display: none;
position: absolute;
inset: 0;
}
.runningWhaleStill {
display: initial;
}
@supports (mask-mode: alpha) and (mask-image: url('')) {
@media (prefers-reduced-motion: no-preference) and (forced-colors: none) {
.runningWhaleAnimated {
display: block;
background: currentColor;
mask: url('../assets/running-whale@2x.png') center / 100% 100% no-repeat;
mask-mode: alpha;
/* Keep animated-mask repaints on an icon-sized composited layer. */
will-change: transform;
}
.runningWhaleStill {
display: none;
}
}
}
.openError {
color: var(--dsw-alias-state-error-primary);
font-size: var(--dsh-content-font-size-secondary, 13px);
line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px));
}
.older {
display: flex;
justify-content: center;
}
.older button {
border: none;
border-radius: var(--dsw-radius-sm);
padding: 4px 12px;
font-size: 12px;
color: var(--dsw-alias-label-secondary);
background: var(--dsw-alias-interactive-bg-hover-solid);
cursor: pointer;
}
.older button:disabled {
cursor: default;
opacity: 0.6;
}
/* Floating controls sit outside the clipped transcript. A standalone Chat
anchors them to its frame; the shared host supplies the sticky scrollport. */
.toBottomSlot {
position: absolute;
right: 0;
bottom: 16px;
left: 0;
/* Above the sticky composer (z-index 7) so the control stays clickable and
visible over the input card. Exception: while an @/slash menu is open the
seat lifts to 9 (ui-conversation ConversationRoot) so the menu is never
covered by this control. */
z-index: 8;
height: 0;
display: flex;
justify-content: flex-end;
padding-right: max(
calc(var(--dsh-composer-side-clearance) + 16px),
calc((100% - var(--dsh-chat-content-width)) / 2)
);
pointer-events: none;
}
:global([data-conversation-scroll]) .toBottomSlot {
position: sticky;
/* Clears the sticky composer stack (docks + input card + stats): the live
height rides --dsh-composer-height (ConversationRoot's seat observer) so
the control follows a growing textarea; the fallback covers the first
paint before the observer fires. */
bottom: calc(var(--dsh-composer-height, 152px) + 16px);
}
.toBottom {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
margin-top: -34px;
padding: 0;
border: 0;
--dsw-elevation-stroke-color: var(--dsw-alias-border-l3);
border-radius: 100px;
corner-shape: round;
color: var(--dsw-alias-label-primary);
background: var(--dsw-alias-button-floating-fill);
box-shadow: var(--dsw-elevation-panel);
cursor: pointer;
pointer-events: auto;
}
@media (hover: hover) {
.toBottom:hover {
background: var(--dsw-alias-button-floating-hover);
}
}
/* Host open-path refusal: same dialog family as the workspace folder error. */
.modalAction {
min-width: 72px;
}
/* Local surfaces do not contribute flex spacing or selected message text. */
.column > .textInteraction {
position: absolute;
/* Remain below the sticky composer while scroll placement catches up. */
z-index: 6;
visibility: hidden;
margin: 0;
width: max-content;
}