refactor: archive GUI clients; keep minimal TUI with hooks/API
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

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>
This commit is contained in:
WHC
2026-10-30 22:49:23 +08:00
co-authored by Cursor
parent ea38247161
commit 5538ed3412
184 changed files with 14098 additions and 1858 deletions
@@ -0,0 +1,458 @@
.root {
display: flex;
flex-direction: column;
align-items: center;
/* Side pads ride the shared clearance (figma Input_Bottom drew L32/R32/B8;
the sides narrow with the shared width axis); the bottom gradient mask
is owned by the chat scroller. No top pad: the composer stack's gap owns
the space above; the status strip still carries its own margin. */
padding: 0 var(--dsh-composer-side-clearance) 4px;
}
.dock {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
max-width: 100%;
padding-top: 4px;
}
.hero .dock:empty {
display: none;
}
.hero {
/* No bottom pad in the centered hero, but the side clearance must survive:
the hero wrapper is full-width on narrow viewports, so this padding is
the only thing keeping the card off the edges there. */
padding: 0 var(--dsh-composer-side-clearance);
}
.notice {
width: 100%;
max-width: var(--dsh-composer-card-max-width);
margin-bottom: 6px;
padding: 4px 8px;
border-radius: var(--dsw-radius-md);
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-secondary);
font-size: 12px;
line-height: 18px;
}
.card {
box-sizing: border-box;
position: relative; /* overlay anchor positioning context */
display: flex;
flex-direction: column;
/* figma Input 75:8208: 12px between the text area and the button row; the
card's top pad before .InputText tightened from the drawn 10 to 8. */
gap: 12px;
width: 100%;
max-width: var(--dsh-composer-card-max-width);
padding-top: 8px;
/* Input stroke: the light neutral (l2, one step above the menus' l1),
drawn as the elevation hairline so it costs no layout; the shadow is the
soft elevation tier. Fill: the input surface token (elevated in dark). */
border: 0;
--dsw-elevation-stroke-color: var(--dsw-alias-border-l2);
border-radius: var(--dsw-radius-panel);
background: var(--dsw-specific-input-major);
box-shadow: var(--dsw-elevation-soft);
/* The draft text follows the Settings font-size preference (the textarea,
mirror, and backdrop all inherit from here, so the three layers stay in
step); the line height keeps the default 10px leading via the shared px
delta. */
font-size: var(--dsh-content-font-size, 14px);
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
/* Elevated surface in dark, same as the menus: the draft scrollport inside
scrolls once the composer hits its height cap, so the thumb takes the l2
pair. Declared on the card because the elevation belongs to the surface,
and the custom properties inherit down to the box that actually scrolls
(see ui-theme styles/scrollbar.css for the rebinding contract). */
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
@media (pointer: coarse) {
.card {
font-size: max(16px, var(--dsh-content-font-size, 14px));
}
}
/* No-workspace trigger state: an inset 1px dashed outline marks the card as a
workspace picker rather than a live composer. Keeping the complete stroke
inside the rounded box prevents clipped corner pixels from reading heavier. */
.cardWorkspaceTrigger {
--dsw-elevation-stroke-color: transparent;
cursor: pointer;
}
.cardWorkspaceTrigger::after {
content: '';
position: absolute;
inset: 0;
box-sizing: border-box;
border: 1px dashed currentColor;
border-radius: var(--dsw-radius-panel);
color: var(--dsw-alias-border-l4);
transition: color 100ms ease;
pointer-events: none;
}
/* Disabled toolbar controls neither receive nor swallow clicks in the trigger
state: pointer events fall through to the card's own click handler, making
the whole card one pick target. */
.cardWorkspaceTrigger :disabled {
pointer-events: none;
}
@media (hover: hover) {
.cardWorkspaceTrigger:hover::after {
color: var(--dsw-alias-state-business-primary);
}
}
.accessory {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px 0;
}
/* Floating overlay anchor (menu / popupSelect shell): entries position
themselves against the card (bottom: 100% + gap); closed entries render null. */
.overlayAnchor {
position: absolute;
inset: 0 0 auto;
height: 0;
}
/* The draft's scrollport, and the ONLY scrolling box in the composer. The
contenteditable grows with its content inside; the 14-line cap lives here
because this is the box the cap describes. */
.scroll {
max-height: var(--dsh-composer-text-max-height);
overflow-y: auto;
/* Keeps the bar off the card's right border; .input's right pad gives the
4px back so the text column is unchanged. */
margin-right: 4px;
}
/* The card's corner arc still overlaps the scrollport's top (8px top
pad reaches only y=8): start the thumb's travel below the arc so it never
pokes outside the capsule. WebKit-path only; Firefox's thin bar has no
track margin, an accepted remainder. */
.scroll::-webkit-scrollbar-track {
margin-top: 8px;
}
/* Auto-grow anchor: the contenteditable is in normal flow and sets the
draft's height; the placeholder rides it absolutely. */
.grow {
position: relative;
}
/* Machine pending dot (adjudicating / submitting). */
.pending {
width: 8px;
height: 8px;
border-radius: 50%;
corner-shape: round;
background: var(--dsw-alias-state-business-primary);
animation: input-pending 1s ease-in-out infinite alternate;
}
@keyframes input-pending {
from { opacity: 0.35; }
to { opacity: 1; }
}
/* The contenteditable draft surface (grows with its content; .scroll caps
and scrolls it). figma .InputText 34:10434 drew pl 16 / pr 12 / pt 4; the
left pad tightened to 14, and the right pad is 8 here plus .scroll's 4px
bar inset. */
.input {
box-sizing: border-box;
/* Docked floor: one 24px line + 4px top pad = 28 content height; 36 leaves
an 8px breath below the line (the hero keeps its own 52px floor below). */
min-height: 36px;
padding: 4px 8px 0 14px;
font-family: var(--dsw-font-family);
font-size: inherit;
line-height: inherit;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
outline: none;
color: var(--dsw-alias-label-primary);
/* Business blue, not brand-primary: that token resolves to ink in this sheet. */
caret-color: var(--dsw-alias-state-business-primary);
}
/* Lexical paragraphs are <p> blocks: strip the UA margins so the surface
keeps the textarea's line rhythm. */
.input p {
margin: 0;
}
/* Claim ghost hint as generated content after the last paragraph: the bar
sets --dsh-composer-hint (a quoted string) while the claim's args are
blank; without the variable the declaration is invalid and nothing shows. */
.input p:last-child::after {
content: var(--dsh-composer-hint);
color: var(--dsw-alias-label-caption);
}
.input[data-composer-composing] p:last-child::after {
content: none;
}
/* A selected command chip stands in for the caret until the next key. */
.input[data-composer-chip-selected] {
caret-color: transparent;
}
.input[data-composer-composing] + .placeholder {
visibility: hidden;
}
/* figma 34:10434: #ADB2B8 light / #81858C dark — the caption pair exactly.
Insets mirror .input's pads (the other right 4px sits on .scroll). One
ellipsized line: a wrapped second line would clip against the docked
36px floor. */
.placeholder {
position: absolute;
inset: 4px 8px auto 14px;
color: var(--dsw-alias-label-caption);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
user-select: none;
}
.inputDisabled {
color: var(--dsw-alias-label-tertiary);
cursor: not-allowed;
}
.input[aria-haspopup='menu'] {
cursor: pointer;
}
/* Hero (centered empty-state) keeps the 2-line floor (figma min-h 52 = ~2 × 24
line + 4pt); the docked composer collapses to the content height. */
.hero .input {
min-height: 52px;
}
/* The hero's 52px floor fits two lines, so its placeholder clamps at two
instead of the docked single line. */
.hero .placeholder {
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
}
/* Toolbar: attach + Plan + Read-only on the left; model + send on the right
(figma Input_Bottom chrome). */
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
/* 2px moved from the bottom pad to the top: the whole control row sits 2px
lower in the card (it read too high against the textarea) while the
controls' own centering stays untouched. */
padding: 2px 8px 6px;
min-width: 0;
/* Size container so the chips inside can collapse to icon-only when the
card runs out of row width (PermissionSelect @container rule). Anonymous
on purpose: CSS modules hash container-name per module, so a name declared
here can never match a query in another module's sheet. */
container-type: inline-size;
}
.row[data-model-compact] {
--dsh-composer-model-text-display: none;
--dsh-composer-model-icon-display: block;
}
.tools,
.modes,
.trailing {
display: flex;
align-items: center;
min-width: 0;
}
/* figma 75:8208 drew 16 between + and the mode chips and 4 between Plan /
Read-only; both gaps settled on 12 so the pill chips read as separate
controls without drifting from the + control. */
.tools {
gap: 12px;
}
.modes {
gap: 12px;
}
.trailing {
flex: none;
/* Wrap keeps the left mode chips and the right controls apart when the card
runs out of row width: the trailing group (model + send) moves to its own
line instead of the left group shrinking until its chip overlaps the
model trigger (external:107). The auto margin re-anchors it right on the
wrapped line; on a single line space-between already pins it right. */
margin-left: auto;
gap: 12px;
}
/* Compact spacing preserves room for the fixed-width controls in narrow cards. */
@container (max-width: 560px) {
.tools,
.modes,
.trailing {
gap: 8px;
}
}
/* Attach circle (figma + control): 28px, selector fill, primary glyph. */
.add {
display: grid;
place-items: center;
flex: none;
width: 28px;
height: 28px;
border: none;
border-radius: 999px;
corner-shape: round;
background: var(--dsw-specific-selector);
color: var(--dsw-alias-label-primary);
cursor: pointer;
}
@media (hover: hover) {
.add:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover-solid);
}
}
.add:disabled {
opacity: 0.5;
cursor: default;
}
/* Plan / Read-only / model — native <select>, chip-like closed chrome
(figma ToggleButton: 13/20 medium secondary, 12px chevron). */
.select {
max-width: 220px;
height: 28px;
padding: 0 20px 0 8px;
border: none;
border-radius: var(--dsw-radius-sm);
outline: none;
background-color: transparent;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 4px center;
background-size: 12px 12px;
color: var(--dsw-alias-label-secondary);
font-size: 13px;
line-height: 20px;
font-weight: 500;
white-space: nowrap;
cursor: pointer;
appearance: none;
}
@media (hover: hover) {
.select:hover:not(:disabled) {
background-color: var(--dsw-alias-interactive-bg-hover);
}
}
.select:disabled {
opacity: 0.5;
cursor: default;
}
/* Primary send (figma IconButton 34:10465): 34px circle, #3964FE light /
#679EFE dark — the info-fill pair (500→400), NOT button-primary (ink);
white glyph; empty text = 0.4 opacity. */
.primary {
display: grid;
place-items: center;
flex: none;
width: 34px;
height: 34px;
border: none;
border-radius: 999px;
corner-shape: round;
background: var(--dsw-alias-button-info-fill);
/* Static white, not the foreground token: the arrow stays white on the blue
fill in both themes (design 34:10465). */
color: #fff;
cursor: pointer;
transition: background-color 100ms ease;
/* Opts out of the row's 2px downward shift (.row top pad): the send circle
keeps its original seat while the smaller chips sit lower. Transform, not
margin, so flex centering math is untouched. */
transform: translateY(-2px);
}
@media (hover: hover) {
.primary:hover:not(:disabled) {
background: var(--dsw-alias-button-info-hover);
}
}
.primary:disabled {
opacity: 0.4;
cursor: default;
}
/* Sending: the message waits for the Host to accept it. The circle keeps its
full fill rather than the disabled fade, and an arc turns where the arrow was. */
.primary[data-sending]:disabled {
opacity: 1;
cursor: progress;
}
.sendingSpinner {
animation: input-sending 800ms linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.sendingSpinner {
animation-duration: 2400ms;
}
}
@keyframes input-sending {
to { transform: rotate(360deg); }
}
.retry {
margin-left: 8px;
padding: 1px 8px;
border: 1px solid currentColor;
border-radius: var(--dsw-radius-sm);
background: transparent;
color: inherit;
font-size: 12px;
cursor: pointer;
}
.standardControls { display: flex; align-items: center; gap: 12px; min-width: 0; }
.tools[hidden], .standardControls[hidden] { display: none; }
.trailingActive { flex: 1; margin-left: 0; }
.activity { display: flex; align-items: center; flex: none; }
.activity:empty { display: none; }
.activityExpanded { flex: 1; min-width: 0; }