pixelkiln 0.32.1 → 0.33.0

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.
package/dist/cli.js CHANGED
@@ -2488,6 +2488,7 @@ var PixelLabProvider = class _PixelLabProvider {
2488
2488
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
2489
2489
  return { status: "processing" };
2490
2490
  } catch (err) {
2491
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
2491
2492
  if (!(err instanceof PixelLabError) || err.status !== 404) throw err;
2492
2493
  const survivor = await this.client.getObject(jobId).catch(() => null);
2493
2494
  const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
@@ -9431,6 +9432,18 @@ function renderGallery(snapshot, opts = {}) {
9431
9432
  background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
9432
9433
  .pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
9433
9434
  .pair figcaption { font:11px/1.4 var(--mono); color:var(--dim); background:var(--panel); padding:2px 6px; margin-top:6px; }
9435
+ .dot.busy { background:var(--cool); animation: pulse 1.2s ease-in-out infinite; }
9436
+ @keyframes pulse { 0%, 100% { opacity:1; } 50% { opacity:.25; } }
9437
+ @media (prefers-reduced-motion: reduce) { .dot.busy { animation:none; } }
9438
+ .card .busy-badge { position:absolute; left:6px; bottom:6px; display:inline-flex; align-items:center; gap:5px;
9439
+ background:var(--panel); color:var(--cool); border:1px solid var(--cool); padding:2px 7px; font:650 10.5px/1.4 var(--mono); }
9440
+ .drawer .job-strip { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; align-items:baseline; margin-top:10px;
9441
+ padding:10px 12px; border:1px solid var(--cool); background:color-mix(in srgb, var(--cool) 8%, var(--panel-deep)); font-size:12.5px; }
9442
+ .drawer .job-strip .ph { display:inline-flex; align-items:center; gap:6px; font:650 12px/1.4 var(--mono); color:var(--cool); white-space:nowrap; }
9443
+ .drawer .job-strip .last { color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
9444
+ .drawer .job-strip .what { grid-column:1 / -1; color:var(--text); }
9445
+ .drawer .job-strip .acts { grid-column:1 / -1; display:flex; gap:6px; }
9446
+ .drawer .job-strip .acts button { padding:3px 9px; font-size:12px; }
9434
9447
  .card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
9435
9448
  font:700 11px/1 var(--mono); padding:3px 5px; }
9436
9449
  .hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
@@ -9589,6 +9602,11 @@ const splitTags = (value) => value.split(',').map((t) => t.trim()).filter(Boolea
9589
9602
 
9590
9603
  const ACTIVE_PHASES = new Set(['queued', 'submitting', 'polling', 'fetching', 'review']);
9591
9604
  const PHASE_TONE = { queued: 'cool', submitting: 'cool', polling: 'cool', fetching: 'cool', review: 'warn', done: 'ok', failed: 'bad' };
9605
+ // The job still working on this record, if any: what the drawer and its card
9606
+ // show while a regeneration, restore, or pull is in flight.
9607
+ const activeJobFor = (item) => GEN.jobs.find((job) => ACTIVE_PHASES.has(job.phase) && job.project === item.project && job.keys.includes(item.key)) || null;
9608
+ const JOB_VERB = { generate: 'regenerating', resume: 'resuming', refresh: 'pulling upstream changes for', restore: 'restoring' };
9609
+ const PHASE_TEXT = { queued: 'queued', submitting: 'submitting to the provider', polling: 'waiting for the provider to finish', fetching: 'downloading the result', review: 'candidates are ready to review' };
9592
9610
  const remainingBudget = (provider) => {
9593
9611
  const keyed = GEN.budget.byProvider[provider];
9594
9612
  const ceiling = keyed !== undefined ? keyed : GEN.budget.amount;
@@ -9618,10 +9636,33 @@ async function pollJobs() {
9618
9636
  renderHeader();
9619
9637
  let changed = false;
9620
9638
  for (const job of GEN.jobs) {
9621
- if (!ACTIVE_PHASES.has(job.phase) && !ui.settled.has(job.id)) { ui.settled.add(job.id); changed = true; }
9639
+ if (!ACTIVE_PHASES.has(job.phase) && !ui.settled.has(job.id)) {
9640
+ ui.settled.add(job.id); changed = true;
9641
+ // The open record hears how its job ended, where the strip was.
9642
+ const open = ui.open && snap.items.find((i) => i.id === ui.open);
9643
+ if (open && job.project === open.project && job.keys.includes(open.key)) {
9644
+ ui.notice = { id: open.id, text: job.phase === 'failed'
9645
+ ? (JOB_VERB[job.mode] || job.mode).replace(/ing$/, 'ing') + ' this asset failed: ' + (job.error || 'see the log above')
9646
+ : job.mode === 'generate' ? 'Regenerated. The new result is on disk and recorded as this generation.'
9647
+ : job.mode === 'restore' ? 'Restored. The recorded bytes are back on disk.'
9648
+ : job.mode === 'refresh' ? (job.counts.downloaded ? 'Pulled the upstream change; the lockfile records the new bytes.' : 'Nothing changed upstream; the file was left alone.')
9649
+ : 'Done.' };
9650
+ }
9651
+ }
9622
9652
  if (job.phase === 'review' && !ui.settled.has(job.id + ':review')) { ui.settled.add(job.id + ':review'); changed = true; }
9623
9653
  }
9624
9654
  if (changed) refresh();
9655
+ else {
9656
+ // A job in flight: keep the open record's strip and the card badges honest without a full re-render.
9657
+ if (ui.open && !$('dialog-host').childNodes.length) renderDrawer();
9658
+ for (const c of document.querySelectorAll('.card')) {
9659
+ const item = snap.items.find((i) => i.id === c.dataset.key);
9660
+ const job = item && activeJobFor(item);
9661
+ const badge = c.querySelector('.busy-badge');
9662
+ if (job && !badge) c.replaceWith(card(item));
9663
+ else if (!job && badge) badge.remove();
9664
+ }
9665
+ }
9625
9666
  const active = GEN.jobs.some((job) => ACTIVE_PHASES.has(job.phase) && job.phase !== 'review');
9626
9667
  clearTimeout(jobsTimer);
9627
9668
  if (active) jobsTimer = setTimeout(pollJobs, 2000);
@@ -10270,6 +10311,13 @@ function card(item) {
10270
10311
  if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
10271
10312
  const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
10272
10313
  }
10314
+ const job = activeJobFor(item);
10315
+ if (job) {
10316
+ const badge = el('span', 'busy-badge');
10317
+ badge.append(el('i', 'dot busy'), document.createTextNode(job.phase === 'review' ? 'review' : job.mode === 'generate' ? 'generating' : job.mode));
10318
+ badge.title = (JOB_VERB[job.mode] || job.mode) + ' this asset \u2014 ' + (PHASE_TEXT[job.phase] || job.phase);
10319
+ cell.append(badge);
10320
+ }
10273
10321
  c.append(cell, body);
10274
10322
  c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
10275
10323
  return c;
@@ -10476,6 +10524,30 @@ function upstreamSection(item) {
10476
10524
  return s;
10477
10525
  }
10478
10526
 
10527
+ // What a job is doing to this record right now. The record's state line still
10528
+ // says what is on disk; this says what is about to replace it.
10529
+ function jobStrip(item, job) {
10530
+ const strip = el('div', 'job-strip');
10531
+ const ph = el('span', 'ph');
10532
+ ph.append(el('i', 'dot busy'), document.createTextNode(job.phase));
10533
+ const last = el('span', 'last', job.messages.length ? job.messages[job.messages.length - 1].trim() : '');
10534
+ last.title = job.messages.join('\\n');
10535
+ const what = el('span', 'what', (JOB_VERB[job.mode] || job.mode) + ' this asset \u2014 ' + (PHASE_TEXT[job.phase] || job.phase) +
10536
+ (job.mode === 'generate' && job.phase !== 'review' ? '. The current file stays until the new result is downloaded.' : '.'));
10537
+ strip.append(ph, last, what);
10538
+ const acts = el('div', 'acts');
10539
+ if (job.phase === 'review' && job.review.includes(item.key)) {
10540
+ const b = el('button', 'primary', 'Review candidates'); b.type = 'button'; b.onclick = () => openReview(job.id);
10541
+ acts.append(b);
10542
+ }
10543
+ const logBtn = el('button', null, ui.logs.has(job.id) ? 'Hide log' : 'Log'); logBtn.type = 'button';
10544
+ logBtn.onclick = () => { ui.logs.has(job.id) ? ui.logs.delete(job.id) : ui.logs.add(job.id); renderDrawer(); };
10545
+ acts.append(logBtn);
10546
+ strip.append(acts);
10547
+ if (ui.logs.has(job.id)) { const pre = el('pre', null, job.messages.join('\\n')); pre.style.gridColumn = '1 / -1'; strip.append(pre); }
10548
+ return strip;
10549
+ }
10550
+
10479
10551
  function generateActions(item) {
10480
10552
  const row = el('div', 'gen');
10481
10553
  if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
@@ -11183,7 +11255,9 @@ function renderDrawer() {
11183
11255
  body.append(previewHost);
11184
11256
  if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
11185
11257
  body.append(stateNode(item.state, item.reason));
11186
- if (GENERATION) body.append(generateActions(item));
11258
+ const job = GENERATION ? activeJobFor(item) : null;
11259
+ if (job) body.append(jobStrip(item, job));
11260
+ else if (GENERATION) body.append(generateActions(item));
11187
11261
  // Writes are opt-in flags on the command line; say so where the buttons would be.
11188
11262
  if ((!EDITABLE || !GENERATION) && item.declared) {
11189
11263
  const missing = [];