mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
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:
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user