@galda/cli 0.10.38 → 0.10.40
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/CLAUDE.md +7 -3
- package/app/index.html +511 -342
- package/engine/lib.mjs +450 -37
- package/engine/server.mjs +274 -279
- package/engine/shot.mjs +96 -0
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -853,20 +853,15 @@
|
|
|
853
853
|
.reqdesc .mdsp{height:6px}
|
|
854
854
|
.reqdesc code,.gdtaskresult code{font-family:var(--mono);font-size:11px;background:rgba(var(--tint),.07);padding:1px 4px;border-radius:4px}
|
|
855
855
|
.reqdesc strong{color:var(--ink);font-weight:700}
|
|
856
|
+
/* an image the AI pasted into its own report: shown full-width at its own aspect,
|
|
857
|
+
hairline edge only (§5.5 縁は1px以下のヘアライン), never a fixed-ratio crop box */
|
|
858
|
+
.mdimg{display:block;max-width:100%;height:auto;margin:8px 0;border-radius:8px;border:1px solid var(--hair)}
|
|
856
859
|
.reqcond{font-family:var(--mono);font-size:11px;color:var(--ink3);margin-top:4px}
|
|
857
860
|
.reqvideo{margin:10px 0}
|
|
858
861
|
.reqnovideo{font-size:12px;color:var(--ink3);border:1px dashed var(--hair2);border-radius:10px;padding:16px;text-align:center;margin:10px 0}
|
|
859
862
|
.beforeafter{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
|
860
863
|
.ba-col{min-width:0}
|
|
861
864
|
.balabel{display:block;font-family:var(--mono);font-size:9px;letter-spacing:.06em;color:var(--ink3);margin-bottom:2px}
|
|
862
|
-
/* diff box + zoomed inset (HANDOFF-review-card-diffbox-2026-07-18) — only emitted
|
|
863
|
-
by beforeAfterBoxedHtml(), so these never touch the plain pair above or the
|
|
864
|
-
older per-task renderReviewChecklist before/after (out of scope, untouched). */
|
|
865
|
-
.ba-imgwrap{position:relative;margin-top:8px}
|
|
866
|
-
.ba-imgwrap .proofimg{width:100%;height:auto;max-height:none;object-fit:initial;margin-top:0;display:block}
|
|
867
|
-
.ba-box{position:absolute;border:1.5px solid var(--danger);border-radius:2px;pointer-events:none}
|
|
868
|
-
.ba-zoomhd{margin-top:14px}
|
|
869
|
-
.lg-ev .ba-zoomimg{display:block;width:100%;height:auto;max-height:none;object-fit:initial;margin-top:8px;border-radius:9px;border:1px solid var(--hair2)}
|
|
870
865
|
.reqbtns{display:flex;gap:8px;margin-top:4px}
|
|
871
866
|
.reqbtn{height:30px;padding:0 14px;border-radius:9px;border:1px solid var(--hair2);background:transparent;
|
|
872
867
|
color:var(--ink2);font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;transition:.15s}
|
|
@@ -1620,6 +1615,14 @@
|
|
|
1620
1615
|
.lbc.is-quiet .lbc-t{color:var(--ink2)} /* Done / Later / To Do: visible, not shouting */
|
|
1621
1616
|
.lbc-w{font:600 8.5px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;margin-left:auto}
|
|
1622
1617
|
.lbc-w.bad{color:var(--danger)} .lbc-w.intr{color:#ffca16}
|
|
1618
|
+
/* P0b: the AI's own To-do breakdown, scaled down to the card's density (§1.5-8 fold, not
|
|
1619
|
+
scroll — capped + "+N more" text rather than an inner scrollbar). Reuses the .todo/.box/
|
|
1620
|
+
.tdtext vocabulary from the chat feed (todosHtml) so the ✓/▸/○ language stays one system. */
|
|
1621
|
+
.lbc-todos{display:flex;flex-direction:column;gap:3px}
|
|
1622
|
+
.lbc-todos .todo{font-size:10.5px;line-height:1.4;gap:5px}
|
|
1623
|
+
.lbc-todos .todo .box{width:11px;height:11px;font-size:9px;line-height:11px;margin-top:1px}
|
|
1624
|
+
.lbc-todos.prep{font-size:10.5px;color:var(--ink3);font-style:italic}
|
|
1625
|
+
.lbc-more{font:500 9.5px/1 var(--mono);color:var(--ink3)}
|
|
1623
1626
|
.lbc-meter{display:flex;align-items:center;gap:7px}
|
|
1624
1627
|
.lbc-meter .bar{flex:1;height:2px;border-radius:2px;background:color-mix(in srgb,var(--ink) 11%,transparent);overflow:hidden}
|
|
1625
1628
|
.lbc-meter .bar i{display:block;height:100%;border-radius:2px;background:var(--green);transition:width .3s}
|
|
@@ -1712,33 +1715,12 @@
|
|
|
1712
1715
|
/* review row number — #NN PREFIX on the title (HANDOFF-v45 §11), same form as the To Do
|
|
1713
1716
|
rows (PRD §5: numbering matches the sidebar) */
|
|
1714
1717
|
#fsRoot .rcard .rid{font-size:10.5px;font-weight:600;color:var(--ink3);margin:0 5px 0 0;display:inline-block;font-variant-numeric:tabular-nums}
|
|
1715
|
-
/* PRD §6.5
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
#fsRoot .rcard .fnote{font-family:var(--mono);font-size:9.5px;color:var(--ink3);margin-top:3px;display:block}
|
|
1722
|
-
/* In rv3 the meta sits on the near-black photo scrim whatever the theme, where --ink3
|
|
1723
|
-
is effectively invisible (§5.5「見えるが騒がない」— near-invisible is a fail). Match
|
|
1724
|
-
.rid's white-on-scrim treatment. The folded card opts out of the scrim (rule above),
|
|
1725
|
-
so it keeps --ink3 on the panel where that tone is correct. */
|
|
1726
|
-
#fsRoot[data-rv="3"] .rcard:not(.fold) .fnote{color:rgba(255,255,255,.66);text-shadow:0 1px 3px rgba(0,0,0,.55)}
|
|
1727
|
-
/* A folded card is settled, not failed: title sinks to the settled tone, and it
|
|
1728
|
-
keeps no ✓/✕ because the thing to approve lives on the goal it was folded into. */
|
|
1729
|
-
#fsRoot .rcard.fold .t{color:var(--ink2)}
|
|
1730
|
-
/* A folded card is a pointer, not a review item: no proof to show, nothing to approve.
|
|
1731
|
-
So it opts OUT of the rv3 thumbnail treatment (whose .rc is absolutely positioned
|
|
1732
|
-
over an 84px photo ground) and stays the same quiet line in every view — the form
|
|
1733
|
-
Masa drew. Giving it a fake dark thumbnail would claim it has something to look at. */
|
|
1734
|
-
/* .fold.noimg, not .fold: the placeholder-ground rule below is `.rcard.noimg`, which
|
|
1735
|
-
ties this on specificity and would win on order — a folded card would keep the fake
|
|
1736
|
-
dark thumbnail ground. Matching both classes settles it regardless of order. */
|
|
1737
|
-
#fsRoot[data-rv="3"] .rcard.fold.noimg{display:flex;position:static;align-items:center;gap:10px;
|
|
1738
|
-
padding:9px 10px;min-height:0;border:1px solid var(--hair);border-radius:11px;background:none;box-shadow:none}
|
|
1739
|
-
#fsRoot[data-rv="3"] .rcard.fold.noimg .rc{position:static;padding:0;background:none}
|
|
1740
|
-
#fsRoot[data-rv="3"] .rcard.fold.noimg .t{color:var(--ink2);font-size:12.5px;text-shadow:none}
|
|
1741
|
-
#fsRoot[data-rv="3"] .rcard.fold.noimg .rid{color:var(--ink3)}
|
|
1718
|
+
/* PRD §6.5 → Masa 2026-07-21: the fold note ("also carries #13" on the carrier card,
|
|
1719
|
+
"merged into #12" on the folded card) no longer renders in the Review list at all —
|
|
1720
|
+
it read as the same report-in-the-list clutter the feature was meant to avoid. Where
|
|
1721
|
+
folded work went is now logged in the opened detail's "Also carries" section only, so
|
|
1722
|
+
the .fnote / .rcard.fold* rules that used to style these list-level notes are gone
|
|
1723
|
+
with them (dead CSS orphaned the moment nothing emits those classes any more). */
|
|
1742
1724
|
/* reference numbers everywhere else (HANDOFF-v45 §11): a leading #NN prefix on every
|
|
1743
1725
|
To Do item's name — same goal = same number (goalReviewNumber), distinct from the
|
|
1744
1726
|
PR's own #id shown separately. */
|
|
@@ -2010,6 +1992,17 @@
|
|
|
2010
1992
|
.redesign .rmeta-detail.open{display:block}
|
|
2011
1993
|
.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
1994
|
.redesign .rmeta-detail .v{font:500 11.5px/1.6 var(--ui);color:var(--ink2)}
|
|
1995
|
+
/* Open it section: the address is the control. Mono so it reads as an address, plain
|
|
1996
|
+
text weight so it doesn't shout, and hover moves the GLYPH colour only — never a
|
|
1997
|
+
filled surface (§5.5 アイコンボタンのhoverは面を塗らない). */
|
|
1998
|
+
.redesign .ropen .olink{display:inline-flex;align-items:baseline;gap:6px;padding:0;border:0;background:none;cursor:pointer;
|
|
1999
|
+
font:400 13.5px/1.62 var(--mono);color:var(--ink);letter-spacing:-.004em;text-align:left;overflow-wrap:anywhere;transition:color .14s}
|
|
2000
|
+
.redesign .ropen .olink:hover{color:var(--green)}
|
|
2001
|
+
.redesign .ropen .olink[disabled]{color:var(--ink3);cursor:default}
|
|
2002
|
+
.redesign .ropen .olink .ar{font-size:.85em;color:var(--ink3)}
|
|
2003
|
+
/* the note is body prose like every other section's — same 14px / −.012em / --ink as
|
|
2004
|
+
.rbody (Masa 2026-07-22: it was a shade lighter and read as a different kind of text) */
|
|
2005
|
+
.redesign .ropen .v{margin:4px 0 0 !important}
|
|
2013
2006
|
/* sections — Linear-docs bold sentence-case headings + high-contrast body.
|
|
2014
2007
|
margins are !important: the app's pre-existing .lg / .sa-item rules load first and
|
|
2015
2008
|
otherwise tie/beat these on specificity, flattening the Linear-docs rhythm. */
|
|
@@ -2019,6 +2012,36 @@
|
|
|
2019
2012
|
13px read thinner/lighter for Japanese than the surrounding app). §2. */
|
|
2020
2013
|
.redesign .rbody{font-size:14px;line-height:1.62;letter-spacing:-.012em;color:var(--ink);max-width:70ch;margin:0 !important}
|
|
2021
2014
|
.redesign .rdesc,.redesign .rrisk{color:var(--ink2)}
|
|
2015
|
+
/* Result (P1, Masa 2026-07-21): the AI's own report, rendered as written. Same 14px
|
|
2016
|
+
body rhythm as .rbody — it is prose the reviewer reads, not a log. Headings/lists/
|
|
2017
|
+
code inside it are the report's own structure, so they get the section's type scale
|
|
2018
|
+
rather than the smaller .reqdesc one used in the older checklist dialog. */
|
|
2019
|
+
.redesign .rreport{font-size:14px;line-height:1.62;letter-spacing:-.012em;color:var(--ink);max-width:70ch;overflow-wrap:anywhere}
|
|
2020
|
+
.redesign .rreport .mdh{font-weight:680;color:var(--ink);margin:14px 0 4px}
|
|
2021
|
+
.redesign .rreport .mdh:first-child{margin-top:0}
|
|
2022
|
+
.redesign .rreport .mdul{margin:2px 0;padding-left:20px}
|
|
2023
|
+
.redesign .rreport .mdul li{margin:2px 0}
|
|
2024
|
+
.redesign .rreport .mdsp{height:8px}
|
|
2025
|
+
.redesign .rreport code{font-family:var(--mono);font-size:12px;background:rgba(var(--tint),.07);padding:1px 4px;border-radius:4px}
|
|
2026
|
+
.redesign .rreport strong{color:var(--ink);font-weight:700}
|
|
2027
|
+
.redesign .rreport a{color:var(--ink);text-decoration:underline;text-underline-offset:2px}
|
|
2028
|
+
/* "Open it" is waiting on a real process to boot (up to ~15s), so the chip says so
|
|
2029
|
+
rather than looking dead. Dim + no pointer, no spinner — §5.5 薄く・主張しない. */
|
|
2030
|
+
.redesign .mx.mwait{opacity:.55;pointer-events:none}
|
|
2031
|
+
/* the AI's own screenshots: full width, own aspect, hairline edge (§5.5 縁は1px以下).
|
|
2032
|
+
No grid, no crop — one shot is often a comparison sheet and dies when shrunk. */
|
|
2033
|
+
.redesign .rshot{margin:0 0 16px}
|
|
2034
|
+
.redesign .rshot:last-child{margin-bottom:0}
|
|
2035
|
+
.redesign .rshot img{display:block;width:100%;height:auto;border-radius:10px;border:1px solid var(--hair)}
|
|
2036
|
+
.redesign .rshot figcaption{margin-top:6px;font-family:var(--mono);font-size:11px;color:var(--ink3)}
|
|
2037
|
+
/* before/after pair + the red box over what actually changed (pixel-diffed here from
|
|
2038
|
+
the worker's own two shots — no box appears unless the pixels really differ) */
|
|
2039
|
+
.redesign .beforeafter .proofimg{width:100%;height:auto;max-height:none;object-fit:initial;display:block;border-radius:8px;border:1px solid var(--hair)}
|
|
2040
|
+
.redesign .ba-imgwrap{position:relative}
|
|
2041
|
+
.redesign .ba-imgwrap .proofimg{margin-top:0}
|
|
2042
|
+
.redesign .ba-box{position:absolute;border:1.5px solid var(--danger);border-radius:2px;pointer-events:none}
|
|
2043
|
+
.redesign .ba-zoomhd{margin:14px 0 8px;font-family:var(--mono);font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--ink3)}
|
|
2044
|
+
.redesign .ba-zoomimg{display:block;width:100%;height:auto;border-radius:8px;border:1px solid var(--hair)}
|
|
2022
2045
|
.redesign .rfiles{margin:12px 0 0;font-family:var(--mono);font-size:12px;color:var(--ink2);line-height:1.75}
|
|
2023
2046
|
.redesign .rfiles .a{color:#3fd07a;font-style:normal}.redesign .rfiles .d{color:#ff7b72;font-style:normal}
|
|
2024
2047
|
/* Request accordion (spec §3) — chevron › rotate 90, body indented, no left rule */
|
|
@@ -2091,22 +2114,25 @@
|
|
|
2091
2114
|
in at the bottom of the opened body. */
|
|
2092
2115
|
.redesign .ract{margin-top:24px !important}
|
|
2093
2116
|
.redesign .ract>.rsec-h{margin-bottom:10px !important}
|
|
2094
|
-
.redesign .lacc>summary{list-style:none;cursor:pointer;display:flex;gap:
|
|
2117
|
+
.redesign .lacc>summary{list-style:none;cursor:pointer;display:flex;gap:14px;align-items:baseline}
|
|
2095
2118
|
.redesign .lacc>summary::-webkit-details-marker{display:none}
|
|
2096
|
-
.redesign .lacc .ltog{flex:0 0 auto;color:var(--
|
|
2119
|
+
.redesign .lacc .ltog{flex:0 0 auto;color:var(--ink2);font-style:normal;font-size:1.15em;line-height:1.25;transition:transform .14s}
|
|
2097
2120
|
.redesign .lacc[open]>summary .ltog{transform:rotate(90deg)}
|
|
2098
2121
|
.redesign .lacc .lfirst{flex:1;min-width:0}
|
|
2099
2122
|
.redesign .lacc .lbody{margin-top:12px;padding-left:18px}
|
|
2100
2123
|
/* transcript rows — Linear-clean, borderless (You / tool mono / res green / Claude) */
|
|
2101
2124
|
.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)
|
|
2125
|
+
.redesign .txr{display:flex;gap:12px;padding:5px 0;font-size:13px;line-height:1.55;align-items:baseline}
|
|
2126
|
+
.redesign .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3)}
|
|
2104
2127
|
.redesign .txr.you .w,.redesign .txr.cl .w{color:var(--ink);font-weight:600}
|
|
2105
2128
|
.redesign .txr .m{flex:1;min-width:0;color:var(--ink2)}
|
|
2106
2129
|
.redesign .txr.cl .m{color:var(--ink)}
|
|
2107
2130
|
.redesign .txr.tool .m{color:var(--ink2);font-family:var(--mono);font-size:12px}
|
|
2108
2131
|
.redesign .txr.tool .m b{color:var(--ink);font-weight:600}
|
|
2109
2132
|
.redesign .txr.res .m{font-family:var(--mono);font-size:12px;color:var(--green)}
|
|
2133
|
+
/* Manager's own progress notes inside the implementation log: commentary, not results.
|
|
2134
|
+
Green would read as "passed" (§5.5 UIクロームは薄く・優しく・主張しない). */
|
|
2135
|
+
.redesign .txr.note .m{font-family:var(--mono);font-size:12px;color:var(--ink3)}
|
|
2110
2136
|
/* Discussion (review back-and-forth) folded at the bottom of Activity's body */
|
|
2111
2137
|
.redesign .lbody .rthread{display:flex;flex-direction:column;gap:16px;margin:0}
|
|
2112
2138
|
.redesign .lbody .rmsg .who{font-size:12px;font-weight:600;color:var(--ink);margin-bottom:4px}
|
|
@@ -3361,16 +3387,6 @@ function reviewCardConfigFor(projectId){ return { ...DEFAULT_REVIEW_DEFINITION.r
|
|
|
3361
3387
|
// goal's own request language, same as the planner/worker; ja/en pin the
|
|
3362
3388
|
// project to one language regardless of what a given goal was typed in.
|
|
3363
3389
|
function reviewLangFor(projectId){ const l = reviewDefinitionFor(projectId).language; return (l === 'en' || l === 'ja') ? l : 'auto'; }
|
|
3364
|
-
// PRD §6.5 fold note language (Masa 2026-07-17: "ユーザーの言語に合わせて" — not
|
|
3365
|
-
// hardcoded English). Mirrors the server's own 'auto' resolution exactly
|
|
3366
|
-
// (reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : ...,
|
|
3367
|
-
// engine/server.mjs) so this note reads in the same language as the review
|
|
3368
|
-
// headline/PR body for the same goal: the project's fixed override wins if set,
|
|
3369
|
-
// otherwise match the language the goal's own request was written in.
|
|
3370
|
-
function foldLangFor(goal){
|
|
3371
|
-
const l = reviewLangFor(state.active);
|
|
3372
|
-
return (l === 'ja' || l === 'en') ? l : (/[-ヿ㐀-鿿]/.test(goal?.text || '') ? 'ja' : 'en');
|
|
3373
|
-
}
|
|
3374
3390
|
|
|
3375
3391
|
function esc(s){ const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
|
3376
3392
|
|
|
@@ -3522,30 +3538,28 @@ function saReviewTitle(g){
|
|
|
3522
3538
|
// reviewSummary.title, so they fall back to the request text unchanged (no regression).
|
|
3523
3539
|
return String(g?.reviewSummary?.title || g?.text || g?.plan?.[0] || g?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim().slice(0, 90);
|
|
3524
3540
|
}
|
|
3525
|
-
// H23 (2026-07-18) evidence-kind detection.
|
|
3526
|
-
//
|
|
3527
|
-
//
|
|
3528
|
-
//
|
|
3529
|
-
//
|
|
3530
|
-
//
|
|
3531
|
-
//
|
|
3532
|
-
//
|
|
3533
|
-
|
|
3534
|
-
|
|
3541
|
+
// H23 (2026-07-18) evidence-kind detection. flagship's demo data carries an explicit
|
|
3542
|
+
// `kind` field; real goals/tasks have no such field, so the review card derives one
|
|
3543
|
+
// from real signals it already has (a captured proof image, whether the changed files
|
|
3544
|
+
// are all docs-only).
|
|
3545
|
+
//
|
|
3546
|
+
// P1 (Masa 2026-07-21): the 'missing' kind is gone, and with it the app-local copy of
|
|
3547
|
+
// isUiChange(). It said "this touched the UI, so a screenshot SHOULD exist" — a rule
|
|
3548
|
+
// from the era when we photographed the work ourselves. That capture never ran outside
|
|
3549
|
+
// this one repo, so the only thing the rule ever produced was an apology on every UI
|
|
3550
|
+
// goal. We don't take the photo any more, so there is nothing to apologise for.
|
|
3535
3551
|
const DOC_ONLY_RE_LOCAL = /(^|\/)(README|START-HERE|CHANGELOG|LICENSE)(\.[\w.-]+)?$|(^|\/)docs\/.*\.(md|mdx|txt)$|\.mdx?$|\.txt$/i;
|
|
3536
3552
|
function hasTestRelevantChangesLocal(files){
|
|
3537
3553
|
const fs = (files ?? []).map((f) => String(f ?? '').trim()).filter(Boolean);
|
|
3538
3554
|
if (!fs.length) return true;
|
|
3539
3555
|
return fs.some((f) => !DOC_ONLY_RE_LOCAL.test(f));
|
|
3540
3556
|
}
|
|
3541
|
-
// Priority: image (a real capture exists) >
|
|
3542
|
-
// capture per PRD §4(C) but doesn't) > docs (every changed file is docs-only) >
|
|
3557
|
+
// Priority: image (a real capture exists) > docs (every changed file is docs-only) >
|
|
3543
3558
|
// logic (verified only by tests, no UI). Falls back to 'image' (today's generic
|
|
3544
3559
|
// fact-row + no image) when no signal is conclusive — never claims a stronger kind
|
|
3545
3560
|
// than the data actually supports.
|
|
3546
3561
|
function saEvidenceKind(filePaths, hasProofImg, testResult){
|
|
3547
3562
|
if (hasProofImg) return 'image';
|
|
3548
|
-
if (isUiChangeLocal(filePaths)) return 'missing';
|
|
3549
3563
|
if (filePaths.length && !hasTestRelevantChangesLocal(filePaths)) return 'docs';
|
|
3550
3564
|
if (testResult?.ran) return 'logic';
|
|
3551
3565
|
return 'image';
|
|
@@ -3596,11 +3610,28 @@ function requirementSummaryLine(task){
|
|
|
3596
3610
|
// Worker reports are written in Markdown (## headings, **bold**, - lists, `code`).
|
|
3597
3611
|
// The review cards showed them raw, so "## " and "**" leaked as literal text.
|
|
3598
3612
|
// Render the common subset to HTML (escaping first) so they read cleanly.
|
|
3599
|
-
|
|
3613
|
+
// P1 (Masa 2026-07-21): the AI's report IS the deliverable, so an image it pasted
|
|
3614
|
+
// (``) has to render as that image — this is the whole "paste it and it
|
|
3615
|
+
// shows up" wiring. Only src values we can vouch for are allowed through: http(s),
|
|
3616
|
+
// a same-origin path, or an inline data:image. Anything else (javascript:, data:text
|
|
3617
|
+
// /html, …) renders as the literal text it was, never as an <img src>.
|
|
3618
|
+
const MD_IMG_SRC_OK = /^(?:https?:\/\/|\/(?!\/)|data:image\/(?:png|jpe?g|gif|webp|svg\+xml);base64,)/i;
|
|
3619
|
+
// `images: false` renders a pasted image as nothing at all. Masa 2026-07-22, after
|
|
3620
|
+
// seeing one in a real card: the review card is not a gallery — the way to see the work
|
|
3621
|
+
// is Open it, and a lone blurry still of "one of the three" is worse than no still. The
|
|
3622
|
+
// renderer keeps the capability (chat and the older checklist still use images); the
|
|
3623
|
+
// review card is simply not the place.
|
|
3624
|
+
function mdLite(src, { images = true } = {}){
|
|
3600
3625
|
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
3601
3626
|
const inline = (s) => esc(s)
|
|
3602
3627
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
3603
3628
|
.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>')
|
|
3629
|
+
// images before links —  would otherwise match the link rule below
|
|
3630
|
+
// and render as a bare "!" plus an <a>.
|
|
3631
|
+
.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, url) => (!images ? ''
|
|
3632
|
+
: MD_IMG_SRC_OK.test(url)
|
|
3633
|
+
? `<img class="mdimg" src="${url.replace(/"/g, '"')}" alt="${alt}" loading="lazy">`
|
|
3634
|
+
: m))
|
|
3604
3635
|
// Feedback: make links tappable — markdown [text](url) and bare URLs (e.g. a
|
|
3605
3636
|
// "open the ReadMe/PROOF.md" link in the worker report).
|
|
3606
3637
|
.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
|
@@ -3740,30 +3771,22 @@ function proofMediaHtml(taskId, proof, altLabel){
|
|
|
3740
3771
|
}
|
|
3741
3772
|
return imgTag;
|
|
3742
3773
|
}
|
|
3743
|
-
//
|
|
3744
|
-
//
|
|
3745
|
-
//
|
|
3746
|
-
//
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
//
|
|
3758
|
-
|
|
3759
|
-
// a red box over it on both images, plus a cropped/enlarged row below. Computed
|
|
3760
|
-
// client-side from the two PNGs via <canvas> pixel comparison at render time — not
|
|
3761
|
-
// baked in at capture time — so it works retroactively on every proof pair already
|
|
3762
|
-
// on disk with no new server-side image dependency (see the handoff for the full
|
|
3763
|
-
// rationale). Reference algorithm, verified against a real before/after pair
|
|
3764
|
-
// (composer icon swap, 640x200): correctly isolated a 57x52px box centered on the
|
|
3765
|
-
// changed icon — 0.13% image coverage, no false positives elsewhere in the frame.
|
|
3766
|
-
function computeDiffBox(beforeImg, afterImg, { threshold = 24, minCoverage = 0.0005, maxCoverage = 0.7, pad = 0.03 } = {}) {
|
|
3774
|
+
// (P1, Masa 2026-07-21) beforeAfterMediaHtml() was deleted here. The pair was staged by
|
|
3775
|
+
// us, not by the work: the "before" shot came from the same photo-staging pass that P1
|
|
3776
|
+
// removed on the engine side (#228), and in practice it never existed at all, so every
|
|
3777
|
+
// card fell through to the single shot anyway. Callers now ask for that shot directly.
|
|
3778
|
+
// Red box (restored 2026-07-22 on Masa's ask). This machinery was deleted in #230 with
|
|
3779
|
+
// the staged before/after pair, because the 'before' was a photo WE took of a screen
|
|
3780
|
+
// that had nothing to do with the change. It comes back on a different footing: the two
|
|
3781
|
+
// images are both the worker's own shots, taken by the agent that did the work, and the
|
|
3782
|
+
// box is computed here from those two PNGs by pixel comparison — nothing is invented, a
|
|
3783
|
+
// box only appears when the pixels really differ in one region.
|
|
3784
|
+
// minCoverage lowered 2026-07-22: the old 0.05% floor was tuned on a 640x200 proof pair.
|
|
3785
|
+
// A real worker shot is a whole 900-1440px page, where a genuine change (a dot going
|
|
3786
|
+
// green, a label swapping) is well under that floor and was being thrown away as noise.
|
|
3787
|
+
// 30 pixels differing by more than `threshold` is a change someone made on purpose;
|
|
3788
|
+
// compression noise does not clear a 24-point RGB delta.
|
|
3789
|
+
function computeDiffBox(beforeImg, afterImg, { threshold = 24, minPixels = 30, maxCoverage = 0.7, pad = 0.03 } = {}) {
|
|
3767
3790
|
const w = beforeImg.naturalWidth, h = beforeImg.naturalHeight;
|
|
3768
3791
|
if (w !== afterImg.naturalWidth || h !== afterImg.naturalHeight) return null; // different-shaped images: don't guess
|
|
3769
3792
|
const c1 = document.createElement('canvas'); c1.width = w; c1.height = h;
|
|
@@ -3785,7 +3808,7 @@ function computeDiffBox(beforeImg, afterImg, { threshold = 24, minCoverage = 0.0
|
|
|
3785
3808
|
}
|
|
3786
3809
|
}
|
|
3787
3810
|
const total = w * h;
|
|
3788
|
-
if (maxX < 0 || diffPx
|
|
3811
|
+
if (maxX < 0 || diffPx < minPixels) return null; // no real difference — don't box a no-op
|
|
3789
3812
|
if (diffPx / total > maxCoverage) return null; // diff too diffuse to be "a spot" — a full redesign shouldn't get a red box either
|
|
3790
3813
|
const padPx = Math.round(Math.max(w, h) * pad);
|
|
3791
3814
|
minX = Math.max(0, minX - padPx); minY = Math.max(0, minY - padPx);
|
|
@@ -3811,34 +3834,45 @@ function baZoomDataUrl(img, box, maxW = 420, maxH = 200){
|
|
|
3811
3834
|
c.getContext('2d').drawImage(img, box.x, box.y, box.w, box.h, 0, 0, outW, outH);
|
|
3812
3835
|
return c.toDataURL('image/png');
|
|
3813
3836
|
}
|
|
3814
|
-
//
|
|
3815
|
-
//
|
|
3816
|
-
//
|
|
3817
|
-
//
|
|
3818
|
-
//
|
|
3819
|
-
// the
|
|
3820
|
-
//
|
|
3821
|
-
//
|
|
3822
|
-
//
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
const
|
|
3837
|
+
// A before/after pair exists only when the AI SAYS two of its shots are the same view
|
|
3838
|
+
// before and after (task.run.compare = [beforeName, afterName]). Masa 2026-07-22: an
|
|
3839
|
+
// earlier version of this guessed the pair from file names containing "before"/"after",
|
|
3840
|
+
// which is this layer inventing a relationship it cannot know — the same mistake as the
|
|
3841
|
+
// intake AI writing the pass condition. A request for three options has no "after"; only
|
|
3842
|
+
// the agent that did the work knows which images relate, so only it can say so.
|
|
3843
|
+
//
|
|
3844
|
+
// Nothing declares `compare` yet (the engine keeps cmd/url/note/check), so no card draws
|
|
3845
|
+
// a box today. The machinery below stays wired: the day the worker can declare a pair,
|
|
3846
|
+
// it lights up with no further app change.
|
|
3847
|
+
function saShotPair(shots, declared){
|
|
3848
|
+
const list = shots ?? [];
|
|
3849
|
+
const pair = Array.isArray(declared) ? declared : null;
|
|
3850
|
+
if (!pair || pair.length !== 2) return null;
|
|
3851
|
+
const find = (n) => list.find((s) => s.name === n || String(s.url || '').endsWith(String(n)));
|
|
3852
|
+
const before = find(pair[0]), after = find(pair[1]);
|
|
3853
|
+
return before && after && before !== after ? { before, after } : null;
|
|
3854
|
+
}
|
|
3855
|
+
function saPlainPairHtml(pair){
|
|
3856
|
+
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>`;
|
|
3857
|
+
return `<div class="beforeafter">${col(pair.before, 'Before')}${col(pair.after, 'After')}</div>`;
|
|
3858
|
+
}
|
|
3859
|
+
// Renders the pair with the changed region boxed on both, plus a cropped enlargement.
|
|
3860
|
+
// Returns null on every fallback (not two images, load failure, no clean box, different
|
|
3861
|
+
// sizes) so the caller keeps showing the plain shots — never a guessed box.
|
|
3862
|
+
async function shotsBoxedHtml(before, after){
|
|
3863
|
+
if (!before?.url || !after?.url) return null;
|
|
3864
|
+
let bImg, aImg;
|
|
3865
|
+
try { [bImg, aImg] = await Promise.all([baLoadImage(before.url), baLoadImage(after.url)]); }
|
|
3866
|
+
catch { return null; }
|
|
3867
|
+
const box = computeDiffBox(bImg, aImg);
|
|
3834
3868
|
if (!box) return null;
|
|
3835
|
-
const w =
|
|
3836
|
-
const
|
|
3837
|
-
const frame = (img, label) => `<div class="ba-col"><span class="balabel">${label}</span><div class="ba-imgwrap"><img class="proofimg" src="${img.src}" alt="${label.toLowerCase()}
|
|
3838
|
-
const
|
|
3839
|
-
return `<div class="beforeafter">${frame(
|
|
3840
|
-
|
|
3841
|
-
|
|
3869
|
+
const w = bImg.naturalWidth, h = bImg.naturalHeight;
|
|
3870
|
+
const st = `left:${(box.x / w * 100).toFixed(3)}%;top:${(box.y / h * 100).toFixed(3)}%;width:${(box.w / w * 100).toFixed(3)}%;height:${(box.h / h * 100).toFixed(3)}%`;
|
|
3871
|
+
const frame = (img, label) => `<div class="ba-col"><span class="balabel">${label}</span><div class="ba-imgwrap"><img class="proofimg" src="${img.src}" alt="${label.toLowerCase()}" loading="lazy"><div class="ba-box" style="${st}"></div></div></div>`;
|
|
3872
|
+
const zoom = (dataUrl, label) => `<div class="ba-col"><img class="ba-zoomimg" src="${dataUrl}" alt="zoomed ${label}"></div>`;
|
|
3873
|
+
return `<div class="beforeafter">${frame(bImg, 'Before')}${frame(aImg, 'After')}</div>`
|
|
3874
|
+
+ `<div class="ba-zoomhd">Zoomed on the boxed area</div>`
|
|
3875
|
+
+ `<div class="beforeafter ba-zoomrow">${zoom(baZoomDataUrl(bImg, box), 'before')}${zoom(baZoomDataUrl(aImg, box), 'after')}</div>`;
|
|
3842
3876
|
}
|
|
3843
3877
|
// swap the still for the real video only on demand, one at a time
|
|
3844
3878
|
document.addEventListener('click', (e) => {
|
|
@@ -3904,15 +3938,28 @@ function progressMeta(t){
|
|
|
3904
3938
|
// checked + struck through, the in-progress one is highlighted, pending ones
|
|
3905
3939
|
// are dimmed. Empty list → nothing (workers without todos look unchanged).
|
|
3906
3940
|
const TODO_MARK = { completed: '✓', in_progress: '▸', pending: '○' };
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3941
|
+
// P0b (Masa 2026-07-21, planner demoted): the AI's own TodoWrite list is now the
|
|
3942
|
+
// only breakdown a goal has (1 goal = 1 task, no invented sub-tasks). `state.todos`
|
|
3943
|
+
// mirrors it live via the 'todos' SSE event; once the task finishes, the server
|
|
3944
|
+
// freezes the same shape into `task.todosFinal` (engine/server.mjs) since the live
|
|
3945
|
+
// field gets stripped off finished tasks. This is the one place that resolves
|
|
3946
|
+
// "which list to show" — live wins while it has content, else the frozen one.
|
|
3947
|
+
function todoSourceFor(t){
|
|
3948
|
+
const live = state.todos[t?.id];
|
|
3949
|
+
if (Array.isArray(live) && live.length) return live;
|
|
3950
|
+
return Array.isArray(t?.todosFinal) ? t.todosFinal : [];
|
|
3951
|
+
}
|
|
3952
|
+
function todoRowsHtml(list){
|
|
3953
|
+
return list.map((d) => {
|
|
3911
3954
|
const st = ['completed', 'in_progress', 'pending'].includes(d.status) ? d.status : 'pending';
|
|
3912
3955
|
const label = st === 'in_progress' && d.activeForm ? d.activeForm : d.content;
|
|
3913
3956
|
return `<div class="todo ${st}"><span class="box">${TODO_MARK[st]}</span><span class="tdtext">${esc(label ?? '')}</span></div>`;
|
|
3914
3957
|
}).join('');
|
|
3915
|
-
|
|
3958
|
+
}
|
|
3959
|
+
function todosHtml(t){
|
|
3960
|
+
const list = todoSourceFor(t);
|
|
3961
|
+
if (!list.length) return '';
|
|
3962
|
+
return `<div class="todos"><div class="tdhead">To-do</div>${todoRowsHtml(list)}</div>`;
|
|
3916
3963
|
}
|
|
3917
3964
|
|
|
3918
3965
|
function taskBlock(t){
|
|
@@ -4386,7 +4433,6 @@ function renderTasks(){
|
|
|
4386
4433
|
const digestRows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
4387
4434
|
const digestCount = digestRows.length;
|
|
4388
4435
|
const dGreen = digestRows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
4389
|
-
const dProof = digestRows.filter((r) => r.proof).length;
|
|
4390
4436
|
const dPct = digestCount ? Math.round((dGreen / digestCount) * 100) : 0;
|
|
4391
4437
|
const dBlocked = pgoals.filter((g) => g.status === 'blocked').length; // §10: reflect real blocked count, don't hardcode
|
|
4392
4438
|
// CDO review 2026-07-08 §6: dGreen/digestCount already exclude blocked goals (they
|
|
@@ -4404,7 +4450,7 @@ function renderTasks(){
|
|
|
4404
4450
|
? `<div class="s5sum" id="summaryEntry" title="Open the review summary (ペライチ)">
|
|
4405
4451
|
<span class="s5exp"><svg width="13" height="13" viewBox="0 0 24 24"><path d="M15 4h5v5M20 4l-7 7M9 20H4v-5M4 20l7-7"/></svg></span>
|
|
4406
4452
|
<span class="s5ring" style="background:conic-gradient(var(--green) 0 ${dPct}%, var(--hair2) ${dPct}% 100%)"><span class="s5in">${dGreen}/${digestCount}</span></span>
|
|
4407
|
-
<span class="s5tx"><span class="s5hl">${dAllClear ? 'Ready to review' : `${dGreen} of ${digestCount} ready`}</span><span class="s5sub">${dAllClear ? 'all green' : dGreen === digestCount ? 'blocked needs attention' : 'some failing'} · ${
|
|
4453
|
+
<span class="s5tx"><span class="s5hl">${dAllClear ? 'Ready to review' : `${dGreen} of ${digestCount} ready`}</span><span class="s5sub">${dAllClear ? 'all green' : dGreen === digestCount ? 'blocked needs attention' : 'some failing'} · ${dBlocked ? `${dBlocked} blocked` : 'none blocked'}</span></span>
|
|
4408
4454
|
</div>`
|
|
4409
4455
|
: '';
|
|
4410
4456
|
const REVIEW_LIMIT = 5;
|
|
@@ -4418,16 +4464,12 @@ function renderTasks(){
|
|
|
4418
4464
|
// Feedback 3: the row carries only the check-line + PR link. Tests/proof are
|
|
4419
4465
|
// internal gate checks (they still block), NOT list clutter; the Review number
|
|
4420
4466
|
// is redundant with the title; tapping the row opens the detail (no button).
|
|
4421
|
-
// PRD §6.5
|
|
4422
|
-
//
|
|
4423
|
-
//
|
|
4424
|
-
//
|
|
4425
|
-
|
|
4426
|
-
const carryNote = carried.length ? (foldLangFor(g) === 'ja'
|
|
4427
|
-
? `+ ${carried.map((x) => `#${x.id}`).join(', ')} の仕事もここに入っています`
|
|
4428
|
-
: `also carries ${carried.map((x) => `#${x.id}`).join(', ')}`) : '';
|
|
4467
|
+
// PRD §6.5 → Masa 2026-07-21: goals folded onto this one are NOT announced here
|
|
4468
|
+
// any more ("+ #901 の仕事もここに入っています" read as the same report-in-the-list
|
|
4469
|
+
// clutter as the old "→ #12 にまとめました" row — Masa: review欄にいらない). Where
|
|
4470
|
+
// the work went is logged inside the opened detail's "Also carries" section
|
|
4471
|
+
// (see saItemHtml's carriesBody) instead — the list row stays just the ask.
|
|
4429
4472
|
return `<div class="task revrow" data-goal="${g.id}"><div class="ic pending"></div><div class="tx"><div class="name">${esc(checkLine)} ${goalSourceBadge(g.source)}</div>
|
|
4430
|
-
${carryNote ? `<div class="cond carries">${carryNote}</div>` : ''}
|
|
4431
4473
|
<div class="cond">${g.pr ? `<a class="prbig" href="${esc(g.pr)}" target="_blank">PR #${esc((g.pr.match(/pull\/(\d+)/) ?? [,''])[1])} ↗</a>` : ''}</div>
|
|
4432
4474
|
<div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="直したい点を書いて送信(Todoに戻して再作業)"><button type="button" class="qreplysend" data-qreplysend="${g.id}" aria-label="Send">${ICONS.up}</button></div></div>
|
|
4433
4475
|
<div class="revquick">
|
|
@@ -4437,16 +4479,10 @@ function renderTasks(){
|
|
|
4437
4479
|
</div></div>`;
|
|
4438
4480
|
}).join('')
|
|
4439
4481
|
+ (reviewGoalsAll.length > REVIEW_LIMIT ? `<div class="more" data-reviewmore="1">${state.showAllReview ? 'show less' : `more · +${reviewGoalsAll.length - REVIEW_LIMIT}`}</div>` : '');
|
|
4440
|
-
// PRD §3.5: a folded goal
|
|
4441
|
-
//
|
|
4442
|
-
//
|
|
4443
|
-
//
|
|
4444
|
-
const foldedRows = pgoals.filter((g) => g.status === 'folded').map((g) => {
|
|
4445
|
-
const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').slice(0, 66);
|
|
4446
|
-
const mergedNote = foldLangFor(g) === 'ja' ? `→ #${esc(String(g.foldedInto))} にまとめました` : `merged into #${esc(String(g.foldedInto))}`;
|
|
4447
|
-
return `<div class="task revrow foldrow" data-goal="${g.id}"><div class="ic pending"></div><div class="tx"><div class="name">${esc(checkLine)} ${goalSourceBadge(g.source)}</div>
|
|
4448
|
-
<div class="cond">${mergedNote}</div></div></div>`;
|
|
4449
|
-
}).join('');
|
|
4482
|
+
// PRD §3.5 → Masa 2026-07-21: a folded goal no longer gets its own "→ #12 にまとめました"
|
|
4483
|
+
// row in the Review column (Masa: review欄にいらない・わかりにくい). It is never deleted —
|
|
4484
|
+
// where its work went is now logged inside the carrier review's detail ("Also carries",
|
|
4485
|
+
// see saItemHtml), so the column stays clean while the trail is still one tap away.
|
|
4450
4486
|
const doneRows = grouped(list.filter((t) => ['done', 'skipped'].includes(t.status)).sort((a, b) => b.num - a.num), (t) => row(t));
|
|
4451
4487
|
// Pending = deliberately shelved goals (e.g. "npm公開はPhase 2"): never
|
|
4452
4488
|
// planned or queued until Start is pressed. Parked below To do on purpose.
|
|
@@ -4458,7 +4494,7 @@ function renderTasks(){
|
|
|
4458
4494
|
const savedQAns = captureFocusedAnswerInput($('tasklist'), 'data-qansinput');
|
|
4459
4495
|
$('tasklist').innerHTML =
|
|
4460
4496
|
sec('Attention', attention, 'attn')
|
|
4461
|
-
+ sec(pickColumnLabel(cols, 'review', 'Review'), reviewDigest + review
|
|
4497
|
+
+ sec(pickColumnLabel(cols, 'review', 'Review'), reviewDigest + review, 'reviewsec', reviewDefinitionFor(state.active).description, REVIEW_DEF_HINT)
|
|
4462
4498
|
+ sec(pickColumnLabel(cols, 'doing', 'Doing'), doing)
|
|
4463
4499
|
+ sec(pickColumnLabel(cols, 'todo', 'To do'), todo, 'todosec')
|
|
4464
4500
|
+ sec('Pending', pendingRows, 'dim')
|
|
@@ -4910,9 +4946,6 @@ function renderRdEditor(){
|
|
|
4910
4946
|
<label class="rdrow"><input type="checkbox" id="rdCardScreenshots"${state.rdDraft.reviewCard.screenshots ? ' checked' : ''}>
|
|
4911
4947
|
<span><span class="rdlabel">スクショ / Screenshots</span>
|
|
4912
4948
|
<span class="rdhint">レビューカードに証跡(スクショ/GIF)を表示する。</span></span></label>
|
|
4913
|
-
<label class="rdrow"><input type="checkbox" id="rdCardBeforeAfter"${state.rdDraft.reviewCard.beforeAfter ? ' checked' : ''}>
|
|
4914
|
-
<span><span class="rdlabel">Before/After</span>
|
|
4915
|
-
<span class="rdhint">変更前後を並べて表示する(変更前キャプチャが無い場合は通常表示になります)。</span></span></label>
|
|
4916
4949
|
|
|
4917
4950
|
<div class="rdsub">出力言語 / Output language</div>
|
|
4918
4951
|
<label class="rdrow"><select id="rdLanguage">
|
|
@@ -4933,7 +4966,6 @@ function renderRdEditor(){
|
|
|
4933
4966
|
$('rdPrScreenshots').addEventListener('change', (e) => { state.rdDraft.prSections.screenshots = e.target.checked; });
|
|
4934
4967
|
$('rdPrRisk').addEventListener('change', (e) => { state.rdDraft.prSections.risk = e.target.checked; });
|
|
4935
4968
|
$('rdCardScreenshots').addEventListener('change', (e) => { state.rdDraft.reviewCard.screenshots = e.target.checked; });
|
|
4936
|
-
$('rdCardBeforeAfter').addEventListener('change', (e) => { state.rdDraft.reviewCard.beforeAfter = e.target.checked; });
|
|
4937
4969
|
$('rdLanguage').addEventListener('change', (e) => { state.rdDraft.language = e.target.value; });
|
|
4938
4970
|
}
|
|
4939
4971
|
function renderWfEditor(){
|
|
@@ -5127,10 +5159,8 @@ function renderReviewChecklist(){
|
|
|
5127
5159
|
$('reviewDoneMsg').classList.toggle('show', goal.status === 'done');
|
|
5128
5160
|
// task 97: doneカードの『元に戻す』——押し間違いのApproveを取り消し、reviewへ戻す。
|
|
5129
5161
|
$('reviewUndoBtn').onclick = () => revertDoneGoal(goal);
|
|
5130
|
-
// reviewCard.screenshots off → the media block
|
|
5131
|
-
//
|
|
5132
|
-
// on → try the before/after comparison (falls back to the single shot when
|
|
5133
|
-
// there's no "before" capture — see beforeAfterMediaHtml).
|
|
5162
|
+
// reviewCard.screenshots off → the media block is skipped entirely, not just
|
|
5163
|
+
// emptied. (The Before/After switch is gone with the staged pair it controlled — P1.)
|
|
5134
5164
|
const rc = reviewCardConfigFor(state.active);
|
|
5135
5165
|
$('reviewList').innerHTML = reqs.map((t, i) => {
|
|
5136
5166
|
const st = approvals[t.id];
|
|
@@ -5141,13 +5171,14 @@ function renderReviewChecklist(){
|
|
|
5141
5171
|
<div class="reqhead"><span class="tick">✓</span><span class="reqnum">${t.num ?? i + 1}</span><span class="reqtitle">${esc(t.title ?? '')}</span><span class="apvd">Approved · tap to reopen</span></div>
|
|
5142
5172
|
</div>`;
|
|
5143
5173
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5174
|
+
// P1: one shot when a promised check actually captured one, otherwise nothing at
|
|
5175
|
+
// all — "No capture yet" was us apologising for a photo we no longer go and take.
|
|
5176
|
+
const media = proofMediaHtml(t.id, t.proof, esc(t.title ?? ''));
|
|
5146
5177
|
// task 156/234 + 160: show the WORKER'S REPORT (what it actually did/found,
|
|
5147
5178
|
// incl. any test-only verification it describes) — that's what you read to
|
|
5148
5179
|
// decide. Falls back to the instruction if the worker left no result.
|
|
5149
5180
|
const report = String(t.result ?? '').replace(/^検証(PASS|FAIL)[^\n]*\n+/, '').replace(/\s+$/, '').trim();
|
|
5150
|
-
const desc = report ? `<div class="reqdesc"><span class="reqreportlbl">Worker report</span>${mdLite(report
|
|
5181
|
+
const desc = report ? `<div class="reqdesc"><span class="reqreportlbl">Worker report</span>${mdLite(report)}</div>`
|
|
5151
5182
|
: t.detail ? `<div class="reqdesc">${mdLite(t.detail.slice(0, 600))}</div>` : '';
|
|
5152
5183
|
const usage = formatUsageCompact(t.usage);
|
|
5153
5184
|
const usageBadge = usage ? `<span class="tbadge usage">${usage} tokens</span>` : '';
|
|
@@ -6093,6 +6124,11 @@ function renderFsBoard(){
|
|
|
6093
6124
|
const paused = !!state.projects.find((p) => p.id === state.active)?.paused; // Doing列トグルの状態
|
|
6094
6125
|
const { pgoals, list } = fsBucketTodo();
|
|
6095
6126
|
const tt = (s, n) => esc(String(s ?? '').replace(/\s+/g, ' ').slice(0, n ?? 60));
|
|
6127
|
+
// Card copy language — same rule as reviewLangFor(state.active) === 'ja', inlined
|
|
6128
|
+
// (not calling reviewLangFor itself) because engine/test/board-goal-level.test.mjs
|
|
6129
|
+
// evals this function body standalone off a minimal `state` stub with no
|
|
6130
|
+
// state.reviewDefinitions; reviewLangFor would throw there.
|
|
6131
|
+
const boardJa = state.reviewDefinitions?.[state.active]?.language === 'ja';
|
|
6096
6132
|
const goalNo = new Map();
|
|
6097
6133
|
for (const g of pgoals) { const n = goalReviewNumber({ goal: g, tasks: list }); if (n != null) goalNo.set(g.id, n); }
|
|
6098
6134
|
// + New in EVERY column, matching the flagship Artifact: a hover-reveal "+" in the header
|
|
@@ -6107,6 +6143,35 @@ function renderFsBoard(){
|
|
|
6107
6143
|
// status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
|
|
6108
6144
|
// the goal is moving) — a coloured word (Failed/Interrupted) says it instead.
|
|
6109
6145
|
const goalTasks = (g) => list.filter((t) => t.goalId === g.id && !t.reply);
|
|
6146
|
+
// P0b (Masa 2026-07-21): the card's breakdown is the AI's OWN TodoWrite list — never
|
|
6147
|
+
// the planner's invented sub-tasks (there aren't any anymore; 1 goal = 1 task). Live
|
|
6148
|
+
// list wins while it has content, else the frozen task.todosFinal (mirrors the
|
|
6149
|
+
// module-level todoSourceFor() used by the chat feed's todosHtml — duplicated inline
|
|
6150
|
+
// rather than called, same reason as boardJa above: this whole function is evaluated
|
|
6151
|
+
// standalone by the board-goal-level test harness, which only stubs `state`/`esc`).
|
|
6152
|
+
const todoListFor = (t) => {
|
|
6153
|
+
const live = state.todos?.[t?.id];
|
|
6154
|
+
if (Array.isArray(live) && live.length) return live;
|
|
6155
|
+
return Array.isArray(t?.todosFinal) ? t.todosFinal : [];
|
|
6156
|
+
};
|
|
6157
|
+
// Capped + folded rather than scrolled (§1.5-8). While the task is actively running
|
|
6158
|
+
// but hasn't reached its first TodoWrite yet, the list is genuinely empty — show one
|
|
6159
|
+
// small "preparing" line instead of an empty container (§1.5-3), never a bare gap.
|
|
6160
|
+
const TODO_CARD_CAP = 5, TODO_MARK_BOARD = { completed: '✓', in_progress: '▸', pending: '○' };
|
|
6161
|
+
const goalCardTodosHtml = (t) => {
|
|
6162
|
+
if (!t) return '';
|
|
6163
|
+
const list_ = todoListFor(t);
|
|
6164
|
+
if (!list_.length) return t.status === 'running' ? `<div class="lbc-todos prep">${boardJa ? '準備中…' : 'Preparing…'}</div>` : '';
|
|
6165
|
+
const shown = list_.slice(0, TODO_CARD_CAP);
|
|
6166
|
+
const restN = list_.length - shown.length;
|
|
6167
|
+
const rows = shown.map((d) => {
|
|
6168
|
+
const st = ['completed', 'in_progress', 'pending'].includes(d.status) ? d.status : 'pending';
|
|
6169
|
+
const label = st === 'in_progress' && d.activeForm ? d.activeForm : d.content;
|
|
6170
|
+
return `<div class="todo ${st}"><span class="box">${TODO_MARK_BOARD[st]}</span><span class="tdtext">${esc(label ?? '')}</span></div>`;
|
|
6171
|
+
}).join('');
|
|
6172
|
+
const more = restN > 0 ? `<div class="lbc-more">+${restN} more</div>` : '';
|
|
6173
|
+
return `<div class="lbc-todos">${rows}${more}</div>`;
|
|
6174
|
+
};
|
|
6110
6175
|
const goalCard = (g) => {
|
|
6111
6176
|
const bkt = fsBoardBucket(g, list);
|
|
6112
6177
|
// errored is status-based now (blocked/failed/partial/interrupted live in the Needs-you column,
|
|
@@ -6131,13 +6196,21 @@ function renderFsBoard(){
|
|
|
6131
6196
|
+ (word ? `<span class="lbc-w ${amber ? 'intr' : 'bad'}">${word}</span>` : '')
|
|
6132
6197
|
+ '</div>'
|
|
6133
6198
|
+ `<div class="lbc-t">${tt(g.text || g.plan?.[0], 90)}</div>`;
|
|
6199
|
+
// The goal's one task (post-demotion there's exactly one non-reply task; [0]
|
|
6200
|
+
// is just defensive against any pre-migration leftovers with more than one).
|
|
6201
|
+
const task = goalTasks(g)[0];
|
|
6202
|
+
// Todos: not shown for To Do / Later — nothing has actually started yet there, so
|
|
6203
|
+
// there is nothing honest to fold open (quiet stays quiet). Doing/Needs/Review/Done
|
|
6204
|
+
// all show the AI's own breakdown — that's what makes it survive past "running".
|
|
6205
|
+
if (task && bkt !== 'todo' && bkt !== 'later') h += goalCardTodosHtml(task);
|
|
6134
6206
|
// n/m is Doing-only (Masa 2026-07-17): a fraction on a stopped/queued/done card is
|
|
6135
6207
|
// residue, not progress — a card showing n/m reads unambiguously as "moving right now".
|
|
6136
|
-
// done/total
|
|
6137
|
-
|
|
6138
|
-
|
|
6208
|
+
// P0b: done/total now reflect the AI's OWN todos (not the planner's invented tasks,
|
|
6209
|
+
// which under 1-goal=1-task degenerate to a useless 0/1 → 1/1).
|
|
6210
|
+
if (healthyRun && task) {
|
|
6211
|
+
const tl = todoListFor(task), total = tl.length;
|
|
6139
6212
|
if (total) {
|
|
6140
|
-
const done =
|
|
6213
|
+
const done = tl.filter((d) => d.status === 'completed').length;
|
|
6141
6214
|
h += `<div class="lbc-meter"><span class="bar"><i style="width:${Math.round(done / total * 100)}%"></i></span><span class="frac">${done}/${total}</span></div>`;
|
|
6142
6215
|
}
|
|
6143
6216
|
}
|
|
@@ -6227,16 +6300,11 @@ function renderFsReview(){
|
|
|
6227
6300
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
6228
6301
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
6229
6302
|
const rows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
6230
|
-
// PRD §3.5/§6.5: folded goals
|
|
6231
|
-
//
|
|
6232
|
-
//
|
|
6233
|
-
|
|
6234
|
-
// early there would make #13 vanish for the length of the re-run — exactly the "where
|
|
6235
|
-
// did my finished work go" this feature exists to prevent.
|
|
6236
|
-
const foldedGoals = pgoals.filter((g) => g.status === 'folded');
|
|
6237
|
-
if (!rows.length && !foldedGoals.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6303
|
+
// PRD §3.5/§6.5 → Masa 2026-07-21: folded goals no longer get their own card in the
|
|
6304
|
+
// Review column (Masa: review欄にいらない). They were never deleted — where their work
|
|
6305
|
+
// went is now logged inside the carrier review's detail ("Also carries", see saItemHtml).
|
|
6306
|
+
if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6238
6307
|
const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
6239
|
-
const proofN = rows.filter((r) => r.proof).length;
|
|
6240
6308
|
const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
|
|
6241
6309
|
const pct = rows.length ? Math.round((green / rows.length) * 100) : 0;
|
|
6242
6310
|
// CDO review 2026-07-08 §6: same fix as the s5sum digest above — green/rows.length
|
|
@@ -6244,7 +6312,7 @@ function renderFsReview(){
|
|
|
6244
6312
|
// ratio was already correct; "all green" is the line that used to fire regardless
|
|
6245
6313
|
// of blockedN, reading as "18/18 ready · all green · 1 blocked" (self-contradictory).
|
|
6246
6314
|
const allClear = green === rows.length && !blockedN;
|
|
6247
|
-
const sline = `${allClear ? 'all green' : green === rows.length ? 'blocked needs attention' : `${rows.length - green} failing`} · ${
|
|
6315
|
+
const sline = `${allClear ? 'all green' : green === rows.length ? 'blocked needs attention' : `${rows.length - green} failing`} · ${blockedN ? `${blockedN} blocked` : 'none blocked'}`;
|
|
6248
6316
|
const RCAP = 3;
|
|
6249
6317
|
const shown = fsUI.revMore ? rows : rows.slice(0, RCAP);
|
|
6250
6318
|
const card = (r) => {
|
|
@@ -6264,31 +6332,18 @@ function renderFsReview(){
|
|
|
6264
6332
|
// retest ×3 note (v45 §5.3): the goal came back green from a Ledger Dismiss
|
|
6265
6333
|
const g0 = pgoals.find((g) => g.id === r.goalIds[0]);
|
|
6266
6334
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
6267
|
-
// PRD §6.5
|
|
6268
|
-
//
|
|
6269
|
-
//
|
|
6270
|
-
|
|
6271
|
-
const cnote = carried.length ? `<span class="fnote">${foldLangFor(g0) === 'ja'
|
|
6272
|
-
? `+ ${carried.map((x) => `#${x.id}`).join(', ')} の仕事もここに入っています`
|
|
6273
|
-
: `also carries ${carried.map((x) => `#${x.id}`).join(', ')}`}</span>` : '';
|
|
6335
|
+
// PRD §6.5 → Masa 2026-07-21: goals folded onto this card's PR no longer get an
|
|
6336
|
+
// inline "+ #901 の仕事もここに入っています" note here — same report-in-the-list
|
|
6337
|
+
// clutter the old standalone fold row was (Masa: review欄にいらない). It is logged
|
|
6338
|
+
// inside the opened detail's "Also carries" section instead (see saItemHtml).
|
|
6274
6339
|
// Title = the goal's own title (saReviewTitle → text-first), same as the See-all
|
|
6275
6340
|
// Ledger and the To Do list, so a review card and its list row read identically
|
|
6276
6341
|
// (Masa 2026-07-20: this card showed reviewSummary.check "…が出ない" while the list
|
|
6277
6342
|
// showed the request text). Plan-reviews keep their "PLAN — approve to execute:"
|
|
6278
6343
|
// checkLine, which is the actionable label there, not a title mismatch.
|
|
6279
|
-
return `<div class="rcard${img ? '' : ' noimg'}" data-fsrev="${r.goalIds[0]}">${img}<div class="rc"><div class="t">${rid}${esc(r.plan ? r.checkLine : saReviewTitle(g0))}</div>${rnote}${
|
|
6344
|
+
return `<div class="rcard${img ? '' : ' noimg'}" data-fsrev="${r.goalIds[0]}">${img}<div class="rc"><div class="t">${rid}${esc(r.plan ? r.checkLine : saReviewTitle(g0))}</div>${rnote}${pr}</div>
|
|
6280
6345
|
<div class="act"><button class="b" data-fsapprove="${ids}" title="Approve">✓</button><button class="b no" data-fsdismiss="${ids}" title="Send back">✕</button></div></div>`;
|
|
6281
6346
|
};
|
|
6282
|
-
// A folded goal is settled, not failed: no ✓/✕ (the thing to approve lives on the goal
|
|
6283
|
-
// it was folded into), no red, no painted surface (§1.5-8) — just its own number, what
|
|
6284
|
-
// it asked for, and where the work went. Tapping it opens that goal (§1.5-9 wording).
|
|
6285
|
-
const foldCard = (g) => {
|
|
6286
|
-
const line = saReviewTitle(g).slice(0, 60); // title-first, same as the review card above
|
|
6287
|
-
const mergedNote = foldLangFor(g) === 'ja' ? `→ #${esc(String(g.foldedInto))} にまとめました` : `merged into #${esc(String(g.foldedInto))}`;
|
|
6288
|
-
return `<div class="rcard noimg fold" data-fsfold="${g.id}">
|
|
6289
|
-
<div class="rc"><div class="t"><span class="rid gno" data-n="${g.id}">#${g.id}</span>${esc(line)}</div>
|
|
6290
|
-
<span class="fnote">${mergedNote}</span></div></div>`;
|
|
6291
|
-
};
|
|
6292
6347
|
// V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
|
|
6293
6348
|
// above. Reviews render directly: meter + Review header + cards (+N more fold, §1.5-8).
|
|
6294
6349
|
// The meter counts what is reviewable, so it only exists when something is (§1.5-3):
|
|
@@ -6304,7 +6359,7 @@ function renderFsReview(){
|
|
|
6304
6359
|
<button class="rvb${fsUI.rv === 1 ? ' on' : ''}" data-fsrv="1" title="List"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="7" height="7" rx="1.5"/><path d="M13 6h8M13 9h5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M13 16h8M13 19h5"/></svg></button>
|
|
6305
6360
|
<button class="rvb${fsUI.rv === 2 ? ' on' : ''}" data-fsrv="2" title="Compact"><svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg></button>
|
|
6306
6361
|
</span></div>
|
|
6307
|
-
<div class="revlist">${shown.map(card).join('')}
|
|
6362
|
+
<div class="revlist">${shown.map(card).join('')}
|
|
6308
6363
|
${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
|
|
6309
6364
|
// Mirror the ready-review count onto the mobile top-bar "Review N" opener (H23).
|
|
6310
6365
|
const _hn = document.getElementById('fsHambN'); if (_hn) _hn.textContent = green > 0 ? String(green) : '';
|
|
@@ -6324,19 +6379,8 @@ function renderFsReview(){
|
|
|
6324
6379
|
if (e.target.closest('a') || e.target.closest('.act')) return;
|
|
6325
6380
|
openReviewChecklist(Number(c.dataset.fsrev));
|
|
6326
6381
|
};
|
|
6327
|
-
//
|
|
6328
|
-
//
|
|
6329
|
-
// finishes, so resolve from its live status instead of assuming (openReviewChecklist
|
|
6330
|
-
// on a running goal would present an empty checklist).
|
|
6331
|
-
for (const c of el.querySelectorAll('[data-fsfold]')) c.onclick = (e) => {
|
|
6332
|
-
if (e.target.closest('a')) return;
|
|
6333
|
-
const g = state.goals.find((x) => x.id === Number(c.dataset.fsfold));
|
|
6334
|
-
const target = state.goals.find((x) => x.id === g?.foldedInto);
|
|
6335
|
-
if (!target) return;
|
|
6336
|
-
goToGoalInChat(target.id);
|
|
6337
|
-
if (target.status === 'review') openReviewChecklist(target.id);
|
|
6338
|
-
else openGoalDetail(target.id);
|
|
6339
|
-
};
|
|
6382
|
+
// Folded goals no longer render their own card here (Masa 2026-07-21) — where their
|
|
6383
|
+
// work went is logged inside the carrier review's detail ("Also carries"), not tapped.
|
|
6340
6384
|
for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
|
|
6341
6385
|
let ok = true, mergeGoal = null;
|
|
6342
6386
|
for (const id of b.dataset.fsapprove.split(',')) {
|
|
@@ -7413,8 +7457,14 @@ async function refresh(){
|
|
|
7413
7457
|
// back the already-accumulated activity for 'running' tasks.
|
|
7414
7458
|
for (const t of state.tasks) { if (t.activity?.length) state.act[t.id] = t.activity; }
|
|
7415
7459
|
// Same for the live to-do list: /api/state ships t.todos for running tasks
|
|
7416
|
-
// (task 108/109) so a mid-run refresh restores the checklist immediately.
|
|
7417
|
-
|
|
7460
|
+
// (task 108/109) so a mid-run refresh restores the checklist immediately. A
|
|
7461
|
+
// FINISHED task has no live t.todos (trimTaskActivityForState strips it) but
|
|
7462
|
+
// keeps t.todosFinal — seed from that too (P0b), so a reload of a completed
|
|
7463
|
+
// goal still shows the AI's own breakdown instead of losing it silently.
|
|
7464
|
+
for (const t of state.tasks) {
|
|
7465
|
+
if (t.todos?.length) state.todos[t.id] = t.todos;
|
|
7466
|
+
else if (t.todosFinal?.length) state.todos[t.id] = t.todosFinal;
|
|
7467
|
+
}
|
|
7418
7468
|
state.connectCommand = s.connectCommand ?? ''; state.models = s.models ?? ['sonnet']; state.agentModels = s.agentModels ?? null; state.agentEfforts = s.agentEfforts ?? null; state.agents = s.agents ?? ['claude-code', 'codex']; state.uiVersion = s.uiVersion;
|
|
7419
7469
|
state.billing = s.billing ?? { licensed: false, email: null };
|
|
7420
7470
|
renderAppSelect(); // picker follows the server's installed-agent set; may correct a stale connectedApp
|
|
@@ -8053,7 +8103,7 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
8053
8103
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
8054
8104
|
// settles it (see saSync's "returned to review" reset).
|
|
8055
8105
|
// ============================================================================
|
|
8056
|
-
const SA = { open: false, items: [], diffs: {},
|
|
8106
|
+
const SA = { open: false, items: [], diffs: {}, actLog: {}, shotBox: {}, detOpen: new Set(), focusGid: null, focusScrolled: false, focusTimer: null, cur: null };
|
|
8057
8107
|
window.SA = SA; // top-level const isn't a window property; external checks probe window.SA (same convention as window.state above)
|
|
8058
8108
|
const SA_SVG = {
|
|
8059
8109
|
ok: '<svg viewBox="0 0 24 24"><path d="M4 12l5 5L20 6"/></svg>',
|
|
@@ -8086,8 +8136,13 @@ function saRows(){
|
|
|
8086
8136
|
return { no: goalReviewNumber({ goal: sg, tasks: ts }), title: saReviewTitle(sg), cur: gid === r.goalIds[0] };
|
|
8087
8137
|
})
|
|
8088
8138
|
: null;
|
|
8139
|
+
// Masa 2026-07-21: goals folded onto this review no longer take their own Review-column
|
|
8140
|
+
// row ("→ #12 にまとめました" read as clutter). Instead this review's detail logs what
|
|
8141
|
+
// it carries. Mirrors engine/lib.mjs foldedIntoReview() (browser can't import it).
|
|
8142
|
+
const folded = gs.filter((x) => x.status === 'folded' && x.foldedInto != null && r.goalIds.includes(x.foldedInto))
|
|
8143
|
+
.map((x) => ({ id: x.id, title: saReviewTitle(x) }));
|
|
8089
8144
|
return {
|
|
8090
|
-
ids: r.goalIds, goalId: r.goalIds[0], goal: g,
|
|
8145
|
+
ids: r.goalIds, goalId: r.goalIds[0], goal: g, folded,
|
|
8091
8146
|
no: r.reviewNumber,
|
|
8092
8147
|
title: saReviewTitle(g),
|
|
8093
8148
|
check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
|
|
@@ -8095,6 +8150,47 @@ function saRows(){
|
|
|
8095
8150
|
did, siblings, risk: g?.risk ?? null,
|
|
8096
8151
|
note: g?.retest?.passed ? g.retest.note : null,
|
|
8097
8152
|
pr: r.pr,
|
|
8153
|
+
// P0b (Masa 2026-07-21): the AI's own TodoWrite breakdown for the "Steps" section
|
|
8154
|
+
// below — the primary goal's own non-reply task, frozen at completion. Distinct
|
|
8155
|
+
// from `did` above (task.result prose) and from Activity (raw tool-call log).
|
|
8156
|
+
todosFinal: (() => {
|
|
8157
|
+
const t = ts.find((x) => x.goalId === g?.id && !x.reply);
|
|
8158
|
+
return Array.isArray(t?.todosFinal) ? t.todosFinal : [];
|
|
8159
|
+
})(),
|
|
8160
|
+
// Screenshots the worker took of what it built (#229). Measured 2026-07-22 on real
|
|
8161
|
+
// runs: it takes 1-3 depending on the ask — three variants as three shots, or four
|
|
8162
|
+
// variants as ONE comparison sheet. A single shot is not a lazy shot, so none of
|
|
8163
|
+
// these get thumbnailed down.
|
|
8164
|
+
//
|
|
8165
|
+
// Folded goals count: a goal folded into this one keeps its own tasks, and those
|
|
8166
|
+
// tasks carry shots. Reading only the primary goal's tasks makes real deliverables
|
|
8167
|
+
// vanish from the screen (measured: #4 folded into #3, its images disappeared).
|
|
8168
|
+
shots: (() => {
|
|
8169
|
+
const ids = [...r.goalIds, ...folded.map((f) => f.id)];
|
|
8170
|
+
return ts.filter((t) => ids.includes(t.goalId)).flatMap((t) => t.shots ?? []);
|
|
8171
|
+
})(),
|
|
8172
|
+
// The task whose implementation log the engine froze at completion (P1.5). The
|
|
8173
|
+
// lines are NOT in state — 1000 of them per task would bloat every page load — so
|
|
8174
|
+
// this is just the id to ask for when the reviewer opens Activity.
|
|
8175
|
+
logTaskId: (() => {
|
|
8176
|
+
const t = ts.find((x) => x.goalId === g?.id && !x.reply && x.activityLog);
|
|
8177
|
+
return t?.id ?? null;
|
|
8178
|
+
})(),
|
|
8179
|
+
// P1 (Masa 2026-07-21): the AI's own final report, verbatim and untrimmed — the
|
|
8180
|
+
// card renders it as Markdown and interprets nothing. This is the deliverable for
|
|
8181
|
+
// every ask that has no running thing to open ("research this", "explain that"),
|
|
8182
|
+
// and the explanation for every ask that does.
|
|
8183
|
+
report: (() => {
|
|
8184
|
+
const t = ts.find((x) => x.goalId === g?.id && !x.reply && x.result);
|
|
8185
|
+
return String(t?.result ?? '').replace(/^検証(PASS|FAIL)[^\n]*\n+/, '').trim();
|
|
8186
|
+
})(),
|
|
8187
|
+
// …and the worker's own "here's how you see it running" declaration (task.run,
|
|
8188
|
+
// written by the worker into .manager-run.json). Absent for asks with nothing to
|
|
8189
|
+
// run — then no button, which is the honest state, not a missing feature.
|
|
8190
|
+
run: (() => {
|
|
8191
|
+
const t = ts.find((x) => x.goalId === g?.id && !x.reply && x.run);
|
|
8192
|
+
return t ? { taskId: t.id, ...t.run } : null;
|
|
8193
|
+
})(),
|
|
8098
8194
|
// Explicit review-summary display fields (spec §11: the card's shape follows the
|
|
8099
8195
|
// task). A goal's reviewSummary may carry them directly (e.g. a conversational task
|
|
8100
8196
|
// that reports an Answer + its Activity, or a ticket whose test count / diff totals
|
|
@@ -8102,19 +8198,13 @@ function saRows(){
|
|
|
8102
8198
|
testResult: (typeof (g?.reviewSummary?.tests) === 'string')
|
|
8103
8199
|
? { ran: true, ok: true, passed: parseInt(g.reviewSummary.tests, 10) || 0, failed: 0, label: g.reviewSummary.tests }
|
|
8104
8200
|
: r.testResult,
|
|
8105
|
-
answer: g?.reviewSummary?.answer ?? null,
|
|
8106
8201
|
activity: Array.isArray(g?.reviewSummary?.activity) ? g.reviewSummary.activity : null,
|
|
8107
|
-
discussion: Array.isArray(g?.reviewSummary?.discussion) ? g.reviewSummary.discussion : null,
|
|
8108
8202
|
changedDesc: g?.reviewSummary?.changed ?? null,
|
|
8109
8203
|
metaFiles: Array.isArray(g?.reviewSummary?.files) ? g.reviewSummary.files : null,
|
|
8110
8204
|
metaAdd: g?.reviewSummary?.a ?? null, metaDel: g?.reviewSummary?.d ?? null,
|
|
8111
8205
|
ghRows: Array.isArray(g?.reviewSummary?.ghdiff) ? g.reviewSummary.ghdiff : null,
|
|
8112
8206
|
diffTotal: g?.reviewSummary?.diffTotal ?? null,
|
|
8113
8207
|
proofTaskId: pt?.id ?? null, proofImg: pt?.proof?.png ?? pt?.proof?.gif ?? null,
|
|
8114
|
-
// Ledger before/after (docs/design/HANDOFF-review-card-evidence-first-2026-07-18.md
|
|
8115
|
-
// §3 kind==='image'): most tasks never have a "before" capture, so this is almost
|
|
8116
|
-
// always null and beforeAfterMediaHtml() falls back to the single-shot view.
|
|
8117
|
-
proofBefore: pt?.proof?.beforePng ?? pt?.proof?.beforeGif ?? null,
|
|
8118
8208
|
// Recording (spec §5/§6): a proof task may carry a screen recording (proof.mp4)
|
|
8119
8209
|
// or just a recording:true flag; the goal's reviewSummary can also name one.
|
|
8120
8210
|
// Drives the neutral "Verify recording ↗" meta chip + the Proof video player.
|
|
@@ -8123,7 +8213,6 @@ function saRows(){
|
|
|
8123
8213
|
recordingUrl: pt?.proof?.mp4 ? withKey(`/proof/${pt.id}/${pt.proof.mp4}`) : (typeof g?.reviewSummary?.recording === 'string' ? g.reviewSummary.recording : null),
|
|
8124
8214
|
// Local URL (spec §5): a running localhost the reviewer can open ("Local localhost:4809 ↗").
|
|
8125
8215
|
// Falls back to the existing "Local run ↗" (runlocal) button when absent.
|
|
8126
|
-
localUrl: g?.reviewSummary?.localUrl ?? null,
|
|
8127
8216
|
// Client-side diff fallback: the git-backed /diff endpoint is null for seeded/
|
|
8128
8217
|
// offline goals, so "What changed" never renders. A unified-diff string stored on
|
|
8129
8218
|
// the goal (or any of its tasks) lets saGhDiffHtml render locally with no server.
|
|
@@ -8146,85 +8235,33 @@ function saRows(){
|
|
|
8146
8235
|
};
|
|
8147
8236
|
});
|
|
8148
8237
|
}
|
|
8149
|
-
//
|
|
8150
|
-
//
|
|
8151
|
-
//
|
|
8152
|
-
//
|
|
8153
|
-
//
|
|
8154
|
-
//
|
|
8155
|
-
// than pretending to execute anything — never a fake "running…" state.
|
|
8156
|
-
function saMetaRowHtml(it, i){
|
|
8157
|
-
const canRunLocal = Boolean(it.testResult?.ran);
|
|
8158
|
-
if (!it.pr && !it.tokenOpt && !canRunLocal) {
|
|
8159
|
-
return '<div class="lg-meta"><span class="none">No PR link / token-savings data on this task.</span></div>';
|
|
8160
|
-
}
|
|
8161
|
-
const chips = [];
|
|
8162
|
-
if (it.pr) chips.push(`<a class="sa-prlink sm" href="${esc(it.pr)}" target="_blank" title="Open pull request">PR #${esc((it.pr.match(/pull\/(\d+)/) ?? [, ''])[1])} ↗</a>`);
|
|
8163
|
-
if (it.tokenOpt) {
|
|
8164
|
-
const pct = Number(it.tokenOpt.percent);
|
|
8165
|
-
const label = pct > 0 ? `saved ${pct}% tokens` : 'token usage';
|
|
8166
|
-
chips.push(`<button class="tokchip" type="button" data-saact="tok:${i}">${esc(label)} <i class="tokchevron">›</i></button>`);
|
|
8167
|
-
}
|
|
8168
|
-
if (canRunLocal) chips.push(`<button class="sa-runlocal" type="button" data-saact="runlocal:${i}" title="Run this locally"><svg viewBox="0 0 24 24"><path d="M6 4l14 8-14 8V4z"/></svg>Run locally</button>`);
|
|
8169
|
-
const tokCmp = it.tokenOpt?.basis === 'baseline' ? 'vs your recent average' : 'from prompt caching';
|
|
8170
|
-
const tokTech = (it.tokenOpt?.techniques ?? []).slice(0, 4).join(' / ');
|
|
8171
|
-
const tokDetail = it.tokenOpt ? `<div class="tokdetail" id="tokdetail${i}"><span class="k">How this run used tokens</span><span class="v">${esc(tokCmp)} · ${esc(tokTech)}</span></div>` : '';
|
|
8172
|
-
const runNote = canRunLocal ? `<div class="tokdetail" id="runlocal${i}"><span class="v">No local run command is wired for this task yet.</span></div>` : '';
|
|
8173
|
-
return `<div class="lg-meta">${chips.join('')}</div>${tokDetail}${runNote}`;
|
|
8174
|
-
}
|
|
8175
|
-
// Context: closed by default, one-line verbatim-quote summary, chevron large and at
|
|
8176
|
-
// the far right (H23 §2). Quotes goal.text — the user's own words — never
|
|
8177
|
-
// task.detail/title (AI-authored; an earlier round of this design mistakenly quoted
|
|
8178
|
-
// the AI's own read on the request as "your ask" and it wasn't recognizable).
|
|
8179
|
-
function saContextHtml(it, gk){
|
|
8180
|
-
const goalTxt = it.goalText;
|
|
8181
|
-
const summary = goalTxt
|
|
8182
|
-
? `"${esc(goalTxt.length > 60 ? `${goalTxt.slice(0, 60)}…` : goalTxt)}"`
|
|
8183
|
-
: 'No verbatim ask on record for this goal.';
|
|
8184
|
-
const split = it.siblings
|
|
8185
|
-
? `<div class="lg-splitnote"><div class="hd">Split into ${it.siblings.length} goals — nothing else was dropped</div><ul>${it.siblings.map((s) => `<li class="${s.cur ? 'cur' : ''}">${s.no != null ? `#${esc(s.no)} ` : ''}${esc(s.title)}${s.cur ? ' <b>(this one)</b>' : ''}</li>`).join('')}</ul></div>`
|
|
8186
|
-
: '';
|
|
8187
|
-
const did = it.did?.length ? `<div class="lg-bullethd">What we did</div><ul class="lg-bullets">${it.did.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '';
|
|
8188
|
-
const look = it.look?.length ? `<div class="lg-bullethd">What to look at</div><ul class="lg-bullets">${it.look.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '';
|
|
8189
|
-
const inner = (split || did || look) ? `${split}${did}${look}` : '<div class="lg-notfound">Nothing else on record for this goal.</div>';
|
|
8190
|
-
const open = SA.detOpen.has(`${gk}:ctx`) ? ' open' : '';
|
|
8191
|
-
return `<details class="lg-goal" data-detkey="${gk}:ctx"${open}><summary><span class="k">Context</span><span class="v">${summary}</span><i class="gtoggle">›</i></summary><div class="lg-context">${inner}</div></details>`;
|
|
8192
|
-
}
|
|
8193
|
-
// Evidence hero: shape follows task kind, not one image-shaped template (H23 §3).
|
|
8194
|
-
// Real data check: most finished tasks have no proof image at all (logic-only /
|
|
8195
|
-
// docs-only / research changes verify without a screenshot) — kind is derived, see
|
|
8196
|
-
// saEvidenceKind() above.
|
|
8197
|
-
function saEvidenceHtml(kind, factRow, img, filePaths, diffText, gtr, it){
|
|
8198
|
-
if (kind === 'logic') {
|
|
8199
|
-
const passed = gtr?.passed ?? 0, failed = gtr?.failed ?? 0, ok = gtr?.ok !== false;
|
|
8200
|
-
const n = ok ? String(passed) : `${passed}/${passed + failed}`;
|
|
8201
|
-
return `<div class="lg-ev"><div class="lg-evhd">Evidence — tests, no UI to show</div>
|
|
8202
|
-
<div class="lg-herostat${ok ? '' : ' fail'}"><span class="n">${esc(n)}</span><span class="l">tests passed<br>${esc(filePaths.join(', '))}</span></div></div>`;
|
|
8203
|
-
}
|
|
8204
|
-
if (kind === 'docs') {
|
|
8205
|
-
const lines = diffText ? diffText.split('\n').filter((l) => l.length) : [];
|
|
8206
|
-
const diffHtml = lines.map((l) => {
|
|
8207
|
-
const isAdd = l.startsWith('+');
|
|
8208
|
-
return `<div class="ln${isAdd ? ' a' : ''}"><span class="gutter">+</span><span class="txt">${esc(l.slice(1) || l)}</span></div>`;
|
|
8209
|
-
}).join('');
|
|
8210
|
-
return `<div class="lg-ev"><div class="lg-evhd">Evidence — the diff, no test to run</div>
|
|
8211
|
-
<div class="row"><span class="fls">${esc(filePaths.join(', '))} · docs-only, no test target</span></div>
|
|
8212
|
-
${diffHtml ? `<div class="lg-diffblock">${diffHtml}</div>` : ''}</div>`;
|
|
8213
|
-
}
|
|
8214
|
-
if (kind === 'missing') {
|
|
8215
|
-
const note = it.check || 'Please verify this change manually — no automated capture exists for it.';
|
|
8216
|
-
return `<div class="lg-ev"><div class="lg-missinghd"><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>No capture — this touched the UI and should have one</div>
|
|
8217
|
-
<div class="lg-manualnote">${esc(note)}</div></div>`;
|
|
8218
|
-
}
|
|
8219
|
-
// kind === 'image' (default)
|
|
8220
|
-
return `<div class="lg-ev"><div class="lg-evhd">Evidence</div><div class="row">${factRow}</div>${img}</div>`;
|
|
8221
|
-
}
|
|
8238
|
+
// (P1, Masa 2026-07-21) saMetaRowHtml() was deleted here — the pre-redesign meta row,
|
|
8239
|
+
// uncalled since the review-card redesign. Its "Run locally" chip only ever opened a
|
|
8240
|
+
// note saying no run command was wired; the card's Open it chip now starts the real one.
|
|
8241
|
+
// (saContextHtml() was deleted here — Masa 2026-07-22. The pre-redesign Context fold:
|
|
8242
|
+
// verbatim quote, 'What we did' / 'What to look at' bullets, sibling split note. Nothing
|
|
8243
|
+
// has called it since the review-card redesign; Request and Result carry that ground now.)
|
|
8222
8244
|
// Unified-diff → GitHub two-gutter model (REVIEW-CARD-SPEC §7 "GitHub diff = Bright"):
|
|
8223
8245
|
// walk the @@ hunk headers to recover the old/new line numbers, emit one row per line
|
|
8224
8246
|
// {o:oldNo|null, n:newNo|null, t:'add'|'del'|'ctx', c:text}. Word-level highlight is
|
|
8225
8247
|
// NOT fabricated from a raw diff (the product never invents evidence) — the Bright
|
|
8226
8248
|
// line+gutter tint carries the change and .gh-hi simply stays absent. Pure, so it is
|
|
8227
8249
|
// extracted and unit-tested in engine/test/review-card-diff.test.mjs.
|
|
8250
|
+
// How many body rows the diff really has, ignoring the display cap. A big change used
|
|
8251
|
+
// to be cut at 80 rows with nothing said (SPEC §7 asks for "…+N 行 · GitHub で開く ↗"
|
|
8252
|
+
// precisely so a reviewer is never shown a slice that looks like the whole thing).
|
|
8253
|
+
// Same skip rules as saUnifiedToGh below — headers and @@ markers are not rows.
|
|
8254
|
+
function saDiffRowCount(diff){
|
|
8255
|
+
let n = 0;
|
|
8256
|
+
for (const raw of String(diff || '').split('\n')) {
|
|
8257
|
+
if (raw === '') continue;
|
|
8258
|
+
if (/^(diff --git|index |--- |\+\+\+ |new file|deleted file|old mode|new mode|similarity|rename |Binary )/.test(raw)) continue;
|
|
8259
|
+
if (/^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(raw)) continue;
|
|
8260
|
+
if (raw.startsWith('\\')) continue;
|
|
8261
|
+
n++;
|
|
8262
|
+
}
|
|
8263
|
+
return n;
|
|
8264
|
+
}
|
|
8228
8265
|
function saUnifiedToGh(diff, max){
|
|
8229
8266
|
const cap = max || 80; const out = []; let o = 0, n = 0;
|
|
8230
8267
|
for (const raw of String(diff || '').split('\n')) {
|
|
@@ -8240,10 +8277,13 @@ function saUnifiedToGh(diff, max){
|
|
|
8240
8277
|
}
|
|
8241
8278
|
return out;
|
|
8242
8279
|
}
|
|
8243
|
-
function saGhDiffHtml(diff){
|
|
8280
|
+
function saGhDiffHtml(diff, pr){
|
|
8244
8281
|
const rows = saUnifiedToGh(diff);
|
|
8245
8282
|
if (!rows.length) return '';
|
|
8246
|
-
|
|
8283
|
+
// Say what was left out. Before this, a 5000-line change rendered its first 80 rows
|
|
8284
|
+
// and stopped — a quiet lie, and the one thing SPEC §7 names as forbidden.
|
|
8285
|
+
const more = Math.max(0, saDiffRowCount(diff) - rows.length);
|
|
8286
|
+
return saGhDiffRowsHtml(rows, more, pr || '');
|
|
8247
8287
|
}
|
|
8248
8288
|
// One Activity transcript row (spec §3): You / tool (mono, bold name) / res (green) /
|
|
8249
8289
|
// Claude. Mirrors the flagship artifact's tx mapping so the pulldown pixel-matches.
|
|
@@ -8253,6 +8293,18 @@ function saTxRowHtml(e){
|
|
|
8253
8293
|
if (e[0] === 'user') return `<div class="txr you"><span class="w">You</span><span class="m">${esc(e[1])}</span></div>`;
|
|
8254
8294
|
return `<div class="txr cl"><span class="w">Claude</span><span class="m">${esc(e[1])}</span></div>`;
|
|
8255
8295
|
}
|
|
8296
|
+
// One line of the frozen implementation log. The worker's own tool calls come through
|
|
8297
|
+
// as "Read app/index.html" / "Bash npm test"; the Manager's own progress notes are
|
|
8298
|
+
// prefixed with ✎ by sendProcessAct() and read as commentary, so they take the quieter
|
|
8299
|
+
// row. Nothing is parsed beyond that split — the line is shown as it was written.
|
|
8300
|
+
function saLogRowHtml(line){
|
|
8301
|
+
const s = String(line ?? '');
|
|
8302
|
+
if (s.startsWith('✎')) return `<div class="txr note"><span class="w"></span><span class="m">${esc(s.replace(/^✎\s*/, ''))}</span></div>`;
|
|
8303
|
+
const m = s.match(/^([A-Z][A-Za-z]*)\s+([\s\S]*)$/);
|
|
8304
|
+
return m
|
|
8305
|
+
? `<div class="txr tool"><span class="w">tool</span><span class="m"><b>${esc(m[1])}</b> ${esc(m[2])}</span></div>`
|
|
8306
|
+
: `<div class="txr cl"><span class="w"></span><span class="m">${esc(s)}</span></div>`;
|
|
8307
|
+
}
|
|
8256
8308
|
// Structured GitHub diff (REVIEW-CARD-SPEC §7): renders {o,n,t,c,h} rows with a
|
|
8257
8309
|
// word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub で開く ↗"
|
|
8258
8310
|
// footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
|
|
@@ -8289,8 +8341,18 @@ function saVideoFigHtml(url){
|
|
|
8289
8341
|
// No fabricated pipeline: an untested goal shows □ Tested, not a green ✓.
|
|
8290
8342
|
function saWorkflowHtml(it){
|
|
8291
8343
|
const ran = Boolean(it.testResult?.ran);
|
|
8292
|
-
|
|
8293
|
-
|
|
8344
|
+
// P1 (Masa 2026-07-21): "Proof captured □" is gone. We no longer go take photos of
|
|
8345
|
+
// the work, so an empty box there was scoring the goal against a step that isn't
|
|
8346
|
+
// part of the process any more — a permanent unchecked box nobody could ever check.
|
|
8347
|
+
// "Deploy" is gone too (Masa 2026-07-22): nothing in the product observes a deploy,
|
|
8348
|
+
// so that box could never be ticked — the same permanent-□ problem as Proof captured.
|
|
8349
|
+
// Merge stays because it IS observed: the PR poller flips the goal to done on merge.
|
|
8350
|
+
//
|
|
8351
|
+
// (PRD §5.5-A/15 asks for the USER'S OWN pipeline — PRD→design→implement→check→PR→
|
|
8352
|
+
// merge→deploy, varying per person. There is no place to record such a pipeline yet,
|
|
8353
|
+
// so inventing one here would be this layer guessing someone's process. Left as a
|
|
8354
|
+
// decision, not faked.)
|
|
8355
|
+
const steps = [['Implemented', 'ok'], ['Tested', ran ? 'ok' : 'todo'], ['In review', 'now'], ['Merge', 'todo']];
|
|
8294
8356
|
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
8357
|
}
|
|
8296
8358
|
function saItemHtml(it, i){
|
|
@@ -8304,15 +8366,9 @@ function saItemHtml(it, i){
|
|
|
8304
8366
|
const d = SA.diffs[it.goalId];
|
|
8305
8367
|
const gtr = it.testResult;
|
|
8306
8368
|
const hasProofImg = it.proofImg && it.proofTaskId != null;
|
|
8307
|
-
//
|
|
8308
|
-
//
|
|
8309
|
-
|
|
8310
|
-
// Diff-box + zoom inset is computed async after first paint (saEnhanceDiffBoxes);
|
|
8311
|
-
// SA.diffBoxHtml caches it so the box survives every later saRender().
|
|
8312
|
-
const boxedImg = SA.diffBoxHtml[it.goalId];
|
|
8313
|
-
const img = hasProofImg
|
|
8314
|
-
? (typeof boxedImg === 'string' ? boxedImg : beforeAfterMediaHtml(it.proofTaskId, { png: it.proofImg, beforePng: it.proofBefore }, esc(it.title ?? '')))
|
|
8315
|
-
: '';
|
|
8369
|
+
// Proof image (P1): the single capture from a check the AI itself promised and ran.
|
|
8370
|
+
// No pair, no red box — those compared against a "before" shot we used to stage.
|
|
8371
|
+
const img = hasProofImg ? proofMediaHtml(it.proofTaskId, { png: it.proofImg }, esc(it.title ?? '')) : '';
|
|
8316
8372
|
// Changed-file paths: d from the diff endpoint ({add,del,files:[{path,add,del}],diff}),
|
|
8317
8373
|
// falling back to task.changedFiles when the server has no diff for this goal.
|
|
8318
8374
|
// A usable git diff has actual files; the endpoint returns an empty {add:0,del:0,
|
|
@@ -8339,9 +8395,9 @@ function saItemHtml(it, i){
|
|
|
8339
8395
|
const kind = saEvidenceKind(filePaths, hasProofImg, gtr);
|
|
8340
8396
|
const gk = it.goalId;
|
|
8341
8397
|
const rs = it.goal?.reviewSummary || {};
|
|
8342
|
-
//
|
|
8343
|
-
//
|
|
8344
|
-
const isConv =
|
|
8398
|
+
// A conversational task (no code change) has no Proof / What changed to show. Its
|
|
8399
|
+
// Result IS the answer (P1) — the old separate Answer section is gone.
|
|
8400
|
+
const isConv = !it.report && !filePaths.length;
|
|
8345
8401
|
// ── meta (spec §5) — fully data-driven / hide-empty: each chip renders only when its
|
|
8346
8402
|
// data exists (no PR → no PR chip · no tests → no Tests · no file totals → no Files ·
|
|
8347
8403
|
// no recording → no Verify). PR = bold green, no underline (a real <a href>). ──
|
|
@@ -8358,11 +8414,13 @@ function saItemHtml(it, i){
|
|
|
8358
8414
|
if (testsLabel) chips.push(`<span class="mx"><span class="mk">Tests</span> <b class="ok">${esc(testsLabel)}</b></span>`);
|
|
8359
8415
|
// Files: hide-empty — only with changed files AND totals (a bare-count chip reads odd).
|
|
8360
8416
|
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
|
|
8362
|
-
//
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8417
|
+
// (The Local chip was deleted here — Masa 2026-07-22. It read reviewSummary.localUrl,
|
|
8418
|
+
// a key parseReviewSummary never emits, so it only ever appeared in demo data; and the
|
|
8419
|
+
// real thing it promised is the Open it section.)
|
|
8420
|
+
// (Open it is NOT a meta chip — Masa 2026-07-22. It was saying the same thing as the
|
|
8421
|
+
// Open it section below, in the PR chip's vocabulary, so the card spoke two languages
|
|
8422
|
+
// about one thing. It lives in the body now, in the same typography as every other
|
|
8423
|
+
// section.)
|
|
8366
8424
|
// Verify = a screen recording of the check (spec §5): NEUTRAL. Opens the file when there
|
|
8367
8425
|
// is a real URL, otherwise scrolls to the in-card player.
|
|
8368
8426
|
if (it.recording) chips.push(it.recordingUrl
|
|
@@ -8371,23 +8429,69 @@ function saItemHtml(it, i){
|
|
|
8371
8429
|
const tokCmp = it.tokenOpt?.basis === 'baseline' ? 'vs your recent average' : 'from prompt caching';
|
|
8372
8430
|
const tokTech = (it.tokenOpt?.techniques ?? []).slice(0, 4).join(' / ');
|
|
8373
8431
|
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
|
-
|
|
8375
|
-
|
|
8432
|
+
// §1.5-3: with Open it moved into the body, a card with no PR / tokens / tests has no
|
|
8433
|
+
// chips at all — render no meta row rather than an empty one holding open a gap.
|
|
8434
|
+
const meta = chips.length ? `<div class="rmeta">${chips.join('')}</div>${metaDetail}` : '';
|
|
8376
8435
|
// ── Request (spec §4): a PLAIN section — the user's own short words (goal.text). Never
|
|
8377
8436
|
// an accordion, never the implementation spec (that lives in Activity below). ──
|
|
8378
8437
|
const reqSec = it.goalText
|
|
8379
8438
|
? `<section class="rsec rreq"><div class="rsec-h">Request</div><p class="rbody rask">${esc(it.goalText)}</p></section>`
|
|
8380
8439
|
: '';
|
|
8440
|
+
// ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
|
|
8441
|
+
// heading + body shape as Request and Result, not a chip in another vocabulary. The
|
|
8442
|
+
// address the worker declared IS the control (press it → the process starts on demand →
|
|
8443
|
+
// it opens); its own note sits under it, and that note is where the status and any
|
|
8444
|
+
// failure reason land after the press. Nothing declared → no section (§1.5-3). ──
|
|
8445
|
+
const openBody = it.run
|
|
8446
|
+
? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}</p></div>`
|
|
8447
|
+
: '';
|
|
8448
|
+
// ── Also carries (Masa 2026-07-21): goals folded onto this review are logged HERE, inside
|
|
8449
|
+
// the review's own content — not as loose "→ #12 にまとめました" rows in the Review column.
|
|
8450
|
+
// Chrome label stays English (§377-7); the request titles keep whatever language they were
|
|
8451
|
+
// written in. Only rendered when something was actually folded in (§1.5-3 no empty box). ──
|
|
8452
|
+
const carriesBody = it.folded?.length
|
|
8453
|
+
? `<ul class="rcheck">${it.folded.map((f) => `<li>#${esc(String(f.id))} ${esc(f.title)}</li>`).join('')}</ul>`
|
|
8454
|
+
: '';
|
|
8381
8455
|
// ── What to check (spec §2.5): a bulleted list when several, a line when one. ──
|
|
8382
8456
|
// Only an explicit review-summary check renders here — never the goal text echoed back
|
|
8383
8457
|
// (it.check falls back to goal.text via checkLine, which would fabricate a "What to
|
|
8384
8458
|
// check" for a conversational task that has none). §0.5 no placeholder.
|
|
8385
|
-
|
|
8459
|
+
// The working AI's own sentence wins (P1, Masa 2026-07-22): it declares what to look
|
|
8460
|
+
// at in the same file it declares how to run the thing. reviewSummary.check below is
|
|
8461
|
+
// the intake AI's invention and only fills in for goals that predate the declaration.
|
|
8462
|
+
const checkArr = it.run?.check ? [it.run.check]
|
|
8463
|
+
: Array.isArray(rs.checkList) ? rs.checkList.filter(Boolean)
|
|
8386
8464
|
: Array.isArray(rs.check) ? rs.check.filter(Boolean)
|
|
8387
8465
|
: (typeof rs.check === 'string' && rs.check.trim() ? [rs.check.trim()] : []);
|
|
8388
8466
|
const checkBody = checkArr.length > 1
|
|
8389
8467
|
? `<ul class="rcheck">${checkArr.map((c) => `<li>${esc(c)}</li>`).join('')}</ul>`
|
|
8390
8468
|
: (checkArr.length ? `<p class="rbody rcheck1">${esc(checkArr[0])}</p>` : '');
|
|
8469
|
+
// ── Steps (P0b, Masa 2026-07-21): the AI's own TodoWrite breakdown — distinct from
|
|
8470
|
+
// "What to check" (the human's pass condition) and Activity (the raw tool-call log).
|
|
8471
|
+
// Reuses the same .todos/.todo vocabulary as the chat feed / board card so the ✓/▸/○
|
|
8472
|
+
// language is consistent everywhere it appears. Empty → no section (§1.5-3).
|
|
8473
|
+
const stepsBody = it.todosFinal?.length ? `<div class="todos">${todoRowsHtml(it.todosFinal)}</div>` : '';
|
|
8474
|
+
// ── Result (P1, Masa 2026-07-21): the AI's own report, rendered as written and read
|
|
8475
|
+
// whole (no 160-char squeeze, no summariser of ours in the way). This is the one
|
|
8476
|
+
// section that carries every kind of ask — a研究 report reads here as prose, a UI
|
|
8477
|
+
// change explains itself here, and an image the AI pasted shows up here as an image.
|
|
8478
|
+
// We interpret nothing; mdLite only turns Markdown into the shapes it already means. ──
|
|
8479
|
+
const reportBody = it.report ? `<div class="rreport">${mdLite(it.report, { images: false })}</div>` : '';
|
|
8480
|
+
// ── Screenshots (P2, Masa 2026-07-22): the pictures the AI took of its own result.
|
|
8481
|
+
// Full width, at their own aspect — a four-variant comparison sheet is unreadable
|
|
8482
|
+
// shrunk into a thumbnail, and that is the shape the AI actually produces. The file
|
|
8483
|
+
// name it chose is the caption (it names them variant-1-linear.png / compare.png), so
|
|
8484
|
+
// it labels its own work; we add nothing. ──
|
|
8485
|
+
// A before/after pair is two shots the worker named as such — it decides whether the
|
|
8486
|
+
// change had a "before" worth shooting, we only recognise the naming. Anything else is
|
|
8487
|
+
// shown as the plain list.
|
|
8488
|
+
const shotPair = saShotPair(it.shots, it.run?.compare);
|
|
8489
|
+
const boxed = shotPair ? SA.shotBox[it.goalId] : null;
|
|
8490
|
+
const shotsBody = shotPair
|
|
8491
|
+
? `<div class="rshotpair" data-shotpair="${it.goalId}">${typeof boxed === 'string' ? boxed : saPlainPairHtml(shotPair)}</div>`
|
|
8492
|
+
: it.shots?.length
|
|
8493
|
+
? 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('')
|
|
8494
|
+
: '';
|
|
8391
8495
|
// ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
|
|
8392
8496
|
// run-log for logic-only tests; dropped entirely for a conversational task. ──
|
|
8393
8497
|
let proofInner = '';
|
|
@@ -8400,14 +8504,17 @@ function saItemHtml(it, i){
|
|
|
8400
8504
|
// peeks then "…+N 行 · GitHub で開く ↗" for a huge change (no GitHub link when there's no
|
|
8401
8505
|
// PR). Structured recorded rows (ghRows) win over a git/client unified diff. Dropped
|
|
8402
8506
|
// entirely for a conversational task. ──
|
|
8403
|
-
|
|
8507
|
+
// No prose here (P1, Masa 2026-07-22): reviewSummary.changed was the intake AI writing
|
|
8508
|
+
// its own summary of the change, and on a real goal it came out as the request echoed
|
|
8509
|
+
// back. What changed is now only what actually changed — files and the diff.
|
|
8510
|
+
const changedDescHtml = '';
|
|
8404
8511
|
const filesListHtml = it.metaFiles
|
|
8405
8512
|
? `<div class="rfiles">${it.metaFiles.map((f) => esc(f)).join('<br>')}</div>`
|
|
8406
8513
|
: (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
8514
|
: (clientDiff && filePaths.length) ? `<div class="rfiles">${filePaths.map((p) => esc(p)).join('<br>')}</div>` : '';
|
|
8408
8515
|
const diffHtml = it.ghRows
|
|
8409
8516
|
? saGhDiffRowsHtml(it.ghRows, it.diffTotal || 0, it.pr || '')
|
|
8410
|
-
: hasGitDiff ? saGhDiffHtml(d.diff) : clientDiff ? saGhDiffHtml(clientDiff) : '';
|
|
8517
|
+
: hasGitDiff ? saGhDiffHtml(d.diff, it.pr) : clientDiff ? saGhDiffHtml(clientDiff, it.pr) : '';
|
|
8411
8518
|
const changedBody = isConv ? '' : (changedDescHtml + filesListHtml + diffHtml);
|
|
8412
8519
|
// ── sensitive-area note (real: classifyChangeRisk forced the pause) ──
|
|
8413
8520
|
const risk = it.risk;
|
|
@@ -8419,31 +8526,50 @@ function saItemHtml(it, i){
|
|
|
8419
8526
|
// below, offset-indented. The review Discussion folds in at the bottom of the body. The
|
|
8420
8527
|
// transcript is the recorded build (activity); real goals fall back to "what we did".
|
|
8421
8528
|
// Persisted open-state via data-detkey so SSE re-renders don't collapse it. ──
|
|
8529
|
+
// The "what we did" fallback is the same report as Result above, cut to 160 chars —
|
|
8530
|
+
// showing both makes the card repeat itself in a worse form (P1). Only fall back when
|
|
8531
|
+
// there is no Result to read.
|
|
8422
8532
|
const txArr = (it.activity && it.activity.length)
|
|
8423
8533
|
? it.activity.map((e) => saTxRowHtml(e))
|
|
8424
|
-
: (it.did?.length ? it.did.map((s) => saTxRowHtml(['claude', s])) : []);
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8534
|
+
: ((!reportBody && it.did?.length) ? it.did.map((s) => saTxRowHtml(['claude', s])) : []);
|
|
8535
|
+
// (Discussion was deleted here — Masa 2026-07-22. It read reviewSummary.discussion,
|
|
8536
|
+
// which parseReviewSummary never emits, so it never rendered on a real goal; replies
|
|
8537
|
+
// already live in the card's own reply box.)
|
|
8538
|
+
const threadHtml = '';
|
|
8539
|
+
// ── Activity = the implementation log (P1.5, Masa 2026-07-22): what the AI actually
|
|
8540
|
+
// did, read AFTER it finished. Fetched only when the section is opened — a task's log
|
|
8541
|
+
// runs to ~500KB and most cards are never expanded — so the closed state is one line
|
|
8542
|
+
// and the lines arrive on the first open (same "only when asked" shape as Open it).
|
|
8543
|
+
// Once fetched they live in SA.actLog so an SSE rebuild doesn't wipe them.
|
|
8544
|
+
const logLines = it.logTaskId != null ? SA.actLog[it.logTaskId] : null;
|
|
8545
|
+
const logHtml = Array.isArray(logLines)
|
|
8546
|
+
? (logLines.length
|
|
8547
|
+
? logLines.map((l) => saLogRowHtml(l)).join('')
|
|
8548
|
+
: '<div class="txr res"><span class="w"></span><span class="m">Nothing was logged for this one.</span></div>')
|
|
8549
|
+
: '<div class="txr res"><span class="w"></span><span class="m">Loading…</span></div>';
|
|
8429
8550
|
const actOpen = SA.detOpen.has(`${gk}:act`) ? ' open' : '';
|
|
8430
8551
|
const txFirst = txArr[0] || '<div class="txr"><span class="w"></span><span class="m">Activity</span></div>';
|
|
8431
8552
|
const txRest = txArr.slice(1).join('');
|
|
8432
8553
|
const discInner = threadHtml ? `<div class="rsub">Discussion</div><div class="rthread">${threadHtml}</div>` : '';
|
|
8433
8554
|
const activitySec = (txArr.length || threadHtml)
|
|
8434
8555
|
? `<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
|
-
:
|
|
8556
|
+
: it.logTaskId != null
|
|
8557
|
+
? `<section class="rsec ract"><div class="rsec-h">Activity</div><details class="lacc" data-detkey="${gk}:act" data-actlog="${it.logTaskId}"${actOpen}><summary><i class="ltog">›</i><div class="rtx lfirst">${saTxRowHtml(['user', it.goalText || it.title])}</div></summary><div class="lbody"><div class="rtx" data-actbody="${it.logTaskId}">${logHtml}</div>${discInner}</div></details></section>`
|
|
8558
|
+
: '';
|
|
8436
8559
|
const S = (label, body) => body ? `<section class="rsec"><div class="rsec-h">${label}</div>${body}</section>` : '';
|
|
8437
8560
|
return `<div class="sa-item lg redesign${it.goalId === SA.cur ? ' cur' : ''}" data-gid="${it.goalId}">
|
|
8438
8561
|
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span><span class="rst"><span class="rdot"></span>In review</span></div>
|
|
8439
8562
|
<h2 class="rtitle">${esc(it.title)}</h2>
|
|
8440
8563
|
${meta}
|
|
8441
8564
|
${reqSec}
|
|
8565
|
+
${S('Open it', openBody)}
|
|
8566
|
+
${S('Also carries', carriesBody)}
|
|
8442
8567
|
${S('What to check', checkBody)}
|
|
8443
|
-
${S('
|
|
8568
|
+
${S('Result', reportBody)}
|
|
8569
|
+
${S('Screenshots', shotsBody)}
|
|
8570
|
+
${S('Steps', stepsBody)}
|
|
8444
8571
|
${S('Proof', proofInner)}
|
|
8445
8572
|
${S('What changed', changedBody)}
|
|
8446
|
-
${S('Not done', it.risk?.summary ? `<p class="rbody rrisk">${esc(it.risk.summary)}</p>` : '')}
|
|
8447
8573
|
${riskHtml}
|
|
8448
8574
|
${activitySec}
|
|
8449
8575
|
${S('Workflow', saWorkflowHtml(it))}
|
|
@@ -8458,12 +8584,45 @@ function saScrollCardToTop(el){
|
|
|
8458
8584
|
if (!body || !el) return;
|
|
8459
8585
|
body.scrollTop += el.getBoundingClientRect().top - body.getBoundingClientRect().top;
|
|
8460
8586
|
}
|
|
8587
|
+
// Fetch one task's frozen implementation log and paint it into the open card. Cached in
|
|
8588
|
+
// SA.actLog (including an empty result) so a card can be opened and closed without
|
|
8589
|
+
// asking twice. A failure writes an honest line rather than an empty box — the log
|
|
8590
|
+
// either exists or it doesn't, and we say which.
|
|
8591
|
+
async function saLoadActivityLog(taskId){
|
|
8592
|
+
if (!Number.isFinite(taskId) || SA.actLog[taskId] !== undefined) return;
|
|
8593
|
+
SA.actLog[taskId] = null; // in flight — keeps a second toggle from re-fetching
|
|
8594
|
+
let lines = [];
|
|
8595
|
+
try {
|
|
8596
|
+
const r = await fetch(withKey(`/api/tasks/${taskId}/activity`));
|
|
8597
|
+
lines = r.ok ? ((await r.json()).lines ?? []) : [];
|
|
8598
|
+
} catch { lines = []; }
|
|
8599
|
+
SA.actLog[taskId] = lines;
|
|
8600
|
+
const body = document.querySelector(`#seeall [data-actbody="${taskId}"]`);
|
|
8601
|
+
if (!body) return;
|
|
8602
|
+
body.innerHTML = lines.length
|
|
8603
|
+
? lines.map((l) => saLogRowHtml(l)).join('')
|
|
8604
|
+
: '<div class="txr res"><span class="w"></span><span class="m">Nothing was logged for this one.</span></div>';
|
|
8605
|
+
}
|
|
8606
|
+
// After first paint, compute the red box for any card showing a before/after pair and
|
|
8607
|
+
// patch that card's node directly — a full re-render here would close every open
|
|
8608
|
+
// pulldown just to draw a box. Cached per goal (null included) so it runs once.
|
|
8609
|
+
async function saEnhanceShotBoxes(){
|
|
8610
|
+
const targets = SA.items.filter((it) => saShotPair(it.shots, it.run?.compare) && SA.shotBox[it.goalId] === undefined);
|
|
8611
|
+
await Promise.all(targets.map(async (it) => {
|
|
8612
|
+
const pair = saShotPair(it.shots, it.run?.compare);
|
|
8613
|
+
let html = null;
|
|
8614
|
+
try { html = await shotsBoxedHtml(pair.before, pair.after); } catch { html = null; }
|
|
8615
|
+
SA.shotBox[it.goalId] = html;
|
|
8616
|
+
if (!html || !SA.open) return;
|
|
8617
|
+
const host = document.querySelector(`#seeall [data-shotpair="${it.goalId}"]`);
|
|
8618
|
+
if (host) host.innerHTML = html;
|
|
8619
|
+
}));
|
|
8620
|
+
}
|
|
8461
8621
|
function saRender(){
|
|
8462
8622
|
const S = $('seeall'); if (!S) return;
|
|
8463
8623
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
8464
8624
|
const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
|
|
8465
|
-
const
|
|
8466
|
-
const c = `${pending.length} ready · ${green === pending.length ? 'all green' : `${pending.length - green} failing`} · ${proofN} with proof`;
|
|
8625
|
+
const c = `${pending.length} ready · ${green === pending.length ? 'all green' : `${pending.length - green} failing`}`;
|
|
8467
8626
|
const curIdx = pending.findIndex((x) => x.goalId === SA.cur);
|
|
8468
8627
|
const posLabel = pending.length && curIdx >= 0 ? `${curIdx + 1} / ${pending.length}` : '';
|
|
8469
8628
|
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>
|
|
@@ -8475,7 +8634,14 @@ function saRender(){
|
|
|
8475
8634
|
det.addEventListener('toggle', () => {
|
|
8476
8635
|
const k = det.dataset.detkey;
|
|
8477
8636
|
if (det.open) SA.detOpen.add(k); else SA.detOpen.delete(k);
|
|
8637
|
+
// Opening Activity is what fetches the implementation log (Masa 2026-07-22:
|
|
8638
|
+
// "開いた時だけ取りに行く"). Closed cards cost nothing; a reopened one is served
|
|
8639
|
+
// from SA.actLog.
|
|
8640
|
+
if (det.open && det.dataset.actlog) saLoadActivityLog(Number(det.dataset.actlog));
|
|
8478
8641
|
});
|
|
8642
|
+
// A card re-rendered while its Activity was already open (SSE tick) keeps the log
|
|
8643
|
+
// it had — the fetch below no-ops once SA.actLog holds the answer.
|
|
8644
|
+
if (det.open && det.dataset.actlog) saLoadActivityLog(Number(det.dataset.actlog));
|
|
8479
8645
|
}
|
|
8480
8646
|
// "ないものは載せない" (Masa): a Proof <img> whose file is missing/broken must never
|
|
8481
8647
|
// show a broken-image placeholder. proofImg is only a stored filename, so a file that
|
|
@@ -8528,25 +8694,6 @@ async function saFetchDiffs(){
|
|
|
8528
8694
|
}));
|
|
8529
8695
|
if (SA.open) saSync();
|
|
8530
8696
|
}
|
|
8531
|
-
// Diff-box + zoom inset (HANDOFF-review-card-diffbox-2026-07-18): unlike
|
|
8532
|
-
// saFetchDiffs above (whose fetch settling triggers a full saSync()/saRender()), this
|
|
8533
|
-
// patches each card's already-rendered .beforeafter node directly — a full re-render
|
|
8534
|
-
// here would reset scroll, close every open <details>, and drop an in-progress reply
|
|
8535
|
-
// keystroke, all just to draw a box around one screenshot. SA.diffBoxHtml is still
|
|
8536
|
-
// cached per goal, so if a later SSE-driven saSync() does rebuild the card anyway,
|
|
8537
|
-
// saItemHtml() picks the cached result back up with no flicker to the plain pair.
|
|
8538
|
-
async function saEnhanceDiffBoxes(){
|
|
8539
|
-
const targets = SA.items.filter((it) => it.proofImg && it.proofTaskId != null && it.proofBefore && SA.diffBoxHtml[it.goalId] === undefined);
|
|
8540
|
-
await Promise.all(targets.map(async (it) => {
|
|
8541
|
-
let html = null;
|
|
8542
|
-
try { html = await beforeAfterBoxedHtml(it.proofTaskId, { png: it.proofImg, beforePng: it.proofBefore }, esc(it.title ?? '')); }
|
|
8543
|
-
catch { html = null; } // a decode/pixel-diff failure is a fallback, never a crash
|
|
8544
|
-
SA.diffBoxHtml[it.goalId] = html; // cache even on null — never re-attempt an already-answered "no box"
|
|
8545
|
-
if (!html || !SA.open) return;
|
|
8546
|
-
const host = document.querySelector(`.sa-item[data-gid="${it.goalId}"] .beforeafter`);
|
|
8547
|
-
if (host) host.outerHTML = html; // html itself opens with the replacement .beforeafter
|
|
8548
|
-
}));
|
|
8549
|
-
}
|
|
8550
8697
|
function saOpen(focusGoalId){
|
|
8551
8698
|
SA.open = true;
|
|
8552
8699
|
SA.items = saRows().map((r) => ({ ...r, st: 0 }));
|
|
@@ -8576,7 +8723,7 @@ function saOpen(focusGoalId){
|
|
|
8576
8723
|
if (el) saScrollCardToTop(el);
|
|
8577
8724
|
});
|
|
8578
8725
|
saFetchDiffs();
|
|
8579
|
-
|
|
8726
|
+
saEnhanceShotBoxes();
|
|
8580
8727
|
}
|
|
8581
8728
|
function saClose(){ SA.open = false; SA.focusGid = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } $('seeall').classList.remove('open'); }
|
|
8582
8729
|
// Continuous review (Masa 2026-07-08): move a persistent "current" card with
|
|
@@ -8635,10 +8782,32 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
8635
8782
|
const it = SA.items[i];
|
|
8636
8783
|
if (k === 'close') return saClose();
|
|
8637
8784
|
if (!it) return;
|
|
8638
|
-
// meta-row
|
|
8639
|
-
// Run-locally to reveal the honest "not wired yet" note — neither performs an action.
|
|
8785
|
+
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
8640
8786
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
8641
|
-
|
|
8787
|
+
// Open it (P1, Masa 2026-07-21): start the thing the worker said to run, then open it.
|
|
8788
|
+
// Booting a real process takes seconds, so the chip says "Starting…" while it waits —
|
|
8789
|
+
// and a failure lands in the detail line as a plain reason. It never turns the goal
|
|
8790
|
+
// red: failing to boot a preview says nothing about whether the work itself is good.
|
|
8791
|
+
if (k === 'preview') {
|
|
8792
|
+
if (!it.run?.taskId) return;
|
|
8793
|
+
const sec = $('seeall').querySelector(`#runlocal${i}`);
|
|
8794
|
+
const link = $('seeall').querySelector(`#prevlink${i}`);
|
|
8795
|
+
const v = sec?.querySelector('.v');
|
|
8796
|
+
if (link) link.disabled = true;
|
|
8797
|
+
if (v) v.textContent = 'Starting it…';
|
|
8798
|
+
let r = null, body = {};
|
|
8799
|
+
try { r = await fetch(`/api/tasks/${it.run.taskId}/preview`, { method: 'POST' }); body = await r.json().catch(() => ({})); }
|
|
8800
|
+
catch (e) { body = { error: String(e.message ?? e) }; }
|
|
8801
|
+
if (link) link.disabled = false;
|
|
8802
|
+
const noteText = it.run.note || it.run.cmd || 'Starts when you press it.';
|
|
8803
|
+
// A tab opened: that IS the feedback. Put the worker's own note back rather than
|
|
8804
|
+
// narrating what the reviewer just watched happen.
|
|
8805
|
+
if (r?.ok && body.url) { window.open(body.url, '_blank', 'noopener'); if (v) v.textContent = noteText; return; }
|
|
8806
|
+
// Started, but the worker declared no URL: its own note is where it said to look.
|
|
8807
|
+
if (r?.ok) { if (v) v.textContent = it.run.note || it.run.cmd || 'Started — nothing to open.'; return; }
|
|
8808
|
+
if (v) v.textContent = `Could not start it: ${body.error || `HTTP ${r?.status ?? 'error'}`}`;
|
|
8809
|
+
return;
|
|
8810
|
+
}
|
|
8642
8811
|
// Verify chip (no external recording URL) → scroll the in-card video player into view.
|
|
8643
8812
|
if (k === 'rec') { $('seeall').querySelector(`.sa-item[data-gid="${it.goalId}"] .rvid`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); return; }
|
|
8644
8813
|
// every verdict auto-advances the focus to the next pending card (tempo).
|