Files
deepseek-harness/packages/client/ui-primitives/src/TerminalBlock.module.css
T
Chinesezjc b588060b90 fix(web): close a run at the line end and count columns correctly
Eight findings, each terminal case verified in a real terminal first:

`\x1b[32mdone\rok\x1b[0m` then `plain` shows `okne` green and `plain` in the
DEFAULT color. The replay returned the last written cell's state, so a reset
landing after the final write vanished from both the text and the returned
state — and every build tool writes exactly that shape, so the color leaked
onto all later output. The replay now converges to the state the scan ended
in, which is also what it hands to the next line.

`abcd\b\x1b[1K|` shows `   |`: CSI 1K erases THROUGH the cursor column, and
the loop stopped before it. The erase mode also reads only the first
parameter now, since a terminal treats `1;2K` exactly as `1K`.

`éx\rYZ` shows `YZ`: a combining mark takes no column, so it attaches
to the cell already written instead of advancing the cursor and leaving the
`x` standing.

`中x\rA` shows `A x`: overwriting a wide character's lead cell leaves its
spacer as a blank rather than closing the gap, which would shift everything
after it one column left.

The banner lost its span when the gutter became padding — a plain block child
only reaches the content box, so the reserved column was painted in the body
color and the card's top-left radius drawn in it. Invisible in the light
theme, where banner and body share a token; visible in the dark one. The
header now pulls back across the gutter and re-insets by the same amount.

The replay trigger matches the same CSI shape the parser accepts, so a form
like `\x1b[1;2K` can no longer skip its own erase, and `replayLine`'s JSDoc
documents its new parameter and returned pair.

Docs: four places still described the dot as sitting left of the card surface,
which stopped being true when the gutter became the card's own padding, and
two fixture comments still referenced the exit marker that was deliberately
removed.
2026-07-29 16:10:37 +08:00

153 lines
4.5 KiB
CSS

/* Geometry mirrors CodeBlock (12px radius, code-block surface + banner rows,
markdown code-block font) so a terminal card and a fenced code block read as
one family. The one deliberate divergence: output keeps `white-space: pre`
and scrolls horizontally, because folding a column-aligned command's output
destroys its alignment. */
.block {
--dsl-terminal-radius: 12px;
--dsl-terminal-line-height: 22px;
/* The card's own left inset, holding the run-state dot in a column of its own
so it never competes with the commands for horizontal space. */
--dsl-terminal-gutter: 30px;
position: relative;
margin: 16px 0;
/* The gutter is the card's OWN padding, not a margin: every consumer rewrites
`margin` wholesale (each render site sets its own indent), which silently
cancelled the reservation and let the dot fall outside the card into a
container that clips it. Owning the reservation here keeps the invariant
with the component that depends on it. */
padding-left: var(--dsl-terminal-gutter);
color: var(--dsw-alias-label-primary);
background: var(--dsw-alias-markdown-code-block);
border-radius: var(--dsl-terminal-radius);
}
/* Top-aligned: the status pill and copy control stay on the first prompt row
however many command lines the card carries. */
.header {
display: flex;
align-items: flex-start;
gap: 12px;
/* Pulled back across the card's gutter padding so the banner background and
its top-left radius span the FULL surface, then re-inset by the same amount
so the prompt text and the dot keep their positions. A plain block child
only reaches the content box, which left the gutter column painted in the
body color and drew the card's top-left corner in it — invisible in the
light theme, where banner and body share a token, and visible in the dark
one, where they do not. */
margin-left: calc(-1 * var(--dsl-terminal-gutter));
padding: 9px 14px 9px var(--dsl-terminal-gutter);
background: var(--dsw-alias-markdown-code-block-banner);
border-top-left-radius: var(--dsl-terminal-radius);
border-top-right-radius: var(--dsl-terminal-radius);
}
/* One row per command line. The prompt column is the only element allowed to
shrink; the status pill and the copy control keep their intrinsic width. */
.prompt {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
font: var(--dsw-font-markdown-code-block);
}
.promptLine {
position: relative;
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
line-height: var(--dsl-terminal-line-height);
}
/* Out of flow inside the card's own gutter padding, so the reservation and the
dot move together and no consumer margin can pull them apart; the dot neither
indents its command nor depends on the command's text metrics to line up.
Centered against the row's line box, not the code font's baseline. */
.runState {
position: absolute;
left: calc(-1 * var(--dsl-terminal-gutter) + 8px);
top: 50%;
transform: translateY(-50%);
}
/* The dot is aria-hidden; this is its text label for assistive technology. */
.runStateLabel {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.cwd {
flex: none;
color: var(--dsw-alias-label-tertiary);
}
/* `pre`, not `nowrap`: the prompt row renders the command verbatim, and
`nowrap` collapses the repeated spaces, tabs, and alignment of an indented
continuation. Both hold the single row and the ellipsis. */
.command {
min-width: 0;
color: var(--dsw-alias-label-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
.status {
flex: none;
color: var(--dsw-alias-state-error-primary);
}
.copyButton {
flex: none;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
font: var(--dsw-font-xs-13);
}
.output {
padding: 12px 14px 12px 0;
font: var(--dsw-font-markdown-code-block);
overflow-x: auto;
overflow-y: hidden;
}
/* No wrapping, no word-break: alignment is the payload of terminal output. */
.line {
min-height: var(--dsl-terminal-line-height);
white-space: pre;
}
.expand {
display: block;
width: 100%;
padding: 0;
border: none;
background-color: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
font: inherit;
text-align: left;
}
.expand:hover {
color: var(--dsw-alias-label-secondary);
}
.empty {
padding: 12px 14px 12px 0;
font: var(--dsw-font-markdown-code-block);
color: var(--dsw-alias-label-tertiary);
}