Files
tutti/tools/scripts/agent-gui-composer-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

512 lines
18 KiB
JavaScript

import {
requiredScenarioData,
scenarioSummary as summary,
sqlString,
startupWorkspaceID,
updateAgentGUISnapshot
} from "./agent-gui-performance-snapshot-helpers.mjs";
import {
evaluate,
finishRendererScenario,
markRenderer,
startRendererScenario,
waitForEvaluation
} from "./agent-gui-performance-helpers.mjs";
const editorSelector = '[data-testid="agent-gui-composer-editor"]';
const paletteSelector = '[data-testid="agent-gui-mention-palette-surface"]';
const ordinaryText =
"Measure AgentGUI composer input one character at a time. ";
const multilineText = "2\n3\n4\n5";
const imeCandidates = ["x", "xing", "性能"];
const imeCommittedText = "性能";
const composerInputMarkers = {
start: "tutti-perf:composer-input:start",
multilineExpanded: "tutti-perf:composer-input:multiline-expanded-observed",
multilineShrunk: "tutti-perf:composer-input:multiline-shrunk-observed",
textEntered: "tutti-perf:composer-input:text-entered-observed",
imeComposing: "tutti-perf:composer-input:ime-composing-observed",
imeCommitted: "tutti-perf:composer-input:ime-committed-observed",
mentionOpened: "tutti-perf:composer-input:mention-opened-observed",
mentionNavigated: "tutti-perf:composer-input:mention-navigated-observed",
mentionClosed: "tutti-perf:composer-input:mention-closed-observed",
end: "tutti-perf:composer-input:end"
};
export const composerInputScenario = {
id: "composer-input",
markers: composerInputMarkers,
milestones: [
{
key: "multilineExpanded",
label: "four-line composer expanded",
marker: composerInputMarkers.multilineExpanded
},
{
key: "multilineShrunk",
label: "composer shrunk to one line",
marker: composerInputMarkers.multilineShrunk
},
{
key: "textEntered",
label: "per-character text entered",
marker: composerInputMarkers.textEntered
},
{
key: "imeComposing",
label: "IME composition updated",
marker: composerInputMarkers.imeComposing
},
{
key: "imeCommitted",
label: "IME composition committed",
marker: composerInputMarkers.imeCommitted
},
{
key: "mentionOpened",
label: "@ mention panel opened",
marker: composerInputMarkers.mentionOpened
},
{
key: "mentionNavigated",
label: "mention highlight and category changed",
marker: composerInputMarkers.mentionNavigated
},
{
key: "mentionClosed",
label: "mention panel closed",
marker: composerInputMarkers.mentionClosed
}
],
prepareSnapshot: prepareComposerInputSnapshot,
prepare: prepareComposerInput,
execute: executeComposerInput,
describe(prepared) {
return `${prepared.sessionID}; ${Array.from(multilineText).length} multiline inserts + shrink + ${Array.from(ordinaryText).length} text inserts + ${imeCandidates.length} IME updates + @ keyboard navigation`;
},
summarize(prepared, result) {
return summary(
[
{ name: "dock composer active", passed: prepared.dockComposer },
{
name: "four-line composer expanded",
passed:
result.expandedGeometry.height > result.collapsedGeometry.height + 1
},
{
name: "composer shrank to one line",
passed:
Math.abs(
result.shrunkGeometry.height - result.collapsedGeometry.height
) <= 1
},
{
name: "action button stayed bottom-aligned",
passed:
Math.abs(
result.expandedGeometry.buttonBottomOffset -
result.collapsedGeometry.buttonBottomOffset
) <= 1 &&
Math.abs(
result.shrunkGeometry.buttonBottomOffset -
result.collapsedGeometry.buttonBottomOffset
) <= 1
},
{
name: "per-character text input observed",
passed: result.inputEvents >= Array.from(ordinaryText).length
},
{
name: "IME composition lifecycle observed",
passed:
result.compositionStarts > 0 &&
result.compositionUpdates >= imeCandidates.length &&
result.compositionEnds > 0
},
{ name: "IME text committed once", passed: result.imeCommitted },
{ name: "@ mention panel opened", passed: result.mentionOpened },
{
name: "mention selection moved",
passed: result.highlightChanged
},
{ name: "mention category cycled", passed: result.categoryChanged },
{
name: "mention keyboard events observed",
passed: result.mentionKeys.join(",") === "ArrowDown,Tab,Escape"
},
{ name: "mention panel closed", passed: result.mentionClosed }
],
[
{ label: "Session", value: prepared.sessionID },
{
label: "Composer heights",
value: `${result.collapsedGeometry.height.toFixed(1)} -> ${result.expandedGeometry.height.toFixed(1)} -> ${result.shrunkGeometry.height.toFixed(1)} px`
},
{
label: "Text inserts",
value: String(
Array.from(multilineText).length + Array.from(ordinaryText).length
)
},
{ label: "Input events", value: String(result.inputEvents) },
{
label: "IME events",
value: `${result.compositionStarts} start / ${result.compositionUpdates} update / ${result.compositionEnds} end`
},
{
label: "Mention keys",
value: result.mentionKeys.join(" -> ")
}
],
"CDP expands the composer through explicit newline transactions, deletes back to one line, injects ordinary text character by character, drives one real IME composition lifecycle, then verifies @ keyboard navigation"
);
}
};
async function prepareComposerInputSnapshot(context) {
const workspaceID = await startupWorkspaceID(context);
const candidates = await context.sqliteJSON(
context.databasePath,
`
SELECT s.agent_session_id AS sessionID,
s.agent_target_id AS targetID
FROM workspace_agent_sessions s
JOIN agent_targets t ON t.id = s.agent_target_id
WHERE s.workspace_id = '${sqlString(workspaceID)}'
AND s.deleted_at_unix_ms = 0
AND s.session_kind = 'root'
AND s.active_turn_id IS NULL
AND t.enabled = 1
ORDER BY s.updated_at_unix_ms DESC, s.agent_session_id ASC
LIMIT 1;
`
);
const candidate = candidates[0];
if (!candidate?.sessionID || !candidate.targetID) {
throw new Error(
"composer-input requires one settled root session for an enabled Agent target"
);
}
await updateAgentGUISnapshot(context, (state) => ({
...state,
agentTargetId: candidate.targetID,
conversationRailCollapsed: false,
lastActiveAgentSessionId: candidate.sessionID,
lastActiveAgentSessionIdByAgentTargetId: {
...(state.lastActiveAgentSessionIdByAgentTargetId ?? {}),
[candidate.targetID]: candidate.sessionID
}
}));
return {
data: {
sessionID: candidate.sessionID,
targetID: candidate.targetID,
workspaceID
}
};
}
async function prepareComposerInput(context, options) {
const fixture = requiredScenarioData(context, "composer-input");
const ready = await waitForEvaluation(
context.pageClient,
`(() => {
const editor = document.querySelector(${JSON.stringify(editorSelector)});
const activeSession = document.querySelector(${JSON.stringify(`[data-testid="agent-gui-conversation-item-${fixture.sessionID}"]`)});
const dockComposer = Boolean(editor?.closest('[data-testid="agent-gui-composer-input-shell"]'));
return {
ready: editor instanceof HTMLElement && editor.getAttribute('contenteditable') === 'true' && activeSession?.dataset.active === 'true' && dockComposer,
dockComposer,
editorReady: editor instanceof HTMLElement
};
})()`,
options.timeoutMs,
"enabled AgentGUI composer editor"
);
await evaluate(
context.pageClient,
`(() => {
const editor = document.querySelector(${JSON.stringify(editorSelector)});
if (!(editor instanceof HTMLElement)) throw new Error('composer editor is unavailable');
editor.focus();
document.execCommand('selectAll', false);
document.execCommand('delete', false);
return true;
})()`
);
await waitForEditorText(context.pageClient, "", options.timeoutMs);
return {
dockComposer: ready.dockComposer,
editorReady: ready.editorReady,
sessionID: fixture.sessionID,
targetID: fixture.targetID
};
}
async function executeComposerInput(context, _prepared, options) {
const { pageClient } = context;
await installComposerInputCounters(pageClient);
await startRendererScenario(pageClient, composerInputMarkers.start);
const collapsedGeometry = await readComposerGeometry(pageClient);
for (const character of Array.from(multilineText)) {
await pageClient.send("Input.insertText", { text: character });
}
await waitForEditorText(pageClient, multilineText, options.timeoutMs);
const expandedGeometry = await waitForComposerGeometry(
pageClient,
`height > ${JSON.stringify(collapsedGeometry.height + 1)} && Math.abs(height - targetHeight) <= 1`,
options.timeoutMs,
"four-line composer expansion"
);
await markRenderer(pageClient, composerInputMarkers.multilineExpanded);
await clearComposerEditor(pageClient);
await waitForEditorText(pageClient, "", options.timeoutMs);
const shrunkGeometry = await waitForComposerGeometry(
pageClient,
`Math.abs(height - ${JSON.stringify(collapsedGeometry.height)}) <= 1`,
options.timeoutMs,
"composer shrink to one line"
);
await markRenderer(pageClient, composerInputMarkers.multilineShrunk);
for (const character of Array.from(ordinaryText)) {
await pageClient.send("Input.insertText", { text: character });
}
await waitForEditorText(pageClient, ordinaryText, options.timeoutMs);
await markRenderer(pageClient, composerInputMarkers.textEntered);
for (const candidate of imeCandidates) {
await pageClient.send("Input.imeSetComposition", {
text: candidate,
selectionStart: candidate.length,
selectionEnd: candidate.length
});
}
await waitForEditorText(
pageClient,
`${ordinaryText}${imeCommittedText}`,
options.timeoutMs
);
await markRenderer(pageClient, composerInputMarkers.imeComposing);
await pageClient.send("Input.insertText", { text: imeCommittedText });
const committed = await waitForEditorText(
pageClient,
`${ordinaryText}${imeCommittedText}`,
options.timeoutMs
);
await markRenderer(pageClient, composerInputMarkers.imeCommitted);
await pageClient.send("Input.insertText", { text: " @" });
const opened = await waitForMentionState(
pageClient,
"Boolean(palette)",
options.timeoutMs,
"open @ mention panel"
);
await markRenderer(pageClient, composerInputMarkers.mentionOpened);
const initialHighlight = opened.highlightedIndex;
const initialCategory = opened.activeCategoryIndex;
await dispatchKey(pageClient, "ArrowDown", "ArrowDown", 40);
const highlighted = await waitForMentionState(
pageClient,
`Boolean(palette) && highlightedIndex !== ${JSON.stringify(initialHighlight)}`,
options.timeoutMs,
"changed mention highlight"
);
await dispatchKey(pageClient, "Tab", "Tab", 9);
const navigated = await waitForMentionState(
pageClient,
`Boolean(palette) && activeCategoryIndex !== ${JSON.stringify(initialCategory)}`,
options.timeoutMs,
"changed mention category"
);
await markRenderer(pageClient, composerInputMarkers.mentionNavigated);
await dispatchKey(pageClient, "Escape", "Escape", 27);
const closed = await waitForMentionState(
pageClient,
"!palette",
options.timeoutMs,
"closed mention panel"
);
await markRenderer(pageClient, composerInputMarkers.mentionClosed);
const counters = await readAndRemoveComposerInputCounters(pageClient);
await finishRendererScenario(pageClient, composerInputMarkers.end);
return {
...counters,
collapsedGeometry,
expandedGeometry,
shrunkGeometry,
categoryChanged: navigated.activeCategoryIndex !== initialCategory,
highlightChanged: highlighted.highlightedIndex !== initialHighlight,
imeCommitted:
committed.text === `${ordinaryText}${imeCommittedText}` &&
countOccurrences(committed.text, imeCommittedText) === 1,
mentionClosed: closed.palettePresent === false,
mentionOpened: opened.palettePresent === true
};
}
async function clearComposerEditor(client) {
await evaluate(
client,
`(() => {
const editor = document.querySelector(${JSON.stringify(editorSelector)});
if (!(editor instanceof HTMLElement)) throw new Error('composer editor is unavailable');
editor.focus();
document.execCommand('selectAll', false);
document.execCommand('delete', false);
return true;
})()`
);
}
async function readComposerGeometry(client) {
return evaluate(client, composerGeometryExpression("true"));
}
async function waitForComposerGeometry(client, predicate, timeoutMs, label) {
return waitForEvaluation(
client,
composerGeometryExpression(predicate),
timeoutMs,
label,
25
);
}
function composerGeometryExpression(predicate) {
return `(() => {
const editor = document.querySelector(${JSON.stringify(editorSelector)});
const inputArea = editor?.closest('[data-testid="agent-gui-composer-input-shell"]');
const actionButton = document.querySelector('[data-testid="agent-gui-composer-send"], [data-testid="agent-gui-composer-stop-active-turn"]');
if (!(inputArea instanceof HTMLElement) || !(actionButton instanceof HTMLElement)) {
return { ready: false, buttonBottomOffset: -1, height: -1, targetHeight: -1 };
}
const inputRect = inputArea.getBoundingClientRect();
const buttonRect = actionButton.getBoundingClientRect();
const height = inputRect.height;
const targetHeight = Number.parseFloat(
inputArea.style.getPropertyValue('--agent-gui-composer-input-height')
);
const buttonBottomOffset = inputRect.bottom - buttonRect.bottom;
return {
ready: ${predicate},
buttonBottomOffset,
height,
targetHeight
};
})()`;
}
async function installComposerInputCounters(client) {
await evaluate(
client,
`(() => {
const editor = document.querySelector(${JSON.stringify(editorSelector)});
if (!(editor instanceof HTMLElement)) throw new Error('composer editor is unavailable');
const state = {
compositionStarts: 0,
compositionUpdates: 0,
compositionEnds: 0,
inputEvents: 0,
mentionKeys: []
};
const handlers = {
compositionstart: () => { state.compositionStarts += 1; },
compositionupdate: () => { state.compositionUpdates += 1; },
compositionend: () => { state.compositionEnds += 1; },
input: () => { state.inputEvents += 1; }
};
for (const [type, handler] of Object.entries(handlers)) editor.addEventListener(type, handler, true);
const keydown = (event) => {
if (event.target === editor && ['ArrowDown', 'Tab', 'Escape'].includes(event.key)) state.mentionKeys.push(event.key);
};
document.addEventListener('keydown', keydown, true);
window.__tuttiPerfComposerInput = { editor, handlers, keydown, state };
editor.focus();
return true;
})()`
);
}
async function readAndRemoveComposerInputCounters(client) {
return evaluate(
client,
`(() => {
const owner = window.__tuttiPerfComposerInput;
if (!owner) throw new Error('composer input counters are unavailable');
for (const [type, handler] of Object.entries(owner.handlers)) owner.editor.removeEventListener(type, handler, true);
document.removeEventListener('keydown', owner.keydown, true);
delete window.__tuttiPerfComposerInput;
return owner.state;
})()`
);
}
async function waitForEditorText(client, expected, timeoutMs) {
return waitForEvaluation(
client,
`(() => {
const editor = document.querySelector(${JSON.stringify(editorSelector)});
const text = editor
? [...editor.children].map((block) => block.textContent ?? '').join('\\n')
: '';
return { ready: text === ${JSON.stringify(expected)}, text };
})()`,
timeoutMs,
`composer text ${JSON.stringify(expected)}`,
25
);
}
async function waitForMentionState(client, predicate, timeoutMs, label) {
return waitForEvaluation(
client,
`(() => {
const palette = document.querySelector(${JSON.stringify(paletteSelector)});
const highlighted = palette?.querySelector('[data-highlighted]') ?? null;
const highlightable = palette ? [...palette.querySelectorAll('[role="option"], button[data-highlighted]')] : [];
const activeCategory = palette?.querySelector('[role="tab"][aria-selected="true"]') ?? null;
const categories = palette ? [...palette.querySelectorAll('[role="tab"]')] : [];
const activeCategoryIndex = activeCategory ? categories.indexOf(activeCategory) : -1;
const highlightedIndex = highlighted ? highlightable.indexOf(highlighted) : -1;
return {
ready: ${predicate},
activeCategoryIndex,
highlightedIndex,
palettePresent: Boolean(palette)
};
})()`,
timeoutMs,
label,
25
);
}
async function dispatchKey(client, key, code, virtualKeyCode) {
await client.send("Input.dispatchKeyEvent", {
type: "rawKeyDown",
key,
code,
windowsVirtualKeyCode: virtualKeyCode,
nativeVirtualKeyCode: virtualKeyCode
});
await client.send("Input.dispatchKeyEvent", {
type: "keyUp",
key,
code,
windowsVirtualKeyCode: virtualKeyCode,
nativeVirtualKeyCode: virtualKeyCode
});
}
function countOccurrences(value, needle) {
return value.split(needle).length - 1;
}