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>
410 lines
14 KiB
CSS
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;
|
|
}
|