@galda/cli 0.10.104 → 0.10.106
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/app/index.html +258 -59
- package/engine/server.mjs +19 -1
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -877,6 +877,23 @@
|
|
|
877
877
|
reviewed goal has no g.pr — one card per requirement (task) with its
|
|
878
878
|
proof video, Approve/Dismiss, and a revision-request chat. */
|
|
879
879
|
.reviewmodal{width:640px;max-height:74vh;overflow-y:auto}
|
|
880
|
+
/* goal-806/#851 案3: 複数レビュー内でどのタスクを見ているか掴めるよう、左に
|
|
881
|
+
細い番号レール。IntersectionObserver でスクロール位置に連動してハイライトし、
|
|
882
|
+
クリックで気持ちよく(smooth)その見出し頭へジャンプできる。1件しか
|
|
883
|
+
requirement が無いレビューでは要らないので JS 側で .show を切り替える。 */
|
|
884
|
+
.reviewmodal{display:flex;align-items:flex-start;gap:0}
|
|
885
|
+
.revrail{position:sticky;top:0;align-self:flex-start;flex:0 0 auto;width:0;overflow:hidden;
|
|
886
|
+
display:flex;flex-direction:column;align-items:center;padding-top:1px}
|
|
887
|
+
.revrail.show{width:30px;margin-right:12px}
|
|
888
|
+
.revrail button{all:unset;box-sizing:border-box;cursor:pointer;display:flex;flex-direction:column;align-items:center;
|
|
889
|
+
gap:4px;padding:7px 0;width:100%;color:var(--ink3);font:600 10px/1 var(--mono);transition:color .15s}
|
|
890
|
+
.revrail button .bar{width:3px;height:16px;border-radius:2px;background:var(--hair2);transition:background .15s,height .15s}
|
|
891
|
+
.revrail button.on{color:var(--ink)}
|
|
892
|
+
.revrail button.on .bar{background:var(--green);height:24px}
|
|
893
|
+
/* task-971: 白黒(モノクロ)版 — ?railmono=1 で緑を使わずinkの濃淡だけで
|
|
894
|
+
現在地を示す比較用バリアント。配色を使わない代替案の見え方を確認するため。 */
|
|
895
|
+
.revrail.mono button.on .bar{background:var(--ink)}
|
|
896
|
+
.revbody{flex:1;min-width:0}
|
|
880
897
|
.revsummary{margin:-4px 0 12px}
|
|
881
898
|
/* task 96: recap block shared by the checklist top summary and the chat导线 */
|
|
882
899
|
.rvhead{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;color:var(--ink2);font-weight:700}
|
|
@@ -953,7 +970,7 @@
|
|
|
953
970
|
@keyframes upcelflash{0%{transform:scale(.4);opacity:.95}100%{transform:scale(3);opacity:0}}
|
|
954
971
|
@media (prefers-reduced-motion:reduce){ .upcel.show .upcelflash{animation:none;opacity:0} }
|
|
955
972
|
.reqlist{display:flex;flex-direction:column;gap:12px}
|
|
956
|
-
.reqcard{border:1px solid var(--hair2);border-radius:14px;padding:14px 16px;background:var(--paper);transition:border-color .15s,background-color .15s}
|
|
973
|
+
.reqcard{border:1px solid var(--hair2);border-radius:14px;padding:14px 16px;background:var(--paper);transition:border-color .15s,background-color .15s;scroll-margin-top:8px}
|
|
957
974
|
.reqcard.approved{border-color:rgba(var(--green-rgb),.5);background:rgba(var(--green-rgb),.05)}
|
|
958
975
|
.reqcard.dismissed{border-color:rgba(var(--danger-rgb),.45);background:rgba(var(--danger-rgb),.05)}
|
|
959
976
|
.reqhead{display:flex;align-items:baseline;gap:8px}
|
|
@@ -1080,7 +1097,7 @@
|
|
|
1080
1097
|
sentence-case UI section heads, 14px --ink body prose, canvas log box. Same
|
|
1081
1098
|
IDs/classes/markup as before — ONLY the look changes. Behaviour is unchanged
|
|
1082
1099
|
(Approve stays hidden for failed; failure brief / Archive / send-back shown).
|
|
1083
|
-
All tokens (--panel/--hair/--ink
|
|
1100
|
+
All tokens (--panel/--hair/--ink family/--canvas/--ui/--mono) so it adapts per theme. */
|
|
1084
1101
|
.gdoverlay .modal{width:min(680px,94%);max-height:min(86vh,760px);display:flex;flex-direction:column;position:relative;overflow:hidden;
|
|
1085
1102
|
background:var(--panel);border:0;border-radius:18px;padding:0;
|
|
1086
1103
|
box-shadow:0 18px 54px -18px rgba(15,17,40,.30),0 0 0 1px var(--hair);
|
|
@@ -1581,16 +1598,19 @@
|
|
|
1581
1598
|
background:var(--panel);color:var(--ink);font:400 12.5px/1 var(--ui);padding:0 44px 0 12px;outline:none}
|
|
1582
1599
|
#fsRoot .cc-ask .answ-inline input:focus{border-color:color-mix(in srgb,var(--blue) 55%,var(--hair2))}
|
|
1583
1600
|
#fsRoot .cc-ask .answ-inline .sa-send{right:5px}
|
|
1584
|
-
/*
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
#fsRoot .cc-ask
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1601
|
+
/* Which request this question belongs to, on the label's own line: "#835 モバイル
|
|
1602
|
+
でGalda is offline…". Not the mono/uppercase/blue of the label itself — that
|
|
1603
|
+
vocabulary means "this is a Question"; the goal is ordinary prose, and it is
|
|
1604
|
+
dim because it identifies rather than asks (§5.5 薄く・騒がない). */
|
|
1605
|
+
#fsRoot .cc-ask-of{font-family:var(--ui);font-size:11px;letter-spacing:0;text-transform:none;
|
|
1606
|
+
color:var(--ink3);margin-left:8px}
|
|
1607
|
+
/* Third question onward: a count, not a card (§1.5-8). A line of text, no box —
|
|
1608
|
+
the box is what would take the room this is trying to save. */
|
|
1609
|
+
#fsRoot .cc-ask-more{font-size:11px;color:var(--ink3);margin:4px 0 2px 2px}
|
|
1610
|
+
/* A trash used to sit here, hover-revealed top-right. It called fsTaskDelete —
|
|
1611
|
+
it deleted the GOAL — from a card that did not say which goal it was, next to
|
|
1612
|
+
the answer box (Masa 2026-07-29). Deleting lives on the card and in the right
|
|
1613
|
+
lane, where the thing being deleted is named. */
|
|
1594
1614
|
#fsRoot .qchips{display:flex;gap:6px;flex-wrap:wrap}
|
|
1595
1615
|
#fsRoot .achip{height:28px;padding:0 12px;border-radius:8px;border:1px solid var(--hair2);background:var(--panel);
|
|
1596
1616
|
color:var(--ink2);font:inherit;font-size:12.5px;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
|
|
@@ -1903,7 +1923,12 @@
|
|
|
1903
1923
|
.sa-hd .keys{margin-left:auto;font:500 9px/1 var(--mono);color:var(--ink3);letter-spacing:.08em}
|
|
1904
1924
|
.sa-x{width:30px;height:30px;border:0;border-radius:8px;background:transparent;color:var(--ink3);font-size:15px;cursor:pointer}
|
|
1905
1925
|
.sa-x:hover{color:var(--ink);background:transparent}
|
|
1906
|
-
|
|
1926
|
+
/* goal-806/#851 案3: 複数のレビュー(sa-item)を縦スクロールで読んでいるとき、
|
|
1927
|
+
どれが何件目かを左の細いレールで示す。sa-hd の "N / M" は明示アクション
|
|
1928
|
+
(↑↓/クリック/Approve)でしか動かないので、素のスクロールでは追従しない
|
|
1929
|
+
——ここだけスクロール位置に連動してハイライトする。 */
|
|
1930
|
+
.sa-mid{flex:1;min-height:0;display:flex;align-items:stretch}
|
|
1931
|
+
.sa-body{flex:1;min-width:0;overflow-y:auto;overflow-x:hidden;padding:16px 22px 22px;scrollbar-width:none;display:flex;flex-direction:column;gap:12px;align-items:center}
|
|
1907
1932
|
.sa-body::-webkit-scrollbar{display:none}
|
|
1908
1933
|
.sa-item{background:var(--panel);border-radius:16px;box-shadow:0 1px 2px rgba(20,23,31,.05),0 0 0 1px var(--hair);padding:16px 18px;position:relative;width:min(720px,96%)}
|
|
1909
1934
|
/* H14: single-review tap → the See-all card flashes a blue ring to guide the eye */
|
|
@@ -2192,6 +2217,14 @@
|
|
|
2192
2217
|
.redesign .rshot:last-child{margin-bottom:0}
|
|
2193
2218
|
.redesign .rshot img{display:block;width:100%;height:auto;border-radius:10px;border:1px solid var(--hair)}
|
|
2194
2219
|
.redesign .rshot figcaption{margin-top:6px;font-family:var(--mono);font-size:11px;color:var(--ink3)}
|
|
2220
|
+
/* older shots from the same attempt fold away by default — the newest one already
|
|
2221
|
+
shows open; this is a manual "show more", not another auto-stacking pile. */
|
|
2222
|
+
.redesign .rshotfold{margin-top:8px}
|
|
2223
|
+
.redesign .rshotfold summary{cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--ink3);list-style:none}
|
|
2224
|
+
.redesign .rshotfold summary::-webkit-details-marker{display:none}
|
|
2225
|
+
.redesign .rshotfold summary:before{content:'▸ ';color:var(--ink3)}
|
|
2226
|
+
.redesign .rshotfold[open] summary:before{content:'▾ '}
|
|
2227
|
+
.redesign .rshotfold .rshot{margin-top:12px}
|
|
2195
2228
|
/* before/after pair + the red box over what actually changed (pixel-diffed here from
|
|
2196
2229
|
the worker's own two shots — no box appears unless the pixels really differ) */
|
|
2197
2230
|
.redesign .beforeafter .proofimg{width:100%;height:auto;max-height:none;object-fit:initial;display:block;border-radius:8px;border:1px solid var(--hair)}
|
|
@@ -2224,17 +2257,41 @@
|
|
|
2224
2257
|
Replaces the single Request paragraph + separate "This review" section. */
|
|
2225
2258
|
.redesign .rqlist{display:flex;flex-direction:column;gap:12px;margin-top:2px}
|
|
2226
2259
|
.redesign .rqit{display:flex;gap:10px;align-items:baseline}
|
|
2227
|
-
.redesign .rqit .rqn{flex:0 0 16px;font:600 12px/1.5 var(--mono);color:var(--
|
|
2260
|
+
.redesign .rqit .rqn{flex:0 0 16px;font:600 12px/1.5 var(--mono);color:var(--ink2);font-variant-numeric:tabular-nums}
|
|
2228
2261
|
.redesign .rqit .rqbody{flex:1;min-width:0}
|
|
2229
2262
|
/* §2 forces .rask to full-contrast ink !important everywhere else in the card (it's
|
|
2230
2263
|
the user's own words, never faded) — the Request stack is the one place that rule
|
|
2231
2264
|
is deliberately overridden: only the LATEST ask reads at full weight. */
|
|
2232
2265
|
.redesign .rqit .rqbody .rbody.rask{color:var(--ink2) !important}
|
|
2233
|
-
|
|
2234
|
-
|
|
2266
|
+
/* latest = monochrome (ink), not blue — a color for "current" competes with the review
|
|
2267
|
+
state dot/PR green elsewhere on the card (Masa 2026-07-29 follow-up). Weight B from
|
|
2268
|
+
the A/B compare (Masa picked B 2026-07-29): number stays ink2, body drops to 500 —
|
|
2269
|
+
calmer than the shipped bold ink. */
|
|
2270
|
+
.redesign .rqit.latest .rqn{color:var(--ink2)}
|
|
2271
|
+
.redesign .rqit.latest .rqbody .rbody.rask{color:var(--ink) !important;font-weight:500}
|
|
2272
|
+
/* Request stack cap (Masa 2026-07-29): middle asks collapse into one dots-only row,
|
|
2273
|
+
no label text, no "collapsing" wording — tap to expand in place. */
|
|
2274
|
+
.redesign .rqfold-row{margin:0;cursor:pointer}
|
|
2275
|
+
.redesign .rqfold-row>summary{list-style:none;display:flex;align-items:center;justify-content:center;padding:2px 0;border-radius:8px;font:600 13px/1 var(--mono);color:var(--ink3);letter-spacing:.08em}
|
|
2276
|
+
.redesign .rqfold-row>summary::-webkit-details-marker{display:none}
|
|
2277
|
+
.redesign .rqfold-row>summary:hover{background:var(--hover-s,rgba(255,255,255,.05));color:var(--ink)}
|
|
2278
|
+
.redesign .rqfold-row .rqmid{display:flex;flex-direction:column;gap:12px;margin-top:12px}
|
|
2235
2279
|
/* Changed/Tests aligns with the request TEXT, not the number/chevron — same 18px
|
|
2236
2280
|
the fold body below uses, so both sit under the same column (Masa 2026-07-29). */
|
|
2237
2281
|
.redesign .rqit .rqev{font-size:12px;color:var(--ink3);margin:5px 0 0;padding-left:18px}
|
|
2282
|
+
/* Canonical requirement coverage: one compact ledger, not three competing
|
|
2283
|
+
interpretations. Status is text + a small rule/mark (no badge surface),
|
|
2284
|
+
and Manager checks stay visually distinct from worker claims. */
|
|
2285
|
+
.redesign .rqcov{display:flex;flex-direction:column;margin-top:2px}
|
|
2286
|
+
.redesign .rqcov-row{padding:11px 0;border-top:1px solid var(--hair)}
|
|
2287
|
+
.redesign .rqcov-row:first-child{padding-top:0;border-top:0}
|
|
2288
|
+
.redesign .rqcov-main{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:baseline}
|
|
2289
|
+
.redesign .rqcov-text{font-size:13.5px;line-height:1.55;color:var(--ink)}
|
|
2290
|
+
.redesign .rqcov-state{font:600 10px/1.4 var(--mono);letter-spacing:.02em;color:var(--ink3);white-space:nowrap}
|
|
2291
|
+
.redesign .rqcov-state.verified{color:var(--green)}
|
|
2292
|
+
.redesign .rqcov-state.failed{color:var(--danger)}
|
|
2293
|
+
.redesign .rqcov-detail{margin-top:5px;font-size:11.5px;line-height:1.55;color:var(--ink3)}
|
|
2294
|
+
.redesign .rqcov-detail b{font-weight:600;color:var(--ink2)}
|
|
2238
2295
|
/* A long ask folds with the SAME .lacc pulldown Activity/Conversation use — chevron on
|
|
2239
2296
|
the peek line, body is the genuine rest (no duplicate, no trailing "…": Masa
|
|
2240
2297
|
2026-07-29 flagged both the vertical anchor line and the "…" as unnecessary — the
|
|
@@ -3267,6 +3324,8 @@
|
|
|
3267
3324
|
|
|
3268
3325
|
<div class="overlay revfloat" id="reviewOverlay">
|
|
3269
3326
|
<div class="modal reviewmodal">
|
|
3327
|
+
<div class="revrail" id="reviewRail"></div>
|
|
3328
|
+
<div class="revbody">
|
|
3270
3329
|
<div class="revcount" id="reviewCount">REVIEW</div>
|
|
3271
3330
|
<h3 id="reviewGoalTitle">Review</h3>
|
|
3272
3331
|
<p id="reviewIntro">Review each requirement with its capture and approve it individually — approved ones fold away. Or approve the whole goal below to send it straight to Done; the next review pops in automatically. Tap outside to close.</p>
|
|
@@ -3292,6 +3351,7 @@
|
|
|
3292
3351
|
<input type="text" id="reviewFixInput" placeholder="Type fix instructions (attach a screenshot too) — sends the goal back to the worker…">
|
|
3293
3352
|
<button type="button" id="reviewFixSend">Send</button>
|
|
3294
3353
|
</div>
|
|
3354
|
+
</div>
|
|
3295
3355
|
</div>
|
|
3296
3356
|
</div>
|
|
3297
3357
|
|
|
@@ -4174,6 +4234,20 @@ function saShotPair(shots, declared){
|
|
|
4174
4234
|
const before = find(pair[0]), after = find(pair[1]);
|
|
4175
4235
|
return before && after && before !== after ? { before, after } : null;
|
|
4176
4236
|
}
|
|
4237
|
+
// A goal can carry several shots from one attempt (variant comparisons, retries). Piling
|
|
4238
|
+
// all of them full-width makes the Review dialog scroll forever (#881 follow-up), so only
|
|
4239
|
+
// the most recent shot shows open; older ones fold behind a disclosure the user opens on
|
|
4240
|
+
// demand — screenshots stop auto-stacking without losing any of them.
|
|
4241
|
+
function saShotFigHtml(sh){
|
|
4242
|
+
return `<figure class="rshot"><img src="${esc(sh.url)}" alt="${esc(sh.name || 'screenshot')}" loading="lazy"><figcaption>${esc(String(sh.name || '').replace(/\.[a-z]+$/i, ''))}</figcaption></figure>`;
|
|
4243
|
+
}
|
|
4244
|
+
function saShotsListHtml(shots){
|
|
4245
|
+
const list = shots ?? [];
|
|
4246
|
+
if (!list.length) return '';
|
|
4247
|
+
if (list.length === 1) return saShotFigHtml(list[0]);
|
|
4248
|
+
const older = list.slice(0, -1);
|
|
4249
|
+
return `${saShotFigHtml(list[list.length - 1])}<details class="rshotfold"><summary>ほかの${older.length}枚を表示</summary>${older.map(saShotFigHtml).join('')}</details>`;
|
|
4250
|
+
}
|
|
4177
4251
|
function saPlainPairHtml(pair){
|
|
4178
4252
|
const col = (sh, label) => `<div class="ba-col"><span class="balabel">${label}</span><img class="proofimg" src="${esc(sh.url)}" alt="${esc(label)}" loading="lazy"></div>`;
|
|
4179
4253
|
return `<div class="beforeafter">${col(pair.before, 'Before')}${col(pair.after, 'After')}</div>`;
|
|
@@ -5446,6 +5520,7 @@ function openReviewChecklist(goalId){
|
|
|
5446
5520
|
}
|
|
5447
5521
|
function closeReviewChecklist(){
|
|
5448
5522
|
$('reviewOverlay').classList.remove('show');
|
|
5523
|
+
reviewRailWiring?.destroy(); reviewRailWiring = null;
|
|
5449
5524
|
const goalId = state.reviewOpenGoal;
|
|
5450
5525
|
state.reviewOpenGoal = null;
|
|
5451
5526
|
// task 86: レビュー画面からチャットへ戻る瞬間に、そのゴールの末尾(完了して
|
|
@@ -5612,6 +5687,7 @@ function renderReviewChecklist(){
|
|
|
5612
5687
|
${chat}` : ''}
|
|
5613
5688
|
</div>`;
|
|
5614
5689
|
}).join('');
|
|
5690
|
+
renderReviewRail(reqs);
|
|
5615
5691
|
for (const b of $('reviewList').querySelectorAll('.reqbtn')) {
|
|
5616
5692
|
b.onclick = () => setRequirementStatus(goalId, Number(b.dataset.task), b.dataset.act);
|
|
5617
5693
|
}
|
|
@@ -5646,6 +5722,56 @@ function renderReviewChecklist(){
|
|
|
5646
5722
|
};
|
|
5647
5723
|
}
|
|
5648
5724
|
}
|
|
5725
|
+
// goal-806/#851 案3: 複数カードのレビューで「今どこを見ているか」を左の細い
|
|
5726
|
+
// レールの番号で示す共通ヘルパー。scroller をスクロールするたび、その上端
|
|
5727
|
+
// から少し(bandPx)下のラインを跨いでいるカードを「今読んでいる」と判定して
|
|
5728
|
+
// ハイライトする(IntersectionObserver の rootMargin では、まだ画面外にある
|
|
5729
|
+
// 手前のカードの巨大な rect が「一部交差」して誤って選ばれることがあるため、
|
|
5730
|
+
// 素直に座標で判定する)。クリックは scrollIntoView(smooth) で気持ちよく
|
|
5731
|
+
// その見出し頭へ着地させる。戻り値は teardown 用の { destroy() }。
|
|
5732
|
+
function wireScrollRail({ rail, scroller, cards, getKey, onClick }){
|
|
5733
|
+
if (!rail || !scroller || cards.length < 2) { rail?.classList.remove('show'); if (rail) rail.innerHTML = ''; return { destroy(){} }; }
|
|
5734
|
+
rail.classList.add('show');
|
|
5735
|
+
const setOn = (key) => { for (const b of rail.querySelectorAll('button')) b.classList.toggle('on', b.dataset.railkey === String(key)); };
|
|
5736
|
+
setOn(getKey(cards[0]));
|
|
5737
|
+
for (const b of rail.querySelectorAll('button')) {
|
|
5738
|
+
b.onclick = () => onClick(b.dataset.railkey);
|
|
5739
|
+
}
|
|
5740
|
+
// task 959/960: bandPx=24 だと「タイトルが天井にほぼ着くまで」次カードに切り替わらず、
|
|
5741
|
+
// ユーザーが実際に読んでいる体感より遅れる。960でさらに早めてほしいとの指摘があり、
|
|
5742
|
+
// scroller高さの1/2程度(下限40px・上限260px)を判定ラインにして、少しスクロールした
|
|
5743
|
+
// だけで次のカードへ移った体感になるようにする。scroller.clientHeight はダイアログの
|
|
5744
|
+
// レイアウト完了後でないと0のことがあるので、固定値でなく毎回 onScroll 内で読み直す。
|
|
5745
|
+
const onScroll = () => {
|
|
5746
|
+
const bandPx = Math.min(260, Math.max(40, scroller.clientHeight * 0.5));
|
|
5747
|
+
const bandY = scroller.getBoundingClientRect().top + bandPx;
|
|
5748
|
+
let current = cards[0];
|
|
5749
|
+
for (const card of cards) {
|
|
5750
|
+
const r = card.getBoundingClientRect();
|
|
5751
|
+
if (r.top <= bandY) current = card; else break;
|
|
5752
|
+
}
|
|
5753
|
+
setOn(getKey(current));
|
|
5754
|
+
};
|
|
5755
|
+
scroller.addEventListener('scroll', onScroll, { passive: true });
|
|
5756
|
+
onScroll();
|
|
5757
|
+
return { destroy(){ scroller.removeEventListener('scroll', onScroll); } };
|
|
5758
|
+
}
|
|
5759
|
+
let reviewRailWiring = null;
|
|
5760
|
+
function renderReviewRail(reqs){
|
|
5761
|
+
const rail = $('reviewRail');
|
|
5762
|
+
if (!rail) return;
|
|
5763
|
+
reviewRailWiring?.destroy();
|
|
5764
|
+
if (reqs.length < 2) { rail.classList.remove('show'); rail.innerHTML = ''; return; }
|
|
5765
|
+
rail.classList.add('show');
|
|
5766
|
+
rail.innerHTML = reqs.map((t, i) => `<button type="button" data-railkey="${t.id}" title="${esc(t.title ?? '')}"><span class="bar"></span>${t.num ?? i + 1}</button>`).join('');
|
|
5767
|
+
const scroller = $('reviewList').closest('.reviewmodal');
|
|
5768
|
+
const cards = [...$('reviewList').querySelectorAll('.reqcard')];
|
|
5769
|
+
reviewRailWiring = wireScrollRail({
|
|
5770
|
+
rail, scroller, cards,
|
|
5771
|
+
getKey: (card) => card.dataset.task ?? card.dataset.reopen,
|
|
5772
|
+
onClick: (key) => cards.find((c) => (c.dataset.task ?? c.dataset.reopen) === key)?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5649
5775
|
// task 122: render()で消えるReviewの行を、Doneの行位置へ『飛んでいく』ように
|
|
5650
5776
|
// 見せるFLIPアニメーション。captureReviewFly()をrender()の直前(行がまだ
|
|
5651
5777
|
// DOMにある間)に呼んで元位置を覚え、playReviewFly()をrender()の直後に呼んで
|
|
@@ -6766,17 +6892,38 @@ function renderFsFeed(){
|
|
|
6766
6892
|
// Questions render at the FEED BOTTOM (composer-adjacent) — built into askHtml
|
|
6767
6893
|
// so they sit AFTER the conversation turns (H18/H19 / Pattern A: 質問は末尾固定).
|
|
6768
6894
|
let askHtml = '';
|
|
6769
|
-
|
|
6895
|
+
// This loop renders EVERY goal in the project that is waiting on an answer —
|
|
6896
|
+
// not the one you happen to be watching — and the card used to print nothing
|
|
6897
|
+
// but the word "Question" and the text. Two of them stacked, each with its own
|
|
6898
|
+
// free-text box, and nothing on screen said which request you were answering
|
|
6899
|
+
// (Masa 2026-07-29, with #835 and #856 sitting inside goal-847's live feed).
|
|
6900
|
+
// The right lane has named them all along ("#835 モバイルでGalda is offline…");
|
|
6901
|
+
// this makes the middle say the same thing, in the same shape.
|
|
6902
|
+
const asking = state.goals.filter((g) => g.projectId === state.active && g.status === 'needsInput' && g.question);
|
|
6903
|
+
// §1.5-8 量には畳みで先に応え、スクロールは後. Past the second card the rest are
|
|
6904
|
+
// counted, not drawn — a queue of questions must not push the live output off
|
|
6905
|
+
// the screen. They are all still in Needs you, which is where the count points.
|
|
6906
|
+
const ASK_SHOWN = 2;
|
|
6907
|
+
for (const g of asking.slice(0, ASK_SHOWN)) {
|
|
6770
6908
|
const chips = (g.question.options || []).map((o) => `<button class="achip" data-fsanswer="${g.id}" data-a="${esc(o)}">${esc(o)}</button>`).join('');
|
|
6771
|
-
|
|
6772
|
-
//
|
|
6773
|
-
// -
|
|
6774
|
-
|
|
6909
|
+
const title = String(g.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
6910
|
+
// QUESTION card (H18): mono label + the goal it belongs to + prompt + option
|
|
6911
|
+
// chips + always-on free-text answer. Data hooks (data-fsanswer / -ansinput /
|
|
6912
|
+
// -anssend) are unchanged so the existing wiring below keeps working.
|
|
6913
|
+
//
|
|
6914
|
+
// The trash is deliberately absent (Masa 2026-07-29). It called fsTaskDelete:
|
|
6915
|
+
// it deleted the GOAL, not the question, from an unlabelled card — and an
|
|
6916
|
+
// irreversible action does not belong beside a text box in a feed you are
|
|
6917
|
+
// skim-reading. Deleting still lives on the card and in the right lane.
|
|
6918
|
+
askHtml += `<div class="cc-ask"><div class="cc-ask-lb">Question`
|
|
6919
|
+
+ `<span class="cc-ask-of">#${esc(String(g.id))}${title ? ` ${esc(title)}` : ''}</span></div>`
|
|
6775
6920
|
+ `<div class="cc-ask-q">${esc(g.question.text)}</div>`
|
|
6776
6921
|
+ (chips ? `<div class="qchips">${chips}</div>` : '')
|
|
6777
6922
|
+ `<div class="answ-inline"><input type="text" data-fsansinput="${g.id}" placeholder="Type your answer…" aria-label="Type your answer">`
|
|
6778
|
-
+ `<button type="button" class="sa-send" data-fsanssend="${g.id}" title="Send answer" aria-label="Send answer"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div
|
|
6779
|
-
|
|
6923
|
+
+ `<button type="button" class="sa-send" data-fsanssend="${g.id}" title="Send answer" aria-label="Send answer"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div>`;
|
|
6924
|
+
}
|
|
6925
|
+
if (asking.length > ASK_SHOWN) {
|
|
6926
|
+
askHtml += `<div class="cc-ask-more">+${asking.length - ASK_SHOWN} more waiting on you</div>`;
|
|
6780
6927
|
}
|
|
6781
6928
|
// Clawd states (§4.5, driven by REAL state — no timers): activity in this column
|
|
6782
6929
|
// → speaker row (Clawd + Claude Code + goal metadata) above the raw CC lines;
|
|
@@ -6861,7 +7008,7 @@ function renderFsFeed(){
|
|
|
6861
7008
|
};
|
|
6862
7009
|
// Delete a "needs your answer" card outright (restores 「Needs your answer」カードを削除
|
|
6863
7010
|
// できるように — a question you don't want to answer had no discard path in flagship).
|
|
6864
|
-
|
|
7011
|
+
// data-fsanswerdel is gone with the trash it wired (see .cc-ask above).
|
|
6865
7012
|
}
|
|
6866
7013
|
async function fsAnswerQuestion(goalId, answer){
|
|
6867
7014
|
if (!answer) return;
|
|
@@ -9409,6 +9556,8 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9409
9556
|
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 };
|
|
9410
9557
|
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9411
9558
|
const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
|
|
9559
|
+
// task-971: 白黒(モノクロ)版のレールを ?railmono=1 で見比べられるようにする。
|
|
9560
|
+
const SA_RAIL_MONO = new URLSearchParams(location.search).get('railmono') === '1';
|
|
9412
9561
|
function saReplyAttachmentsHtml(goalId){
|
|
9413
9562
|
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('');
|
|
9414
9563
|
}
|
|
@@ -9473,6 +9622,33 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
|
9473
9622
|
note: prev.run?.note ? `${note} ${prev.run.note}` : note,
|
|
9474
9623
|
};
|
|
9475
9624
|
}
|
|
9625
|
+
function canonicalRequirementCoverage(goal, tasks){
|
|
9626
|
+
const requirements = Array.isArray(goal?.requirements) ? goal.requirements.filter((r) => r?.id && r?.text) : [];
|
|
9627
|
+
if (!requirements.length) {
|
|
9628
|
+
return (tasks || []).filter((t) => t.goalId === goal?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you'))
|
|
9629
|
+
.sort((a, b) => (a.num ?? 0) - (b.num ?? 0))
|
|
9630
|
+
.map((t) => ({ id: `legacy-task-${t.id}`, text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), coverage: 'unverified', legacy: true }));
|
|
9631
|
+
}
|
|
9632
|
+
const attemptOrder = new Map((goal.attempts || []).map((attempt, index) => [attempt.id, index]));
|
|
9633
|
+
const evidence = Array.isArray(goal.requirementEvidence) ? goal.requirementEvidence : [];
|
|
9634
|
+
return requirements.map((requirement) => {
|
|
9635
|
+
const rows = evidence.filter((row) => row?.requirementId === requirement.id)
|
|
9636
|
+
.sort((a, b) => (attemptOrder.get(a.attemptId) ?? -1) - (attemptOrder.get(b.attemptId) ?? -1));
|
|
9637
|
+
const latestAttemptId = rows.at(-1)?.attemptId;
|
|
9638
|
+
const latestRows = latestAttemptId ? rows.filter((row) => row.attemptId === latestAttemptId) : [];
|
|
9639
|
+
const statuses = latestRows.map((row) => row.coverage);
|
|
9640
|
+
const coverage = statuses.includes('failed') ? 'failed'
|
|
9641
|
+
: statuses.includes('verified') ? 'verified'
|
|
9642
|
+
: statuses.length && statuses.every((status) => status === 'not-applicable') ? 'not-applicable'
|
|
9643
|
+
: 'unverified';
|
|
9644
|
+
const claims = [...new Set(latestRows.map((row) => row.claim).filter(Boolean))];
|
|
9645
|
+
const workerEvidence = [...new Set(latestRows.map((row) => row.workerEvidence || row.evidence).filter(Boolean))];
|
|
9646
|
+
const checks = latestRows.flatMap((row) => Array.isArray(row.checks) ? row.checks : [])
|
|
9647
|
+
.filter((check) => Array.isArray(check.requirementIds) && check.requirementIds.includes(requirement.id));
|
|
9648
|
+
const checkFacts = [...new Set(checks.map((check) => `${check.kind}: ${check.status}`))];
|
|
9649
|
+
return { id: requirement.id, text: requirement.text, coverage, claims, workerEvidence, checkFacts, attemptId: latestAttemptId ?? null };
|
|
9650
|
+
});
|
|
9651
|
+
}
|
|
9476
9652
|
// One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
|
|
9477
9653
|
// "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
|
|
9478
9654
|
// whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
|
|
@@ -9522,10 +9698,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
9522
9698
|
// simply omit it, so the UI remains backward compatible during rollout.
|
|
9523
9699
|
attempt: g?.reviewAttempt ?? null,
|
|
9524
9700
|
unmappedChangedFiles: outcome?.unmappedChangedFiles ?? [],
|
|
9525
|
-
|
|
9526
|
-
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 })),
|
|
9527
|
-
requirementEvidence: outcome?.requirementEvidence?.requirements ?? [],
|
|
9528
|
-
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 })),
|
|
9701
|
+
requirementCoverage: canonicalRequirementCoverage(g, ts),
|
|
9529
9702
|
did, siblings, risk: g?.risk ?? null,
|
|
9530
9703
|
verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
|
|
9531
9704
|
prNote: g?.prNote ?? null,
|
|
@@ -9946,7 +10119,7 @@ function saReqStackRows(it, attemptEvidence){
|
|
|
9946
10119
|
const reqMsgs = SA.msgs[gk];
|
|
9947
10120
|
const reqAsks = Array.isArray(reqMsgs) ? reqMsgs.filter((m) => m?.from === 'you' && String(m.text || '').trim()) : [];
|
|
9948
10121
|
const reqItems = reqAsks.length ? reqAsks : (it.goalText ? [{ text: it.goalText }] : []);
|
|
9949
|
-
|
|
10122
|
+
const row = (m, idx) => {
|
|
9950
10123
|
const text = String(m.text || '').trim();
|
|
9951
10124
|
const isLatest = idx === reqItems.length - 1;
|
|
9952
10125
|
const evLine = isLatest && attemptEvidence ? `<p class="rqev">${esc(attemptEvidence)}</p>` : '';
|
|
@@ -9960,7 +10133,19 @@ function saReqStackRows(it, attemptEvidence){
|
|
|
9960
10133
|
})()
|
|
9961
10134
|
: `<p class="rbody rask">${esc(text)}</p>`;
|
|
9962
10135
|
return `<div class="rqit${isLatest ? ' latest' : ''}"><span class="rqn">${idx + 1}</span><div class="rqbody">${body}${evLine}</div></div>`;
|
|
9963
|
-
}
|
|
10136
|
+
};
|
|
10137
|
+
// Cap the stack at first + latest 2 (Masa 2026-07-29 follow-up: "3個以上の表示はいらない
|
|
10138
|
+
// ...良い感じに間を...にして"). The first ask (original request) and the latest 2 (current
|
|
10139
|
+
// state) stay visible; anything in between collapses into one dots-only row (no label
|
|
10140
|
+
// text, no "collapsing" wording) that expands in place — numbering stays the real,
|
|
10141
|
+
// continuous index so nothing reads as renumbered.
|
|
10142
|
+
if (reqItems.length <= 3) return reqItems.map(row).join('');
|
|
10143
|
+
const midFrom = 1, midTo = reqItems.length - 3;
|
|
10144
|
+
const foldKey = `${gk}:reqfold`;
|
|
10145
|
+
const midOpen = SA.detOpen.has(foldKey);
|
|
10146
|
+
const midRows = reqItems.slice(midFrom, midTo + 1).map((m, k) => row(m, midFrom + k)).join('');
|
|
10147
|
+
const foldRow = `<details class="rqfold-row" data-detkey="${foldKey}"${midOpen ? ' open' : ''}><summary class="rqdots">···</summary><div class="rqmid">${midRows}</div></details>`;
|
|
10148
|
+
return row(reqItems[0], 0) + foldRow + reqItems.slice(-2).map((m, k) => row(m, reqItems.length - 2 + k)).join('');
|
|
9964
10149
|
}
|
|
9965
10150
|
function saItemHtml(it, i){
|
|
9966
10151
|
// st===3 is NOT a terminal row (REVIEW-FEEDBACK-FLOW 決定3): a reply is not a dismissal.
|
|
@@ -10081,31 +10266,27 @@ function saItemHtml(it, i){
|
|
|
10081
10266
|
const reqSec = reqStackRows
|
|
10082
10267
|
? `<section class="rsec rreq" data-reqmsgs="${gk}"><div class="rsec-h">Request</div><div class="rqlist">${reqStackRows}</div></section>`
|
|
10083
10268
|
: '';
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
const
|
|
10269
|
+
// ── Summary (Masa 2026-07-29, 案2): "何の修正か一目で分からない" への対策として、雑に
|
|
10270
|
+
//書かれたRequest本文の上に一言サマリを置く。色は使わない — 他のセクションと同じ
|
|
10271
|
+
// rsec/rsec-h の見た目に揃えた通常セクション(案1のバナー・案3の絵文字強調は却下)。
|
|
10272
|
+
// 出典はレビュー要約(reviewSummary.changed)のみ — worker側の新フィールドを増やさず、
|
|
10273
|
+
// 既存の値をそのまま短く見せるだけなのでworker(Claude Code/Codex)側の負担は増えない。
|
|
10274
|
+
const summaryText = String(it.goal?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim();
|
|
10275
|
+
const summarySec = summaryText
|
|
10276
|
+
? `<section class="rsec rsum"><div class="rsec-h">Summary</div><p class="rbody v">${esc(summaryText.slice(0, 200))}</p></section>`
|
|
10277
|
+
: '';
|
|
10091
10278
|
const coverage = (it.requirementCoverage || []).filter((r) => r.text);
|
|
10092
|
-
// For one requirement copied from Request, its status is useful but the full
|
|
10093
|
-
// sentence is not. Multiple or narrowed requirements retain their text.
|
|
10094
|
-
const coverageIsSingleRequest = coverage.length === 1
|
|
10095
|
-
&& String(coverage[0].text).replace(/\s+/g, ' ').trim() === requestText;
|
|
10096
10279
|
const coverageRows = coverage.map((r) => {
|
|
10097
|
-
const
|
|
10098
|
-
const
|
|
10099
|
-
|
|
10280
|
+
const labels = { verified: 'Verified', failed: 'Failed', 'not-applicable': 'Not applicable', unverified: 'Not verified' };
|
|
10281
|
+
const details = [
|
|
10282
|
+
r.legacy ? 'Legacy item — no canonical evidence was recorded' : '',
|
|
10283
|
+
r.checkFacts?.length ? `Manager: ${r.checkFacts.join(' · ')}` : '',
|
|
10284
|
+
r.claims?.length ? `Worker claim: ${r.claims.join(', ')}` : '',
|
|
10285
|
+
r.workerEvidence?.length ? r.workerEvidence.join(' · ') : '',
|
|
10286
|
+
].filter(Boolean);
|
|
10287
|
+
return `<div class="rqcov-row" data-requirement-id="${esc(r.id)}"><div class="rqcov-main"><div class="rqcov-text">${esc(r.text)}</div><span class="rqcov-state ${esc(r.coverage)}">${esc(labels[r.coverage] || labels.unverified)}</span></div>${details.length ? `<div class="rqcov-detail">${details.map((detail, index) => index === 0 ? esc(detail) : `<br>${esc(detail)}`).join('')}</div>` : ''}</div>`;
|
|
10100
10288
|
}).join('');
|
|
10101
|
-
const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div>${coverageRows}</section>` : '';
|
|
10102
|
-
// Worker-declared evidence is displayed verbatim as an auditable label, not
|
|
10103
|
-
// promoted into a Manager verdict. Empty means the worker did not provide it.
|
|
10104
|
-
const evidenceRows = (it.requirementEvidence || []).map((entry) => {
|
|
10105
|
-
const tone = entry.status === 'addressed' ? 'ok' : entry.status === 'partial' ? '' : 'dim';
|
|
10106
|
-
return `<p class="rbody v"><b class="${tone}">${esc(entry.status)}</b> · ${esc(entry.evidence || entry.id)}</p>`;
|
|
10107
|
-
}).join('');
|
|
10108
|
-
const evidenceSec = evidenceRows ? `<section class="rsec"><div class="rsec-h">Requirement evidence</div>${evidenceRows}</section>` : '';
|
|
10289
|
+
const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div><div class="rqcov">${coverageRows}</div></section>` : '';
|
|
10109
10290
|
// ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
|
|
10110
10291
|
// heading + body shape as Request and Result, not a chip in another vocabulary. The
|
|
10111
10292
|
// address the worker declared IS the control (press it → the process starts on demand →
|
|
@@ -10214,9 +10395,7 @@ function saItemHtml(it, i){
|
|
|
10214
10395
|
const boxed = shotPair ? SA.shotBox[it.goalId] : null;
|
|
10215
10396
|
const shotsBody = shotPair
|
|
10216
10397
|
? `<div class="rshotpair" data-shotpair="${it.goalId}">${typeof boxed === 'string' ? boxed : saPlainPairHtml(shotPair)}</div>`
|
|
10217
|
-
: it.shots
|
|
10218
|
-
? it.shots.map((sh) => `<figure class="rshot"><img src="${esc(sh.url)}" alt="${esc(sh.name || 'screenshot')}" loading="lazy"><figcaption>${esc(String(sh.name || '').replace(/\.[a-z]+$/i, ''))}</figcaption></figure>`).join('')
|
|
10219
|
-
: '';
|
|
10398
|
+
: saShotsListHtml(it.shots);
|
|
10220
10399
|
// ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
|
|
10221
10400
|
// run-log for logic-only tests; dropped entirely for a conversational task. ──
|
|
10222
10401
|
let proofInner = '';
|
|
@@ -10351,12 +10530,11 @@ function saItemHtml(it, i){
|
|
|
10351
10530
|
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span>${rst}</div>
|
|
10352
10531
|
<h2 class="rtitle">${esc(it.title)}</h2>
|
|
10353
10532
|
${meta}
|
|
10533
|
+
${summarySec}
|
|
10354
10534
|
${reqSec}
|
|
10355
10535
|
${S('Open it', openBody + infraBody)}
|
|
10356
10536
|
${S('What to check', checkBody)}
|
|
10357
10537
|
${coverageSec}
|
|
10358
|
-
${evidenceSec}
|
|
10359
|
-
${reqVerifySec}
|
|
10360
10538
|
${S('Run state', interruptedBody)}
|
|
10361
10539
|
${S('PR', prRepairBody)}
|
|
10362
10540
|
${S('Also carries', carriesBody)}
|
|
@@ -10460,6 +10638,25 @@ async function saEnhanceShotBoxes(){
|
|
|
10460
10638
|
if (host) host.innerHTML = html;
|
|
10461
10639
|
}));
|
|
10462
10640
|
}
|
|
10641
|
+
let saRailWiring = null;
|
|
10642
|
+
// goal-806/#851 案3: 素のスクロールでは SA.cur/"N / M" が動かないので、レール
|
|
10643
|
+
// だけスクロール位置を追いかけて示す(SA.cur 自体は触らない=↑↓/Approve の
|
|
10644
|
+
// 対象を変えない)。共通実装は wireScrollRail (engine 側の renderReviewRail と同じ)。
|
|
10645
|
+
function renderSaRail(pending, body){
|
|
10646
|
+
const rail = $('saRail');
|
|
10647
|
+
if (!rail) return;
|
|
10648
|
+
saRailWiring?.destroy(); saRailWiring = null;
|
|
10649
|
+
if (!body || pending.length < 2) { rail.classList.remove('show'); rail.innerHTML = ''; return; }
|
|
10650
|
+
rail.classList.add('show');
|
|
10651
|
+
rail.classList.toggle('mono', SA_RAIL_MONO);
|
|
10652
|
+
rail.innerHTML = pending.map((it, i) => `<button type="button" data-railkey="${it.goalId}" title="${esc((it.title ?? '').slice(0, 80))}"><span class="bar"></span>${i + 1}</button>`).join('');
|
|
10653
|
+
const cards = [...body.querySelectorAll('.sa-item[data-gid]')];
|
|
10654
|
+
saRailWiring = wireScrollRail({
|
|
10655
|
+
rail, scroller: body, cards,
|
|
10656
|
+
getKey: (card) => card.dataset.gid,
|
|
10657
|
+
onClick: (key) => body.querySelector(`.sa-item[data-gid="${key}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
|
|
10658
|
+
});
|
|
10659
|
+
}
|
|
10463
10660
|
function saRender(){
|
|
10464
10661
|
const S = $('seeall'); if (!S) return;
|
|
10465
10662
|
// SSE, diff fetches and action responses rebuild the dialog. Remember the
|
|
@@ -10514,7 +10711,8 @@ function saRender(){
|
|
|
10514
10711
|
// a synthetic "new review just arrived" render the same way saSync does mid-read.
|
|
10515
10712
|
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>' : '';
|
|
10516
10713
|
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>
|
|
10517
|
-
<div class="sa-
|
|
10714
|
+
<div class="sa-mid"><div class="revrail" id="saRail"></div>
|
|
10715
|
+
<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></div>`;
|
|
10518
10716
|
// Most SSE goal/task events are unrelated to the card the person is reading.
|
|
10519
10717
|
// Replacing #seeall with identical markup destroyed and recreated every node,
|
|
10520
10718
|
// producing a blank flash and resetting browser-owned state. Keep the current
|
|
@@ -10538,6 +10736,7 @@ function saRender(){
|
|
|
10538
10736
|
SA.lastMarkup = markup;
|
|
10539
10737
|
S.innerHTML = markup;
|
|
10540
10738
|
const body = S.querySelector('.sa-body');
|
|
10739
|
+
renderSaRail(pending, body);
|
|
10541
10740
|
let focusPlacedNow = false;
|
|
10542
10741
|
// H14's "scroll the focused card to top" used to always run one requestAnimationFrame
|
|
10543
10742
|
// after this point. That's still required for the very FIRST render of a fresh
|
|
@@ -10709,7 +10908,7 @@ function saOpen(focusGoalId, solo = false){
|
|
|
10709
10908
|
saFetchDiffs();
|
|
10710
10909
|
saEnhanceShotBoxes();
|
|
10711
10910
|
}
|
|
10712
|
-
function saClose(){ SA.open = false; SA.solo = null; SA.focusGid = null; SA.lastMarkup = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } $('seeall').classList.remove('open'); applyPendingUiVersionIfAny(); }
|
|
10911
|
+
function saClose(){ SA.open = false; SA.solo = null; SA.focusGid = null; SA.lastMarkup = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } saRailWiring?.destroy(); saRailWiring = null; $('seeall').classList.remove('open'); applyPendingUiVersionIfAny(); }
|
|
10713
10912
|
// Continuous review (Masa 2026-07-08): move a persistent "current" card with
|
|
10714
10913
|
// ↑↓ and auto-advance to the next pending after each verdict — same screen, same
|
|
10715
10914
|
// Ledger, so approving flows without hunting for the next one.
|
package/engine/server.mjs
CHANGED
|
@@ -1726,8 +1726,26 @@ function activeSlotCount(projectId = null) {
|
|
|
1726
1726
|
if (projectId != null) return (queues.get(projectId)?.running.size ?? 0) + planningCount(projectId);
|
|
1727
1727
|
return globalRunningCount() + planningCount();
|
|
1728
1728
|
}
|
|
1729
|
+
// A goal holds its slot for as long as it is in Doing — from the moment planning
|
|
1730
|
+
// starts until it leaves — and NOT only while one of its tasks happens to be
|
|
1731
|
+
// executing.
|
|
1732
|
+
//
|
|
1733
|
+
// activeSlotCount() counts running TASKS (plus goals still planning). That is the
|
|
1734
|
+
// right question for pump(), which decides how many workers may run. It is the
|
|
1735
|
+
// wrong question here: a goal whose task was sitting in the queue counted for
|
|
1736
|
+
// nothing, so a free slot appeared, so the next stacked goal was planned too —
|
|
1737
|
+
// and so on. Measured with MANAGER_MAX_PARALLEL=2 and four goals: two tasks ran
|
|
1738
|
+
// (the cap held), while all four goals sat in Doing and `stacked` never had a
|
|
1739
|
+
// single member. The board says four things are being worked on when two are,
|
|
1740
|
+
// and because nothing is `stacked`, the row that explains the wait — "Waiting
|
|
1741
|
+
// for parallel slot · active #N · starts when one reaches Review/Done" — never
|
|
1742
|
+
// renders at all.
|
|
1743
|
+
function activeGoalSlots(projectId = null) {
|
|
1744
|
+
return goals.filter((g) => (g.status === 'planning' || g.status === 'running')
|
|
1745
|
+
&& (projectId == null || g.projectId === projectId)).length;
|
|
1746
|
+
}
|
|
1729
1747
|
function stackedStartSlots(projectId) {
|
|
1730
|
-
return Math.min(projectParallelCap() -
|
|
1748
|
+
return Math.min(projectParallelCap() - activeGoalSlots(projectId), GLOBAL_MAX_PARALLEL - activeGoalSlots());
|
|
1731
1749
|
}
|
|
1732
1750
|
function startPlanningGoal(goal) {
|
|
1733
1751
|
goal.status = 'planning';
|
package/package.json
CHANGED