@cyber-dash-tech/revela 0.18.8 → 0.18.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -224,6 +224,7 @@ async function handleRequest(req: Request): Promise<Response> {
224
224
  if (url.pathname === "/api/comment" && req.method === "POST") {
225
225
  const session = validateSession(url.searchParams.get("token"))
226
226
  if (!session.ok) return session.response
227
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
227
228
  return handleComment(req, session.value)
228
229
  }
229
230
 
@@ -236,6 +237,7 @@ async function handleRequest(req: Request): Promise<Response> {
236
237
  if (url.pathname === "/api/comments" && req.method === "POST") {
237
238
  const session = validateSession(url.searchParams.get("token"))
238
239
  if (!session.ok) return session.response
240
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
239
241
  return handleReviewCommentCreate(req, session.value)
240
242
  }
241
243
 
@@ -243,6 +245,7 @@ async function handleRequest(req: Request): Promise<Response> {
243
245
  if (applyMatch && req.method === "POST") {
244
246
  const session = validateSession(url.searchParams.get("token"))
245
247
  if (!session.ok) return session.response
248
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
246
249
  return handleReviewCommentApply(decodeURIComponent(applyMatch[1]), req, session.value)
247
250
  }
248
251
 
@@ -250,6 +253,7 @@ async function handleRequest(req: Request): Promise<Response> {
250
253
  if (stopMatch && req.method === "POST") {
251
254
  const session = validateSession(url.searchParams.get("token"))
252
255
  if (!session.ok) return session.response
256
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
253
257
  return handleReviewCommentStop(decodeURIComponent(stopMatch[1]), session.value)
254
258
  }
255
259
 
@@ -257,6 +261,7 @@ async function handleRequest(req: Request): Promise<Response> {
257
261
  if (deleteMatch && req.method === "DELETE") {
258
262
  const session = validateSession(url.searchParams.get("token"))
259
263
  if (!session.ok) return session.response
264
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
260
265
  return handleReviewCommentDelete(decodeURIComponent(deleteMatch[1]), session.value)
261
266
  }
262
267
 
@@ -299,6 +304,7 @@ async function handleRequest(req: Request): Promise<Response> {
299
304
  if (url.pathname === "/api/visual-changes" && req.method === "POST") {
300
305
  const session = validateSession(url.searchParams.get("token"))
301
306
  if (!session.ok) return session.response
307
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
302
308
  return handleVisualChanges(req, session.value)
303
309
  }
304
310
 
@@ -311,6 +317,7 @@ async function handleRequest(req: Request): Promise<Response> {
311
317
  if (url.pathname === "/api/assets/save" && req.method === "POST") {
312
318
  const session = validateSession(url.searchParams.get("token"))
313
319
  if (!session.ok) return session.response
320
+ if (session.value.activeApplyCommentId) return applyLockedResponse()
314
321
  return handleAssetSave(req, session.value)
315
322
  }
316
323
 
@@ -323,6 +330,14 @@ async function handleRequest(req: Request): Promise<Response> {
323
330
  return textResponse("Not found", 404)
324
331
  }
325
332
 
333
+ function applyLockedResponse(): Response {
334
+ return jsonResponse({
335
+ ok: false,
336
+ code: "apply_locked",
337
+ error: "Wait for the current apply to finish before making another change.",
338
+ }, 409)
339
+ }
340
+
326
341
  async function handleAssetSearch(url: URL, session: EditSession): Promise<Response> {
327
342
  const query = (url.searchParams.get("query") || "").trim()
328
343
  if (!query) return jsonResponse({ ok: false, error: "query is required" }, 400)
@@ -888,17 +903,7 @@ function handleReviewCommentDelete(commentId: string, session: EditSession): Res
888
903
 
889
904
  async function enqueueOrStartPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise<Response> {
890
905
  session.applyQueue = session.applyQueue ?? []
891
- if (session.activeApplyCommentId === comment.id) {
892
- const current = readReviewComment(session.workspaceRoot, comment.id) ?? comment
893
- return jsonResponse({
894
- ok: true,
895
- requestId: current.lastApplyRequestId,
896
- commentRequestId: current.lastApplyRequestId,
897
- deckVersion: readDeckVersion(session).version,
898
- status: "pending",
899
- comment: current,
900
- })
901
- }
906
+ if (session.activeApplyCommentId) return applyLockedResponse()
902
907
 
903
908
  const queuedIndex = session.applyQueue.indexOf(comment.id)
904
909
  if (queuedIndex >= 0) {
@@ -912,20 +917,6 @@ async function enqueueOrStartPersistedReviewCommentApply(session: EditSession, c
912
917
  })
913
918
  }
914
919
 
915
- if (session.activeApplyCommentId) {
916
- session.applyQueue.push(comment.id)
917
- const queued = markReviewCommentQueued(session.workspaceRoot, comment.id) ?? comment
918
- session.lastActiveAt = Date.now()
919
- scheduleIdleStop()
920
- return jsonResponse({
921
- ok: true,
922
- deckVersion: readDeckVersion(session).version,
923
- status: "queued",
924
- queuePosition: session.applyQueue.length,
925
- comment: queued,
926
- })
927
- }
928
-
929
920
  return startPersistedReviewCommentApply(session, comment, body)
930
921
  }
931
922
 
@@ -1387,31 +1378,31 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1387
1378
  .comment-editor:empty::before { content: attr(data-placeholder); color: #94a3b8; pointer-events: none; }
1388
1379
  .ref-chip { display: inline-flex; align-items: center; max-width: 32ch; overflow: hidden; text-overflow: ellipsis; margin: 0 2px; padding: 1px 7px; border-radius: 999px; background: var(--ref-bg, #eff6ff); color: var(--ref-text, #1d4ed8); border: 1px solid var(--ref-border, #bfdbfe); font-weight: 800; white-space: nowrap; }
1389
1380
  .activity-panel { display: flex; flex: 1 1 auto; flex-direction: column; gap: 8px; min-height: 0; padding-top: 2px; }
1390
- .comment-thread { display: flex; flex: 1 1 auto; flex-direction: column; gap: 9px; min-height: 160px; overflow-y: auto; overflow-x: hidden; padding: 1px 2px 1px 1px; }
1381
+ .comment-thread { display: flex; flex: 1 1 auto; flex-direction: column; gap: 16px; min-height: 160px; overflow-y: auto; overflow-x: hidden; padding: 12px 14px; }
1391
1382
  .comment-thread:empty::before { content: "No activity yet. Leave a comment to start."; display: block; padding: 14px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; font-size: 12px; line-height: 1.45; background: #f8fafc; box-shadow: none; }
1392
- .comment-bubble { position: relative; display: flex; flex: 0 0 132px; flex-direction: column; min-height: 132px; max-height: 132px; overflow: hidden; border: 1px solid #e2e8f0; border-radius: 15px; padding: 11px 12px 10px 14px; background: #ffffff; color: #334155; font-size: 13px; line-height: 1.45; box-shadow: 0 1px 2px rgba(15,23,42,.04), inset 3px 0 0 var(--comment-accent, #e2e8f0); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; cursor: pointer; }
1393
- .comment-bubble:hover { transform: translateY(-1px); border-color: #cbd5e1; box-shadow: 0 8px 20px rgba(15,23,42,.07), inset 3px 0 0 var(--comment-accent, #e2e8f0); }
1394
- .comment-bubble.active { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12), 0 8px 20px rgba(15,23,42,.08), inset 3px 0 0 #2563eb; }
1395
- .comment-bubble.sending { --comment-accent: #3b82f6; border-color: #bfdbfe; background: #f8fbff; }
1396
- .comment-bubble.open { --comment-accent: #cbd5e1; border-color: #e2e8f0; background: #ffffff; }
1397
- .comment-bubble.queued { --comment-accent: #f59e0b; border-color: #fde68a; background: #fffdf4; }
1398
- .comment-bubble.applying { --comment-accent: #2563eb; border-color: #bfdbfe; background: #f8fbff; box-shadow: 0 8px 22px rgba(37,99,235,.1), inset 3px 0 0 var(--comment-accent); }
1399
- .comment-bubble.applied { --comment-accent: #16a34a; border-color: #bbf7d0; background: #f8fefb; }
1400
- .comment-bubble.updated { --comment-accent: #15803d; border-color: #bbf7d0; background: #f7fef9; }
1401
- .comment-bubble.stale { --comment-accent: #d97706; border-color: #fed7aa; background: #fffaf0; }
1402
- .comment-bubble.failed { --comment-accent: #dc2626; border-color: #fecaca; background: #fffafa; }
1383
+ .comment-bubble { position: relative; display: flex; flex: 0 0 150px; flex-direction: column; min-height: 150px; max-height: 150px; overflow: hidden; border: 1px solid #e2e8f0; border-radius: 15px; padding: 14px 15px 13px 16px; background: #ffffff; color: #334155; font-size: 13px; line-height: 1.45; box-shadow: 0 1px 2px rgba(15,23,42,.04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; cursor: pointer; }
1384
+ .comment-bubble:hover { transform: translateY(-1px); border-color: #cbd5e1; box-shadow: 0 8px 20px rgba(15,23,42,.07); }
1385
+ .comment-bubble.active { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12), 0 8px 20px rgba(15,23,42,.08); }
1386
+ .comment-bubble.sending { border-color: #bfdbfe; background: #f8fbff; }
1387
+ .comment-bubble.open { border-color: #e2e8f0; background: #ffffff; }
1388
+ .comment-bubble.queued { border-color: #fde68a; background: #fffdf4; }
1389
+ .comment-bubble.applying { border: 2px solid transparent; padding: 13px 14px 12px 15px; background: linear-gradient(#f8fbff, #f8fbff) padding-box, conic-gradient(from var(--comment-aurora-angle), #10b981, #14b8a6, #22d3ee, #2563eb, #7c3aed, #d946ef, #10b981) border-box; box-shadow: 0 0 0 1px rgba(20,184,166,.14), 0 0 18px rgba(6,182,212,.26), 0 0 28px rgba(124,58,237,.18), 0 8px 22px rgba(15,23,42,.08); animation: comment-aurora-flow 2.8s linear infinite; }
1390
+ .comment-bubble.applying.active { border-color: transparent; box-shadow: 0 0 0 3px rgba(6,182,212,.16), 0 0 22px rgba(20,184,166,.26), 0 0 32px rgba(124,58,237,.2), 0 8px 22px rgba(15,23,42,.09); }
1391
+ .comment-bubble.applied { border-color: #bbf7d0; background: #f8fefb; }
1392
+ .comment-bubble.updated { border-color: #bbf7d0; background: #f7fef9; }
1393
+ .comment-bubble.stale { border-color: #fed7aa; background: #fffaf0; }
1394
+ .comment-bubble.failed { border-color: #fecaca; background: #fffafa; }
1403
1395
  .comment-bubble-text { flex: 1 1 auto; min-height: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
1404
- .comment-bubble-state { margin-top: 8px; align-self: flex-start; padding: 2px 7px; border-radius: 999px; background: #f1f5f9; color: #475569; font-size: 11px; font-weight: 800; }
1396
+ .comment-bubble-state { margin-top: 8px; align-self: flex-start; padding: 0; background: transparent; color: #475569; font-size: 11px; font-weight: 800; }
1405
1397
  .comment-bubble-meta { margin-bottom: 6px; color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
1406
- .comment-actions { position: absolute; right: 9px; bottom: 9px; display: flex; gap: 6px; }
1398
+ .comment-actions { position: absolute; right: 11px; bottom: 11px; display: flex; gap: 6px; }
1407
1399
  .comment-action-button { display: inline-flex; align-items: center; justify-content: center; width: 30px; min-width: 30px; height: 30px; min-height: 30px; padding: 0; border-radius: 999px; border-color: #e2e8f0; background: rgba(255,255,255,.92); color: #475569; box-shadow: 0 1px 2px rgba(15,23,42,.06); }
1408
1400
  .comment-action-button:hover:not(:disabled) { background: #f1f5f9; color: #111827; transform: translateY(-1px); }
1409
1401
  .comment-action-button.danger { color: #dc2626; }
1410
1402
  .comment-action-button.stop { color: #b45309; }
1411
1403
  .comment-action-icon { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
1412
1404
  .comment-progress { margin-top: 7px; display: flex; flex-direction: column; gap: 4px; color: #475569; font-size: 12px; }
1413
- .comment-progress-line { display: flex; gap: 8px; align-items: flex-start; padding: 7px 8px; border: 1px solid #dbeafe; border-radius: 10px; background: #eff6ff; }
1414
- .comment-progress-line::before { content: ""; width: 7px; height: 7px; margin-top: 5px; border-radius: 999px; background: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,.12); flex: 0 0 auto; animation: progress-pulse 1.2s ease-in-out infinite; }
1405
+ .comment-progress-line { display: flex; gap: 8px; align-items: flex-start; padding: 0; border: 0; background: transparent; }
1415
1406
  .comment-raw { margin-top: 8px; color: #b91c1c; font-size: 12px; }
1416
1407
  .comment-raw summary { cursor: pointer; font-weight: 800; }
1417
1408
  .comment-raw pre { margin: 6px 0 0; max-height: 160px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; background: #f8fafc; border: 1px solid #fecaca; border-radius: 8px; padding: 8px; }
@@ -1433,12 +1424,12 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1433
1424
  .codex-log-modal .codex-log-list { margin: 0; padding: 14px; max-height: none; overflow: auto; }
1434
1425
  .codex-log-modal .codex-log-entry { background: transparent; }
1435
1426
  .codex-log-modal .codex-log-detail { max-height: 260px; }
1436
- .comment-bubble.updated .comment-bubble-state { background: #dcfce7; color: #166534; }
1437
- .comment-bubble.applied .comment-bubble-state { background: #dcfce7; color: #166534; }
1438
- .comment-bubble.applying .comment-bubble-state { background: #dbeafe; color: #1d4ed8; }
1439
- .comment-bubble.queued .comment-bubble-state { background: #fef3c7; color: #92400e; }
1440
- .comment-bubble.stale .comment-bubble-state { background: #ffedd5; color: #9a3412; }
1441
- .comment-bubble.failed .comment-bubble-state { background: #fee2e2; color: #991b1b; }
1427
+ .comment-bubble.updated .comment-bubble-state { color: #166534; }
1428
+ .comment-bubble.applied .comment-bubble-state { color: #166534; }
1429
+ .comment-bubble.applying .comment-bubble-state { color: #0f766e; }
1430
+ .comment-bubble.queued .comment-bubble-state { color: #92400e; }
1431
+ .comment-bubble.stale .comment-bubble-state { color: #9a3412; }
1432
+ .comment-bubble.failed .comment-bubble-state { color: #991b1b; }
1442
1433
  .inspect-actions { display: flex; flex-direction: column; gap: 8px; }
1443
1434
  .inspect-options { display: flex; flex-direction: column; gap: 5px; }
1444
1435
  .inspect-options label { color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
@@ -1465,10 +1456,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1465
1456
  .asset-card.is-saving::after { content: ""; position: absolute; inset: 0; background: rgba(15,23,42,.32); }
1466
1457
  .asset-card.is-saving .asset-save { z-index: 1; }
1467
1458
  .asset-card.is-saved-candidate .asset-thumb { opacity: .72; }
1459
+ @property --comment-aurora-angle { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
1468
1460
  @keyframes spin { to { transform: rotate(360deg); } }
1469
- @keyframes progress-pulse { 0%, 100% { transform: scale(.85); opacity: .68; } 50% { transform: scale(1.16); opacity: 1; } }
1461
+ @keyframes comment-aurora-flow { to { --comment-aurora-angle: 360deg; } }
1470
1462
  @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
1471
- @media (prefers-reduced-motion: reduce) { .comment-bubble, .comment-progress-line::before, .skeleton-line, .spinner { animation: none !important; transition: none !important; } .comment-bubble:hover { transform: none; } }
1463
+ @media (prefers-reduced-motion: reduce) { .comment-bubble, .skeleton-line, .spinner { animation: none !important; transition: none !important; } .comment-bubble:hover { transform: none; } }
1472
1464
  .asset-search { display: grid; grid-template-columns: minmax(0, 1fr) 118px; gap: 8px; }
1473
1465
  .asset-search input, .asset-search select { min-width: 0; padding: 10px 11px; border: 1px solid #dbe3ef; border-radius: 12px; background: #ffffff; color: #111827; font: inherit; font-size: 12px; font-weight: 700; outline: none; }
1474
1466
  .asset-search input:focus, .asset-search select:focus { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
@@ -1511,12 +1503,17 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1511
1503
  .composer-send .spinner + span { display: none; }
1512
1504
  .composer-icon { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
1513
1505
  .status { min-height: 20px; color: #64748b; font-size: 13px; line-height: 1.45; }
1506
+ .apply-lock-overlay { position: absolute; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(248,250,252,.52); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); pointer-events: auto; }
1507
+ .apply-lock-overlay.open { display: flex; }
1508
+ .apply-lock-card { width: min(320px, calc(100% - 48px)); padding: 16px 18px; border: 1px solid rgba(20,184,166,.26); border-radius: 16px; background: rgba(255,255,255,.94); color: #0f172a; box-shadow: 0 24px 70px rgba(15,23,42,.2), 0 0 36px rgba(6,182,212,.16); text-align: center; }
1509
+ .apply-lock-card strong { display: block; margin-bottom: 4px; font-size: 13px; font-weight: 900; }
1510
+ .apply-lock-card span { color: #64748b; font-size: 12px; line-height: 1.4; }
1514
1511
  @media (max-width: 900px) { .app { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; } .resize-handle { display: none; } aside { max-height: 48vh; min-width: 0; border-left: 0; border-top: 1px solid #e2e8f0; } .deck-nav { bottom: 10px; } .asset-search { grid-template-columns: 1fr; } }
1515
1512
  </style>
1516
1513
  </head>
1517
1514
  <body class="${bodyClass}">
1518
1515
  <main class="app">
1519
- <section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="commentHighlightLayer" class="comment-highlight-layer" aria-hidden="true"></div><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div><div id="visualMoveHandle" class="visual-move-handle" aria-hidden="true"></div><div id="visualResizeHandle" class="visual-resize-handle" aria-hidden="true"></div><div id="visualEditToolbar" class="visual-edit-toolbar" aria-live="polite"><span id="visualEditCount">No unsaved visual changes</span><button id="visualUndo" type="button">Undo</button><button id="visualReset" type="button">Reset</button><button id="visualSave" class="save-visual" type="button">Save Changes</button></div><nav class="deck-nav" aria-label="Deck navigation"><button id="deckPrev" type="button" title="Previous slide (ArrowLeft / ArrowUp / PageUp)">Previous</button><div id="deckCounter" class="deck-nav-status" aria-live="polite">-- / --</div><button id="deckNext" type="button" title="Next slide (ArrowRight / ArrowDown / Space / PageDown)">Next</button></nav></section>
1516
+ <section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="commentHighlightLayer" class="comment-highlight-layer" aria-hidden="true"></div><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div><div id="visualMoveHandle" class="visual-move-handle" aria-hidden="true"></div><div id="visualResizeHandle" class="visual-resize-handle" aria-hidden="true"></div><div id="visualEditToolbar" class="visual-edit-toolbar" aria-live="polite"><span id="visualEditCount">No unsaved visual changes</span><button id="visualUndo" type="button">Undo</button><button id="visualReset" type="button">Reset</button><button id="visualSave" class="save-visual" type="button">Save Changes</button></div><nav class="deck-nav" aria-label="Deck navigation"><button id="deckPrev" type="button" title="Previous slide (ArrowLeft / ArrowUp / PageUp)">Previous</button><div id="deckCounter" class="deck-nav-status" aria-live="polite">-- / --</div><button id="deckNext" type="button" title="Next slide (ArrowRight / ArrowDown / Space / PageDown)">Next</button></nav><div id="applyLockOverlay" class="apply-lock-overlay" aria-hidden="true"><div class="apply-lock-card"><strong>Applying deck edit...</strong><span>Preview is locked until Codex finishes.</span></div></div></section>
1520
1517
  <div id="resizeHandle" class="resize-handle" role="separator" aria-label="Resize editor panel" aria-orientation="vertical" title="Drag to resize editor. Double-click to reset."></div>
1521
1518
  <aside>
1522
1519
  <div>
@@ -1692,6 +1689,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1692
1689
  assetShuffleButton: null,
1693
1690
  assetResults: null,
1694
1691
  editSavedAssets: null,
1692
+ applyLockOverlay: null,
1695
1693
  codexLogModal: null,
1696
1694
  codexLogBackdrop: null,
1697
1695
  codexLogClose: null,
@@ -1749,6 +1747,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1749
1747
  els.assetShuffleButton = document.getElementById('assetShuffleButton');
1750
1748
  els.assetResults = document.getElementById('assetResults');
1751
1749
  els.editSavedAssets = document.getElementById('editSavedAssets');
1750
+ els.applyLockOverlay = document.getElementById('applyLockOverlay');
1752
1751
  els.codexLogModal = document.getElementById('codexLogModal');
1753
1752
  els.codexLogBackdrop = document.getElementById('codexLogBackdrop');
1754
1753
  els.codexLogClose = document.getElementById('codexLogClose');
@@ -1758,7 +1757,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1758
1757
 
1759
1758
  els.inspectLanguage = document.getElementById('inspectLanguage');
1760
1759
 
1761
- if (!els.frame || !els.commentHighlightLayer || !els.hitbox || !els.resizeHandle || !els.visualMoveHandle || !els.visualResizeHandle || !els.visualEditToolbar || !els.visualEditCount || !els.visualUndo || !els.visualReset || !els.visualSave || !els.deckPrev || !els.deckNext || !els.deckCounter || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectComment || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.localAssetToggle || !els.localAssetMenu || !els.assetSearchToggle || !els.assetSearchBack || !els.assetSearchView || !els.assetQuery || !els.assetPurpose || !els.assetSearchButton || !els.assetShuffleButton || !els.assetResults || !els.editSavedAssets || !els.codexLogModal || !els.codexLogBackdrop || !els.codexLogClose || !els.codexLogTitle || !els.codexLogBody || !els.status) {
1760
+ if (!els.frame || !els.commentHighlightLayer || !els.hitbox || !els.resizeHandle || !els.visualMoveHandle || !els.visualResizeHandle || !els.visualEditToolbar || !els.visualEditCount || !els.visualUndo || !els.visualReset || !els.visualSave || !els.deckPrev || !els.deckNext || !els.deckCounter || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectComment || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.localAssetToggle || !els.localAssetMenu || !els.assetSearchToggle || !els.assetSearchBack || !els.assetSearchView || !els.assetQuery || !els.assetPurpose || !els.assetSearchButton || !els.assetShuffleButton || !els.assetResults || !els.editSavedAssets || !els.applyLockOverlay || !els.codexLogModal || !els.codexLogBackdrop || !els.codexLogClose || !els.codexLogTitle || !els.codexLogBody || !els.status) {
1762
1761
  throw new Error('Editor boot failed: required DOM nodes are missing.');
1763
1762
  }
1764
1763
 
@@ -1787,10 +1786,19 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1787
1786
  closeCodexLogModal();
1788
1787
  return;
1789
1788
  }
1789
+ if (isApplyLocked()) {
1790
+ event.preventDefault();
1791
+ setStatus(applyLockStatus());
1792
+ return;
1793
+ }
1790
1794
  closeLocalAssetMenu();
1791
1795
  clearHover();
1792
1796
  return;
1793
1797
  }
1798
+ if (isApplyLocked() && !isTextInputTarget(event.target)) {
1799
+ event.preventDefault();
1800
+ return;
1801
+ }
1794
1802
  if (isTextInputTarget(event.target) || event.metaKey || event.ctrlKey || event.altKey) return;
1795
1803
  if (['ArrowDown', 'ArrowRight', ' ', 'PageDown'].includes(event.key)) {
1796
1804
  event.preventDefault();
@@ -1830,6 +1838,10 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1830
1838
  if (event.ctrlKey || event.metaKey) event.preventDefault();
1831
1839
  });
1832
1840
  els.hitbox.addEventListener('wheel', (event) => {
1841
+ if (isApplyLocked()) {
1842
+ event.preventDefault();
1843
+ return;
1844
+ }
1833
1845
  const win = els.frame.contentWindow;
1834
1846
  if (!win) return;
1835
1847
  event.preventDefault();
@@ -1885,7 +1897,38 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1885
1897
  return state.mode === 'inspect' ? els.inspectComment : els.comment;
1886
1898
  }
1887
1899
 
1900
+ function isApplyLocked() {
1901
+ return state.pendingComments.some((comment) => comment && comment.status === 'applying');
1902
+ }
1903
+
1904
+ function applyLockStatus() {
1905
+ return 'Wait for the current apply to finish before making another change.';
1906
+ }
1907
+
1908
+ function updateApplyLockUi() {
1909
+ const locked = isApplyLocked();
1910
+ els.applyLockOverlay.classList.toggle('open', locked);
1911
+ els.applyLockOverlay.setAttribute('aria-hidden', locked ? 'false' : 'true');
1912
+ els.comment.setAttribute('contenteditable', 'true');
1913
+ els.inspectComment.setAttribute('contenteditable', 'true');
1914
+ els.localAssetToggle.disabled = locked;
1915
+ els.assetSearchToggle.disabled = locked;
1916
+ els.assetSearchBack.disabled = locked;
1917
+ els.assetQuery.disabled = locked;
1918
+ els.assetPurpose.disabled = locked;
1919
+ if (locked) {
1920
+ closeLocalAssetMenu();
1921
+ closeAssetSearchPanel();
1922
+ clearHoverSilently();
1923
+ }
1924
+ updateDeckNavControls();
1925
+ updateVisualToolbar();
1926
+ updateAssetShuffleState();
1927
+ updateSendState();
1928
+ }
1929
+
1888
1930
  function toggleLocalAssetMenu() {
1931
+ if (isApplyLocked()) return setStatus(applyLockStatus());
1889
1932
  setLocalAssetMenuOpen(!els.localAssetMenu.classList.contains('open'));
1890
1933
  }
1891
1934
 
@@ -1900,6 +1943,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1900
1943
  }
1901
1944
 
1902
1945
  function toggleAssetSearchPanel() {
1946
+ if (isApplyLocked()) return setStatus(applyLockStatus());
1903
1947
  const open = !els.assetSearchView.classList.contains('open');
1904
1948
  setAssetSearchOpen(open);
1905
1949
  }
@@ -1927,6 +1971,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1927
1971
  }
1928
1972
 
1929
1973
  function startEditorResize(event) {
1974
+ if (isApplyLocked()) return setStatus(applyLockStatus());
1930
1975
  event.preventDefault();
1931
1976
  const currentWidth = Number.parseFloat(getComputedStyle(document.querySelector('.app')).getPropertyValue('--editor-width')) || DEFAULT_EDITOR_WIDTH;
1932
1977
  state.resizeDrag = { startX: event.clientX, startWidth: currentWidth };
@@ -2072,6 +2117,12 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2072
2117
  }
2073
2118
 
2074
2119
  function renderVisualHandles(target) {
2120
+ if (isApplyLocked()) {
2121
+ state.hoverVisualTarget = null;
2122
+ renderVisualMoveHandle(null);
2123
+ renderVisualResizeHandle(null);
2124
+ return;
2125
+ }
2075
2126
  state.hoverVisualTarget = target && (isDirectResizable(target) || isDirectMovable(target)) ? target : null;
2076
2127
  renderVisualMoveHandle(state.hoverVisualTarget);
2077
2128
  renderVisualResizeHandle(state.hoverVisualTarget);
@@ -2090,6 +2141,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2090
2141
  }
2091
2142
 
2092
2143
  function startVisualMove(event) {
2144
+ if (isApplyLocked()) return false;
2093
2145
  const target = state.hoverVisualTarget;
2094
2146
  if (!target || !isDirectMovable(target)) return false;
2095
2147
  event.preventDefault();
@@ -2148,6 +2200,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2148
2200
  }
2149
2201
 
2150
2202
  function startVisualResize(event) {
2203
+ if (isApplyLocked()) return false;
2151
2204
  const target = state.hoverVisualTarget;
2152
2205
  if (!target || !isDirectResizable(target)) return false;
2153
2206
  event.preventDefault();
@@ -2218,6 +2271,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2218
2271
  }
2219
2272
 
2220
2273
  function undoVisualChange() {
2274
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2221
2275
  const change = state.visualChanges.pop();
2222
2276
  if (!change) return;
2223
2277
  const target = elementFromVisualChange(change);
@@ -2229,6 +2283,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2229
2283
  }
2230
2284
 
2231
2285
  function resetVisualChanges() {
2286
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2232
2287
  while (state.visualChanges.length) {
2233
2288
  const change = state.visualChanges.pop();
2234
2289
  const target = change ? elementFromVisualChange(change) : null;
@@ -2242,14 +2297,16 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2242
2297
 
2243
2298
  function updateVisualToolbar() {
2244
2299
  const count = state.visualChanges.length;
2300
+ const locked = isApplyLocked();
2245
2301
  els.visualEditToolbar.classList.toggle('active', count > 0);
2246
2302
  els.visualEditCount.textContent = count === 0 ? 'No unsaved visual changes' : count + ' unsaved visual change' + (count === 1 ? '' : 's');
2247
- els.visualUndo.disabled = count === 0 || state.savingVisualChanges;
2248
- els.visualReset.disabled = count === 0 || state.savingVisualChanges;
2249
- els.visualSave.disabled = count === 0 || state.savingVisualChanges;
2303
+ els.visualUndo.disabled = locked || count === 0 || state.savingVisualChanges;
2304
+ els.visualReset.disabled = locked || count === 0 || state.savingVisualChanges;
2305
+ els.visualSave.disabled = locked || count === 0 || state.savingVisualChanges;
2250
2306
  }
2251
2307
 
2252
2308
  async function saveVisualChanges() {
2309
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2253
2310
  if (!state.visualChanges.length || state.savingVisualChanges) return;
2254
2311
  state.savingVisualChanges = true;
2255
2312
  updateVisualToolbar();
@@ -2352,16 +2409,19 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2352
2409
  function updateDeckNavControls() {
2353
2410
  const total = state.deckSlideCount;
2354
2411
  const current = total > 0 ? state.deckSlideIndex + 1 : 0;
2412
+ const locked = isApplyLocked();
2355
2413
  els.deckCounter.textContent = total > 0 ? current + ' / ' + total : '-- / --';
2356
- els.deckPrev.disabled = total <= 1 || state.deckSlideIndex <= 0;
2357
- els.deckNext.disabled = total <= 1 || state.deckSlideIndex >= total - 1;
2414
+ els.deckPrev.disabled = locked || total <= 1 || state.deckSlideIndex <= 0;
2415
+ els.deckNext.disabled = locked || total <= 1 || state.deckSlideIndex >= total - 1;
2358
2416
  }
2359
2417
 
2360
2418
  function prevDeckSlide() {
2419
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2361
2420
  goToDeckSlide(state.deckSlideIndex - 1);
2362
2421
  }
2363
2422
 
2364
2423
  function nextDeckSlide() {
2424
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2365
2425
  goToDeckSlide(state.deckSlideIndex + 1);
2366
2426
  }
2367
2427
 
@@ -2519,6 +2579,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2519
2579
  }
2520
2580
 
2521
2581
  function onHover(event) {
2582
+ if (isApplyLocked()) return clearHoverSilently();
2522
2583
  try {
2523
2584
  initFrame();
2524
2585
  const directTarget = visualTargetFromPointer(event);
@@ -2538,6 +2599,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2538
2599
  }
2539
2600
 
2540
2601
  function onClick(event) {
2602
+ if (isApplyLocked()) return;
2541
2603
  try {
2542
2604
  initFrame();
2543
2605
  const target = selectable(targetFromPointer(event));
@@ -2554,6 +2616,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2554
2616
  }
2555
2617
 
2556
2618
  function onPointerDown(event) {
2619
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2557
2620
  if (!event.ctrlKey && !event.metaKey && pointerIsOnVisualMoveHandle(event)) {
2558
2621
  if (startVisualMove(event)) return;
2559
2622
  }
@@ -2572,6 +2635,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2572
2635
  }
2573
2636
 
2574
2637
  async function sendComment() {
2638
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2575
2639
  syncReferencesFromComment(false, els.comment);
2576
2640
  syncSelectedAssetFromComment();
2577
2641
  const text = getCommentText().trim();
@@ -2652,6 +2716,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2652
2716
  }
2653
2717
 
2654
2718
  async function applyPersistedComment(commentId) {
2719
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2655
2720
  const comment = state.pendingComments.find((item) => item.id === commentId);
2656
2721
  if (!comment || comment.status === 'applying' || comment.status === 'queued') return;
2657
2722
  updatePendingCommentStatus(commentId, 'applying', { baseDeckVersion: state.deckVersion || comment.baseDeckVersion, progressEvent: null, eventLog: [], failureRaw: '', failureMessage: '' });
@@ -2680,6 +2745,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2680
2745
  }
2681
2746
 
2682
2747
  async function stopPersistedComment(commentId) {
2748
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2683
2749
  const comment = state.pendingComments.find((item) => item.id === commentId);
2684
2750
  if (!comment || !canStopPersistedComment(comment.status)) return;
2685
2751
  try {
@@ -2695,6 +2761,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2695
2761
  }
2696
2762
 
2697
2763
  async function deletePersistedComment(commentId) {
2764
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2698
2765
  const comment = state.pendingComments.find((item) => item.id === commentId);
2699
2766
  if (!comment || !canDeletePersistedComment(comment.status)) return;
2700
2767
  try {
@@ -2748,6 +2815,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2748
2815
  }
2749
2816
 
2750
2817
  async function searchAssets(nextBatch) {
2818
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2751
2819
  const query = (els.assetQuery.value || '').trim();
2752
2820
  if (!query || state.assetSearchBusy) return;
2753
2821
  const key = query + '\u0000' + (els.assetPurpose.value || 'illustration');
@@ -2794,7 +2862,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2794
2862
  }
2795
2863
 
2796
2864
  function updateAssetShuffleState() {
2797
- els.assetShuffleButton.disabled = state.assetSearchBusy || !state.assetCandidates.length;
2865
+ els.assetSearchButton.disabled = isApplyLocked() || state.assetSearchBusy;
2866
+ els.assetShuffleButton.disabled = isApplyLocked() || state.assetSearchBusy || !state.assetCandidates.length;
2798
2867
  }
2799
2868
 
2800
2869
  function renderAssetCandidates() {
@@ -2823,6 +2892,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2823
2892
  }
2824
2893
 
2825
2894
  async function saveCandidate(index) {
2895
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2826
2896
  const candidate = state.assetCandidates[index];
2827
2897
  if (!candidate) return;
2828
2898
  state.assetSavingIndex = index;
@@ -2963,6 +3033,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2963
3033
  }
2964
3034
 
2965
3035
  function addAssetToComment(asset) {
3036
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2966
3037
  if (!asset) return;
2967
3038
  state.selectedAsset = asset;
2968
3039
  removeAssetChip();
@@ -2977,6 +3048,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2977
3048
  }
2978
3049
 
2979
3050
  function onAssetDragOver(event) {
3051
+ if (isApplyLocked()) return;
2980
3052
  if (!state.draggingAsset) return;
2981
3053
  event.preventDefault();
2982
3054
  if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
@@ -2991,6 +3063,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2991
3063
  }
2992
3064
 
2993
3065
  async function onAssetDrop(event) {
3066
+ if (isApplyLocked()) return setStatus(applyLockStatus());
2994
3067
  const asset = state.draggingAsset || findSavedAsset(event.dataTransfer?.getData('application/revela-asset-id'));
2995
3068
  if (!asset) return;
2996
3069
  event.preventDefault();
@@ -3171,6 +3244,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3171
3244
  }
3172
3245
 
3173
3246
  async function sendAssetPlacement(asset, placement) {
3247
+ if (isApplyLocked()) return setStatus(applyLockStatus());
3174
3248
  const modeText = placement.targetMode === 'replace'
3175
3249
  ? 'replace the image at the drop target'
3176
3250
  : placement.targetMode === 'insert-into'
@@ -3201,6 +3275,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3201
3275
  }
3202
3276
 
3203
3277
  function toggleReference(target) {
3278
+ if (isApplyLocked()) return setStatus(applyLockStatus());
3204
3279
  if (!target) {
3205
3280
  setStatus('No selectable deck element found under pointer.');
3206
3281
  return;
@@ -3478,6 +3553,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3478
3553
 
3479
3554
  function renderCommentThread(scrollToBottom = true) {
3480
3555
  els.commentThread.textContent = '';
3556
+ const locked = isApplyLocked();
3481
3557
  state.pendingComments.forEach((comment) => {
3482
3558
  const bubble = document.createElement('div');
3483
3559
  bubble.className = 'comment-bubble ' + comment.status + (comment.id === state.activeCommentId ? ' active' : '');
@@ -3510,7 +3586,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3510
3586
  if (refs) bubble.appendChild(refs);
3511
3587
  bubble.appendChild(text);
3512
3588
  bubble.appendChild(status);
3513
- if (comment.persisted && canApplyPersistedComment(comment.status)) {
3589
+ if (!locked && comment.persisted && canApplyPersistedComment(comment.status)) {
3514
3590
  const actions = document.createElement('div');
3515
3591
  actions.className = 'comment-actions';
3516
3592
  const apply = commentActionButton(isReapplyStatus(comment.status) ? 'Re-apply' : 'Apply', isReapplyStatus(comment.status) ? '${lucideIcon("refresh-cw", "comment-action-icon")}' : '${lucideIcon("play", "comment-action-icon")}');
@@ -3518,7 +3594,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3518
3594
  actions.appendChild(apply);
3519
3595
  bubble.appendChild(actions);
3520
3596
  }
3521
- if (comment.persisted && canStopPersistedComment(comment.status)) {
3597
+ if (!locked && comment.persisted && canStopPersistedComment(comment.status)) {
3522
3598
  const actions = bubble.querySelector('.comment-actions') || document.createElement('div');
3523
3599
  actions.className = 'comment-actions';
3524
3600
  const stop = commentActionButton('Stop', '${lucideIcon("square", "comment-action-icon")}', 'stop');
@@ -3526,7 +3602,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3526
3602
  actions.appendChild(stop);
3527
3603
  if (!actions.parentElement) bubble.appendChild(actions);
3528
3604
  }
3529
- if (comment.persisted && canDeletePersistedComment(comment.status)) {
3605
+ if (!locked && comment.persisted && canDeletePersistedComment(comment.status)) {
3530
3606
  const actions = bubble.querySelector('.comment-actions') || document.createElement('div');
3531
3607
  actions.className = 'comment-actions';
3532
3608
  const remove = commentActionButton('Delete', '${lucideIcon("trash-2", "comment-action-icon")}', 'danger');
@@ -3564,6 +3640,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3564
3640
  }
3565
3641
  els.commentThread.appendChild(bubble);
3566
3642
  });
3643
+ updateApplyLockUi();
3567
3644
  if (scrollToBottom) scrollCommentThreadToBottom();
3568
3645
  }
3569
3646
 
@@ -3788,12 +3865,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3788
3865
  }
3789
3866
 
3790
3867
  function updateSendState() {
3868
+ const locked = isApplyLocked();
3791
3869
  if (state.sendingEdit) setButtonLoading(els.send, true, 'Sending...');
3792
3870
  else setButtonLoading(els.send, false, sendButtonHtml, true);
3793
- els.send.disabled = state.sendingEdit || !getCommentText().trim();
3871
+ els.send.disabled = locked || state.sendingEdit || !getCommentText().trim();
3794
3872
  if (state.inspecting) setButtonLoading(els.inspectButton, true, 'Getting insight...');
3795
3873
  else setButtonLoading(els.inspectButton, false, 'Get Insight');
3796
- els.inspectButton.disabled = state.inspecting || state.references.length === 0;
3874
+ els.inspectButton.disabled = locked || state.inspecting || state.references.length === 0;
3797
3875
  }
3798
3876
 
3799
3877
  function setButtonLoading(button, loading, label, html) {
@@ -3840,6 +3918,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
3840
3918
  }
3841
3919
 
3842
3920
  async function inspectCurrentSelection() {
3921
+ if (isApplyLocked()) return setStatus(applyLockStatus());
3843
3922
  if (!state.references.length || state.inspecting) return;
3844
3923
  const snapshot = collectReferenceSnapshot();
3845
3924
  const comment = getInspectComment();