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
308 lines
9.1 KiB
JavaScript
308 lines
9.1 KiB
JavaScript
const cssPresentationHintProperties = new Set([
|
|
"animation",
|
|
"animation-iteration-count",
|
|
"backface-visibility",
|
|
"transform",
|
|
"will-change"
|
|
]);
|
|
|
|
const presentationSchedulerPattern =
|
|
/\b(?:requestAnimationFrame|requestIdleCallback)\s*\(|\bnew\s+ResizeObserver\s*\(/g;
|
|
const inlineCompositorHintPattern = /\bwillChange\s*:|translate(?:3d|Z)\s*\(/g;
|
|
const presentationWorkReasonPattern =
|
|
/(?:\/\/|\/\*|\*)\s*presentation-work:\s*\S/;
|
|
|
|
export const agentGuiPresentationStylesheet =
|
|
"packages/agent/gui/app/renderer/agentactivity.css";
|
|
|
|
export const requiredAgentGuiPresentationDeclarations = [
|
|
{
|
|
property: "animation-play-state",
|
|
selector:
|
|
'.agent-gui-node__layout[data-agent-gui-visible="false"] :where(*,*::before,*::after)',
|
|
value: "paused !important"
|
|
},
|
|
{
|
|
property: "content-visibility",
|
|
selector: '.agent-gui-node__layout[data-agent-gui-visible="false"]',
|
|
value: "hidden"
|
|
},
|
|
{
|
|
property: "animation",
|
|
selector:
|
|
'.agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-track,.agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-item',
|
|
value: "none"
|
|
},
|
|
{
|
|
property: "will-change",
|
|
selector:
|
|
'.agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-track,.agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-item',
|
|
value: "auto"
|
|
}
|
|
];
|
|
|
|
export function countPresentationSchedulers(source) {
|
|
return countMatches(
|
|
maskJavaScriptCommentsAndStrings(source),
|
|
() => new RegExp(presentationSchedulerPattern.source, "g")
|
|
);
|
|
}
|
|
|
|
export function countInlineCompositorHints(source) {
|
|
return countMatches(
|
|
maskJavaScriptComments(source),
|
|
() => new RegExp(inlineCompositorHintPattern.source, "g")
|
|
);
|
|
}
|
|
|
|
export function isPresentationSchedulerLine(sourceLine) {
|
|
return new RegExp(presentationSchedulerPattern.source).test(sourceLine);
|
|
}
|
|
|
|
export function isInlineCompositorHintLine(sourceLine) {
|
|
return new RegExp(inlineCompositorHintPattern.source).test(sourceLine);
|
|
}
|
|
|
|
export function hasPresentationWorkReason(contentLines, lineNumber) {
|
|
const line = contentLines[lineNumber - 1] ?? "";
|
|
const previousLine = contentLines[lineNumber - 2] ?? "";
|
|
return (
|
|
presentationWorkReasonPattern.test(line) ||
|
|
presentationWorkReasonPattern.test(previousLine)
|
|
);
|
|
}
|
|
|
|
export function analyzeAgentGuiPresentationCss(relativePath, source) {
|
|
const declarations = parseCssDeclarations(source);
|
|
const hints = [];
|
|
const violations = [];
|
|
|
|
for (const declaration of declarations) {
|
|
if (isForbiddenTransitionAll(declaration)) {
|
|
violations.push({
|
|
line: declaration.line,
|
|
message:
|
|
"`transition: all` is forbidden on AgentGUI surfaces; name the properties that actually animate",
|
|
rule: "no-transition-all"
|
|
});
|
|
}
|
|
if (!isPresentationHint(declaration)) {
|
|
continue;
|
|
}
|
|
const fingerprint = presentationHintFingerprint(relativePath, declaration);
|
|
hints.push({ ...declaration, fingerprint });
|
|
}
|
|
|
|
const missingRequiredDeclarations =
|
|
relativePath === agentGuiPresentationStylesheet
|
|
? findMissingRequiredDeclarations(declarations)
|
|
: [];
|
|
|
|
return { hints, missingRequiredDeclarations, violations };
|
|
}
|
|
|
|
export function presentationHintFingerprint(relativePath, declaration) {
|
|
return `${relativePath} | ${declaration.selector} | ${declaration.property}: ${declaration.value}`;
|
|
}
|
|
|
|
export function validatePresentationHintReasons(hints, reasons) {
|
|
const hintFingerprints = new Set(hints.map((hint) => hint.fingerprint));
|
|
const missing = [...hintFingerprints]
|
|
.filter((fingerprint) => {
|
|
const reason = reasons[fingerprint];
|
|
return typeof reason !== "string" || reason.trim().length === 0;
|
|
})
|
|
.sort();
|
|
const stale = Object.keys(reasons)
|
|
.filter((fingerprint) => !hintFingerprints.has(fingerprint))
|
|
.sort();
|
|
return { missing, stale };
|
|
}
|
|
|
|
function findMissingRequiredDeclarations(declarations) {
|
|
return requiredAgentGuiPresentationDeclarations.filter(
|
|
(required) =>
|
|
!declarations.some(
|
|
(declaration) =>
|
|
declaration.selector === required.selector &&
|
|
declaration.property === required.property &&
|
|
declaration.value === required.value
|
|
)
|
|
);
|
|
}
|
|
|
|
function isForbiddenTransitionAll({ property, value }) {
|
|
if (property === "transition-property") {
|
|
return value
|
|
.split(",")
|
|
.some((candidate) => candidate.trim().toLowerCase() === "all");
|
|
}
|
|
if (property !== "transition") {
|
|
return false;
|
|
}
|
|
return value.split(",").some((candidate) => {
|
|
const firstToken = candidate.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
return firstToken === "all";
|
|
});
|
|
}
|
|
|
|
function isPresentationHint({ property, value }) {
|
|
if (!cssPresentationHintProperties.has(property)) {
|
|
return false;
|
|
}
|
|
const normalizedValue = value.toLowerCase();
|
|
if (property === "will-change") {
|
|
return normalizedValue !== "auto";
|
|
}
|
|
if (property === "transform") {
|
|
return /translate(?:3d|z)\s*\(/i.test(value);
|
|
}
|
|
if (property === "backface-visibility") {
|
|
return normalizedValue === "hidden";
|
|
}
|
|
if (property === "animation-iteration-count") {
|
|
return normalizedValue
|
|
.split(",")
|
|
.some((candidate) => candidate.trim() === "infinite");
|
|
}
|
|
return property === "animation" && /\binfinite\b/i.test(value);
|
|
}
|
|
|
|
function parseCssDeclarations(source) {
|
|
const maskedSource = maskCssComments(source);
|
|
const declarations = [];
|
|
const blockPattern = /([^{}]+)\{([^{}]*)\}/g;
|
|
|
|
for (const match of maskedSource.matchAll(blockPattern)) {
|
|
const rawSelector = match[1] ?? "";
|
|
const rawBody = match[2] ?? "";
|
|
const selector = normalizeCssFragment(rawSelector);
|
|
if (shouldSkipCssBlock(selector)) {
|
|
continue;
|
|
}
|
|
const bodyStart = (match.index ?? 0) + rawSelector.length + 1;
|
|
const declarationPattern = /(?:^|;)\s*([\w-]+)\s*:\s*([^;{}]+)/g;
|
|
for (const declarationMatch of rawBody.matchAll(declarationPattern)) {
|
|
const property = (declarationMatch[1] ?? "").toLowerCase();
|
|
const value = normalizeCssFragment(declarationMatch[2] ?? "");
|
|
if (!property || !value) {
|
|
continue;
|
|
}
|
|
const declarationStart =
|
|
bodyStart +
|
|
(declarationMatch.index ?? 0) +
|
|
(declarationMatch[0]?.indexOf(property) ?? 0);
|
|
declarations.push({
|
|
line: lineNumberAt(source, declarationStart),
|
|
property,
|
|
selector,
|
|
value
|
|
});
|
|
}
|
|
}
|
|
return declarations;
|
|
}
|
|
|
|
function shouldSkipCssBlock(selector) {
|
|
return (
|
|
selector.startsWith("@") ||
|
|
selector === "from" ||
|
|
selector === "to" ||
|
|
/^(?:\d+(?:\.\d+)?%)(?:\s*,\s*\d+(?:\.\d+)?%)*$/.test(selector)
|
|
);
|
|
}
|
|
|
|
function normalizeCssFragment(value) {
|
|
return value
|
|
.replace(/\s+/g, " ")
|
|
.replace(/\s*([>,+~])\s*/g, "$1")
|
|
.trim();
|
|
}
|
|
|
|
function maskCssComments(source) {
|
|
return source.replace(/\/\*[\s\S]*?\*\//g, (comment) =>
|
|
comment.replace(/[^\n]/g, " ")
|
|
);
|
|
}
|
|
|
|
function maskJavaScriptCommentsAndStrings(source) {
|
|
let result = "";
|
|
let cursor = 0;
|
|
while (cursor < source.length) {
|
|
const character = source[cursor];
|
|
const next = source[cursor + 1];
|
|
if (character === "/" && next === "/") {
|
|
const end = source.indexOf("\n", cursor);
|
|
const stop = end === -1 ? source.length : end;
|
|
result += " ".repeat(stop - cursor);
|
|
cursor = stop;
|
|
continue;
|
|
}
|
|
if (character === "/" && next === "*") {
|
|
const end = source.indexOf("*/", cursor + 2);
|
|
const stop = end === -1 ? source.length : end + 2;
|
|
result += source.slice(cursor, stop).replace(/[^\n]/g, " ");
|
|
cursor = stop;
|
|
continue;
|
|
}
|
|
if (character === '"' || character === "'" || character === "`") {
|
|
const start = cursor;
|
|
cursor += 1;
|
|
while (cursor < source.length) {
|
|
if (source[cursor] === "\\") {
|
|
cursor += 2;
|
|
continue;
|
|
}
|
|
if (source[cursor] === character) {
|
|
cursor += 1;
|
|
break;
|
|
}
|
|
cursor += 1;
|
|
}
|
|
result += source.slice(start, cursor).replace(/[^\n]/g, " ");
|
|
continue;
|
|
}
|
|
result += character;
|
|
cursor += 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function maskJavaScriptComments(source) {
|
|
let result = "";
|
|
let cursor = 0;
|
|
while (cursor < source.length) {
|
|
const character = source[cursor];
|
|
const next = source[cursor + 1];
|
|
if (character === "/" && next === "/") {
|
|
const end = source.indexOf("\n", cursor);
|
|
const stop = end === -1 ? source.length : end;
|
|
result += " ".repeat(stop - cursor);
|
|
cursor = stop;
|
|
continue;
|
|
}
|
|
if (character === "/" && next === "*") {
|
|
const end = source.indexOf("*/", cursor + 2);
|
|
const stop = end === -1 ? source.length : end + 2;
|
|
result += source.slice(cursor, stop).replace(/[^\n]/g, " ");
|
|
cursor = stop;
|
|
continue;
|
|
}
|
|
result += character;
|
|
cursor += 1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function countMatches(source, patternFactory) {
|
|
let count = 0;
|
|
for (const _match of source.matchAll(patternFactory())) {
|
|
count += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function lineNumberAt(source, index) {
|
|
return source.slice(0, index).split("\n").length;
|
|
}
|