Fix TUI redraw artifacts in row-based views (#325)

Co-authored-by: pr0ln <pr0ln@pr0lnui-Macmini.local>
This commit is contained in:
pr0ln
2026-04-04 18:19:31 +09:00
committed by GitHub
parent c3c60b7bab
commit 3c2e80a1ae
8 changed files with 55 additions and 31 deletions

View File

@@ -9,6 +9,7 @@ import { formatDuration, formatNumber } from '../../utils/format.js';
import { toInkColor } from '../../utils/ink.js'; import { toInkColor } from '../../utils/ink.js';
import type { Theme } from '../../utils/theme.js'; import type { Theme } from '../../utils/theme.js';
import { Byline } from '../design-system/Byline.js'; import { Byline } from '../design-system/Byline.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
import { GlimmerMessage } from './GlimmerMessage.js'; import { GlimmerMessage } from './GlimmerMessage.js';
import { SpinnerGlyph } from './SpinnerGlyph.js'; import { SpinnerGlyph } from './SpinnerGlyph.js';
import type { SpinnerMode } from './types.js'; import type { SpinnerMode } from './types.js';
@@ -223,11 +224,13 @@ export function SpinnerAnimationRow({
<Byline>{parts}</Byline> <Byline>{parts}</Byline>
<Text dimColor>)</Text> <Text dimColor>)</Text>
</> : null; </> : null;
return <Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1} width="100%"> return <FullWidthRow>
<SpinnerGlyph frame={frame} messageColor={messageColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} reducedMotion={reducedMotion} time={time} /> <Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1}>
<GlimmerMessage message={message} mode={mode} messageColor={messageColor} glimmerIndex={glimmerIndex} flashOpacity={flashOpacity} shimmerColor={shimmerColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} /> <SpinnerGlyph frame={frame} messageColor={messageColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} reducedMotion={reducedMotion} time={time} />
{status} <GlimmerMessage message={message} mode={mode} messageColor={messageColor} glimmerIndex={glimmerIndex} flashOpacity={flashOpacity} shimmerColor={shimmerColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} />
</Box>; {status}
</Box>
</FullWidthRow>;
} }
function SpinnerModeGlyph(t0) { function SpinnerModeGlyph(t0) {
const $ = _c(2); const $ = _c(2);

View File

@@ -13,6 +13,7 @@ import { summarizeRecentActivities } from '../utils/collapseReadSearch.js';
import { truncateToWidth } from '../utils/format.js'; import { truncateToWidth } from '../utils/format.js';
import { isTodoV2Enabled, type Task } from '../utils/tasks.js'; import { isTodoV2Enabled, type Task } from '../utils/tasks.js';
import type { Theme } from '../utils/theme.js'; import type { Theme } from '../utils/theme.js';
import FullWidthRow from './design-system/FullWidthRow.js';
import ThemedText from './design-system/ThemedText.js'; import ThemedText from './design-system/ThemedText.js';
type Props = { type Props = {
tasks: Task[]; tasks: Task[];
@@ -186,11 +187,11 @@ export function TaskListV2({
} }
const content = <> const content = <>
{visibleTasks.map(task_0 => <TaskItem key={task_0.id} task={task_0} ownerColor={task_0.owner ? teammateColors[task_0.owner] : undefined} openBlockers={task_0.blockedBy.filter(id_3 => unresolvedTaskIds.has(id_3))} activity={task_0.owner ? teammateActivity[task_0.owner] : undefined} ownerActive={task_0.owner ? activeTeammates.has(task_0.owner) : false} columns={columns} />)} {visibleTasks.map(task_0 => <TaskItem key={task_0.id} task={task_0} ownerColor={task_0.owner ? teammateColors[task_0.owner] : undefined} openBlockers={task_0.blockedBy.filter(id_3 => unresolvedTaskIds.has(id_3))} activity={task_0.owner ? teammateActivity[task_0.owner] : undefined} ownerActive={task_0.owner ? activeTeammates.has(task_0.owner) : false} columns={columns} />)}
{maxDisplay > 0 && hiddenSummary && <Text dimColor>{hiddenSummary}</Text>} {maxDisplay > 0 && hiddenSummary && <FullWidthRow><Text dimColor>{hiddenSummary}</Text></FullWidthRow>}
</>; </>;
if (isStandalone) { if (isStandalone) {
return <Box flexDirection="column" marginTop={1} marginLeft={2}> return <Box flexDirection="column" marginTop={1} marginLeft={2} width="100%">
<Box> <Box width="100%">
<Text dimColor> <Text dimColor>
<Text bold>{tasks.length}</Text> <Text bold>{tasks.length}</Text>
{' tasks ('} {' tasks ('}
@@ -207,7 +208,7 @@ export function TaskListV2({
{content} {content}
</Box>; </Box>;
} }
return <Box flexDirection="column">{content}</Box>; return <Box flexDirection="column" width="100%">{content}</Box>;
} }
type TaskItemProps = { type TaskItemProps = {
task: Task; task: Task;
@@ -340,7 +341,7 @@ function TaskItem(t0) {
} }
let t10; let t10;
if ($[26] !== t5 || $[27] !== t7 || $[28] !== t8 || $[29] !== t9) { if ($[26] !== t5 || $[27] !== t7 || $[28] !== t8 || $[29] !== t9) {
t10 = <Box>{t5}{t7}{t8}{t9}</Box>; t10 = <FullWidthRow>{t5}{t7}{t8}{t9}</FullWidthRow>;
$[26] = t5; $[26] = t5;
$[27] = t7; $[27] = t7;
$[28] = t8; $[28] = t8;
@@ -351,7 +352,7 @@ function TaskItem(t0) {
} }
let t11; let t11;
if ($[31] !== displayActivity || $[32] !== showActivity) { if ($[31] !== displayActivity || $[32] !== showActivity) {
t11 = showActivity && displayActivity && <Box><Text dimColor={true}>{" "}{displayActivity}{figures.ellipsis}</Text></Box>; t11 = showActivity && displayActivity && <FullWidthRow><Text dimColor={true}>{" "}{displayActivity}{figures.ellipsis}</Text></FullWidthRow>;
$[31] = displayActivity; $[31] = displayActivity;
$[32] = showActivity; $[32] = showActivity;
$[33] = t11; $[33] = t11;
@@ -360,7 +361,7 @@ function TaskItem(t0) {
} }
let t12; let t12;
if ($[34] !== t10 || $[35] !== t11) { if ($[34] !== t10 || $[35] !== t11) {
t12 = <Box flexDirection="column">{t10}{t11}</Box>; t12 = <Box flexDirection="column" width="100%">{t10}{t11}</Box>;
$[34] = t10; $[34] = t10;
$[35] = t11; $[35] = t11;
$[36] = t12; $[36] = t12;

View File

@@ -6,6 +6,7 @@ import { useKeybinding } from '../../keybindings/useKeybinding.js';
import type { Theme } from '../../utils/theme.js'; import type { Theme } from '../../utils/theme.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Byline } from './Byline.js'; import { Byline } from './Byline.js';
import FullWidthRow from './FullWidthRow.js';
import { KeyboardShortcutHint } from './KeyboardShortcutHint.js'; import { KeyboardShortcutHint } from './KeyboardShortcutHint.js';
import { Pane } from './Pane.js'; import { Pane } from './Pane.js';
type DialogProps = { type DialogProps = {
@@ -102,7 +103,7 @@ export function Dialog(t0) {
} }
let t9; let t9;
if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) { if ($[16] !== defaultInputGuide || $[17] !== exitState || $[18] !== hideInputGuide || $[19] !== inputGuide) {
t9 = !hideInputGuide && <Box marginTop={1}><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></Box>; t9 = !hideInputGuide && <Box marginTop={1}><FullWidthRow><Text dimColor={true} italic={true}>{inputGuide ? inputGuide(exitState) : defaultInputGuide}</Text></FullWidthRow></Box>;
$[16] = defaultInputGuide; $[16] = defaultInputGuide;
$[17] = exitState; $[17] = exitState;
$[18] = hideInputGuide; $[18] = hideInputGuide;

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import { Box } from '../../ink.js';
type Props = {
children: React.ReactNode;
};
export default function FullWidthRow({
children
}: Props): React.ReactNode {
return <Box flexDirection="row" width="100%">
{children}
<Box flexGrow={1} />
</Box>;
}

View File

@@ -24,6 +24,7 @@ import { BLACK_CIRCLE } from '../../constants/figures.js';
import { TeammateMessageContent } from './UserTeammateMessage.js'; import { TeammateMessageContent } from './UserTeammateMessage.js';
import { isShutdownApproved } from '../../utils/teammateMailbox.js'; import { isShutdownApproved } from '../../utils/teammateMailbox.js';
import { CtrlOToExpand } from '../CtrlOToExpand.js'; import { CtrlOToExpand } from '../CtrlOToExpand.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
import { FilePathLink } from '../FilePathLink.js'; import { FilePathLink } from '../FilePathLink.js';
import { feature } from 'bun:bundle'; import { feature } from 'bun:bundle';
import { useSelectedMessageBg } from '../messageActions.js'; import { useSelectedMessageBg } from '../messageActions.js';
@@ -514,7 +515,7 @@ function Line(t0) {
const bg = useSelectedMessageBg(); const bg = useSelectedMessageBg();
let t2; let t2;
if ($[0] !== children || $[1] !== color || $[2] !== dimColor) { if ($[0] !== children || $[1] !== color || $[2] !== dimColor) {
t2 = <MessageResponse><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></MessageResponse>; t2 = <MessageResponse><FullWidthRow><Text color={color} dimColor={dimColor} wrap="wrap">{children}</Text></FullWidthRow></MessageResponse>;
$[0] = children; $[0] = children;
$[1] = color; $[1] = color;
$[2] = dimColor; $[2] = dimColor;

View File

@@ -5,6 +5,7 @@ import { NO_CONTENT_MESSAGE } from '../../constants/messages.js';
import { Box, Text } from '../../ink.js'; import { Box, Text } from '../../ink.js';
import { extractTag } from '../../utils/messages.js'; import { extractTag } from '../../utils/messages.js';
import { Markdown } from '../Markdown.js'; import { Markdown } from '../Markdown.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
import { MessageResponse } from '../MessageResponse.js'; import { MessageResponse } from '../MessageResponse.js';
type Props = { type Props = {
content: string; content: string;
@@ -77,7 +78,7 @@ function IndentedContent(t0) {
} }
let t2; let t2;
if ($[3] !== children) { if ($[3] !== children) {
t2 = <Box flexDirection="row">{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></Box>; t2 = <FullWidthRow>{t1}<Box flexDirection="column" flexGrow={1}><Markdown>{children}</Markdown></Box></FullWidthRow>;
$[3] = children; $[3] = children;
$[4] = t2; $[4] = t2;
} else { } else {
@@ -147,7 +148,7 @@ function CloudLaunchContent(t0) {
} }
let t6; let t6;
if ($[14] !== rest) { if ($[14] !== rest) {
t6 = rest && <Box flexDirection="row"><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></Box>; t6 = rest && <FullWidthRow><Text dimColor={true}>{" \u23BF "}</Text><Text dimColor={true}>{rest}</Text></FullWidthRow>;
$[14] = rest; $[14] = rest;
$[15] = t6; $[15] = t6;
} else { } else {

View File

@@ -11,6 +11,7 @@ import { getSettingSourceName, type SettingSource } from '../../utils/settings/c
import { plural } from '../../utils/stringUtils.js'; import { plural } from '../../utils/stringUtils.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'; import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Dialog } from '../design-system/Dialog.js'; import { Dialog } from '../design-system/Dialog.js';
import FullWidthRow from '../design-system/FullWidthRow.js';
// Skills are always PromptCommands with CommandBase properties // Skills are always PromptCommands with CommandBase properties
type SkillCommand = CommandBase & PromptCommand; type SkillCommand = CommandBase & PromptCommand;
@@ -105,14 +106,14 @@ export function SkillsMenu(t0) {
if (skills.length === 0) { if (skills.length === 0) {
let t3; let t3;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) { if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text>; t3 = <FullWidthRow><Text dimColor={true}>Create skills in .claude/skills/ or ~/.claude/skills/</Text></FullWidthRow>;
$[6] = t3; $[6] = t3;
} else { } else {
t3 = $[6]; t3 = $[6];
} }
let t4; let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) { if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>; t4 = <FullWidthRow><Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text></FullWidthRow>;
$[7] = t4; $[7] = t4;
} else { } else {
t4 = $[7]; t4 = $[7];
@@ -137,7 +138,7 @@ export function SkillsMenu(t0) {
} }
const title = getSourceTitle(source_0); const title = getSourceTitle(source_0);
const subtitle = getSourceSubtitle(source_0, groupSkills); const subtitle = getSourceSubtitle(source_0, groupSkills);
return <Box flexDirection="column" key={source_0}><Box><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</Box>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>; return <Box flexDirection="column" key={source_0}><FullWidthRow><Text bold={true} dimColor={true}>{title}</Text>{subtitle && <Text dimColor={true}> ({subtitle})</Text>}</FullWidthRow>{groupSkills.map(skill_1 => renderSkill(skill_1))}</Box>;
}; };
$[10] = skillsBySource; $[10] = skillsBySource;
$[11] = t3; $[11] = t3;
@@ -209,7 +210,7 @@ export function SkillsMenu(t0) {
} }
let t13; let t13;
if ($[30] === Symbol.for("react.memo_cache_sentinel")) { if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
t13 = <Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text>; t13 = <FullWidthRow><Text dimColor={true} italic={true}><ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="close" /></Text></FullWidthRow>;
$[30] = t13; $[30] = t13;
} else { } else {
t13 = $[30]; t13 = $[30];
@@ -230,7 +231,7 @@ function _temp3(skill_0) {
const estimatedTokens = estimateSkillFrontmatterTokens(skill_0); const estimatedTokens = estimateSkillFrontmatterTokens(skill_0);
const tokenDisplay = `~${formatTokens(estimatedTokens)}`; const tokenDisplay = `~${formatTokens(estimatedTokens)}`;
const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined; const pluginName = skill_0.source === "plugin" ? skill_0.pluginInfo?.pluginManifest.name : undefined;
return <Box key={`${skill_0.name}-${skill_0.source}`}><Text>{getSkillListLabel(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></Box>; return <FullWidthRow key={`${skill_0.name}-${skill_0.source}`}><Text>{getSkillListLabel(skill_0)}</Text><Text dimColor={true}>{pluginName ? ` · ${pluginName}` : ""} · {tokenDisplay} description tokens</Text></FullWidthRow>;
} }
function _temp2(a, b) { function _temp2(a, b) {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);

View File

@@ -1,6 +1,7 @@
import { c as _c } from "react-compiler-runtime"; import { c as _c } from "react-compiler-runtime";
import React from 'react'; import React from 'react';
import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js'; import { removeSandboxViolationTags } from 'src/utils/sandbox/sandbox-ui-utils.js';
import FullWidthRow from '../../components/design-system/FullWidthRow.js';
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'; import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
import { MessageResponse } from '../../components/MessageResponse.js'; import { MessageResponse } from '../../components/MessageResponse.js';
import { OutputLine } from '../../components/shell/OutputLine.js'; import { OutputLine } from '../../components/shell/OutputLine.js';
@@ -100,7 +101,7 @@ export default function BashToolResultMessage(t0) {
if (isImage) { if (isImage) {
let t8; let t8;
if ($[11] === Symbol.for("react.memo_cache_sentinel")) { if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
t8 = <MessageResponse height={1}><Text dimColor={true}>[Image data detected and sent to Claude]</Text></MessageResponse>; t8 = <MessageResponse height={1}><FullWidthRow><Text dimColor={true}>[Image data detected and sent to Claude]</Text></FullWidthRow></MessageResponse>;
$[11] = t8; $[11] = t8;
} else { } else {
t8 = $[11]; t8 = $[11];
@@ -145,7 +146,7 @@ export default function BashToolResultMessage(t0) {
} }
let t8; let t8;
if ($[15] !== cwdResetWarning) { if ($[15] !== cwdResetWarning) {
t8 = cwdResetWarning ? <MessageResponse><Text dimColor={true}>{cwdResetWarning}</Text></MessageResponse> : null; t8 = cwdResetWarning ? <MessageResponse><FullWidthRow><Text dimColor={true}>{cwdResetWarning}</Text></FullWidthRow></MessageResponse> : null;
$[15] = cwdResetWarning; $[15] = cwdResetWarning;
$[16] = t8; $[16] = t8;
} else { } else {
@@ -153,7 +154,7 @@ export default function BashToolResultMessage(t0) {
} }
let t9; let t9;
if ($[17] !== backgroundTaskId || $[18] !== cwdResetWarning || $[19] !== noOutputExpected || $[20] !== returnCodeInterpretation || $[21] !== stderr || $[22] !== stdout) { if ($[17] !== backgroundTaskId || $[18] !== cwdResetWarning || $[19] !== noOutputExpected || $[20] !== returnCodeInterpretation || $[21] !== stderr || $[22] !== stdout) {
t9 = stdout === "" && stderr.trim() === "" && !cwdResetWarning ? <MessageResponse height={1}><Text dimColor={true}>{backgroundTaskId ? <>Running in the background{" "}<KeyboardShortcutHint shortcut={"\u2193"} action="manage" parens={true} /></> : returnCodeInterpretation || (noOutputExpected ? "Done" : "(No output)")}</Text></MessageResponse> : null; t9 = stdout === "" && stderr.trim() === "" && !cwdResetWarning ? <MessageResponse height={1}><FullWidthRow><Text dimColor={true}>{backgroundTaskId ? <>Running in the background{" "}<KeyboardShortcutHint shortcut={"\u2193"} action="manage" parens={true} /></> : returnCodeInterpretation || (noOutputExpected ? "Done" : "(No output)")}</Text></FullWidthRow></MessageResponse> : null;
$[17] = backgroundTaskId; $[17] = backgroundTaskId;
$[18] = cwdResetWarning; $[18] = cwdResetWarning;
$[19] = noOutputExpected; $[19] = noOutputExpected;
@@ -174,7 +175,7 @@ export default function BashToolResultMessage(t0) {
} }
let t11; let t11;
if ($[26] !== T0 || $[27] !== t10 || $[28] !== t4 || $[29] !== t5 || $[30] !== t6 || $[31] !== t8 || $[32] !== t9) { if ($[26] !== T0 || $[27] !== t10 || $[28] !== t4 || $[29] !== t5 || $[30] !== t6 || $[31] !== t8 || $[32] !== t9) {
t11 = <T0 flexDirection={t4}>{t5}{t6}{t8}{t9}{t10}</T0>; t11 = <T0 flexDirection={t4} width="100%">{t5}{t6}{t8}{t9}{t10}</T0>;
$[26] = T0; $[26] = T0;
$[27] = t10; $[27] = t10;
$[28] = t4; $[28] = t4;