Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
73bb817
perf(desktop): bound rendering within oversized turns
liugddx Aug 30, 2026
d393e70
fix(ui): release live tail before geometry changes
liugddx Aug 30, 2026
c0f6631
test(desktop): enforce oversized turn performance gate
liugddx Aug 30, 2026
895cb38
fix(ui): release live tail for focus navigation
liugddx Aug 31, 2026
d12b061
fix(ui): preserve live tail for visible focus
liugddx Aug 31, 2026
3d26257
test(desktop): target visible focus growth race
liugddx Aug 31, 2026
af3cebf
fix(ui): classify focus before browser reveal
liugddx Aug 31, 2026
24a2094
test(desktop): assert focused card position
liugddx Aug 31, 2026
efe3acb
perf(desktop): give tall transcript blocks a realistic intrinsic-size…
liugddx Sep 2, 2026
4fc19d6
fix(ui): address oversized-turn review feedback
liugddx Sep 2, 2026
51d17cf
Merge remote-tracking branch 'upstream/main' into review/pr4259-fixes
liugddx Sep 2, 2026
ffc698b
test(desktop): tolerate transient fixture cleanup locks
liugddx Sep 2, 2026
214c442
fix(desktop): reach the production PageUp path, drop dead scroll indi…
liugddx Sep 2, 2026
615bcce
test(desktop): pin the visible-Tab keep-tail contract under pending g…
liugddx Sep 2, 2026
267ebd8
fix(ui): keep scroll authority test doubles compatible
liugddx Sep 3, 2026
5f70b2c
test(desktop): focus real controls in oversized turn regression
liugddx Sep 3, 2026
1d4c637
test(desktop): wait for tool group geometry to settle
liugddx Sep 3, 2026
0fa6a8d
test(desktop): target visible controls in expanded tool card
liugddx Sep 4, 2026
49f8d5c
test(desktop): choose actually visible transcript control
liugddx Sep 4, 2026
496f201
fix(ui): release the transcript pin from position, drop input enumera…
liugddx Sep 4, 2026
c3ad913
fix(ui): release the transcript pin on upward reader motion, not dist…
liugddx Sep 4, 2026
e0a9665
Merge remote-tracking branch 'upstream/main' into HEAD
liugddx Sep 4, 2026
c033dbb
fix(ui): let the transcript pin own overflow anchoring for #4269
liugddx Sep 4, 2026
8f4842f
Merge remote-tracking branch 'upstream/main' into HEAD
liugddx Sep 4, 2026
ba8f91b
fix(ui): keep tail-follow from asking for earlier history
liugddx Sep 5, 2026
54fa258
Merge upstream/main: adopt its landed #4269 scroll fix, keep only #42…
liugddx Sep 5, 2026
b0e8d81
test(desktop): address containment review — anchoring CSS, reasoning,…
liugddx Sep 5, 2026
81ff492
test(desktop): measure the cold-scroll anchor in viewport space
liugddx Sep 5, 2026
bfee6e1
test(desktop): step the cold-scroll story instantly, log per-step geo…
liugddx Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions apps/desktop/e2e/fixtures.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -483,7 +483,9 @@ async function withE2eWindow(
});
let page: Page;
try {
page = await app.firstWindow();
// Parallel CI workers and cold Windows hosts can finish process launch
// before the first BrowserWindow crosses Playwright's 30s default.
page = await app.firstWindow({ timeout: 60_000 });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
const logs = mainLogs.length > 0 ? `\nElectron main console:\n${mainLogs.join('\n')}` : '';
Expand DownExpand Up@@ -514,7 +516,12 @@ async function withE2eWindow(
try {
if (app) await closeElectronApplication(app, 5_000);
} finally {
await rm(userDataDir, { recursive: true, force: true });
await rm(userDataDir, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
});
}
}
}
Expand DownExpand Up@@ -573,6 +580,7 @@ type E2eTestFixtures = {
promptRailWindow: Page;
threadSearchWindow: Page;
partialHistoryWindow: Page;
oversizedTurnWindow: Page;
requestHeaderRowWindow: Page;
permissionCenterWindow: Page;
newTaskTargetWindow: Page;
Expand DownExpand Up@@ -777,6 +785,17 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
showWindow: true,
}, use);
},
// One Turn larger than the transcript byte budget. Shown because the test
// reads Chromium's actual content-visibility state while crossing it.
oversizedTurnWindow: async ({}, use) => {
await withE2eWindow({
seed: false,
readinessSelector: '[data-turn-id="turn-oversized-fixture"]',
e2eFixtureScenario: 'chat-oversized-turn',
locale: 'zh',
showWindow: true,
}, use);
},
// Settings → 模型, where `no-models` is the seeded openai-compatible relay —
// the connection type whose detail page owns the custom request headers
// editor. Shown, because what this window is for is a rendered box
Expand Down
52 changes: 52 additions & 0 deletions apps/desktop/e2e/oversized-turn-render.spec.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { expect, test } from './fixtures';

