@galda/cli 0.10.37 → 0.10.38
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 +453 -68
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -1948,6 +1948,171 @@
|
|
|
1948
1948
|
.sa-done .undo{margin-left:auto;border:0;background:none;color:var(--ink3);font:500 10px/1 var(--mono);cursor:pointer}
|
|
1949
1949
|
.sa-done .undo:hover{color:var(--ink);background:transparent}
|
|
1950
1950
|
.sa-empty{font:500 11px/1 var(--mono);color:var(--ink3);padding:20px 0}
|
|
1951
|
+
/* ═══ Review card redesign (REVIEW-CARD-SPEC.md · Masa 2026-07-20 confirmed):
|
|
1952
|
+
Linear-docs typography · META = green outline pill (Chip) · GitHub diff = Bright.
|
|
1953
|
+
Ported from the flagship redesign (fs-redesign.css), the dev-only PLAN/META/DIFF
|
|
1954
|
+
switchers resolved to the confirmed defaults plan=d / meta=a / gh=d, and the
|
|
1955
|
+
flagship fallback hexes mapped to the app's real theme tokens (theme.css).
|
|
1956
|
+
Scoped to .sa-item.redesign so the plain .sa-done one-liner is untouched. ═══ */
|
|
1957
|
+
#seeall .sa-panel{width:min(940px,94%);max-width:940px;backdrop-filter:blur(30px) saturate(1.4);-webkit-backdrop-filter:blur(30px) saturate(1.4)}
|
|
1958
|
+
/* body/header/card geometry ported verbatim from the artifact (fs-redesign.css plan=d):
|
|
1959
|
+
the CARD carries the 40px side gutters + 28/34 top/bottom, the header matches (40/34),
|
|
1960
|
+
and the See-all body drops its own gutter/gap/centering so the card padding is the
|
|
1961
|
+
single source of the Linear-docs air. !important because the app's own .sa-item /
|
|
1962
|
+
.sa-body / .lg rules (added first, same-or-higher specificity) otherwise win. */
|
|
1963
|
+
#seeall .sa-body:has(.sa-item.redesign){gap:0 !important;align-items:stretch !important}
|
|
1964
|
+
#seeall .sa-panel .sa-hd:has(~ .sa-body .sa-item.redesign){padding:16px 34px 12px 40px !important}
|
|
1965
|
+
.sa-item.redesign{width:100% !important;background:none !important;border:0 !important;border-radius:0 !important;box-shadow:none !important;
|
|
1966
|
+
padding:28px 40px 34px !important;font-family:var(--ui);color:var(--ink);line-height:1.6}
|
|
1967
|
+
.sa-item.redesign *{box-sizing:border-box}
|
|
1968
|
+
/* first card: no extra top margin — the card's own 28px padding-top (plus the
|
|
1969
|
+
body's 16px) is the top air, matching the ~40px inter-card rhythm below.
|
|
1970
|
+
The old 52px pushed the header→#N gap to ~96px of dead whitespace (Masa 2026-07-20). */
|
|
1971
|
+
#seeall .sa-body>.sa-item.redesign:first-child{margin-top:0 !important}
|
|
1972
|
+
#seeall .sa-body>.sa-item.redesign+.sa-item.redesign{border-top:1px solid var(--hair) !important;margin-top:0 !important;padding-top:40px !important}
|
|
1973
|
+
/* reply composer: match the flagship saItem field (34px tall · radius 10 · 12px left pad)
|
|
1974
|
+
so it doesn't read cramped next to the 32px Approve/Dismiss (Masa 2026-07-20).
|
|
1975
|
+
padding-right (42px for the send ring) stays from .sa-chatwrap .sa-chat. */
|
|
1976
|
+
#seeall .sa-chat{height:34px;border-radius:10px;padding-left:12px}
|
|
1977
|
+
/* header — mono eyebrow · review dot · In review */
|
|
1978
|
+
.redesign .rhead{display:flex;align-items:center;gap:12px;font-family:var(--mono);font-size:11px;color:var(--ink3)}
|
|
1979
|
+
.redesign .rtag{letter-spacing:.02em;font-variant-numeric:tabular-nums}
|
|
1980
|
+
.redesign .rst{display:inline-flex;align-items:center;gap:6px;color:var(--ink2)}
|
|
1981
|
+
.redesign .rdot{width:6px;height:6px;border-radius:50%;background:var(--amber,#e0a021);display:inline-block}
|
|
1982
|
+
.redesign .rtitle{font-family:var(--ui);font-size:18px;font-weight:680;letter-spacing:-.018em;line-height:1.3;margin:8px 0 0 !important;color:var(--ink)}
|
|
1983
|
+
/* meta — Linear sans, green/neutral chips, hairline under (spec §4) */
|
|
1984
|
+
.redesign .rmeta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin:12px 0 0 !important;padding-bottom:12px !important;
|
|
1985
|
+
border-bottom:1px solid var(--hair);font-family:var(--ui);font-size:12px;letter-spacing:-.004em;color:var(--ink2)}
|
|
1986
|
+
.redesign .rmeta .mx{display:inline-flex;align-items:center;gap:5px}
|
|
1987
|
+
.redesign .rmeta .mk{color:var(--ink3)}
|
|
1988
|
+
.redesign .rmeta b{color:var(--ink);font-weight:600}
|
|
1989
|
+
.redesign .rmeta .mmut{color:var(--ink3);font-weight:400}
|
|
1990
|
+
.redesign .rmeta .ar{font-size:.82em;margin-left:1px;color:var(--ink3)}
|
|
1991
|
+
.redesign .rmeta .a{color:#3fd07a;font-style:normal} .redesign .rmeta .d{color:#ff7b72;font-style:normal}
|
|
1992
|
+
/* Chip = outline pill (meta=a): green for PR/Token, neutral for Local */
|
|
1993
|
+
.redesign .rmeta .mlink{cursor:pointer;padding:3px 10px;border-radius:999px;
|
|
1994
|
+
border:1px solid color-mix(in srgb,currentColor 40%,var(--hair));text-decoration:none;background:none;font:inherit}
|
|
1995
|
+
.redesign .rmeta .mlink:hover{background:color-mix(in srgb,currentColor 9%,transparent)}
|
|
1996
|
+
.redesign .rmeta .mlink.pr,.redesign .rmeta .mlink.pr *{color:var(--green)}
|
|
1997
|
+
/* whole PR chip bold, not just the number (REVIEW-CARD-SPEC §5) — scoped to .mlink.pr so Token/Local chips are unaffected */
|
|
1998
|
+
.redesign .rmeta .mlink.pr,.redesign .rmeta .mlink.pr .mk,.redesign .rmeta .mlink.pr b,.redesign .rmeta .mlink.pr .mmut{font-weight:700}
|
|
1999
|
+
.redesign .rmeta .mlink:not(.pr){color:var(--ink2)}
|
|
2000
|
+
.redesign .rmeta .mlink:not(.pr) .mk{color:var(--ink3)}
|
|
2001
|
+
.redesign .rmeta .mlink:not(.pr) b{color:var(--ink)}
|
|
2002
|
+
.redesign .rmeta .mtok{cursor:pointer;padding:3px 10px;border-radius:999px;font:inherit;
|
|
2003
|
+
border:1px solid color-mix(in srgb,var(--green) 40%,var(--hair));background:none}
|
|
2004
|
+
.redesign .rmeta .mtok,.redesign .rmeta .mtok *{color:var(--green)}
|
|
2005
|
+
.redesign .rmeta .mtok b{font-weight:700}
|
|
2006
|
+
.redesign .rmeta .ok{color:var(--green)}
|
|
2007
|
+
.redesign .rmeta .tokchevron{font-style:normal;font-size:11px;line-height:1;transition:transform .2s;opacity:.8}
|
|
2008
|
+
.redesign .rmeta .mtok.open .tokchevron{transform:rotate(90deg)}
|
|
2009
|
+
.redesign .rmeta-detail{display:none;margin:12px 0 0;padding:9px 12px;background:color-mix(in srgb,var(--ink) 4%,transparent);border-radius:8px}
|
|
2010
|
+
.redesign .rmeta-detail.open{display:block}
|
|
2011
|
+
.redesign .rmeta-detail .k{font:600 8px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink3);margin-bottom:5px;display:block}
|
|
2012
|
+
.redesign .rmeta-detail .v{font:500 11.5px/1.6 var(--ui);color:var(--ink2)}
|
|
2013
|
+
/* sections — Linear-docs bold sentence-case headings + high-contrast body.
|
|
2014
|
+
margins are !important: the app's pre-existing .lg / .sa-item rules load first and
|
|
2015
|
+
otherwise tie/beat these on specificity, flattening the Linear-docs rhythm. */
|
|
2016
|
+
.redesign .rsec{margin-top:24px !important}
|
|
2017
|
+
.redesign .rsec-h{font-family:var(--ui);font-size:15px;font-weight:680;letter-spacing:-.012em;line-height:1.3;color:var(--ink);margin:0 0 9px !important}
|
|
2018
|
+
/* body prose = the app's normal text: 14px / −.012em, matching .bubble (Masa 2026-07-20:
|
|
2019
|
+
13px read thinner/lighter for Japanese than the surrounding app). §2. */
|
|
2020
|
+
.redesign .rbody{font-size:14px;line-height:1.62;letter-spacing:-.012em;color:var(--ink);max-width:70ch;margin:0 !important}
|
|
2021
|
+
.redesign .rdesc,.redesign .rrisk{color:var(--ink2)}
|
|
2022
|
+
.redesign .rfiles{margin:12px 0 0;font-family:var(--mono);font-size:12px;color:var(--ink2);line-height:1.75}
|
|
2023
|
+
.redesign .rfiles .a{color:#3fd07a;font-style:normal}.redesign .rfiles .d{color:#ff7b72;font-style:normal}
|
|
2024
|
+
/* Request accordion (spec §3) — chevron › rotate 90, body indented, no left rule */
|
|
2025
|
+
.redesign .rreq .rsec-h{margin-bottom:6px !important}
|
|
2026
|
+
.redesign .reqacc{margin-top:0 !important}
|
|
2027
|
+
.redesign .racc>summary{list-style:none;cursor:pointer;display:flex;gap:10px;align-items:baseline}
|
|
2028
|
+
.redesign .racc>summary::-webkit-details-marker{display:none}
|
|
2029
|
+
.redesign .racc>summary::before{content:"›";font-size:1.05em;line-height:1;color:var(--ink2);display:inline-block;transition:transform .14s}
|
|
2030
|
+
.redesign .racc[open]>summary::before{transform:rotate(90deg)}
|
|
2031
|
+
.redesign .reqacc>summary{color:var(--ink);font-size:13.5px;line-height:1.55}
|
|
2032
|
+
.redesign .reqbody{margin:14px 0 0 !important;padding-left:22px !important}
|
|
2033
|
+
.redesign .rsub{font-family:var(--ui);font-size:11px;font-weight:600;color:var(--ink3);margin:16px 0 8px !important}
|
|
2034
|
+
.redesign .reqbody>.rsub:first-child{margin-top:2px !important}
|
|
2035
|
+
.redesign .reqbody .lg-bullets{margin:0;padding:0 0 0 16px;font-size:13px;line-height:1.65;color:var(--ink2)}
|
|
2036
|
+
.redesign .reqbody .lg-bullets li{margin-bottom:5px}
|
|
2037
|
+
.redesign .reqbody .lg-splitnote{background:color-mix(in srgb,var(--ink) 4%,transparent);border-radius:8px;padding:10px 12px;margin-top:12px}
|
|
2038
|
+
.redesign .reqbody .lg-splitnote .hd{font:600 8.5px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);margin-bottom:7px}
|
|
2039
|
+
.redesign .reqbody .lg-splitnote ul{margin:0;padding:0;list-style:none;font-size:12px;line-height:1.8;color:var(--ink3)}
|
|
2040
|
+
.redesign .reqbody .lg-splitnote li.cur{color:var(--ink)} .redesign .reqbody .lg-splitnote li.cur b{color:var(--green);font-weight:600}
|
|
2041
|
+
/* workflow (spec §8) — vertical ✓ green / → green / □ dim */
|
|
2042
|
+
.redesign .rwf{display:flex;flex-direction:column;gap:2px;margin:10px 0 0 !important;font-family:var(--ui);font-size:12px;letter-spacing:-.004em;color:var(--ink3)}
|
|
2043
|
+
.redesign .rwf .wf-s{display:flex;align-items:center;gap:9px}
|
|
2044
|
+
.redesign .rwf .wf-s .mk{width:12px;text-align:center;flex:0 0 12px}
|
|
2045
|
+
.redesign .rwf .wf-s.ok{color:var(--ink2)}.redesign .rwf .wf-s.ok .mk{color:var(--green)}
|
|
2046
|
+
.redesign .rwf .wf-s.now{color:var(--ink);font-weight:600}.redesign .rwf .wf-s.now .mk{color:var(--green);font-weight:700}
|
|
2047
|
+
.redesign .rwf .wf-s.todo{color:var(--ink3)}.redesign .rwf .wf-s.todo .mk{color:var(--ink3);font-size:.82em}
|
|
2048
|
+
/* proof (spec §6) — before/after forced same size · run-log · honest missing note */
|
|
2049
|
+
.redesign .beforeafter{display:grid;grid-template-columns:1fr 1fr;gap:12px;max-width:640px;align-items:start}
|
|
2050
|
+
.redesign .ba-col{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
2051
|
+
.redesign .balabel{font-family:var(--mono);font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--ink3)}
|
|
2052
|
+
.redesign .proofimg{display:block;width:100%;height:auto;max-height:none;object-fit:contain;border:1px solid var(--hair);border-radius:9px;cursor:zoom-in;margin:0}
|
|
2053
|
+
.redesign .rproof{max-width:640px}
|
|
2054
|
+
/* screen-recording player (spec §6) — sits under before/after, same 640px column */
|
|
2055
|
+
.redesign .rvid{margin:12px 0 0;max-width:640px}
|
|
2056
|
+
.redesign .rvid svg,.redesign .rvid .proofvid{display:block;width:100%;height:auto;border:1px solid var(--hair);border-radius:9px;background:#000}
|
|
2057
|
+
.redesign .rvid .proofvid{aspect-ratio:16/9;object-fit:contain}
|
|
2058
|
+
.redesign .rvid figcaption{margin-top:7px;font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:var(--ink3)}
|
|
2059
|
+
.redesign .rrun{border:1px solid var(--hair);border-radius:10px;overflow:hidden;font-family:var(--mono);font-size:12.5px;max-width:640px}
|
|
2060
|
+
.redesign .rrun .rr{display:flex;justify-content:space-between;gap:16px;padding:10px 14px;border-top:1px solid var(--hair)}
|
|
2061
|
+
.redesign .rrun .rr:first-child{border-top:0}
|
|
2062
|
+
.redesign .rrun .rr .lab{color:var(--ink2)}.redesign .rrun .rr .v{color:var(--green);text-align:right}
|
|
2063
|
+
.redesign .rmiss{display:flex;align-items:flex-start;gap:8px;font-size:12.5px;line-height:1.6;color:var(--ink2)}
|
|
2064
|
+
.redesign .rmiss svg{width:15px;height:15px;flex:0 0 15px;margin-top:2px;fill:none;stroke:var(--amber);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
2065
|
+
/* GitHub diff = Bright (gh=d): old|new gutters · +/− column · line+gutter tint */
|
|
2066
|
+
.redesign .rdiff2{margin:12px 0 0;border:1px solid var(--hair);border-radius:10px;overflow:hidden;font-family:var(--mono);font-size:12px;line-height:1.6}
|
|
2067
|
+
.redesign .ghl{display:grid;grid-template-columns:46px 46px 22px 1fr}
|
|
2068
|
+
.redesign .ghl .gho,.redesign .ghl .ghn{color:var(--ink3);opacity:.6;text-align:right;padding:1px 8px;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums}
|
|
2069
|
+
.redesign .ghl .ghm{text-align:center;color:var(--ink3);padding:1px 0}
|
|
2070
|
+
.redesign .ghl .ghc{padding:1px 10px;white-space:pre-wrap;color:var(--ink)}
|
|
2071
|
+
.redesign .gh-add{background:rgba(46,204,113,.22)}
|
|
2072
|
+
.redesign .gh-add .gho,.redesign .gh-add .ghn{background:rgba(46,204,113,.30)}
|
|
2073
|
+
.redesign .gh-add .ghm{color:#3fb950}
|
|
2074
|
+
.redesign .gh-del{background:rgba(248,81,73,.20)}
|
|
2075
|
+
.redesign .gh-del .gho,.redesign .gh-del .ghn{background:rgba(248,81,73,.30)}
|
|
2076
|
+
.redesign .gh-del .ghm{color:#f85149}
|
|
2077
|
+
.redesign .gh-hi{border-radius:2px;padding:0 1px}
|
|
2078
|
+
.redesign .gh-add .gh-hi{background:rgba(46,204,113,.55)} .redesign .gh-del .gh-hi{background:rgba(248,81,73,.5)}
|
|
2079
|
+
/* huge-diff footer (spec §7): "…+N 行 · GitHub で開く ↗" */
|
|
2080
|
+
.redesign .ghfoot{margin-top:8px;font-family:var(--mono);font-size:11.5px;color:var(--ink3);display:flex;gap:2px;align-items:center;flex-wrap:wrap}
|
|
2081
|
+
.redesign .ghfoot .ghopen{color:var(--ink2);text-decoration:none}
|
|
2082
|
+
.redesign .ghfoot .ghopen:hover{color:var(--blue)}
|
|
2083
|
+
/* meta links carry NO underline — PR is a real <a href> (spec §5, Masa 2026-07-20) */
|
|
2084
|
+
.redesign .rmeta a{text-decoration:none}
|
|
2085
|
+
/* Request / What-to-check = full contrast, same ink as the title (spec §2, no fade) */
|
|
2086
|
+
.redesign .rask,.redesign .rcheck,.redesign .rcheck li,.redesign .rcheck1{color:var(--ink) !important}
|
|
2087
|
+
.redesign ul.rcheck{margin:2px 0 0;padding:0 0 0 18px;font-size:14px;line-height:1.7;letter-spacing:-.012em}
|
|
2088
|
+
.redesign ul.rcheck li{margin-bottom:6px}
|
|
2089
|
+
/* Activity (spec §3) = ONE Linear-docs pulldown. Heading plain; chevron on the FIRST
|
|
2090
|
+
line; tapping opens the rest below, offset-indented (no left rule); Discussion folds
|
|
2091
|
+
in at the bottom of the opened body. */
|
|
2092
|
+
.redesign .ract{margin-top:24px !important}
|
|
2093
|
+
.redesign .ract>.rsec-h{margin-bottom:10px !important}
|
|
2094
|
+
.redesign .lacc>summary{list-style:none;cursor:pointer;display:flex;gap:9px;align-items:baseline}
|
|
2095
|
+
.redesign .lacc>summary::-webkit-details-marker{display:none}
|
|
2096
|
+
.redesign .lacc .ltog{flex:0 0 auto;color:var(--ink3);font-style:normal;line-height:1;transition:transform .14s}
|
|
2097
|
+
.redesign .lacc[open]>summary .ltog{transform:rotate(90deg)}
|
|
2098
|
+
.redesign .lacc .lfirst{flex:1;min-width:0}
|
|
2099
|
+
.redesign .lacc .lbody{margin-top:12px;padding-left:18px}
|
|
2100
|
+
/* transcript rows — Linear-clean, borderless (You / tool mono / res green / Claude) */
|
|
2101
|
+
.redesign .rtx{border:0;background:none}
|
|
2102
|
+
.redesign .txr{display:flex;gap:12px;padding:5px 0;font-size:13px;line-height:1.55}
|
|
2103
|
+
.redesign .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3);padding-top:1px}
|
|
2104
|
+
.redesign .txr.you .w,.redesign .txr.cl .w{color:var(--ink);font-weight:600}
|
|
2105
|
+
.redesign .txr .m{flex:1;min-width:0;color:var(--ink2)}
|
|
2106
|
+
.redesign .txr.cl .m{color:var(--ink)}
|
|
2107
|
+
.redesign .txr.tool .m{color:var(--ink2);font-family:var(--mono);font-size:12px}
|
|
2108
|
+
.redesign .txr.tool .m b{color:var(--ink);font-weight:600}
|
|
2109
|
+
.redesign .txr.res .m{font-family:var(--mono);font-size:12px;color:var(--green)}
|
|
2110
|
+
/* Discussion (review back-and-forth) folded at the bottom of Activity's body */
|
|
2111
|
+
.redesign .lbody .rthread{display:flex;flex-direction:column;gap:16px;margin:0}
|
|
2112
|
+
.redesign .lbody .rmsg .who{font-size:12px;font-weight:600;color:var(--ink);margin-bottom:4px}
|
|
2113
|
+
.redesign .lbody .rmsg .msg{font-size:13px;line-height:1.55;color:var(--ink2)}
|
|
2114
|
+
/* sensitive-area note (real) sits below What changed */
|
|
2115
|
+
.redesign .lg-risk{margin-top:20px}
|
|
1951
2116
|
#saconf{position:fixed;inset:0;pointer-events:none;z-index:99;display:block}
|
|
1952
2117
|
/* retest ×3 note surfaces on the lane review card too (v45 §5.3) */
|
|
1953
2118
|
#fsRoot .rcard .rnote{display:block;font:500 9.5px/1.4 var(--mono);color:var(--green);margin-top:3px}
|
|
@@ -2834,7 +2999,7 @@
|
|
|
2834
2999
|
<div class="reqlist" id="reviewList"></div>
|
|
2835
3000
|
<div class="revactions">
|
|
2836
3001
|
<button type="button" class="reqbtn approve" id="reviewApprove">Approve goal</button>
|
|
2837
|
-
<button type="button" class="reqbtn dismiss" id="reviewDismiss">
|
|
3002
|
+
<button type="button" class="reqbtn dismiss" id="reviewDismiss">Dismiss</button>
|
|
2838
3003
|
<span style="flex:1"></span>
|
|
2839
3004
|
<button class="mbtn ghost" id="reviewClose">Close</button>
|
|
2840
3005
|
</div>
|
|
@@ -2904,7 +3069,7 @@
|
|
|
2904
3069
|
</div>
|
|
2905
3070
|
<div class="sa-act">
|
|
2906
3071
|
<button type="button" class="sa-app" id="gdApprove"><svg viewBox="0 0 24 24"><path d="M5 12.5l4.2 4.2L19 7"/></svg>Approve</button>
|
|
2907
|
-
<button type="button" class="sa-dis" id="gdDismiss">
|
|
3072
|
+
<button type="button" class="sa-dis" id="gdDismiss">Dismiss</button>
|
|
2908
3073
|
<div class="sa-chatwrap">
|
|
2909
3074
|
<input type="text" class="sa-chat" id="gdRevisionInput" placeholder="Tell the worker what to try next…">
|
|
2910
3075
|
<button type="button" class="sa-send" id="gdSend" title="Send" aria-label="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>
|
|
@@ -2989,7 +3154,7 @@ const savedTheme = (() => {
|
|
|
2989
3154
|
if (v !== s) localStorage.setItem('theme', v);
|
|
2990
3155
|
return v;
|
|
2991
3156
|
})();
|
|
2992
|
-
const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: '
|
|
3157
|
+
const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
2993
3158
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
2994
3159
|
// except model which is sticky in localStorage). palette = open command list.
|
|
2995
3160
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -3022,7 +3187,7 @@ function restoreFocusedAnswerInput(container, attr, saved){
|
|
|
3022
3187
|
const STATUS_LABEL = {
|
|
3023
3188
|
sending: 'Sending…', stacked: 'Queued', planning: 'Planning…', queued: 'To do', running: 'Working…', needsInput: 'Needs input',
|
|
3024
3189
|
review: 'In review', done: 'Done', failed: 'Failed', partial: 'Partly failed', interrupted: 'Interrupted', skipped: 'Skipped',
|
|
3025
|
-
rejected: '
|
|
3190
|
+
rejected: 'Dismissed',
|
|
3026
3191
|
};
|
|
3027
3192
|
// Mirrors engine/lib.mjs goalChip() — kept in sync manually since this
|
|
3028
3193
|
// inline script can't import server-side code. `columns` (a project's
|
|
@@ -3041,7 +3206,7 @@ const GOAL_CHIP = (s, columns, startedAt) => s === 'stacked' || s === 'sending'
|
|
|
3041
3206
|
s === 'done' ? [pickColumnLabel(columns, 'done', 'DONE'), ''] :
|
|
3042
3207
|
s === 'retesting' ? ['RE-TESTING', 'doing'] :
|
|
3043
3208
|
s === 'reverted' ? ['REVERTED', ''] :
|
|
3044
|
-
s === 'rejected' ? ['
|
|
3209
|
+
s === 'rejected' ? ['DISMISSED', 'rejected'] :
|
|
3045
3210
|
s === 'folded' ? ['FOLDED', ''] :
|
|
3046
3211
|
s === 'blocked' ? ['BLOCKED', 'blocked'] : s === 'needsInput' ? ['NEEDS INPUT', 'blocked'] : ['FAIL', ''];
|
|
3047
3212
|
const FINISHED = (s) => ['done', 'partial', 'failed', 'interrupted'].includes(s);
|
|
@@ -3351,7 +3516,11 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3351
3516
|
// stays only as a last resort for a goal with no text. Pulled into one place so the
|
|
3352
3517
|
// review card's own title and its sibling-goal titles (H23 split note) stay consistent.
|
|
3353
3518
|
function saReviewTitle(g){
|
|
3354
|
-
|
|
3519
|
+
// The goal's short ticket NAME wins everywhere it's shown (See-all title, Review-list
|
|
3520
|
+
// rail, board, sibling list) so the title reads the same in every place and never
|
|
3521
|
+
// repeats the full request (Masa 2026-07-21, spec §4). Real goals have no
|
|
3522
|
+
// reviewSummary.title, so they fall back to the request text unchanged (no regression).
|
|
3523
|
+
return String(g?.reviewSummary?.title || g?.text || g?.plan?.[0] || g?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim().slice(0, 90);
|
|
3355
3524
|
}
|
|
3356
3525
|
// H23 (2026-07-18) evidence-kind detection. Mirrors engine/lib.mjs isUiChange() /
|
|
3357
3526
|
// hasTestRelevantChanges() — kept in sync manually since the browser script isn't a
|
|
@@ -4370,7 +4539,7 @@ function renderTasks(){
|
|
|
4370
4539
|
// no auto-rework. Rework is asked for via chat.
|
|
4371
4540
|
b.onclick = async (e) => { e.stopPropagation();
|
|
4372
4541
|
let r; for (const id of b.dataset.qdismiss.split(',')) r = await fetch(withKey(`/api/goals/${id}/reject`), { method: 'POST' });
|
|
4373
|
-
qrefresh(r, '
|
|
4542
|
+
qrefresh(r, 'Dismiss failed.'); };
|
|
4374
4543
|
}
|
|
4375
4544
|
// Archive: retire an unfinished goal cleanly (no rework spawned). The escape
|
|
4376
4545
|
// hatch for half-finished (partial/blocked) items so Attention stays drainable.
|
|
@@ -5290,7 +5459,7 @@ async function sendReviewReject(){
|
|
|
5290
5459
|
upsert(state.goals, body.goal);
|
|
5291
5460
|
closeReviewChecklist();
|
|
5292
5461
|
render();
|
|
5293
|
-
} catch { showErr('
|
|
5462
|
+
} catch { showErr('Dismiss failed.'); }
|
|
5294
5463
|
}
|
|
5295
5464
|
$('reviewDismiss').onclick = () => sendReviewReject();
|
|
5296
5465
|
$('reviewFixSend').onclick = () => { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); };
|
|
@@ -5490,7 +5659,7 @@ async function sendReject(goalId){
|
|
|
5490
5659
|
upsert(state.goals, body.goal);
|
|
5491
5660
|
closeGoalDetail();
|
|
5492
5661
|
render();
|
|
5493
|
-
} catch { showErr('
|
|
5662
|
+
} catch { showErr('Dismiss failed.'); }
|
|
5494
5663
|
$('gdApprove').disabled = false; $('gdDismiss').disabled = false; $('gdSend').disabled = false;
|
|
5495
5664
|
}
|
|
5496
5665
|
async function sendDismiss(goalId, text){
|
|
@@ -6181,7 +6350,7 @@ function renderFsReview(){
|
|
|
6181
6350
|
for (const b of el.querySelectorAll('[data-fsdismiss]')) b.onclick = async (e) => { e.stopPropagation();
|
|
6182
6351
|
// 07-20c: the review-card ✕ is a pure Reject → Rejected column (/reject), no auto-rework.
|
|
6183
6352
|
let r; for (const id of b.dataset.fsdismiss.split(',')) r = await fetch(withKey(`/api/goals/${id}/reject`), { method: 'POST' });
|
|
6184
|
-
if (r && r.ok) await refresh(); else showErr('
|
|
6353
|
+
if (r && r.ok) await refresh(); else showErr('Dismiss failed.'); };
|
|
6185
6354
|
}
|
|
6186
6355
|
// Shared To Do bucketing (list rows AND the kanban board read the same groups —
|
|
6187
6356
|
// single source of truth, see renderFsBoard below). Pure of the DOM: state in, groups out.
|
|
@@ -6308,7 +6477,7 @@ function renderFsTodo(){
|
|
|
6308
6477
|
<div class="tl">
|
|
6309
6478
|
${(doingRows || paused) ? `<span class="tlcap tlcap-doing" style="padding-top:0">Doing<button class="tlcap-pause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume — 再開' : 'Pause — 一時停止'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button></span>${doingRows}` : ''}
|
|
6310
6479
|
${needsRows ? `<span class="tlcap needs"${(doingRows || paused) ? '' : ' style="padding-top:0"'}>Needs you</span>${needsRows}` : ''}
|
|
6311
|
-
${rejected.length ? `<span class="tlcap rejcap"${(doingRows || paused) || needsRows ? '' : ' style="padding-top:0"'}>
|
|
6480
|
+
${rejected.length ? `<span class="tlcap rejcap"${(doingRows || paused) || needsRows ? '' : ' style="padding-top:0"'}>Dismissed<span class="num">${rejected.length}</span></span>${rejected.map((g) => `<div class="trow up rej"><span class="st rej"></span>${noSpan(g.id)}<span class="tt">${tt(g.text, 70)}</span><span class="acts"><button class="ab" data-fsreopen="${g.id}" title="Reopen — back to Review">${FS_ICONS.reopen}</button><button class="ab" data-fsrejdel="${g.id}" title="Delete — discard (closes the PR)">${FS_ICONS.del}</button></span></div>`).join('')}` : ''}
|
|
6312
6481
|
${upRows ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${upRows}` : ''}
|
|
6313
6482
|
${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
|
|
6314
6483
|
${laterRows ? `<span class="tlcap">Later</span>${laterRows}` : ''}
|
|
@@ -7925,16 +8094,46 @@ function saRows(){
|
|
|
7925
8094
|
goalText: String(g?.text ?? '').replace(/\s+/g, ' ').trim(),
|
|
7926
8095
|
did, siblings, risk: g?.risk ?? null,
|
|
7927
8096
|
note: g?.retest?.passed ? g.retest.note : null,
|
|
7928
|
-
pr: r.pr,
|
|
8097
|
+
pr: r.pr,
|
|
8098
|
+
// Explicit review-summary display fields (spec §11: the card's shape follows the
|
|
8099
|
+
// task). A goal's reviewSummary may carry them directly (e.g. a conversational task
|
|
8100
|
+
// that reports an Answer + its Activity, or a ticket whose test count / diff totals
|
|
8101
|
+
// are recorded rather than re-derived); each falls back to the computed value.
|
|
8102
|
+
testResult: (typeof (g?.reviewSummary?.tests) === 'string')
|
|
8103
|
+
? { ran: true, ok: true, passed: parseInt(g.reviewSummary.tests, 10) || 0, failed: 0, label: g.reviewSummary.tests }
|
|
8104
|
+
: r.testResult,
|
|
8105
|
+
answer: g?.reviewSummary?.answer ?? null,
|
|
8106
|
+
activity: Array.isArray(g?.reviewSummary?.activity) ? g.reviewSummary.activity : null,
|
|
8107
|
+
discussion: Array.isArray(g?.reviewSummary?.discussion) ? g.reviewSummary.discussion : null,
|
|
8108
|
+
changedDesc: g?.reviewSummary?.changed ?? null,
|
|
8109
|
+
metaFiles: Array.isArray(g?.reviewSummary?.files) ? g.reviewSummary.files : null,
|
|
8110
|
+
metaAdd: g?.reviewSummary?.a ?? null, metaDel: g?.reviewSummary?.d ?? null,
|
|
8111
|
+
ghRows: Array.isArray(g?.reviewSummary?.ghdiff) ? g.reviewSummary.ghdiff : null,
|
|
8112
|
+
diffTotal: g?.reviewSummary?.diffTotal ?? null,
|
|
7929
8113
|
proofTaskId: pt?.id ?? null, proofImg: pt?.proof?.png ?? pt?.proof?.gif ?? null,
|
|
7930
8114
|
// Ledger before/after (docs/design/HANDOFF-review-card-evidence-first-2026-07-18.md
|
|
7931
8115
|
// §3 kind==='image'): most tasks never have a "before" capture, so this is almost
|
|
7932
8116
|
// always null and beforeAfterMediaHtml() falls back to the single-shot view.
|
|
7933
8117
|
proofBefore: pt?.proof?.beforePng ?? pt?.proof?.beforeGif ?? null,
|
|
8118
|
+
// Recording (spec §5/§6): a proof task may carry a screen recording (proof.mp4)
|
|
8119
|
+
// or just a recording:true flag; the goal's reviewSummary can also name one.
|
|
8120
|
+
// Drives the neutral "Verify recording ↗" meta chip + the Proof video player.
|
|
8121
|
+
// "ないものは載せない": almost all real tasks have none, so both stay hidden.
|
|
8122
|
+
recording: Boolean(pt?.proof?.mp4 || pt?.proof?.recording || g?.reviewSummary?.recording),
|
|
8123
|
+
recordingUrl: pt?.proof?.mp4 ? withKey(`/proof/${pt.id}/${pt.proof.mp4}`) : (typeof g?.reviewSummary?.recording === 'string' ? g.reviewSummary.recording : null),
|
|
8124
|
+
// Local URL (spec §5): a running localhost the reviewer can open ("Local localhost:4809 ↗").
|
|
8125
|
+
// Falls back to the existing "Local run ↗" (runlocal) button when absent.
|
|
8126
|
+
localUrl: g?.reviewSummary?.localUrl ?? null,
|
|
8127
|
+
// Client-side diff fallback: the git-backed /diff endpoint is null for seeded/
|
|
8128
|
+
// offline goals, so "What changed" never renders. A unified-diff string stored on
|
|
8129
|
+
// the goal (or any of its tasks) lets saGhDiffHtml render locally with no server.
|
|
8130
|
+
diffStr: g?.diff ?? (ts.filter((t) => r.goalIds.includes(t.goalId)).map((t) => t.diff).find(Boolean)) ?? null,
|
|
7934
8131
|
// Token-frugality is the product's whole pitch — surface it here too, not
|
|
7935
8132
|
// just in the per-goal review modal. buildReviewSummary computes the same
|
|
7936
8133
|
// estimate; reuse its logic directly off the goal's runUsage/runTokens.
|
|
7937
|
-
tokenOpt:
|
|
8134
|
+
tokenOpt: (typeof g?.reviewSummary?.token === 'number')
|
|
8135
|
+
? { percent: g.reviewSummary.token, basis: 'cache', techniques: ['prompt caching', '/compact'] }
|
|
8136
|
+
: estimateTokenOptimizationLocal({
|
|
7938
8137
|
usage: g?.runUsage, weightedTokens: g?.runTokens, cacheReadWeight: g?.runTokenWeight,
|
|
7939
8138
|
sessionMode: g?.executionPlan?.sessionMode,
|
|
7940
8139
|
usedHandoff: g?.executionPlan?.sessionMode === 'cold-handoff' || Boolean(g?.contextHandoff),
|
|
@@ -8020,8 +8219,82 @@ function saEvidenceHtml(kind, factRow, img, filePaths, diffText, gtr, it){
|
|
|
8020
8219
|
// kind === 'image' (default)
|
|
8021
8220
|
return `<div class="lg-ev"><div class="lg-evhd">Evidence</div><div class="row">${factRow}</div>${img}</div>`;
|
|
8022
8221
|
}
|
|
8222
|
+
// Unified-diff → GitHub two-gutter model (REVIEW-CARD-SPEC §7 "GitHub diff = Bright"):
|
|
8223
|
+
// walk the @@ hunk headers to recover the old/new line numbers, emit one row per line
|
|
8224
|
+
// {o:oldNo|null, n:newNo|null, t:'add'|'del'|'ctx', c:text}. Word-level highlight is
|
|
8225
|
+
// NOT fabricated from a raw diff (the product never invents evidence) — the Bright
|
|
8226
|
+
// line+gutter tint carries the change and .gh-hi simply stays absent. Pure, so it is
|
|
8227
|
+
// extracted and unit-tested in engine/test/review-card-diff.test.mjs.
|
|
8228
|
+
function saUnifiedToGh(diff, max){
|
|
8229
|
+
const cap = max || 80; const out = []; let o = 0, n = 0;
|
|
8230
|
+
for (const raw of String(diff || '').split('\n')) {
|
|
8231
|
+
if (raw === '') continue; // split artifact / blank input — a real blank ctx line is a single space
|
|
8232
|
+
if (/^(diff --git|index |--- |\+\+\+ |new file|deleted file|old mode|new mode|similarity|rename |Binary )/.test(raw)) continue;
|
|
8233
|
+
const m = raw.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
8234
|
+
if (m) { o = +m[1]; n = +m[2]; continue; }
|
|
8235
|
+
if (raw.startsWith('\\')) continue; // ""
|
|
8236
|
+
if (raw.startsWith('+')) out.push({ o: null, n: n++, t: 'add', c: raw.slice(1) });
|
|
8237
|
+
else if (raw.startsWith('-')) out.push({ o: o++, n: null, t: 'del', c: raw.slice(1) });
|
|
8238
|
+
else out.push({ o: o++, n: n++, t: 'ctx', c: raw.replace(/^ /, '') });
|
|
8239
|
+
if (out.length >= cap) break;
|
|
8240
|
+
}
|
|
8241
|
+
return out;
|
|
8242
|
+
}
|
|
8243
|
+
function saGhDiffHtml(diff){
|
|
8244
|
+
const rows = saUnifiedToGh(diff);
|
|
8245
|
+
if (!rows.length) return '';
|
|
8246
|
+
return saGhDiffRowsHtml(rows);
|
|
8247
|
+
}
|
|
8248
|
+
// One Activity transcript row (spec §3): You / tool (mono, bold name) / res (green) /
|
|
8249
|
+
// Claude. Mirrors the flagship artifact's tx mapping so the pulldown pixel-matches.
|
|
8250
|
+
function saTxRowHtml(e){
|
|
8251
|
+
if (e[0] === 'tool') return `<div class="txr tool"><span class="w">tool</span><span class="m"><b>${esc(e[1])}</b> ${esc(e[2] || '')}</span></div>`;
|
|
8252
|
+
if (e[0] === 'res') return `<div class="txr res"><span class="w"></span><span class="m">${esc(String(e[1]).replace(/^✗ /, '').replace(/^✓ /, ''))}</span></div>`;
|
|
8253
|
+
if (e[0] === 'user') return `<div class="txr you"><span class="w">You</span><span class="m">${esc(e[1])}</span></div>`;
|
|
8254
|
+
return `<div class="txr cl"><span class="w">Claude</span><span class="m">${esc(e[1])}</span></div>`;
|
|
8255
|
+
}
|
|
8256
|
+
// Structured GitHub diff (REVIEW-CARD-SPEC §7): renders {o,n,t,c,h} rows with a
|
|
8257
|
+
// word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub で開く ↗"
|
|
8258
|
+
// footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
|
|
8259
|
+
// ghDiffHtml so the app pixel-matches. `more` = extra unshown line count; `pr` = url.
|
|
8260
|
+
function saGhDiffRowsHtml(rows, more, pr){
|
|
8261
|
+
if (!rows || !rows.length) return '';
|
|
8262
|
+
const body = '<div class="rdiff2">' + rows.map((l) => {
|
|
8263
|
+
const cls = l.t === 'add' ? 'gh-add' : l.t === 'del' ? 'gh-del' : 'gh-ctx';
|
|
8264
|
+
const mk = l.t === 'add' ? '+' : l.t === 'del' ? '−' : '';
|
|
8265
|
+
let code = esc(l.c);
|
|
8266
|
+
if (l.h) { const h = esc(l.h); code = code.replace(h, `<span class="gh-hi">${h}</span>`); }
|
|
8267
|
+
return `<div class="ghl ${cls}"><span class="gho">${l.o ?? ''}</span><span class="ghn">${l.n ?? ''}</span><span class="ghm">${mk}</span><span class="ghc">${code}</span></div>`;
|
|
8268
|
+
}).join('') + '</div>';
|
|
8269
|
+
const foot = more ? `<div class="ghfoot"><span>…+${more} 行</span>${pr ? ` · <a class="ghopen" href="${esc(pr)}" target="_blank" rel="noopener">GitHub で開く ↗</a>` : ''}</div>` : '';
|
|
8270
|
+
return body + foot;
|
|
8271
|
+
}
|
|
8272
|
+
// Screen-recording player for Proof (REVIEW-CARD-SPEC §6). A real file (recordingUrl)
|
|
8273
|
+
// renders a native <video controls>; otherwise a self-contained SVG player mockup
|
|
8274
|
+
// (faint app frame · centred play · scrub bar) matching the artifact's videoFig, with
|
|
8275
|
+
// a "▸ Watch recording · 0:12" caption. Purely presentational — no fabricated evidence
|
|
8276
|
+
// (only rendered when a recording actually exists on the task/goal).
|
|
8277
|
+
function saVideoFigHtml(url){
|
|
8278
|
+
if (url) return `<figure class="rvid"><video class="proofvid" src="${esc(url)}" controls preload="metadata"></video><figcaption>▸ Watch recording</figcaption></figure>`;
|
|
8279
|
+
const P = 'var(--panel)', H = 'var(--hair)';
|
|
8280
|
+
const svg = `<svg viewBox="0 0 560 315" preserveAspectRatio="xMidYMid meet"><rect width="560" height="315" fill="${P}"/>`
|
|
8281
|
+
+ `<rect x="24" y="20" width="120" height="10" rx="3" fill="${H}"/><rect x="24" y="44" width="220" height="8" rx="3" fill="${H}"/><rect x="24" y="64" width="180" height="8" rx="3" fill="${H}"/>`
|
|
8282
|
+
+ `<circle cx="280" cy="165" r="34" fill="rgba(0,0,0,.35)"/><path d="M270 150 l24 15 l-24 15 z" fill="#fff"/>`
|
|
8283
|
+
+ `<rect x="24" y="286" width="512" height="4" rx="2" fill="${H}"/><rect x="24" y="286" width="150" height="4" rx="2" fill="#4fd79c"/><circle cx="174" cy="288" r="6" fill="#4fd79c"/></svg>`;
|
|
8284
|
+
return `<figure class="rvid">${svg}<figcaption>▸ Watch recording · 0:12</figcaption></figure>`;
|
|
8285
|
+
}
|
|
8286
|
+
// Workflow (REVIEW-CARD-SPEC §8): vertical ✓ / → / □ built ONLY from real signals on
|
|
8287
|
+
// this goal — each ✓ reflects something that actually happened (implemented, tested,
|
|
8288
|
+
// proof captured); the card's own "In review" is the → here; merge/deploy are ahead.
|
|
8289
|
+
// No fabricated pipeline: an untested goal shows □ Tested, not a green ✓.
|
|
8290
|
+
function saWorkflowHtml(it){
|
|
8291
|
+
const ran = Boolean(it.testResult?.ran);
|
|
8292
|
+
const hasProof = Boolean(it.proofImg && it.proofTaskId != null);
|
|
8293
|
+
const steps = [['Implemented', 'ok'], ['Tested', ran ? 'ok' : 'todo'], ['Proof captured', hasProof ? 'ok' : 'todo'], ['In review', 'now'], ['Merge', 'todo'], ['Deploy', 'todo']];
|
|
8294
|
+
return '<div class="rwf">' + steps.map(([s, st]) => `<div class="wf-s ${st}"><span class="mk">${st === 'ok' ? '✓' : st === 'now' ? '→' : '□'}</span>${s}</div>`).join('') + '</div>';
|
|
8295
|
+
}
|
|
8023
8296
|
function saItemHtml(it, i){
|
|
8024
|
-
const done = { 1: ['ok', '✓', 'Approved'], 2: ['no', '↩', '
|
|
8297
|
+
const done = { 1: ['ok', '✓', 'Approved'], 2: ['no', '↩', 'Dismissed'], 3: ['no', '↩', 'Feedback sent — returned to To Do'], 4: ['ar', '—', 'Archived'], 5: ['no', '↩', 'Reverted — change undone'] }[it.st];
|
|
8025
8298
|
if (done) {
|
|
8026
8299
|
// Revert deliberately has NO undo: the PR is closed and its branch deleted —
|
|
8027
8300
|
// an "UNDO" here would be a lie (HANDOFF-v45 §5.3 honesty over symmetry).
|
|
@@ -8029,70 +8302,162 @@ function saItemHtml(it, i){
|
|
|
8029
8302
|
return `<div class="sa-done ${done[0]}"><span class="mk">${done[1]}</span>${done[2]} · <span style="color:var(--ink3)">${esc(it.title.slice(0, 44))}</span>${undo}</div>`;
|
|
8030
8303
|
}
|
|
8031
8304
|
const d = SA.diffs[it.goalId];
|
|
8032
|
-
// Honest evidence chips (mirrors the vchip tested/proof/needs-you logic in
|
|
8033
|
-
// renderReviewChecklist ~L3742-3752): absence of a signal is shown as a muted
|
|
8034
|
-
// chip, never silently dropped — an untested/proofless goal must never LOOK verified.
|
|
8035
|
-
// H23: facts stop looking like buttons — glyph carries the state colour, text
|
|
8036
|
-
// stays plain ink, no border/pill (border is reserved for the PR link).
|
|
8037
8305
|
const gtr = it.testResult;
|
|
8038
|
-
const testsChip = gtr?.ran
|
|
8039
|
-
? (gtr.ok ? `<span class="vf ok"><i class="g">✓</i>${gtr.passed} passed</span>` : `<span class="vf no"><i class="g">✗</i>${gtr.passed}/${gtr.passed + (gtr.failed || 0)} passed</span>`)
|
|
8040
|
-
: '<span class="vf none">— not tested</span>';
|
|
8041
8306
|
const hasProofImg = it.proofImg && it.proofTaskId != null;
|
|
8042
|
-
// Before/after (
|
|
8043
|
-
//
|
|
8044
|
-
//
|
|
8045
|
-
//
|
|
8046
|
-
//
|
|
8047
|
-
// after the card first paints (saEnhanceDiffBoxes, below saRender). SA.diffBoxHtml
|
|
8048
|
-
// holds the result — a string once a box is found, null once ruled out, undefined
|
|
8049
|
-
// until then — so it survives every later saRender() the same way SA.diffs already
|
|
8050
|
-
// does for the changed-files diff.
|
|
8307
|
+
// Before/after (spec §6): reuse the same beforeafter/ba-col/balabel renderer as the
|
|
8308
|
+
// per-task checklist (beforeAfterMediaHtml). it.proofBefore is null for the ~all
|
|
8309
|
+
// tasks with no "before" capture, so it falls back to the single <img class="proofimg">.
|
|
8310
|
+
// Diff-box + zoom inset is computed async after first paint (saEnhanceDiffBoxes);
|
|
8311
|
+
// SA.diffBoxHtml caches it so the box survives every later saRender().
|
|
8051
8312
|
const boxedImg = SA.diffBoxHtml[it.goalId];
|
|
8052
8313
|
const img = hasProofImg
|
|
8053
|
-
? (typeof boxedImg === 'string' ? boxedImg : beforeAfterMediaHtml(it.proofTaskId, { png: it.proofImg, beforePng: it.proofBefore }, '
|
|
8314
|
+
? (typeof boxedImg === 'string' ? boxedImg : beforeAfterMediaHtml(it.proofTaskId, { png: it.proofImg, beforePng: it.proofBefore }, esc(it.title ?? '')))
|
|
8054
8315
|
: '';
|
|
8055
|
-
// Changed-file paths
|
|
8056
|
-
//
|
|
8057
|
-
//
|
|
8316
|
+
// Changed-file paths: d from the diff endpoint ({add,del,files:[{path,add,del}],diff}),
|
|
8317
|
+
// falling back to task.changedFiles when the server has no diff for this goal.
|
|
8318
|
+
// A usable git diff has actual files; the endpoint returns an empty {add:0,del:0,
|
|
8319
|
+
// files:[],diff:''} object for goals with no committed range (seeded/uncommitted),
|
|
8320
|
+
// which must NOT shadow the client-side fallback below.
|
|
8321
|
+
const hasGitDiff = Boolean(d && d.diff);
|
|
8058
8322
|
let filePaths = [], nf = 0;
|
|
8059
|
-
if (d) { filePaths = d.files.map((f) => f.path); nf = filePaths.length; }
|
|
8060
|
-
else
|
|
8323
|
+
if (d && d.files.length) { filePaths = d.files.map((f) => f.path); nf = filePaths.length; }
|
|
8324
|
+
else {
|
|
8325
|
+
// no usable git diff: fall back to the tasks' own changedFiles so Files / What
|
|
8326
|
+
// changed still populate for seeded/offline goals.
|
|
8061
8327
|
filePaths = [...new Set((state.tasks || []).filter((t) => it.ids.includes(t.goalId)).flatMap((t) => t.changedFiles ?? []))];
|
|
8062
8328
|
nf = filePaths.length;
|
|
8063
8329
|
}
|
|
8330
|
+
// Client-side unified diff (spec §4 fallback): render "What changed" locally when the
|
|
8331
|
+
// server has no usable git diff. Line totals feed the Files chip's +add / −del.
|
|
8332
|
+
const clientDiff = (!hasGitDiff && it.diffStr) ? it.diffStr : null;
|
|
8333
|
+
let cAdd = 0, cDel = 0;
|
|
8334
|
+
if (clientDiff) for (const l of saUnifiedToGh(clientDiff, 2000)) { if (l.t === 'add') cAdd++; else if (l.t === 'del') cDel++; }
|
|
8335
|
+
// Recorded display fields (spec §11: card shape follows the task). A goal's reviewSummary
|
|
8336
|
+
// may carry an explicit file list; prefer it (real git-backed goals keep the computed
|
|
8337
|
+
// filePaths above). Split the "path +N −M" string down to the bare path for kind/log.
|
|
8338
|
+
if (it.metaFiles) { filePaths = it.metaFiles.map((f) => String(f).split(/\s{2,}/)[0]); nf = it.metaFiles.length; }
|
|
8064
8339
|
const kind = saEvidenceKind(filePaths, hasProofImg, gtr);
|
|
8065
|
-
const factRow = [
|
|
8066
|
-
testsChip,
|
|
8067
|
-
it.note ? `<span class="vf ok"><i class="g">✓</i>${esc(it.note)}</span>` : '',
|
|
8068
|
-
`<span class="fls">${nf} file${nf === 1 ? '' : 's'}</span>`,
|
|
8069
|
-
hasProofImg ? '' : '<span class="vf none">— no proof</span>',
|
|
8070
|
-
].filter(Boolean).join('');
|
|
8071
8340
|
const gk = it.goalId;
|
|
8072
|
-
|
|
8073
|
-
//
|
|
8074
|
-
//
|
|
8075
|
-
const
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8341
|
+
const rs = it.goal?.reviewSummary || {};
|
|
8342
|
+
// Conversational task (no code change): drops Proof + What changed, shows an Answer
|
|
8343
|
+
// instead (spec §11); its Activity IS the conversation.
|
|
8344
|
+
const isConv = Boolean(it.answer);
|
|
8345
|
+
// ── meta (spec §5) — fully data-driven / hide-empty: each chip renders only when its
|
|
8346
|
+
// data exists (no PR → no PR chip · no tests → no Tests · no file totals → no Files ·
|
|
8347
|
+
// no recording → no Verify). PR = bold green, no underline (a real <a href>). ──
|
|
8348
|
+
const arw = '<span class="ar">↗</span>';
|
|
8349
|
+
const prNum = it.pr ? (it.pr.match(/pull\/(\d+)/) ?? [, ''])[1] : '';
|
|
8350
|
+
const tokPct = it.tokenOpt ? Number(it.tokenOpt.percent) : 0;
|
|
8351
|
+
const testsLabel = gtr?.ran ? (gtr.label || (gtr.ok ? `${gtr.passed} passed` : `${gtr.passed}/${gtr.passed + (gtr.failed || 0)} passed`)) : '';
|
|
8352
|
+
// Files totals: recorded strings (metaAdd/metaDel, already signed) win, else git/client.
|
|
8353
|
+
const fAdd = it.metaAdd ?? (hasGitDiff ? `+${d.add}` : clientDiff ? `+${cAdd}` : null);
|
|
8354
|
+
const fDel = it.metaDel ?? (hasGitDiff ? `−${d.del}` : clientDiff ? `−${cDel}` : null);
|
|
8355
|
+
const chips = [];
|
|
8356
|
+
if (it.pr) chips.push(`<a class="mx mlink pr" href="${esc(it.pr)}" target="_blank" rel="noopener" title="Open pull request"><span class="mk">PR</span> <b>#${esc(prNum)}</b> <span class="mmut">Open</span>${arw}</a>`);
|
|
8357
|
+
if (it.tokenOpt && tokPct > 0) chips.push(`<button type="button" class="mx mtok" data-saact="tok:${i}"><span class="mk">Token</span> <b>${tokPct}%</b> <span class="mmut">saved</span> <i class="tokchevron">›</i></button>`);
|
|
8358
|
+
if (testsLabel) chips.push(`<span class="mx"><span class="mk">Tests</span> <b class="ok">${esc(testsLabel)}</b></span>`);
|
|
8359
|
+
// Files: hide-empty — only with changed files AND totals (a bare-count chip reads odd).
|
|
8360
|
+
if (nf && fAdd) chips.push(`<span class="mx"><span class="mk">Files</span> <b>${nf}</b> <span class="mmut">· <i class="a">${esc(fAdd)}</i> <i class="d">${esc(fDel)}</i></span></span>`);
|
|
8361
|
+
// Local: an open localhost URL (neutral, not green). A bare host:port isn't navigable in
|
|
8362
|
+
// the demo, so it renders as a non-navigating chip; a real URL opens in a new tab.
|
|
8363
|
+
if (it.localUrl) { const lu = String(it.localUrl); const isUrl = /^https?:/.test(lu);
|
|
8364
|
+
chips.push(`<a class="mx mlink"${isUrl ? ` href="${esc(lu)}" target="_blank" rel="noopener"` : ' style="cursor:pointer"'} title="Open the running app"><span class="mk">Local</span> <b>${esc(lu.replace(/^https?:\/\//, ''))}</b>${arw}</a>`); }
|
|
8365
|
+
else if (gtr?.ran) chips.push(`<button type="button" class="mx mlink" data-saact="runlocal:${i}" title="Run this locally"><span class="mk">Local</span> <b>run</b>${arw}</button>`);
|
|
8366
|
+
// Verify = a screen recording of the check (spec §5): NEUTRAL. Opens the file when there
|
|
8367
|
+
// is a real URL, otherwise scrolls to the in-card player.
|
|
8368
|
+
if (it.recording) chips.push(it.recordingUrl
|
|
8369
|
+
? `<a class="mx mlink" href="${esc(it.recordingUrl)}" target="_blank" rel="noopener" title="Watch the recording"><span class="mk">Verify</span> <b>recording</b>${arw}</a>`
|
|
8370
|
+
: `<a class="mx mlink" data-saact="rec:${i}" style="cursor:pointer" title="Watch the recording"><span class="mk">Verify</span> <b>recording</b>${arw}</a>`);
|
|
8371
|
+
const tokCmp = it.tokenOpt?.basis === 'baseline' ? 'vs your recent average' : 'from prompt caching';
|
|
8372
|
+
const tokTech = (it.tokenOpt?.techniques ?? []).slice(0, 4).join(' / ');
|
|
8373
|
+
const metaDetail = (it.tokenOpt && tokPct > 0) ? `<div class="rmeta-detail" id="tokdetail${i}"><span class="k">How this run used tokens</span><span class="v">${esc(tokCmp)}${tokTech ? ` · ${esc(tokTech)}` : ''}</span></div>` : '';
|
|
8374
|
+
const runNote = gtr?.ran ? `<div class="rmeta-detail" id="runlocal${i}"><span class="v">No local run command is wired for this task yet.</span></div>` : '';
|
|
8375
|
+
const meta = `<div class="rmeta">${chips.join('')}</div>${metaDetail}${runNote}`;
|
|
8376
|
+
// ── Request (spec §4): a PLAIN section — the user's own short words (goal.text). Never
|
|
8377
|
+
// an accordion, never the implementation spec (that lives in Activity below). ──
|
|
8378
|
+
const reqSec = it.goalText
|
|
8379
|
+
? `<section class="rsec rreq"><div class="rsec-h">Request</div><p class="rbody rask">${esc(it.goalText)}</p></section>`
|
|
8380
|
+
: '';
|
|
8381
|
+
// ── What to check (spec §2.5): a bulleted list when several, a line when one. ──
|
|
8382
|
+
// Only an explicit review-summary check renders here — never the goal text echoed back
|
|
8383
|
+
// (it.check falls back to goal.text via checkLine, which would fabricate a "What to
|
|
8384
|
+
// check" for a conversational task that has none). §0.5 no placeholder.
|
|
8385
|
+
const checkArr = Array.isArray(rs.checkList) ? rs.checkList.filter(Boolean)
|
|
8386
|
+
: Array.isArray(rs.check) ? rs.check.filter(Boolean)
|
|
8387
|
+
: (typeof rs.check === 'string' && rs.check.trim() ? [rs.check.trim()] : []);
|
|
8388
|
+
const checkBody = checkArr.length > 1
|
|
8389
|
+
? `<ul class="rcheck">${checkArr.map((c) => `<li>${esc(c)}</li>`).join('')}</ul>`
|
|
8390
|
+
: (checkArr.length ? `<p class="rbody rcheck1">${esc(checkArr[0])}</p>` : '');
|
|
8391
|
+
// ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
|
|
8392
|
+
// run-log for logic-only tests; dropped entirely for a conversational task. ──
|
|
8393
|
+
let proofInner = '';
|
|
8394
|
+
if (!isConv) {
|
|
8395
|
+
if (hasProofImg) proofInner = `<div class="rproof">${img}</div>`;
|
|
8396
|
+
else if (kind === 'logic' && gtr?.ran) proofInner = `<div class="rrun"><div class="rr"><span class="lab">Tests</span><span class="v">${esc(testsLabel)}</span></div>${filePaths.length ? `<div class="rr"><span class="lab">Files</span><span class="v">${esc(filePaths.join(', '))}</span></div>` : ''}</div>`;
|
|
8397
|
+
if (it.recording) proofInner += saVideoFigHtml(it.recordingUrl);
|
|
8398
|
+
}
|
|
8399
|
+
// ── What changed (spec §4/§7): description + files summary (always) + the diff, which
|
|
8400
|
+
// peeks then "…+N 行 · GitHub で開く ↗" for a huge change (no GitHub link when there's no
|
|
8401
|
+
// PR). Structured recorded rows (ghRows) win over a git/client unified diff. Dropped
|
|
8402
|
+
// entirely for a conversational task. ──
|
|
8403
|
+
const changedDescHtml = (it.changedDesc || rs.changed) ? `<p class="rbody rdesc">${esc(String(it.changedDesc || rs.changed).slice(0, 600))}</p>` : '';
|
|
8404
|
+
const filesListHtml = it.metaFiles
|
|
8405
|
+
? `<div class="rfiles">${it.metaFiles.map((f) => esc(f)).join('<br>')}</div>`
|
|
8406
|
+
: (d && d.files.length) ? `<div class="rfiles">${d.files.map((f) => `${esc(f.path)} <i class="a">+${f.add}</i> <i class="d">−${f.del}</i>`).join('<br>')}</div>`
|
|
8407
|
+
: (clientDiff && filePaths.length) ? `<div class="rfiles">${filePaths.map((p) => esc(p)).join('<br>')}</div>` : '';
|
|
8408
|
+
const diffHtml = it.ghRows
|
|
8409
|
+
? saGhDiffRowsHtml(it.ghRows, it.diffTotal || 0, it.pr || '')
|
|
8410
|
+
: hasGitDiff ? saGhDiffHtml(d.diff) : clientDiff ? saGhDiffHtml(clientDiff) : '';
|
|
8411
|
+
const changedBody = isConv ? '' : (changedDescHtml + filesListHtml + diffHtml);
|
|
8412
|
+
// ── sensitive-area note (real: classifyChangeRisk forced the pause) ──
|
|
8083
8413
|
const risk = it.risk;
|
|
8084
8414
|
const riskHtml = risk?.shouldPause
|
|
8085
8415
|
? `<div class="lg-risk"><svg viewBox="0 0 24 24"><path d="M12 8v5M12 16.5v.5"/><path d="M10.3 3.9 2.4 18a1.9 1.9 0 0 0 1.7 2.9h15.8a1.9 1.9 0 0 0 1.7-2.9L13.7 3.9a1.9 1.9 0 0 0-3.4 0z"/></svg>Needs your review — touches a sensitive area${risk.categories?.length ? `: ${esc(risk.categories.map((c) => c.label).join(', '))}` : ''}</div>`
|
|
8086
8416
|
: '';
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8417
|
+
// ── Activity (spec §3): ONE Linear-docs pulldown. The "Activity" heading is plain; the
|
|
8418
|
+
// chevron sits on the FIRST line (the opening "You …" row); tapping it opens the rest
|
|
8419
|
+
// below, offset-indented. The review Discussion folds in at the bottom of the body. The
|
|
8420
|
+
// transcript is the recorded build (activity); real goals fall back to "what we did".
|
|
8421
|
+
// Persisted open-state via data-detkey so SSE re-renders don't collapse it. ──
|
|
8422
|
+
const txArr = (it.activity && it.activity.length)
|
|
8423
|
+
? it.activity.map((e) => saTxRowHtml(e))
|
|
8424
|
+
: (it.did?.length ? it.did.map((s) => saTxRowHtml(['claude', s])) : []);
|
|
8425
|
+
const discArr = it.discussion || it.thread || null;
|
|
8426
|
+
const threadHtml = (discArr && discArr.length)
|
|
8427
|
+
? discArr.map((m) => `<div class="rmsg${m[0] === 'you' ? ' you' : ''}"><div class="who">${m[0] === 'you' ? 'You' : 'Manager'}</div><div class="msg">${esc(m[1])}</div></div>`).join('')
|
|
8428
|
+
: '';
|
|
8429
|
+
const actOpen = SA.detOpen.has(`${gk}:act`) ? ' open' : '';
|
|
8430
|
+
const txFirst = txArr[0] || '<div class="txr"><span class="w"></span><span class="m">Activity</span></div>';
|
|
8431
|
+
const txRest = txArr.slice(1).join('');
|
|
8432
|
+
const discInner = threadHtml ? `<div class="rsub">Discussion</div><div class="rthread">${threadHtml}</div>` : '';
|
|
8433
|
+
const activitySec = (txArr.length || threadHtml)
|
|
8434
|
+
? `<section class="rsec ract"><div class="rsec-h">Activity</div><details class="lacc" data-detkey="${gk}:act"${actOpen}><summary><i class="ltog">›</i><div class="rtx lfirst">${txFirst}</div></summary><div class="lbody">${txRest ? `<div class="rtx">${txRest}</div>` : ''}${discInner}</div></details></section>`
|
|
8435
|
+
: '';
|
|
8436
|
+
const S = (label, body) => body ? `<section class="rsec"><div class="rsec-h">${label}</div>${body}</section>` : '';
|
|
8437
|
+
return `<div class="sa-item lg redesign${it.goalId === SA.cur ? ' cur' : ''}" data-gid="${it.goalId}">
|
|
8438
|
+
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span><span class="rst"><span class="rdot"></span>In review</span></div>
|
|
8439
|
+
<h2 class="rtitle">${esc(it.title)}</h2>
|
|
8440
|
+
${meta}
|
|
8441
|
+
${reqSec}
|
|
8442
|
+
${S('What to check', checkBody)}
|
|
8443
|
+
${S('Answer', it.answer ? `<p class="rbody">${esc(it.answer)}</p>` : '')}
|
|
8444
|
+
${S('Proof', proofInner)}
|
|
8445
|
+
${S('What changed', changedBody)}
|
|
8446
|
+
${S('Not done', it.risk?.summary ? `<p class="rbody rrisk">${esc(it.risk.summary)}</p>` : '')}
|
|
8092
8447
|
${riskHtml}
|
|
8093
|
-
|
|
8448
|
+
${activitySec}
|
|
8449
|
+
${S('Workflow', saWorkflowHtml(it))}
|
|
8450
|
+
<div class="sa-act"><button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button><button class="sa-dis" data-saact="dis:${i}">Dismiss</button><div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" placeholder="Reply, ask, or send back to the AI…"><button class="sa-send" data-saact="send:${i}" title="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div><button class="sa-ic" title="Archive" data-saact="arch:${i}">${SA_SVG.arch}</button><button class="sa-ic" title="Revert" data-saact="rev:${i}">${SA_SVG.trash}</button></div>
|
|
8094
8451
|
</div>`;
|
|
8095
8452
|
}
|
|
8453
|
+
// Scroll the See-all body so a card's top edge (.rhead) sits at the top of the visible
|
|
8454
|
+
// scroll area — the reviewer always starts reading a card from its header, never partway
|
|
8455
|
+
// down (Masa 2026-07-20). Uses a rect delta so it works regardless of offsetParent.
|
|
8456
|
+
function saScrollCardToTop(el){
|
|
8457
|
+
const body = document.querySelector('#seeall .sa-body');
|
|
8458
|
+
if (!body || !el) return;
|
|
8459
|
+
body.scrollTop += el.getBoundingClientRect().top - body.getBoundingClientRect().top;
|
|
8460
|
+
}
|
|
8096
8461
|
function saRender(){
|
|
8097
8462
|
const S = $('seeall'); if (!S) return;
|
|
8098
8463
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
@@ -8101,27 +8466,37 @@ function saRender(){
|
|
|
8101
8466
|
const c = `${pending.length} ready · ${green === pending.length ? 'all green' : `${pending.length - green} failing`} · ${proofN} with proof`;
|
|
8102
8467
|
const curIdx = pending.findIndex((x) => x.goalId === SA.cur);
|
|
8103
8468
|
const posLabel = pending.length && curIdx >= 0 ? `${curIdx + 1} / ${pending.length}` : '';
|
|
8104
|
-
S.innerHTML = `<div class="sa-panel"><div class="sa-hd"><span class="t">Review</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">↑↓ MOVE · A APPROVE · D
|
|
8469
|
+
S.innerHTML = `<div class="sa-panel"><div class="sa-hd"><span class="t">Review</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">↑↓ MOVE · A APPROVE · D DISMISS · ESC CLOSE</span><button class="sa-x" data-saact="close:0">✕</button></div>
|
|
8105
8470
|
<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>`;
|
|
8106
8471
|
// Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
|
|
8107
8472
|
// track what's open per goal+section in SA.detOpen (persists across re-renders)
|
|
8108
8473
|
// and wire each one's toggle to keep that set current.
|
|
8109
|
-
for (const det of S.querySelectorAll('
|
|
8474
|
+
for (const det of S.querySelectorAll('[data-detkey]')) {
|
|
8110
8475
|
det.addEventListener('toggle', () => {
|
|
8111
8476
|
const k = det.dataset.detkey;
|
|
8112
8477
|
if (det.open) SA.detOpen.add(k); else SA.detOpen.delete(k);
|
|
8113
8478
|
});
|
|
8114
8479
|
}
|
|
8115
|
-
//
|
|
8116
|
-
//
|
|
8117
|
-
//
|
|
8118
|
-
//
|
|
8119
|
-
|
|
8480
|
+
// "ないものは載せない" (Masa): a Proof <img> whose file is missing/broken must never
|
|
8481
|
+
// show a broken-image placeholder. proofImg is only a stored filename, so a file that
|
|
8482
|
+
// isn't on disk (or fails to load) can only be caught at load time — drop the whole
|
|
8483
|
+
// Proof section if any of its images fails. Real captures load and stay.
|
|
8484
|
+
for (const im of S.querySelectorAll('.redesign .rproof img')) {
|
|
8485
|
+
const drop = () => { const sec = im.closest('section.rsec'); if (sec) sec.remove(); };
|
|
8486
|
+
if (im.complete && im.naturalWidth === 0) drop();
|
|
8487
|
+
else im.addEventListener('error', drop, { once: true });
|
|
8488
|
+
}
|
|
8489
|
+
// H14: if a single-review focus is pending, bring the card's HEADER (#tag + In review
|
|
8490
|
+
// + title) to the very top of the scroll area — never mid-card (Masa 2026-07-20) — and
|
|
8491
|
+
// flash a blue ring. Re-scroll on EVERY render for the focus window — the post-diff
|
|
8492
|
+
// saSync rebuild resets .sa-body scroll to the top, so a one-shot scroll would land back
|
|
8493
|
+
// there. Self-clears after ~1.4s (outlasts the localhost diff fetch) so live SSE ticks
|
|
8494
|
+
// stop yanking the view.
|
|
8120
8495
|
if (SA.focusGid != null) {
|
|
8121
8496
|
requestAnimationFrame(() => {
|
|
8122
8497
|
const el = S.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
|
|
8123
8498
|
if (!el) return;
|
|
8124
|
-
el
|
|
8499
|
+
saScrollCardToTop(el);
|
|
8125
8500
|
el.classList.add('sa-flash');
|
|
8126
8501
|
if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
|
|
8127
8502
|
});
|
|
@@ -8192,6 +8567,14 @@ function saOpen(focusGoalId){
|
|
|
8192
8567
|
}
|
|
8193
8568
|
saRender();
|
|
8194
8569
|
$('seeall').classList.add('open');
|
|
8570
|
+
// Land the opening card's header at the very top of the scroll area (Masa 2026-07-20).
|
|
8571
|
+
// With focusGid set, saRender's focus block already does this; without it (a plain
|
|
8572
|
+
// "See all"), resolved one-liners can sit above the first pending card, so scrollTop 0
|
|
8573
|
+
// isn't enough — scroll the current card's header to the top explicitly.
|
|
8574
|
+
if (SA.focusGid == null && SA.cur != null) requestAnimationFrame(() => {
|
|
8575
|
+
const el = $('seeall').querySelector(`.sa-item[data-gid="${SA.cur}"]`);
|
|
8576
|
+
if (el) saScrollCardToTop(el);
|
|
8577
|
+
});
|
|
8195
8578
|
saFetchDiffs();
|
|
8196
8579
|
saEnhanceDiffBoxes();
|
|
8197
8580
|
}
|
|
@@ -8256,6 +8639,8 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
8256
8639
|
// Run-locally to reveal the honest "not wired yet" note — neither performs an action.
|
|
8257
8640
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
8258
8641
|
if (k === 'runlocal') { $('seeall').querySelector(`#runlocal${i}`)?.classList.toggle('open'); return; }
|
|
8642
|
+
// Verify chip (no external recording URL) → scroll the in-card video player into view.
|
|
8643
|
+
if (k === 'rec') { $('seeall').querySelector(`.sa-item[data-gid="${it.goalId}"] .rvid`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); return; }
|
|
8259
8644
|
// every verdict auto-advances the focus to the next pending card (tempo).
|
|
8260
8645
|
if (k === 'app') { const bx = b.getBoundingClientRect(); return saAct(it, 1, 'approve').then((ok) => {
|
|
8261
8646
|
if (!ok) return;
|
package/package.json
CHANGED