@galda/cli 0.10.105 → 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 +208 -31
- 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,14 +2257,25 @@
|
|
|
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}
|
|
@@ -3280,6 +3324,8 @@
|
|
|
3280
3324
|
|
|
3281
3325
|
<div class="overlay revfloat" id="reviewOverlay">
|
|
3282
3326
|
<div class="modal reviewmodal">
|
|
3327
|
+
<div class="revrail" id="reviewRail"></div>
|
|
3328
|
+
<div class="revbody">
|
|
3283
3329
|
<div class="revcount" id="reviewCount">REVIEW</div>
|
|
3284
3330
|
<h3 id="reviewGoalTitle">Review</h3>
|
|
3285
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>
|
|
@@ -3305,6 +3351,7 @@
|
|
|
3305
3351
|
<input type="text" id="reviewFixInput" placeholder="Type fix instructions (attach a screenshot too) — sends the goal back to the worker…">
|
|
3306
3352
|
<button type="button" id="reviewFixSend">Send</button>
|
|
3307
3353
|
</div>
|
|
3354
|
+
</div>
|
|
3308
3355
|
</div>
|
|
3309
3356
|
</div>
|
|
3310
3357
|
|
|
@@ -4187,6 +4234,20 @@ function saShotPair(shots, declared){
|
|
|
4187
4234
|
const before = find(pair[0]), after = find(pair[1]);
|
|
4188
4235
|
return before && after && before !== after ? { before, after } : null;
|
|
4189
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
|
+
}
|
|
4190
4251
|
function saPlainPairHtml(pair){
|
|
4191
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>`;
|
|
4192
4253
|
return `<div class="beforeafter">${col(pair.before, 'Before')}${col(pair.after, 'After')}</div>`;
|
|
@@ -5459,6 +5520,7 @@ function openReviewChecklist(goalId){
|
|
|
5459
5520
|
}
|
|
5460
5521
|
function closeReviewChecklist(){
|
|
5461
5522
|
$('reviewOverlay').classList.remove('show');
|
|
5523
|
+
reviewRailWiring?.destroy(); reviewRailWiring = null;
|
|
5462
5524
|
const goalId = state.reviewOpenGoal;
|
|
5463
5525
|
state.reviewOpenGoal = null;
|
|
5464
5526
|
// task 86: レビュー画面からチャットへ戻る瞬間に、そのゴールの末尾(完了して
|
|
@@ -5625,6 +5687,7 @@ function renderReviewChecklist(){
|
|
|
5625
5687
|
${chat}` : ''}
|
|
5626
5688
|
</div>`;
|
|
5627
5689
|
}).join('');
|
|
5690
|
+
renderReviewRail(reqs);
|
|
5628
5691
|
for (const b of $('reviewList').querySelectorAll('.reqbtn')) {
|
|
5629
5692
|
b.onclick = () => setRequirementStatus(goalId, Number(b.dataset.task), b.dataset.act);
|
|
5630
5693
|
}
|
|
@@ -5659,6 +5722,56 @@ function renderReviewChecklist(){
|
|
|
5659
5722
|
};
|
|
5660
5723
|
}
|
|
5661
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
|
+
}
|
|
5662
5775
|
// task 122: render()で消えるReviewの行を、Doneの行位置へ『飛んでいく』ように
|
|
5663
5776
|
// 見せるFLIPアニメーション。captureReviewFly()をrender()の直前(行がまだ
|
|
5664
5777
|
// DOMにある間)に呼んで元位置を覚え、playReviewFly()をrender()の直後に呼んで
|
|
@@ -6779,17 +6892,38 @@ function renderFsFeed(){
|
|
|
6779
6892
|
// Questions render at the FEED BOTTOM (composer-adjacent) — built into askHtml
|
|
6780
6893
|
// so they sit AFTER the conversation turns (H18/H19 / Pattern A: 質問は末尾固定).
|
|
6781
6894
|
let askHtml = '';
|
|
6782
|
-
|
|
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)) {
|
|
6783
6908
|
const chips = (g.question.options || []).map((o) => `<button class="achip" data-fsanswer="${g.id}" data-a="${esc(o)}">${esc(o)}</button>`).join('');
|
|
6784
|
-
|
|
6785
|
-
//
|
|
6786
|
-
// -
|
|
6787
|
-
|
|
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>`
|
|
6788
6920
|
+ `<div class="cc-ask-q">${esc(g.question.text)}</div>`
|
|
6789
6921
|
+ (chips ? `<div class="qchips">${chips}</div>` : '')
|
|
6790
6922
|
+ `<div class="answ-inline"><input type="text" data-fsansinput="${g.id}" placeholder="Type your answer…" aria-label="Type your answer">`
|
|
6791
|
-
+ `<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
|
|
6792
|
-
|
|
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>`;
|
|
6793
6927
|
}
|
|
6794
6928
|
// Clawd states (§4.5, driven by REAL state — no timers): activity in this column
|
|
6795
6929
|
// → speaker row (Clawd + Claude Code + goal metadata) above the raw CC lines;
|
|
@@ -6874,7 +7008,7 @@ function renderFsFeed(){
|
|
|
6874
7008
|
};
|
|
6875
7009
|
// Delete a "needs your answer" card outright (restores 「Needs your answer」カードを削除
|
|
6876
7010
|
// できるように — a question you don't want to answer had no discard path in flagship).
|
|
6877
|
-
|
|
7011
|
+
// data-fsanswerdel is gone with the trash it wired (see .cc-ask above).
|
|
6878
7012
|
}
|
|
6879
7013
|
async function fsAnswerQuestion(goalId, answer){
|
|
6880
7014
|
if (!answer) return;
|
|
@@ -9422,6 +9556,8 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9422
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 };
|
|
9423
9557
|
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9424
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';
|
|
9425
9561
|
function saReplyAttachmentsHtml(goalId){
|
|
9426
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('');
|
|
9427
9563
|
}
|
|
@@ -9983,7 +10119,7 @@ function saReqStackRows(it, attemptEvidence){
|
|
|
9983
10119
|
const reqMsgs = SA.msgs[gk];
|
|
9984
10120
|
const reqAsks = Array.isArray(reqMsgs) ? reqMsgs.filter((m) => m?.from === 'you' && String(m.text || '').trim()) : [];
|
|
9985
10121
|
const reqItems = reqAsks.length ? reqAsks : (it.goalText ? [{ text: it.goalText }] : []);
|
|
9986
|
-
|
|
10122
|
+
const row = (m, idx) => {
|
|
9987
10123
|
const text = String(m.text || '').trim();
|
|
9988
10124
|
const isLatest = idx === reqItems.length - 1;
|
|
9989
10125
|
const evLine = isLatest && attemptEvidence ? `<p class="rqev">${esc(attemptEvidence)}</p>` : '';
|
|
@@ -9997,7 +10133,19 @@ function saReqStackRows(it, attemptEvidence){
|
|
|
9997
10133
|
})()
|
|
9998
10134
|
: `<p class="rbody rask">${esc(text)}</p>`;
|
|
9999
10135
|
return `<div class="rqit${isLatest ? ' latest' : ''}"><span class="rqn">${idx + 1}</span><div class="rqbody">${body}${evLine}</div></div>`;
|
|
10000
|
-
}
|
|
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('');
|
|
10001
10149
|
}
|
|
10002
10150
|
function saItemHtml(it, i){
|
|
10003
10151
|
// st===3 is NOT a terminal row (REVIEW-FEEDBACK-FLOW 決定3): a reply is not a dismissal.
|
|
@@ -10118,6 +10266,15 @@ function saItemHtml(it, i){
|
|
|
10118
10266
|
const reqSec = reqStackRows
|
|
10119
10267
|
? `<section class="rsec rreq" data-reqmsgs="${gk}"><div class="rsec-h">Request</div><div class="rqlist">${reqStackRows}</div></section>`
|
|
10120
10268
|
: '';
|
|
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
|
+
: '';
|
|
10121
10278
|
const coverage = (it.requirementCoverage || []).filter((r) => r.text);
|
|
10122
10279
|
const coverageRows = coverage.map((r) => {
|
|
10123
10280
|
const labels = { verified: 'Verified', failed: 'Failed', 'not-applicable': 'Not applicable', unverified: 'Not verified' };
|
|
@@ -10238,9 +10395,7 @@ function saItemHtml(it, i){
|
|
|
10238
10395
|
const boxed = shotPair ? SA.shotBox[it.goalId] : null;
|
|
10239
10396
|
const shotsBody = shotPair
|
|
10240
10397
|
? `<div class="rshotpair" data-shotpair="${it.goalId}">${typeof boxed === 'string' ? boxed : saPlainPairHtml(shotPair)}</div>`
|
|
10241
|
-
: it.shots
|
|
10242
|
-
? 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('')
|
|
10243
|
-
: '';
|
|
10398
|
+
: saShotsListHtml(it.shots);
|
|
10244
10399
|
// ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
|
|
10245
10400
|
// run-log for logic-only tests; dropped entirely for a conversational task. ──
|
|
10246
10401
|
let proofInner = '';
|
|
@@ -10375,6 +10530,7 @@ function saItemHtml(it, i){
|
|
|
10375
10530
|
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span>${rst}</div>
|
|
10376
10531
|
<h2 class="rtitle">${esc(it.title)}</h2>
|
|
10377
10532
|
${meta}
|
|
10533
|
+
${summarySec}
|
|
10378
10534
|
${reqSec}
|
|
10379
10535
|
${S('Open it', openBody + infraBody)}
|
|
10380
10536
|
${S('What to check', checkBody)}
|
|
@@ -10482,6 +10638,25 @@ async function saEnhanceShotBoxes(){
|
|
|
10482
10638
|
if (host) host.innerHTML = html;
|
|
10483
10639
|
}));
|
|
10484
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
|
+
}
|
|
10485
10660
|
function saRender(){
|
|
10486
10661
|
const S = $('seeall'); if (!S) return;
|
|
10487
10662
|
// SSE, diff fetches and action responses rebuild the dialog. Remember the
|
|
@@ -10536,7 +10711,8 @@ function saRender(){
|
|
|
10536
10711
|
// a synthetic "new review just arrived" render the same way saSync does mid-read.
|
|
10537
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>' : '';
|
|
10538
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>
|
|
10539
|
-
<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>`;
|
|
10540
10716
|
// Most SSE goal/task events are unrelated to the card the person is reading.
|
|
10541
10717
|
// Replacing #seeall with identical markup destroyed and recreated every node,
|
|
10542
10718
|
// producing a blank flash and resetting browser-owned state. Keep the current
|
|
@@ -10560,6 +10736,7 @@ function saRender(){
|
|
|
10560
10736
|
SA.lastMarkup = markup;
|
|
10561
10737
|
S.innerHTML = markup;
|
|
10562
10738
|
const body = S.querySelector('.sa-body');
|
|
10739
|
+
renderSaRail(pending, body);
|
|
10563
10740
|
let focusPlacedNow = false;
|
|
10564
10741
|
// H14's "scroll the focused card to top" used to always run one requestAnimationFrame
|
|
10565
10742
|
// after this point. That's still required for the very FIRST render of a fresh
|
|
@@ -10731,7 +10908,7 @@ function saOpen(focusGoalId, solo = false){
|
|
|
10731
10908
|
saFetchDiffs();
|
|
10732
10909
|
saEnhanceShotBoxes();
|
|
10733
10910
|
}
|
|
10734
|
-
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(); }
|
|
10735
10912
|
// Continuous review (Masa 2026-07-08): move a persistent "current" card with
|
|
10736
10913
|
// ↑↓ and auto-advance to the next pending after each verdict — same screen, same
|
|
10737
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