@galda/cli 0.10.101 → 0.10.103

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.
Files changed (2) hide show
  1. package/app/index.html +139 -38
  2. package/package.json +1 -1
package/app/index.html CHANGED
@@ -1923,9 +1923,9 @@
1923
1923
  box-shadow:inset 0 0 0 1px rgba(var(--green-rgb),.30);font:600 12px/1 var(--ui);cursor:pointer;display:inline-flex;align-items:center;gap:7px}
1924
1924
  .sa-app:hover{box-shadow:inset 0 0 0 1px var(--green);background:rgba(var(--green-rgb),.12)}
1925
1925
  .sa-app svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
1926
- .sa-approve-wrap{display:flex;flex-direction:column;align-items:center;gap:5px;flex:0 0 auto}
1926
+ .sa-approve-wrap{display:flex;flex-direction:column;align-items:flex-start;gap:5px;flex:0 0 auto}
1927
1927
  .sa-approve-wrap + .sa-dis{align-self:flex-start}
1928
- .sa-delivery{font:500 9.5px/1.15 var(--mono);color:var(--ink3);text-decoration:none;white-space:nowrap}
1928
+ .sa-delivery{font:500 9.5px/1.15 var(--mono);color:var(--ink3);text-decoration:none;white-space:nowrap;margin-left:16px}
1929
1929
  a.sa-delivery{color:var(--green)}
1930
1930
  a.sa-delivery:hover{text-decoration:underline}
1931
1931
  .sa-dis{height:32px;padding:0 13px;border:0;border-radius:9px;background:color-mix(in srgb,var(--danger) 8%,var(--panel));
@@ -3793,7 +3793,7 @@ function buildReviewDigest({ goals, tasks } = {}){
3793
3793
  for (const g of goals ?? []) {
3794
3794
  if (g?.status !== 'review') continue;
3795
3795
  const goalTasks = (tasks ?? []).filter((t) => t.goalId === g.id);
3796
- const proofTask = goalTasks.find((t) => t.proof);
3796
+ const proofTask = latestProofTask(goalTasks);
3797
3797
  const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').trim().slice(0, 66);
3798
3798
  // Mirrors lib.mjs isPlanReview: a plan awaiting approval reads "PLAN — approve
3799
3799
  // to execute" and carries its plan body so the reviewer sees the plan in place.
@@ -9247,7 +9247,7 @@ function peraData(){
9247
9247
  const gs = (state.goals || []).filter((g) => g.projectId === state.active);
9248
9248
  const ts = (state.tasks || []).filter((t) => t.projectId === state.active);
9249
9249
  return buildReviewDigest({ goals: gs, tasks: ts }).map((r) => {
9250
- const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
9250
+ const pt = latestProofTask(ts.filter((t) => r.goalIds.includes(t.goalId)), true);
9251
9251
  return { ...r, proofTaskId: pt?.id ?? null, proofImg: pt?.proof?.png ?? pt?.proof?.gif ?? null };
9252
9252
  });
9253
9253
  }
@@ -9360,6 +9360,8 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
9360
9360
  // settles it (see saSync's "returned to review" reset).
9361
9361
  // ============================================================================
9362
9362
  const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
9363
+ // Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
9364
+ const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
9363
9365
  function saReplyAttachmentsHtml(goalId){
9364
9366
  return (SA.attachments[goalId] || []).map((a, i) => `<span class="sa-replythumb"><img src="${esc(a.url)}" alt="Attached image"><button type="button" data-saarm="${goalId}:${i}" aria-label="Remove image">×</button></span>`).join('');
9365
9367
  }
@@ -9394,6 +9396,17 @@ function latestGoalOutcomeTask(goalId, tasks){
9394
9396
  }
9395
9397
  function taskAttemptId(t){ return t?.attemptId || (t?.id != null ? `task-${t.id}` : null); }
9396
9398
  function taskSortTime(t){ return Date.parse(t?.finishedAt || t?.startedAt || t?.createdAt || 0) || 0; }
9399
+ // The proof a reviewer is being asked to look at is the NEWEST one. A reply re-runs
9400
+ // the work and takes its own shot, so the goal accumulates several. Every call site
9401
+ // used to be `tasks.find((t) => t.proof)`, which returns the OLDEST — the card then
9402
+ // showed the first attempt's screenshot underneath the latest attempt's words
9403
+ // (Result already sorts by time via latestGoalOutcomeTask). `image` = the call site
9404
+ // renders a thumbnail and needs a png/gif, not a proof that only carries a video.
9405
+ function latestProofTask(tasks, image = false){
9406
+ return (tasks || [])
9407
+ .filter((t) => t.proof && (!image || t.proof.png || t.proof.gif))
9408
+ .sort((a, b) => taskSortTime(b) - taskSortTime(a))[0] || null;
9409
+ }
9397
9410
  function latestGoalRunForOutcome(goalId, tasks, outcome){
9398
9411
  if (outcome?.run) return { taskId: outcome.id, attemptId: taskAttemptId(outcome), inherited: false, ...outcome.run };
9399
9412
  const runs = (tasks || []).filter((t) => t.goalId === goalId && t.run)
@@ -9424,7 +9437,7 @@ function saRowFromDigest(r, gs, ts){
9424
9437
  // here so the card never calls that state a generic failure.
9425
9438
  const interruptedTask = [...ts].filter((t) => r.goalIds.includes(t.goalId) && t.status === 'interrupted')
9426
9439
  .sort((a, b) => taskSortTime(b) - taskSortTime(a))[0] || null;
9427
- const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
9440
+ const pt = latestProofTask(ts.filter((t) => r.goalIds.includes(t.goalId)), true);
9428
9441
  // "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
9429
9442
  // per finished requirement task's own report; falls back to the goal's plan
9430
9443
  // steps when nothing has finished yet (e.g. a plan awaiting approval — the plan
@@ -9463,9 +9476,9 @@ function saRowFromDigest(r, gs, ts){
9463
9476
  attempt: g?.reviewAttempt ?? null,
9464
9477
  unmappedChangedFiles: outcome?.unmappedChangedFiles ?? [],
9465
9478
  requirementVerification: outcome?.requirementVerification ?? null,
9466
- requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String(t.reply ? t.detail : t.title).replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
9479
+ requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
9467
9480
  requirementEvidence: outcome?.requirementEvidence?.requirements ?? [],
9468
- requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String(t.reply ? t.detail : t.title).replace(/\s+/g, ' ').trim(), status: t.status })),
9481
+ requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), status: t.status })),
9469
9482
  did, siblings, risk: g?.risk ?? null,
