fix(web): replay cursor movements the way a terminal paints them
Carriage return and backspace only MOVE the cursor; neither erases. Both of my earlier approximations were wrong, and I checked each case against a real terminal rather than reasoning about it: `100%\rOK` shows `OK0%`, not `OK` — the redraw is shorter than the frame beneath it, so the tail stands. `abc\b` still shows `abc`, not `ab` — a trailing backspace has nothing to overwrite. `\x1b[31mgone\rkept` paints `kept` RED, because a carriage return does not reset the graphic state, which one of my own tests had asserted the opposite of. Both now replay into a per-line column buffer with SGR state stamped per column, as a terminal stores it per cell. That gives the partial-overwrite case its real result too: red `bad`, three backspaces, then `ok` shows `okd` with the `d` still red, since `ok` reached only two of the three cells. The presenter description now also renders at every site. An expanded row draws it itself — the collapsed summary is hidden while open, so otherwise the description was visible only collapsed, the opposite of "above the card" — and the details panel draws it above the card as well. Three of my own tests encoded the wrong semantics and were corrected with their behavior, and the emit loop's gap-filling arm was removed as unreachable: `\r` and backspace only move left, so no column can be unwritten.
This commit is contained in:
@@ -182,6 +182,14 @@ button.leading {
|
||||
also replaces each primitive's own standalone vertical spacing with the
|
||||
flow's row rhythm. */
|
||||
.codeBody,
|
||||
/* Indented to the terminal body's own column, so the description reads as the
|
||||
card's heading rather than as another summary row. */
|
||||
.terminalDescription {
|
||||
margin: 4px 0 0 22px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xs-13);
|
||||
}
|
||||
|
||||
.terminalBody {
|
||||
margin: 4px 0 4px 22px;
|
||||
}
|
||||
|
||||
@@ -156,6 +156,12 @@ export function ToolRow({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* The terminal presenter's description belongs ABOVE the card per the
|
||||
render-intent contract, so an expanded terminal row keeps showing it
|
||||
even though the collapsed summary is hidden while open. */}
|
||||
{open && terminalBody?.description !== undefined && (
|
||||
<div className={css.terminalDescription}>{terminalBody.description}</div>
|
||||
)}
|
||||
{open && (terminalBody !== null
|
||||
? <TerminalBlock {...terminalBody.card} maxLines={CHAT_TERMINAL_MAX_LINES} className={css.terminalBody} />
|
||||
: variant === 'code'
|
||||
|
||||
@@ -95,6 +95,14 @@
|
||||
|
||||
/* The terminal card sits directly under its section label, so it drops the
|
||||
primitive's standalone vertical margin; the section owns the spacing. */
|
||||
/* Above the card, which is where the render-intent contract puts a terminal
|
||||
call's description; the panel has no summary row to carry it. */
|
||||
.terminalDescription {
|
||||
margin: 0 0 6px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xs-13);
|
||||
}
|
||||
|
||||
.terminal {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,18 @@ export function DetailsPanel({ useSession, useSessions, sessionId, useStore, clo
|
||||
*/
|
||||
function OutputBody({ material, cwd }: { material: CallMaterial; cwd: string | undefined }) {
|
||||
const terminal = terminalCardModel(material.block, cwd)
|
||||
if (terminal !== null) return <TerminalBlock {...terminal.card} className={css.terminal} />
|
||||
if (terminal !== null) {
|
||||
// The contract renders the presenter's description above the card, and the
|
||||
// panel has no summary row to carry it, so it is drawn here.
|
||||
return (
|
||||
<>
|
||||
{terminal.description !== undefined && (
|
||||
<div className={css.terminalDescription}>{terminal.description}</div>
|
||||
)}
|
||||
<TerminalBlock {...terminal.card} className={css.terminal} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
// A settled call always carries the result node the flattened form needs;
|
||||
// the running shape has no result to flatten.
|
||||
if (!('kind' in material.block)) return <div className={css.empty}>运行中…</div>
|
||||
|
||||
Reference in New Issue
Block a user