Files
qwen-code/packages/web-shell/client/components/ChatEditor.module.css
T
97cb625888 fix(web-shell): keep mobile composer controls reachable above a soft keyboard (#12462)
* fix(web-shell): keep mobile composer controls reachable above a soft keyboard

Real-device testing of the touch composer on Android Chrome with Gboard
found three layout problems once the soft keyboard shrinks the viewport:

- The workspace/Git row sits inside the composer's height cap, so with the
  keyboard open the attachments strip collapsed to 16px and attached files
  could be neither seen nor removed. The cap now includes that row's height.
- History search opens upward from the composer with a fixed cap, so its
  search field and close button slid under the header. The panel is now also
  limited to the room above the composer, the same way the @ panel is.
- The editing row overlapped the workspace/Git row by 8px, so tapping the
  bottom of a workspace or branch chip recalled a history entry instead.

* fix(web-shell): keep mobile history search inside the visible chat area

Bound the history-search panel by the real clipping edge above the
composer (the chat pane under the header, or a host-declared
--web-shell-popover-safe-top) instead of a 48px guess. When not even its
96px minimum fits, the panel moves down over the composer so the search
field and close button stay below the header. Re-measurement is
coalesced to one pass per animation frame.

Measure the workspace/Git row instead of assuming 44px: with a second
workspace or a long branch it wraps onto two lines. The cap also adds
the 8px pull-up the editing row gives up below the row, and the editing
row keeps that pull-up when no workspace row is shown, as on main.

A declared zero safe top is now kept as zero; the @ reference panel
reads the variable through the same helper.

* refactor(web-shell): share the popover floor and fit the history panel imperatively

Round-2 review follow-up:

- Export POPOVER_MIN_HEIGHT from popoverRoom.ts and use it for both
  upward-opening popovers instead of two bare 96 literals.
- Derive the history panel's room/shift from its rendered height (a
  no-match panel is just the search bar) and write the two custom
  properties straight to the panel node, so per-frame composer movement
  no longer re-renders the whole composer through state.
- Observe the panel itself so a change in match count re-measures.
- Name the mobile composer cap once as --chat-editor-composer-cap and
  derive the :has(.mobileContextRow) override from it.
- Pin the ResizeObserver re-measure paths with target-tracking stubs,
  pin auto as a clipping overflow value, and add a 412x360 e2e asserting
  the workspace row never shrinks the attachments strip.
- Qualify the soft-keyboard acceptance criterion with the verified
  height and document --web-shell-popover-safe-top in the design docs
  and the web-shell README.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): focus history search after input mounts

* test(web-shell): cover history search in short desktop windows

---------

Co-authored-by: probe <probe@local>
Co-authored-by: Qwen-Coder <[email protected]>
2026-09-23 03:37:42 +00:00

