mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 02:46:29 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7602b968 |
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import type { TurnTiming } from "@/timeline/turn-time";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import {
|
||||
collectAssistantTurnContentForStreamRenderStrategy,
|
||||
orderHeadForStreamRenderStrategy,
|
||||
orderTailForStreamRenderStrategy,
|
||||
type StreamStrategy,
|
||||
@@ -310,6 +311,34 @@ describe("layoutStream", () => {
|
||||
expect(footerOwners(layout)).toEqual([assistant.id]);
|
||||
});
|
||||
|
||||
it.each(["web", "android"] as const)(
|
||||
"copies every assistant block when a completed turn spans history and live head on %s",
|
||||
(platform) => {
|
||||
const strategy = strategyFor(platform);
|
||||
const layout = layoutFor({
|
||||
platform,
|
||||
tail: [
|
||||
userMessage("u1", 1),
|
||||
assistantMessage("first block", 2, { groupId: "turn-1", index: 0 }),
|
||||
assistantMessage("second block", 3, { groupId: "turn-1", index: 1 }),
|
||||
],
|
||||
head: [assistantMessage("final block", 4, { groupId: "turn-1", index: 2 })],
|
||||
});
|
||||
const footer = layout.auxiliaryTurnFooter;
|
||||
|
||||
expect(footer).not.toBeNull();
|
||||
expect(
|
||||
collectAssistantTurnContentForStreamRenderStrategy({
|
||||
strategy,
|
||||
items: footer!.items,
|
||||
startIndex: footer!.startIndex,
|
||||
precedingItems: footer!.precedingItems,
|
||||
precedingStartIndex: footer!.precedingStartIndex,
|
||||
}),
|
||||
).toBe("first block\n\nsecond block\n\nfinal block");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["web", "android"] as const)(
|
||||
"places inline footer after trailing visible tool rows before the next user on %s",
|
||||
(platform) => {
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface TurnFooterHost {
|
||||
items: StreamItem[];
|
||||
timing?: TurnTiming;
|
||||
startIndex: number;
|
||||
precedingItems: StreamItem[] | null;
|
||||
precedingStartIndex: number | null;
|
||||
}
|
||||
|
||||
export interface StreamLayoutItem {
|
||||
@@ -66,12 +68,16 @@ function createTurnFooterHost(input: {
|
||||
items: StreamItem[];
|
||||
index: number;
|
||||
timingByAssistantId: Map<string, TurnTiming>;
|
||||
precedingItems?: StreamItem[] | null;
|
||||
precedingStartIndex?: number | null;
|
||||
}): TurnFooterHost {
|
||||
return {
|
||||
itemId: input.item.id,
|
||||
items: input.items,
|
||||
timing: input.timingByAssistantId.get(input.item.id),
|
||||
startIndex: input.index,
|
||||
precedingItems: input.precedingItems ?? null,
|
||||
precedingStartIndex: input.precedingStartIndex ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,6 +147,11 @@ function resolveAuxiliaryTurnFooter(input: StreamLayoutInput): TurnFooterHost |
|
||||
items: assistant.items,
|
||||
index: assistant.index,
|
||||
timingByAssistantId: input.timingByAssistantId,
|
||||
precedingItems: footerItems === input.liveHead ? input.history : null,
|
||||
precedingStartIndex:
|
||||
footerItems === input.liveHead
|
||||
? input.strategy.getHistoryLiveBoundaryIndex(input.history)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,6 +185,8 @@ function resolveCompletedFooter(input: {
|
||||
items: assistant.items,
|
||||
index: assistant.index,
|
||||
timingByAssistantId: input.timingByAssistantId,
|
||||
precedingItems: input.boundaryAboveItems,
|
||||
precedingStartIndex: input.boundaryAboveIndex,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -272,8 +272,25 @@ export function collectAssistantTurnContentForStreamRenderStrategy(params: {
|
||||
strategy: StreamStrategy;
|
||||
items: StreamItem[];
|
||||
startIndex: number;
|
||||
precedingItems?: StreamItem[] | null;
|
||||
precedingStartIndex?: number | null;
|
||||
}): string {
|
||||
return params.strategy.collectAssistantTurnContent(params.items, params.startIndex);
|
||||
const content = params.strategy.collectAssistantTurnContent(params.items, params.startIndex);
|
||||
if (!params.precedingItems || params.precedingStartIndex == null) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const precedingContent = params.strategy.collectAssistantTurnContent(
|
||||
params.precedingItems,
|
||||
params.precedingStartIndex,
|
||||
);
|
||||
if (!precedingContent) {
|
||||
return content;
|
||||
}
|
||||
if (!content) {
|
||||
return precedingContent;
|
||||
}
|
||||
return `${precedingContent}\n\n${content}`;
|
||||
}
|
||||
|
||||
export function isNearBottomForStreamRenderStrategy(
|
||||
|
||||
@@ -3,8 +3,6 @@ import { View } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import type { TurnTiming } from "@/timeline/turn-time";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import {
|
||||
collectAssistantTurnContentForStreamRenderStrategy,
|
||||
type StreamStrategy,
|
||||
@@ -62,9 +60,7 @@ export const TurnFooter = memo(function TurnFooter({
|
||||
return (
|
||||
<CompletedTurnFooterRow
|
||||
strategy={strategy}
|
||||
items={host.items}
|
||||
timing={host.timing}
|
||||
startIndex={host.startIndex}
|
||||
host={host}
|
||||
supportsTimelineCursor={supportsTimelineCursor}
|
||||
onForkAssistantTurn={onForkAssistantTurn}
|
||||
/>
|
||||
@@ -73,16 +69,12 @@ export const TurnFooter = memo(function TurnFooter({
|
||||
|
||||
export const CompletedTurnFooterRow = memo(function CompletedTurnFooterRow({
|
||||
strategy,
|
||||
items,
|
||||
timing,
|
||||
startIndex,
|
||||
host,
|
||||
supportsTimelineCursor,
|
||||
onForkAssistantTurn,
|
||||
}: {
|
||||
strategy: TurnContentStrategy;
|
||||
items: StreamItem[];
|
||||
timing?: TurnTiming;
|
||||
startIndex: number;
|
||||
host: TurnFooterHost;
|
||||
supportsTimelineCursor: boolean;
|
||||
onForkAssistantTurn?: AssistantTurnForkHandler;
|
||||
}) {
|
||||
@@ -90,9 +82,7 @@ export const CompletedTurnFooterRow = memo(function CompletedTurnFooterRow({
|
||||
<TurnFooterRow>
|
||||
<CompletedTurnFooter
|
||||
strategy={strategy}
|
||||
items={items}
|
||||
timing={timing}
|
||||
startIndex={startIndex}
|
||||
host={host}
|
||||
supportsTimelineCursor={supportsTimelineCursor}
|
||||
onForkAssistantTurn={onForkAssistantTurn}
|
||||
/>
|
||||
@@ -133,16 +123,12 @@ function RunningTurnFooter({ inFlightTurnStartedAt }: { inFlightTurnStartedAt: D
|
||||
|
||||
function CompletedTurnFooter({
|
||||
strategy,
|
||||
items,
|
||||
timing,
|
||||
startIndex,
|
||||
host,
|
||||
supportsTimelineCursor,
|
||||
onForkAssistantTurn,
|
||||
}: {
|
||||
strategy: TurnContentStrategy;
|
||||
items: StreamItem[];
|
||||
timing?: TurnTiming;
|
||||
startIndex: number;
|
||||
host: TurnFooterHost;
|
||||
supportsTimelineCursor: boolean;
|
||||
onForkAssistantTurn?: AssistantTurnForkHandler;
|
||||
}) {
|
||||
@@ -150,14 +136,16 @@ function CompletedTurnFooter({
|
||||
() =>
|
||||
collectAssistantTurnContentForStreamRenderStrategy({
|
||||
strategy,
|
||||
items,
|
||||
startIndex,
|
||||
items: host.items,
|
||||
startIndex: host.startIndex,
|
||||
precedingItems: host.precedingItems,
|
||||
precedingStartIndex: host.precedingStartIndex,
|
||||
}),
|
||||
[strategy, items, startIndex],
|
||||
[strategy, host],
|
||||
);
|
||||
const boundary = resolveAssistantTurnForkBoundary({
|
||||
items,
|
||||
startIndex,
|
||||
items: host.items,
|
||||
startIndex: host.startIndex,
|
||||
supportsTimelineCursor,
|
||||
});
|
||||
const handleFork = useCallback(
|
||||
@@ -173,8 +161,8 @@ function CompletedTurnFooter({
|
||||
<View style={stylesheet.turnFooterSlot}>
|
||||
<AssistantTurnFooter
|
||||
getContent={getContent}
|
||||
completedAt={timing?.completedAt}
|
||||
durationMs={timing?.durationMs}
|
||||
completedAt={host.timing?.completedAt}
|
||||
durationMs={host.timing?.durationMs}
|
||||
onFork={boundary && onForkAssistantTurn ? handleFork : undefined}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -153,9 +153,7 @@ function renderStreamItemWithTurnFooter(input: {
|
||||
const footer = footerHost ? (
|
||||
<CompletedTurnFooterRow
|
||||
strategy={input.strategy}
|
||||
items={footerHost.items}
|
||||
timing={footerHost.timing}
|
||||
startIndex={footerHost.startIndex}
|
||||
host={footerHost}
|
||||
supportsTimelineCursor={input.supportsTimelineCursor}
|
||||
onForkAssistantTurn={input.onForkAssistantTurn}
|
||||
/>
|
||||
|
||||
@@ -7,20 +7,6 @@ function cells(text: string): TerminalState["grid"][number] {
|
||||
}
|
||||
|
||||
describe("renderTerminalSnapshotToAnsi", () => {
|
||||
it("preserves adjacent wide characters", () => {
|
||||
const state: TerminalState = {
|
||||
rows: 1,
|
||||
cols: 4,
|
||||
scrollback: [],
|
||||
grid: [[{ char: "\u4e2d" }, { char: "" }, { char: "\u6587" }, { char: "" }]],
|
||||
cursor: { row: 0, col: 4 },
|
||||
};
|
||||
|
||||
const ansi = renderTerminalSnapshotToAnsi(state);
|
||||
|
||||
expect(ansi).toContain("\u4e2d\u6587");
|
||||
});
|
||||
|
||||
it("renders soft-wrapped rows as one contiguous logical line when wrap flags are present", () => {
|
||||
// The server soft-wrapped one logical line "ABCDEFGHIJKLMNOP" at 10 cols into
|
||||
// two grid rows. gridWrapped[0] = true marks row 0 as continuing into row 1.
|
||||
|
||||
@@ -87,15 +87,12 @@ function renderTerminalRow(row: TerminalCell[], padToCols?: number): string {
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const cell = row[index] ?? { char: " " };
|
||||
if (cell.char === "") {
|
||||
continue;
|
||||
}
|
||||
const nextStyle = getTerminalStyle(cell);
|
||||
if (!terminalStylesEqual(previousStyle, nextStyle)) {
|
||||
output.push(styleToAnsi(nextStyle));
|
||||
previousStyle = nextStyle;
|
||||
}
|
||||
output.push(cell.char);
|
||||
output.push(cell.char || " ");
|
||||
}
|
||||
|
||||
if (!terminalStylesEqual(previousStyle, DEFAULT_STYLE)) {
|
||||
|
||||
@@ -1626,29 +1626,6 @@ describe("capture", () => {
|
||||
expect(capture.totalLines).toBeGreaterThan(0);
|
||||
}, 15000);
|
||||
|
||||
test("preserves adjacent CJK characters in captured output", async () => {
|
||||
const cwd = tmpCwd();
|
||||
tempDirs.push(cwd);
|
||||
const created = await createTerminalInWorkspace(ctx.client, { cwd });
|
||||
const terminalId = created.terminal!.id;
|
||||
|
||||
await ctx.client.subscribeTerminal(terminalId);
|
||||
ctx.client.sendTerminalInput(terminalId, {
|
||||
type: "input",
|
||||
data: `node -e "process.stdout.write('START\\u4e2d\\u6587END\\n')"\r`,
|
||||
});
|
||||
await waitForTerminalOutput(
|
||||
ctx.client,
|
||||
terminalId,
|
||||
(text) => text.includes("START\u4e2d\u6587END"),
|
||||
15000,
|
||||
);
|
||||
|
||||
const capture = await ctx.client.captureTerminal(terminalId);
|
||||
|
||||
expect(capture.lines).toContain("START\u4e2d\u6587END");
|
||||
}, 15000);
|
||||
|
||||
test("captures with start/end line range", async () => {
|
||||
const cwd = tmpCwd();
|
||||
tempDirs.push(cwd);
|
||||
|
||||
@@ -494,7 +494,7 @@ function extractCell(terminal: TerminalType, row: number, col: number): Terminal
|
||||
const bg = bgMode !== 0 ? cell.getBgColor() : undefined;
|
||||
|
||||
return {
|
||||
char: cell.getWidth() === 0 ? "" : cell.getChars() || " ",
|
||||
char: cell.getChars() || " ",
|
||||
fg,
|
||||
bg,
|
||||
fgMode: fgMode !== 0 ? fgMode : undefined,
|
||||
@@ -553,7 +553,7 @@ function extractScrollback(
|
||||
const fg = fgMode !== 0 ? cell.getFgColor() : undefined;
|
||||
const bg = bgMode !== 0 ? cell.getBgColor() : undefined;
|
||||
rowCells.push({
|
||||
char: cell.getWidth() === 0 ? "" : cell.getChars() || " ",
|
||||
char: cell.getChars() || " ",
|
||||
fg,
|
||||
bg,
|
||||
fgMode: fgMode !== 0 ? fgMode : undefined,
|
||||
|
||||
Reference in New Issue
Block a user