Files
tutti/tools/scripts/agent-gui-layout-performance-scenarios.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

814 lines
27 KiB
JavaScript

import { chmod, copyFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
clickAgentWindowControl,
clickProviderTarget,
clickSession,
evaluate,
finishRendererScenario,
markRenderer,
selectProvider,
startRendererScenario,
waitForActiveSession,
waitForAgentWorkbenchWindow,
waitForEvaluation,
waitForProviderTiles,
waitForSelectedTarget,
waitForStableAgentWorkbenchWindow,
waitForStableRail,
waitForStableViewport
} from "./agent-gui-performance-helpers.mjs";
import {
requiredScenarioData,
scenarioSummary as summary,
sqlString,
startupWorkspaceID,
updateAgentGUISnapshot
} from "./agent-gui-performance-snapshot-helpers.mjs";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const cursorFixtureDirectory = join(
scriptDirectory,
"fixtures",
"agent-gui-performance"
);
const conversationItemPrefix = "agent-gui-conversation-item-";
const virtualizedStreamingMarkers = {
start: "tutti-perf:virtualized-streaming:start",
submitted: "tutti-perf:virtualized-streaming:submitted-observed",
firstMutation: "tutti-perf:virtualized-streaming:first-mutation-observed",
settled: "tutti-perf:virtualized-streaming:settled-observed",
end: "tutti-perf:virtualized-streaming:end"
};
export const virtualizedStreamingScenario = {
id: "virtualized-streaming",
markers: virtualizedStreamingMarkers,
milestones: [
{
key: "submitted",
label: "fixture prompt submitted",
marker: virtualizedStreamingMarkers.submitted
},
{
key: "firstMutation",
label: "first virtualized transcript mutation",
marker: virtualizedStreamingMarkers.firstMutation
},
{
key: "settled",
label: "streaming fixture settled",
marker: virtualizedStreamingMarkers.settled
}
],
prepareSnapshot: prepareVirtualizedTranscriptSnapshot,
prepare: prepareVirtualizedStreaming,
execute: executeVirtualizedStreaming,
describe(prepared) {
return `${prepared.sessionID}; ${prepared.turnCount} persisted turns + deterministic ACP stream`;
},
summarize(prepared, result) {
return summary(
[
{
name: "transcript virtualized before stream",
passed: prepared.virtualized
},
{
name: "stream entered working state",
passed: result.started
},
{
name: "virtualizer DOM mutated repeatedly",
passed: result.mutationBatches >= 8
},
{
name: "transcript remained virtualized",
passed: result.virtualizedAfter
},
{ name: "stream settled", passed: result.settled }
],
[
{ label: "Session", value: prepared.sessionID },
{ label: "Persisted turns", value: String(prepared.turnCount) },
{ label: "Mutation batches", value: String(result.mutationBatches) },
{ label: "DOM mutations", value: String(result.mutations) },
{ label: "Provider", value: "isolated fake Cursor ACP" }
],
"virtualized DOM is observed before submit; deterministic local ACP chunks produce at least eight MutationObserver batches; working state settles before the trace tail"
);
}
};
export async function prepareVirtualizedTranscriptSnapshot(
context,
options = {}
) {
const fixtureEnvironment = await installCursorPerformanceFixture(context);
const workspaceID = await startupWorkspaceID(context);
const richTextCandidateRequirement = options.richTextFixture
? "AND userTextMessageCount >= 4"
: "";
const candidates = await context.sqliteJSON(
context.databasePath,
`
SELECT s.agent_session_id AS sessionID,
COUNT(t.turn_id) AS turnCount,
(
SELECT COUNT(*)
FROM workspace_agent_messages m
WHERE m.workspace_id = s.workspace_id
AND m.agent_session_id = s.agent_session_id
AND m.deleted_at_unix_ms = 0
AND m.role = 'user'
AND m.kind = 'text'
) AS userTextMessageCount
FROM workspace_agent_sessions s
JOIN workspace_agent_turns t
ON t.workspace_id = s.workspace_id
AND t.agent_session_id = s.agent_session_id
WHERE s.workspace_id = '${sqlString(workspaceID)}'
AND s.deleted_at_unix_ms = 0
AND s.origin = 'WORKSPACE_AGENT_SESSION_ORIGIN_RUNTIME'
AND s.session_kind = 'root'
AND s.active_turn_id IS NULL
GROUP BY s.agent_session_id
HAVING COUNT(t.turn_id) >= 30
${richTextCandidateRequirement}
ORDER BY COUNT(t.turn_id) ASC, s.agent_session_id ASC
LIMIT 1;
`
);
const candidate = candidates[0];
if (!candidate?.sessionID) {
throw new Error(
options.richTextFixture
? "virtualized-scroll-locator requires one root session with at least 30 settled turns and four user text messages in the source snapshot"
: "virtualized-streaming requires one root session with at least 30 settled turns in the source snapshot"
);
}
const now = Date.now();
const fixtureMention = `[@Cursor](mention://agent-target/local%3Acursor?workspaceId=${encodeURIComponent(workspaceID)})`;
const richTextFixture = [
"Virtualized transcript performance fixture",
`${fixtureMention} inspect the historical scroll anchor and preserve the visible turn`,
"Paragraph three contains enough deterministic text to produce stable wrapping across narrow and wide transcript viewports",
"Paragraph four exercises static rich-text document creation without mounting an editable ProseMirror view",
`${fixtureMention} compare the current viewport with the message locator while older turns enter the virtual window`,
"Paragraph six keeps each historical user message tall enough to sustain a long monotonic upward scroll",
"Paragraph seven verifies that repeated line boundaries remain static while the virtualizer replaces mounted turns",
`${fixtureMention} finish the fixture with a third mention chip and a final wrapped paragraph`
].join("\n");
const richTextFixtureUpdate = options.richTextFixture
? `UPDATE workspace_agent_messages
SET payload_json = json_set(
payload_json,
'$.text',
'${sqlString(richTextFixture)}'
),
updated_at_unix_ms = ${now}
WHERE workspace_id = '${sqlString(workspaceID)}'
AND agent_session_id = '${sqlString(candidate.sessionID)}'
AND deleted_at_unix_ms = 0
AND role = 'user'
AND kind = 'text';`
: "";
await context.sqliteExec(
context.databasePath,
`
PRAGMA foreign_keys = ON;
UPDATE agent_targets
SET enabled = 1, updated_at_ms = ${now}
WHERE id = 'local:cursor';
UPDATE workspace_agent_sessions
SET agent_target_id = 'local:cursor',
provider = 'cursor',
provider_session_id = 'tutti-perf-cursor-session',
model = '',
settings_json = '{}',
cwd = '${sqlString(context.workspaceRoot)}',
rail_section_kind = 'conversations',
rail_project_path = '',
rail_section_key = 'conversations',
session_metadata_json = json_set(
session_metadata_json,
'$.visible', json('true'),
'$.imported', json('false')
),
internal_runtime_context_json = '{}',
updated_at_unix_ms = ${now}
WHERE workspace_id = '${sqlString(workspaceID)}'
AND agent_session_id = '${sqlString(candidate.sessionID)}';
${richTextFixtureUpdate}
`
);
return {
data: {
sessionID: candidate.sessionID,
turnCount: Number(candidate.turnCount),
richTextMessageCount: Number(candidate.userTextMessageCount),
richTextMentionsPerMessage: options.richTextFixture ? 3 : 0,
richTextParagraphsPerMessage: options.richTextFixture ? 8 : 0,
workspaceID
},
environment: fixtureEnvironment
};
}
export async function installCursorPerformanceFixture(context) {
const fixtureBinDirectory = join(context.runtimeDirectory, "state", "bin");
await mkdir(fixtureBinDirectory, { recursive: true });
const fixtureBinaryPath = join(fixtureBinDirectory, "cursor-agent");
await copyFile(
join(cursorFixtureDirectory, "cursor-agent"),
fixtureBinaryPath
);
await chmod(fixtureBinaryPath, 0o755);
return {
PATH: `${fixtureBinDirectory}:${process.env.PATH ?? ""}`,
SHELL: fixtureBinaryPath
};
}
async function prepareVirtualizedStreaming(context, options) {
return prepareVirtualizedTranscript(
context,
options,
"virtualized-streaming"
);
}
export async function prepareVirtualizedTranscript(
context,
options,
scenarioID
) {
const fixture = requiredScenarioData(context, scenarioID);
const providers = await waitForProviderTiles(
context.pageClient,
options.timeoutMs
);
if (
!providers.tiles.some(
(tile) => tile.targetID === "local:cursor" && !tile.disabled
)
) {
throw new Error(
`enabled local:cursor target is unavailable; visible targets: ${providers.tiles.map((tile) => `${tile.targetID}${tile.disabled ? " (disabled)" : ""}`).join(", ")}`
);
}
await selectProvider(context.pageClient, "local:cursor", options.timeoutMs);
await clickSession(context.pageClient, fixture.sessionID);
await waitForActiveSession(
context.pageClient,
fixture.sessionID,
options.timeoutMs
);
const transcript = await waitForEvaluation(
context.pageClient,
`(() => {
const timeline = document.querySelector('[data-testid="agent-gui-timeline"]');
const virtualized = timeline?.querySelector('[data-agent-transcript-virtualized="true"]');
const editor = document.querySelector('[data-testid="agent-gui-composer-editor"]');
return {
ready: Boolean(timeline && virtualized && editor && editor.getAttribute('aria-disabled') !== 'true'),
virtualized: Boolean(virtualized)
};
})()`,
options.timeoutMs,
"virtualized transcript and enabled composer"
);
return { ...fixture, virtualized: transcript.virtualized };
}
async function executeVirtualizedStreaming(context, _prepared, options) {
const { pageClient } = context;
await evaluate(
pageClient,
`(() => {
const root = document.querySelector('[data-agent-transcript-virtualized="true"]');
if (!(root instanceof HTMLElement)) throw new Error('virtualized transcript is unavailable');
const state = window.__tuttiPerfVirtualizedStreaming = {
firstMutationMarked: false,
mutationBatches: 0,
mutations: 0
};
state.observer = new MutationObserver((records) => {
state.mutationBatches += 1;
state.mutations += records.length;
if (!state.firstMutationMarked) {
state.firstMutationMarked = true;
console.timeStamp(${JSON.stringify(virtualizedStreamingMarkers.firstMutation)});
}
});
state.observer.observe(root, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
return true;
})()`
);
await startRendererScenario(pageClient, virtualizedStreamingMarkers.start);
await enterAndSubmitComposerPrompt(
pageClient,
"Render deterministic virtualized streaming performance fixture",
options.timeoutMs
);
const started = await waitForEvaluation(
pageClient,
`({ ready: Boolean(document.querySelector('[data-testid="agent-gui-composer-stop-active-turn"]')) })`,
options.timeoutMs,
"streaming fixture working state"
);
await markRenderer(pageClient, virtualizedStreamingMarkers.submitted);
const settled = await waitForEvaluation(
pageClient,
`(() => {
const state = window.__tuttiPerfVirtualizedStreaming;
return {
ready: Boolean(state?.firstMutationMarked && state.mutationBatches >= 8 && !document.querySelector('[data-testid="agent-gui-composer-stop-active-turn"]')),
mutationBatches: state?.mutationBatches ?? 0,
mutations: state?.mutations ?? 0,
virtualizedAfter: Boolean(document.querySelector('[data-agent-transcript-virtualized="true"]'))
};
})()`,
options.timeoutMs,
"settled virtualized streaming fixture",
50
);
await markRenderer(pageClient, virtualizedStreamingMarkers.settled);
await evaluate(
pageClient,
`(() => {
window.__tuttiPerfVirtualizedStreaming?.observer?.disconnect();
return true;
})()`
);
await finishRendererScenario(pageClient, virtualizedStreamingMarkers.end);
return {
mutationBatches: settled.mutationBatches,
mutations: settled.mutations,
settled: settled.ready,
started: started.ready,
virtualizedAfter: settled.virtualizedAfter
};
}
const railRevealMarkers = {
start: "tutti-perf:rail-scope-reveal:start",
selected: "tutti-perf:rail-scope-reveal:selected-observed",
revealed: "tutti-perf:rail-scope-reveal:scroll-into-view-observed",
stable: "tutti-perf:rail-scope-reveal:stable-observed",
end: "tutti-perf:rail-scope-reveal:end"
};
export const railScopeRevealScenario = {
id: "rail-scope-reveal",
markers: railRevealMarkers,
milestones: [
{
key: "selected",
label: "target Agent scope selected",
marker: railRevealMarkers.selected
},
{
key: "revealed",
label: "active session scrollIntoView",
marker: railRevealMarkers.revealed
},
{
key: "stable",
label: "target rail stable",
marker: railRevealMarkers.stable
}
],
prepareSnapshot: prepareRailScopeRevealSnapshot,
prepare: prepareRailScopeReveal,
execute: executeRailScopeReveal,
describe(prepared) {
return `${prepared.sourceTargetID} -> ${prepared.targetTargetID}; reveal ${prepared.sessionID}`;
},
summarize(prepared, result) {
return summary(
[
{ name: "target Agent selected", passed: result.selected },
{ name: "target session active", passed: result.active },
{
name: "target session received scrollIntoView",
passed: result.revealCalls > 0
},
{ name: "rail stable", passed: result.stable }
],
[
{
label: "Agent scope",
value: `${prepared.sourceTargetID} → ${prepared.targetTargetID}`
},
{ label: "Session", value: prepared.sessionID },
{ label: "scrollIntoView calls", value: String(result.revealCalls) }
],
"a fresh Agent scope restores its mapped active session; the scenario records the exact target row scrollIntoView call before waiting for five stable rail snapshots"
);
}
};
async function prepareRailScopeRevealSnapshot(context) {
const workspaceID = await startupWorkspaceID(context);
const targets = await context.sqliteJSON(
context.databasePath,
`
SELECT agent_target_id AS targetID, COUNT(*) AS sessionCount
FROM workspace_agent_sessions
WHERE workspace_id = '${sqlString(workspaceID)}'
AND deleted_at_unix_ms = 0
AND agent_target_id IS NOT NULL
GROUP BY agent_target_id
HAVING COUNT(*) >= 2
ORDER BY COUNT(*) DESC, agent_target_id ASC
LIMIT 2;
`
);
if (targets.length < 2) {
throw new Error(
"rail-scope-reveal requires sessions for at least two Agent targets"
);
}
const targetTargetID = targets[0].targetID;
const sourceTargetID = targets[1].targetID;
const sessions = await context.sqliteJSON(
context.databasePath,
`
SELECT agent_session_id AS sessionID
FROM workspace_agent_sessions
WHERE workspace_id = '${sqlString(workspaceID)}'
AND agent_target_id = '${sqlString(targetTargetID)}'
AND deleted_at_unix_ms = 0
ORDER BY updated_at_unix_ms DESC, agent_session_id ASC
LIMIT 1 OFFSET 4;
`
);
const sessionID = sessions[0]?.sessionID;
if (!sessionID) {
throw new Error(
"rail-scope-reveal could not select a fifth target session"
);
}
await updateAgentGUISnapshot(context, (state) => ({
...state,
agentTargetId: sourceTargetID,
lastActiveAgentSessionId: null,
lastActiveAgentSessionIdByAgentTargetId: {
...(state.lastActiveAgentSessionIdByAgentTargetId ?? {}),
[targetTargetID]: sessionID
}
}));
return {
data: { sessionID, sourceTargetID, targetTargetID, workspaceID }
};
}
async function prepareRailScopeReveal(context, options) {
const fixture = requiredScenarioData(context, "rail-scope-reveal");
const providers = await waitForProviderTiles(
context.pageClient,
options.timeoutMs
);
const available = new Set(
providers.tiles
.filter((tile) => tile.disabled !== true)
.map((tile) => tile.targetID)
);
if (
!available.has(fixture.sourceTargetID) ||
!available.has(fixture.targetTargetID)
) {
throw new Error(
`rail-scope-reveal requires enabled targets ${fixture.sourceTargetID} and ${fixture.targetTargetID}`
);
}
if (providers.selectedTargetID !== fixture.sourceTargetID) {
await selectProvider(
context.pageClient,
fixture.sourceTargetID,
options.timeoutMs
);
} else {
await waitForStableRail(
context.pageClient,
fixture.sourceTargetID,
options.timeoutMs
);
}
return fixture;
}
async function executeRailScopeReveal(context, prepared, options) {
const { pageClient } = context;
await evaluate(
pageClient,
`(() => {
const original = Element.prototype.scrollIntoView;
window.__tuttiPerfRailReveal = { calls: [], original };
Element.prototype.scrollIntoView = function(options) {
const testID = this instanceof HTMLElement ? (this.dataset.testid ?? '') : '';
window.__tuttiPerfRailReveal.calls.push({ options, testID });
return original.call(this, options);
};
return true;
})()`
);
await startRendererScenario(pageClient, railRevealMarkers.start);
await clickProviderTarget(pageClient, prepared.targetTargetID);
const selected = await waitForSelectedTarget(
pageClient,
prepared.targetTargetID,
options.timeoutMs
);
await markRenderer(pageClient, railRevealMarkers.selected);
const active = await waitForActiveSession(
pageClient,
prepared.sessionID,
options.timeoutMs
);
const revealed = await waitForEvaluation(
pageClient,
`(() => {
const testID = ${JSON.stringify(conversationItemPrefix)} + ${JSON.stringify(prepared.sessionID)};
const calls = window.__tuttiPerfRailReveal?.calls ?? [];
return { ready: calls.some((call) => call.testID === testID), revealCalls: calls.filter((call) => call.testID === testID).length };
})()`,
options.timeoutMs,
`scrollIntoView for ${prepared.sessionID}`,
50
);
await markRenderer(pageClient, railRevealMarkers.revealed);
const stable = await waitForStableRail(
pageClient,
prepared.targetTargetID,
options.timeoutMs
);
await markRenderer(pageClient, railRevealMarkers.stable);
await evaluate(
pageClient,
`(() => {
const state = window.__tuttiPerfRailReveal;
if (state?.original) Element.prototype.scrollIntoView = state.original;
return true;
})()`
);
await finishRendererScenario(pageClient, railRevealMarkers.end);
return {
active: active.ready,
revealCalls: revealed.revealCalls,
selected: selected.ready,
stable: stable.sectionCount > 0
};
}
const composerResizeMarkers = {
start: "tutti-perf:composer-overflow-resize:start",
narrowed: "tutti-perf:composer-overflow-resize:narrowed-observed",
overflowing: "tutti-perf:composer-overflow-resize:overflow-observed",
restored: "tutti-perf:composer-overflow-resize:restored-observed",
end: "tutti-perf:composer-overflow-resize:end"
};
export const composerOverflowResizeScenario = {
id: "composer-overflow-resize",
markers: composerResizeMarkers,
milestones: [
{
key: "narrowed",
label: "renderer viewport narrowed",
marker: composerResizeMarkers.narrowed
},
{
key: "overflowing",
label: "prompt tip layout measurement observed",
marker: composerResizeMarkers.overflowing
},
{
key: "restored",
label: "renderer viewport restored",
marker: composerResizeMarkers.restored
}
],
prepare: prepareComposerOverflowResize,
execute: executeComposerOverflowResize,
describe(prepared) {
return `${prepared.originalWidth}px -> narrow -> ${prepared.originalWidth}px`;
},
summarize(prepared, result) {
return summary(
[
{ name: "hero prompt tip present", passed: prepared.promptTipPresent },
{ name: "viewport narrowed", passed: result.narrowed },
{
name: "prompt tip layout measured after resize",
passed: result.measurementReads > 0
},
{ name: "viewport restored", passed: result.restored }
],
[
{ label: "Original viewport", value: `${prepared.originalWidth}px` },
{ label: "Narrow viewport", value: `${result.narrowWidth}px` },
{ label: "Prompt tip", value: "hero composer" },
{
label: "Layout getter reads",
value: String(result.measurementReads)
}
],
"AgentGUI Workbench is fullscreen; native scrollWidth/clientWidth getters are transparently counted while renderer resizes drive the prompt-tip ResizeObserver, then viewport metrics are restored"
);
}
};
async function prepareComposerOverflowResize(context, options) {
const { pageClient } = context;
const providers = await waitForProviderTiles(pageClient, options.timeoutMs);
const targetIDValue =
providers.selectedTargetID ?? providers.tiles[0]?.targetID;
if (!targetIDValue) {
throw new Error("composer-overflow-resize has no Agent target");
}
if (providers.selectedTargetID !== targetIDValue) {
await clickProviderTarget(pageClient, targetIDValue);
await waitForSelectedTarget(pageClient, targetIDValue, options.timeoutMs);
}
const windowState = await waitForStableAgentWorkbenchWindow(
pageClient,
options.timeoutMs
);
if (windowState.displayMode !== "fullscreen") {
await clickAgentWindowControl(
pageClient,
windowState.id,
"agent-gui-window-toggle-display-mode"
);
await waitForAgentWorkbenchWindow(
pageClient,
options.timeoutMs,
"windowState?.displayMode === 'fullscreen'",
"fullscreen AgentGUI Workbench window",
windowState.id
);
}
const promptTip = await waitForEvaluation(
pageClient,
`(() => {
const tip = document.querySelector('[data-testid="agent-gui-prompt-tip"]');
return { ready: Boolean(tip), promptTipPresent: Boolean(tip) };
})()`,
options.timeoutMs,
"hero composer prompt tip"
);
const viewport = await waitForStableViewport(pageClient, options.timeoutMs);
return {
nodeID: windowState.id,
originalHeight: viewport.height,
originalWidth: viewport.width,
promptTipPresent: promptTip.promptTipPresent
};
}
async function executeComposerOverflowResize(context, prepared, options) {
const widths = [760, 680, 600, 520];
let measurement = null;
await evaluate(
context.pageClient,
`(() => {
const scrollDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'scrollWidth');
const clientDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'clientWidth');
if (!scrollDescriptor?.get || !clientDescriptor?.get) {
throw new Error('native inline layout getters are unavailable');
}
const state = window.__tuttiPerfComposerResize = {
clientWidthReads: 0,
scrollWidthReads: 0,
scrollDescriptor,
clientDescriptor
};
Object.defineProperty(Element.prototype, 'scrollWidth', {
...scrollDescriptor,
get() {
const value = scrollDescriptor.get.call(this);
if (this instanceof HTMLElement && this.matches('[data-testid="agent-gui-prompt-tip"]')) {
state.scrollWidthReads += 1;
}
return value;
}
});
Object.defineProperty(Element.prototype, 'clientWidth', {
...clientDescriptor,
get() {
const value = clientDescriptor.get.call(this);
if (this instanceof HTMLElement && this.matches('[data-testid="agent-gui-prompt-tip"]')) {
state.clientWidthReads += 1;
}
return value;
}
});
return true;
})()`
);
await startRendererScenario(context.pageClient, composerResizeMarkers.start);
for (const width of widths) {
await context.pageClient.send("Emulation.setDeviceMetricsOverride", {
deviceScaleFactor: 1,
height: prepared.originalHeight,
mobile: false,
width
});
const viewport = await waitForStableViewport(
context.pageClient,
options.timeoutMs
);
await markRenderer(context.pageClient, composerResizeMarkers.narrowed);
measurement = await evaluate(
context.pageClient,
`(() => {
const state = window.__tuttiPerfComposerResize;
return {
measurementReads: Math.min(
state?.scrollWidthReads ?? 0,
state?.clientWidthReads ?? 0
),
viewportWidth: window.innerWidth
};
})()`
);
if (measurement?.measurementReads > 0) {
measurement.viewportWidth = viewport.width;
break;
}
}
if (!measurement?.measurementReads) {
throw new Error(
"prompt tip layout getters were not read after viewport resize"
);
}
await markRenderer(context.pageClient, composerResizeMarkers.overflowing);
await context.pageClient.send("Emulation.clearDeviceMetricsOverride");
const restored = await waitForStableViewport(
context.pageClient,
options.timeoutMs
);
await evaluate(
context.pageClient,
`(() => {
const state = window.__tuttiPerfComposerResize;
if (!state) return false;
Object.defineProperty(Element.prototype, 'scrollWidth', state.scrollDescriptor);
Object.defineProperty(Element.prototype, 'clientWidth', state.clientDescriptor);
return true;
})()`
);
await markRenderer(context.pageClient, composerResizeMarkers.restored);
await finishRendererScenario(context.pageClient, composerResizeMarkers.end);
return {
narrowWidth: measurement.viewportWidth,
narrowed: measurement.viewportWidth < prepared.originalWidth,
measurementReads: measurement.measurementReads,
restored: Math.abs(restored.width - prepared.originalWidth) <= 2
};
}
export async function enterAndSubmitComposerPrompt(client, prompt, timeoutMs) {
await evaluate(
client,
`(() => {
const editor = document.querySelector('[data-testid="agent-gui-composer-editor"]');
if (!(editor instanceof HTMLElement)) throw new Error('composer editor is unavailable');
editor.focus();
document.execCommand('selectAll', false);
if (!document.execCommand('insertText', false, ${JSON.stringify(prompt)})) {
throw new Error('could not enter composer prompt');
}
return true;
})()`
);
await waitForEvaluation(
client,
`(() => {
const submit = document.querySelector('[data-testid="agent-gui-composer-send"]');
return { ready: submit instanceof HTMLButtonElement && !submit.disabled };
})()`,
timeoutMs,
"enabled composer submit button",
25
);
await evaluate(
client,
`(() => {
const submit = document.querySelector('[data-testid="agent-gui-composer-send"]');
if (!(submit instanceof HTMLButtonElement) || submit.disabled) throw new Error('composer send button is unavailable');
submit.click();
return true;
})()`
);
}