Files
jhonny-editor/bm/premiere-pro-mcp-main/uxp-plugin/index.cjs
T
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

583 lines
28 KiB
JavaScript
Executable File

"use strict";
const { entrypoints, host, storage } = require("uxp");
const ppro = require("premierepro");
const Protocol = globalThis.PremiereMcpProtocol;
const TranscriptSupport = globalThis.PremiereMcpTranscript;
const TranscriptImport = globalThis.PremiereMcpTranscriptImport || (typeof require === "function" ? require("./transcript-import.cjs") : null);
const WorkspaceSupport = globalThis.PremiereMcpWorkspace;
const EventSupport = globalThis.PremiereMcpEvents;
const Commands = globalThis.PremiereMcpCommands;
const workspaceBroker = WorkspaceSupport.createWorkspaceBroker({ fs: storage && storage.localFileSystem });
const eventJournal = EventSupport.createEventJournal({ capacity: 512 });
const transcriptImportRuntime = TranscriptImport && typeof TranscriptImport.createTranscriptImportRuntime === "function"
? TranscriptImport.createTranscriptImportRuntime({ ppro, TranscriptSupport, Protocol })
: null;
const commandRegistry = Commands.createCommandRegistry({
ppro,
Protocol,
workspace: workspaceBroker,
events: eventJournal,
storage: typeof globalThis !== "undefined" ? globalThis.localStorage : null,
transcriptImportHandler: transcriptImportRuntime && transcriptImportRuntime.importTranscript,
transcriptImportProbe: transcriptImportRuntime && transcriptImportRuntime.canImportTranscript
});
let socket = null;
let reconnectTimer = null;
let lastState = "";
let stateRevision = 0;
let fallbackPollTimer = null;
const operationTracker = Protocol.createOperationTracker();
const eventSubscriptions = [];
const targetEventSubscriptions = [];
let targetSubscriptionGeneration = 0;
entrypoints.setup({
panels: {
mcpBridgePanel: {
create() { subscribeHostEvents(); },
show() { publishState("panel.show"); },
destroy() { unsubscribeHostEvents(); stopFallbackPolling(); eventJournal.close(); void commandRegistry.dispose(); disconnect(); }
}
}
});
document.addEventListener("DOMContentLoaded", async () => {
document.getElementById("connect").addEventListener("click", connect);
document.getElementById("refresh").addEventListener("click", () => publishState("manual"));
document.getElementById("choose-workspace").addEventListener("click", chooseWorkspace);
document.getElementById("revoke-workspace").addEventListener("click", revokeWorkspace);
try { await workspaceBroker.initialize(); } catch (error) { setStatus(error.message || String(error)); }
try { await commandRegistry.initialize(); } catch (error) { setStatus(error.message || String(error)); }
renderWorkspaceStatus();
subscribeHostEvents();
connect();
startFallbackPolling();
});
async function capabilities() {
const value = await commandRegistry.capabilities();
value.commands["capabilities.get"].cancellable = false;
value.commands["state.get"].cancellable = false;
value.commands["operation.cancel"] = { supported: true, readOnly: false, scope: "preflight only" };
if (value.commands["frame.export"]) Object.assign(value.commands["frame.export"], {
cancellable: "preflight only", verification: "exporter return value", undoable: false, atomic: false
});
const supportedHost = TranscriptSupport.versionAtLeast(host && host.version, "25.6.0");
const transcriptImportHost = TranscriptSupport.versionAtLeast(host && host.version, "26.3.0");
const transcriptExportApi = supportedHost && !!(ppro.Transcript && ppro.Transcript.exportToJSON && ppro.Transcript.importFromJSON);
const transcriptNativeHasApi = supportedHost && !!(ppro.Transcript && typeof ppro.Transcript.hasTranscript === "function");
const transcriptHasApi = transcriptNativeHasApi || !!(supportedHost && ppro.Transcript && typeof ppro.Transcript.exportToJSON === "function");
Object.assign(value.commands, {
"transcript.export": { supported: transcriptExportApi, readOnly: true, minVersion: "25.6.0" },
"transcript.search": { supported: transcriptExportApi, readOnly: true, minVersion: "25.6.0" },
"transcript.has": {
supported: transcriptHasApi, readOnly: true, minVersion: transcriptNativeHasApi ? "26.3.0" : "25.6.0",
nativeCheckMinVersion: "26.3.0", nativeCheck: transcriptNativeHasApi,
fallback: transcriptNativeHasApi ? null : transcriptHasApi ? "export-probe" : null,
fallbackMinVersion: transcriptHasApi ? "25.6.0" : null
},
"transcript.import": {
supported: transcriptImportHost && !!transcriptImportRuntime && transcriptImportRuntime.canImportTranscript(),
readOnly: false,
destructive: true,
undoable: true,
idempotent: true,
minVersion: "26.3.0",
confirmationRequired: true,
verification: "bounded exact transcript-export SHA-256 readback"
},
"captions.inspect": { supported: supportedHost, readOnly: true, minVersion: "25.6.0" },
"captions.create": { supported: false, reason: "No documented Premiere UXP caption creation API." },
"captions.update": { supported: false, reason: "No documented Premiere UXP caption text/timing mutation API." },
"captions.delete": { supported: false, reason: "No documented Premiere UXP caption deletion API." }
});
value.hostVersion = host && host.version || null;
Object.assign(value, {
events: {
host: supportedHostEvents().map((event) => event.name),
stateNotifications: true,
journal: eventJournal.status(),
fallbackPolling: { enabled: true, intervalMs: 5000 },
operationLifecycle: ["started", "progress", "completed", "failed", "cancelled"]
},
operationSemantics: {
mutations: "Only action-based commands executed by Project.executeTransaction may claim an undo boundary.",
rollback: "No atomic rollback is claimed. Callers must inspect result verification metadata.",
cancellation: "Cooperative before a non-cancellable Premiere host call; no interruption is claimed after that boundary."
},
fallback: { backend: "cep", reason: "Use CEP/QE only when a command is absent or reports unsupported; never silently retry a failed UXP mutation." }
});
return value;
}
function castClipProjectItem(item) {
try {
const clip = ppro.ClipProjectItem.cast(item);
if (clip) return clip;
} catch (_) {}
throw new Error("The resolved project item is not a media clip");
}
async function selectedClipProjectItem(project) {
if (!ppro.ProjectUtils || typeof ppro.ProjectUtils.getSelection !== "function") {
throw new Error("Project panel selection is unavailable; pass projectItemId or projectItemName");
}
const selection = await ppro.ProjectUtils.getSelection(project);
const items = selection && await selection.getItems();
if (!items || items.length !== 1) throw new Error("Select exactly one media project item, or pass projectItemId/projectItemName");
return castClipProjectItem(items[0]);
}
async function findProjectItem(project, args) {
const wantedId = args && args.projectItemId != null ? String(args.projectItemId) : "";
const wantedName = args && args.projectItemName != null ? String(args.projectItemName) : "";
if (!wantedId && !wantedName) return selectedClipProjectItem(project);
const queue = [await project.getRootItem()];
while (queue.length) {
const folder = queue.shift();
const children = await folder.getItems();
for (let i = 0; i < children.length; i += 1) {
const item = children[i];
const itemId = typeof item.getId === "function" ? String(item.getId()) : "";
const candidate = TranscriptSupport.matchingClipCandidate(
item, itemId, wantedId, wantedName, castClipProjectItem
);
if (candidate.clip) return candidate.clip;
try {
const childFolder = ppro.FolderItem.cast(item);
if (childFolder) queue.push(childFolder);
} catch (_) {}
}
}
throw new Error("Project item not found");
}
async function transcriptContext(args) {
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
if (!ppro.Transcript || typeof ppro.Transcript.exportToJSON !== "function") throw new Error("Transcript APIs require Premiere Pro 25.6 or newer");
const clip = await findProjectItem(project, args || {});
const projectItem = ppro.ProjectItem.cast(clip);
return {
project,
projectGuid: String(project.guid),
clip,
projectItemId: String(await projectItem.getId()),
projectItemName: clip.name
};
}
async function exportTranscript(args) {
const context = await transcriptContext(args);
const json = await ppro.Transcript.exportToJSON(context.clip);
if (typeof json !== "string" || !json) throw new Error("The selected clip has no transcript");
TranscriptSupport.parseTranscriptJSON(json);
return {
projectGuid: context.projectGuid,
projectItemId: context.projectItemId,
projectItemName: context.projectItemName,
json
};
}
async function searchTranscript(args) {
const exported = await exportTranscript(args);
const result = TranscriptSupport.searchTranscriptJSON(exported.json, args.query, {
caseSensitive: args.caseSensitive, maxResults: args.maxResults
});
return Object.assign({ projectItemId: exported.projectItemId, projectItemName: exported.projectItemName }, result);
}
async function hasTranscript(args) {
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
if (!ppro.Transcript) throw new Error("Transcript APIs require Premiere Pro 25.6 or newer");
const clip = await findProjectItem(project, args || {});
const projectItem = ppro.ProjectItem.cast(clip);
const context = {
projectGuid: String(project.guid),
projectItemId: String(await projectItem.getId()),
projectItemName: clip.name,
clip
};
if (typeof ppro.Transcript.hasTranscript === "function") {
return {
projectGuid: context.projectGuid,
projectItemId: context.projectItemId,
projectItemName: context.projectItemName,
hasTranscript: !!await ppro.Transcript.hasTranscript(context.clip),
method: "native"
};
}
if (typeof ppro.Transcript.exportToJSON !== "function") throw new Error("This Premiere build cannot check transcripts");
const present = await TranscriptSupport.probeTranscriptExport(function () {
return ppro.Transcript.exportToJSON(context.clip);
});
return {
projectGuid: context.projectGuid,
projectItemId: context.projectItemId,
projectItemName: context.projectItemName,
hasTranscript: present,
method: "export-probe"
};
}
async function inspectCaptions() {
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
const sequence = await project.getActiveSequence();
if (!sequence) throw new Error("No active sequence");
const count = await sequence.getCaptionTrackCount();
const tracks = [];
for (let i = 0; i < count; i += 1) {
const track = await sequence.getCaptionTrack(i);
const items = await track.getTrackItems(ppro.Constants.TrackItemType.CLIP, false);
tracks.push({ id: track.id, index: await track.getIndex(), name: track.name, muted: await track.isMuted(), itemCount: items ? items.length : 0 });
}
return { sequenceId: String(sequence.guid), sequenceName: sequence.name, trackCount: count, tracks };
}
async function stateSnapshot() {
const project = await ppro.Project.getActiveProject();
const sequence = project && await project.getActiveSequence();
const position = sequence && await sequence.getPlayerPosition();
return {
revision: stateRevision,
projectOpen: !!project,
project: project ? { id: project.guid || null, name: project.name || null, path: project.path || null } : null,
sequenceOpen: !!sequence,
sequence: sequence ? { id: sequence.guid || null, name: sequence.name || null } : null,
playheadSeconds: position ? position.seconds : null
};
}
async function exportFrame(args, operation) {
assertNotCancelled(operation);
publishOperation("progress", operation, { phase: "preflight", progress: 0.1 });
const project = await ppro.Project.getActiveProject();
if (!project) throw new Error("No active project");
const sequence = await project.getActiveSequence();
if (!sequence) throw new Error("No active sequence");
if (!args.outputDirectory) throw new Error("outputDirectory is required");
const outputDirectory = await workspaceBroker.assertPathAllowed(args.outputDirectory, { label: "outputDirectory", kind: "directory" });
const filename = Protocol.safeFilename(args.filename);
const exporterFilename = Protocol.exporterFrameName(filename);
const position = args.seconds == null ? await sequence.getPlayerPosition() : await tickTime(args.seconds);
const size = await sequence.getFrameSize();
const width = positiveInt(args.width, size.width), height = positiveInt(args.height, size.height);
assertNotCancelled(operation);
operation.phase = "host_call";
publishOperation("progress", operation, { phase: "host_call", progress: 0.4, cancellable: false });
const returned = await ppro.Exporter.exportSequenceFrame(sequence, position, exporterFilename, outputDirectory, width, height);
if (returned !== true) throw new Error("Premiere did not confirm frame export; no output path is reported");
const path = Protocol.joinPath(outputDirectory, filename);
return {
path, width, height, seconds: position.seconds, exporterResult: returned,
operation: Protocol.operationSemantics({
mutatesProject: false,
verificationStatus: "not_verified",
verificationBoundary: "exporter_return_value",
verificationEvidence: [{ type: "host_return", value: returned }],
cancellationSupported: true
})
};
}
async function tickTime(seconds) {
if (ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function") return ppro.TickTime.createWithSeconds(Number(seconds));
throw new Error("This Premiere build cannot create TickTime; omit seconds to capture the playhead");
}
function positiveInt(value, fallback) { const n = Number(value == null ? fallback : value); if (!Number.isFinite(n) || n <= 0) throw new Error("frame dimensions must be positive"); return Math.round(n); }
async function dispatch(raw) {
let cmd;
let operation;
try {
cmd = Protocol.parseCommand(raw);
if (cmd.command === "operation.cancel") {
const result = operationTracker.requestCancel(cmd.args.requestId);
send(Protocol.envelope("result", { ok: true, result }, cmd.requestId));
return;
}
operation = operationTracker.begin(cmd.requestId, cmd.command);
publishOperation("started", operation, { phase: "preflight", progress: 0 });
let result;
if (cmd.command === "capabilities.get") result = await capabilities();
else if (cmd.command === "state.get") result = await stateSnapshot();
else if (cmd.command === "frame.export") result = await exportFrame(cmd.args, operation);
else if (cmd.command === "transcript.export") result = await exportTranscript(cmd.args);
else if (cmd.command === "transcript.search") result = await searchTranscript(cmd.args);
else if (cmd.command === "transcript.has") result = await hasTranscript(cmd.args);
else if (cmd.command === "captions.inspect") result = await inspectCaptions();
else {
assertNotCancelled(operation);
operation.phase = "host_call";
result = await commandRegistry.dispatch(cmd.command, cmd.args);
}
const response = Protocol.envelope("result", {
ok: true,
result,
operation: result && result.operation
? result.operation
: Protocol.operationSemantics(
cmd.command.indexOf("transition.video.") === 0 && cmd.command !== "transition.video.list"
? {
mutatesProject: true,
verificationStatus: "verified",
verificationBoundary: "project_executeTransaction_return",
verificationEvidence: [{ type: "transaction", accepted: true }],
undoSupported: true,
undoLabel: cmd.command === "transition.video.add"
? "Add video transition"
: "Remove video transition",
transactionActionGroup: true,
cancellationSupported: true
}
: {
mutatesProject: false,
verificationStatus: "verified",
verificationBoundary: "host_snapshot"
}
)
}, cmd.requestId);
// Validate the exact response before emitting a terminal success event. If
// serialization rejects an oversized result, the catch path emits only
// `failed`, never both `completed` and `failed` for one operation.
Protocol.serializeEnvelope(response);
publishOperation("completed", operation, { phase: "complete", progress: 1 });
send(response);
} catch (error) {
const cancelled = error && error.code === "UXP_OPERATION_CANCELLED";
const errorCode = cancelled
? "UXP_OPERATION_CANCELLED"
: error && typeof error.code === "string" && /^UXP_[A-Z0-9_]+$/.test(error.code)
? error.code
: "UXP_COMMAND_FAILED";
if (operation) publishOperation(cancelled ? "cancelled" : "failed", operation, {
phase: operation.phase, progress: null, error: error.message || String(error)
});
send(Protocol.envelope("result", {
ok: false,
error: {
code: errorCode,
message: error.message || String(error),
operation: Protocol.operationSemantics({ cancellationSupported: true })
}
}, cmd && cmd.requestId));
} finally {
if (operation) operationTracker.finish(operation);
}
}
function connect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
if (socket) try { socket.onclose = null; socket.close(); } catch (_) {}
const configuredUrl = document.getElementById("bridge-url").value;
const token = document.getElementById("bridge-token").value;
let url;
try {
url = WorkspaceSupport.validateLoopbackBridgeUrl(configuredUrl);
if (token) url.searchParams.set("token", token);
url = url.toString();
} catch (_) {
return scheduleReconnect("Invalid bridge URL");
}
setStatus("Connecting to " + url.origin + url.pathname);
try { socket = new WebSocket(url); } catch (e) { return scheduleReconnect(e.message); }
socket.onopen = async () => { setStatus("Connected"); send(Protocol.envelope("hello", await capabilities())); publishState("connected"); };
socket.onmessage = (event) => dispatch(event.data);
socket.onerror = () => setStatus("Bridge connection error");
socket.onclose = () => { void commandRegistry.dispose(); scheduleReconnect("Disconnected"); };
}
function scheduleReconnect(message) { setStatus(message + "; retrying in 2s"); reconnectTimer = setTimeout(connect, 2000); }
function send(value) { if (socket && socket.readyState === WebSocket.OPEN) socket.send(Protocol.serializeEnvelope(value)); }
function disconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = null;
if (socket) try { socket.onclose = null; socket.close(); } catch (_) {}
socket = null;
}
function publishOperation(name, operation, detail) {
send(Protocol.operationEvent(name, operation, detail));
}
function assertNotCancelled(operation) {
if (!operation || !operation.cancelRequested) return;
const error = new Error("Operation cancelled before the Premiere host call");
error.code = "UXP_OPERATION_CANCELLED";
throw error;
}
function supportedHostEvents() {
const constants = ppro.Constants || {};
const project = constants.ProjectEvent || {};
const sequence = constants.SequenceEvent || {};
const operation = constants.OperationCompleteEvent || {};
const encoder = ppro.EncoderManager || {};
const snapEvents = typeof EventSupport.createTimelineSnapEventDefinitions === "function"
? EventSupport.createTimelineSnapEventDefinitions(ppro.SnapEvent)
: [];
const operationBoundaryEvents = typeof EventSupport.createOperationBoundaryEventDefinitions === "function"
? EventSupport.createOperationBoundaryEventDefinitions(ppro.OperationCompleteEvent)
: [];
return [
hostEvent("project", "project.opened", project.OPENED, true),
hostEvent("project", "project.closed", project.CLOSED, true),
hostEvent("project", "project.dirty", project.DIRTY, true),
hostEvent("project", "project.activated", project.ACTIVATED, true),
hostEvent("project", "project.selection.changed", project.PROJECT_ITEM_SELECTION_CHANGED, true),
hostEvent("sequence", "sequence.activated", sequence.ACTIVATED, true),
hostEvent("sequence", "sequence.closed", sequence.CLOSED, true),
hostEvent("sequence", "sequence.selection.changed", sequence.SELECTION_CHANGED, true),
hostEvent("operation", "operation.import.complete", operation.IMPORT_MEDIA_COMPLETE || operation.EVENT_IMPORT_MEDIA_COMPLETE, false),
hostEvent("operation", "operation.export.complete", operation.EXPORT_MEDIA_COMPLETE || operation.EVENT_EXPORT_MEDIA_COMPLETE, false),
hostEvent("operation", "operation.effect.drop.complete", operation.EFFECT_DROP_COMPLETE || operation.EVENT_EFFECT_DROP_COMPLETE, false),
hostEvent("operation", "operation.generative.extend.complete", operation.GENERATIVE_EXTEND_COMPLETE || operation.EVENT_GENERATIVE_EXTEND_COMPLETE, false),
hostEvent("encoder", "encoder.queued", encoder.EVENT_RENDER_QUEUE, false),
hostEvent("encoder", "encoder.progress", encoder.EVENT_RENDER_PROGRESS, false, "encoder.progress"),
hostEvent("encoder", "encoder.complete", encoder.EVENT_RENDER_COMPLETE, false),
hostEvent("encoder", "encoder.error", encoder.EVENT_RENDER_ERROR, false),
hostEvent("encoder", "encoder.cancelled", encoder.EVENT_RENDER_CANCEL, false)
].concat(snapEvents, operationBoundaryEvents).filter((event) => event.eventName !== undefined && event.eventName !== null);
}
function hostEvent(category, name, eventName, stateInvalidating, coalesceKey) {
return { category, name, eventName, stateInvalidating, coalesceKey: coalesceKey || null };
}
function subscribeHostEvents() {
if (!ppro.EventManager) return;
if (!eventSubscriptions.length) {
supportedHostEvents().forEach((definition) => {
const handler = (event) => recordHostEvent(definition, event);
ppro.EventManager.addGlobalEventListener(definition.eventName, handler, false);
eventSubscriptions.push({ eventName: definition.eventName, handler });
});
}
void rebindTrackEvents();
}
function unsubscribeHostEvents() {
if (!ppro.EventManager) return;
unsubscribeTrackEvents();
eventSubscriptions.splice(0).forEach(({ eventName, handler }) => {
try { ppro.EventManager.removeGlobalEventListener(eventName, handler, false); } catch (_) {}
});
}
function recordHostEvent(definition, event) {
const detail = {};
if (event && typeof event === "object") {
if (Number.isFinite(Number(event.state))) detail.state = Number(event.state);
if (Number.isFinite(Number(event.progress))) detail.progress = Number(event.progress);
}
const receipt = eventJournal.recordHostEvent({
category: definition.category,
name: definition.name,
detail,
coalesceKey: definition.coalesceKey
});
if (receipt) send(Protocol.envelope("event", { name: "premiere.host.event", receipt }));
if (definition.stateInvalidating) publishState("host-event", definition.name);
if (definition.name === "project.opened" || definition.name === "project.closed" ||
definition.name === "project.activated" || definition.name === "sequence.activated" ||
definition.name === "sequence.closed") void rebindTrackEvents();
}
async function rebindTrackEvents() {
if (!ppro.EventManager || typeof ppro.EventManager.addEventListener !== "function") return;
const generation = ++targetSubscriptionGeneration;
unsubscribeTrackEvents(false);
try {
const project = await ppro.Project.getActiveProject();
const sequence = project && await project.getActiveSequence();
if (!sequence || generation !== targetSubscriptionGeneration) return;
const definitions = [
{ mediaType: "video", countMethod: "getVideoTrackCount", getMethod: "getVideoTrack", api: ppro.VideoTrack },
{ mediaType: "audio", countMethod: "getAudioTrackCount", getMethod: "getAudioTrack", api: ppro.AudioTrack }
];
for (let d = 0; d < definitions.length; d += 1) {
const definition = definitions[d];
if (typeof sequence[definition.countMethod] !== "function" || typeof sequence[definition.getMethod] !== "function") continue;
const count = Math.min(256, Math.max(0, Number(await sequence[definition.countMethod]()) || 0));
for (let index = 0; index < count; index += 1) {
if (generation !== targetSubscriptionGeneration) return;
const track = await sequence[definition.getMethod](index);
if (generation !== targetSubscriptionGeneration) return;
if (!track) continue;
const eventKinds = [
["changed", definition.api && definition.api.EVENT_TRACK_CHANGED],
["info_changed", definition.api && definition.api.EVENT_TRACK_INFO_CHANGED],
["lock_changed", definition.api && definition.api.EVENT_TRACK_LOCK_CHANGED]
];
for (let e = 0; e < eventKinds.length; e += 1) {
const kind = eventKinds[e][0], eventName = eventKinds[e][1];
if (eventName == null) continue;
const handler = () => recordTrackEvent(definition.mediaType, index, kind);
ppro.EventManager.addEventListener(track, eventName, handler, false);
targetEventSubscriptions.push({ target: track, eventName, handler });
}
}
}
} catch (_) {}
}
function unsubscribeTrackEvents(incrementGeneration = true) {
if (incrementGeneration) targetSubscriptionGeneration += 1;
targetEventSubscriptions.splice(0).forEach(({ target, eventName, handler }) => {
try { ppro.EventManager.removeEventListener(target, eventName, handler); } catch (_) {}
});
}
function recordTrackEvent(mediaType, trackIndex, eventKind) {
const receipt = eventJournal.recordHostEvent({
category: "track",
name: "track." + mediaType + "." + eventKind,
detail: { mediaType, trackIndex, eventKind }
});
if (receipt) send(Protocol.envelope("event", { name: "premiere.host.event", receipt }));
}
function startFallbackPolling() {
if (!fallbackPollTimer) fallbackPollTimer = setInterval(() => publishState("fallback-poll"), 5000);
}
function stopFallbackPolling() {
if (fallbackPollTimer) clearInterval(fallbackPollTimer);
fallbackPollTimer = null;
}
async function publishState(reason, hostEvent) {
try {
stateRevision += 1;
const state = await stateSnapshot();
const comparable = Object.assign({}, state, { revision: 0 });
const encoded = JSON.stringify(comparable);
if (reason !== "fallback-poll" || encoded !== lastState) {
lastState = encoded;
send(Protocol.envelope("event", {
name: "premiere.state.changed",
reason,
hostEvent: hostEvent || null,
state
}));
}
} catch (e) { setStatus(e.message); }
}
function setStatus(value) { const el = document.getElementById("status"); if (el) el.textContent = value; }
async function chooseWorkspace() {
try {
setStatus("Choose the folder that contains approved media, presets, and exports");
await workspaceBroker.requestRoot();
renderWorkspaceStatus();
setStatus("Workspace access granted");
} catch (error) {
setStatus(error.message || String(error));
}
}
async function revokeWorkspace() {
try {
await workspaceBroker.revoke();
renderWorkspaceStatus();
setStatus("Workspace access revoked");
} catch (error) {
setStatus(error.message || String(error));
}
}
function renderWorkspaceStatus() {
const element = document.getElementById("workspace-status"), value = workspaceBroker.status();
if (element) element.textContent = value.configured
? "Approved folder: " + value.rootName + "\nPersistent access: yes\nNative path: redacted\nCanonical path validation: " + value.canonicalPathValidation
: "No approved workspace folder";
}