Skip to content

Commit 0298c45

Browse files
Cyberforkercodex
andcommitted
保留生成态最新用户输入框
Bug修复: - 修复旧 active plan 导致最新用户输入框被 live 折叠隐藏的问题 - 调整 live 卡片折叠逻辑,优先保留最近的用户问题 优化: - 增加终端生成态用户输入框可见性的回归测试 Co-Authored-By: Codex <noreply@openai.com>
1 parent a383282 commit 0298c45

2 files changed

Lines changed: 96 additions & 9 deletions

File tree

src/cli/ui/layout/CardStream.tsx

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const LIVE_CARD_TAIL_LIMIT = 4;
1414
export type CardStreamItem<T> =
1515
| { kind: "spacer"; rows: number; key: string }
1616
| { kind: "card"; card: T };
17+
type LiveRenderItem = { kind: "fold"; count: number; key: string } | { kind: "card"; card: Card };
1718

1819
/** Decide which cards render live vs collapse into a spacer, given the cached
1920
* heights and the current viewport position. Window is quantized to
@@ -67,8 +68,7 @@ export function CardStream({
6768
suppressLive && live.length > 0 && !isFullySettled(live[live.length - 1]!)
6869
? live.slice(0, -1)
6970
: live;
70-
const hiddenLive = Math.max(0, visibleLive.length - LIVE_CARD_TAIL_LIMIT);
71-
const liveTail = hiddenLive > 0 ? visibleLive.slice(-LIVE_CARD_TAIL_LIMIT) : visibleLive;
71+
const liveItems = selectLiveRenderItems(visibleLive);
7272

7373
return (
7474
<>
@@ -79,25 +79,59 @@ export function CardStream({
7979
</Box>
8080
)}
8181
</Static>
82-
{liveTail.length > 0 ? (
82+
{liveItems.length > 0 ? (
8383
<Box
8484
flexDirection="column"
8585
flexShrink={1}
8686
maxHeight={maxRows !== undefined ? Math.max(1, maxRows) : undefined}
8787
overflow="hidden"
8888
>
89-
{hiddenLive > 0 ? <LiveFoldHint count={hiddenLive} /> : null}
90-
{liveTail.map((card) => (
91-
<Box key={card.id} flexDirection="column">
92-
<CardRenderer card={card} compact={isFullySettled(card)} />
93-
</Box>
94-
))}
89+
{liveItems.map((item) =>
90+
item.kind === "fold" ? (
91+
<LiveFoldHint key={item.key} count={item.count} />
92+
) : (
93+
<Box key={item.card.id} flexDirection="column">
94+
<CardRenderer card={item.card} compact={isFullySettled(item.card)} />
95+
</Box>
96+
),
97+
)}
9598
</Box>
9699
) : null}
97100
</>
98101
);
99102
}
100103

104+
function selectLiveRenderItems(cards: ReadonlyArray<Card>): LiveRenderItem[] {
105+
if (cards.length <= LIVE_CARD_TAIL_LIMIT) return cards.map((card) => ({ kind: "card", card }));
106+
107+
const tailStart = Math.max(0, cards.length - LIVE_CARD_TAIL_LIMIT);
108+
const lastUserIndex = findLastIndex(cards, (card) => card.kind === "user");
109+
110+
if (lastUserIndex >= 0 && lastUserIndex < tailStart) {
111+
const items: LiveRenderItem[] = [];
112+
if (lastUserIndex > 0)
113+
items.push({ kind: "fold", count: lastUserIndex, key: "fold-before-user" });
114+
items.push({ kind: "card", card: cards[lastUserIndex]! });
115+
const hiddenBetween = tailStart - lastUserIndex - 1;
116+
if (hiddenBetween > 0)
117+
items.push({ kind: "fold", count: hiddenBetween, key: "fold-after-user" });
118+
for (const card of cards.slice(tailStart)) items.push({ kind: "card", card });
119+
return items;
120+
}
121+
122+
return [
123+
{ kind: "fold", count: tailStart, key: "fold-live-head" },
124+
...cards.slice(tailStart).map((card) => ({ kind: "card" as const, card })),
125+
];
126+
}
127+
128+
function findLastIndex<T>(items: ReadonlyArray<T>, predicate: (item: T) => boolean): number {
129+
for (let i = items.length - 1; i >= 0; i--) {
130+
if (predicate(items[i]!)) return i;
131+
}
132+
return -1;
133+
}
134+
101135
function LiveFoldHint({ count }: { count: number }): React.ReactElement {
102136
return (
103137
<Box paddingLeft={2}>

tests/ui-codex-style.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,59 @@ describe("Codex-style terminal surface", () => {
364364
expect(out).not.toContain("verbose body from tool 5");
365365
});
366366

367+
it("keeps the latest user prompt visible when an older active plan keeps the turn live", () => {
368+
setLanguageRuntime("en");
369+
const liveCards: Card[] = [
370+
{
371+
kind: "plan",
372+
id: "plan-active",
373+
ts: 1,
374+
title: "Teams MVP",
375+
variant: "active",
376+
steps: [
377+
{ id: "s1", title: "done step", status: "done" },
378+
{ id: "s2", title: "queued step", status: "queued" },
379+
],
380+
},
381+
{
382+
kind: "user",
383+
id: "user-latest",
384+
ts: 2,
385+
text: "teams这些的操作有没有help或者文档之类的说明?",
386+
},
387+
...Array.from({ length: 6 }, (_, i) => ({
388+
kind: "tool" as const,
389+
id: `tool-after-user-${i}`,
390+
ts: i + 3,
391+
name: "run_command",
392+
args: { command: `echo ${i}` },
393+
output: `verbose output ${i}`,
394+
done: true,
395+
exitCode: 0,
396+
elapsedMs: 20,
397+
})),
398+
{
399+
kind: "streaming",
400+
id: "reply-live",
401+
ts: 20,
402+
text: "全部通过。现在 Teams 有三层帮助。",
403+
done: false,
404+
userPrompt: "teams这些的操作有没有help或者文档之类的说明?",
405+
},
406+
];
407+
const { lastFrame, unmount } = render(
408+
<AgentStoreProvider session={SESSION} initialCards={liveCards}>
409+
<CardStream maxRows={18} />
410+
</AgentStoreProvider>,
411+
);
412+
const out = lastFrame() ?? "";
413+
unmount();
414+
415+
expect(out).toContain("> teams这些的操作有没有help或者文档之类的说明?");
416+
expect(out).toContain("+");
417+
expect(out).toContain("全部通过。现在 Teams 有三层帮助。");
418+
});
419+
367420
it("keeps failed shell summaries ahead of tail output", () => {
368421
setLanguageRuntime("zh-CN");
369422
const { lastFrame, unmount } = render(

0 commit comments

Comments
 (0)