const SEGMENT = '[data-maka-transcript-boundary]';

test('an oversized single Turn skips offscreen timeline blocks', async ({
oversizedTurnWindow: page,
}) => {
await page.setViewportSize({ width: 900, height: 700 });
const segments = page.locator(SEGMENT);
await expect(segments).not.toHaveCount(0);
expect(await segments.count()).toBeGreaterThan(80);

const state = await segments.evaluateAll((elements) => {
const rows = elements as HTMLElement[];
return {
automatic: rows.filter((element) =>
getComputedStyle(element).contentVisibility === 'auto').length,
skipped: rows.filter((element) =>
!element.checkVisibility({ contentVisibilityAuto: true })).length,
};
});
expect(state.automatic).toBe(await segments.count());
expect(state.skipped).toBeGreaterThan(0);

const first = segments.first();
await first.evaluate((element) => element.scrollIntoView({ block: 'center' }));
await page.evaluate(() => new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
));
expect(await first.evaluate((element) =>
element.checkVisibility({ contentVisibilityAuto: true }),
)).toBe(true);
});
13 changes: 13 additions & 0 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,7 @@ import {
LONG_SIDEBAR_PROJECT_ID,
LONG_SIDEBAR_PROJECT_NAME,
LONG_SIDEBAR_SESSION_PREFIX,
OVERSIZED_TURN_SESSION_ID,
PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_SESSION_ID,
AGENT_GRAPH_SESSION_ID,
Expand All@@ -49,6 +50,8 @@ import {
import {
partialHistoryMessages,
partialHistorySession,
oversizedTurnMessages,
oversizedTurnSession,
promptRailMessages,
promptRailSession,
turnMessages,
Expand All@@ -71,6 +74,7 @@ const E2E_FIXTURE_SCENARIOS = new Set<E2eFixtureScenario>([
'turn-narrative-browser',
'chat-prompt-rail',
'chat-partial-history',
'chat-oversized-turn',
'settings-data',
'settings-bots-onboarding',
'settings-general',
Expand DownExpand Up@@ -191,6 +195,8 @@ export function getE2eFixtureState(fixture: E2eFixture | null): E2eFixtureState
return { ...state, activeSessionId: PROMPT_RAIL_SESSION_ID, workbarCollapsed: true };
case 'chat-partial-history':
return { ...state, activeSessionId: PARTIAL_HISTORY_SESSION_ID, workbarCollapsed: true };
case 'chat-oversized-turn':
return { ...state, activeSessionId: OVERSIZED_TURN_SESSION_ID, workbarCollapsed: true };
case 'settings-data':
return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'data' };
case 'settings-bots-onboarding':
Expand DownExpand Up@@ -276,6 +282,13 @@ export async function seedE2eFixture(input: {
partialHistoryMessages(now),
);
}
if (scenario === 'chat-oversized-turn') {
await writeSession(
input.workspaceRoot,
oversizedTurnSession(now),
oversizedTurnMessages(now),
);
}
if (scenario === 'sidebar-search-modal-open') {
for (const seed of longSidebarSessions(now)) {
await writeSession(input.workspaceRoot, seed.header, seed.messages);
Expand Down
99 changes: 99 additions & 0 deletions apps/desktop/src/main/e2e-fixture/scenarios-chat.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ import type { SessionHeader, StoredMessage } from '@maka/core/session';
import {
header,
AGENT_GRAPH_SESSION_ID,
OVERSIZED_TURN_SESSION_ID,
PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_PROMPT_COUNT,
PROMPT_RAIL_SESSION_ID,
Expand DownExpand Up@@ -201,3 +202,101 @@ export function partialHistoryMessages(now: number): StoredMessage[] {
}
return messages;
}

export function oversizedTurnSession(now: number): SessionHeader {
return header({
id: OVERSIZED_TURN_SESSION_ID,
name: '单轮超长渲染边界示例',
connection: 'zai-live',
model: 'glm-5.1',
now,
lastMessageAt: now - 60_000,
});
}

/**
* One synthetic Turn that exceeds the Desktop transcript byte budget by
* itself. Alternating answers and tool evidence create many stable visual
* blocks inside the same Turn, reproducing the shape that whole-Turn
* containment cannot bound without carrying any real conversation data.
*/
export function oversizedTurnMessages(now: number): StoredMessage[] {
const turnId = 'turn-oversized-fixture';
const messages: StoredMessage[] = [{
type: 'user',
id: 'msg-oversized-user',
turnId,
ts: now - 10 * 60_000,
text: '检查一组独立的合成步骤,并逐项给出简短结果。',
}];
const prose = [
'这一段只包含确定性的合成文本,用于测量长对话的滚动渲染。',
'',
'- 已检查输入边界',
'- 已记录合成结果',
'- 下一步继续验证',
].join('\n');
const toolOutput = 'synthetic output line\n'.repeat(600);
// Reasoning is the block #4256 reports as the dominant cost, and it is unlike
// collapsed tool output: `ChatReasoning` keeps its body mounted and only swaps
// a wrapper class, so a folded reasoning run still lays out. Give each step a
// multi-paragraph run so the `.maka-deep-thinking` boundary carries real,
// mounted content rather than free collapsed-stdout bytes.
const reasoning = [
'先确认这一步的输入边界:空样例、超长样例、并发样例三类分别对照期望结果,',
'逐项记录偏差,再对合成输出做一次去抖动检查,确保占位高度不随展开态漂移。',
'',
'- 输入域:空 / 超长 / 并发',
'- 期望:确定性、可重放',
'- 检查:占位高度稳定,无跨帧跳变',
'',
'综合以上,本步没有回归,可以进入下一组合成检查。',
].join('\n');
for (let index = 1; index <= 48; index += 1) {
const ts = now - (49 - index) * 10_000;
messages.push({
type: 'assistant',
id: `msg-oversized-assistant-${index}`,
turnId,
ts,
text: `### 合成步骤 ${index}\n\n${prose.repeat(12)}`,
thinking: { text: `${reasoning}\n\n${reasoning}\n\n${reasoning}` },
modelId: 'glm-5.1',
});
messages.push({
type: 'tool_call',
id: `tool-oversized-${index}`,
turnId,
ts: ts + 1_000,
toolName: 'Bash',
displayName: `合成检查 ${index}`,
intent: `读取第 ${index} 组固定测试数据`,
args: { cmd: `fixture-check --step ${index}`, cwd: '/workspace/maka' },
});
messages.push({
type: 'tool_result',
id: `tool-oversized-result-${index}`,
turnId,
ts: ts + 2_000,
toolUseId: `tool-oversized-${index}`,
isError: false,
durationMs: 100 + index,
content: {
kind: 'terminal',
cwd: '/workspace/maka',
cmd: `fixture-check --step ${index}`,
status: 'completed',
exitCode: 0,
output: {
mode: 'pipes',
stdout: toolOutput,
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
redacted: false,
},
},
});
}
return messages;
}
1 change: 1 addition & 0 deletions apps/desktop/src/main/e2e-fixture/seed-helpers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ export const TURN_SESSION_ID = 'e2e-fixture-turn';
export const AGENT_GRAPH_SESSION_ID = 'e2e-fixture-agent-graph';
export const PROMPT_RAIL_SESSION_ID = 'e2e-fixture-prompt-rail';
export const PARTIAL_HISTORY_SESSION_ID = 'e2e-fixture-partial-history';
export const OVERSIZED_TURN_SESSION_ID = 'e2e-fixture-oversized-turn';
/** Exceeds both the 64-tick rail and the bounded active transcript range. */
export const PROMPT_RAIL_PROMPT_COUNT = 120;
export const LONG_SIDEBAR_SESSION_PREFIX = 'e2e-fixture-sidebar-long-';
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/renderer/styles/chat-message.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,6 +123,35 @@
gap: var(--space-1);
}

/* A transcript range is bounded by complete Turns, so one unusually large
Turn can still be much taller than the scrollport. The outer Turn's
content-visibility boundary stops helping as soon as any part of that Turn
becomes relevant. Keep the stable timeline blocks inside it independently
skippable so Chromium does not lay out and paint every Markdown, reasoning,
and tool subtree while the reader crosses one nearby block.

Renderers apply the marker at the source of each timeline block, including
the children of a Processing fold. `auto` retains the measured block size
after first paint, preserving native scroll
anchoring when a skipped block leaves and re-enters the viewport. */
.maka-chat-message-list [data-maka-transcript-boundary] {
content-visibility: auto;
Comment thread
liugddx marked this conversation as resolved.
/* First-paint intrinsic-size ESTIMATE for scroll-anchor stability, not a
fixed height: `auto <n>px` still grows to the block's real size after
paint. 96px is the single-line answer baseline; tall multi-line blocks
override it below. */
contain-intrinsic-block-size: auto 96px;
}

/* Reasoning runs, Processing children, linked-agent lists, and tool/activity
cards are routinely multi-line. Their 320px first-paint estimate lets native
overflow anchoring absorb more of the intrinsic-size correction. It remains
an estimate rather than a clamp: the block grows to its measured size after
paint. */
.maka-chat-message-list [data-maka-transcript-boundary="large"] {
contain-intrinsic-block-size: auto 320px;
}

/* Expanded activity headers stay reachable while their own detail is being
read. Native sticky positioning keeps the header in the transcript flow,
so it leaves naturally at the card boundary and does not disturb ChatLayout
Expand Down
Loading
Loading