Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c2cc374
fix(ui): keep fenced code blocks at the code leading
Astro-Han Sep 16, 2026
8986006
refactor(desktop): load the transcript whole and virtualize it in the…
Astro-Han Sep 16, 2026
f432486
Merge branch 'main' of https://github.com/apache/maka into refactor/t…
Astro-Han Sep 16, 2026
edbff8d
fix(ui): measure transcript rows before they reach the reader
Astro-Han Sep 16, 2026
0471059
test(desktop): weigh a transcript held past the history budget
Astro-Han Sep 16, 2026
7fcef3a
test(desktop): push the transcript memory measurement to its limits
Astro-Han Sep 16, 2026
18e6583
test(desktop): take the transcript memory measurement out of CI
Astro-Han Sep 16, 2026
a4fb607
fix(ui): keep the reader still when earlier history arrives
Astro-Han Sep 16, 2026
67547f8
fix(ui): keep the load-earlier control reachable from an empty transc…
Astro-Han Sep 16, 2026
364f2cc
fix(desktop): read a Turn through the rows nested inside it
Astro-Han Sep 16, 2026
aff4dd4
fix(desktop): settle a Turn on the read that walks it, not on the tail
Astro-Han Sep 16, 2026
d2d6e3e
fix(desktop): recover the history a reader was given, not a budget of it
Astro-Han Sep 16, 2026
31b85ce
test(desktop): put the transcript's scroll geometry in the tier that …
Astro-Han Sep 16, 2026
bdfa125
fix(desktop): cut history answers where the Host says a Turn ends
Astro-Han Sep 16, 2026
7f4b742
fix(ui): move the load-earlier hold with the reader instead of droppi…
Astro-Han Sep 16, 2026
9b00471
fix(runtime-host): end a transcript page at the last whole Turn on it
Astro-Han Sep 16, 2026
8aba1f5
fix(runtime-host): cut a guest's transcript page where an owner's is cut
Astro-Han Sep 16, 2026
febb9a4
fix(desktop): recover the Turn a byte-bounded tail begins inside
Astro-Han Sep 16, 2026
c49dfbc
fix(desktop): reopen a replaced connection onto the history the reade…
Astro-Han Sep 16, 2026
5d80936
fix(ui): let a navigation command cancel a load-earlier hold
Astro-Han Sep 16, 2026
d0be522
test(desktop): move transcript geometry out of the Electron tier
Astro-Han Sep 16, 2026
6220592
Merge main into refactor/transcript-full-load
Astro-Han Sep 16, 2026
a63f5c3
Merge remote-tracking branch 'origin/main' into refactor/transcript-f…
Astro-Han Sep 16, 2026
9bb29ed
feat(storage): index each transcript Turn's ordinal extent
Astro-Han Sep 16, 2026
2d5cc42
fix(runtime-host): end transcript pages only where no Turn crosses
Astro-Han Sep 16, 2026
32a38df
fix(desktop): recover a transcript reload that lands on the cached view
Astro-Han Sep 16, 2026
a1dfde3
feat(runtime-host): answer where a Turn is from the Turn extent index
Astro-Han Sep 16, 2026
5459c0a
Merge remote-tracking branch 'origin/main' into refactor/transcript-f…
Astro-Han Sep 16, 2026
ae173df
feat(runtime-host): say where a Turn ends in its landmark
Astro-Han Sep 16, 2026
5511094
feat(desktop): locate Turns through the Host Turn index from Main
Astro-Han Sep 16, 2026
a4415e8
feat(desktop): reach any Turn through the Host Turn index from the Re…
Astro-Han Sep 16, 2026
28e9f3b
test(perf): follow the prompt-rail fixture's Turn count in the naviga…
Astro-Han Sep 16, 2026
fabf7eb
fix(ui): give every transcript scroll write one owner that knows the …
Astro-Han Sep 16, 2026
f338f61
test(perf): measure navigation and dense input against the 500-Turn f…
Astro-Han Sep 16, 2026
30dbb56
test(desktop): budget the far rail tick in the Electron tier
Astro-Han Sep 16, 2026
087e2a1
chore(desktop): count the Turn index state in the shell hook inventor…
Astro-Han Sep 16, 2026
c7433c8
refactor(desktop): open a Session's transcript history through one ad…
Astro-Han Sep 16, 2026
92a0fe8
fix(storage): derive Turn extents from one guarded, order-free visibi…
Astro-Han Sep 16, 2026
da44a87
fix(desktop): refuse a Turn read whose pages do not advance
Astro-Han Sep 16, 2026
75bf8d5
fix(desktop): judge a far Turn by the transcript range that answered
Astro-Han Sep 16, 2026
9fb32ff
fix(ui): end a rail navigation whose Turn does not arrive
Astro-Han Sep 16, 2026
a547889
test(desktop): drop the whole-read Electron spec
Astro-Han Sep 16, 2026
335859f
test(desktop): move transcript navigation checks out of Electron and …
Astro-Han Sep 16, 2026
a3609f8
test(desktop): wait for the unfiltered WorkHub rows before focusing a…
Astro-Han Sep 17, 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
6 changes: 1 addition & 5 deletions apps/desktop/e2e-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"partial-history-notice.spec.ts": {
"tests": 1,
"electron": "bounded paging crosses renderer, preload and Host transcript storage; the return-to-tail assertion also uses a real pointer hit-test"
"electron": "bounded paging crosses renderer, preload and Host transcript storage: each page comes from the Host only when the reader asks, and the Host Turn index lists the whole Session before it is loaded; the return-to-tail assertion also uses a real pointer hit-test. Where the arriving rows leave the reader is renderer-owned and lives in the PrependedHistoryKeepsMeasuredHeights story."
},
"proxy-password-editing.spec.ts": {
"tests": 1,
Expand Down Expand Up @@ -69,10 +69,6 @@
"tests": 4,
"electron": "observation seeding, reconnect and settle are Host subscriptions surviving a renderer remount"
},
"transcript-scroll-cost.spec.ts": {
"tests": 1,
"electron": "One traversal exercises real Host transcript paging through preload/IPC, bounded retained ranges, reader displacement when pages install, and a return-to-tail Host read. Fixture motion runs in the TranscriptRenderCost story."
},
"workhub-layout.spec.ts": {
"tests": 3,
"electron": "WorkHub model configuration and attachment sending cross the Host boundary; the Project New task action must dismiss the native dock owned by a separate WebContentsView; the native Workbar menu must coexist with the live sibling WebContentsView without hiding its compositor; the compact native window grows only with the inline composer wheel and keeps its bottom anchor; opening context details routes to Main’s Workbar without resizing or docking the floating conversation, while the thinking menu stays within the compact bounds, and pointer dragging scrolls the no-drag wheel instead of moving the frameless window; native floating preserves macOS Dock visibility; a held-open Host Turn verifies immediate prompt visibility, durable transcript handoff and persistence after renderer reload"
Expand Down
48 changes: 24 additions & 24 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ export async function withE2eWindow(
{
seed,
readinessSelector,
readinessTimeoutMs = 20_000,
e2eFixtureScenario,
locale,
platform,
Expand All @@ -407,6 +408,8 @@ export async function withE2eWindow(
}: {
seed: boolean;
readinessSelector: string;
/** Raise only for a scenario whose seeding is itself heavy enough to delay the first paint. */
readinessTimeoutMs?: number;
e2eFixtureScenario?: string;
locale?: 'zh-CN' | 'zh-TW' | 'en';
/** #1312: force app:info's platform so the window boots natively into that platform's `data-os` cascade. */
Expand Down Expand Up @@ -486,7 +489,7 @@ export async function withE2eWindow(
}
// Centralize the cold-start wait so test bodies are flake-free under retries:0.
try {
await page.waitForSelector(readinessSelector, { timeout: 20_000 });
await page.waitForSelector(readinessSelector, { timeout: readinessTimeoutMs });
if (invocableSkills) {
await waitForInvocableSkills(page, ['project-only', 'workspace-only']);
}
Expand All @@ -504,7 +507,7 @@ export async function withE2eWindow(
env: buildFixtureEnv(userDataDir, homeDir, { scenario: e2eFixtureScenario, locale, platform, showWindow: visibleWindow }),
});
const restored = await app.firstWindow();
await restored.waitForSelector(readinessSelector, { timeout: 20_000 });
await restored.waitForSelector(readinessSelector, { timeout: readinessTimeoutMs });
return restored;
} });
} finally {
Expand All @@ -531,8 +534,8 @@ type E2eTestFixtures = {
renameFocusWindow: { page: Page; app: ElectronApplication };
parentRemovalWindow: Page;
railRenderWindow: Page;
promptRailWindow: Page;
partialHistoryWindow: Page;
largeHistoryWindow: { page: Page; app: ElectronApplication };
newTaskTargetWindow: Page;
directoryReferenceWindow: { page: Page; folder: string };
accessibilityNarrativeWindow: Page;
Expand Down Expand Up @@ -647,27 +650,8 @@ export const test = base.extend<E2eTestFixtures>({
use,
);
},
// A multi-prompt transcript. Each cost assertion gets an isolated Host and
// renderer so observation state cannot bleed between tests. The window is
// shown because these cases drive the real compositor through CDP.
promptRailWindow: async ({}, use) => {
await withE2eWindow({
seed: false,
// A rendered turn, deliberately not the rail: Playwright treats a
// zero-area element as hidden, so gating readiness on a tick would turn
// every rail regression into a 20s cold-start timeout instead of the
// assertion that names it.
readinessSelector: '[data-turn-id]',
e2eFixtureScenario: 'chat-prompt-rail',
// Every other fixture window names its locale; without one the renderer
// takes the host's, so any test that reaches a control by its label
// passes on a Chinese desktop and cannot find it on an English CI runner.
locale: 'zh-CN',
showWindow: true,
}, use);
},
// A transcript larger than the bounded Desktop range. Clicking an unloaded
// prompt exercises the real load-around path and its partial-history UI.
// A transcript larger than the Desktop history budget, so earlier Turns load
// only through the load-earlier control.
partialHistoryWindow: async ({}, use) => {
await withE2eWindow({
seed: false,
Expand All @@ -677,6 +661,22 @@ export const test = base.extend<E2eTestFixtures>({
showWindow: true,
}, use);
},
// Three transcripts past the real 64 MiB history budget, for weighing what
// holding the loaded history costs the renderer. Writing them is hundreds of
// MiB of durable transcript, which is why the cold-start wait is raised.
largeHistoryWindow: async ({}, use) => {
await withE2eWindow(
{
seed: false,
readinessSelector: '[data-turn-id]',
readinessTimeoutMs: 300_000,
e2eFixtureScenario: 'chat-large-history',
locale: 'zh-CN',
showWindow: true,
},
async (page, { app }) => use({ page, app }),
);
},
// A data-backed conversation with settled tool evidence and the workbar open
// beside it. Shown because the accessibility journey follows real native
// focus order through the transcript into the composer controls.
Expand Down
89 changes: 58 additions & 31 deletions apps/desktop/e2e/partial-history-notice.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,53 +17,80 @@
* under the License.
*/

/**
* A Session too large for one read: what the Host hands over is a tail, and
* the rest comes only when the reader asks, one bounded page at a time.
*
* Where the arriving rows leave the reader is renderer-owned and lives in the
* `PrependedHistoryKeepsMeasuredHeights` browser story over real layout.
*/

import type { Page } from '@playwright/test';
import { expect, test } from './fixtures';

const TURN = '.maka-transcript-turn';
const SCROLLER = '[data-chat-scroll-container="true"]';
const TICK = '.maka-prompt-rail-tick';
/** Turns the partial-history fixture seeds. */
const PARTIAL_HISTORY_TURN_COUNT = 18;

test('a bounded transcript range reaches its whole history without a control to ask', async ({
async function frames(page: Page, count = 2): Promise<void> {
await page.evaluate((remaining) => new Promise<void>((resolve) => {
const step = (left: number): void => {
if (left === 0) resolve();
else requestAnimationFrame(() => step(left - 1));
};
step(remaining);
}), count);
}

/** Wheel to the top as a reader would; a programmatic scroll does not release the tail pin. */
async function wheelToTop(page: Page): Promise<void> {
const scroller = page.locator(SCROLLER);
await expect(async () => {
// Re-read each attempt: the window may still be resizing.
const box = await scroller.boundingBox();
if (!box) throw new Error('the chat scroll container has no box');
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 4);
await page.mouse.wheel(0, -4_000);
await frames(page, 4);
expect(await scroller.evaluate((element) => element.scrollTop)).toBe(0);
}).toPass({ timeout: 30_000 });
await frames(page, 4);
}

test('a transcript over the history budget loads earlier Turns only on request', async ({
partialHistoryWindow: page,
}) => {
test.setTimeout(90_000);
await page.setViewportSize({ width: 1_400, height: 800 });
const loadEarlier = page.getByRole('button', { name: '载入更早的记录' });
const pendingLoad = page.locator('button:disabled', { hasText: '载入更早的记录' });
const ticks = page.locator(TICK);

await expect(page.locator(TURN).first()).toBeVisible();
expect(await page.locator(TURN).count()).toBeLessThan(PARTIAL_HISTORY_TURN_COUNT);
await expect(page.locator(`[data-turn-id="turn-partial-history-${PARTIAL_HISTORY_TURN_COUNT}"]`)).toBeVisible();
// The Host Turn index lists the whole Session before its history is loaded.
await expect(ticks).toHaveCount(PARTIAL_HISTORY_TURN_COUNT);
await expect(loadEarlier).toHaveCount(1);

const oldestPrompt = page.locator(
'.maka-prompt-rail-tick[data-prompt-turn-id="turn-partial-history-1"]',
);
await expect(oldestPrompt).toBeVisible();
await oldestPrompt.click();
let loads = 0;
while ((await loadEarlier.count()) > 0) {
await wheelToTop(page);
await loadEarlier.click();
await expect(pendingLoad).toHaveCount(0, { timeout: 30_000 });
loads += 1;
expect(loads).toBeLessThan(PARTIAL_HISTORY_TURN_COUNT);
}

expect(loads).toBeGreaterThan(0);
await expect(ticks).toHaveCount(PARTIAL_HISTORY_TURN_COUNT);
await wheelToTop(page);
await expect(page.locator('[data-turn-id="turn-partial-history-1"]')).toBeVisible();
// Where the jump landed, read from the reading position rather than from
// `data-search-highlight`: that highlight clears itself 2.2s after the
// command lands, so waiting for the Turn to mount and then asserting it
// fails whenever loading the page around it takes longer than the flash —
// measured here as a 3s pass turning into an 18s timeout under load.
await expect(oldestPrompt).toHaveAttribute('data-active', 'true');
// A jump lands on its own page, not on the whole history.
expect(await page.locator(TURN).count()).toBeLessThan(PARTIAL_HISTORY_TURN_COUNT);

// The newer side of the jump fills on its own as the reader moves into it.
await page.mouse.move(700, 400);
await expect(async () => {
await page.mouse.wheel(0, 400);
await expect(page.locator('[data-turn-id="turn-partial-history-3"]')).toBeVisible();
}).toPass({ timeout: 30_000 });

const returnToLatest = page.getByRole('button', {
name: /^(?:滚动主对话到底部|Scroll main conversation to bottom)$/,
});
await expect(returnToLatest).toBeVisible();
await returnToLatest.click();

// Reading the tail page and rebuilding the window around it is slower than
// the paging above, and measured past the suite's 10s expect timeout here.
await expect(page.locator(`[data-turn-id="turn-partial-history-${PARTIAL_HISTORY_TURN_COUNT}"]`))
.toBeVisible({ timeout: 30_000 });
await expect(oldestPrompt).toBeVisible();
expect(await page.locator(TURN).count()).toBeLessThan(PARTIAL_HISTORY_TURN_COUNT);
await expect(page.locator(`[data-turn-id="turn-partial-history-${PARTIAL_HISTORY_TURN_COUNT}"]`)).toBeVisible();
await expect(ticks).toHaveCount(PARTIAL_HISTORY_TURN_COUNT);
});
Loading
Loading