Files
OpenResearch/ui/src/styles.css
T
Daniel Kim fcba6a25d1 Show a loading state instead of the empty state while chat history loads (v0.1.77) (#118)
* Show a loading state instead of the empty state while chat history loads (v0.1.77)

Switching between agent conversations briefly flashed the empty state
(OpenResearch logo + 'Ask the agent to explore your codebase...') before
the conversation's messages finished fetching. The empty vs transcript
branch was gated purely on messages.length, which can't distinguish a
not-yet-loaded session from a genuinely empty one.

Add a distinct 'Loading conversation…' state, driven by key presence in
messagesBySession (absent = still fetching). A failed fetch now seeds an
empty transcript so it recovers to the usable empty state instead of a
stuck spinner, and drops the loadedSessions guard so a switch-back retries.

* Enlarge the chat loading spinner and text for legibility

The 13px muted text + 13px wheel read as too small and hidden. Bump the
wheel to 22px (thicker stroke), the label to 16px, and use --subtext for
better contrast.

* Guard failed-fetch recovery against clobbering streamed messages

Review nit: the .catch seeded [] unconditionally, which could overwrite a
message that streamed in via SSE during a failed history fetch (self-heals
on the next re-broadcast, but briefly drops it). Add an onlyIfAbsent flag to
the seed action so recovery no-ops when the transcript key already exists.
2026-07-23 18:15:19 -07:00

4403 lines
100 KiB
CSS
Raw Permalink 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.
/* OpenResearch design system, mirrored from openresearch.sh www/src/styles.css.
Light theme by default, dark via prefers-color-scheme. Quiet idiom:
hairline borders, soft radii, flat surfaces that tint on hover. */
:root {
--base: #ffffff;
--canvas: #faf8f4;
--panel: #f3f0ea;
--surface: #faf7f2;
--surface-bright: #fdfbfb;
--highlight: #fdf3f1;
--text: #1d1b1a;
--subtext: #737373;
--muted: #a1a1a1;
--primary: #9a2036;
--primary-subtle: #f7e9ec;
--secondary: #4a90e2;
--error: #e2a100;
--border: #d4d4d4;
--border-variant: #e5e5e5;
--accent-orange: #da642c;
--accent-red: #d94654;
--accent-teal: #209a84;
--accent-blue: #3a8dff;
--accent-pink: #ff2b90;
--accent-amber: #da9100;
--accent-green: #5eb64c;
--accent-purple: #9c5cff;
--accent-green-subtle: #e7f4e5;
--accent-amber-subtle: #fff3e1;
--accent-teal-subtle: #e1f3f0;
--accent-red-subtle: #fbe9ea;
--accent-blue-subtle: #e5f0ff;
--accent-purple-subtle: #f1e8ff;
--dots-muted: #e3ded5;
--dots-strong: #bdb6a8;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
/* Type scale. Sizes are px steps; anything smaller than --fs-2xs is banned. */
--fs-2xs: 10px;
--fs-xs: 11px;
--fs-sm: 12px;
--fs-md: 13px;
--fs-base: 14px;
--fs-lg: 15px;
--fs-xl: 16px;
--fs-2xl: 18px;
--fs-3xl: 22px;
--fs-4xl: 28px;
--fs-5xl: 34px;
/* Weight scale. Display weight is for hero headings only; the brand
wordmark keeps a literal 800. */
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-display: 700;
/* Radius scale. True circles stay literal 50%. */
--radius-xs: 4px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 10px;
--radius-xl: 12px;
--radius-full: 999px;
/* xterm surface — deliberately dark in both themes; keep in sync with the
terminal theme in LogTerminal.tsx. */
--term-bg: #1a1a1a;
/* Readable column cap shared by the chat header/thread/composer (which must
stay in lockstep or the chat column misaligns) and the settings/doc views. */
--readable-col: 760px;
/* Legacy aliases (pre-restyle class/inline usage). New code uses the tokens above. */
--bg: var(--base);
--bg-canvas: var(--canvas);
--surface-2: var(--panel);
--border-strong: var(--border);
--accent: var(--primary);
--teal: var(--accent-teal);
--green: var(--accent-green);
--red: var(--accent-red);
--amber: var(--accent-amber);
--purple: var(--accent-purple);
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--base: #0e0c0c;
--canvas: #141110;
--panel: #221f1e;
--surface: #1d1b1a;
--surface-bright: #130f0f;
--highlight: #393433;
--text: #e6e1e0;
--subtext: #a68e8b;
--muted: #737373;
--primary: #ffb3ad;
--primary-subtle: #33191b;
--secondary: #3e7fcf;
--error: #ffb951;
--border: #525252;
--border-variant: #404040;
--accent-amber: #e67e22;
--accent-green-subtle: #1c2b18;
--accent-amber-subtle: #33260f;
--accent-teal-subtle: #12332d;
--accent-red-subtle: #331418;
--accent-blue-subtle: #10233a;
--accent-purple-subtle: #251933;
--dots-muted: #2a2523;
--dots-strong: #555555;
color-scheme: dark;
}
}
* {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
body {
background: var(--base);
color: var(--text);
font-family: var(--sans);
font-size: var(--fs-base);
line-height: 1.45;
overflow: hidden;
}
::selection {
background: var(--highlight);
}
button {
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
input,
textarea,
select {
font: inherit;
color: var(--text);
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 6px 10px;
outline: none;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--text);
}
input::placeholder,
textarea::placeholder {
color: var(--muted);
opacity: 1; /* Firefox dims placeholders on its own */
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: var(--radius-sm);
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-track {
background: transparent;
}
@keyframes pulse {
50% {
opacity: 0.35;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ---- app shell ---- */
.app {
display: flex;
flex-direction: column;
height: 100%;
}
/* Horizontal padding gives the chat column a constant gutter to the screen
edge; it stays even when a side panel collapses (a flex gap would not).
The panels' inner margins add the matching gutter when a panel is present. */
.app-body {
display: flex;
flex: 1;
min-height: 0;
padding: 0 14px;
}
/* ---- projects home ---- */
.home {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-gutter: stable both-edges;
background: var(--canvas);
}
.home-inner {
max-width: 620px;
margin: 0 auto;
padding: 48px 24px 64px;
}
.wordmark {
display: inline-flex;
align-items: center;
gap: 0.4em;
color: var(--text);
}
.wordmark svg {
width: 1em;
height: 1em;
flex-shrink: 0;
}
.home-brand {
/* Matches the openresearch.sh landing wordmark (font-bold = 700). */
font-weight: var(--fw-display);
font-size: var(--fs-4xl);
letter-spacing: -0.02em;
margin-bottom: 36px;
}
.home-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.home-head h2 {
margin: 0;
font-size: var(--fs-2xl);
letter-spacing: -0.01em;
}
.home-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.project-card {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
text-align: left;
padding: 14px 16px;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
cursor: pointer;
transition:
border-color 120ms ease,
box-shadow 120ms ease;
}
.project-card:hover {
border-color: var(--text);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
}
.project-delete {
position: absolute;
top: 10px;
right: 10px;
display: none;
padding: 5px;
border-radius: var(--radius-sm);
color: var(--muted);
line-height: 0;
}
.project-card:hover .project-delete {
display: block;
}
.project-delete:hover {
color: var(--danger, #d33);
background: var(--overlay, rgba(0, 0, 0, 0.05));
}
.project-card .name {
font-weight: var(--fw-semibold);
font-size: var(--fs-base);
}
.project-card .repo {
color: var(--text);
}
.project-card .paper {
font-size: var(--fs-xs);
color: var(--muted);
}
.project-card .time {
font-size: var(--fs-xs);
color: var(--muted);
}
/* ---- rail header (brand + project switcher atop the agents rail) ---- */
.rail-brand {
display: flex;
align-items: center;
gap: 4px;
height: 48px;
/* 12px left text inset + ~14px icon right edge — keep in step with
.rail-nav below. */
padding: 0 8px 0 12px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
/* Back arrow + current project name; truncates, arrow stays visible. */
.rail-brand .brand {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
font-weight: var(--fw-semibold);
font-size: var(--fs-md);
color: var(--text);
padding: 4px 6px 4px 0;
border-radius: var(--radius-sm);
}
.rail-brand .brand:hover {
color: var(--subtext);
}
.rail-brand .brand svg {
flex-shrink: 0;
}
.rail-brand .brand .brand-project {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: var(--subtext);
}
.icon-btn:hover {
color: var(--text);
background: var(--surface);
}
/* CSS-only tooltip for icon-only controls: `data-tip` holds the label.
Inverted bubble (text-on-base swap) so it reads in both themes. Default
below the control; `.tip-up` flips above (for footer rows). */
[data-tip] {
position: relative;
}
[data-tip]::after {
content: attr(data-tip);
position: absolute;
top: calc(100% + 6px);
right: -4px;
background: var(--text);
color: var(--base);
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
line-height: 1;
padding: 5px 8px;
border-radius: var(--radius-sm);
white-space: nowrap;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.1s ease 0.3s, visibility 0s linear 0.4s;
z-index: 60;
}
[data-tip]:hover::after {
opacity: 1;
visibility: visible;
}
.tip-up[data-tip]::after {
top: auto;
bottom: calc(100% + 6px);
}
/* ---- buttons (quiet: 1px border, rounded, flat) ---- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
color: var(--text);
white-space: nowrap;
transition:
background 120ms ease,
border-color 120ms ease,
color 120ms ease;
}
.btn:hover:not(:disabled) {
background: var(--surface);
}
.btn:active:not(:disabled) {
background: var(--highlight);
}
.btn:disabled {
opacity: 0.45;
cursor: default;
}
.btn.primary {
background: var(--primary);
border-color: var(--primary);
color: var(--base);
}
.btn.primary:hover:not(:disabled) {
background: color-mix(in oklab, var(--primary) 88%, var(--text));
border-color: color-mix(in oklab, var(--primary) 88%, var(--text));
}
.btn.primary:active:not(:disabled) {
background: color-mix(in oklab, var(--primary) 80%, var(--text));
border-color: color-mix(in oklab, var(--primary) 80%, var(--text));
}
.btn.danger {
color: var(--accent-red);
}
.btn.danger:hover:not(:disabled) {
background: color-mix(in oklab, var(--accent-red) 8%, transparent);
}
.btn.danger:active:not(:disabled) {
background: color-mix(in oklab, var(--accent-red) 14%, transparent);
}
.btn.ghost {
border-color: transparent;
color: var(--text);
}
.btn.ghost:hover:not(:disabled) {
color: var(--text);
background: var(--surface);
}
.btn.sm {
padding: 3px 9px;
font-size: var(--fs-xs);
border-radius: var(--radius-sm);
}
/* ---- status badge (colored dot + quiet label; live statuses pulse) ---- */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
color: var(--text);
white-space: nowrap;
}
.status-badge .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.status-badge.live .dot {
animation: pulse 1.2s ease-in-out infinite;
}
/* The status color lives on the dot; the label stays neutral. */
.st-done .dot {
color: var(--accent-green);
}
.st-failed .dot {
color: var(--accent-red);
}
.st-running .dot {
color: var(--accent-teal);
}
.st-starting .dot {
color: var(--accent-amber);
}
.st-cancelled .dot {
color: var(--accent-orange);
}
.st-editing .dot {
color: var(--accent-purple);
}
.st-idle .dot {
color: var(--muted);
}
/* ---- panes ---- */
/* Shared chrome for the floating side panels (rail + right pane): border,
rounded corners, soft two-layer shadow. Background and margins stay
per-panel. */
.floating-panel {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow:
0 6px 24px color-mix(in oklab, var(--text) 5%, transparent),
0 1px 4px color-mix(in oklab, var(--text) 4%, transparent);
overflow: hidden;
}
/* Floating agents rail: fixed-width, collapsible (not resizable). */
.session-rail {
width: 232px;
flex-shrink: 0;
display: flex;
flex-direction: column;
/* right margin = the gutter to the chat column; screen-edge gutter comes
from .app-body's left padding. */
margin: 10px 14px 10px 0;
background: var(--base);
min-height: 0;
}
/* Rail nav: new session + settings sections, above the Recents list. */
.rail-nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
flex-shrink: 0;
}
.rail-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
font-size: var(--fs-md);
color: var(--text);
border-radius: var(--radius-md);
text-align: left;
}
.rail-nav-item:hover {
background: var(--surface);
}
.rail-nav-item.active {
background: var(--panel);
font-weight: var(--fw-semibold);
}
.rail-section-label {
padding: 10px 10px 6px;
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
color: var(--subtext);
}
/* "Recents" label + the session filter button on one line. */
.rail-section-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.rail-filter {
position: relative;
display: inline-flex;
margin-right: 2px;
}
/* A non-default filter keeps the button lit via the shared .icon-btn.active. */
.rail-filter-btn {
width: 24px;
height: 24px;
border-radius: var(--radius-sm);
}
.rail-empty {
padding: 6px 10px;
font-size: var(--fs-sm);
color: var(--muted);
}
.session-rail .rail-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 4px 8px;
}
/* The pane fills the leftover space so the thread's scrollbar reaches its
right edge; the readable-col cap lives on the header, the composer, and
.chat-thread-inner (inside the scroller), which center themselves. */
.chat-pane {
flex: 1;
/* min-width:0 (not 360) so a wide panel can never force the flex row past
the viewport; the panel's own clamp keeps the usable minimum. */
min-width: 0;
display: flex;
flex-direction: column;
background: var(--base);
min-height: 0;
}
/* Floating right panel: a closable, resizable card. `.max` lifts it out of
the flex row to cover (nearly) the whole window, still floating. */
.right-pane {
position: relative;
flex-shrink: 0;
min-width: 0;
display: flex;
flex-direction: column;
/* left margin = the gutter to the chat column; screen-edge gutter comes
from .app-body's right padding. */
margin: 10px 0 10px 14px;
background: var(--canvas);
}
.right-pane.max {
position: fixed;
inset: 10px;
margin: 0;
z-index: 60;
box-shadow: 0 12px 40px color-mix(in oklab, var(--text) 22%, transparent);
}
/* Invisible grab strip on the panel's left edge. Sits above the detail
overlays (.drawer/.term-view are z-index 20, inset 0) so resizing works in
every view, not just the tree. */
.panel-resizer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
cursor: col-resize;
z-index: 30;
}
.panel-resizer:hover,
.panel-resizer:active {
background: color-mix(in oklab, var(--text) 12%, transparent);
}
/* Expand/close cluster at the right end of the panel's tab bar. */
.panel-controls {
display: inline-flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
/* Panel tab bar: pinned Log/Artifacts + closable experiment/file tabs. */
.tabs {
display: flex;
align-items: center;
gap: 4px;
padding: 0 10px;
height: 40px;
border-bottom: 1px solid var(--border);
background: var(--base);
flex-shrink: 0;
}
/* Scrollable strip of tabs. */
.tab-strip {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
.tab-strip::-webkit-scrollbar {
display: none;
}
/* Dynamic (closable) tabs: truncated label, inline close "x". */
.tab.closable {
max-width: 176px;
padding-right: 4px;
}
.tab .tab-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab .tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: var(--radius-xs);
color: var(--muted);
flex-shrink: 0;
}
.tab .tab-close:hover {
background: color-mix(in oklab, var(--text) 15%, transparent);
color: var(--text);
}
/* One tab style for the whole strip — pinned tabs simply have no close "x". */
.tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
border-radius: var(--radius-sm);
white-space: nowrap;
flex-shrink: 0;
}
.tab.active {
background: var(--surface);
}
.tab-body {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
}
/* Toolbar row above a tab's content (e.g. the tree/table switch). */
.pane-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 10px 12px 0;
flex-shrink: 0;
}
.pane-content {
flex: 1;
min-height: 0;
position: relative;
}
/* Right-pane plan tab: a plain, padded document surface — the plan is a
markdown file, not a card, so no tinted background. */
.plan-tab-content {
overflow-y: auto;
background: var(--base);
padding: 18px 24px;
}
.plan-tab-content .md {
max-width: var(--readable-col);
}
/* Segmented control: a clearly-darker track (the theme's creams all blend
into each other) with a raised white pill on the active segment. */
.seg {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: var(--radius-md);
background: color-mix(in oklab, var(--text) 10%, transparent);
}
.seg button {
padding: 3px 12px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
border-radius: var(--radius-sm);
}
.seg button:hover {
color: var(--text);
}
.seg button.active {
background: var(--base);
box-shadow: 0 1px 3px color-mix(in oklab, var(--text) 25%, transparent);
}
/* ---- chat ---- */
/* Borderless header that fades into the chat: the thread scrolls under a
gradient hanging below the bar instead of hitting a hard rule. */
.chat-header {
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px;
background: var(--base);
flex-shrink: 0;
height: 48px;
position: relative;
z-index: 4;
width: 100%;
max-width: var(--readable-col);
margin: 0 auto;
}
/* Rail hidden: the header spans the full pane (Claude-desktop style) so the
reopen toggle lands in the window's top-left corner with the title beside
it; the tighter padding puts the icon on the screen-edge gutter. */
.chat-header.rail-hidden {
max-width: none;
padding: 0 2px;
}
/* Breathing room between the corner toggle and the title. */
.chat-header.rail-hidden > .icon-btn:first-child {
margin-right: 12px;
}
.chat-header::after {
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 24px;
background: linear-gradient(to bottom, var(--base), transparent);
pointer-events: none;
}
.chat-header .title {
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
color: var(--text);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row {
position: relative;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
padding: 7px 10px;
border-radius: var(--radius-md);
font-size: var(--fs-md);
color: var(--text);
cursor: pointer;
user-select: none;
}
.session-row:hover {
background: var(--surface);
}
.session-row.active {
background: var(--surface);
font-weight: var(--fw-medium);
}
/* Reserved status column so titles align whether or not a busy dot shows. */
.session-row .session-dot {
width: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.session-row .session-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row .session-time {
font-size: var(--fs-2xs);
color: var(--muted);
flex-shrink: 0;
}
/* Hover (or an open menu) swaps the timestamp for the three-dot menu button. */
.session-row .session-menu-btn {
display: none;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin: -2px 0;
border-radius: var(--radius-sm);
color: var(--muted);
flex-shrink: 0;
}
.session-row .session-menu-btn:hover {
color: var(--text);
background: var(--panel);
}
/* :focus-within (not :focus-visible): it holds while the kebab itself is
focused — a row-only match would display:none the button mid-Tab and dump
focus to <body>, making the menu keyboard-unreachable. */
.session-row:hover .session-menu-btn,
.session-row:focus-within .session-menu-btn,
.session-row.menu-open .session-menu-btn {
display: inline-flex;
}
.session-row:hover .session-time,
.session-row:focus-within .session-time,
.session-row.menu-open .session-time {
display: none;
}
.session-row .busy-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--primary);
animation: pulse 1.2s infinite;
flex-shrink: 0;
}
/* Turn held on an unanswered card: ready for the user — steady, no pulse. */
.session-row .busy-dot.waiting {
animation: none;
}
/* Inline rename: the title span is replaced by an input sharing its box so the
text doesn't jump. Take the row's full width and hide the kebab/time. */
.session-row .session-title-input {
flex: 1;
min-width: 0;
padding: 1px 5px;
margin: -2px 0;
font: inherit;
color: var(--text);
background: var(--base);
border: 1px solid var(--primary);
border-radius: var(--radius-sm);
outline: none;
}
.session-row.editing {
background: var(--surface);
cursor: default;
}
.session-row.editing .session-menu-btn,
.session-row.editing .session-time {
display: none;
}
.chat-thread {
flex: 1;
min-height: 0;
overflow-y: auto;
/* stable: no shift when the bar appears; both-edges: keeps the column centered */
scrollbar-gutter: stable both-edges;
}
.chat-thread-inner {
max-width: var(--readable-col);
margin: 0 auto;
/* Bottom padding clears the composer's 24px fade so the last message stays
fully legible when scrolled to the very bottom. */
padding: 16px 16px 32px;
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text);
padding: 20px;
text-align: center;
}
.chat-empty h2 {
margin: 0;
font-size: var(--fs-5xl);
font-weight: var(--fw-display);
letter-spacing: -0.02em;
color: var(--text);
}
.chat-empty .chat-empty-hint {
font-size: var(--fs-sm);
color: var(--muted);
max-width: 340px;
text-align: center;
}
.chat-empty p {
margin: 0;
font-size: var(--fs-md);
max-width: 320px;
}
/* one-click /reproduce-paper prefill for paper-born projects */
.chat-empty .chat-suggest {
font-family: var(--mono);
font-size: var(--fs-sm);
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-full);
padding: 6px 14px;
cursor: pointer;
transition: border-color 120ms ease;
}
.chat-empty .chat-suggest:hover {
border-color: var(--text);
}
/* Shown while an existing session's transcript is fetched, so switching
sessions never flashes the empty state before its messages seed in. */
.chat-loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--subtext);
font-size: var(--fs-xl);
padding: 20px;
}
/* Larger, more visible wheel than the inline .spinner used in the thread. */
.chat-loading .spinner {
width: 22px;
height: 22px;
border-width: 3px;
}
/* user message: a right-aligned rounded bubble, no border, no label */
.msg-user {
align-self: flex-end;
max-width: 88%;
background: var(--surface);
border-radius: 16px; /* chat bubble — deliberate outsized radius */
padding: 10px 15px;
font-size: var(--fs-md);
white-space: pre-wrap;
word-break: break-word;
}
.msg-images {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.msg-images img {
max-width: 220px;
max-height: 160px;
border: 1px solid var(--border-variant);
border-radius: var(--radius-xs);
display: block;
}
.msg-assistant {
font-size: var(--fs-base);
line-height: 1.62;
color: var(--text);
min-width: 0;
}
.reasoning {
color: var(--muted);
font-size: var(--fs-sm);
margin: 2px 0;
}
.reasoning summary {
cursor: pointer;
list-style: none;
user-select: none;
font-weight: var(--fw-semibold);
}
.reasoning[open] {
white-space: pre-wrap;
}
/* tool calls: bordered surface card, one row per call */
/* ---- tool calls: compact, coalesced, gray (Claude-desktop style) ---- */
.tool-group {
margin: 2px 0;
}
.tool-group-summary {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 3px 2px;
cursor: pointer;
color: var(--muted);
font-size: var(--fs-md);
text-align: left;
border-radius: var(--radius-sm);
}
.tool-group-summary:hover {
color: var(--subtext);
background: var(--surface);
}
.tool-group.has-error .tool-group-summary {
color: var(--accent-red);
}
.tool-chevron {
flex-shrink: 0;
color: var(--muted);
transition: transform 0.12s ease;
}
.tool-chevron.open {
transform: rotate(90deg);
}
.tool-group-rows {
display: flex;
flex-direction: column;
gap: 1px;
margin: 2px 0 4px 7px;
padding-left: 10px;
border-left: 1px solid var(--border-variant);
}
.tool-row {
display: flex;
flex-direction: column;
}
.tool-row summary {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 4px;
cursor: pointer;
list-style: none;
user-select: none;
min-width: 0;
border-radius: var(--radius-sm);
}
.tool-row summary:hover {
background: var(--surface);
}
.tool-row summary::-webkit-details-marker {
display: none;
}
.tool-line {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--fs-md);
color: var(--subtext);
}
.tool-open {
flex-shrink: 0;
font-size: var(--fs-xs);
color: var(--primary);
}
.tool-open:hover {
text-decoration: underline;
}
.tool-status {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
background: var(--muted);
}
.tool-status.running {
background: var(--accent-amber);
animation: pulse 1.2s ease-in-out infinite;
}
.tool-status.error {
background: var(--accent-red);
}
.tool-detail {
margin: 2px 0 4px 14px;
}
.tool-cmd-full {
padding: 6px 10px;
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--text);
background: var(--surface);
border-radius: var(--radius-sm);
white-space: pre-wrap;
word-break: break-word;
}
.tool-output {
margin-top: 3px;
padding: 6px 10px;
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--subtext);
white-space: pre-wrap;
word-break: break-word;
max-height: 260px;
overflow-y: auto;
background: var(--base);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
}
.working {
display: flex;
align-items: center;
gap: 8px;
color: var(--subtext);
font-size: var(--fs-md);
padding: 2px 0 8px;
}
/* Turn held open on an unanswered card: waiting on the user, not the model. */
.working.awaiting {
font-style: italic;
}
.spinner {
width: 13px;
height: 13px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
/* composer: rounded box and send button. Borderless like the header —
the thread fades into the box through a gradient hanging above it. */
.composer {
/* No top padding: the fade hanging above leads straight into the box. */
padding: 0 12px 12px;
flex-shrink: 0;
position: relative;
z-index: 4;
background: var(--base);
width: 100%;
max-width: var(--readable-col);
margin: 0 auto;
}
.composer::before {
content: "";
position: absolute;
bottom: 100%;
left: 0;
right: 0;
height: 24px;
background: linear-gradient(to top, var(--base), transparent);
pointer-events: none;
}
.composer-box {
position: relative; /* anchors the slash-skill menu */
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
}
.composer textarea {
border: none;
background: none;
resize: none;
padding: 10px 12px 4px;
font-size: var(--fs-md);
min-height: 42px;
max-height: 180px;
}
/* pasted images waiting to be sent */
.composer-attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px 0;
}
.attachment-thumb {
position: relative;
}
.attachment-thumb img {
width: 52px;
height: 52px;
object-fit: cover;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
display: block;
}
.attachment-thumb button {
position: absolute;
top: -5px;
right: -5px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: 1px solid var(--border);
border-radius: 50%;
background: var(--surface);
color: var(--text);
cursor: pointer;
}
.attachment-thumb button:hover {
background: var(--text);
color: var(--base);
}
.composer-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 6px 8px 8px;
}
.send-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background: var(--primary);
color: var(--base);
transition:
background 100ms ease,
opacity 100ms ease;
}
.send-btn:hover:not(:disabled) {
background: color-mix(in oklab, var(--primary) 88%, var(--text));
}
.send-btn:disabled {
opacity: 0.4;
cursor: default;
}
.send-btn.stop {
background: var(--surface);
color: var(--text);
}
.send-btn.stop:hover:not(:disabled) {
background: color-mix(in oklab, var(--surface) 88%, var(--text));
}
/* ---- markdown ---- */
.md {
min-width: 0;
word-break: break-word;
color: var(--text);
line-height: 1.62;
}
.md > *:first-child {
margin-top: 0;
}
.md > *:last-child {
margin-bottom: 0;
}
.md p {
margin: 10px 0;
}
.md strong {
color: var(--text);
font-weight: var(--fw-semibold);
}
.md pre {
background: var(--surface);
border: 1px solid color-mix(in oklab, var(--border) 50%, transparent);
border-radius: var(--radius-md);
padding: 8px 12px;
overflow-x: auto;
font-size: var(--fs-sm);
color: var(--text);
}
.md code {
font-family: var(--mono);
font-size: 0.9em;
font-weight: var(--fw-medium);
color: var(--primary);
background: var(--panel);
border: 1px solid var(--border-variant);
border-radius: var(--radius-xs);
padding: 1px 5px;
}
/* KaTeX math — tame the library's 1.21em default so formulas sit at body
size, and let wide display equations scroll instead of overflowing. */
.md .katex {
font-size: 1.05em;
}
.md .katex-display {
margin: 12px 0;
overflow-x: auto;
overflow-y: hidden;
padding: 2px 0;
}
/* File mention chip — reads as clickable inline code (thin border, panel
fill, mono), matching openresearch.sh's FileMention. */
.md .file-chip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
margin: 0 1px;
padding: 0 6px;
vertical-align: baseline;
font-family: var(--mono);
font-size: 0.9em;
font-weight: var(--fw-medium);
color: var(--text);
background: var(--panel);
border: 1px solid var(--border-variant);
border-radius: var(--radius-xs);
cursor: pointer;
}
.md .file-chip:hover:not(:disabled) {
background: var(--surface);
color: var(--primary);
}
.md .file-chip svg {
flex: none;
opacity: 0.6;
}
.md .file-chip-label {
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.md pre code {
background: none;
border: none;
color: inherit;
padding: 0;
font-weight: var(--fw-regular);
}
.md-code {
position: relative;
margin: 10px 0;
}
.md-code pre {
margin: 0;
}
.md-code-copy {
position: absolute;
top: 6px;
right: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
color: var(--muted);
background: var(--base);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease;
}
.md-code:hover .md-code-copy {
opacity: 1;
}
.md-code-copy:hover {
color: var(--text);
border-color: var(--muted);
}
.md h1,
.md h2,
.md h3,
.md h4 {
color: var(--text);
font-size: 1.05em;
font-weight: var(--fw-semibold);
margin: 12px 0 6px;
}
.md ul,
.md ol {
margin: 6px 0;
padding-left: 22px;
}
.md li::marker {
color: var(--primary);
}
.md a {
color: var(--primary);
}
/* Same wide-table treatment as .report-md table: cells keep whole-word
min-content widths (see .md td below), so an over-wide table scrolls in
place instead of clipping at the pane edge. */
.md table {
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
font-size: var(--fs-md);
margin: 10px 0;
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow-x: auto;
}
.md th,
.md td {
border-bottom: 1px solid var(--border-variant);
padding: 8px 14px;
text-align: left;
color: var(--text);
/* .md's word-break: break-word shrinks cell min-content width to one
character, letting auto table layout split short words mid-word;
overflow-wrap keeps full-word min-content and only breaks overflow. */
word-break: normal;
overflow-wrap: break-word;
}
.md tr:last-child td {
border-bottom: none;
}
.md thead th {
background: var(--surface);
font-weight: var(--fw-medium);
color: var(--text);
border-bottom: 1px solid var(--border);
}
.md tbody tr:hover td {
background: var(--surface-bright);
}
.md blockquote {
margin: 6px 0;
padding: 2px 0 2px 10px;
border-left: 3px solid var(--border);
color: var(--subtext);
}
/* ---- tree nodes (graph cards on the dotted canvas) ---- */
/* Cards must stay `selectable` (ReactFlow sets pointer-events:none on nodes
* that are neither draggable nor selectable, which would kill the action
* buttons) — but selection itself means nothing here, so suppress its
* pointer cursor. Specificity beats RF's stylesheet. Focus/selected visuals
* need no override: nodesFocusable={false} removes the tab stop, RF's own
* CSS zeroes node focus outlines, and its .selected box-shadow only targets
* built-in node types, not custom ones. */
.react-flow__node.react-flow__node-exp.selectable {
cursor: default;
}
.exp-node {
/* Keep in sync with NODE_W in TreeView.tsx (layout math uses it). */
width: 264px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
padding: 10px 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
font-size: var(--fs-md);
transition: box-shadow 120ms ease;
}
.exp-node:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.exp-node.live {
border-color: var(--accent-teal);
box-shadow: 0 2px 12px rgba(32, 154, 132, 0.2);
}
.exp-node .node-eyebrow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
font-size: var(--fs-2xs);
font-weight: var(--fw-medium);
color: var(--muted);
}
.exp-node .node-head {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.exp-node .node-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.exp-node .node-slug {
font-family: var(--mono);
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.exp-node .node-title {
margin-top: 4px;
color: var(--text);
font-size: var(--fs-sm);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.exp-node .node-meta {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: var(--fs-2xs);
color: var(--muted);
}
/* Direct view shortcuts (Changes / Logs) at the card's foot. */
.exp-node .node-actions {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid var(--border-variant);
display: flex;
align-items: center;
gap: 3px;
}
.exp-node .node-action {
display: inline-flex;
align-items: center;
gap: 5px;
/* Tight: Changes + Logs + Code + the GitHub icon share one fixed-width row. */
padding: 3px 6px;
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
color: var(--text);
border-radius: var(--radius-sm);
/* The GitHub shortcut is an <a> among sibling <button>s. */
text-decoration: none;
}
.exp-node .node-action:hover {
color: var(--text);
background: var(--surface);
}
/* External GitHub shortcut: icon-only, pinned to the card's far edge. */
.exp-node .node-action-ext {
margin-left: auto;
padding: 3px 5px;
}
/* run-history squares (RUNS strip on node cards) */
.run-squares {
display: flex;
align-items: center;
gap: 3px;
}
.run-sq {
width: 9px;
height: 9px;
flex-shrink: 0;
}
.run-sq.pass {
background: var(--accent-green);
}
.run-sq.fail {
border: 1.5px solid color-mix(in oklab, var(--accent-red) 55%, transparent);
}
.run-sq.live {
background: var(--accent-teal);
animation: pulse 1.2s ease-in-out infinite;
}
.run-sq.other {
border: 1.5px solid var(--border);
}
.react-flow__handle {
opacity: 0;
pointer-events: none;
}
.react-flow__attribution {
display: none !important;
}
/* ---- runs table ---- */
/* Runs table: a white card on the panel canvas, quiet separators. */
.runs-table-wrap {
position: absolute;
inset: 0;
overflow: auto;
padding: 12px;
}
.runs-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: var(--fs-md);
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.runs-table th {
text-align: left;
color: var(--text);
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
padding: 8px 12px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
background: var(--base);
z-index: 1;
}
.runs-table td {
padding: 8px 12px;
border-bottom: 1px solid color-mix(in oklab, var(--text) 6%, transparent);
white-space: nowrap;
}
.runs-table tr:last-child td {
border-bottom: none;
}
.runs-table tr.clickable {
cursor: pointer;
}
.runs-table tr.clickable:hover td {
background: var(--canvas);
}
.mono {
font-family: var(--mono);
font-size: var(--fs-sm);
}
/* Instances tab: Running / Past section headers. */
.instances-section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 22px 0 10px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
}
.instances-section-title .count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: var(--radius-md);
background: var(--canvas);
border: 1px solid var(--border);
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
color: var(--text);
}
/* Normal-flow wrapper: the shared .runs-table-wrap is position:absolute (it
fills the experiments panel); in the settings pane the table sits in flow and
just needs horizontal-scroll safety on narrow viewports. */
.instances-table-wrap {
overflow-x: auto;
}
/* In-flow empty state (the shared .empty-state is position:absolute and would
cover the whole pane, overlapping the other section). */
.instances-empty {
margin: 0;
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--base);
color: var(--subtext);
font-size: var(--fs-md);
}
/* Badge plus the optional job-page link (HF only — Modal stores no URL). */
.backend-cell {
display: inline-flex;
align-items: center;
gap: 2px;
}
.backend-cell .icon-btn {
width: 22px;
height: 22px;
}
/* Backend cell: brand logo + name + flavor detail. */
.backend-badge {
display: inline-flex;
align-items: center;
gap: 7px;
}
.backend-badge svg {
flex: none;
display: block;
}
.backend-badge .backend-name {
font-weight: var(--fw-medium);
}
.backend-badge .backend-detail {
color: var(--muted);
}
.backend-badge.muted {
color: var(--muted);
}
/* ---- detail drawer ---- */
.drawer {
position: absolute;
inset: 0;
background: var(--base);
display: flex;
flex-direction: column;
z-index: 20;
}
/* No padding-top: it would hold sticky diff-file headers 18px below the top
with scrolled content peeking through the gap. The first child carries the
spacing as margin instead, which scrolls away with the content. */
.drawer-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0 16px 24px;
display: flex;
flex-direction: column;
gap: 22px;
}
.drawer-body > :first-child {
margin-top: 18px;
}
.drawer-section h3 {
margin: 0 0 10px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
}
.kv {
display: grid;
grid-template-columns: auto 1fr;
gap: 3px 14px;
font-size: var(--fs-md);
}
.kv .k {
color: var(--subtext);
}
.kv .v {
font-family: var(--mono);
font-size: var(--fs-sm);
word-break: break-all;
}
/* full-pane terminal view (mirror of openresearch.sh ExperimentFullView) */
.term-view {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
background: var(--base);
z-index: 20;
}
.term-bar {
display: flex;
align-items: center;
gap: 8px;
height: 40px;
padding: 0 10px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.term-title {
min-width: 0;
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.term-bar .error {
font-size: var(--fs-sm);
color: var(--accent-red);
}
.term-bar .btn {
display: inline-flex;
align-items: center;
gap: 5px;
}
.term-fill {
flex: 1;
min-height: 0;
background: var(--term-bg);
padding: 4px 0 4px 6px;
}
.term-empty {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
text-align: center;
font-size: var(--fs-md);
color: var(--muted);
}
/* run history dropdown */
.run-history {
position: relative;
flex-shrink: 0;
}
.run-picker {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 6px 4px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
color: var(--text);
}
.run-picker:hover {
background: var(--surface);
}
.run-picker .run-label {
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
}
.run-picker-chev {
color: var(--muted);
flex-shrink: 0;
}
.history-menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
min-width: 230px;
max-height: 320px;
overflow-y: auto;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
padding: 5px;
z-index: 50;
}
.history-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
padding: 6px 8px;
font-size: var(--fs-sm);
border-radius: var(--radius-sm);
}
.history-item:hover {
background: var(--surface);
}
.history-item.active {
background: var(--surface);
}
.history-item .run-label {
font-weight: var(--fw-semibold);
}
.history-item .when {
margin-left: auto;
font-size: var(--fs-xs);
color: var(--muted);
}
/* commit picker (Changes tab) */
.changes-branch {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.commit-picker {
display: flex;
align-items: center;
gap: 8px;
}
/* Tiny caption naming what a header control is (Branch / Commit / Source) —
* two adjacent unlabeled monospace pickers are indistinguishable. */
.ctl-label {
flex-shrink: 0;
font-size: var(--fs-2xs);
font-weight: var(--fw-medium);
color: var(--subtext);
}
.commit-picker select {
flex: 1;
min-width: 0;
font-family: var(--mono);
font-size: var(--fs-sm);
}
.uncommitted-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-purple);
flex-shrink: 0;
}
.changes-note {
font-size: var(--fs-sm);
color: var(--muted);
}
.empty-state {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 24px;
text-align: center;
color: var(--subtext);
}
.empty-state p {
max-width: 46ch;
margin: 0;
font-size: var(--fs-md);
line-height: 1.5;
text-wrap: balance;
}
.empty-state-cta {
gap: 6px;
}
.empty-state p.empty-state-title {
font-size: var(--fs-2xl);
font-weight: var(--fw-regular);
color: var(--text);
}
.empty-state p.empty-state-hint {
font-size: var(--fs-lg);
color: var(--subtext);
}
/* ---- first-run onboarding (rides the .home canvas) ---- */
.onboarding .home-inner {
max-width: 560px;
padding-top: 96px;
}
.onb-eyebrow {
font-size: var(--fs-2xs);
font-weight: var(--fw-semibold);
color: var(--muted);
margin-bottom: 14px;
}
.onb-title {
margin: 0 0 6px;
font-size: var(--fs-3xl);
letter-spacing: -0.01em;
}
.onb-sub {
color: var(--text);
font-size: var(--fs-base);
line-height: 1.55;
margin: 0 0 22px;
max-width: 480px;
}
.onb-cards {
display: flex;
flex-direction: column;
gap: 10px;
}
.onb-card {
display: flex;
flex-direction: column;
gap: 5px;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 18px 20px;
}
.onb-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.onb-card-name {
font-weight: var(--fw-semibold);
font-size: var(--fs-base);
}
.onb-card-detail {
font-size: var(--fs-sm);
}
.onb-card-meta {
font-size: var(--fs-sm);
color: var(--subtext);
}
.onb-card-meta code {
font-family: var(--mono);
font-size: var(--fs-xs);
}
.onb-card-row {
display: grid;
grid-template-columns: 56px 1fr auto;
align-items: baseline;
gap: 12px;
padding: 3px 0;
}
.onb-loading {
display: flex;
align-items: center;
gap: 8px;
color: var(--subtext);
font-size: var(--fs-md);
padding: 8px 0;
}
.onb-gh-options {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 12px;
padding-top: 14px;
border-top: 1px solid var(--border-variant);
}
.onb-gh-option {
display: flex;
flex-direction: column;
gap: 8px;
}
.onb-gh-option-label {
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
color: var(--text);
}
.onb-gh-option-body {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.onb-gh-cmd {
font-family: var(--mono);
font-size: var(--fs-sm);
background: var(--panel);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
padding: 3px 8px;
}
.onb-gh-hint {
font-size: var(--fs-sm);
color: var(--subtext);
}
.onb-gh-hint code {
font-family: var(--mono);
font-size: var(--fs-xs);
}
.onb-gh-or {
display: flex;
align-items: center;
gap: 10px;
color: var(--muted);
font-size: var(--fs-xs);
}
.onb-gh-or::before,
.onb-gh-or::after {
content: "";
flex: 1;
height: 1px;
background: var(--border-variant);
}
.onb-gh-option .onb-token-form {
flex: 1;
min-width: 0;
margin-top: 0;
}
.onb-token-form {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.onb-token-form input {
flex: 1;
min-width: 220px;
font-family: var(--mono);
font-size: var(--fs-sm);
}
.onb-token-form a {
font-size: var(--fs-sm);
color: var(--subtext);
white-space: nowrap;
}
.onb-token-form .error {
flex-basis: 100%;
color: var(--accent-red);
font-size: var(--fs-md);
white-space: pre-wrap;
}
.onb-actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 22px;
}
/* new-project mode switch (existing repo vs blank repo) */
.form .form-seg {
align-self: flex-start;
margin-bottom: 2px;
}
.form .form-seg button {
padding: 5px 12px;
}
/* repo parse feedback under the new-project repository input */
.form .repo-hint {
font-family: var(--mono);
font-weight: var(--fw-regular);
font-size: var(--fs-xs);
color: var(--muted);
}
.form .repo-hint.ok {
color: var(--accent-teal);
}
/* new-project "from a paper": search results + selected-paper card */
.form .paper-results {
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius-md);
max-height: 240px;
overflow-y: auto;
}
.form .paper-results button {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 8px 10px;
background: none;
border: none;
border-bottom: 1px solid var(--border-variant);
text-align: left;
font: inherit;
color: var(--text);
cursor: pointer;
}
.form .paper-results button:last-child {
border-bottom: none;
}
.form .paper-results button:hover {
background: var(--surface);
}
.form .paper-results .title {
font-size: var(--fs-md);
font-weight: var(--fw-medium);
}
.form .paper-results .id,
.form .paper-pick .id {
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--muted);
}
.form .paper-pick {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
}
.form .paper-pick .meta {
min-width: 0;
}
.form .paper-pick .title {
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
}
/* ---- forms / modal ---- */
.form {
display: flex;
flex-direction: column;
gap: 10px;
}
.form label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: var(--fs-xs);
color: var(--text);
font-weight: var(--fw-medium);
}
.form .row2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.form .actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 6px;
}
.form .error,
.settings-card > .error {
color: var(--accent-red);
font-size: var(--fs-md);
white-space: pre-wrap;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(29, 27, 26, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
width: 480px;
max-width: 94vw;
max-height: 88vh;
overflow-y: auto;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
padding: 24px;
}
.modal h2 {
margin: 0 0 14px;
font-size: var(--fs-xl);
}
/* ---- settings ---- */
.settings-section + .settings-section {
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.settings-section h3 {
margin: 0 0 8px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
}
.settings-loading {
display: flex;
align-items: center;
gap: 8px;
color: var(--subtext);
font-size: var(--fs-md);
padding: 4px 0;
}
.settings-note {
margin: 10px 0 0;
font-size: var(--fs-sm);
padding: 8px 10px;
border: 1px solid var(--accent-amber);
border-radius: var(--radius-md);
background: var(--accent-amber-subtle);
color: var(--accent-amber);
font-weight: var(--fw-medium);
}
/* Determinate progress bar (data-dir move). */
.progress {
margin: 12px 0 4px;
}
.progress-track {
height: 8px;
border-radius: var(--radius-full);
background: var(--surface);
border: 1px solid var(--border);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent);
border-radius: var(--radius-full);
transition: width 0.2s ease;
}
.progress-caption {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-size: var(--fs-sm);
color: var(--muted);
}
.settings-form {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.badge {
display: inline-flex;
align-items: center;
/* Explicit sans so badges nested in mono contexts (.kv .v) don't inherit. */
font-family: var(--sans);
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
padding: 1px 7px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
}
.badge.ok {
color: var(--accent-green);
border-color: var(--accent-green);
background: var(--accent-green-subtle);
}
.badge.err {
color: var(--accent-red);
border-color: var(--accent-red);
background: var(--accent-red-subtle);
}
.badge.warn {
color: var(--accent-amber);
border-color: var(--accent-amber);
background: var(--accent-amber-subtle);
}
/* ---- files tab ---- */
/* ---- Files tab: split explorer — tree on the left, inline preview right ---- */
.files-tab {
height: 100%;
min-height: 0;
display: flex;
background: var(--base);
}
.files-pill {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 7px 11px;
background: var(--base);
color: var(--text);
text-decoration: none;
}
.files-pill code {
font-family: var(--mono);
font-size: var(--fs-sm);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Direct children only — the copy .icon-btn manages its own icon color. */
.files-pill > svg {
flex-shrink: 0;
color: var(--muted);
}
a.files-pill:hover {
border-color: var(--muted);
}
/* Left pane: the file tree, with the dir-path footer pinned under it.
Shares the preview's base background — one surface, split by a hairline —
so it reads as a file browser, not a stray colored strip. */
.ftree-pane {
position: relative;
flex-shrink: 0;
display: flex;
flex-direction: column;
min-height: 0;
border-left: 1px solid var(--border-variant);
border-right: 1px solid var(--border-variant);
background: var(--base);
}
/* Invisible grab strip straddling the tree/preview divider. */
.ftree-resizer {
position: absolute;
right: -3px;
top: 0;
bottom: 0;
width: 6px;
cursor: col-resize;
z-index: 30;
}
.ftree-resizer:hover,
.ftree-resizer:active {
background: color-mix(in oklab, var(--text) 12%, transparent);
}
.ftree-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10px 8px;
}
.ft-row {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
border-radius: var(--radius-sm);
cursor: pointer;
}
.ft-row:hover {
background: var(--surface);
}
.ft-row.selected,
.ft-row.selected:hover {
background: var(--panel);
}
.ft-chevron {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
color: var(--muted);
transition: transform 0.12s ease;
}
.ft-chevron.open {
transform: rotate(90deg);
}
.ft-chevron.spacer {
visibility: hidden;
}
.ft-title {
flex: 1;
min-width: 0;
font-size: var(--fs-md);
font-weight: var(--fw-medium);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ft-dirname,
.ft-name {
flex: 1;
min-width: 0;
font-family: var(--mono);
font-size: var(--fs-sm);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ft-dirname {
color: var(--text);
}
.ft-thumb {
width: 18px;
height: 18px;
object-fit: cover;
border-radius: var(--radius-xs);
border: 1px solid var(--border-variant);
flex-shrink: 0;
}
.ft-divider {
padding: 12px 8px 4px;
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
color: var(--muted);
}
/* The files-dir path — demoted from header pills to a quiet footer. */
.ftree-footer {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 2px;
padding: 5px 8px;
border-top: 1px solid var(--border-variant);
}
.ftree-footer code {
flex: 1;
min-width: 0;
/* RTL ellipsis trick: keep the tail of the path visible, elide the front. */
direction: rtl;
text-align: left;
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ftree-footer .icon-btn {
width: 22px;
height: 22px;
}
/* Right pane: the selected entry, rendered inline. */
.fpreview {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
min-height: 0;
background: var(--base);
}
.fpreview-none {
align-items: center;
justify-content: center;
gap: 8px;
font-size: var(--fs-md);
color: var(--muted);
}
.fpreview-head {
height: 40px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 14px;
border-bottom: 1px solid var(--border-variant);
color: var(--subtext);
flex-shrink: 0;
}
.fpreview-path {
font-family: var(--mono);
font-size: var(--fs-sm);
color: var(--text);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fpreview-date,
.fpreview-size {
font-size: var(--fs-xs);
color: var(--muted);
white-space: nowrap;
flex-shrink: 0;
}
.fpreview-body {
flex: 1;
min-height: 0;
overflow: auto;
}
/* Documents (reports, markdown) read as a centered readable column. */
.fpreview-body.doc {
padding: 18px 28px 48px;
}
.fpreview-body.doc .report-md {
max-width: var(--readable-col);
margin: 0 auto;
}
.fpreview-image {
display: flex;
align-items: flex-start;
justify-content: center;
padding: 24px;
}
.fpreview-image img {
max-width: 100%;
height: auto;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.fpreview-pdf {
display: block;
width: 100%;
height: 100%;
border: 0;
}
/* Whole-tab empty state: what this tab is for, plus the drop-in path. */
.files-empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 24px;
text-align: center;
color: var(--muted);
}
.files-empty-state h3 {
margin: 6px 0 0;
font-size: var(--fs-base);
font-weight: var(--fw-semibold);
color: var(--text);
}
.files-empty-state p {
margin: 0;
max-width: 420px;
font-size: var(--fs-md);
line-height: 1.55;
color: var(--subtext);
}
.files-empty-state .ftree-footer {
margin-top: 10px;
max-width: 100%;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 6px 10px;
background: var(--base);
}
.files-empty-state .ftree-footer code {
max-width: 380px;
}
.files-truncated {
margin: 0;
padding: 8px 14px;
font-size: var(--fs-xs);
color: var(--muted);
}
/* Reports are long-form documents, so preserve their Markdown hierarchy.
The shared .md rules intentionally keep chat headings compact. */
.report-md {
font-size: var(--fs-lg);
}
.report-md h1 {
font-size: 2em;
line-height: 1.18;
margin: 28px 0 14px;
}
.report-md h2 {
font-size: 1.5em;
line-height: 1.25;
margin: 28px 0 10px;
}
.report-md h3 {
font-size: 1.2em;
line-height: 1.35;
margin: 22px 0 8px;
}
.report-md h4 {
font-size: 1em;
line-height: 1.4;
margin: 18px 0 6px;
}
/* Wide GFM tables scroll in place instead of widening the report pane. */
.report-md table {
display: block;
width: max-content;
max-width: 100%;
overflow-x: auto;
}
.report-md .report-img {
display: block;
margin: 12px 0;
}
.report-md .report-img img {
max-width: 100%;
height: auto;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.report-md .report-img-caption {
display: block;
margin-top: 4px;
text-align: center;
font-size: var(--fs-sm);
color: var(--subtext);
}
/* ---- diff view (react-diff-view; mirrors .openresearch-diff) ---- */
.openresearch-diff {
display: flex;
flex-direction: column;
gap: 16px;
}
.diff-file-card {
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
}
/* The header owns the card's rounded top outline (overlaid on the card
border via -1px margins) so it still looks like a rounded card top while
stuck — the card's own border scrolls away with the card. Painted via two
layered pseudo-elements because negative-z children cover the element's own
background/border: a square white mask below (hides code scrolling past the
corner notches), the rounded bordered surface above it, text on top. */
.diff-file-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: -1px -1px 0;
width: calc(100% + 2px);
text-align: left;
padding: 8px 12px;
cursor: pointer;
}
.diff-file-header::before {
content: "";
position: absolute;
inset: 0;
z-index: -2;
background: var(--base);
}
.diff-file-header::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: var(--canvas);
border: 1px solid var(--border);
border-radius: var(--radius-md) var(--radius-md) 0 0;
}
.diff-file-header .chev {
color: var(--muted);
font-size: var(--fs-2xs);
flex-shrink: 0;
width: 12px;
}
.diff-file-header .path {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.diff-file-header .path code {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--mono);
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
color: var(--text);
}
.diff-file-header .stats {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
font-family: var(--mono);
font-size: var(--fs-2xs);
font-weight: var(--fw-medium);
font-variant-numeric: tabular-nums;
}
.diff-stat-add {
color: var(--accent-green);
}
.diff-stat-del {
color: var(--accent-red);
}
.diff-empty {
padding: 8px 12px;
color: var(--muted);
font-size: var(--fs-md);
}
.truncated-notice {
border: 1px solid var(--accent-amber);
border-radius: var(--radius-md);
background: var(--accent-amber-subtle);
padding: 12px 14px;
font-size: var(--fs-md);
}
.truncated-notice h4 {
margin: 0 0 4px;
font-size: var(--fs-md);
color: var(--accent-amber);
}
.truncated-notice p {
margin: 0;
color: var(--subtext);
}
.openresearch-diff .openresearch-diff-file {
--openresearch-diff-selection-background-color: color-mix(
in oklab,
var(--surface) 76%,
var(--primary)
);
--openresearch-diff-gutter-selection-background-color: color-mix(
in oklab,
var(--surface) 68%,
var(--primary)
);
/* GitHub-strength washes: the tint should read as a highlight, not paint. */
--openresearch-diff-insert-gutter-background-color: color-mix(
in oklab,
var(--base) 84%,
var(--accent-green)
);
--openresearch-diff-delete-gutter-background-color: color-mix(
in oklab,
var(--base) 86%,
var(--accent-red)
);
--openresearch-diff-insert-code-background-color: color-mix(
in oklab,
var(--base) 91%,
var(--accent-green)
);
--openresearch-diff-delete-code-background-color: color-mix(
in oklab,
var(--base) 92%,
var(--accent-red)
);
--openresearch-diff-insert-edit-background-color: color-mix(
in oklab,
var(--base) 72%,
var(--accent-green)
);
--openresearch-diff-delete-edit-background-color: color-mix(
in oklab,
var(--base) 78%,
var(--accent-red)
);
--openresearch-diff-divider-color: var(--border);
--openresearch-diff-omit-gutter-line-color: color-mix(in oklab, var(--base) 86%, var(--text));
--openresearch-diff-unified-gutter-text-color: color-mix(in oklab, var(--text) 45%, var(--base));
--diff-background-color: var(--base);
--diff-text-color: var(--text);
--diff-font-family: var(--mono);
--diff-selection-text-color: var(--primary);
--diff-selection-background-color: var(--openresearch-diff-selection-background-color);
--diff-gutter-selected-text-color: var(--diff-selection-text-color);
--diff-gutter-selected-background-color: var(
--openresearch-diff-gutter-selection-background-color
);
--diff-code-selected-text-color: var(--diff-selection-text-color);
--diff-code-selected-background-color: var(--diff-selection-background-color);
--diff-gutter-insert-text-color: var(--accent-green);
--diff-gutter-insert-background-color: var(--openresearch-diff-insert-gutter-background-color);
--diff-gutter-delete-text-color: var(--accent-red);
--diff-gutter-delete-background-color: var(--openresearch-diff-delete-gutter-background-color);
--diff-code-insert-text-color: var(--diff-text-color);
--diff-code-insert-background-color: var(--openresearch-diff-insert-code-background-color);
--diff-code-delete-text-color: var(--diff-text-color);
--diff-code-delete-background-color: var(--openresearch-diff-delete-code-background-color);
--diff-code-insert-edit-text-color: var(--diff-text-color);
--diff-code-insert-edit-background-color: var(--openresearch-diff-insert-edit-background-color);
--diff-code-delete-edit-text-color: var(--diff-text-color);
--diff-code-delete-edit-background-color: var(--openresearch-diff-delete-edit-background-color);
--diff-omit-gutter-line-color: var(--openresearch-diff-omit-gutter-line-color);
width: 100%;
font-size: var(--fs-sm);
line-height: 1.5;
}
.openresearch-diff .openresearch-diff-file.diff-unified {
table-layout: auto;
}
.openresearch-diff .openresearch-diff-file.diff-unified .diff-line > td:first-child {
display: none;
}
.openresearch-diff .openresearch-diff-file.diff-unified .diff-line > td:nth-child(2) {
width: 0%;
padding: 0 0.5rem 0 0.625rem;
white-space: nowrap;
text-align: right;
color: var(--openresearch-diff-unified-gutter-text-color);
border-right: 1px solid var(--openresearch-diff-divider-color);
user-select: none;
cursor: default;
}
.openresearch-diff .openresearch-diff-file .diff-line {
line-height: 1.5;
}
.openresearch-diff .openresearch-diff-file .diff-hunk + .diff-hunk .diff-line:first-child > td {
border-top: 1px solid var(--openresearch-diff-divider-color);
}
/* prism token colors (One Light / One Dark, same hexes as openresearch.sh) */
/* prettier-ignore */
:is(.md, .openresearch-diff, .file-view) .token.comment,
:is(.md, .openresearch-diff, .file-view) .token.prolog,
:is(.md, .openresearch-diff, .file-view) .token.cdata { font-style: italic; color: #a0a1a7; }
:is(.md, .openresearch-diff, .file-view) .token.punctuation { color: #383a42; }
:is(.md, .openresearch-diff, .file-view) .token.property,
:is(.md, .openresearch-diff, .file-view) .token.tag,
:is(.md, .openresearch-diff, .file-view) .token.deleted { color: #e45649; }
:is(.md, .openresearch-diff, .file-view) .token.constant,
:is(.md, .openresearch-diff, .file-view) .token.symbol,
:is(.md, .openresearch-diff, .file-view) .token.boolean,
:is(.md, .openresearch-diff, .file-view) .token.number { color: #986801; }
:is(.md, .openresearch-diff, .file-view) .token.selector,
:is(.md, .openresearch-diff, .file-view) .token.attr-name,
:is(.md, .openresearch-diff, .file-view) .token.char,
:is(.md, .openresearch-diff, .file-view) .token.inserted,
:is(.md, .openresearch-diff, .file-view) .token.string { color: #50a14f; }
:is(.md, .openresearch-diff, .file-view) .token.builtin { color: #c18401; }
:is(.md, .openresearch-diff, .file-view) .token.operator,
:is(.md, .openresearch-diff, .file-view) .token.entity,
:is(.md, .openresearch-diff, .file-view) .token.url { color: #56b6c2; }
:is(.md, .openresearch-diff, .file-view) .token.atrule,
:is(.md, .openresearch-diff, .file-view) .token.attr-value { color: #986801; }
:is(.md, .openresearch-diff, .file-view) .token.keyword { color: #a626a4; }
:is(.md, .openresearch-diff, .file-view) .token.function,
:is(.md, .openresearch-diff, .file-view) .token.decorator,
:is(.md, .openresearch-diff, .file-view) .token.def { color: #4078f2; }
:is(.md, .openresearch-diff, .file-view) .token.class-name,
:is(.md, .openresearch-diff, .file-view) .token.namespace { color: #c18401; }
:is(.md, .openresearch-diff, .file-view) .token.regex { color: #50a14f; }
:is(.md, .openresearch-diff, .file-view) .token.important,
:is(.md, .openresearch-diff, .file-view) .token.variable { color: #e45649; }
:is(.md, .openresearch-diff, .file-view) .token.parameter { color: #383a42; }
@media (prefers-color-scheme: dark) {
:is(.md, .openresearch-diff, .file-view) .token.comment,
:is(.md, .openresearch-diff, .file-view) .token.prolog,
:is(.md, .openresearch-diff, .file-view) .token.cdata { font-style: italic; color: #7f848e; }
:is(.md, .openresearch-diff, .file-view) .token.punctuation { color: #abb2bf; }
:is(.md, .openresearch-diff, .file-view) .token.property,
:is(.md, .openresearch-diff, .file-view) .token.tag,
:is(.md, .openresearch-diff, .file-view) .token.deleted { color: #e06c75; }
:is(.md, .openresearch-diff, .file-view) .token.constant,
:is(.md, .openresearch-diff, .file-view) .token.symbol,
:is(.md, .openresearch-diff, .file-view) .token.boolean,
:is(.md, .openresearch-diff, .file-view) .token.number { color: #d19a66; }
:is(.md, .openresearch-diff, .file-view) .token.selector,
:is(.md, .openresearch-diff, .file-view) .token.attr-name,
:is(.md, .openresearch-diff, .file-view) .token.char,
:is(.md, .openresearch-diff, .file-view) .token.inserted,
:is(.md, .openresearch-diff, .file-view) .token.string { color: #98c379; }
:is(.md, .openresearch-diff, .file-view) .token.builtin { color: #e5c07b; }
:is(.md, .openresearch-diff, .file-view) .token.operator,
:is(.md, .openresearch-diff, .file-view) .token.entity,
:is(.md, .openresearch-diff, .file-view) .token.url { color: #56b6c2; }
:is(.md, .openresearch-diff, .file-view) .token.atrule,
:is(.md, .openresearch-diff, .file-view) .token.attr-value { color: #d19a66; }
:is(.md, .openresearch-diff, .file-view) .token.keyword { color: #c678dd; }
:is(.md, .openresearch-diff, .file-view) .token.function,
:is(.md, .openresearch-diff, .file-view) .token.decorator,
:is(.md, .openresearch-diff, .file-view) .token.def { color: #61afef; }
:is(.md, .openresearch-diff, .file-view) .token.class-name,
:is(.md, .openresearch-diff, .file-view) .token.namespace { color: #e5c07b; }
:is(.md, .openresearch-diff, .file-view) .token.regex { color: #98c379; }
:is(.md, .openresearch-diff, .file-view) .token.important,
:is(.md, .openresearch-diff, .file-view) .token.variable { color: #e06c75; }
:is(.md, .openresearch-diff, .file-view) .token.parameter { color: #abb2bf; }
}
/* ---- settings sections (shown in the middle pane, picked from the rail) ---- */
/* Like .chat-thread: the scroller spans the pane so its scrollbar sits at the
pane's right edge, and .settings-view re-applies the readable width. */
.settings-view-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-gutter: stable both-edges;
}
.settings-view {
max-width: var(--readable-col);
margin: 0 auto;
padding: 24px 32px 60px;
}
.settings-view h1 {
margin: 0 0 6px;
font-size: var(--fs-3xl);
}
.settings-head-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.settings-head-row h1 {
margin: 0;
}
/* Errors shown directly in a settings tab (outside a .form wrapper), e.g. the
Instances load error. Mirrors `.form .error`. */
.settings-view > .error {
color: var(--accent-red);
font-size: var(--fs-md);
white-space: pre-wrap;
margin: 0 0 12px;
}
.settings-sub {
margin: 0 0 18px;
color: var(--text);
font-size: var(--fs-md);
}
.settings-card {
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 16px 18px;
margin-bottom: 16px;
}
.settings-card h3 {
margin: 0 0 10px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
}
.settings-card .settings-sub {
margin-bottom: 12px;
}
.settings-card-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.settings-card .kv {
gap: 6px 18px;
}
.settings-empty {
color: var(--muted);
font-size: var(--fs-md);
margin: 4px 0 0;
}
.muted {
color: var(--muted);
}
/* compute target list (Settings → Compute) */
.compute-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 14px;
}
.compute-row {
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
.compute-row-head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
cursor: pointer;
user-select: none;
}
.compute-row-head:hover {
background: var(--surface);
border-radius: var(--radius-lg);
}
.compute-row.open .compute-row-head:hover {
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.compute-row-logo {
display: inline-flex;
align-items: center;
flex: none;
}
.compute-row-name {
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
color: var(--text);
flex: none;
}
.compute-row-summary {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--muted);
font-size: var(--fs-sm);
}
.compute-row-head .badge,
.compute-make-default,
.compute-default-pill {
flex: none;
}
/* Rendered as `badge compute-default-pill` — only the accent color differs. */
.compute-default-pill {
color: var(--primary);
border-color: var(--primary);
}
.compute-chevron-btn {
flex: none;
display: inline-flex;
align-items: center;
padding: 2px;
border-radius: var(--radius-sm);
}
.compute-chevron-btn:hover {
background: var(--panel);
}
.compute-chevron {
color: var(--muted);
transition: transform 120ms ease;
}
.compute-row.open .compute-chevron {
transform: rotate(180deg);
}
.compute-row-body {
border-top: 1px solid var(--border);
padding: 14px;
}
.compute-row-body .settings-card {
margin-bottom: 0;
margin-top: 14px;
}
.compute-default-note {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.compute-flavor-form {
margin-bottom: 14px;
}
.compute-flavor-form label {
max-width: 320px;
}
.compute-flavor-hint {
font-size: var(--fs-sm);
}
/* Standing disclosure, not a warning — quiet muted text, no amber callout. */
.compute-footnote {
display: flex;
align-items: flex-start;
gap: 6px;
margin: 2px 0 0;
font-size: var(--fs-sm);
color: var(--muted);
}
.compute-footnote svg {
flex: none;
margin-top: 1px; /* optical align with the first text line */
}
/* harness sub-tabs */
.harness-tabs {
display: flex;
gap: 4px;
margin-bottom: 14px;
border-bottom: 1px solid var(--border-variant);
}
.harness-tabs button {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 12px;
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
color: var(--text);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.harness-tabs button:hover {
color: var(--text);
}
.harness-tabs button.active {
border-bottom-color: var(--primary);
}
.harness-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--muted);
}
.harness-dot.ok {
background: var(--accent-green);
}
.harness-dot.err {
background: var(--accent-red);
}
.harness-dot.warn {
background: var(--accent-amber);
}
/* env var table — fixed layout so buttons appearing never reflow columns */
.env-table {
width: 100%;
border-collapse: collapse;
font-size: var(--fs-md);
table-layout: fixed;
}
.env-table td:first-child {
width: 32%;
overflow-wrap: anywhere;
}
.env-table .badge {
margin-left: 8px;
}
/* inline edit/add inputs read as table cells, not boxes; the underline is the
focus indicator the removed border would otherwise provide */
.env-table input {
width: 100%;
border: none;
background: transparent;
padding: 0;
}
.env-table input:focus {
box-shadow: 0 1px 0 0 var(--text);
}
/* fixed row height: inline inputs and buttons appear without shifting rows */
.env-table td {
height: 36px;
padding: 0 10px 0 0;
vertical-align: middle;
border-bottom: 1px solid var(--border-variant);
}
.env-table td:last-child {
width: 116px;
white-space: nowrap;
text-align: right;
}
/* a full-width colspan cell (hint row) is also :last-child — undo the
action-column styling so the note wraps and aligns normally */
.env-table td[colspan] {
white-space: normal;
text-align: left;
}
.env-table .icon-btn {
margin-left: 8px;
vertical-align: middle;
}
.env-table .icon-btn:hover {
color: var(--accent-red);
}
/* k8s flavor table (Settings → Compute) */
.flavor-table {
width: 100%;
border-collapse: collapse;
font-size: var(--fs-md);
}
.flavor-table th {
padding: 5px 10px 5px 0;
border-bottom: 1px solid var(--border);
text-align: left;
font-weight: var(--fw-medium);
color: var(--text);
}
.flavor-table td {
padding: 5px 10px 5px 0;
border-bottom: 1px solid var(--border-variant);
}
/* ssh hosts table — fixed layout with anywhere-wrapping so long aliases and
key paths can never push the action column outside the card. Widths track
the column order in SshSection: host, address, identity (flex), status,
test button. */
.ssh-table {
table-layout: fixed;
}
.ssh-table th:nth-child(1) {
width: 20%; /* host */
}
.ssh-table th:nth-child(2) {
width: 26%; /* address */
}
.ssh-table th:nth-child(4) {
width: 108px; /* status */
}
.ssh-table th:nth-child(5) {
width: 52px; /* test button */
}
.ssh-table td {
overflow-wrap: anywhere;
}
.ssh-table td:last-child {
padding-right: 0;
text-align: right;
}
.ssh-tested-at {
display: block;
margin-top: 2px;
color: var(--muted);
font-size: var(--fs-xs);
}
@keyframes settings-spin {
to {
transform: rotate(360deg);
}
}
.spin {
animation: settings-spin 0.9s linear infinite;
}
/* ---- composer model picker ---- */
.model-picker,
.option-picker {
position: relative;
display: inline-flex;
}
/* Boxed composer control (permission mode, model). */
.composer-pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: var(--fs-sm);
color: var(--text);
padding: 5px 8px;
border-radius: var(--radius-sm);
}
.composer-pill:hover {
background: var(--surface);
color: var(--text);
}
/* Text-only composer control (reasoning level) — sits beside the model label. */
.composer-bare {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: var(--fs-sm);
color: var(--text);
padding: 5px 4px;
border-radius: var(--radius-sm);
}
.composer-bare:hover {
color: var(--text);
}
.model-menu,
.option-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
max-height: 380px;
display: flex;
flex-direction: column;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
z-index: 50;
overflow: hidden;
}
.model-menu {
width: 320px;
}
.option-menu {
min-width: 190px;
padding: 6px;
}
/* Context-window meter: ring trigger + title/value head over a thin bar. */
.composer-bare.context-ring {
display: inline-flex;
align-items: center;
margin-right: 8px;
}
.context-meter-menu {
width: 280px;
padding: 10px 12px 12px;
}
.context-meter-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
font-size: var(--fs-sm);
color: var(--muted);
}
.context-meter-value {
color: var(--text);
font-variant-numeric: tabular-nums;
}
.context-meter-menu .progress {
margin: 8px 0 0;
}
.context-meter-menu .progress-track {
height: 5px;
border: none;
background: var(--border);
}
/* Right-anchored menus (bottom-right controls) open toward the left edge. */
.model-menu.align-right,
.option-menu.align-right {
left: auto;
right: 0;
}
/* Rail menus open downward (composer menus open upward, above the box). */
.option-menu.drop-down {
bottom: auto;
top: calc(100% + 4px);
}
/* Session kebab menu: anchored to the row, hugging its right edge. Absolute
inside the scrolling rail, so near the list's bottom it extends the scroll
area instead of overlaying — acceptable while lists stay short; a portal or
upward flip is the fix if that changes. */
.option-menu.session-menu {
left: auto;
right: 6px;
top: calc(100% - 2px);
min-width: 140px;
}
.model-item.danger,
.model-item.danger:hover {
color: var(--accent-red);
}
/* "· Default" note on the pinned default row. */
.option-default {
color: var(--muted);
font-weight: var(--fw-regular);
}
/* Separator under the default row. */
.option-sep {
height: 1px;
margin: 5px 4px;
background: var(--border-variant);
}
/* Right-aligned 1-based number hints in the mode menu. */
.option-num {
color: var(--muted);
font-size: var(--fs-xs);
font-variant-numeric: tabular-nums;
}
.model-menu input {
border: none;
border-bottom: 1px solid var(--border-variant);
background: none;
padding: 9px 12px;
font-size: var(--fs-md);
outline: none;
}
.model-menu-list {
overflow-y: auto;
padding: 6px;
}
.model-group {
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
color: var(--muted);
padding: 8px 8px 4px;
}
.model-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
text-align: left;
padding: 6px 8px;
font-size: var(--fs-md);
border-radius: var(--radius-sm);
}
.model-item:hover {
background: var(--surface);
}
.model-item .model-id {
display: block;
font-family: var(--mono);
font-size: var(--fs-2xs);
color: var(--muted);
}
.model-more {
padding: 4px 8px 8px;
font-size: var(--fs-xs);
color: var(--muted);
}
/* Mid-session footer: the harness is fixed, so only its models are offered. */
.model-locked-note {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 12px;
font-size: var(--fs-xs);
color: var(--muted);
border-top: 1px solid var(--border-variant);
}
.model-locked-note svg {
flex-shrink: 0;
}
/* ---- composer slash-skill menu ---- */
.skill-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
min-width: 340px;
max-width: 100%;
padding: 6px;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
z-index: 50;
overflow: hidden;
}
.skill-item {
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
text-align: left;
padding: 7px 8px;
border-radius: var(--radius-sm);
}
.skill-item.active {
background: var(--surface);
}
.skill-item .skill-name {
font-size: var(--fs-sm);
}
.skill-item .skill-hint {
color: var(--muted);
}
.skill-item .skill-desc {
font-size: var(--fs-xs);
color: var(--subtext);
}
/* Command chip — the picked slash-skill, rendered as a unit in the composer
(removable) and echoed in sent user bubbles (inert). */
.skill-chip {
display: inline-flex;
align-items: center;
padding: 1px 7px;
font-family: var(--mono);
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
color: var(--primary);
background: var(--primary-subtle);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
}
/* The chip overlays the textarea's first line, Claude-desktop style; the
textarea's first line is indented past it (inline text-indent, measured
from the chip's width), so wrapped args flow full-width beneath the chip. */
.composer-input {
position: relative;
display: flex;
/* Clips the chip once the textarea scrolls it out of the first line. */
overflow: hidden;
}
.composer-input textarea {
flex: 1;
}
.composer-chip {
position: absolute;
/* Sit on the textarea's first text line (10px top padding, --fs-md text)
despite the chip's smaller --fs-sm body. */
top: 9px;
left: 12px;
z-index: 1;
/* Inert like inline text — clicks land in the textarea underneath. */
pointer-events: none;
}
.msg-user .skill-chip {
margin-right: 2px;
vertical-align: baseline;
}
/* --- file viewer (right-pane tab opened from chat tool rows) ------------- */
.file-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.file-view-header {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
border-bottom: 1px solid var(--border-variant);
color: var(--text);
flex-shrink: 0;
}
.file-view-path {
font-family: var(--mono);
font-size: var(--fs-sm);
color: var(--text);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Branch badge on committed-state file views (code browser in branch mode) —
a plain source label, not a BranchPill: the header shouldn't grow a second
navigation affordance next to the path. */
.file-view-ref {
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--muted);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
padding: 1px 6px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.file-view-body {
flex: 1;
min-height: 0;
overflow: auto;
background: var(--base);
}
/* Gutter + code share one row so line numbers track content rows 1:1 — the
code never soft-wraps (min-width: max-content), so line N is row N. */
.file-view-codewrap {
display: flex;
align-items: flex-start;
min-width: max-content;
}
/* Identical font metrics + vertical padding are the 1:1 row-alignment
invariant — one rule so they can't drift apart. */
.file-view-gutter,
.file-view-code {
margin: 0;
padding-top: 14px;
padding-bottom: 14px;
font-family: var(--mono);
font-size: var(--fs-sm);
line-height: 1.55;
}
.file-view-gutter {
padding-left: 14px;
padding-right: 10px;
text-align: right;
color: var(--muted);
user-select: none;
/* Stay visible while the code scrolls horizontally. */
position: sticky;
left: 0;
background: var(--base);
border-right: 1px solid var(--border-variant);
flex-shrink: 0;
}
.file-view-code {
padding-left: 16px;
padding-right: 16px;
tab-size: 4;
min-width: max-content;
}
.file-view-note {
padding: 10px 16px;
font-size: var(--fs-sm);
color: var(--muted);
}
/* Rendered markdown files (.md tabs) — doc-sized type over the chat .md base. */
.file-view-md {
max-width: var(--readable-col);
padding: 18px 20px 32px;
}
.file-view-md .md {
font-size: var(--fs-base);
}
.file-view-md .md h1 {
font-size: 1.5em;
margin: 18px 0 8px;
}
.file-view-md .md h2 {
font-size: 1.25em;
margin: 16px 0 8px;
}
.file-view-md .md h3 {
font-size: 1.1em;
}
.icon-btn.active {
color: var(--primary);
background: var(--surface);
}
/* --- interactive prompt cards (plan / permission / question) ------------------
Emitted by the harness when the agent asks for a plan approval, a permission,
or a choice. Kept quiet: a hairline border, a thin per-kind left accent, no
shadow — it should read as an inline aside, not a callout that shouts. */
.prompt-card {
margin: 8px 0;
padding: 12px 14px;
border: 1px solid var(--border);
border-left: 3px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
display: flex;
flex-direction: column;
gap: 9px;
}
.prompt-card.plan {
border-left-color: var(--accent-blue);
}
.prompt-card.permission {
border-left-color: var(--accent-amber);
}
.prompt-card.question {
border-left-color: var(--accent-purple);
}
/* A card in a read-only host (no respond handler): muted, buttons disabled.
Resolved cards never render as .prompt-card at all — permission cards
vanish, plan/question cards collapse to .prompt-collapsed rows. */
.prompt-card.readonly {
opacity: 0.6;
}
.prompt-head {
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
color: var(--text);
}
.prompt-head code {
font-family: var(--mono);
font-size: var(--fs-sm);
color: var(--text);
}
.prompt-sub {
font-size: var(--fs-sm);
color: var(--subtext);
word-break: break-word;
}
.prompt-q {
font-size: var(--fs-base);
font-weight: var(--fw-semibold);
line-height: 1.5;
color: var(--text);
}
.prompt-plan {
font-size: var(--fs-base);
line-height: 1.6;
color: var(--text);
max-height: 340px;
overflow-y: auto;
}
/* Question options: each is a full-width button with the label on its own line
and the (optional) description block beneath it — never inline, or the two
run together ("labelDescription…"). */
.prompt-options {
display: flex;
flex-direction: column;
gap: 6px;
}
.prompt-option {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
width: 100%;
padding: 8px 11px;
text-align: left;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--base);
color: var(--text);
cursor: pointer;
transition:
border-color 80ms ease,
background 80ms ease;
}
.prompt-option:hover:not(:disabled) {
border-color: var(--border-strong);
background: var(--surface);
}
.prompt-option.sel {
border-color: var(--primary);
background: var(--primary-subtle);
}
.prompt-option:disabled {
cursor: default;
}
.prompt-option-label {
display: block;
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
}
.prompt-option-desc {
display: block;
font-size: var(--fs-sm);
font-weight: var(--fw-regular);
line-height: 1.45;
color: var(--subtext);
}
/* Action row (plan approve / permission allow-deny / multi-select submit). */
.prompt-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.prompt-actions .btn-primary,
.prompt-actions .btn-ghost {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 13px;
font-family: inherit;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
transition:
background 80ms ease,
border-color 80ms ease;
}
.prompt-actions .btn-primary {
background: var(--primary);
color: var(--base);
}
.prompt-actions .btn-primary:hover:not(:disabled) {
opacity: 0.9;
}
.prompt-actions .btn-ghost {
background: transparent;
border-color: var(--border);
color: var(--subtext);
}
.prompt-actions .btn-ghost:hover:not(:disabled) {
border-color: var(--border-strong);
color: var(--text);
background: var(--surface);
}
.prompt-actions button:disabled {
opacity: 0.5;
cursor: default;
}
/* Resolved plan/question cards collapse to a one-line expandable row at their
chronological position (Claude-desktop style); resolved permission cards
render nothing at all. Same visual family as `.reasoning`. */
.prompt-collapsed {
color: var(--muted);
font-size: var(--fs-md);
margin: 4px 0;
}
.prompt-collapsed summary {
display: flex;
align-items: baseline;
gap: 8px;
cursor: pointer;
list-style: none;
user-select: none;
}
.prompt-collapsed summary::-webkit-details-marker {
display: none;
}
.prompt-collapsed summary::after {
content: "›";
color: var(--muted);
transition: transform 80ms ease;
}
.prompt-collapsed[open] summary::after {
transform: rotate(90deg);
}
.prompt-collapsed-title {
font-weight: var(--fw-semibold);
word-break: break-word;
}
.prompt-outcome {
font-size: var(--fs-sm);
color: var(--subtext);
word-break: break-word;
}
.prompt-outcome.approved,
.prompt-outcome.chosen {
color: var(--accent-green);
}
.prompt-outcome.approved::before,
.prompt-outcome.chosen::before {
content: "✓ ";
}
.prompt-outcome.revised,
.prompt-outcome.rejected {
color: var(--accent-amber);
}
.prompt-collapsed-body {
margin-top: 6px;
padding-left: 12px;
border-left: 2px solid var(--border);
font-size: var(--fs-md);
color: var(--subtext);
}
.prompt-collapsed-options {
margin: 6px 0 0;
padding-left: 18px;
}
.prompt-collapsed-options .sel {
color: var(--text);
font-weight: var(--fw-semibold);
}
.prompt-collapsed-note {
margin-top: 6px;
font-style: italic;
}
/* --- plan strip (docked above the composer while a plan awaits a decision) --- */
.plan-strip {
/* Lives INSIDE .composer (so the composer's picker popovers stack above
it). Claude-desktop shape: title line on top, a row of buttons below. */
position: relative;
width: 100%;
margin: 0 0 10px;
padding: 11px 13px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
border: 1px solid var(--border);
border-left: 3px solid var(--accent-blue);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: 0 2px 10px rgb(0 0 0 / 6%);
}
.plan-strip-info {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.plan-strip-open {
margin-left: auto;
padding: 0;
border: none;
background: none;
color: var(--accent-blue);
font-size: var(--fs-md);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
.plan-strip-open:hover {
text-decoration: underline;
}
.plan-strip-spacer {
flex: 1;
}
/* Revise mode: a standalone bordered textarea (the strip itself has none —
unlike the borderless composer textarea, this needs to read as its own
input), focus ring on :focus like any other text field. */
.plan-strip-revise-input {
width: 100%;
resize: none;
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 9px 11px;
font-size: var(--fs-md);
font-family: inherit;
background: var(--base);
color: var(--text);
}
.plan-strip-revise-input:focus {
border-color: var(--accent-blue);
}
.plan-strip-icon {
color: var(--accent-blue);
flex-shrink: 0;
align-self: center;
}
.plan-strip-title {
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
white-space: nowrap;
}
/* Button styling comes from the shared `.prompt-actions` rules, but the
strip is monochrome (no brand red): black text, black borders, quiet
hover. `--text` keeps it theme-correct in dark mode. The spacer splits
reject/revise (left) from the accepts (right); when the pane is too
narrow the accept group wraps to its own right-aligned line. */
.plan-strip-actions {
gap: 6px 8px;
justify-content: flex-end;
}
.plan-strip-actions .btn-primary,
.plan-strip-actions .btn-ghost {
background: transparent;
border: 1px solid var(--text);
color: var(--text);
}
.plan-strip-actions .btn-primary:hover:not(:disabled),
.plan-strip-actions .btn-ghost:hover:not(:disabled) {
background: var(--surface-2, rgb(0 0 0 / 5%));
border-color: var(--text);
color: var(--text);
opacity: 1;
}
/* The accept primary: solid dark (desktop's black button), not brand red.
`--text`-on-`--base` inverts correctly in dark mode. */
.plan-strip-actions .plan-strip-primary {
background: var(--text);
border-color: var(--text);
color: var(--base);
}
.plan-strip-actions .plan-strip-primary:hover:not(:disabled) {
background: color-mix(in oklab, var(--text) 85%, var(--base));
border-color: var(--text);
color: var(--base);
}
.plan-strip-approve {
position: relative;
display: flex;
}
/* Split button: flat inner corners; the caret drops its own left border and
draws a faint base-colored seam instead. (0,3,0)+ selectors so the
`.prompt-actions`/`.plan-strip-actions` shorthands can't re-round or
re-border the seam. */
.plan-strip-approve .btn-primary:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.plan-strip-actions .plan-strip-caret {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: 0 6px;
display: flex;
align-items: center;
border-left: 1px solid color-mix(in oklab, var(--base) 35%, var(--text));
}
.plan-strip-menu {
position: absolute;
right: 0;
bottom: calc(100% + 4px);
display: flex;
flex-direction: column;
min-width: 190px;
padding: 4px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: 0 6px 20px rgb(0 0 0 / 12%);
z-index: 6;
}
.plan-strip-menu button {
text-align: left;
padding: 7px 9px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
font-size: var(--fs-md);
cursor: pointer;
}
.plan-strip-menu button:hover {
background: var(--surface-2, rgb(0 0 0 / 5%));
}
/* Compact in-transcript plan card while the strip owns the actions. */
.prompt-plan.clamped {
max-height: 9.5em;
overflow: hidden;
position: relative;
}
.prompt-plan.clamped::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 34px;
background: linear-gradient(to bottom, transparent, var(--surface));
pointer-events: none;
}
.prompt-plan-open {
align-self: flex-start;
border: none;
background: transparent;
color: var(--accent-blue);
font-size: var(--fs-sm);
padding: 0;
cursor: pointer;
}
.prompt-plan-open:hover {
text-decoration: underline;
}
/* Code browser tab — project-clone / branch file tree in flow layout (NOT
.runs-table-wrap, which is position:absolute and would jump to (0,0)). */
.code-tab {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.code-tab-header {
display: flex;
align-items: center;
gap: 8px;
/* Match .file-view-header so Code ↔ file tab switches don't jump chrome. */
padding: 4px 12px;
border-bottom: 1px solid var(--border-variant);
flex-shrink: 0;
}
.code-tab-select {
max-width: 280px;
min-width: 0;
/* Compact metrics so the header's height matches .file-view-header's
* (whose tallest child is a 28px icon-btn) — the base select rule's
* 6px/14px would make Code <-> file tab switches jump the divider. */
padding: 3px 8px;
font-size: var(--fs-sm);
}
.code-tab-note {
padding: 8px 16px;
font-size: var(--fs-sm);
color: var(--muted);
border-bottom: 1px solid var(--border-variant);
flex-shrink: 0;
}
.code-tab-body {
flex: 1;
min-height: 0;
overflow: auto;
background: var(--base);
}
.code-tree {
padding: 6px 0;
font-size: var(--fs-md);
}
.code-tree-row {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 3px 10px;
border: none;
background: transparent;
color: var(--text);
text-align: left;
cursor: pointer;
font-family: inherit;
font-size: inherit;
}
.code-tree-row:hover {
background: var(--panel);
}
.code-tree-row > svg {
flex-shrink: 0;
color: var(--subtext);
}
.code-tree-row > svg.code-tree-chev {
color: var(--muted);
}
.code-tree-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ---- live worktree tab (WorktreeTab.tsx) ----
The Changes/Files viewer for a chat session's private worktree. Reuses the
.code-tab shell; adds a branch chip, a status-badged change list, and a diff
region below it. */
.wt-branch-chip {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
padding: 2px 8px;
border-radius: var(--radius-full);
background: color-mix(in oklab, var(--text) 8%, transparent);
color: var(--subtext);
font-size: var(--fs-xs);
}
.wt-branch-chip > svg {
flex-shrink: 0;
}
.wt-branch-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--mono);
}
.wt-count {
padding: 0;
border: none;
}
/* Same horizontal gutter as .drawer-body so the diff cards match the
* experiment Changes view; no padding-top for the same reason there — sticky
* diff-file headers would sit below the top with content peeking through.
* The first child carries the spacing as margin instead. */
.wt-changes {
padding: 0 16px 24px;
}
.wt-changes > :first-child {
margin-top: 14px;
}
.wt-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 48px 24px;
text-align: center;
color: var(--muted);
}
.wt-empty > svg {
color: var(--subtext);
}
.wt-empty p {
margin: 0;
max-width: 320px;
font-size: var(--fs-sm);
}
/* ---- onboarding tour ----
Spotlight walkthrough of the workspace (Tour.tsx). The overlay sits above
the z-100 modal ladder; the dim is a single oversized box-shadow around the
spotlight box so there's only one element to morph between steps. */
.tour-overlay {
position: fixed;
inset: 0;
z-index: 200;
}
.tour-dim {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
}
.tour-spotlight {
position: absolute;
border-radius: var(--radius-lg);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
transition: all 300ms ease;
}
.tour-ring {
position: absolute;
border-radius: var(--radius-lg);
border: 2px solid var(--primary);
transition: all 300ms ease;
}
.tour-card {
position: absolute;
width: 390px;
max-width: calc(100vw - 32px);
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
padding: 21px 23px 0;
transition:
left 200ms ease,
top 200ms ease;
}
.tour-card.centered {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
transition: none;
}
.tour-card h3 {
margin: 0 26px 7px 0;
font-size: var(--fs-lg);
font-weight: var(--fw-semibold);
}
.tour-card p {
margin: 0;
font-size: var(--fs-base);
line-height: 1.55;
color: var(--text);
}
.tour-card .tour-close {
position: absolute;
top: 12px;
right: 12px;
}
.tour-footer {
display: flex;
align-items: center;
border-top: 1px solid var(--border-variant);
margin-top: 16px;
padding: 11px 0;
}
.tour-footer-side {
flex: 1;
display: flex;
}
.tour-footer-side.end {
justify-content: flex-end;
}
.tour-count {
font-size: var(--fs-sm);
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* Rotated-square pointer on the card edge nearest the spotlight; the two
outward borders draw the triangle. Class names follow the card's anchor
("above" = card above the target, so the arrow is on the bottom edge). */
.tour-arrow {
position: absolute;
width: 12px;
height: 12px;
background: var(--base);
rotate: 45deg;
pointer-events: none;
}
.tour-arrow.above {
bottom: -6.5px;
left: calc(50% - 6px);
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.tour-arrow.below {
top: -6.5px;
left: calc(50% - 6px);
border-left: 1px solid var(--border);
border-top: 1px solid var(--border);
}
.tour-arrow.left {
right: -6.5px;
top: calc(50% - 6px);
border-top: 1px solid var(--border);
border-right: 1px solid var(--border);
}
.tour-arrow.right {
left: -6.5px;
top: calc(50% - 6px);
border-bottom: 1px solid var(--border);
border-left: 1px solid var(--border);
}