2861 lines
56 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
.editorShell {
position: relative;
container-type: inline-size;
margin: 0 0 14px;
}
.editorShell[data-web-shell-compact-composer] {
width: 100%;
min-width: 0;
box-sizing: border-box;
margin-bottom: 8px;
}
.editorShellDropdownOpen {
z-index: 7;
}
.container {
position: relative;
container-type: inline-size;
border-radius: 12px;
margin: 0;
cursor: text;
--chat-editor-min-height: 116px;
--chat-editor-max-height: min(350px, 40vh);
--chat-editor-input-min-height: 44px;
--chat-editor-input-max-height: 300px;
--chat-editor-attachments-max-height: 136px;
--dac-glow-on: 0;
--dac-glow-pulse: 0;
--dac-g1: #6a78ff;
--dac-g2: #8a7bff;
--chat-send-button-disabled-foreground: var(--secondary-foreground);
--chat-send-button-foreground: #fafafa;
}
.editorShell[data-web-shell-compact-composer] .container {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.content {
position: relative;
z-index: 2;
display: flex;
box-sizing: border-box;
min-height: var(--chat-editor-min-height, 116px);
max-height: var(--chat-editor-max-height, min(350px, 40vh));
flex-direction: column;
overflow: visible;
border: 1.5px solid var(--agent-gray-200);
border-radius: inherit;
background: var(--chat-editor-bg-primary);
opacity: 1;
padding: 12px;
}
.editorShell[data-web-shell-compact-composer] .content {
padding: 10px;
}
.container:focus-within .content,
.container[data-at-panel-open] .content {
border-color: var(--chat-editor-accent-color);
}
@supports (selector(:has(*))) {
.container:has([aria-expanded='true']) .content {
border-color: var(--chat-editor-accent-color);
}
}
.container[data-image-drag-active] .content {
border-color: var(--primary);
background: color-mix(
in srgb,
var(--primary) 6%,
var(--chat-editor-bg-primary)
);
}
.dacAura,
.dacHalo {
pointer-events: none;
}
.dacAura {
position: absolute;
inset: -50px;
z-index: 0;
opacity: calc(var(--dac-glow-on) * (0.33 + var(--dac-glow-pulse) * 0.17));
-webkit-mask: radial-gradient(
ellipse 56% 64% at center,
transparent 28%,
#000 48%,
transparent 94%
);
mask: radial-gradient(
ellipse 56% 64% at center,
transparent 28%,
#000 48%,
transparent 94%
);
will-change: opacity;
}
.dacAura::before,
.dacAura::after {
position: absolute;
inset: 0;
content: '';
will-change: opacity;
}
.dacAura::before {
background: color-mix(in srgb, var(--dac-g1) 58%, transparent);
-webkit-mask:
repeating-linear-gradient(90deg, #000 0 1px, transparent 1px 5px),
repeating-linear-gradient(0deg, #000 0 1px, transparent 1px 5px);
-webkit-mask-composite: source-in;
mask:
repeating-linear-gradient(90deg, #000 0 1px, transparent 1px 5px),
repeating-linear-gradient(0deg, #000 0 1px, transparent 1px 5px);
mask-composite: intersect;
opacity: 1;
transition: opacity 0.2s ease;
}
.dacAura::after {
background: color-mix(in srgb, var(--dac-g1) 66%, transparent);
-webkit-mask:
repeating-linear-gradient(90deg, #000 0 1px, transparent 1px 5px),
repeating-linear-gradient(0deg, #000 0 1px, transparent 1px 5px),
repeating-linear-gradient(
90deg,
#000 0,
rgba(0, 0, 0, 0.08) 28px,
#000 56px
);
-webkit-mask-composite: source-in, source-in;
mask:
repeating-linear-gradient(90deg, #000 0 1px, transparent 1px 5px),
repeating-linear-gradient(0deg, #000 0 1px, transparent 1px 5px),
repeating-linear-gradient(
90deg,
#000 0,
rgba(0, 0, 0, 0.08) 28px,
#000 56px
);
mask-composite: intersect, intersect;
opacity: 0;
animation: dac-aura-cols 2.4s linear infinite paused;
transition: opacity 0.2s ease;
}
.container[data-dac-typing] .dacAura::before {
opacity: 0;
}
.container[data-dac-typing] .dacAura::after {
opacity: 1;
animation-play-state: running;
}
.dacHalo {
position: absolute;
inset: 0;
z-index: 1;
border-radius: inherit;
opacity: calc(var(--dac-glow-on) * 0.5);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--dac-g2) 36%, transparent),
0 1px 5px color-mix(in srgb, var(--dac-g1) 18%, transparent),
0 6px 30px color-mix(in srgb, var(--dac-g1) 22%, transparent);
will-change: opacity;
}
.dacHalo::after {
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 6px 40px color-mix(in srgb, var(--dac-g1) 14%, transparent);
content: '';
opacity: calc(var(--dac-glow-on) * var(--dac-glow-pulse) * 0.5);
will-change: opacity;
}
@keyframes dac-aura-cols {
from {
-webkit-mask-position:
0 0,
0 0,
0 0;
mask-position:
0 0,
0 0,
0 0;
}
to {
-webkit-mask-position:
0 0,
0 0,
56px 0;
mask-position:
0 0,
0 0,
56px 0;
}
}
@media (prefers-reduced-motion: reduce) {
.dacAura::after,
.container[data-dac-typing] .dacAura::after {
opacity: 0;
animation: none;
}
.container[data-dac-typing] .dacAura::before {
opacity: 1;
}
}
.atPortalLayer {
display: contents;
}
:global([data-web-shell-slash-menu]) {
width: 620px;
max-width: min(
calc(var(--radix-popover-trigger-width) - 32px),
var(--radix-popover-content-available-width),
calc(100vw - 24px)
);
max-height: var(--radix-popover-content-available-height);
overflow: hidden;
}
:global([data-web-shell-slash-menu][data-web-shell-compact-overlay]) {
width: min(360px, calc(100vw - 16px));
max-width: min(
var(--radix-popover-content-available-width),
calc(100vw - 16px)
);
box-sizing: border-box;
min-width: 0;
padding: 6px;
}
:global([data-web-shell-slash-detail]) {
z-index: calc(var(--web-shell-popover-z-index, 1000) + 1);
width: min(320px, calc(100vw - 24px));
max-height: min(200px, var(--radix-popover-content-available-height));
overflow: hidden;
}
:global([data-web-shell-toolbar-popover]) {
width: min(max-content, var(--radix-popover-content-available-width));
max-height: var(--radix-popover-content-available-height);
overflow: hidden;
}
:global([data-web-shell-toolbar-popover][data-web-shell-compact-overlay]) {
width: min(360px, calc(100vw - 16px));
max-width: calc(100vw - 16px);
box-sizing: border-box;
padding: 6px;
}
:global([data-web-shell-toolbar-popover][data-web-shell-compact-overlay])
.dropdownItemDesc {
display: -webkit-box;
overflow-wrap: anywhere;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
:global([data-web-shell-reasoning-popover]) {
overflow-x: hidden;
overflow-y: auto;
}
:global([data-web-shell-model-submenu]) {
z-index: calc(var(--web-shell-popover-z-index, 1000) + 1);
min-width: min(280px, var(--radix-popover-content-available-width));
}
.slashPanel {
max-height: min(
460px,
calc(var(--radix-popover-content-available-height) - 20px)
);
overflow: hidden;
cursor: default;
}
.slashPanelBody {
max-height: inherit;
overflow: hidden;
}
.slashList {
display: flex;
width: 100%;
max-height: inherit;
flex-direction: column;
gap: 2px;
overflow-x: hidden;
overflow-y: auto;
scroll-padding-bottom: 12px;
scrollbar-color: var(--chat-editor-border-color) transparent;
scrollbar-width: thin;
}
.slashList::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.slashList::-webkit-scrollbar-track {
background: transparent;
}
.slashList::-webkit-scrollbar-thumb {
border-radius: 3px;
background: var(--chat-editor-border-color);
}
.slashEntry {
display: flex;
min-width: 0;
flex-direction: column;
}
.slashSection {
height: 1px;
margin: 5px 8px;
background: var(--chat-editor-border-color);
}
.slashSectionHeader {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
padding: 4px 8px 2px;
color: var(--muted-foreground);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
user-select: none;
}
.slashSectionCount {
flex: 0 0 auto;
color: var(--muted-foreground);
font-weight: 400;
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
.slashItem {
position: relative;
display: grid;
width: 100%;
min-width: 0;
grid-template-columns:
minmax(0, 220px)
minmax(0, 1fr);
column-gap: 8px;
align-items: baseline;
padding: 5px 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--foreground);
font: inherit;
text-align: left;
cursor: pointer;
}
.slashItem:not([data-has-description]) {
grid-template-columns: minmax(0, 1fr);
column-gap: 0;
}
:global([data-web-shell-slash-menu][data-web-shell-compact-overlay])
.slashItem {
display: flex;
align-items: stretch;
flex-direction: column;
gap: 1px;
padding: 6px 8px;
}
:global([data-web-shell-slash-menu][data-web-shell-compact-overlay])
.slashDescription {
display: none;
overflow-wrap: anywhere;
text-align: left;
white-space: normal;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
:global([data-web-shell-slash-menu][data-web-shell-compact-overlay])
.slashItemActive
.slashDescription {
display: -webkit-box;
}
:global([data-web-shell-slash-menu][data-web-shell-compact-overlay])
.slashCommand {
min-width: 0;
overflow-wrap: anywhere;
text-overflow: clip;
white-space: normal;
}
:global([data-web-shell-slash-menu][data-web-shell-compact-overlay])
.slashSectionCount {
display: none;
}
.slashItem:hover,
.slashItemActive {
background: var(--accent);
}
.slashItem:hover .slashCommand,
.slashItemActive .slashCommand {
color: var(--foreground);
}
.slashDetail {
min-height: 0;
max-height: min(
180px,
max(0px, calc(var(--radix-popover-content-available-height) - 20px))
);
overflow: auto;
overflow-wrap: anywhere;
}
.slashDetailCommand {
margin-bottom: 5px;
color: var(--foreground);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 14px;
font-weight: 400;
line-height: 22px;
white-space: nowrap;
}
.slashDetailText {
color: var(--muted-foreground);
font-size: 12px;
font-weight: 400;
line-height: 20px;
white-space: pre-wrap;
}
.slashCommand {
min-width: 0;
overflow: hidden;
color: var(--foreground);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 14px;
font-weight: 400;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
.slashArgumentHint {
color: var(--muted-foreground);
font-size: 12px;
font-weight: 400;
}
.slashDescription {
min-width: 0;
overflow: hidden;
color: var(--muted-foreground);
font-size: 12px;
font-weight: 400;
line-height: 20px;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.atPanel {
position: fixed;
z-index: var(--web-shell-popover-z-index, 1000);
--at-anchor-width: 620px;
display: flex;
width: min(360px, calc(var(--at-anchor-width) - 32px), calc(100vw - 24px));
max-height: min(var(--at-panel-max-height, 300px), 42vh);
box-sizing: border-box;
flex-direction: column;
padding: 6px;
overflow: hidden;
border: 1px solid var(--chat-editor-border-color);
border-radius: 8px;
background: var(--background);
box-shadow:
0 2px 4px -2px rgba(0, 0, 0, 0.1),
0 4px 6px -1px rgba(0, 0, 0, 0.1);
color: var(--chat-editor-text-primary);
cursor: default;
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 13px;
line-height: 1.35;
}
.atPanel[data-web-shell-compact-overlay] {
width: min(360px, calc(100vw - 16px));
padding: 4px;
}
.atPanel[data-web-shell-compact-overlay] .atItemDescription {
overflow-wrap: anywhere;
white-space: normal;
}
.atPanel[data-web-shell-compact-overlay] .atItemMain {
grid-template-columns: minmax(0, 1fr);
row-gap: 1px;
}
.atPanel[data-web-shell-compact-overlay] .atItemSubtitle {
justify-self: start;
overflow-wrap: anywhere;
text-align: left;
text-overflow: clip;
white-space: normal;
}
.atPanelHeaderWrap {
flex: 0 0 auto;
padding: 3px 4px 7px;
margin-bottom: 4px;
border-bottom: 1px solid var(--chat-editor-border-color);
}
.atPanelHeader {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.atBackButton {
display: inline-flex;
width: 22px;
height: 22px;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
font: inherit;
font-size: 18px;
line-height: 1;
}
.atBackButton:hover {
background: var(--accent);
color: var(--foreground);
}
.atPanelTitle {
min-width: 0;
overflow: hidden;
color: var(--foreground);
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.atTabsWrap {
display: grid;
min-width: 0;
grid-template-columns: 22px minmax(0, 1fr) 22px;
align-items: center;
gap: 4px;
margin-top: 4px;
margin-bottom: 0;
}
.atTabs {
display: flex;
min-width: 0;
gap: 4px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
}
.atTabs::-webkit-scrollbar {
display: none;
}
.atTab,
.atTabScrollButton {
display: inline-flex;
min-width: 0;
height: 22px;
align-items: center;
justify-content: center;
padding: 0 8px;
border: 0;
border-radius: 5px;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
font: inherit;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.atTab {
flex: 0 0 auto;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
.atTabScrollButton {
width: 22px;
padding: 0;
font-size: 16px;
}
.atTab:hover,
.atTabActive,
.atTabScrollButton:hover {
background: var(--accent);
color: var(--foreground);
}
.atTab:disabled {
opacity: 0.45;
cursor: default;
}
.atSearchInput {
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 5px 7px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 6px;
outline: none;
background: transparent;
color: var(--foreground);
font: inherit;
font-size: 12px;
line-height: 18px;
}
.atSearchInput::placeholder {
color: var(--muted-foreground);
}
.atList {
display: flex;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
gap: 2px;
overflow-x: hidden;
overflow-y: auto;
border-radius: 6px;
scroll-padding-bottom: 6px;
scrollbar-color: var(--chat-editor-border-color) transparent;
scrollbar-width: thin;
}
.atList::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.atList::-webkit-scrollbar-track {
background: transparent;
}
.atList::-webkit-scrollbar-thumb {
border-radius: 3px;
background: var(--chat-editor-border-color);
}
.atItem {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-areas:
'label trailing'
'description trailing';
column-gap: 8px;
row-gap: 1px;
align-items: center;
padding: 6px 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--foreground);
font: inherit;
text-align: left;
cursor: pointer;
}
.atItem:hover,
.atItemActive {
background: var(--accent);
}
.atItemSingleLine {
grid-template-areas: 'label trailing';
padding-top: 5px;
padding-bottom: 5px;
}
.atItemMain {
grid-area: label;
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: var(--at-item-main-template, minmax(0, 1fr));
column-gap: 8px;
align-items: end;
justify-self: stretch;
}
.atItemLeading {
display: grid;
width: 100%;
min-width: 0;
grid-template-columns: auto minmax(0, 1fr);
align-items: end;
gap: 6px;
overflow: hidden;
}
.atItemIcon {
display: inline-block;
width: 16px;
height: 16px;
flex: 0 0 auto;
background: currentColor;
color: var(--muted-foreground);
mask: var(--at-item-icon-url) center / contain no-repeat;
-webkit-mask: var(--at-item-icon-url) center / contain no-repeat;
}
.atItemUploadIcon {
align-self: center;
width: 14px;
height: 14px;
color: var(--muted-foreground);
}
.atItemImageIcon {
display: block;
width: 16px;
height: 16px;
flex: 0 0 auto;
object-fit: contain;
}
.atItemIconSpin {
animation: at-item-icon-spin 1.2s linear infinite;
}
.atItemLabel,
.atItemSubtitle {
min-width: 0;
overflow: hidden;
font-size: 13px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.atItemLabel {
display: block;
width: 100%;
color: var(--foreground);
}
.atItemSubtitle {
color: var(--muted-foreground);
justify-self: end;
text-align: right;
}
@keyframes at-item-icon-spin {
to {
transform: rotate(360deg);
}
}
.atItemDescription {
grid-area: description;
min-width: 0;
overflow: hidden;
color: var(--muted-foreground);
font-size: 12px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
}
.atItemTrailing {
grid-area: trailing;
color: var(--muted-foreground);
font-size: 16px;
line-height: 1;
}
.atEmpty {
padding: 10px 8px;
color: var(--muted-foreground);
font-size: 12px;
}
.attachments {
display: flex;
min-height: 0;
max-height: var(--chat-editor-attachments-max-height, 136px);
flex: 0 1 auto;
flex-direction: column;
overflow-y: auto;
overscroll-behavior: contain;
}
.tags {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 6px;
padding: 0 0 8px;
}
.tag {
position: relative;
display: inline-flex;
align-items: center;
max-width: 100%;
min-height: 22px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 4px;
background: var(--chat-editor-bg-tertiary);
color: var(--chat-editor-text-primary);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.2;
}
.tagContent {
display: inline-flex;
min-width: 0;
max-width: 100%;
flex: 1 1 auto;
align-items: center;
}
.tagTooltip {
z-index: calc(var(--web-shell-tooltip-z-index, 1000) + 1);
box-sizing: border-box;
min-width: 160px;
max-width: min(320px, 80vw);
max-height: min(
calc(100vh - 16px),
var(--radix-tooltip-content-available-height, calc(100vh - 16px))
);
padding: 8px 10px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 6px;
background: var(--background);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
color: var(--foreground);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 12px;
line-height: 1.5;
overflow-y: auto;
overscroll-behavior: contain;
pointer-events: auto;
white-space: normal;
}
@supports (height: 100dvh) {
.tagTooltip {
max-height: min(
calc(100dvh - 16px),
var(--radix-tooltip-content-available-height, calc(100dvh - 16px))
);
}
}
.tagLabel,
.tagValue {
color: var(--chat-editor-text-primary);
}
.tagIcon {
display: block;
width: 12px;
height: 12px;
flex: 0 0 auto;
margin-left: 7px;
background: currentColor;
mask: var(--composer-tag-icon-url) center / contain no-repeat;
-webkit-mask: var(--composer-tag-icon-url) center / contain no-repeat;
}
.tagLabel,
.tagValue {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tagLabel {
padding: 3px 0 3px 7px;
color: var(--chat-editor-accent-color);
}
.tagIcon + .tagLabel {
padding-left: 0.5ch;
}
.tagValue {
max-width: 32ch;
padding: 3px 0 3px 0.5ch;
color: var(--foreground, #e4e4e4);
opacity: 1;
}
.tagRemove {
flex: 0 0 auto;
width: 22px;
height: 22px;
padding: 0;
border: 0;
background: transparent;
color: var(--chat-editor-text-dimmed);
font: inherit;
line-height: 22px;
cursor: pointer;
border-radius: 0 4px 4px 0;
}
.tagRemove:hover,
.tagRemove:focus-visible {
color: var(--error-color);
outline: none;
}
.editorArea {
display: flex;
flex: 1 1 auto;
align-items: stretch;
gap: 6px;
min-height: var(--chat-editor-input-min-height, 44px);
padding: 4px 0;
overflow: clip;
}
.editorArea > :last-child {
display: grid;
min-width: 0;
min-height: var(--chat-editor-input-min-height, 44px);
flex: 1;
overflow: clip;
}
.editorArea :global(.cm-editor) {
height: 100%;
min-height: 0;
}
.editorArea :global(.cm-scroller) {
height: 100%;
min-height: 0;
}
.shellPrefix {
flex: 0 0 auto;
align-self: flex-start;
padding-top: 1px;
color: var(--chat-editor-accent-color);
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
line-height: 1.6;
user-select: none;
pointer-events: none;
}
.editorArea .cm-content {
color: var(--foreground);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 14px;
line-height: 1.6;
}
/* Touch-device textarea backend (#5958). Mirrors the CodeMirror look; the
16px font size is mandatory: iOS Safari auto-zooms the page when focusing
any input with a smaller font. */
.mobileTextarea {
field-sizing: content;
width: 100%;
min-height: var(--chat-editor-input-min-height, 44px);
max-height: var(--chat-editor-input-max-height, 300px);
padding: 0;
border: none;
outline: none;
background: transparent;
resize: none;
color: var(--chat-editor-text-primary, #e0e0e0);
caret-color: var(--chat-editor-accent-color, #4a9eff);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 16px;
line-height: 1.6;
}
.mobileTextarea::placeholder {
color: var(--chat-editor-text-dimmed, #666);
}
/* As .editorArea's last child the textarea would inherit `overflow: clip`
from the rule above, pinning scrollTop to 0 — content beyond the height
cap would become unreachable. The extra `textarea` type wins that
specificity. */
.editorArea > textarea.mobileTextarea {
overflow-y: auto;
}
.editorArea .cm-scroller {
scrollbar-color: var(--chat-editor-border-color) transparent;
scrollbar-width: thin;
}
.editorArea .cm-scroller::-webkit-scrollbar {
width: 6px;
}
.editorArea .cm-scroller::-webkit-scrollbar-track {
background: transparent;
}
.editorArea .cm-scroller::-webkit-scrollbar-thumb {
border-radius: 3px;
background: var(--chat-editor-border-color);
}
.toolbar {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
min-width: 0;
padding: 4px 0 0;
gap: 8px;
}
.editorShell[data-web-shell-compact-composer] .toolbar {
gap: 4px;
}
/* The workspace and git chips, moved out of the toolbar into their own row
under the composer box so the composer stays about the prompt itself. The
4px inset plus the workspace chip's 8px padding lines their content up with the
editor's text. */
.contextRow {
display: flex;
min-width: 0;
align-items: center;
gap: 4px;
margin-top: -12px;
padding: 18px 4px 6px;
border-radius: 0 0 12px 12px;
background: color-mix(in srgb, var(--muted) 70%, transparent);
}
.contextRow button[data-web-shell-git-branch] {
height: 28px;
padding: 0 12px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--agent-gray-500);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 13px;
font-weight: 400;
}
.contextRow button[data-web-shell-git-branch]:hover,
.contextRow button[data-web-shell-git-branch]:focus-visible {
background: var(--chat-editor-bg-tertiary);
color: var(--chat-editor-text-primary);
}
.contextRow button[data-web-shell-git-branch] > svg {
display: none;
}
.toolbarLeading,
.toolbarStart,
.toolbarEnd,
.toolbarLeft,
.toolbarRight {
display: flex;
align-items: center;
gap: 4px;
}
.toolbarLeading {
margin-left: -5px;
min-width: 0;
}
.editorShell[data-web-shell-compact-composer] .toolbarLeading {
margin-left: 0;
}
.toolbar[data-mobile-voice-active='true'] .toolbarLeading,
.toolbar[data-mobile-voice-active='true'] [data-hide-during-mobile-voice] {
display: none;
}
.toolbar[data-mobile-voice-active='true'] .toolbarRight {
margin-left: auto;
}
.toolbarStart {
min-width: 0;
}
.toolbarEnd {
min-width: 0;
}
.toolbarLeft {
min-width: 0;
flex-wrap: nowrap;
}
.gitBranchChip {
display: inline-flex;
min-width: 0;
max-width: 180px;
height: 28px;
padding: 0 8px;
align-items: center;
gap: 5px;
border-radius: 6px;
color: var(--agent-gray-500);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 13px;
line-height: 1;
}
/* When the chip opens the Changes dialog it renders as a <button>; reset the
UA button chrome so it looks identical to the read-only <output> chip. */
.gitBranchChipButton {
appearance: none;
border: 0;
background: transparent;
cursor: pointer;
margin: 0;
}
.gitBranchChipButton:hover {
background: var(--chat-editor-bg-tertiary);
color: var(--chat-editor-text-primary);
}
.gitBranchChipButton:focus-visible {
outline: 2px solid var(--chat-editor-accent-color);
outline-offset: -2px;
}
.gitBranchIcon {
display: inline-flex;
width: var(--git-branch-icon-size, 16px);
height: var(--git-branch-icon-size, 16px);
flex: 0 0 var(--git-branch-icon-size, 16px);
}
.gitBranchIcon svg {
width: var(--git-branch-icon-size, 16px);
height: var(--git-branch-icon-size, 16px);
}
.gitBranchText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gitBranchChip[data-detached='true'] .gitBranchText {
color: var(--warning-color);
}
.gitBranchChip[data-worktree='true'] {
color: var(--color-accent-fg, #8b5cf6);
}
.gitBranchIconWrap {
position: relative;
display: inline-flex;
flex: 0 0 var(--git-branch-icon-size, 16px);
}
/* Compact (icon-only) chip: a single severity dot on the branch icon so the
working-tree state is still glanceable without the text + inline indicators. */
.gitBranchBadgeDot {
position: absolute;
top: var(--git-branch-badge-offset, -2px);
right: var(--git-branch-badge-offset, -3px);
width: var(--git-branch-badge-size, 7px);
height: var(--git-branch-badge-size, 7px);
border-radius: 50%;
background: var(--chat-editor-accent-color);
box-shadow: 0 0 0 1.5px var(--chat-editor-bg-primary, transparent);
}
.gitBranchBadgeDot[data-tone='warning'] {
background: var(--warning-color);
}
.gitBranchBadgeDot[data-tone='error'] {
background: var(--error-color);
}
.gitBranchIndicators {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 0 0 auto;
}
.gitBranchDirtyDot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--chat-editor-accent-color);
display: inline-block;
}
.gitBranchAheadBehind {
color: var(--muted-foreground);
font-variant-numeric: tabular-nums;
}
.gitBranchStash {
display: inline-flex;
align-items: center;
gap: 2px;
color: var(--muted-foreground);
font-variant-numeric: tabular-nums;
}
.gitBranchStash svg {
width: 13px;
height: 13px;
}
.gitBranchConflicted {
display: inline-flex;
align-items: center;
gap: 2px;
color: var(--error-color);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.gitBranchConflicted svg {
width: 13px;
height: 13px;
}
.gitBranchOperation {
color: var(--warning-color);
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.gitBranchTooltip {
display: flex;
flex-direction: column;
gap: 2px;
}
.gitBranchTooltipTitle {
font-weight: 600;
}
.gitBranchTooltipRow {
opacity: 0.85;
}
/* Which workspace a split-view pane's session lives in — parallel to the git
branch chip, shown only on a multi-workspace daemon. */
.workspaceChip {
display: inline-flex;
min-width: 0;
max-width: 160px;
height: 28px;
padding: 0 8px;
align-items: center;
gap: 5px;
border-radius: 6px;
color: var(--agent-gray-500);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 13px;
line-height: 1;
}
/* On a multi-workspace daemon the chip gets a stable per-workspace accent
(--ws-accent, set by a shared accent class): a faint tinted background plus a
colored folder, so panes stay distinguishable even when the chip collapses to
the icon-only compact state. */
.workspaceChipAccented {
background: color-mix(in srgb, var(--ws-accent) 12%, transparent);
}
.workspaceChipAccented .workspaceChipIcon {
color: var(--ws-accent);
}
.workspaceChipIcon {
display: inline-flex;
width: 16px;
height: 16px;
flex: 0 0 16px;
}
.workspaceChipIcon svg {
width: 16px;
height: 16px;
}
.workspaceChipText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolbarRight {
flex-shrink: 0;
margin-right: -5px;
}
.editorShell[data-web-shell-compact-composer] .toolbarRight {
gap: 0;
margin-right: 0;
}
.editorShell[data-web-shell-compact-composer]
.toolbarRight
button[data-available] {
width: 28px;
height: 28px;
border-radius: 6px;
}
.editorShell[data-web-shell-compact-composer] .toolbarRight button svg {
width: 16px;
height: 16px;
}
.toolbarRightCustom {
display: inline-flex;
min-width: 0;
align-items: center;
}
.toolbarMeasurements {
position: fixed;
top: -10000px;
left: -10000px;
display: flex;
flex-direction: column;
align-items: flex-start;
visibility: hidden;
pointer-events: none;
}
.dropdownWrapper {
position: relative;
}
.dropdownWrapperCompact {
width: 28px;
flex: 0 0 28px;
}
.dropdownList {
display: flex;
min-height: 0;
flex: 1 1 auto;
flex-direction: column;
gap: 2px;
overflow-x: hidden;
overflow-y: auto;
}
.dropdownListConstrained {
max-height: 250px;
}
.dropdownEmpty {
padding: 8px 9px;
color: var(--muted-foreground);
font-size: 13px;
line-height: 18px;
}
.dropdownItem {
display: block;
width: 100%;
padding: 6px 12px;
border: none;
background: transparent;
color: var(--foreground);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 14px;
font-weight: 400;
line-height: 22px;
text-align: left;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.dropdownRich .dropdownItem {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) 18px;
gap: 10px;
align-items: center;
padding: 6px 9px;
white-space: normal;
}
.dropdownCheck .dropdownItem {
display: grid;
grid-template-columns: minmax(0, 1fr) 18px;
gap: 10px;
align-items: center;
padding: 6px 9px;
}
.dropdownItem:hover {
background: var(--accent);
}
.dropdownItemActive {
color: var(--foreground);
background: var(--accent);
}
.dropdownItemIcon,
.dropdownItemCheck {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--secondary-foreground);
}
.dropdownItemIcon svg {
width: 19px;
height: 19px;
}
.dropdownItemCheck svg {
width: 16px;
height: 16px;
}
.dropdownItemContent {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.dropdownItemLabel {
overflow: hidden;
color: var(--foreground);
font-size: 14px;
font-weight: 400;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdownItemDesc {
overflow: hidden;
color: var(--muted-foreground);
font-size: 12px;
font-weight: 400;
line-height: 1.2;
text-overflow: ellipsis;
}
.dropdownSubmenuTrigger {
display: flex;
min-height: 34px;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.dropdownSubmenuTrigger span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.dropdownSubmenuTrigger svg {
width: 16px;
height: 16px;
flex: 0 0 auto;
color: var(--muted-foreground);
}
.dropdownSubmenuSection {
padding-top: 4px;
border-top: 1px solid var(--border);
}
.reasoningOptions {
display: flex;
flex-direction: column;
padding-bottom: 4px;
}
.reasoningSectionTitle {
padding: 4px 9px;
color: var(--muted-foreground);
font-size: 12px;
}
.reasoningThinkingRow,
.reasoningEffortRow {
display: flex;
min-height: 34px;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 9px;
}
.reasoningEffortRow {
width: 100%;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--foreground);
font: inherit;
text-align: left;
cursor: pointer;
}
.reasoningEffortRow:hover:not(:disabled) {
background: var(--accent);
}
.reasoningEffortRow:disabled {
cursor: default;
opacity: 0.45;
}
.reasoningDivider {
height: 1px;
margin: 4px 0;
background: var(--border);
}
.dropdownItemActive .dropdownItemIcon,
.dropdownItemActive .dropdownItemCheck {
color: var(--secondary-foreground);
}
.toolBtn {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
height: 28px;
padding: 0 8px;
border: none;
background: transparent;
color: var(--agent-gray-500);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 13px;
line-height: 1;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
}
.modeToolBtn,
.modelToolBtn {
min-width: 0;
}
.toolBtn:not(:disabled):not([data-disabled]):hover {
background: var(--chat-editor-bg-tertiary);
color: var(--chat-editor-text-primary);
}
.planControl {
flex: 0 0 auto;
}
.planControl[data-disabled] {
cursor: default;
}
/* Shown only while Plan is on: the state has to stay visible even though the
entry itself is tucked into the add menu. */
.planChip {
flex: 0 0 auto;
background: var(--subtle-bg-strong);
color: var(--chat-editor-text-primary);
}
/* Not the shorter `.toolBtn.planChip:hover`: this ties the generic toolbar
hover above on specificity and wins by coming later, which the shorter
selector would not. The chip is never natively disabled, so the tint does
not drop under the pointer during the request the click itself starts. */
.toolBtn.planChip:not(:disabled):hover {
background: color-mix(
in srgb,
var(--chat-editor-text-primary) 12%,
transparent
);
color: var(--chat-editor-text-primary);
}
.planChip .toolBtnText {
color: inherit;
}
/* No dimming: like every other .toolBtn, including the permission button
beside it, the chip keeps its appearance while mode controls are busy. That
state is brief, and dimming one control of the pair would flicker. */
.planChip[aria-disabled='true'] {
cursor: default;
}
/* At rest the chip shows the Plan icon and no close mark. The mark shares the
icon slot and takes it over on hover or keyboard focus, so revealing it
never changes how wide the chip is. */
.planChipIcon {
position: relative;
}
.planChipIcon > .planChipClose {
position: absolute;
inset: 0;
display: inline-flex;
width: auto;
height: auto;
align-items: center;
justify-content: center;
opacity: 0;
}
.planChipIcon > .planChipClose svg {
width: 12px;
height: 12px;
}
/* None of the reveals is limited to an enabled chip: it is inert for the
length of the request its own click starts, and swapping back under the
pointer would read as the click having been refused. */
.planChip:focus-visible .planChipIcon > :not(.planChipClose) {
opacity: 0;
}
.planChip:focus-visible .planChipClose {
opacity: 1;
}
/* Only where the pointer really hovers. A touch browser keeps :hover on the
last thing tapped, which would leave the mark stuck on a chip whose tap did
nothing. */
@media (hover: hover) {
.planChip:hover .planChipIcon > :not(.planChipClose) {
opacity: 0;
}
.planChip:hover .planChipClose {
opacity: 1;
}
}
/* Without hover only keyboard focus would ever reveal the mark, so it stays
shown — but only beside the label. A chip too narrow for its label keeps the Plan icon
instead: a bare × would name nothing, and a pressed button that a tap
releases needs no mark to work. Keyed on the primary pointer, so a
touchscreen laptop driven by a mouse still gets the quiet chip. */
@media (hover: none) {
.planChip[data-labelled] .planChipIcon > :not(.planChipClose) {
opacity: 0;
}
.planChip[data-labelled] .planChipClose {
opacity: 1;
}
}
/* Forced colours repaint a masked background away, which would leave a chip
without its label blank at rest. Painting the mask with a system colour
keeps the icon. Every mode icon has the same gap; this covers only the one
this chip depends on. */
@media (forced-colors: active) {
.planChipIcon > span:not(.planChipClose) {
forced-color-adjust: none;
background: ButtonText;
}
}
.addMenuPlanIcon {
display: inline-flex;
width: 16px;
height: 16px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
color: var(--muted-foreground);
}
.workspaceSelectTrigger {
min-width: 0;
max-width: 176px;
align-items: center;
box-shadow: none;
}
.workspaceSelectTrigger[data-disabled] {
cursor: default;
opacity: 1;
}
.workspaceSelectTrigger > svg {
display: block;
flex-shrink: 0;
align-self: center;
}
.workspaceSelectTooltipTrigger {
display: inline-flex;
min-width: 0;
max-width: 176px;
}
.workspaceSelectTrigger [data-slot='select-value'] {
display: block;
min-width: 0;
flex: 1 1 auto;
align-self: center;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gitBranchChipCompact,
.workspaceChipCompact,
.workspaceSelectTriggerCompact,
.toolBtnCompact {
width: 28px;
max-width: 28px;
flex: 0 0 28px;
gap: 0;
padding: 0 6px;
justify-content: center;
box-sizing: border-box;
}
.workspaceSelectTooltipTriggerCompact {
width: 28px;
max-width: 28px;
flex: 0 0 28px;
}
.gitBranchChipCompact .gitBranchText,
.workspaceChipCompact .workspaceChipText,
.workspaceSelectTriggerCompact [data-slot='select-value'],
.workspaceSelectTriggerCompact > svg:last-child,
.workspaceSelectTriggerCompact .toolBtnText,
.workspaceSelectTriggerCompact .toolBtnArrow,
.toolBtnCompact .toolBtnText,
.toolBtnCompact .toolBtnArrow {
display: none;
}
.toolBtn[data-tooltip]:hover::after,
.toolBtn[data-tooltip]:focus-visible::after {
position: absolute;
right: 50%;
bottom: calc(100% + 8px);
z-index: 120;
padding: 4px 7px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 6px;
background: var(--background);
color: var(--chat-editor-text-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);
content: attr(data-tooltip);
font-family: var(--font-sans, system-ui, sans-serif);
font-size: 12px;
line-height: 1.2;
pointer-events: none;
transform: translateX(50%);
white-space: nowrap;
}
.toolBtnIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 16px;
line-height: 1;
}
.toolBtnIcon svg {
display: block;
width: 16px;
height: 16px;
}
.widthModeBtn {
flex-shrink: 0;
}
.addMenuBtn {
width: 32px;
height: 32px;
flex-shrink: 0;
justify-content: center;
padding: 0;
border-radius: 50%;
}
.addMenuBtn .toolBtnIcon svg {
width: 18px;
height: 18px;
}
.contextUsageBtn {
flex-shrink: 0;
padding: 0 4px;
}
.contextUsagePercentage {
display: none;
font-variant-numeric: tabular-nums;
}
.contextUsagePercentage[data-level='normal'] {
color: var(--chat-editor-text-secondary);
}
.contextUsagePercentage[data-level='warning'] {
color: var(--warning-color);
}
.contextUsagePercentage[data-level='error'] {
color: var(--error-color);
}
.contextTooltip {
--muted-foreground: var(--chat-editor-text-secondary);
--destructive: var(--error-color);
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
width: 280px;
max-width: calc(100vw - 24px);
padding: 12px;
white-space: normal;
}
.contextTooltipHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-weight: 600;
}
.contextTooltipHeader strong {
font-size: 18px;
font-variant-numeric: tabular-nums;
}
.contextTooltipMeter {
height: 6px;
overflow: hidden;
border-radius: 3px;
background: var(--border);
}
.contextTooltipMeter > span {
display: block;
height: 100%;
background: var(--agent-blue-500);
}
.contextTooltipMeter > span[data-level='warning'] {
background: var(--warning-color);
}
.contextTooltipMeter > span[data-level='error'] {
background: var(--error-color);
}
.contextTooltipStats {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 6px 12px;
margin: 0;
}
.contextTooltipStats dt,
.contextTooltipHint {
color: var(--chat-editor-text-secondary);
}
.contextTooltipStats dd {
margin: 0;
text-align: right;
font-variant-numeric: tabular-nums;
}
.contextTooltipHint {
padding-top: 8px;
border-top: 1px solid var(--border);
}
@supports (container-type: inline-size) {
@container (min-width: 521px) {
.contextUsagePercentage {
display: inline;
}
}
}
.contextRingTrack {
stroke: var(--border);
}
.contextRingValue {
stroke: var(--agent-blue-500);
transition: stroke-dashoffset 0.25s ease;
}
.contextRingValueWarning {
stroke: var(--warning-color);
}
.contextRingValueError {
stroke: var(--error-color);
}
@media (prefers-reduced-motion: reduce) {
.contextRingValue {
transition: none;
}
}
.widthModeBtn .toolBtnIcon svg {
width: 18px;
height: 18px;
}
.toolBtnModeIcon,
.toolBtnModelIcon {
display: inline-flex;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
}
.toolBtnModeIcon svg,
.toolBtnModelIcon svg {
display: block;
width: 16px;
height: 16px;
}
.toolBtnModeIcon > span {
width: 14px;
height: 14px;
}
.toolBtnText {
min-width: 0;
overflow: hidden;
display: inline-flex;
align-items: center;
color: var(--agent-gray-500);
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.toolBtnArrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: var(--muted-foreground);
}
.toolBtnArrow svg {
display: block;
width: 16px;
height: 16px;
}
.sendBtn {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 8px;
border: none;
background: var(--agent-gray-200);
color: var(--chat-send-button-disabled-foreground);
cursor: default;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin-left: 4px;
transition: all 0.2s ease;
}
.sendBtn:disabled {
opacity: 1;
cursor: not-allowed;
}
.sendBtn:not(:disabled) {
border: none;
background: var(--agent-gray-800);
box-shadow: var(--agent-purple-shadow);
color: var(--chat-send-button-foreground);
cursor: pointer;
}
.sendBtn:not(:disabled):hover {
filter: brightness(1.08);
}
.sendBtn:not(:disabled):active {
transform: scale(0.96);
}
.sendBtnRunning,
.sendBtnRunning:not(:disabled) {
border: none;
background: var(--agent-gray-800);
box-shadow: none;
color: var(--chat-send-button-foreground);
cursor: pointer;
}
.sendBtnRunning:not(:disabled):hover {
filter: brightness(1.08);
}
.sendIcon {
display: inline-block;
width: 16px;
height: 16px;
}
.stopIcon {
width: 10px;
height: 10px;
border-radius: 3px;
background: currentColor;
}
.loadingIcon {
width: 14px;
height: 14px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: sendBtnLoadingSpin 0.8s linear infinite;
}
/* Armed-to-cancel state: first Esc primes the stop, the button shows an "Esc"
hint, a subtle pulse, and a depleting ring counting down the confirm window
until the second press confirms or the window lapses. */
.sendBtnArmed,
.sendBtnArmed:not(:disabled) {
position: relative;
border-radius: 50%;
background: var(--error-color, #e06c75);
animation: sendBtnArmedPulse 1s ease-in-out infinite;
}
/* Registered so the conic angle can animate smoothly. */
@property --esc-countdown {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
/* Countdown ring around the armed button. Its duration matches
ESC_CANCEL_CONFIRM_WINDOW_MS in App.tsx — keep the two in sync. */
.sendBtnArmed::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
background: conic-gradient(
var(--error-color, #e06c75) var(--esc-countdown),
transparent 0
);
-webkit-mask: radial-gradient(
farthest-side,
transparent calc(100% - 2px),
#000 calc(100% - 2px)
);
mask: radial-gradient(
farthest-side,
transparent calc(100% - 2px),
#000 calc(100% - 2px)
);
animation: escCountdown 2000ms linear forwards;
pointer-events: none;
}
@keyframes escCountdown {
to {
--esc-countdown: 0%;
}
}
@media (prefers-reduced-motion: reduce) {
.loadingIcon {
animation: none;
}
.sendBtnArmed,
.sendBtnArmed:not(:disabled) {
animation: none;
}
.sendBtnArmed::after {
animation: none;
--esc-countdown: 100%;
}
}
@keyframes sendBtnLoadingSpin {
to {
transform: rotate(360deg);
}
}
.escLabel {
font-size: 11px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.02em;
}
/* Visually hidden but exposed to assistive tech (for aria-live announcements). */
.srOnly {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@keyframes sendBtnArmedPulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.images {
position: relative;
z-index: 2;
display: flex;
flex: 0 0 auto;
gap: 6px;
padding: 0 0 8px;
flex-wrap: wrap;
}
.imageThumb {
position: relative;
width: 48px;
height: 48px;
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--chat-editor-border-color);
}
.imageThumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.imageRemove,
.fileChipRemove {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border: none;
cursor: pointer;
border-radius: 50%;
padding: 0;
opacity: 0;
transition:
opacity 0.15s ease,
background 0.15s ease;
}
.imageRemove {
position: absolute;
top: 2px;
right: 2px;
}
.imageRemove svg,
.fileChipRemove svg {
display: block;
}
.imageThumb:hover .imageRemove:not(:disabled),
.fileChip:hover .fileChipRemove:not(:disabled),
.imageRemove:focus-visible,
.fileChipRemove:focus-visible {
opacity: 1;
}
.imageRemove:not(:disabled):hover,
.fileChipRemove:not(:disabled):hover {
background: rgba(0, 0, 0, 0.8);
}
.imageRemove:disabled,
.fileChipRemove:disabled {
cursor: default;
opacity: 0;
}
.files {
display: flex;
flex: 0 0 auto;
gap: 6px;
padding: 4px 0 0;
flex-wrap: wrap;
}
/* Two classes so this wins over `.fileChip`'s fixed width, which is declared
later in this file: a text card sizes to its own content. */
.fileChip.fileChipText {
width: auto;
min-width: min(264px, 100%);
max-width: min(100%, 360px);
align-items: flex-start;
gap: 12px;
}
.fileChipIcon {
display: flex;
flex: 0 0 48px;
width: 48px;
height: 48px;
align-items: center;
justify-content: center;
border-radius: 12px;
background: var(--muted);
color: var(--muted-foreground);
}
.fileChipBody {
display: flex;
flex: 1 1 auto;
min-width: 0;
flex-direction: column;
gap: 3px;
text-align: left;
}
.fileChipTitle {
overflow: hidden;
min-width: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--foreground);
cursor: pointer;
font: inherit;
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.fileChipTitle:disabled {
cursor: default;
}
.fileChipTitle:not(:disabled):hover {
text-decoration: underline;
}
.fileChipTitle:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.fileChipMeta {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
color: var(--muted-foreground);
font-size: 12px;
line-height: 18px;
white-space: nowrap;
/* The row is nowrap, so at the narrowest panes let the size drop to its own
line rather than escaping the card. */
flex-wrap: wrap;
}
.fileChipExpand {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 2px;
padding: 0;
border: 0;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
font: inherit;
font-size: 12px;
line-height: 18px;
}
.fileChipExpand:not(:disabled):hover {
color: var(--foreground);
text-decoration: underline;
}
.fileChipExpand:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.fileChipExpand:disabled {
cursor: default;
opacity: 0.5;
}
.fileChip {
position: relative;
display: inline-flex;
align-items: center;
box-sizing: border-box;
width: 272px;
max-width: 100%;
padding: 8px 32px 8px 8px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 16px;
color: var(--chat-editor-text-primary);
}
.fileChipPreviewable {
cursor: pointer;
}
.fileChipPreview:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.fileChipPreview {
display: inline-flex;
min-width: 0;
width: 100%;
align-items: center;
gap: 12px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
cursor: inherit;
font: inherit;
}
.fileChipPreview:disabled {
cursor: default;
}
.fileChipRemove {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
width: 20px;
height: 20px;
opacity: 1;
pointer-events: auto;
}
.searchPanel {
display: flex;
flex-direction: column;
position: absolute;
right: 0;
bottom: calc(100% + 8px - var(--chat-editor-search-shift, 0px));
left: 0;
z-index: 20;
max-height: min(320px, 50vh, var(--chat-editor-search-room, 320px));
padding: 8px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 12px;
background: var(--background);
box-shadow: 0 8px 24px color-mix(in srgb, #000 12%, transparent);
cursor: default;
}
.searchBar {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 2px 6px;
font-size: 12px;
}
.searchLabel {
color: var(--chat-editor-text-dimmed);
flex-shrink: 0;
}
.searchInput {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--chat-editor-text-primary);
font-family: var(--font-mono);
font-size: 12px;
}
.searchInput::placeholder {
color: var(--chat-editor-text-dimmed);
}
.searchResults {
display: flex;
flex-direction: column;
gap: 2px;
min-height: 0;
padding: 2px 0 0;
overflow-y: auto;
overscroll-behavior: contain;
}
.searchResult {
display: grid;
grid-template-columns: 12px minmax(0, 1fr);
align-items: center;
gap: 8px;
width: 100%;
flex: none;
height: 30px;
padding: 5px 8px;
overflow: hidden;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--chat-editor-text-secondary);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.4;
text-align: left;
white-space: nowrap;
cursor: pointer;
}
.searchResult:hover,
.searchResultActive {
background: var(--chat-editor-bg-tertiary);
color: var(--chat-editor-accent-color);
}
.searchResultMarker {
color: var(--chat-editor-accent-color);
line-height: 1.45;
}
.searchResultText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.searchEmpty {
padding: 4px 2px 0;
color: var(--chat-editor-text-dimmed);
font-size: 12px;
}
/* File upload ----------------------------------------------------------- */
.hiddenUploadInput {
display: none;
}
.uploadStrip {
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 0 10px;
}
.uploadRow {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
box-sizing: border-box;
width: 100%;
max-width: 100%;
padding: 8px;
border: 1px solid var(--chat-editor-border-color);
border-radius: 10px;
background: var(--chat-editor-bg-primary);
color: var(--chat-editor-text-primary);
font-size: 12px;
line-height: 1.4;
}
.uploadRow[data-status='error'] {
border-color: var(--error-border);
background: var(--error-bg);
}
.uploadRow[data-previewable='true'] {
cursor: pointer;
}
.uploadRowPreview:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.uploadRowPreview {
display: flex;
min-width: 0;
flex: 1 1 auto;
align-items: center;
gap: 10px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
cursor: inherit;
font: inherit;
}
.uploadRowIcon {
flex: 0 0 auto;
}
.uploadRowName {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.uploadRowPreview:disabled {
cursor: default;
}
.uploadRowAction svg,
.uploadRowSpinner {
flex: 0 0 auto;
width: 14px;
height: 14px;
}
.uploadRowSpinner {
color: var(--agent-blue-500);
animation: uploadRowSpin 1s linear infinite;
}
/* Must follow the base rule: equal-specificity cascade resolves to the
last declaration, so an earlier override would stay dead code. */
@media (prefers-reduced-motion: reduce) {
.uploadRowSpinner {
animation: none;
}
}
@keyframes uploadRowSpin {
to {
transform: rotate(360deg);
}
}
.uploadRowStatus {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
max-width: 40%;
flex: 0 0 auto;
overflow: hidden;
color: var(--chat-editor-text-dimmed);
}
.uploadRowStatusText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.uploadRow[data-status='error'] .uploadRowStatus {
color: var(--error-color);
}
.uploadRowAction {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 20px;
height: 20px;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: var(--chat-editor-text-dimmed);
cursor: pointer;
}
.uploadRowAction:hover {
background: var(--chat-editor-border-color);
color: var(--chat-editor-text-primary);
}
.uploadDropOverlay {
position: absolute;
inset: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 2px dashed var(--chat-editor-accent-color);
border-radius: 8px;
background: color-mix(
in srgb,
var(--chat-editor-accent-color) 12%,
transparent
);
color: var(--chat-editor-text-primary);
font-size: 13px;
pointer-events: none;
}
.fileTag {
background: var(--chat-editor-bg-primary);
min-height: 28px;
border-radius: 8px;
font-family: var(--font-sans, system-ui, sans-serif);
}
.fileTagIcon {
flex: 0 0 auto;
margin-left: 8px;
color: var(--muted-foreground);
}
.editorShell[data-web-shell-mobile-composer] .container {
--chat-editor-composer-cap: min(480px, 60dvh);
--chat-editor-max-height: var(--chat-editor-composer-cap);
}
/* The workspace/Git row lives inside the capped box; without its height added
back, a soft keyboard leaves the attachments strip almost no room. The
height is measured because the row wraps on narrow viewports, and the 8px
is the pull-up the editing row gives up below it. */
.editorShell[data-web-shell-mobile-composer] .container:has(.mobileContextRow) {
--chat-editor-max-height: calc(
var(--chat-editor-composer-cap) +
var(--chat-editor-context-row-height, 44px) + 8px
);
}
.editorShell[data-web-shell-mobile-composer] .uploadRowAction,
.editorShell[data-web-shell-mobile-composer] .toolBtn,
.editorShell[data-web-shell-mobile-composer] .sendBtn,
.editorShell[data-web-shell-mobile-composer] .toolbarRight button,
.editorShell[data-web-shell-mobile-composer] .mobileContextRow button,
.editorShell[data-web-shell-mobile-composer] .dropdownWrapperCompact {
min-width: 44px;
height: 44px;
flex-shrink: 0;
}
.editorShell[data-web-shell-mobile-composer] .toolbar,
.editorShell[data-web-shell-mobile-composer] .toolbarLeft {
flex-wrap: wrap;
}
.editorShell[data-web-shell-mobile-composer] .toolbarLeading {
max-width: 100%;
margin-left: 0;
}
.editorShell[data-web-shell-mobile-composer] .toolbarRight {
margin-left: auto;
margin-right: 0;
}
.mobileEditingActions,
.mobileContextRow,
.mobileExpandedHeader {
display: flex;
align-items: center;
gap: 4px;
}
.mobileContextRow {
flex-wrap: wrap;
}
.mobileContextRow > *,
.mobileContextRow [data-web-shell-git-branch] {
max-width: 100%;
}
.editorShell[data-web-shell-mobile-composer] .mobileContextRow button {
max-width: 100%;
flex-shrink: 1;
}
.mobileEditingActions {
flex-wrap: wrap;
margin-top: -8px;
}
/* Pulled up under the context row it would cover the row's touch targets. */
.mobileContextRow + .mobileEditingActions {
margin-top: 0;
}
.mobileEditingActions svg {
width: 18px;
height: 18px;
}
.mobileEditingSpacer {
flex: 1;
}
.mobileExpandedEditor {
display: flex;
width: calc(100% - 16px);
max-width: 720px;
height: 85dvh;
max-height: 100%;
flex-direction: column;
padding: 12px;
gap: 8px;
}
.mobileExpandedHeader h2 {
flex: 1;
}
.mobileExpandedTextarea {
flex: 1;
min-height: 0;
width: 100%;
resize: none;
border: 0;
outline: none;
background: transparent;
color: inherit;
font: inherit;
font-size: 16px;
line-height: 1.5;
}
.editorShell[data-web-shell-mobile-composer] .uploadRowPreview {
min-height: 44px;
}
.editorShell[data-web-shell-mobile-composer] .searchInput {
min-height: 44px;
font-size: 16px;
}
.editorShell[data-web-shell-mobile-composer] .searchResult {
min-height: 44px;
}