donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
174 lines • 7.17 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.flushTbdSessions = flushTbdSessions;
const promises_1 = require("fs/promises");
const CodeGenerator_1 = require("../../../codegen/CodeGenerator");
const Logger_1 = require("../../../utils/Logger");
/**
* Pattern that matches `await page.tbd()` (with optional semicolons,
* whitespace variations, and variable assignment).
*/
const TBD_CALL_PATTERN = /^(\s*)(?:(?:const|let|var)\s+\w+\s*=\s*)?await\s+page\.tbd\(\s*\)\s*;?\s*$/;
/**
* Processes the given tbd() sessions: for each affected source file,
* replaces `await page.tbd()` lines with the generated Playwright code.
*
* Replacements are applied **bottom-up** (highest line number first) so
* that earlier replacements don't shift the line numbers of later ones.
*/
async function flushTbdSessions(sessions) {
if (sessions.length === 0) {
return 0;
}
// Group sessions by source file.
const sessionsByFile = new Map();
for (const session of sessions) {
const file = session.callSite.file;
const existing = sessionsByFile.get(file) ?? [];
existing.push(session);
sessionsByFile.set(file, existing);
}
let totalReplacements = 0;
for (const [filePath, fileSessions] of sessionsByFile) {
try {
const replacements = await rewriteFile(filePath, fileSessions);
totalReplacements += replacements;
}
catch (error) {
Logger_1.appLogger.error(`tbd: failed to rewrite ${filePath}`, error);
}
}
return totalReplacements;
}
async function rewriteFile(filePath, sessions) {
const original = await (0, promises_1.readFile)(filePath, 'utf-8');
const lines = original.split('\n');
// Find ALL lines in the file that contain page.tbd() calls.
const tbdLineIndices = [];
for (let i = 0; i < lines.length; i++) {
if (TBD_CALL_PATTERN.test(lines[i]) || lines[i].includes('page.tbd()')) {
tbdLineIndices.push(i);
}
}
if (tbdLineIndices.length === 0) {
Logger_1.appLogger.warn(`tbd: no page.tbd() calls found in ${filePath} — skipping`);
return 0;
}
// Match each session to the nearest tbd() line in the file. The stack
// trace line number may be off by a few lines due to TypeScript
// compilation, so we find the closest actual tbd() call.
const assignments = matchSessionsToLines(sessions, tbdLineIndices);
// Sort by line index descending (bottom-up) so replacements don't
// shift earlier line numbers.
const sorted = [...assignments].sort((a, b) => b.lineIndex - a.lineIndex);
let replacementCount = 0;
for (const { session, lineIndex } of sorted) {
const line = lines[lineIndex];
// Generate the replacement code.
const generatedCode = generateReplacementCode(session);
if (!generatedCode) {
Logger_1.appLogger.info(`tbd: no actions recorded for page.tbd() at ${filePath}:${lineIndex + 1} — removing the call`);
lines.splice(lineIndex, 1);
replacementCount++;
continue;
}
// Detect the indentation of the original line.
const indent = line.match(/^(\s*)/)?.[1] ?? ' ';
// Indent each line of the generated code to match.
const indentedCode = generatedCode
.split('\n')
.map((codeLine) => (codeLine.trim() ? indent + codeLine.trimStart() : ''))
.join('\n');
// Replace the tbd() line with the generated code.
lines.splice(lineIndex, 1, indentedCode);
replacementCount++;
}
if (replacementCount === 0) {
return 0;
}
// Reassemble and format.
let result = lines.join('\n');
try {
result = await (0, CodeGenerator_1.prettifyCode)(result);
}
catch (error) {
// If prettier fails (e.g. generated code has syntax issues), write
// the unformatted version so the user can still see what was generated.
Logger_1.appLogger.warn('tbd: prettier formatting failed, writing raw output', error);
}
await (0, promises_1.writeFile)(filePath, result, 'utf-8');
Logger_1.appLogger.info(`tbd: replaced ${replacementCount} page.tbd() call(s) in ${filePath}`);
return replacementCount;
}
/**
* Matches sessions to actual tbd() lines in the file. Each session is
* assigned to the nearest unmatched tbd() line (by line distance from
* the reported call-site).
*
* When there's only one session and one tbd() line, it's trivially matched.
* With multiple, we greedily assign each session to its closest available
* line, processing in order of ascending distance.
*/
function matchSessionsToLines(sessions, tbdLineIndices) {
const available = new Set(tbdLineIndices);
const results = [];
// Build (session, lineIndex, distance) tuples for all combinations.
const candidates = [];
for (const session of sessions) {
const reportedIndex = session.callSite.line - 1; // 0-based
for (const lineIndex of tbdLineIndices) {
candidates.push({
session,
lineIndex,
distance: Math.abs(reportedIndex - lineIndex),
});
}
}
// Sort by distance ascending so closest matches are assigned first.
candidates.sort((a, b) => a.distance - b.distance);
const assignedSessions = new Set();
for (const { session, lineIndex } of candidates) {
if (assignedSessions.has(session) || !available.has(lineIndex)) {
continue;
}
results.push({ session, lineIndex });
assignedSessions.add(session);
available.delete(lineIndex);
}
// Warn about unmatched sessions.
for (const session of sessions) {
if (!assignedSessions.has(session)) {
Logger_1.appLogger.warn(`tbd: could not match session (reported line ${session.callSite.line}) to any page.tbd() call in file`);
}
}
return results;
}
/**
* Converts a TbdSession's recorded actions into Playwright code.
*
* Manual interactions are converted using the existing codegen utility.
* AI instructions become `await page.ai(...)` calls.
*/
function generateReplacementCode(session) {
if (session.recordedActions.length === 0) {
return null;
}
const codeLines = [];
for (const action of session.recordedActions) {
if (action.type === 'aiInstruction') {
codeLines.push(`await page.ai(${JSON.stringify(action.instruction)});`);
}
else {
try {
const code = (0, CodeGenerator_1.convertProposedToolCallToPlaywrightCode)(action.toolCall);
codeLines.push(code);
}
catch (error) {
Logger_1.appLogger.warn(`tbd: failed to generate code for tool call ${action.toolCall.name}`, error);
codeLines.push(`// TODO: Could not generate code for ${action.toolCall.name}: ${JSON.stringify(action.toolCall.parameters)}`);
}
}
}
return codeLines.join('\n');
}
//# sourceMappingURL=fileRewriter.js.map