pixelkiln 0.32.0 → 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
@@ -2285,8 +2285,15 @@ function lockKey(styleId, assetId) {
2285
2285
 
2286
2286
  // src/providers/pixellab.ts
2287
2287
  function pixelLabObjectUrl(generator, objectId) {
2288
- if (!objectId || generator !== "map" && generator !== "1dir") return null;
2289
- return `https://www.pixellab.ai/objects/${encodeURIComponent(objectId)}`;
2288
+ if (!objectId) return null;
2289
+ if (generator === "map" || generator === "1dir") {
2290
+ return `https://www.pixellab.ai/create-object/${encodeURIComponent(objectId)}`;
2291
+ }
2292
+ if (generator === "tiles") {
2293
+ const setId = objectId.split("#")[0];
2294
+ return setId ? `https://www.pixellab.ai/maps/tiles/${encodeURIComponent(setId)}` : null;
2295
+ }
2296
+ return null;
2290
2297
  }
2291
2298
  var PixelLabProvider = class _PixelLabProvider {
2292
2299
  constructor(client) {
@@ -2481,6 +2488,7 @@ var PixelLabProvider = class _PixelLabProvider {
2481
2488
  if (obj.status === "failed") return { status: "failed", error: "generation failed upstream" };
2482
2489
  return { status: "processing" };
2483
2490
  } catch (err) {
2491
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
2484
2492
  if (!(err instanceof PixelLabError) || err.status !== 404) throw err;
2485
2493
  const survivor = await this.client.getObject(jobId).catch(() => null);
2486
2494
  const url = firstUrl(survivor?.rotation_urls) ?? survivor?.preview_url ?? null;
@@ -9424,6 +9432,18 @@ function renderGallery(snapshot, opts = {}) {
9424
9432
  background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
9425
9433
  .pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
9426
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; }
9427
9447
  .card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
9428
9448
  font:700 11px/1 var(--mono); padding:3px 5px; }
9429
9449
  .hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
@@ -9582,6 +9602,11 @@ const splitTags = (value) => value.split(',').map((t) => t.trim()).filter(Boolea
9582
9602
 
9583
9603
  const ACTIVE_PHASES = new Set(['queued', 'submitting', 'polling', 'fetching', 'review']);
9584
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' };
9585
9610
  const remainingBudget = (provider) => {
9586
9611
  const keyed = GEN.budget.byProvider[provider];
9587
9612
  const ceiling = keyed !== undefined ? keyed : GEN.budget.amount;
@@ -9611,10 +9636,33 @@ async function pollJobs() {
9611
9636
  renderHeader();
9612
9637
  let changed = false;
9613
9638
  for (const job of GEN.jobs) {
9614
- 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
+ }
9615
9652
  if (job.phase === 'review' && !ui.settled.has(job.id + ':review')) { ui.settled.add(job.id + ':review'); changed = true; }
9616
9653
  }
9617
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
+ }
9618
9666
  const active = GEN.jobs.some((job) => ACTIVE_PHASES.has(job.phase) && job.phase !== 'review');
9619
9667
  clearTimeout(jobsTimer);
9620
9668
  if (active) jobsTimer = setTimeout(pollJobs, 2000);
@@ -10263,6 +10311,13 @@ function card(item) {
10263
10311
  if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
10264
10312
  const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
10265
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
+ }
10266
10321
  c.append(cell, body);
10267
10322
  c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
10268
10323
  return c;
@@ -10451,9 +10506,11 @@ function upstreamSection(item) {
10451
10506
  if (item.upstreamUrl) {
10452
10507
  const a = el('a', null, 'Open in ' + item.provider + ' \u2197');
10453
10508
  a.href = item.upstreamUrl; a.target = '_blank'; a.rel = 'noopener';
10454
- row(dl, 'object', a);
10509
+ const tiles = item.generator === 'tiles';
10510
+ row(dl, tiles ? 'tile set' : 'object', a);
10455
10511
  row(dl, 'note', item.provider === 'pixellab'
10456
- ? 'The account object this generation came from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.'
10512
+ ? (tiles ? 'The tile set this generation was chosen from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.'
10513
+ : 'The account object this generation came from. Edit it there with PixelLab\u2019s editor, then pull the changes back here; the lockfile records the new bytes as this generation.')
10457
10514
  : 'The provider\u2019s page for this object.');
10458
10515
  }
10459
10516
  s.append(dl);
@@ -10467,6 +10524,30 @@ function upstreamSection(item) {
10467
10524
  return s;
10468
10525
  }
10469
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
+
10470
10551
  function generateActions(item) {
10471
10552
  const row = el('div', 'gen');
10472
10553
  if (!GENERATION || !item.declared || item.currentSpecHash === null) return row;
@@ -11174,7 +11255,9 @@ function renderDrawer() {
11174
11255
  body.append(previewHost);
11175
11256
  if (ui.notice && ui.notice.id === item.id) body.append(el('div', 'notice', ui.notice.text));
11176
11257
  body.append(stateNode(item.state, item.reason));
11177
- 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));
11178
11261
  // Writes are opt-in flags on the command line; say so where the buttons would be.
11179
11262
  if ((!EDITABLE || !GENERATION) && item.declared) {
11180
11263
  const missing = [];