feat: snapshot split output format for scoped tokens

Scoped tokens get a split snapshot: trusted @refs section (for click/fill)
separated from untrusted web content in an envelope. Ref names truncated
to 50 chars in trusted section. Root tokens unchanged (backward compat).
Resume command also uses split format for scoped tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-05 11:23:14 -07:00
parent ec7f281a40
commit 617fe8073c
2 changed files with 47 additions and 3 deletions
+13 -2
View File
@@ -348,7 +348,14 @@ export async function handleMetaCommand(
// ─── Snapshot ─────────────────────────────────────
case 'snapshot': {
const snapshotResult = await handleSnapshot(args, bm);
const isScoped = tokenInfo && tokenInfo.clientId !== 'root';
const snapshotResult = await handleSnapshot(args, bm, {
splitForScoped: !!isScoped,
});
// Scoped tokens get split format (refs outside envelope); root gets basic wrapping
if (isScoped) {
return snapshotResult; // already has envelope from split format
}
return wrapUntrustedContent(snapshotResult, bm.getCurrentUrl());
}
@@ -361,7 +368,11 @@ export async function handleMetaCommand(
case 'resume': {
bm.resume();
// Re-snapshot to capture current page state after human interaction
const snapshot = await handleSnapshot(['-i'], bm);
const isScoped2 = tokenInfo && tokenInfo.clientId !== 'root';
const snapshot = await handleSnapshot(['-i'], bm, { splitForScoped: !!isScoped2 });
if (isScoped2) {
return `RESUMED\n${snapshot}`;
}
return `RESUMED\n${wrapUntrustedContent(snapshot, bm.getCurrentUrl())}`;
}
+34 -1
View File
@@ -132,7 +132,8 @@ function parseLine(line: string): ParsedNode | null {
*/
export async function handleSnapshot(
args: string[],
bm: BrowserManager
bm: BrowserManager,
securityOpts?: { splitForScoped?: boolean },
): Promise<string> {
const opts = parseSnapshotArgs(args);
const page = bm.getPage();
@@ -403,5 +404,37 @@ export async function handleSnapshot(
output.unshift(`[Context: iframe src="${frameUrl}"]`);
}
// Split output for scoped tokens: trusted refs + untrusted text
if (securityOpts?.splitForScoped) {
const trustedRefs: string[] = [];
const untrustedLines: string[] = [];
for (const line of output) {
// Lines starting with @ref are interactive elements (trusted metadata)
const refMatch = line.match(/^(\s*)@(e\d+|c\d+)\s+\[([^\]]+)\]\s*(.*)/);
if (refMatch) {
const [, indent, ref, role, rest] = refMatch;
// Truncate element name/content to 50 chars for trusted section
const nameMatch = rest.match(/^"(.+?)"/);
let truncName = nameMatch ? nameMatch[1] : rest.trim();
if (truncName.length > 50) truncName = truncName.slice(0, 47) + '...';
trustedRefs.push(`${indent}@${ref} [${role}] "${truncName}"`);
}
// All lines go to untrusted section (full content)
untrustedLines.push(line);
}
const parts: string[] = [];
if (trustedRefs.length > 0) {
parts.push('INTERACTIVE ELEMENTS (trusted — use these @refs for click/fill):');
parts.push(...trustedRefs);
parts.push('');
}
parts.push('═══ BEGIN UNTRUSTED WEB CONTENT ═══');
parts.push(...untrustedLines);
parts.push('═══ END UNTRUSTED WEB CONTENT ═══');
return parts.join('\n');
}
return output.join('\n');
}