9470
9483
  verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
9471
9484
  prNote: g?.prNote ?? null,
@@ -9573,7 +9586,7 @@ function saRowFromDigest(r, gs, ts){
9573
9586
  // the goal + its tasks exactly as buildReviewDigest does for a review goal.
9574
9587
  function saSoloDigestRow(g, ts){
9575
9588
  const goalTasks = (ts ?? []).filter((t) => t.goalId === g.id);
9576
- const proofTask = goalTasks.find((t) => t.proof);
9589
+ const proofTask = latestProofTask(goalTasks);
9577
9590
  const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').trim().slice(0, 66);
9578
9591
  return {
9579
9592
  goalIds: [g.id], reviewNumber: goalReviewNumber({ goal: g, tasks: ts }), checkLine,
@@ -10404,26 +10417,37 @@ function saRender(){
10404
10417
  const S = $('seeall'); if (!S) return;
10405
10418
  // SSE, diff fetches and action responses rebuild the dialog. Remember the
10406
10419
  // review body's position before replacing it so reading never jumps to top.
10420
+ // A raw scrollTop number is not enough: if a new review card lands ABOVE the
10421
+ // one being read (saSync pushes it earlier in SA.items), restoring the same
10422
+ // scrollTop lands on different content — the card visually jumps ("ガクッ").
10423
+ // Anchor on the card currently at/near the top of the viewport instead, and
10424
+ // after the rebuild re-align that same card back to the same offset.
10407
10425
  const previousBody = S.querySelector('.sa-body');
10408
10426
  const previousScrollTop = previousBody?.scrollTop ?? 0;
10409
- const restoreScroll = Boolean(previousBody) && SA.focusGid == null;
10410
- // A reply changes the height of the card being replied to (new thread line, status
10411
- // change) restoring a raw scrollTop pixel then leaves the viewport at the same
10412
- // number but showing DIFFERENT content, a visible jolt on whatever card the person
10413
- // was actually reading (2026-07-29). Anchor on that card's own goalId instead: find
10414
- // the topmost visible card before the rebuild, and after the rebuild scroll so that
10415
- // SAME card sits at the SAME on-screen position, regardless of how much other cards
10416
- // above it grew or shrank.
10427
+ // Preserve scroll even while a focus-flash is in flight: a new incoming review
10428
+ // (saSync from SSE) used to skip this whenever SA.focusGid was still set, snapping
10429
+ // the body back to scrollTop 0 and jerking the person's read position. The focus
10430
+ // block below only force-scrolls the focused card to the top ONCE per flash window
10431
+ // (SA.focusScrolled see below), so it no longer fights this restore on later
10432
+ // renders within that window.
10433
+ const restoreScroll = Boolean(previousBody);
10417
10434
  let anchorGid = null, anchorOffset = 0;
10435
+ // No `previousScrollTop > 0` guard here on purpose (task-917): right after opening,
10436
+ // or right after the focus-flash auto-scroll lands a card at the top, scrollTop IS 0
10437
+ // while a real card is still on screen. Skipping the anchor capture at exactly that
10438
+ // scrollTop silently let a newly-inserted card take over position 0 with no scroll
10439
+ // adjustment — the card someone was looking at got replaced under them without the
10440
+ // scrollbar moving at all, which read as the exact same "ガクッ" jump the anchor
10441
+ // logic below was built to prevent. Anchor whenever there is a previous body, full stop.
10418
10442
  if (restoreScroll) {
10419
- const containerTop = previousBody.getBoundingClientRect().top;
10420
- // The card actually being read sits near the middle of the viewport, not whatever
10421
- // sliver of a previous card's bottom edge still peeks in at the very top.
10422
- const midY = containerTop + previousBody.clientHeight / 2;
10423
- for (const el of previousBody.querySelectorAll('.sa-item')) {
10443
+ const bodyRect = previousBody.getBoundingClientRect();
10444
+ const midY = bodyRect.top + previousBody.clientHeight / 2;
10445
+ const cards = [...previousBody.querySelectorAll('.sa-item[data-gid]')];
10446
+ const anchor = cards.find((el) => {
10424
10447
  const r = el.getBoundingClientRect();
10425
- if (r.top <= midY && r.bottom >= midY) { anchorGid = el.dataset.gid; anchorOffset = r.top - containerTop; break; }
10426
- }
10448
+ return r.top <= midY && r.bottom >= midY;
10449
+ }) || cards.find((el) => el.getBoundingClientRect().bottom > bodyRect.top);
10450
+ if (anchor) { anchorGid = anchor.dataset.gid; anchorOffset = anchor.getBoundingClientRect().top - bodyRect.top; }
10427
10451
  }
10428
10452
  const focusedReplyGoal = document.activeElement?.matches?.('#seeall .sa-chat')
10429
10453
  ? document.activeElement.dataset.goal
@@ -10438,17 +10462,51 @@ function saRender(){
10438
10462
  const curIdx = pending.findIndex((x) => x.goalId === SA.cur);
10439
10463
  const posLabel = solo ? '' : (pending.length && curIdx >= 0 ? `${curIdx + 1} / ${pending.length}` : '');
10440
10464
  const hdKeys = solo ? '↑↓ MOVE · ESC CLOSE' : '↑↓ MOVE · A APPROVE · D DISMISS · ESC CLOSE';
10441
- const markup = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span><button class="sa-x" data-saact="close:0">✕</button></div>
10465
+ // Manual repro aid for the "ガクッ" scroll-jump bug (task-870): behind ?dev=1 only,
10466
+ // never in a normal session. Lets a person scroll into a review card and then trigger
10467
+ // a synthetic "new review just arrived" render the same way saSync does mid-read.
10468
+ const devInjectBtn = SA_DEV ? '<button class="sa-x" data-saact="devinject:0" title="Insert a fake review above the current scroll (dev only)">+TEST</button>' : '';
10469
+ const markup = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span>${devInjectBtn}<button class="sa-x" data-saact="close:0">✕</button></div>
10442
10470
  <div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div>`;
10443
10471
  // Most SSE goal/task events are unrelated to the card the person is reading.
10444
10472
  // Replacing #seeall with identical markup destroyed and recreated every node,
10445
10473
  // producing a blank flash and resetting browser-owned state. Keep the current
10446
10474
  // dialog DOM when its semantic markup did not change; the board behind it can
10447
10475
  // still refresh normally.
10448
- if (SA.lastMarkup === markup && S.querySelector('.sa-panel')) return;
10476
+ if (SA.lastMarkup === markup && S.querySelector('.sa-panel')) {
10477
+ // A focus change does not alter the card markup, but it still has a visual
10478
+ // side effect: move the newly focused card before paint. Handle that on the
10479
+ // existing DOM instead of returning early and waiting for the later rAF.
10480
+ if (SA.focusGid != null && !SA.focusScrolled) {
10481
+ const el = S.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
10482
+ if (el) {
10483
+ saScrollCardToTop(el);
10484
+ SA.focusScrolled = true;
10485
+ el.classList.add('sa-flash');
10486
+ if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
10487
+ }
10488
+ }
10489
+ return;
10490
+ }
10449
10491
  SA.lastMarkup = markup;
10450
10492
  S.innerHTML = markup;
10451
10493
  const body = S.querySelector('.sa-body');
10494
+ let focusPlacedNow = false;
10495
+ // H14's "scroll the focused card to top" used to always run one requestAnimationFrame
10496
+ // after this point. That's still required for the very FIRST render of a fresh
10497
+ // saOpen(): #seeall is still `display:none` here (the .open class is added by the
10498
+ // caller right after saRender() returns), so getBoundingClientRect() on anything
10499
+ // inside is all-zero and a synchronous scroll here would be a no-op.
10500
+ // But once the panel is already open (S.classList has 'open'), a LATER render
10501
+ // arriving mid-flash (saSync/devinject, task-909) can and must place the card
10502
+ // synchronously here, in the same pass as the anchor restore below — deferring it
10503
+ // to the next rAF left a one-frame window where the browser could paint with the
10504
+ // OLD (pre-focus) scrollTop, then snap to the correct position a frame later: a
10505
+ // visible double-jump ("ガクッ") the first time a card arrived during that window.
10506
+ if (body && S.classList.contains('open') && SA.focusGid != null && !SA.focusScrolled) {
10507
+ const el = body.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
10508
+ if (el) { saScrollCardToTop(el); SA.focusScrolled = true; focusPlacedNow = true; }
10509
+ }
10452
10510
  // Once the person scrolls, their position wins over the short opening-focus
10453
10511
  // animation. Programmatic opening scroll fires this too, which is fine: the
10454
10512
  // first placement happened and later SSE ticks should preserve it.
@@ -10464,12 +10522,16 @@ function saRender(){
10464
10522
  // also what actually grows a replied-to card's height, so the scroll anchor
10465
10523
  // below must run AFTER it, not before (2026-07-29 jolt fix).
10466
10524
  if (body && restoreScroll) {
10467
- const anchorEl = anchorGid != null ? body.querySelector(`.sa-item[data-gid="${anchorGid}"]`) : null;
10468
- if (anchorEl) {
10469
- const newOffset = anchorEl.getBoundingClientRect().top - body.getBoundingClientRect().top;
10470
- body.scrollTop += (newOffset - anchorOffset);
10471
- } else {
10472
- body.scrollTop = previousScrollTop;
10525
+ // An explicit focus move wins for this render. Later SSE renders have
10526
+ // focusScrolled=true and return to the normal reading-anchor preservation.
10527
+ if (!focusPlacedNow) {
10528
+ const anchorEl = anchorGid != null ? body.querySelector(`.sa-item[data-gid="${anchorGid}"]`) : null;
10529
+ if (anchorEl) {
10530
+ const newOffset = anchorEl.getBoundingClientRect().top - body.getBoundingClientRect().top;
10531
+ body.scrollTop += (newOffset - anchorOffset);
10532
+ } else {
10533
+ body.scrollTop = previousScrollTop;
10534
+ }
10473
10535
  }
10474
10536
  }
10475
10537
  for (const det of S.querySelectorAll('[data-detkey]')) {
@@ -10502,17 +10564,18 @@ function saRender(){
10502
10564
  if (im.complete && im.naturalWidth === 0) drop();
10503
10565
  else im.addEventListener('error', drop, { once: true });
10504
10566
  }
10505
- // H14: if a single-review focus is pending, bring the card's HEADER (#tag + In review
10506
- // + title) to the very top of the scroll area never mid-card (Masa 2026-07-20) — and
10507
- // flash a blue ring. Re-scroll on EVERY render for the focus window the post-diff
10508
- // saSync rebuild resets .sa-body scroll to the top, so a one-shot scroll would land back
10509
- // there. Self-clears after ~1.4s (outlasts the localhost diff fetch) so live SSE ticks
10510
- // stop yanking the view.
10567
+ // H14: if a single-review focus is pending, flash a blue ring on its card. The
10568
+ // scroll-to-top placement already happened synchronously above whenever the panel
10569
+ // was visible at render time; this rAF is the fallback for the still-`display:none`
10570
+ // first render of saOpen() (SA.focusScrolled stays false above, since the panel
10571
+ // isn't open yet), plus it always adds the flash class and starts the self-clear
10572
+ // timer, neither of which affect scroll position. Self-clears after ~1.4s (outlasts
10573
+ // the localhost diff fetch) so live SSE ticks stop yanking the view.
10511
10574
  if (SA.focusGid != null) {
10512
10575
  requestAnimationFrame(() => {
10513
10576
  const el = S.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
10514
10577
  if (!el) return;
10515
- saScrollCardToTop(el);
10578
+ if (!SA.focusScrolled) { saScrollCardToTop(el); SA.focusScrolled = true; }
10516
10579
  el.classList.add('sa-flash');
10517
10580
  if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
10518
10581
  });
@@ -10852,6 +10915,15 @@ $('seeall').addEventListener('click', async (e) => {
10852
10915
  // #reviewOverlay sits above #seeall, and a folded goal shows read-only there since
10853
10916
  // canReview=false hides Approve/Dismiss), or restore it via /unfold. Handled before the
10854
10917
  // SA.items[i] lookup below.
10918
+ // Dev-only repro aid (task-870, gated by SA_DEV/?dev=1): simulate a new review card
10919
+ // arriving mid-read the same way saSync splices one in from SSE, so the scroll-jump
10920
+ // fix above can be checked by hand without waiting for a real worker to finish.
10921
+ if (k === 'devinject') {
10922
+ if (!SA_DEV) return;
10923
+ SA.items.unshift(saDevSeedItem());
10924
+ saRender();
10925
+ return;
10926
+ }
10855
10927
  if (k === 'foldopen') return openReviewChecklist(Number(v));
10856
10928
  if (k === 'foldundo') {
10857
10929
  let r; try { r = await fetch(withKey(`/api/goals/${v}/unfold`), { method: 'POST' }); }
@@ -11503,6 +11575,35 @@ $('toggle').onclick = () => { $('side').classList.toggle('collapsed'); rememberS
11503
11575
  // V3-C dev escape hatch (Masa決定 2026-07-07): flagship hides the top bar; ?dev=1
11504
11576
  // (or localStorage devbar=1) shows it there for development (layout/theme switchers).
11505
11577
  if (new URLSearchParams(location.search).get('dev') === '1' || localStorage.getItem('devbar') === '1') document.body.classList.add('devbar');
11578
+ // Manual repro aid for the scroll-jump-on-new-review bug (task-870), ?dev=1 only: a
11579
+ // floating button that opens the Review dialog pre-loaded with several synthetic cards
11580
+ // (no real worker/PR needed), so the "+TEST" button inside the dialog (see saRender) has
11581
+ // something to scroll through. Never shown outside an explicit ?dev=1 session.
11582
+ let saDevSeq = 0;
11583
+ function saDevSeedItem(){
11584
+ saDevSeq++;
11585
+ const n = saDevSeq;
11586
+ return {
11587
+ goalId: -n, ids: [-n], st: 0, goal: {}, title: `Test review #${n}`,
11588
+ report: `Synthetic review card #${n} — for manually checking the scroll position stays put when a new card is inserted (task-870).`,
11589
+ pr: null, testResult: null, tokenOpt: null,
11590
+ metaFiles: [`fixtures/dev-review-${n}.mjs +${n} −${n}`], metaAdd: `+${n}`, metaDel: `−${n}`,
11591
+ };
11592
+ }
11593
+ if (SA_DEV) {
11594
+ const btn = document.createElement('button');
11595
+ btn.textContent = 'Open test review (dev)';
11596
+ btn.title = 'Opens the Review dialog with fake cards so the scroll-jump fix (task-870) can be checked by hand: scroll down, then press +TEST in the dialog header.';
11597
+ btn.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:9999;padding:8px 12px;border-radius:8px;border:1px solid var(--line,#444);background:var(--panel,#222);color:var(--ink,#eee);cursor:pointer;font:12px/1.2 inherit';
11598
+ btn.addEventListener('click', () => {
11599
+ SA.open = true; SA.solo = null; SA.detOpen = new Set(); SA.focusGid = null;
11600
+ SA.items = Array.from({ length: 6 }, () => saDevSeedItem());
11601
+ SA.cur = SA.items[0].goalId;
11602
+ saRender();
11603
+ $('seeall').classList.add('open');
11604
+ });
11605
+ document.body.appendChild(btn);
11606
+ }
11506
11607
  // Feedback 7: layout switcher — apply + persist the chosen arrangement.
11507
11608
  function applyLayout(){
11508
11609
  document.body.dataset.layout = state.layout || 'current';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.101",
3
+ "version": "0.10.103",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {