feat(agent-browser): add opt-in preview data injection

This commit is contained in:
2026-08-26 22:36:54 +08:00
parent 14fec70108
commit 38d63a9b7b
11 changed files with 1024 additions and 3 deletions

View File

@@ -62,6 +62,10 @@ export type PreviewDataSessionInvalidationReason =
| 'replaced'
| 'manual';
export type PreviewDataSessionInvalidationListener = (
reason: PreviewDataSessionInvalidationReason,
) => void;
export type PreviewDataSessionOpenInput = {
projectPath: string;
origin: string;
@@ -232,6 +236,7 @@ export class PreviewDataSessionManager {
private readonly getAccountBinding: typeof getWorksSquareAccountBinding;
private readonly isCurrentAccountBinding: typeof isCurrentWorksSquareAccountBinding;
private readonly unsubscribeWorksSession: () => void;
private readonly invalidationListeners = new Set<PreviewDataSessionInvalidationListener>();
private session: InternalSession | null = null;
private boundAccountBinding: WorksSquareAccountBinding | null;
private disposed = false;
@@ -285,6 +290,7 @@ export class PreviewDataSessionManager {
throw new PreviewDataSessionError('runtime_unavailable', 'Preview data runtime is unavailable');
}
const token = tokenBytes.toString('base64url');
this.invalidate('replaced');
const snapshot: PreviewDataSessionSnapshot = Object.freeze({
projectPath: active.path,
projectId: active.projectId,
@@ -307,6 +313,14 @@ export class PreviewDataSessionManager {
return this.session?.snapshot ?? null;
}
subscribeInvalidation(listener: PreviewDataSessionInvalidationListener): () => void {
if (this.disposed) return () => undefined;
this.invalidationListeners.add(listener);
return () => {
this.invalidationListeners.delete(listener);
};
}
getInjectionValue(hostPort = this.hostPort): PreviewDataInjectionValue | null {
const session = this.session;
if (!session || !Number.isSafeInteger(hostPort) || hostPort < 1 || hostPort > 65_535) {
@@ -409,8 +423,16 @@ export class PreviewDataSessionManager {
}
}
invalidate(_reason: PreviewDataSessionInvalidationReason = 'manual'): void {
invalidate(reason: PreviewDataSessionInvalidationReason = 'manual'): void {
if (!this.session) return;
this.session = null;
for (const listener of this.invalidationListeners) {
try {
listener(reason);
} catch {
// A browser cleanup observer must not interrupt token invalidation.
}
}
}
dispose(): void {
@@ -418,6 +440,7 @@ export class PreviewDataSessionManager {
this.disposed = true;
this.invalidate('main_shutdown');
this.unsubscribeWorksSession();
this.invalidationListeners.clear();
}
}