| 1 |
/** Temporary external sessions: the open editor is the only command executor. */ |
| 2 |
(function (wp) { |
| 3 |
'use strict'; |
| 4 |
if (!wp || !wp.element) { return; } |
| 5 |
var el = wp.element.createElement, useState = wp.element.useState, useRef = wp.element.useRef, useEffect = wp.element.useEffect; |
| 6 |
var __ = wp.i18n.__; |
| 7 |
var cfg = window.bkbgAI || {}; |
| 8 |
var CLIENTS = [ |
| 9 |
{ id: 'codex', label: __( 'Codex', 'blockenberg' ) }, |
| 10 |
{ id: 'claude', label: __( 'Claude Code', 'blockenberg' ) }, |
| 11 |
{ id: 'cursor', label: __( 'Cursor', 'blockenberg' ) } |
| 12 |
]; |
| 13 |
function clientLabel(id) { |
| 14 |
for (var i = 0; i < CLIENTS.length; i++) { |
| 15 |
if (CLIENTS[i].id === id) { return CLIENTS[i].label; } |
| 16 |
} |
| 17 |
return CLIENTS[0].label; |
| 18 |
} |
| 19 |
function promptFor(session, client) { |
| 20 |
if (!session) { return ''; } |
| 21 |
if (session.prompts && session.prompts[client]) { return session.prompts[client]; } |
| 22 |
return session.prompt || ''; |
| 23 |
} |
| 24 |
function request(path, body, session, keepalive) { |
| 25 |
return window.fetch(cfg.restBase + '/ai/external' + path, { |
| 26 |
method: 'POST', credentials: 'same-origin', keepalive: !!keepalive, |
| 27 |
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce, 'X-Blockenberg-Editor': session ? session.editor_token : '' }, |
| 28 |
body: JSON.stringify(body || {}) |
| 29 |
}).then(function (r) { return r.json().then(function (json) { |
| 30 |
if (!r.ok) { var error = new Error(json.message || 'Connection failed'); error.status = r.status; throw error; } |
| 31 |
return json; |
| 32 |
}); }); |
| 33 |
} |
| 34 |
function Duration(props) { |
| 35 |
var tick = useState(0), setTick = tick[1]; |
| 36 |
useEffect(function () { |
| 37 |
if (!props.startedAt || props.finishedAt) { return; } |
| 38 |
var timer, disposed = false; |
| 39 |
function update() { |
| 40 |
if (disposed) { return; } |
| 41 |
setTick(function (value) { return value + 1; }); |
| 42 |
timer = setTimeout(update, 1000); |
| 43 |
} |
| 44 |
timer = setTimeout(update, 1000); |
| 45 |
return function () { disposed = true; clearTimeout(timer); }; |
| 46 |
}, [props.startedAt, props.finishedAt]); |
| 47 |
if (!props.startedAt) { return null; } |
| 48 |
var seconds = Math.max(0, Math.floor(((props.finishedAt || Date.now()) - props.startedAt) / 1000)); |
| 49 |
var hours = Math.floor(seconds / 3600), minutes = Math.floor(seconds % 3600 / 60); |
| 50 |
var value = (hours ? hours + 'h ' : '') + (hours || minutes ? minutes + 'm ' : '') + seconds % 60 + 's'; |
| 51 |
return el('div', { className: 'bkbg-ai-duration', 'aria-live': 'off' }, |
| 52 |
(props.finishedAt ? __('Worked for %s', 'blockenberg') : __('Working for %s', 'blockenberg')).replace('%s', value)); |
| 53 |
} |
| 54 |
window.bkbgDuration = Duration; |
| 55 |
function phaseLabels() { |
| 56 |
return { |
| 57 |
planning: __( 'Planning', 'blockenberg' ), |
| 58 |
building: __( 'Building the page', 'blockenberg' ), |
| 59 |
generating_image: __( 'Generating an image', 'blockenberg' ), |
| 60 |
reviewing: __( 'Checking the result', 'blockenberg' ), |
| 61 |
waiting: __( 'Waiting for an update', 'blockenberg' ), |
| 62 |
done: __( 'Finished', 'blockenberg' ), |
| 63 |
error: __( 'Needs attention', 'blockenberg' ) |
| 64 |
}; |
| 65 |
} |
| 66 |
function completedPlan( plan ) { |
| 67 |
return ( plan || [] ).map( function ( step ) { |
| 68 |
return Object.assign( {}, step, { status: 'done' } ); |
| 69 |
} ); |
| 70 |
} |
| 71 |
function renderExternalWork( live, options ) { |
| 72 |
if ( ! live ) { return null; } |
| 73 |
options = options || {}; |
| 74 |
var finished = !!( options.complete || live.phase === 'done' ); |
| 75 |
var plan = finished ? completedPlan( live.plan ) : ( live.plan || [] ); |
| 76 |
return el( 'div', { className: 'bkbg-ai-external-work' }, |
| 77 |
live.error ? el( 'p', { role: 'alert', className: 'bkbg-ai-activity__error' }, live.error ) : null, |
| 78 |
live.stale && live.phase !== 'done' && ! live.current ? el( 'p', { className: 'bkbg-ai-muted' }, __( 'No recent update. The agent may still be working; check its chat.', 'blockenberg' ) ) : null, |
| 79 |
plan.length ? el( 'ol', { className: 'bkbg-ai-external-plan' }, plan.map( function ( step ) { |
| 80 |
return el( 'li', { key: step.id, className: 'is-' + step.status, 'aria-current': step.status === 'active' ? 'step' : undefined }, |
| 81 |
el( 'span', { 'aria-hidden': true }, step.status === 'done' ? '✓' : step.status === 'active' ? '◉' : '○' ), step.label, |
| 82 |
el( 'span', { className: 'screen-reader-text' }, ' — ' + ( { pending: __( 'Pending', 'blockenberg' ), active: __( 'In progress', 'blockenberg' ), done: __( 'Completed', 'blockenberg' ) } )[ step.status ] ) ); |
| 83 |
} ) ) : null, |
| 84 |
options.renderActivity ? options.renderActivity( live.log, !! live.moving ) : null, |
| 85 |
live.captures && live.captures.length ? el( 'div', { className: 'bkbg-ai-section-previews', 'aria-label': __( 'Section screenshots for visual review', 'blockenberg' ) }, live.captures.map( function ( shot ) { |
| 86 |
return el( 'figure', { key: shot.id }, el( 'img', { src: shot.data, alt: __( 'Section preview', 'blockenberg' ) + ' · ' + shot.viewport } ), el( 'figcaption', null, shot.viewport === 'mobile' ? __( 'Mobile', 'blockenberg' ) : __( 'Desktop', 'blockenberg' ) ) ); |
| 87 |
} ) ) : null, |
| 88 |
live.visualReview ? el( 'p', { className: 'bkbg-ai-muted' }, ( live.visualReview.decision === 'passed' ? __( 'Visual review passed', 'blockenberg' ) : __( 'Visual corrections needed', 'blockenberg' ) ) + ': ' + live.visualReview.notes ) : null, |
| 89 |
live.images && live.images.length ? el( 'div', { className: 'bkbg-ai-external-images', 'aria-label': __( 'Generated images in the Media Library', 'blockenberg' ) }, live.images.map( function ( img ) { |
| 90 |
return el( 'a', { key: img.id, href: img.url, target: '_blank', rel: 'noopener noreferrer', title: img.alt || __( 'Generated image', 'blockenberg' ) }, el( 'img', { src: img.url, alt: img.alt || __( 'Generated image', 'blockenberg' ), loading: 'lazy' } ) ); |
| 91 |
} ) ) : null, |
| 92 |
live.counts && ( live.counts.actions || live.counts.images || live.counts.sections ) ? el( 'div', { className: 'bkbg-ai-external-progress__counts' }, |
| 93 |
live.counts.actions + ' ' + ( live.counts.actions === 1 ? __( 'action', 'blockenberg' ) : __( 'actions', 'blockenberg' ) ), |
| 94 |
live.counts.sections ? ' · ' + live.counts.sections + ' ' + ( live.counts.sections === 1 ? __( 'section', 'blockenberg' ) : __( 'sections', 'blockenberg' ) ) : '', |
| 95 |
' · ', |
| 96 |
live.counts.images + ' ' + ( live.counts.images === 1 ? __( 'image', 'blockenberg' ) : __( 'images', 'blockenberg' ) ) |
| 97 |
) : null |
| 98 |
); |
| 99 |
} |
| 100 |
window.bkbgRenderExternalWork = renderExternalWork; |
| 101 |
function Panel(props) { |
| 102 |
var state = useState(null), session = state[0], setSession = state[1]; |
| 103 |
var statusState = useState(''), status = statusState[0], setStatus = statusState[1]; |
| 104 |
var errorState = useState(''), error = errorState[0], setError = errorState[1]; |
| 105 |
var busyState = useState(false), busy = busyState[0], setBusy = busyState[1]; |
| 106 |
var logState = useState([]), log = logState[0], setLog = logState[1]; |
| 107 |
var logRef = useRef(log); logRef.current = log; |
| 108 |
var progressState = useState(null), progress = progressState[0], setProgress = progressState[1]; |
| 109 |
var currentState = useState(''), current = currentState[0], setCurrent = currentState[1]; |
| 110 |
var copyState = useState(''), copyStatus = copyState[0], setCopyStatus = copyState[1]; |
| 111 |
var promptField = useRef(null), pendingMessage = useRef(null); |
| 112 |
var captureState = useState([]), captures = captureState[0], setCaptures = captureState[1]; |
| 113 |
var runState = useState(null), run = runState[0], setRun = runState[1]; |
| 114 |
var clientState = useState('codex'), promptClient = clientState[0], setPromptClient = clientState[1]; |
| 115 |
var active = useRef(null), mounted = useRef(true), callbacks = useRef(props); |
| 116 |
var progressRef = useRef(null), capturesRef = useRef([]); |
| 117 |
var disconnectRef = useRef(function () {}); |
| 118 |
var liveKeyRef = useRef( '' ); |
| 119 |
callbacks.current = props; |
| 120 |
progressRef.current = progress; |
| 121 |
capturesRef.current = captures; |
| 122 |
function finishRun(owner, clock, message, options) { |
| 123 |
if (!clock || owner.reportedRun === clock.id) { return; } |
| 124 |
owner.reportedRun = clock.id; |
| 125 |
if (!callbacks.current.onCompleted) { return; } |
| 126 |
var report = ( options && options.report ) || progressRef.current || {}; |
| 127 |
var update = report.progress || {}; |
| 128 |
callbacks.current.onCompleted({ |
| 129 |
text: message, |
| 130 |
activity: logRef.current, |
| 131 |
meta: { startedAt: clock.startedAt, finishedAt: clock.finishedAt || Date.now() }, |
| 132 |
runId: clock.id, |
| 133 |
agent: report.agent || '', |
| 134 |
plan: options && options.complete ? completedPlan( update.plan ) : ( update.plan || [] ), |
| 135 |
images: report.images || [], |
| 136 |
visualReview: report.visual_review || null, |
| 137 |
counts: report.counts || { actions: 0, images: 0 } |
| 138 |
}); |
| 139 |
} |
| 140 |
function sendMessage(text) { |
| 141 |
var owner = active.current; |
| 142 |
if (!owner || !owner.session) { return Promise.reject(new Error(__('Connect an agent first.', 'blockenberg'))); } |
| 143 |
if (!pendingMessage.current || pendingMessage.current.text !== text) { pendingMessage.current = {id:'msg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2,12),text:text}; } |
| 144 |
var message = pendingMessage.current; |
| 145 |
return request('/' + owner.session.id + '/outbox', message, owner.session).then(function (saved) { |
| 146 |
if (active.current !== owner) { throw new Error('Connection changed. Check the saved conversation before resending.'); } |
| 147 |
if (callbacks.current.onMessages) { callbacks.current.onMessages([saved]); } |
| 148 |
pendingMessage.current = null; |
| 149 |
}); |
| 150 |
} |
| 151 |
function disconnect(message) { |
| 152 |
var old = active.current; |
| 153 |
active.current = null; |
| 154 |
if (old) { |
| 155 |
finishRun(old, old.run, __('External agent stopped.', 'blockenberg')); |
| 156 |
if (mounted.current && old.run) { setRun(Object.assign({}, old.run, { finishedAt: old.run.finishedAt || Date.now() })); } |
| 157 |
old.controller.abort(); |
| 158 |
clearTimeout(old.timer); |
| 159 |
if (old.session) { request('/' + old.session.id + '/revoke', {}, old.session, true).catch(function () {}); } |
| 160 |
} |
| 161 |
if (mounted.current) { |
| 162 |
setSession(null); setBusy(false); setCurrent(''); setCopyStatus(''); setStatus(message || __('Disconnected', 'blockenberg')); |
| 163 |
callbacks.current.onActive(false); |
| 164 |
if (callbacks.current.onProgress) { callbacks.current.onProgress(null); } |
| 165 |
liveKeyRef.current = ''; |
| 166 |
} |
| 167 |
} |
| 168 |
disconnectRef.current = disconnect; |
| 169 |
useEffect(function () { |
| 170 |
mounted.current = true; |
| 171 |
if (callbacks.current.onSender) { callbacks.current.onSender(sendMessage); } |
| 172 |
if (callbacks.current.onControls) { callbacks.current.onControls({ disconnect: function () { disconnectRef.current(); } }); } |
| 173 |
var leave = function () { disconnectRef.current(); }; |
| 174 |
window.addEventListener('pagehide', leave); |
| 175 |
return function () { |
| 176 |
mounted.current = false; |
| 177 |
disconnectRef.current(); |
| 178 |
if (callbacks.current.onSender) { callbacks.current.onSender(null); } |
| 179 |
if (callbacks.current.onControls) { callbacks.current.onControls(null); } |
| 180 |
window.removeEventListener('pagehide', leave); |
| 181 |
}; |
| 182 |
}, []); |
| 183 |
function start() { |
| 184 |
if (active.current || busy || props.disabled) { return; } |
| 185 |
setError(''); setBusy(true); setLog([]); setProgress(null); setCurrent(''); setCopyStatus(''); setRun(null); setCaptures([]); setPromptClient('codex'); callbacks.current.onActive(true); |
| 186 |
var scope = { mode: props.scope.mode, rootIds: props.scope.rootIds.slice() }; |
| 187 |
var owner = { controller: new window.AbortController(), session: null, timer: null, scope: scope, post: wp.data.select('core/editor').getCurrentPostId(), ack: null, seen: Object.create(null) }; |
| 188 |
active.current = owner; |
| 189 |
var manifest; |
| 190 |
try { manifest = callbacks.current.manifest(scope); } |
| 191 |
catch (e) { active.current = null; setBusy(false); callbacks.current.onActive(false); setError(e.message); return; } |
| 192 |
request('', manifest).then(function (value) { |
| 193 |
owner.session = value; |
| 194 |
if (active.current !== owner || !mounted.current) { request('/' + value.id + '/revoke', {}, value, true).catch(function () {}); return; } |
| 195 |
setSession(value); setBusy(false); setStatus(__('Waiting for an agent…', 'blockenberg')); |
| 196 |
poll(owner); |
| 197 |
}).catch(function (e) { |
| 198 |
if (active.current !== owner || !mounted.current) { return; } |
| 199 |
active.current = null; setBusy(false); callbacks.current.onActive(false); setError(e.message); |
| 200 |
}); |
| 201 |
} |
| 202 |
async function captureSection(owner, commandId, clientId, viewport) { |
| 203 |
setCurrent(__('Capturing section preview', 'blockenberg') + ' · ' + viewport); |
| 204 |
var shot = await callbacks.current.capture({ clientId: clientId, viewport: viewport }, { scope: owner.scope, signal: owner.controller.signal, render: function (content) { return request('/' + owner.session.id + '/render-section', {content:content}, owner.session); } }); |
| 205 |
if (active.current !== owner) { throw new Error('Disconnected during capture.'); } |
| 206 |
var id = commandId + '-' + viewport; |
| 207 |
var result = await request('/' + owner.session.id + '/captures/' + id, Object.assign({}, shot, { clientId: clientId }), owner.session); |
| 208 |
if (active.current !== owner) { throw new Error('Disconnected during capture.'); } |
| 209 |
setCaptures(function (previous) { return previous.filter(function (item) { return item.viewport !== viewport; }).concat([{ id: id, viewport: viewport, data: shot.data }]); }); |
| 210 |
return result; |
| 211 |
} |
| 212 |
function poll(owner) { |
| 213 |
if (active.current !== owner || owner.controller.signal.aborted) { return; } |
| 214 |
if (Date.now() >= owner.session.expires * 1000) { disconnect(__('Session expired. Create a new connection.', 'blockenberg')); return; } |
| 215 |
var ack = owner.ack; |
| 216 |
request('/' + owner.session.id + '/poll', { ack: ack }, owner.session).then(function (response) { |
| 217 |
if (active.current !== owner || owner.controller.signal.aborted) { return; } |
| 218 |
if (ack === owner.ack) { |
| 219 |
if (ack) { owner.seen[ack.id] = { id: ack.id, result: { error: 'Already executed and acknowledged. Read the current page before planning a new change.' } }; } |
| 220 |
owner.ack = null; |
| 221 |
} |
| 222 |
setError(''); |
| 223 |
setProgress(response); |
| 224 |
if (response.messages && callbacks.current.onMessages) { callbacks.current.onMessages(response.messages); } |
| 225 |
if (response.run) { |
| 226 |
var reported = response.run; |
| 227 |
// Translate the server clock once per task; local ticks then remain smooth. |
| 228 |
if (!owner.run || owner.run.id !== reported.id) { |
| 229 |
owner.clockOffset = Date.now() - response.server_time * 1000; |
| 230 |
owner.run = { id: reported.id, startedAt: reported.started_at * 1000 + owner.clockOffset }; |
| 231 |
} |
| 232 |
if (reported.finished_at) { owner.run.finishedAt = reported.finished_at * 1000 + owner.clockOffset; } |
| 233 |
setRun(Object.assign({}, owner.run)); |
| 234 |
if (owner.run.finishedAt) { finishRun(owner, owner.run, response.progress && response.progress.message || __('External agent finished.', 'blockenberg'), { complete: true, report: response }); } |
| 235 |
} |
| 236 |
setStatus(response.agent ? response.agent + ' · ' + __('Connected', 'blockenberg') : __('Waiting for an agent…', 'blockenberg')); |
| 237 |
if (response.agent) { setSession(function (previous) { return previous && previous.prompt ? Object.assign({}, previous, { prompt: '' }) : previous; }); } |
| 238 |
var command = response.command; |
| 239 |
if (!command) { return; } |
| 240 |
if (owner.seen[command.id]) { owner.ack = owner.seen[command.id]; return; } |
| 241 |
var label = callbacks.current.describe(command.tool, command.args); |
| 242 |
setCurrent(label); |
| 243 |
setLog(function (previous) { return previous.concat([{ key: command.id, text: label, state: 'run' }]).slice(-100); }); |
| 244 |
return Promise.resolve().then(function () { |
| 245 |
if (active.current !== owner || owner.controller.signal.aborted) { return { error: 'Disconnected before execution.' }; } |
| 246 |
if (wp.data.select('core/editor').getCurrentPostId() !== owner.post) { return { error: 'The editor is no longer on the connected page.' }; } |
| 247 |
var args = Object.assign({}, command.args); |
| 248 |
if (command.tool === 'create_image') { args.source = 'search'; } |
| 249 |
if (command.tool === 'capture_section') { return captureSection(owner, command.id, args.clientId, args.viewport); } |
| 250 |
return callbacks.current.execute(command.tool, args, { signal: owner.controller.signal, scope: owner.scope }); |
| 251 |
}).then(async function (result) { |
| 252 |
if (command.tool === 'build_section' && result && !result.error && result.focus && callbacks.current.capture) { |
| 253 |
result.captures = []; |
| 254 |
for (var viewport of ['desktop', 'mobile']) { |
| 255 |
if (active.current !== owner) { break; } |
| 256 |
try { result.captures.push(await captureSection(owner, command.id, result.focus, viewport)); } |
| 257 |
catch (error) { result.warnings = (result.warnings || []).concat(['Visual capture (' + viewport + ') failed: ' + error.message + '. Retry capture_section; do not claim the layout was visually checked.']); } |
| 258 |
} |
| 259 |
result.visual_review_required = true; |
| 260 |
} |
| 261 |
return result; |
| 262 |
}).catch(function (e) { return { error: e.message || String(e) }; }).then(function (result) { |
| 263 |
if (active.current !== owner) { return; } |
| 264 |
if (JSON.stringify(result).length > 500000) { result = { error: 'Result is too large. Request a shallower structure or individual block details.' }; } |
| 265 |
setCurrent(''); |
| 266 |
owner.ack = { id: command.id, result: result }; |
| 267 |
owner.seen[command.id] = owner.ack; |
| 268 |
setLog(function (previous) { return previous.map(function (item) { |
| 269 |
return item.key === command.id ? Object.assign({}, item, { state: result.error ? 'error' : 'done', error: result.error, warn: result.warnings && result.warnings[0] }) : item; |
| 270 |
}); }); |
| 271 |
}); |
| 272 |
}).catch(function (e) { |
| 273 |
if (active.current !== owner) { return; } |
| 274 |
if (e.status === 403 || e.status === 410) { disconnect(e.message); return; } |
| 275 |
setError(__('Connection interrupted; retrying. Commands are not replayed.', 'blockenberg')); |
| 276 |
}).then(function () { |
| 277 |
if (active.current === owner) { owner.timer = setTimeout(function () { poll(owner); }, owner.ack ? 50 : 1500); } |
| 278 |
}); |
| 279 |
} |
| 280 |
function copy() { |
| 281 |
if (!session || !promptFor(session, promptClient)) { return; } |
| 282 |
var owner = active.current, field = promptField.current; |
| 283 |
var text = field ? field.value : promptFor(session, promptClient); |
| 284 |
setCopyStatus(''); |
| 285 |
function report(value) { |
| 286 |
if (mounted.current && active.current === owner) { setCopyStatus(value); } |
| 287 |
} |
| 288 |
function fallback() { |
| 289 |
if (!mounted.current || active.current !== owner) { return; } |
| 290 |
var copied = false; |
| 291 |
try { |
| 292 |
if (field && field.isConnected) { |
| 293 |
var doc = field.ownerDocument, previousFocus = doc.activeElement; |
| 294 |
field.focus(); field.select(); field.setSelectionRange(0, text.length); |
| 295 |
// Clipboard API is unavailable on local HTTP sites. Keep this |
| 296 |
// synchronous with the click so the browser permits copying. |
| 297 |
copied = !!doc.execCommand('copy'); |
| 298 |
if (copied && previousFocus && previousFocus.focus) { previousFocus.focus(); } |
| 299 |
} |
| 300 |
} catch (e) { /* Leave the prompt selected for manual copying. */ } |
| 301 |
report(copied ? 'copied' : 'error'); |
| 302 |
} |
| 303 |
try { |
| 304 |
if (window.navigator.clipboard && typeof window.navigator.clipboard.writeText === 'function') { |
| 305 |
Promise.resolve(window.navigator.clipboard.writeText(text)).then(function () { report('copied'); }, fallback); |
| 306 |
return; |
| 307 |
} |
| 308 |
} catch (e) { /* A denied Clipboard API can still allow the selection fallback. */ } |
| 309 |
fallback(); |
| 310 |
} |
| 311 |
var update = progress && progress.progress; |
| 312 |
var phase = update ? update.phase : 'waiting'; |
| 313 |
var stale = !!(progress && progress.agent_seen && progress.server_time - progress.agent_seen > 90); |
| 314 |
var moving = !!current || (!!update && !stale && !error && ['planning', 'building', 'generating_image', 'reviewing'].indexOf(phase) !== -1); |
| 315 |
var labels = phaseLabels(); |
| 316 |
var paired = !!(progress && progress.agent); |
| 317 |
var needsPrompt = !!(session && !paired && promptFor(session, promptClient)); |
| 318 |
var live = { |
| 319 |
session: !!session, |
| 320 |
paired: paired, |
| 321 |
agent: (progress && progress.agent) || '', |
| 322 |
phase: phase, |
| 323 |
phaseLabel: current ? labels.building : (labels[phase] || labels.waiting), |
| 324 |
moving: moving, |
| 325 |
stale: stale, |
| 326 |
error: error, |
| 327 |
current: current, |
| 328 |
message: current || (update && update.message) || '', |
| 329 |
plan: (update && update.plan) || [], |
| 330 |
log: log, |
| 331 |
run: run, |
| 332 |
counts: (progress && progress.counts) || { actions: 0, images: 0 }, |
| 333 |
captures: captures, |
| 334 |
images: (progress && progress.images) || [], |
| 335 |
visualReview: (progress && progress.visual_review) || null |
| 336 |
}; |
| 337 |
var last = live.log && live.log.length ? live.log[ live.log.length - 1 ] : null; |
| 338 |
var key = session || busy ? [ |
| 339 |
session && session.id, paired, live.agent, live.phase, live.moving, live.stale, live.error, |
| 340 |
live.current, live.message, ( live.log || [] ).length, last && last.key, last && last.state, |
| 341 |
live.run && live.run.id, live.run && live.run.finishedAt, |
| 342 |
live.counts && live.counts.actions, live.counts && live.counts.images, live.counts && live.counts.sections, |
| 343 |
( live.plan || [] ).map( function ( step ) { return step.id + ':' + step.status; } ).join( ',' ), |
| 344 |
live.captures && live.captures.length, live.images && live.images.length |
| 345 |
].join( '\x1e' ) : ''; |
| 346 |
if ( key !== liveKeyRef.current ) { |
| 347 |
liveKeyRef.current = key; |
| 348 |
if ( callbacks.current.onProgress ) { callbacks.current.onProgress( session || busy ? live : null ); } |
| 349 |
} |
| 350 |
return el('div', { className: 'bkbg-ai-external-wrap' + (paired ? ' is-paired' : '') }, |
| 351 |
paired ? null : el('details', { className: 'bkbg-ai-external' + (session ? ' is-ready' : ''), open: needsPrompt ? true : undefined }, |
| 352 |
el('summary', null, session ? (needsPrompt ? __('Copy pairing prompt', 'blockenberg') : __('Connection', 'blockenberg')) : [el('span', { className: 'bkbg-ai-external-lead' }, __('Connect', 'blockenberg')), ' ', __('Codex, Claude or Cursor', 'blockenberg')], session ? el('span', { className: 'bkbg-ai-external-dot', 'aria-label': __('Session active', 'blockenberg') }) : null), |
| 353 |
el('div', { className: 'bkbg-ai-external-body' }, |
| 354 |
el('p', null, session |
| 355 |
? __('Paste into your local Codex, Claude Code, or Cursor chat. The session lasts one hour.', 'blockenberg') |
| 356 |
: __('Use a local Codex, Claude Code, or Cursor session. No OpenRouter key is needed.', 'blockenberg')), |
| 357 |
status && ! session ? el('p', { role: 'status' }, status) : null, |
| 358 |
error && ! session ? el('p', { role: 'alert', className: 'bkbg-ai-activity__error' }, error) : null, |
| 359 |
needsPrompt ? el.apply(null, ['div', { className: 'bkbg-ai-external-clients', role: 'group', 'aria-label': __('Agent prompt', 'blockenberg') }].concat(CLIENTS.map(function (client) { |
| 360 |
return el(wp.components.Button, { |
| 361 |
key: client.id, |
| 362 |
className: 'bkbg-ai-external-client', |
| 363 |
variant: promptClient === client.id ? 'primary' : 'secondary', |
| 364 |
'aria-pressed': promptClient === client.id, |
| 365 |
onClick: function () { setPromptClient(client.id); setCopyStatus(''); } |
| 366 |
}, client.label); |
| 367 |
}))) : null, |
| 368 |
needsPrompt ? el('textarea', { ref: promptField, className: 'bkbg-ai-external-prompt', readOnly: true, rows: 3, value: promptFor(session, promptClient), 'aria-label': __('Connection prompt', 'blockenberg'), onFocus: function (e) { e.target.select(); } }) : null, |
| 369 |
needsPrompt && copyStatus ? el('p', { role: copyStatus === 'error' ? 'alert' : 'status', className: copyStatus === 'error' ? 'bkbg-ai-activity__error' : 'bkbg-ai-muted' }, copyStatus === 'error' ? __('Automatic copying is blocked. Select the prompt and press ⌘C on Mac or Ctrl+C on Windows/Linux.', 'blockenberg') : __('Copied. Paste into %s.', 'blockenberg').replace('%s', clientLabel(promptClient))) : null, |
| 370 |
el('div', { className: 'bkbg-ai-external-buttons' }, |
| 371 |
!session ? el(wp.components.Button, { variant: 'secondary', disabled: busy || props.disabled, onClick: start }, busy ? __('Connecting…', 'blockenberg') : __('Create connection', 'blockenberg')) : null, |
| 372 |
needsPrompt ? el(wp.components.Button, { variant: 'primary', onClick: copy }, copyStatus === 'copied' ? __('Copied!', 'blockenberg') : __('Copy prompt', 'blockenberg')) : null, |
| 373 |
session ? el(wp.components.Button, { variant: 'tertiary', onClick: function () { disconnect(); } }, __('Cancel', 'blockenberg')) : null |
| 374 |
) |
| 375 |
) |
| 376 |
), |
| 377 |
paired ? el('div', { className: 'bkbg-ai-external-buttons bkbg-ai-external-buttons--session' }, |
| 378 |
el(wp.components.Button, { className: 'bkbg-ai-external-disconnect', variant: 'tertiary', isDestructive: true, onClick: function () { disconnect(); } }, __('Disconnect', 'blockenberg')) |
| 379 |
) : null |
| 380 |
); |
| 381 |
} |
| 382 |
window.bkbgExternalPanel = Panel; |
| 383 |
})(window.wp); |
| 384 |
|