@galda/cli 0.10.69 → 0.10.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +440 -149
- package/app/review-merge-celebration-preview.html +21 -0
- package/engine/chrome-safety.mjs +9 -0
- package/engine/lib.mjs +575 -2
- package/engine/server.mjs +497 -145
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -297,9 +297,8 @@
|
|
|
297
297
|
.ctxseg svg{width:13px;height:13px;flex:0 0 13px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
298
298
|
.ctxseg b{font-weight:600;font-size:11.5px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;max-width:300px}
|
|
299
299
|
.ctxdiv{width:1px;align-self:stretch;margin:5px 0;background:var(--hair);flex:0 0 1px}
|
|
300
|
-
/*
|
|
301
|
-
|
|
302
|
-
dialog. ON is --ink, never --green: in this product green means "it passed" (PR green,
|
|
300
|
+
/* workspace mode — isolated worktree is the default; direct workspace is explicit.
|
|
301
|
+
ON is --ink, never --green: in this product green means "it passed" (PR green,
|
|
303
302
|
tests green, Token green), and a settings checkbox borrowing it makes that green cheap.
|
|
304
303
|
The tick is knocked out in --panel (the surface it sits on), NOT --invink. */
|
|
305
304
|
.ctxwt{font-size:11.5px;color:var(--ink2)}
|
|
@@ -309,6 +308,9 @@
|
|
|
309
308
|
.ctxwt[aria-checked="true"] .ctxbox{background:var(--ink);border-color:var(--ink)}
|
|
310
309
|
.ctxwt[aria-checked="true"] .ctxbox::after{content:"";position:absolute;left:3px;top:0;width:3px;height:7px;
|
|
311
310
|
border:solid var(--panel);border-width:0 1.6px 1.6px 0;transform:rotate(42deg)}
|
|
311
|
+
.ctxworkwarn{display:flex;align-items:center;gap:6px;margin:-3px 2px 6px;color:var(--amber);
|
|
312
|
+
font:500 11.5px/1.35 var(--ui)}
|
|
313
|
+
.ctxworkwarn[hidden]{display:none}
|
|
312
314
|
.ctxmenu{min-width:230px;max-width:420px}
|
|
313
315
|
/* The folder list is galda's own screen (Claude has none — it opens ONE folder; galda
|
|
314
316
|
moves between repos). The one thing that actually goes wrong is that names alone look
|
|
@@ -568,6 +570,8 @@
|
|
|
568
570
|
.blockreason{color:var(--amber)}
|
|
569
571
|
.rvcheck2{font-size:15px;font-weight:600;color:var(--ink);line-height:1.5;margin-bottom:4px}
|
|
570
572
|
.rvchanged{font-size:12.5px;color:var(--ink2);line-height:1.5;margin-bottom:6px}
|
|
573
|
+
.rvinfra{font-size:12.5px;color:var(--ink2);line-height:1.5;margin:8px 0 6px;padding:8px 10px;border:1px solid rgba(var(--amber-rgb),.35);border-radius:8px;background:rgba(var(--amber-rgb),.06)}
|
|
574
|
+
.rvinfra .retrybtn{margin-left:8px}
|
|
571
575
|
.rvtokenopt{font-family:var(--mono);font-size:10.5px;font-weight:700;color:var(--green);line-height:1.45;margin:2px 0 7px}
|
|
572
576
|
.rvlbl{display:inline-block;font-family:var(--mono);font-size:9px;letter-spacing:.06em;color:var(--ink3);background:var(--hair2);border-radius:4px;padding:1px 6px;margin-right:7px;vertical-align:1px}
|
|
573
577
|
/* goal 427 §4: change-risk section on the review card. §5.5/§1.5-8: an
|
|
@@ -721,9 +725,19 @@
|
|
|
721
725
|
.review-fly .name.flystrike{text-decoration:line-through;text-decoration-color:var(--green);
|
|
722
726
|
opacity:.7;transition:opacity .25s ease}
|
|
723
727
|
|
|
724
|
-
/*
|
|
725
|
-
|
|
728
|
+
/* Approve celebration: bright Figma-like confetti plus a short, explicit
|
|
729
|
+
completion state inside the Review dialog before it closes. */
|
|
726
730
|
.confetti-piece{position:fixed;pointer-events:none;z-index:9999;will-change:transform,opacity}
|
|
731
|
+
.sa-finish{position:fixed;z-index:10000;display:grid;place-items:center;pointer-events:none;overflow:hidden;border-radius:16px;
|
|
732
|
+
background:color-mix(in srgb,var(--canvas) 20%,transparent);backdrop-filter:blur(2px);animation:saFinishFade .9s ease both}
|
|
733
|
+
.sa-finish-mark{width:104px;height:104px;border-radius:50%;display:grid;place-items:center;background:#18c77a;
|
|
734
|
+
box-shadow:0 18px 52px rgba(24,199,122,.34);animation:saFinishPop .72s cubic-bezier(.2,1.35,.35,1) both}
|
|
735
|
+
.sa-finish-mark svg{width:56px;height:56px;fill:none;stroke:#fff;stroke-width:3.4;stroke-linecap:round;stroke-linejoin:round}
|
|
736
|
+
.sa-finish-mark path{stroke-dasharray:70;stroke-dashoffset:70;animation:saFinishDraw .42s ease .18s forwards}
|
|
737
|
+
@keyframes saFinishPop{0%{transform:scale(.45);opacity:0}62%{transform:scale(1.1);opacity:1}100%{transform:scale(1)}}
|
|
738
|
+
@keyframes saFinishDraw{to{stroke-dashoffset:0}}
|
|
739
|
+
@keyframes saFinishFade{0%,72%{opacity:1}100%{opacity:0}}
|
|
740
|
+
@media (prefers-reduced-motion:reduce){.sa-finish,.sa-finish-mark,.sa-finish-mark path{animation:none}.sa-finish{background:transparent}}
|
|
727
741
|
|
|
728
742
|
/* connect modal */
|
|
729
743
|
/* A confirm dialog (deleteConfirmOverlay etc.) must always sit above other
|
|
@@ -1095,6 +1109,7 @@
|
|
|
1095
1109
|
/* 3列カンバン: chat and the task board share the width evenly beside projects */
|
|
1096
1110
|
body[data-layout="kanban"] .chat{flex:1 1 0}
|
|
1097
1111
|
body[data-layout="kanban"] .side{flex:1 1 0;width:auto}
|
|
1112
|
+
body[data-layout="kanban"] .side.collapsed{flex:0 0 0;width:0!important}
|
|
1098
1113
|
/* B · Center-flow — the final layout. Center = To Do (#tasklist) + composer
|
|
1099
1114
|
under it; right = Review (top) + Implementing (bottom). Populated by moving
|
|
1100
1115
|
existing nodes (positionLayout) so all event wiring survives. */
|
|
@@ -1758,10 +1773,9 @@
|
|
|
1758
1773
|
/* ---- + New (CDO decision 5, Masa 2026-07-17): a quiet add-row where + sits in the ● column
|
|
1759
1774
|
and New in the text column — aligned to the row's own grid, not a box shoved right. The
|
|
1760
1775
|
shared .paplus glyph is 13px (the same span as the ● dot), with the stroke re-centred inside
|
|
1761
|
-
that box so the GLYPH lines up, not the box (Masa aesthetic: align glyphs, not boxes).
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
into Done/Review/Doing. ---- */
|
|
1776
|
+
that box so the GLYPH lines up, not the box (Masa aesthetic: align glyphs, not boxes). The
|
|
1777
|
+
bottom List row focuses the composer; the Later row below the Later caption posts pending:true
|
|
1778
|
+
directly so it shelves work without spawning a worker. ---- */
|
|
1765
1779
|
#fsRoot .paplus{width:13px;height:13px;flex:0 0 13px;position:relative;display:inline-block}
|
|
1766
1780
|
#fsRoot .paplus::before,#fsRoot .paplus::after{content:"";position:absolute;background:currentColor;border-radius:1px}
|
|
1767
1781
|
#fsRoot .paplus::before{left:5.85px;top:2px;width:1.3px;height:9px}
|
|
@@ -1769,6 +1783,13 @@
|
|
|
1769
1783
|
/* list rail add-row — padding-left:8px puts + on the .trow ● (8px pad + 13px ● + 9px gap grid) */
|
|
1770
1784
|
#fsRoot .qadd{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:9px;width:100%;padding:6px 4px 6px 8px;border-radius:8px;color:var(--ink3);font-size:11.5px;cursor:pointer;opacity:.45;transition:opacity .15s,color .15s}
|
|
1771
1785
|
#fsRoot .qadd:hover{opacity:1;color:var(--ink)}
|
|
1786
|
+
#fsRoot .qadd.editing{opacity:1;color:var(--ink);cursor:text}
|
|
1787
|
+
#fsRoot .qadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
|
|
1788
|
+
#fsRoot .qadd input::placeholder{color:var(--ink3)}
|
|
1789
|
+
#fsRoot .lateridle{display:flex;align-items:center;gap:9px;margin:5px 0 2px;padding:9px 10px;border:1px solid var(--hair);border-radius:8px;color:var(--ink2);font-size:10.5px;line-height:1.45}
|
|
1790
|
+
#fsRoot .lateridle .msg{flex:1;min-width:0}
|
|
1791
|
+
#fsRoot .lateridle button{height:26px;padding:0 10px;border:1px solid var(--hair2);border-radius:7px;background:var(--panel);color:var(--ink);font:600 10px/1 var(--mono);cursor:pointer;white-space:nowrap}
|
|
1792
|
+
#fsRoot .lateridle button:hover{border-color:var(--ink3)}
|
|
1772
1793
|
/* board To Do column add-row — 2px body pad + 10px card pad = 12px lines + up with the card's #NN */
|
|
1773
1794
|
#fsRoot .fsbadd{margin:5px 2px 0;padding:6px 10px;border-radius:7px;border:0;background:none;color:var(--ink3);font:400 11.5px/1 var(--ui);text-align:left;cursor:pointer;display:flex;align-items:center;gap:9px;opacity:.45;transition:opacity .15s,color .15s}
|
|
1774
1795
|
#fsRoot .fsbadd:hover{opacity:1;color:var(--ink2)}
|
|
@@ -1802,75 +1823,41 @@
|
|
|
1802
1823
|
#fsRoot .sm2 .m-bar{height:3px;border-radius:2px;background:var(--hair);margin:8px 0 6px;overflow:hidden}
|
|
1803
1824
|
#fsRoot .sm2 .m-bar i{display:block;height:100%;background:var(--green)}
|
|
1804
1825
|
#fsRoot .sm2 .s{font-size:10.5px;color:var(--ink3)}
|
|
1805
|
-
/* Review header
|
|
1826
|
+
/* Review header. Review cards use one stable, screenshot-free row design. */
|
|
1806
1827
|
#fsRoot .revhd{display:flex;align-items:baseline;gap:8px;margin-bottom:11px}
|
|
1807
1828
|
#fsRoot .revhd[hidden]{display:none}
|
|
1808
1829
|
#fsRoot .revhd .hd{font-size:13.5px;letter-spacing:-.02em}
|
|
1809
1830
|
#fsRoot .revhd .rnum{font-size:10px;color:var(--ink3)}
|
|
1810
|
-
#fsRoot .rvbtns{margin-left:auto;display:flex;gap:2px}
|
|
1811
|
-
#fsRoot .rvb{width:22px;height:20px;border:0;border-radius:6px;background:transparent;color:var(--ink3);cursor:pointer;display:grid;place-items:center;opacity:.75}
|
|
1812
|
-
#fsRoot .rvb svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
1813
|
-
#fsRoot .rvb:hover{background:transparent;color:var(--ink)} /* icon hover = glyph colour only (§5.5) */
|
|
1814
|
-
#fsRoot .rvb.on{background:var(--hover);color:var(--ink);opacity:1}
|
|
1815
1831
|
/* V3-C (Masa決定 2026-07-07): the collapsed "N ready to review" strip is REMOVED —
|
|
1816
1832
|
it duplicated the sm2 meter directly above. Reviews render as meter + header + cards;
|
|
1817
1833
|
zero reviews render nothing at all (§1.5-3). */
|
|
1818
1834
|
#fsRoot .revlist{display:block}
|
|
1819
|
-
/*
|
|
1820
|
-
|
|
1821
|
-
#fsRoot .rcard
|
|
1835
|
+
/* Approved B Status dot: borderless, title-only Review rows. Details and proof
|
|
1836
|
+
stay in the Review dialog, where they can be inspected without bloating the board. */
|
|
1837
|
+
#fsRoot .rcard{position:relative;min-height:31px;margin-top:1px;border:0;border-radius:0;padding:5px 4px 5px 15px;display:flex;gap:7px;align-items:center;background:transparent;box-shadow:none;cursor:pointer}
|
|
1838
|
+
#fsRoot .rcard::before{content:"";position:absolute;left:4px;top:50%;width:5px;height:5px;border-radius:50%;background:var(--green);transform:translateY(-50%)}
|
|
1839
|
+
#fsRoot .rcard img{display:none}
|
|
1822
1840
|
#fsRoot .rcard .rc{flex:1;min-width:0}
|
|
1823
|
-
#fsRoot .rcard .t{font-size:
|
|
1841
|
+
#fsRoot .rcard .t{font-size:11.5px;font-weight:500;line-height:1.35;color:color-mix(in srgb,var(--ink) 84%,var(--panel));white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1824
1842
|
#fsRoot .rcard .pr{font-family:var(--mono);font-size:9.5px;color:var(--green);background:rgba(var(--green-rgb),.12);border-radius:5px;padding:2px 7px;font-weight:600;margin-top:3px;display:inline-block;text-decoration:none}
|
|
1843
|
+
#fsRoot .rcard .rstat{font-family:var(--mono);font-size:9.5px;color:var(--ink3);letter-spacing:.04em;text-transform:uppercase;margin-top:3px;display:inline-block}
|
|
1844
|
+
#fsRoot .rcard .rstat.conflict{color:var(--amber)}
|
|
1845
|
+
#fsRoot .rcard .pr,#fsRoot .rcard .rstat,#fsRoot .rcard .rnote{display:none}
|
|
1846
|
+
#fsRoot .rcard.foldrow{border-style:dashed;background:transparent}
|
|
1847
|
+
#fsRoot .rcard.foldrow .rstat{color:var(--ink2)}
|
|
1825
1848
|
/* review row number — #NN PREFIX on the title (HANDOFF-v45 §11), same form as the To Do
|
|
1826
1849
|
rows (PRD §5: numbering matches the sidebar) */
|
|
1827
1850
|
#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}
|
|
1828
|
-
/*
|
|
1829
|
-
"merged into #12" on the folded card) no longer renders in the Review list at all —
|
|
1830
|
-
it read as the same report-in-the-list clutter the feature was meant to avoid. Where
|
|
1831
|
-
folded work went is now logged in the opened detail's "Also carries" section only, so
|
|
1832
|
-
the .fnote / .rcard.fold* rules that used to style these list-level notes are gone
|
|
1833
|
-
with them (dead CSS orphaned the moment nothing emits those classes any more). */
|
|
1851
|
+
/* Folded work must stay visible in List view: it is a settled pointer, not a vanished task. */
|
|
1834
1852
|
/* reference numbers everywhere else (HANDOFF-v45 §11): a leading #NN prefix on every
|
|
1835
1853
|
To Do item's name — same goal = same number (goalReviewNumber), distinct from the
|
|
1836
1854
|
PR's own #id shown separately. */
|
|
1837
1855
|
#fsRoot .trow .gno{font-size:10.5px;font-weight:600;color:var(--ink3);margin-right:5px;flex:0 0 auto;font-variant-numeric:tabular-nums}
|
|
1838
1856
|
#fsRoot .rcard .act{display:flex;gap:5px;opacity:0;transition:opacity .15s} /* hover-only ✓/✕ (§1.5-10) */
|
|
1839
1857
|
#fsRoot .rcard:hover .act{opacity:1}
|
|
1840
|
-
#fsRoot .rcard .b{width:
|
|
1858
|
+
#fsRoot .rcard .b{width:21px;height:21px;border-radius:7px;border:1px solid var(--hair2);background:transparent;display:grid;place-items:center;cursor:pointer;color:var(--green);font-size:11px;font:inherit}
|
|
1841
1859
|
#fsRoot .rcard .b.no{color:var(--danger)}
|
|
1842
1860
|
#fsRoot .rcard .b:hover{background:var(--hover)}
|
|
1843
|
-
/* rv2 compact one-line rows */
|
|
1844
|
-
#fsRoot[data-rv="2"] .rcard{margin-top:0;border:0;border-radius:0;border-bottom:1px solid var(--hair);padding:7px 3px;gap:9px}
|
|
1845
|
-
#fsRoot[data-rv="2"] .rcard img{display:none}
|
|
1846
|
-
#fsRoot[data-rv="2"] .rcard .rc{display:flex;align-items:center;gap:8px}
|
|
1847
|
-
#fsRoot[data-rv="2"] .rcard .t{font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1848
|
-
#fsRoot[data-rv="2"] .rcard .pr{margin-top:0;flex:0 0 auto}
|
|
1849
|
-
#fsRoot[data-rv="2"] .rcard .rid{margin:0;flex:0 0 auto;order:-1}
|
|
1850
|
-
#fsRoot[data-rv="2"] .rcard .b{width:22px;height:22px}
|
|
1851
|
-
/* rv3 large proof thumb as hero (default) — legibility scrim is a faithful flagship port.
|
|
1852
|
-
CDO review 2026-07-08 §3: was scoped :not(.noimg) only, so a proof-less review (no PNG/GIF
|
|
1853
|
-
yet) fell through to the plain white/dark-text .rcard base and floated among the dark
|
|
1854
|
-
thumbnail cards. Now EVERY rv3 card (img or not) gets the same gallery container language —
|
|
1855
|
-
dark ground + white heading + scrim — .noimg just swaps the <img> for a placeholder-dark
|
|
1856
|
-
background (no image element to size the card, so it also needs its own min-height). */
|
|
1857
|
-
/* min-height + dark ground live on the BASE card (not just .noimg): the card's
|
|
1858
|
-
height must NOT depend on the proof <img> rendering. When a proof PNG/GIF is
|
|
1859
|
-
slow (loading="lazy") or fails (404/expired key), the img collapsed below 84px,
|
|
1860
|
-
so the bottom-anchored .rc scrim (26px top padding) overflowed UPWARD and got
|
|
1861
|
-
clipped by overflow:hidden → the "レビューがずれる" clipped dark bars. Now every
|
|
1862
|
-
rv3 card reserves 84px over a dark ground; the img just fills it when present,
|
|
1863
|
-
and a broken img (onerror → removed in JS) reveals the same dark ground. */
|
|
1864
|
-
#fsRoot[data-rv="3"] .rcard{display:block;position:relative;margin-top:8px;padding:0;border:0;border-radius:12px;overflow:hidden;box-shadow:0 0 0 1px var(--hair);min-height:84px;background:linear-gradient(155deg,#2b2e37,#15161a)}
|
|
1865
|
-
#fsRoot[data-rv="3"] .rcard img{width:100%;height:84px;border-radius:0;box-shadow:none;object-fit:cover;display:block}
|
|
1866
|
-
#fsRoot[data-rv="3"] .rcard.noimg{min-height:84px;background:linear-gradient(155deg,#2b2e37,#15161a)} /* placeholder-dark ground, no CSS-blob/lorem — a flat tonal fill matching the scrim's own black, not a fake image */
|
|
1867
|
-
#fsRoot[data-rv="3"] .rcard .rc{position:absolute;left:0;right:0;bottom:0;padding:26px 13px 11px;background:linear-gradient(0deg,rgba(8,10,14,.93),rgba(8,10,14,.72) 45%,rgba(8,10,14,.28) 80%,transparent)} /* §15 stronger scrim for bright thumbs/2-line */
|
|
1868
|
-
#fsRoot[data-rv="3"] .rcard .t{color:#fff;font-size:11.5px;line-height:1.25;text-shadow:0 1px 3px rgba(0,0,0,.55)} /* §15 white heading readable on any photo (or the noimg placeholder ground) */
|
|
1869
|
-
#fsRoot[data-rv="3"] .rcard .pr{color:#e9fff6;background:rgba(255,255,255,.16)}
|
|
1870
|
-
#fsRoot[data-rv="3"] .rcard .rid{color:rgba(255,255,255,.66)}
|
|
1871
|
-
#fsRoot[data-rv="3"] .rcard .act{position:absolute;top:9px;right:9px}
|
|
1872
|
-
#fsRoot[data-rv="3"] .rcard .b{background:rgba(255,255,255,.92);border-color:transparent;color:var(--green)}
|
|
1873
|
-
#fsRoot[data-rv="3"] .rcard .b.no{color:#c0392b}
|
|
1874
1861
|
/* fold line — "+N more ›" 統一 (§6.5) */
|
|
1875
1862
|
#fsRoot .morefold{display:flex;justify-content:center;text-align:center;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;color:var(--ink3);cursor:pointer;padding:8px;border-radius:8px;align-items:center}
|
|
1876
1863
|
#fsRoot .morefold:hover{background:var(--hover-s);color:var(--ink)}
|
|
@@ -2141,6 +2128,9 @@
|
|
|
2141
2128
|
/* body prose = the app's normal text: 14px / −.012em, matching .bubble (Masa 2026-07-20:
|
|
2142
2129
|
13px read thinner/lighter for Japanese than the surrounding app). §2. */
|
|
2143
2130
|
.redesign .rbody{font-size:14px;line-height:1.62;letter-spacing:-.012em;color:var(--ink);max-width:70ch;margin:0 !important}
|
|
2131
|
+
.redesign .rinfra{padding:10px 12px;border:1px solid rgba(var(--amber-rgb),.35);border-radius:8px;background:rgba(var(--amber-rgb),.06)}
|
|
2132
|
+
.redesign .rinfra .rbody{color:var(--ink2)}
|
|
2133
|
+
.redesign .rinfra .retrybtn{margin-top:8px;margin-left:0}
|
|
2144
2134
|
.redesign .rdesc,.redesign .rrisk{color:var(--ink2)}
|
|
2145
2135
|
/* Result (P1, Masa 2026-07-21): the AI's own report, rendered as written. Same 14px
|
|
2146
2136
|
body rhythm as .rbody — it is prose the reviewer reads, not a log. Headings/lists/
|
|
@@ -2330,6 +2320,8 @@
|
|
|
2330
2320
|
/* Doing-failure state word (HANDOFF-v45 §11): colour TEXT only, no badge/pill surface —
|
|
2331
2321
|
Failed = red, Interrupted = amber. */
|
|
2332
2322
|
#fsRoot .trow .stword{font-size:10.5px;font-weight:600;color:var(--danger);flex:0 0 auto}
|
|
2323
|
+
#fsRoot .trow .stword.run{color:var(--green)}
|
|
2324
|
+
#fsRoot .trow .stword.wait{color:var(--ink3)}
|
|
2333
2325
|
#fsRoot .trow .stword.int{color:var(--amber)}
|
|
2334
2326
|
#fsRoot .trow .st.ok{border-color:transparent;background:rgba(var(--green-rgb),.12);position:relative}
|
|
2335
2327
|
#fsRoot .trow .st.ok::after{content:"✓";position:absolute;inset:0;display:grid;place-items:center;font-size:8px;color:var(--green)}
|
|
@@ -2986,10 +2978,11 @@
|
|
|
2986
2978
|
<svg viewBox="0 0 24 24"><path d="M6 3v12M6 15a3 3 0 100 6 3 3 0 000-6zM18 9a3 3 0 100-6 3 3 0 000 6zM18 9v3a4 4 0 01-4 4H8"/></svg><span id="ctxbranchname"></span></span>
|
|
2987
2979
|
<span class="ctxdiv"></span>
|
|
2988
2980
|
<button type="button" class="ctxseg ctxwt" id="ctxwt" role="checkbox" aria-checked="true"
|
|
2989
|
-
title="
|
|
2990
|
-
<i class="ctxbox"></i>worktree</button>
|
|
2981
|
+
title="isolated worktree: default clean copy. direct workspace: inherits and edits this folder.">
|
|
2982
|
+
<i class="ctxbox"></i><span id="ctxwtlabel">isolated worktree</span></button>
|
|
2991
2983
|
</div>
|
|
2992
2984
|
</div>
|
|
2985
|
+
<div class="ctxworkwarn" id="ctxworkwarn" hidden></div>
|
|
2993
2986
|
<!-- Conversation-first composer (SLICE 2, docs/HANDOFF-ONBOARDING-conversational.md):
|
|
2994
2987
|
a chat/help/settings message gets a quiet reply HERE instead of
|
|
2995
2988
|
becoming a To-Do. Reuses the .peraanswer surface; hidden until a
|
|
@@ -3081,7 +3074,7 @@
|
|
|
3081
3074
|
Structure ported from docs/design/flagship/flagship.html: rail | Implementing | lane.
|
|
3082
3075
|
The composer (#composerWrap: queue + composer + send/prism ring) is ADOPTED into
|
|
3083
3076
|
#fsCompwrap by positionLayout() — same nodes, same handlers, input-focus fix intact. -->
|
|
3084
|
-
<div id="fsRoot"
|
|
3077
|
+
<div id="fsRoot">
|
|
3085
3078
|
<div class="fscard fsrail">
|
|
3086
3079
|
<button class="railtg" id="fsRailTg" title="Fold the rail"></button><!-- panel icon filled by tgUpdate (v45 §4) -->
|
|
3087
3080
|
<button class="fsprojbtn" id="fsProjBtn" title="Switch project"><span class="pname" id="fsProjBtnName"></span><svg viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg></button>
|
|
@@ -3373,7 +3366,7 @@ const savedTheme = (() => {
|
|
|
3373
3366
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3374
3367
|
return v;
|
|
3375
3368
|
})();
|
|
3376
|
-
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
|
|
3369
|
+
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
|
|
3377
3370
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3378
3371
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3379
3372
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -3704,6 +3697,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3704
3697
|
checkLine: plan ? `PLAN — approve to execute: ${checkLine}`.slice(0, 90) : checkLine,
|
|
3705
3698
|
pr: g.pr ?? null,
|
|
3706
3699
|
testResult: g.testResult ?? null,
|
|
3700
|
+
verificationInfraError: g.verificationInfraError ?? null,
|
|
3707
3701
|
proof: proofTask?.proof ?? null,
|
|
3708
3702
|
proofTaskId: proofTask?.id ?? null,
|
|
3709
3703
|
plan,
|
|
@@ -3715,6 +3709,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3715
3709
|
if (row.reviewNumber != null && (existing.reviewNumber == null || row.reviewNumber < existing.reviewNumber)) existing.reviewNumber = row.reviewNumber;
|
|
3716
3710
|
if (existing.proof == null) { existing.proof = row.proof; existing.proofTaskId = row.proofTaskId; }
|
|
3717
3711
|
if (existing.testResult == null) existing.testResult = row.testResult;
|
|
3712
|
+
if (existing.verificationInfraError == null) existing.verificationInfraError = row.verificationInfraError;
|
|
3718
3713
|
continue;
|
|
3719
3714
|
}
|
|
3720
3715
|
if (row.pr) byPr.set(row.pr, row);
|
|
@@ -4247,7 +4242,11 @@ function goalRow(g){
|
|
|
4247
4242
|
const summary = GOAL_COMPLETE_STATUSES.includes(g.status) && g.summary
|
|
4248
4243
|
? `<div class="gsummary"><div class="gslabel"><span class="dot"></span>Manager結論サマリ</div>${esc(g.summary)}</div>` : '';
|
|
4249
4244
|
const pr = g.pr ? `<a class="prlink" href="${esc(g.pr)}" target="_blank">${g.status === 'review' ? 'In review — open the Pull Request' : 'Open the Pull Request'}</a>`
|
|
4250
|
-
: g.prError ?
|
|
4245
|
+
: g.prError ? (() => {
|
|
4246
|
+
const meta = (() => { const raw = g.prErrorClass || inferErrorClass(`PR step failed: ${g.prError}`, g.agent); const C = FAILURE_COPY[failureLang()]; return { ...raw, title: C.infra.title, body: C.infra[raw.reason] || C.infra.pr, copy: C }; })();
|
|
4247
|
+
const repair = g.prRepair ? `<div class="prrepair"><b>${esc(g.prRepair.title || 'Repair')}</b><ul>${(g.prRepair.steps || []).map((s) => `<li>${esc(s)}</li>`).join('')}</ul>${g.prRepair.dirtyFiles?.length ? `<div class="dim">dirty: ${g.prRepair.dirtyFiles.map(esc).join(' · ')}</div>` : ''}</div>` : '';
|
|
4248
|
+
return `<div class="prfail"><b>${esc(meta.title)}</b> — ${esc(meta.body)} <span class="dim">${esc(g.prError)}</span>${repair}<div class="acts2">${errorButtonsHtml(meta, { goalId: g.id, agent: g.agent, pr: true })}</div></div>`;
|
|
4249
|
+
})() : '';
|
|
4251
4250
|
// task 96: PRなしのreviewゴールは、チャット内で要約+個別Approve+チェックリスト導線を出す。
|
|
4252
4251
|
const reviewBox = g.status === 'review' && !g.pr ? reviewActionsHtml(g) : '';
|
|
4253
4252
|
const active = replies.some((r) => ['running', 'queued'].includes(r.status));
|
|
@@ -4354,6 +4353,9 @@ function renderStream(){
|
|
|
4354
4353
|
for (const b of $('stream').querySelectorAll('.reqopen')) {
|
|
4355
4354
|
b.onclick = () => openReviewChecklist(Number(b.dataset.goal));
|
|
4356
4355
|
}
|
|
4356
|
+
for (const b of $('stream').querySelectorAll('[data-fsprretry]')) {
|
|
4357
|
+
b.onclick = () => fsRetryPrGoal(b);
|
|
4358
|
+
}
|
|
4357
4359
|
// task 96: chat-side per-requirement Approve — same flow as the checklist
|
|
4358
4360
|
// (setRequirementStatus toggles the approval and auto-advances the goal when
|
|
4359
4361
|
// every requirement is approved, then render() redraws this thread).
|
|
@@ -4422,6 +4424,17 @@ function renderQueue(){
|
|
|
4422
4424
|
// layouts keep the explanatory sentence.
|
|
4423
4425
|
const qn = ob.length + stacked.length + planning.length;
|
|
4424
4426
|
const headHtml = qn > 0 ? `<div class="qhead">${state.layout === 'flagship' ? `Queued · ${qn}` : `${qn} queued · goals become To dos in ~30s`}</div>` : '';
|
|
4427
|
+
const stackedMeta = (g) => {
|
|
4428
|
+
const project = state.projects.find((p) => p.id === g.projectId);
|
|
4429
|
+
if (project?.paused) return 'Paused · press play to start';
|
|
4430
|
+
const activeIds = [...new Set([
|
|
4431
|
+
...state.tasks.filter((t) => t.projectId === g.projectId && t.status === 'running' && t.goalId != null).map((t) => t.goalId),
|
|
4432
|
+
...state.goals.filter((x) => x.projectId === g.projectId && x.status === 'planning').map((x) => x.id),
|
|
4433
|
+
])].filter((id) => id !== g.id);
|
|
4434
|
+
const cap = state.parallelLimits?.project ?? 2;
|
|
4435
|
+
if (activeIds.length >= cap) return `Waiting for parallel slot · active ${activeIds.map((id) => `#${id}`).join(', ')} · starts when one reaches Review/Done · Run next / switch Agent`;
|
|
4436
|
+
return `Next up · starts automatically · Agent: ${esc(g.agent || state.connectedApp || 'worker')}`;
|
|
4437
|
+
};
|
|
4425
4438
|
q.innerHTML = connHtml + headHtml + obRows + planningRows + stacked.map((g) => {
|
|
4426
4439
|
if (state.editing === g.id) {
|
|
4427
4440
|
return `<textarea class="qedit" id="qedit-${g.id}">${esc(g.text)}</textarea>
|
|
@@ -4434,7 +4447,7 @@ function renderQueue(){
|
|
|
4434
4447
|
<button class="gbtn qsave" data-id="${g.id}">Save</button><button class="gbtn qcancel">Cancel</button>
|
|
4435
4448
|
</div>`;
|
|
4436
4449
|
}
|
|
4437
|
-
return `<div class="qrow" draggable="true" data-goal="${g.id}"><span class="qdot"></span><span class="qtxt">${esc(g.text.replace(/\s+/g, ' '))}</span>
|
|
4450
|
+
return `<div class="qrow" draggable="true" data-goal="${g.id}"><span class="qdot"></span><span class="qtxt">${esc(g.text.replace(/\s+/g, ' '))}</span><span class="meta">${stackedMeta(g)}</span>
|
|
4438
4451
|
<button class="qico qe" data-id="${g.id}" title="Edit">${ICONS.pencil}</button>
|
|
4439
4452
|
<button class="qico qp" data-id="${g.id}" title="Run next">${ICONS.up}</button>
|
|
4440
4453
|
<button class="qico del qd" data-id="${g.id}" title="Delete">${ICONS.trash}</button></div>`;
|
|
@@ -5324,6 +5337,8 @@ function renderReviewChecklist(){
|
|
|
5324
5337
|
const hasProof = state.tasks.some((t) => t.goalId === goal.id && (t.proof?.png || t.proof?.gif));
|
|
5325
5338
|
const VF = { pass: 'tests pass', fail: 'tests fail', untested: 'not tested', proof: 'proof', noproof: 'no proof', human: 'needs you' }; // #377-7: chrome is English
|
|
5326
5339
|
const vchips = [];
|
|
5340
|
+
vchips.push(`<span class="vchip none">${esc(workspaceModeText(goal))}</span>`);
|
|
5341
|
+
if (goal.workspaceWarning) vchips.push(`<span class="vchip human high">⚠ ${esc(goal.workspaceWarning.slice(0, 80))}</span>`);
|
|
5327
5342
|
vchips.push(gtr?.ran
|
|
5328
5343
|
? (gtr.ok ? `<span class="vchip ok">✓ ${VF.pass}</span>` : `<span class="vchip no">✗ ${VF.fail}</span>`)
|
|
5329
5344
|
: `<span class="vchip none">— ${VF.untested}</span>`);
|
|
@@ -5335,10 +5350,15 @@ function renderReviewChecklist(){
|
|
|
5335
5350
|
const failureHtml = fm?.whatHappened
|
|
5336
5351
|
? `<div class="rvchanged"><span class="rvlbl">Failure memory</span>${esc(fm.whatHappened)} ${fm.nextPolicy ? `<span class="muted">${esc(fm.nextPolicy)}</span>` : ''}</div>`
|
|
5337
5352
|
: '';
|
|
5353
|
+
const infra = goal.verificationInfraError;
|
|
5354
|
+
const infraHtml = infra?.detail
|
|
5355
|
+
? `<div class="rvinfra"><span class="rvlbl">${esc(infra.title || '検証基盤エラー')}</span>${esc(infra.detail)} <button type="button" class="retrybtn" data-goalreverify="${goal.id}">再検証</button></div>`
|
|
5356
|
+
: '';
|
|
5338
5357
|
$('reviewSummary').innerHTML =
|
|
5339
5358
|
vfyHtml
|
|
5340
5359
|
+ (grs.check ? `<div class="rvcheck2"><span class="rvlbl">${L.check}</span>${esc(grs.check)}</div>` : '')
|
|
5341
5360
|
+ (grs.changed ? `<div class="rvchanged"><span class="rvlbl">${L.changed}</span>${esc(grs.changed)}</div>` : '')
|
|
5361
|
+
+ infraHtml
|
|
5342
5362
|
+ failureHtml
|
|
5343
5363
|
+ optHtml
|
|
5344
5364
|
+ `<div class="rvhead">${testLine}${esc(sum.headline)} · ${esc(sum.detail)}${prLink ? ` · ${prLink}` : ''}</div>`
|
|
@@ -5401,6 +5421,15 @@ function renderReviewChecklist(){
|
|
|
5401
5421
|
for (const c of $('reviewList').querySelectorAll('[data-reopen]')) {
|
|
5402
5422
|
c.onclick = () => setRequirementStatus(goalId, Number(c.dataset.reopen), 'approved'); // same status toggles it off
|
|
5403
5423
|
}
|
|
5424
|
+
for (const b of $('reviewSummary').querySelectorAll('[data-goalreverify]')) {
|
|
5425
|
+
b.onclick = async (e) => {
|
|
5426
|
+
e.stopPropagation();
|
|
5427
|
+
b.disabled = true;
|
|
5428
|
+
b.textContent = '再検証中…';
|
|
5429
|
+
const r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' });
|
|
5430
|
+
if (r.ok) await refresh(); else showErr('再検証に失敗');
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5404
5433
|
for (const b of $('reviewList').querySelectorAll('.reqsend')) {
|
|
5405
5434
|
b.onclick = async () => {
|
|
5406
5435
|
const taskId = Number(b.dataset.task);
|
|
@@ -5456,15 +5485,15 @@ function playReviewFly(capture){
|
|
|
5456
5485
|
}
|
|
5457
5486
|
// task 121: 承認が確定した瞬間に一度だけ紙吹雪を弾けさせる共通フック。両方の
|
|
5458
5487
|
// 承認経路(reviewApprove.onclick / advanceGoalAfterAllApproved)から呼ぶ。
|
|
5459
|
-
//
|
|
5460
|
-
//
|
|
5461
|
-
const CONFETTI_COLORS = ['
|
|
5488
|
+
// Figma's playful primary family: vivid enough to read as a reward on both
|
|
5489
|
+
// light and dark themes while the green check remains the success anchor.
|
|
5490
|
+
const CONFETTI_COLORS = ['#F24E1E', '#FF7262', '#A259FF', '#1ABCFE', '#0ACF83', '#FFD166'];
|
|
5462
5491
|
function celebrateApprove(){
|
|
5463
5492
|
const dm = $('reviewDoneMsg');
|
|
5464
5493
|
const rect = dm && dm.offsetParent ? dm.getBoundingClientRect() : null;
|
|
5465
5494
|
const originX = rect ? rect.left + Math.min(28, rect.width / 2) : window.innerWidth / 2;
|
|
5466
5495
|
const originY = rect ? rect.top + rect.height / 2 : window.innerHeight * 0.3;
|
|
5467
|
-
for (let i = 0; i <
|
|
5496
|
+
for (let i = 0; i < 42; i++){
|
|
5468
5497
|
const el = document.createElement('div');
|
|
5469
5498
|
el.className = 'confetti-piece';
|
|
5470
5499
|
const size = 6 + Math.random() * 6;
|
|
@@ -5478,7 +5507,7 @@ function celebrateApprove(){
|
|
|
5478
5507
|
el.style.transition = 'transform .9s cubic-bezier(.15,.7,.3,1), opacity .9s ease .25s';
|
|
5479
5508
|
document.body.appendChild(el);
|
|
5480
5509
|
const angle = Math.random() * Math.PI * 2;
|
|
5481
|
-
const dist =
|
|
5510
|
+
const dist = 90 + Math.random() * 190;
|
|
5482
5511
|
const dx = Math.cos(angle) * dist;
|
|
5483
5512
|
const dy = Math.sin(angle) * dist * 0.6 + 90; // outward burst, then settle downward like falling confetti
|
|
5484
5513
|
const rot = (Math.random() < 0.5 ? -1 : 1) * (360 + Math.random() * 360);
|
|
@@ -5489,6 +5518,34 @@ function celebrateApprove(){
|
|
|
5489
5518
|
setTimeout(() => el.remove(), 1000);
|
|
5490
5519
|
}
|
|
5491
5520
|
}
|
|
5521
|
+
|
|
5522
|
+
// A merged PR is the terminal Review moment. Show the result in the dialog the
|
|
5523
|
+
// person is looking at, then remove that completed review instead of leaving a
|
|
5524
|
+
// stale modal behind. The state update arrives in the same SSE event, so render()
|
|
5525
|
+
// after close also removes the card from the Review lane.
|
|
5526
|
+
function finishOpenReview(goal){
|
|
5527
|
+
const ids = SA.items.flatMap((it) => it.ids || [it.goalId]);
|
|
5528
|
+
if (!SA.open || !ids.includes(goal.id)) { celebrateApprove(); return; }
|
|
5529
|
+
const layer = document.createElement('div');
|
|
5530
|
+
layer.className = 'sa-finish';
|
|
5531
|
+
layer.setAttribute('role', 'status');
|
|
5532
|
+
layer.setAttribute('aria-label', 'Review complete');
|
|
5533
|
+
layer.innerHTML = '<div class="sa-finish-mark"><svg viewBox="0 0 64 64" aria-hidden="true"><path d="M17 33l10 10 21-23"/></svg></div>';
|
|
5534
|
+
const panel = $('seeall').querySelector('.sa-panel')?.getBoundingClientRect();
|
|
5535
|
+
const bounds = panel ?? { left: 0, top: 0, width: innerWidth, height: innerHeight };
|
|
5536
|
+
Object.assign(layer.style, { left: `${bounds.left}px`, top: `${bounds.top}px`, width: `${bounds.width}px`, height: `${bounds.height}px` });
|
|
5537
|
+
// Keep the completion layer outside #seeall: the same SSE schedules a normal
|
|
5538
|
+
// render, which rebuilds the dialog while this short animation is playing.
|
|
5539
|
+
document.body.appendChild(layer);
|
|
5540
|
+
const mark = layer.querySelector('.sa-finish-mark').getBoundingClientRect();
|
|
5541
|
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
5542
|
+
if (!reduce) {
|
|
5543
|
+
confettiBurst(mark.left + mark.width / 2, mark.top + mark.height / 2);
|
|
5544
|
+
celebrateApprove();
|
|
5545
|
+
}
|
|
5546
|
+
const delay = reduce ? 350 : 950;
|
|
5547
|
+
setTimeout(() => { layer.remove(); saClose(); render(); }, delay);
|
|
5548
|
+
}
|
|
5492
5549
|
async function setRequirementStatus(goalId, taskId, status){
|
|
5493
5550
|
const approvals = (state.reviewApprovals[goalId] ??= {});
|
|
5494
5551
|
approvals[taskId] = approvals[taskId] === status ? undefined : status; // click again to undo
|
|
@@ -6235,7 +6292,6 @@ function render(){
|
|
|
6235
6292
|
// queueOrder); actions call the SAME endpoints (approve/dismiss/retry/reply…).
|
|
6236
6293
|
// ============================================================================
|
|
6237
6294
|
const fsUI = {
|
|
6238
|
-
rv: [1, 2, 3].includes(Number(localStorage.getItem('fsRv'))) ? Number(localStorage.getItem('fsRv')) : 3, // review view: 3 thumbnails (default) / 1 list / 2 compact
|
|
6239
6295
|
band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
|
|
6240
6296
|
revMore: false, // "+N more" fold on the review list (§1.5-8)
|
|
6241
6297
|
todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
|
|
@@ -6263,7 +6319,6 @@ function fsActs(goalId){
|
|
|
6263
6319
|
function renderFlagship(){
|
|
6264
6320
|
if (state.layout !== 'flagship') return;
|
|
6265
6321
|
const root = $('fsRoot'); if (!root) return;
|
|
6266
|
-
root.dataset.rv = String(fsUI.rv);
|
|
6267
6322
|
// Wall-clock phase for the spinning markers: any ring re-created this render (the board
|
|
6268
6323
|
// re-renders often on progress %) starts at the angle a continuous one would be at now,
|
|
6269
6324
|
// instead of snapping back to 0deg — no "jump". The rail ring is guarded separately and
|
|
@@ -6340,9 +6395,6 @@ function fsActiveContext(){
|
|
|
6340
6395
|
const focus = pinnedValid ? pinned : running[0] ?? closing[0] ?? failed[failed.length - 1] ?? null;
|
|
6341
6396
|
return { tasks, running, failed, focus, goal: state.goals.find((g) => g.id === focus?.goalId) ?? null };
|
|
6342
6397
|
}
|
|
6343
|
-
// Per-project last non-empty center feed — the whiteout guard (see below): keeps
|
|
6344
|
-
// what the user was looking at when a render pass momentarily produces no lines.
|
|
6345
|
-
const _fsLastFeed = {};
|
|
6346
6398
|
// H-CENTERLANE decision 1: pure merge-sort of timed HTML blocks (the focused task's
|
|
6347
6399
|
// activity block + each chat turn) so the center lane renders in the order things
|
|
6348
6400
|
// actually happened. Stable sort (spec-guaranteed since ES2019) keeps same-ts blocks
|
|
@@ -6416,13 +6468,14 @@ function renderFsFeed(){
|
|
|
6416
6468
|
// it's continuing, so the ⎿ read as floating/orphaned. Now an independent state line:
|
|
6417
6469
|
// colored icon + word only, no continuation glyph.
|
|
6418
6470
|
const isInt = focus.status === 'interrupted';
|
|
6419
|
-
const word = isInt ? 'Interrupted' : 'Failed', icon = isInt ? '⏸' : '✗'
|
|
6471
|
+
const word = isInt ? 'Interrupted' : 'Failed', icon = isInt ? '⏸' : '✗';
|
|
6472
|
+
const meta = taskErrorMeta(focus, goal);
|
|
6420
6473
|
const reason = String(focus.result || '').trim();
|
|
6421
6474
|
// no separate reason text (or it's just a restatement of the status itself) → don't
|
|
6422
6475
|
// print "Interrupted — Interrupted"; the word + icon alone already say it
|
|
6423
6476
|
const reasonSuffix = reason && reason.toLowerCase() !== word.toLowerCase() ? ` — ${esc(reason.replace(/\s+/g, ' ').slice(0, 140))}` : '';
|
|
6424
|
-
html += `<div class="cc-err${isInt ? ' int' : ''}"><div class="msg"><span class="x${isInt ? ' int' : ''}">${icon} ${esc(word)}</span>${reasonSuffix}</div>
|
|
6425
|
-
<div class="acts2"
|
|
6477
|
+
html += `<div class="cc-err${isInt ? ' int' : ''}"><div class="msg"><span class="x${isInt ? ' int' : ''}">${icon} ${esc(word)}</span> <b>${esc(meta.title)}</b> — ${esc(meta.body)}${reasonSuffix}</div>
|
|
6478
|
+
<div class="acts2">${errorButtonsHtml(meta, { taskId: focus.id, goalId: focus.goalId, interrupted: isInt, agent: focus.agent || goal?.agent })}</div></div>`;
|
|
6426
6479
|
}
|
|
6427
6480
|
// AI question — while current it lives in the chat feed (cc-ask, §6.5 dynamic rule / q2).
|
|
6428
6481
|
// Free-text answer row + delete (trash) restore lost behaviors from the pre-flagship
|
|
@@ -6452,15 +6505,9 @@ function renderFsFeed(){
|
|
|
6452
6505
|
const app = connectedAppMeta(focus?.agent ?? goal?.agent);
|
|
6453
6506
|
html = `<div class="cc-who">${app.icon}<span class="cs-n">${esc(app.label)}</span>${meta ? `<span class="cs-d">${esc(meta)}${focus?.status === 'running' ? ' · worktree' : ''}</span>` : ''}</div>` + html;
|
|
6454
6507
|
}
|
|
6455
|
-
//
|
|
6456
|
-
//
|
|
6457
|
-
//
|
|
6458
|
-
// a goal finishing before the next starts, a project switch, a refresh() that
|
|
6459
|
-
// briefly reloads state). Gate it PURELY on whether the project has any
|
|
6460
|
-
// goal/task; connection blips never change that, so the giant hero can no
|
|
6461
|
-
// longer flash in and wipe what the user was looking at. While work exists but
|
|
6462
|
-
// this pass produced no lines, keep the last known feed (or a calm one-line
|
|
6463
|
-
// status) instead of blanking to the hero.
|
|
6508
|
+
// The center must describe current state. Keeping the previous task's activity
|
|
6509
|
+
// after it settled made an idle board look 77% active while the server had zero
|
|
6510
|
+
// workers. Empty live output now gets an explicit idle explanation instead.
|
|
6464
6511
|
// Pattern A (H23): the conversation turns — the user's own messages, Claude
|
|
6465
6512
|
// Code's replies (typed out with a caret on the streaming frontier), and
|
|
6466
6513
|
// "→ Added to To Do #NN" markers — rendered from state.chatTurns so the
|
|
@@ -6500,8 +6547,12 @@ function renderFsFeed(){
|
|
|
6500
6547
|
// end (H18/H19 — a pending question must never be lost mid-scroll).
|
|
6501
6548
|
const timelineBlocks = html ? [{ ts: focusTs, html }, ...chatBlocks] : chatBlocks;
|
|
6502
6549
|
let out = fsMergeTimelineBlocks(timelineBlocks) + askHtml;
|
|
6503
|
-
if (out
|
|
6504
|
-
|
|
6550
|
+
if (!out && hasWork) {
|
|
6551
|
+
const laterN = pgoals.filter((g) => g.status === 'pending').length;
|
|
6552
|
+
const queuedN = ptasks.filter((t) => t.status === 'queued').length + pgoals.filter((g) => ['planning', 'stacked'].includes(g.status)).length;
|
|
6553
|
+
const msg = running.length ? 'working…' : queuedN ? `${queuedN} queued · waiting to start` : laterN ? `no active task · ${laterN} saved in Later` : 'no active task';
|
|
6554
|
+
out = `<div class="cc-c">⎿ ${esc(msg)}</div>`;
|
|
6555
|
+
}
|
|
6505
6556
|
$('fsRoot').classList.toggle('clawdhero', !hasWork);
|
|
6506
6557
|
const sc = feed.parentNode;
|
|
6507
6558
|
const nearBottom = sc.scrollHeight - sc.clientHeight - sc.scrollTop <= 80;
|
|
@@ -6512,6 +6563,9 @@ function renderFsFeed(){
|
|
|
6512
6563
|
window.__fsCineSync?.(); // Cinema dynamic rule: content growth may reach the band region
|
|
6513
6564
|
for (const b of feed.querySelectorAll('[data-fsretry]')) b.onclick = () => fsRetryTask(b.dataset.fsretry);
|
|
6514
6565
|
for (const b of feed.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
6566
|
+
for (const b of feed.querySelectorAll('[data-fscodex]')) b.onclick = () => fsRunWithCodexGoal(b);
|
|
6567
|
+
for (const b of feed.querySelectorAll('[data-fsconnectworker]')) b.onclick = () => fsConnectWorker(b);
|
|
6568
|
+
for (const b of feed.querySelectorAll('[data-fscheckworker]')) b.onclick = () => fsCheckWorker(b);
|
|
6515
6569
|
for (const b of feed.querySelectorAll('[data-fsanswer]')) b.onclick = () => fsAnswerQuestion(b.dataset.fsanswer, b.dataset.a);
|
|
6516
6570
|
// Free-text answer (restores 質問カードの自由記述回答欄 — lost in the flagship rewrite,
|
|
6517
6571
|
// see docs/confirmed-behaviors.md): Enter or the ↑ send button answers with typed text.
|
|
@@ -6550,6 +6604,38 @@ async function fsRetryTask(id){
|
|
|
6550
6604
|
const r = await fetch(withKey(`/api/tasks/${id}/retry`), { method: 'POST' });
|
|
6551
6605
|
if (!r.ok) showErr('Retry failed.'); else await refresh();
|
|
6552
6606
|
}
|
|
6607
|
+
async function fsRunWithCodexGoal(b){
|
|
6608
|
+
b.disabled = true; b.textContent = failureLang() === 'ja' ? '開始中…' : 'Starting…';
|
|
6609
|
+
const r = await fetch(withKey(`/api/goals/${b.dataset.fscodex}/run-with-codex`), { method: 'POST' });
|
|
6610
|
+
if (!r.ok) showErr((await r.json().catch(() => ({}))).error || 'Could not start with Codex.');
|
|
6611
|
+
await refresh();
|
|
6612
|
+
}
|
|
6613
|
+
async function fsConnectWorker(b){
|
|
6614
|
+
b.disabled = true;
|
|
6615
|
+
try {
|
|
6616
|
+
const r = await fetch(withKey('/api/worker-auth/connect'), { method: 'POST' });
|
|
6617
|
+
if (!r.ok) throw new Error();
|
|
6618
|
+
showToast(failureLang() === 'ja' ? '接続画面を開きました。完了後に接続確認してください。' : 'Connection opened. Check connection when finished.');
|
|
6619
|
+
} catch { showErr(failureLang() === 'ja' ? '接続画面を開けませんでした。' : 'Could not open the connection.'); }
|
|
6620
|
+
finally { b.disabled = false; }
|
|
6621
|
+
}
|
|
6622
|
+
async function fsCheckWorker(b){
|
|
6623
|
+
b.disabled = true;
|
|
6624
|
+
try {
|
|
6625
|
+
const r = await fetch(withKey('/api/worker-auth/retry'), { method: 'POST' });
|
|
6626
|
+
const body = await r.json().catch(() => ({}));
|
|
6627
|
+
if (!r.ok) throw new Error();
|
|
6628
|
+
renderAuthBanner(body.auth);
|
|
6629
|
+
if (body.auth?.ok) showToast(failureLang() === 'ja' ? '接続できました。' : 'Connected.');
|
|
6630
|
+
} catch { showErr(failureLang() === 'ja' ? 'まだ接続できていません。' : 'Not connected yet.'); }
|
|
6631
|
+
finally { b.disabled = false; }
|
|
6632
|
+
}
|
|
6633
|
+
async function fsRetryPrGoal(b){
|
|
6634
|
+
b.disabled = true; b.textContent = failureLang() === 'ja' ? '再試行中…' : 'Retrying…';
|
|
6635
|
+
const r = await fetch(withKey(`/api/goals/${b.dataset.fsprretry}/retry-pr`), { method: 'POST' });
|
|
6636
|
+
if (!r.ok) { showErr((await r.json().catch(() => ({}))).error || 'PR retry failed.'); b.disabled = false; b.textContent = FAILURE_COPY[failureLang()].actions.pr; }
|
|
6637
|
+
else await refresh();
|
|
6638
|
+
}
|
|
6553
6639
|
function fsToggleLog(taskId){
|
|
6554
6640
|
const task = state.tasks.find((t) => t.id === taskId);
|
|
6555
6641
|
if (task?.goalId == null) return;
|
|
@@ -6813,11 +6899,9 @@ function renderFsReview(){
|
|
|
6813
6899
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
6814
6900
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
6815
6901
|
const rows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
6816
|
-
//
|
|
6817
|
-
//
|
|
6818
|
-
//
|
|
6819
|
-
// opened detail's "Also carries" section (see saItemHtml's carriesBody). The list stays
|
|
6820
|
-
// just the reviews; folded goals are explained where they were merged.
|
|
6902
|
+
// Folded goals belong only to the carrier's Review dialog ("Also carries").
|
|
6903
|
+
// They are historical context, not independently reviewable work, so rendering
|
|
6904
|
+
// them here produced the contradictory "0 ready" header followed by stale cards.
|
|
6821
6905
|
if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6822
6906
|
const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
6823
6907
|
const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
|
|
@@ -6831,12 +6915,6 @@ function renderFsReview(){
|
|
|
6831
6915
|
const RCAP = 3;
|
|
6832
6916
|
const shown = fsUI.revMore ? rows : rows.slice(0, RCAP);
|
|
6833
6917
|
const card = (r) => {
|
|
6834
|
-
const imgFile = r.proof?.png ?? r.proof?.gif;
|
|
6835
|
-
// onerror: a broken proof (404 / expired key) removes the <img> so the card's
|
|
6836
|
-
// dark ground shows through cleanly instead of a broken-image icon + "proof" alt
|
|
6837
|
-
// text. Card height is guaranteed by the base rule's min-height, not the img.
|
|
6838
|
-
const img = imgFile && r.proofTaskId != null
|
|
6839
|
-
? `<img src="${withKey(`/proof/${r.proofTaskId}/${imgFile}`)}" alt="proof" loading="lazy" onerror="this.remove()">` : '';
|
|
6840
6918
|
const pr = r.pr ? `<a class="pr" href="${esc(r.pr)}" target="_blank">PR #${esc((r.pr.match(/pull\/(\d+)/) ?? [, ''])[1])} ↗</a>` : '';
|
|
6841
6919
|
// #NN PREFIX on the title (HANDOFF-v45 §11 — same form/number as the To Do rows;
|
|
6842
6920
|
// goalReviewNumber = goal.id, so a goal keeps the same number in Review and in To
|
|
@@ -6847,6 +6925,11 @@ function renderFsReview(){
|
|
|
6847
6925
|
// retest ×3 note (v45 §5.3): the goal came back green from a Ledger Dismiss
|
|
6848
6926
|
const g0 = pgoals.find((g) => g.id === r.goalIds[0]);
|
|
6849
6927
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
6928
|
+
const infraNote = r.verificationInfraError?.detail
|
|
6929
|
+
? `<span class="rnote">検証基盤エラー · <button type="button" class="retrybtn" data-goalreverify="${r.goalIds[0]}">再検証</button></span>`
|
|
6930
|
+
: '';
|
|
6931
|
+
const conflictNote = g0?.conflictsWith?.length
|
|
6932
|
+
? `<span class="rstat conflict">Potential conflict with ${esc(g0.conflictsWith.map((id) => `#${id}`).join(', '))}</span>` : '<span class="rstat">In review</span>';
|
|
6850
6933
|
// PRD §6.5 → Masa 2026-07-21: goals folded onto this card's PR no longer get an
|
|
6851
6934
|
// inline "+ #901 の仕事もここに入っています" note here — same report-in-the-list
|
|
6852
6935
|
// clutter the old standalone fold row was (Masa: review欄にいらない). It is logged
|
|
@@ -6856,7 +6939,7 @@ function renderFsReview(){
|
|
|
6856
6939
|
// (Masa 2026-07-20: this card showed reviewSummary.check "…が出ない" while the list
|
|
6857
6940
|
// showed the request text). Plan-reviews keep their "PLAN — approve to execute:"
|
|
6858
6941
|
// checkLine, which is the actionable label there, not a title mismatch.
|
|
6859
|
-
return `<div class="rcard
|
|
6942
|
+
return `<div class="rcard noimg" data-fsrev="${r.goalIds[0]}"><div class="rc"><div class="t">${rid}${esc(r.plan ? r.checkLine : saReviewTitle(g0))}</div>${rnote}${infraNote}${pr}${conflictNote}</div>
|
|
6860
6943
|
<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>`;
|
|
6861
6944
|
};
|
|
6862
6945
|
// V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
|
|
@@ -6868,12 +6951,7 @@ function renderFsReview(){
|
|
|
6868
6951
|
<div class="m-top"><span class="num mt">${green} / ${rows.length}</span><span> ready</span><button class="detailbtn" id="fsSeeAll">See all</button></div>
|
|
6869
6952
|
<div class="m-bar"><i style="width:${pct}%"></i></div>
|
|
6870
6953
|
<div class="s">${esc(sline)}</div></div>` : ''}
|
|
6871
|
-
<div class="revhd" id="fsRevHd"><span class="hd">Review</span><span class="rnum num">${green} ready</span>
|
|
6872
|
-
<span class="rvbtns">
|
|
6873
|
-
<button class="rvb${fsUI.rv === 3 ? ' on' : ''}" data-fsrv="3" title="Thumbnails"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l5-4 4 3 4-5 5 6"/></svg></button>
|
|
6874
|
-
<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>
|
|
6875
|
-
<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>
|
|
6876
|
-
</span></div>
|
|
6954
|
+
<div class="revhd" id="fsRevHd"><span class="hd">Review</span><span class="rnum num">${green} ready</span></div>
|
|
6877
6955
|
<div class="revlist">${shown.map(card).join('')}
|
|
6878
6956
|
${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
|
|
6879
6957
|
// Mirror the ready-review count onto the mobile top-bar "Review N" opener (H23).
|
|
@@ -6888,14 +6966,12 @@ function renderFsReview(){
|
|
|
6888
6966
|
saOpen(); };
|
|
6889
6967
|
const moreEl = el.querySelector('#fsRevMore');
|
|
6890
6968
|
if (moreEl) moreEl.onclick = () => { fsUI.revMore = !fsUI.revMore; renderFsReview(); };
|
|
6891
|
-
for (const b of el.querySelectorAll('[data-fsrv]')) b.onclick = (e) => { e.stopPropagation();
|
|
6892
|
-
fsUI.rv = Number(b.dataset.fsrv); localStorage.setItem('fsRv', String(fsUI.rv)); $('fsRoot').dataset.rv = String(fsUI.rv); renderFsReview(); };
|
|
6893
6969
|
for (const c of el.querySelectorAll('[data-fsrev]')) c.onclick = (e) => {
|
|
6894
|
-
if (e.target.closest('a') || e.target.closest('.act')) return;
|
|
6970
|
+
if (e.target.closest('a') || e.target.closest('.act') || e.target.closest('[data-goalreverify]')) return;
|
|
6895
6971
|
openReviewChecklist(Number(c.dataset.fsrev));
|
|
6896
6972
|
};
|
|
6897
|
-
// Folded goals
|
|
6898
|
-
//
|
|
6973
|
+
// Folded goals are shown, and restored, on the carrier's side in the See-all
|
|
6974
|
+
// "Also carries" section (saItemHtml + the seeall handler).
|
|
6899
6975
|
for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
|
|
6900
6976
|
let ok = true, mergeGoal = null;
|
|
6901
6977
|
for (const id of b.dataset.fsapprove.split(',')) {
|
|
@@ -6938,6 +7014,96 @@ function fsBucketTodo(){
|
|
|
6938
7014
|
rejected: pgoals.filter((g) => g.status === 'rejected'), // 07-20c: Reject parks goals here (own fold, like Done)
|
|
6939
7015
|
};
|
|
6940
7016
|
}
|
|
7017
|
+
const FAILURE_COPY = {
|
|
7018
|
+
ja: {
|
|
7019
|
+
agent: { title: 'Agentエラー', body: 'Claude Code/Codex 側が作業中に止まりました。依頼内容や実装で詰まった可能性があります。' },
|
|
7020
|
+
infra: {
|
|
7021
|
+
title: 'Galda基盤エラー',
|
|
7022
|
+
timeout: 'Galda側の実行上限や待ち時間で止まりました。変更内容の失敗とは限りません。',
|
|
7023
|
+
auth: '接続や認証確認で止まりました。接続を直してから再開できます。',
|
|
7024
|
+
relay: 'relay接続で止まりました。接続状態を確認してから再試行できます。',
|
|
7025
|
+
chrome: 'Chrome/ブラウザ確認で止まりました。画面確認環境の問題で、実装失敗とは限りません。',
|
|
7026
|
+
'test-runner': 'Galdaのtest runnerで止まりました。マシン負荷や起動待ちの可能性があります。',
|
|
7027
|
+
pr: 'PR作成で止まりました。GitHub/gh認証やpush状態を確認してから再試行できます。',
|
|
7028
|
+
'worker-start': 'worker起動で止まりました。Agentコマンドや実行環境を確認してから再開できます。',
|
|
7029
|
+
unknown: 'Galdaの実行基盤で止まりました。ログを確認し、原因に応じて再試行できます。',
|
|
7030
|
+
},
|
|
7031
|
+
actions: {
|
|
7032
|
+
retry: '再試行', resume: '再開', connect: 'Connect', check: '接続確認',
|
|
7033
|
+
other: '別Agentで実行', log: 'ログ確認', tests: 'テスト再実行', pr: 'PR再試行',
|
|
7034
|
+
},
|
|
7035
|
+
},
|
|
7036
|
+
en: {
|
|
7037
|
+
agent: { title: 'Agent error', body: 'Claude Code/Codex stopped while doing the work. The agent may need a revised instruction or another run.' },
|
|
7038
|
+
infra: {
|
|
7039
|
+
title: 'Galda system issue',
|
|
7040
|
+
timeout: 'Galda stopped this at the run limit or a wait timed out. This is not necessarily a problem with the change.',
|
|
7041
|
+
auth: 'Connection or authentication failed. Reconnect, then continue.',
|
|
7042
|
+
relay: 'The relay connection failed. Check the connection, then retry.',
|
|
7043
|
+
chrome: 'Chrome/browser verification failed. The UI check environment stopped, not necessarily the implementation.',
|
|
7044
|
+
'test-runner': 'Galda’s test runner stopped. Machine load or app startup timing may be the cause.',
|
|
7045
|
+
pr: 'PR creation failed. Check GitHub/gh auth or push state, then retry.',
|
|
7046
|
+
'worker-start': 'The worker could not start. Check the agent command or runtime, then continue.',
|
|
7047
|
+
unknown: 'Galda’s execution layer stopped. Check the log and retry based on the cause.',
|
|
7048
|
+
},
|
|
7049
|
+
actions: {
|
|
7050
|
+
retry: 'Retry', resume: 'Resume', connect: 'Connect', check: 'Check connection',
|
|
7051
|
+
other: 'Run with another Agent', log: 'View log', tests: 'Re-run tests', pr: 'Retry PR',
|
|
7052
|
+
},
|
|
7053
|
+
},
|
|
7054
|
+
};
|
|
7055
|
+
function failureLang(){
|
|
7056
|
+
try { return billingLang(); } catch { return document.documentElement.lang === 'ja' ? 'ja' : 'en'; }
|
|
7057
|
+
}
|
|
7058
|
+
function inferErrorClass(text, agent){
|
|
7059
|
+
const low = String(text || '').toLowerCase();
|
|
7060
|
+
const infra = (reason, actionIds) => ({ owner: 'galda-infra', reason, actionIds });
|
|
7061
|
+
if (/run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) return infra('timeout', ['retry', 'run-with-codex', 'view-log']);
|
|
7062
|
+
if (/spawn|enoent|eacces|command not found|worker tool exited early|作業ツールが途中で終了/.test(low)) return infra('worker-start', ['retry', 'run-with-codex', 'view-log']);
|
|
7063
|
+
if (/auth probe|preflight|not logged in|logged out|not authenticated|authentication|unauthorized|invalid api key|bad credentials|401\b|403\b|接続が必要|認証/.test(low)) return infra('auth', ['connect', 'check-connection', 'run-with-codex', 'view-log']);
|
|
7064
|
+
if (/relay|websocket|socket|econnreset|econnrefused|billing-api unreachable|cf access|cloudflare access/.test(low)) return infra('relay', ['retry', 'connect', 'view-log']);
|
|
7065
|
+
if (/chrome|chromium|puppeteer|target closed|browser|navigation timeout|waiting failed|screenshot|headless/.test(low)) return infra('chrome', ['retry', 'view-log']);
|
|
7066
|
+
if (/test runner|node --test|npm test|project tests|server exited \(code \d+\) before it was ready|did not report ready in time|test timed out after|tap|not ok \d+/.test(low)) return infra('test-runner', ['retry-tests', 'view-log']);
|
|
7067
|
+
if (/pr step failed|openpr|pull request|github|gh:|gh auth|git push|git fetch|git .*failed|pr作成|pr failed/.test(low)) return infra('pr', ['retry-pr', 'view-log']);
|
|
7068
|
+
return { owner: 'agent', reason: agent === 'codex' ? 'codex' : 'claude-code', actionIds: ['retry', 'run-with-codex', 'view-log'] };
|
|
7069
|
+
}
|
|
7070
|
+
function taskErrorMeta(t, goal){
|
|
7071
|
+
const meta = t.errorClass || inferErrorClass(t.result, t.agent || goal?.agent);
|
|
7072
|
+
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7073
|
+
const isInfra = meta.owner === 'galda-infra';
|
|
7074
|
+
return {
|
|
7075
|
+
...meta,
|
|
7076
|
+
title: isInfra ? C.infra.title : C.agent.title,
|
|
7077
|
+
body: isInfra ? (C.infra[meta.reason] || C.infra.unknown) : C.agent.body,
|
|
7078
|
+
copy: C,
|
|
7079
|
+
};
|
|
7080
|
+
}
|
|
7081
|
+
function goalBlockedErrorMeta(g){
|
|
7082
|
+
const raw = g.blocked?.errorClass || (g.blocked?.kind === 'test'
|
|
7083
|
+
? { owner: 'galda-infra', reason: 'test-runner', actionIds: ['retry-tests', 'view-log'] }
|
|
7084
|
+
: inferErrorClass(g.blocked?.reason, g.agent));
|
|
7085
|
+
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7086
|
+
const isInfra = raw.owner === 'galda-infra';
|
|
7087
|
+
return { ...raw, title: isInfra ? C.infra.title : C.agent.title, body: isInfra ? (C.infra[raw.reason] || C.infra.unknown) : C.agent.body, copy: C };
|
|
7088
|
+
}
|
|
7089
|
+
function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = false, agent = null, pr = false } = {}){
|
|
7090
|
+
const C = meta.copy.actions;
|
|
7091
|
+
const canCodex = goalId != null && agent !== 'codex' && (state.agents || []).includes('codex');
|
|
7092
|
+
const out = [];
|
|
7093
|
+
for (const id of meta.actionIds || []) {
|
|
7094
|
+
if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
|
|
7095
|
+
else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${C.other}</button>`);
|
|
7096
|
+
else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
|
|
7097
|
+
else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
|
|
7098
|
+
else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
|
|
7099
|
+
else if (id === 'retry-pr' && goalId != null) out.push(`<button class="errbtn" data-fsprretry="${goalId}">${C.pr}</button>`);
|
|
7100
|
+
else if (id === 'view-log' && taskId != null) out.push(`<button class="errbtn" data-fslog="${taskId}">${C.log}</button>`);
|
|
7101
|
+
else if (id === 'view-log' && goalId != null) out.push(`<button class="errbtn" data-fsglog="${goalId}">${C.log}</button>`);
|
|
7102
|
+
}
|
|
7103
|
+
if (!out.length && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
|
|
7104
|
+
if (pr && !out.some((h) => h.includes('data-fsprretry'))) out.unshift(`<button class="errbtn" data-fsprretry="${goalId}">${C.pr}</button>`);
|
|
7105
|
+
return out.join('');
|
|
7106
|
+
}
|
|
6941
7107
|
// To Do (below Review): Doing rows with bar + mono %, Next up in queue order,
|
|
6942
7108
|
// error as a small in-row colour element (§1.5-8), quiet Done fold.
|
|
6943
7109
|
function renderFsTodo(){
|
|
@@ -6963,6 +7129,12 @@ function renderFsTodo(){
|
|
|
6963
7129
|
&& !pausedGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id)
|
|
6964
7130
|
&& list.some((t) => t.goalId === g.id && t.reply && t.status === 'queued')
|
|
6965
7131
|
&& !list.some((t) => t.goalId === g.id && !t.reply && ['running', 'queued'].includes(t.status)));
|
|
7132
|
+
const finalizingGoals = pgoals.filter((g) => g.status === 'running'
|
|
7133
|
+
&& !pausedGoalIds.has(g.id)
|
|
7134
|
+
&& !running.some((t) => t.goalId === g.id)
|
|
7135
|
+
&& !parkedReplyGoals.some((x) => x.id === g.id)
|
|
7136
|
+
&& list.some((t) => t.goalId === g.id)
|
|
7137
|
+
&& list.filter((t) => t.goalId === g.id).every((t) => ['done', 'skipped'].includes(t.status)));
|
|
6966
7138
|
const tt = (s, n) => esc(String(s ?? '').replace(/\s+/g, ' ').slice(0, n ?? 70));
|
|
6967
7139
|
// LATER = deliberately shelved goals (composer "Later" send → server status 'pending';
|
|
6968
7140
|
// PRD §8.2 top gap — they were invisible in flagship). Staged disclosure: the cap
|
|
@@ -6984,9 +7156,10 @@ function renderFsTodo(){
|
|
|
6984
7156
|
running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
|
|
6985
7157
|
const goal = pgoals.find((g) => g.id === t.goalId);
|
|
6986
7158
|
const title = t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
|
|
6987
|
-
return `<div class="drow"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
|
|
7159
|
+
return `<div class="drow"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="stword run">Running</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
|
|
6988
7160
|
<div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
|
|
6989
7161
|
}).join('')
|
|
7162
|
+
+ finalizingGoals.map((g) => `<div class="drow"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword wait">Finalizing</span>${fsActs(g.id)}</div></div>`).join('')
|
|
6990
7163
|
// 一時停止したゴールは Doing に「Paused」1行(アンバー)で留める+Resume。走行/中断行は上と needsRows で抑止済み。
|
|
6991
7164
|
+ pgoals.filter((g) => pausedGoalIds.has(g.id)).map((g) => `<div class="drow"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword int">Paused</span><button class="ab txt" data-fspause="1" title="Resume — 再開">Resume</button></div></div>`).join('');
|
|
6992
7165
|
// NEEDS YOU: stopped/errored work that used to fold into Doing, now under its own caption so
|
|
@@ -7006,19 +7179,23 @@ function renderFsTodo(){
|
|
|
7006
7179
|
// exists (server-side retry = "resume if possible, else re-run"; there is no separate
|
|
7007
7180
|
// resume endpoint to call).
|
|
7008
7181
|
failed.filter((t) => !pausedGoalIds.has(t.goalId) && !resumedGoalIds.has(t.goalId)).map((t) => { const isInt = t.status === 'interrupted';
|
|
7009
|
-
const
|
|
7182
|
+
const goal = pgoals.find((g) => g.id === t.goalId);
|
|
7183
|
+
const meta = taskErrorMeta(t, goal);
|
|
7184
|
+
const word = isInt ? 'Interrupted' : 'Failed', icon = isInt ? '⏸' : '✗';
|
|
7010
7185
|
// don't restate the state word as the "reason" too (e.g. "Interrupted / Interrupted")
|
|
7011
7186
|
// when the task has no distinct result text — the coloured word already said it, so
|
|
7012
7187
|
// the reason line only prints when there's real detail to add.
|
|
7013
7188
|
const reason = String(t.result || '').trim();
|
|
7014
7189
|
const reasonLine = reason && reason.toLowerCase() !== word.toLowerCase()
|
|
7015
|
-
? `<div class="msg"><span class="x ${isInt ? 'int' : ''}">${icon}</span>${tt(reason,
|
|
7190
|
+
? `<div class="msg"><span class="x ${isInt ? 'int' : ''}">${icon}</span><b>${esc(meta.title)}</b> — ${esc(meta.body)} <span class="dim">${tt(reason, 160)}</span></div>`
|
|
7191
|
+
: `<div class="msg"><span class="x ${isInt ? 'int' : ''}">${icon}</span><b>${esc(meta.title)}</b> — ${esc(meta.body)}</div>`;
|
|
7016
7192
|
return `<div class="drow err${isInt ? ' int' : ''}"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run ${isInt ? 'int' : 'bad'}"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span><span class="stword ${isInt ? 'int' : ''}">${word}</span>${fsActs(t.goalId)}</div>
|
|
7017
7193
|
<div class="errline">${reasonLine}
|
|
7018
|
-
<div class="acts2"
|
|
7019
|
-
+ blocked.map((g) => g.blocked?.kind === 'paused'
|
|
7020
|
-
|
|
7021
|
-
<div class="
|
|
7194
|
+
<div class="acts2">${errorButtonsHtml(meta, { taskId: t.id, goalId: t.goalId, interrupted: isInt, agent: t.agent || goal?.agent })}</div></div></div>`; }).join('')
|
|
7195
|
+
+ blocked.map((g) => { if (g.blocked?.kind === 'paused') return ''; const meta = goalBlockedErrorMeta(g);
|
|
7196
|
+
return `<div class="drow err"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run bad"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword">Failed</span></div>
|
|
7197
|
+
<div class="errline"><div class="msg"><span class="x">✗</span><b>${esc(meta.title)}</b> — ${esc(meta.body)} <span class="dim">${tt(g.blocked?.reason ?? 'blocked', 160)}</span></div>
|
|
7198
|
+
<div class="acts2">${errorButtonsHtml(meta, { goalId: g.id, agent: g.agent })}<button class="errbtn" data-fsarchive="${g.id}">Archive</button></div></div></div>`; }).join('')
|
|
7022
7199
|
+ needsInput.map((g) => `<div class="drow err int"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.text || g.plan?.[0], 60)}</span><span class="stword int">Needs input</span></div>
|
|
7023
7200
|
<div class="errline"><div class="msg"><span class="x int">?</span>${tt(g.question?.text || 'A question is waiting on you', 180)}</div>
|
|
7024
7201
|
<div class="acts2"><button class="errbtn" data-fsglog="${g.id}">Answer</button></div></div></div>`).join('');
|
|
@@ -7045,16 +7222,16 @@ function renderFsTodo(){
|
|
|
7045
7222
|
}
|
|
7046
7223
|
return `<div class="trow up${i >= 2 ? ' dim' : ''}" data-goalid="${t.goalId ?? ''}" data-taskid="${t.id}"${t.goalId != null ? ` data-fsgoalrow="${t.goalId}"` : ''}><span class="st"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span>${codexAction(t.goalId, t.agent)}${fsActs(t.goalId)}</div>${grp}`;
|
|
7047
7224
|
}).join('');
|
|
7048
|
-
//
|
|
7049
|
-
// "
|
|
7050
|
-
//
|
|
7051
|
-
// the whole lane ("To Do"), so it now counts everything the lane actually renders
|
|
7052
|
-
// below it: Doing (running) + Needs you (failed/interrupted tasks + stopped/needsInput
|
|
7053
|
-
// goals) + Next up (queued) + Later (pending goals). "· next-up order" stays keyed off
|
|
7054
|
-
// queued.length specifically, since that suffix describes the Next-up sublist's own sort,
|
|
7055
|
-
// not the lane total.
|
|
7225
|
+
// Keep executable work and deliberately-shelved Later work visibly distinct.
|
|
7226
|
+
// "To Do 6" while all six were in Later made an idle queue look broken: Later
|
|
7227
|
+
// never auto-starts by design. The header now tells the operational truth.
|
|
7056
7228
|
const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
|
|
7057
|
-
const
|
|
7229
|
+
const doingCount = running.length + finalizingGoals.length + pausedGoalIds.size;
|
|
7230
|
+
const nextCount = parkedReplyGoals.length + queued.length;
|
|
7231
|
+
const hasPipelineWork = doingCount > 0 || nextCount > 0 || pgoals.some((g) => ['planning', 'stacked'].includes(g.status));
|
|
7232
|
+
const laterIdle = later.length && !hasPipelineWork
|
|
7233
|
+
? `<div class="lateridle"><span class="msg">${document.documentElement.lang === 'ja' ? `実行待ちはありません。Laterの${later.length}件は自動開始されません。` : `Nothing is queued. ${later.length} Later item${later.length === 1 ? '' : 's'} will not start automatically.`}</span><button data-fsstartnextlater="${later[0].id}">${document.documentElement.lang === 'ja' ? '次を開始' : 'Start next'}</button></div>`
|
|
7234
|
+
: '';
|
|
7058
7235
|
// Free-tier pending counter (H22): on the free plan the header shows the wall
|
|
7059
7236
|
// — pending goals out of the 5-slot cap — reddening at the cap and gone once
|
|
7060
7237
|
// paid. Reads the SAME numbers the gate enforces (state.billing, never a local
|
|
@@ -7065,7 +7242,7 @@ function renderFsTodo(){
|
|
|
7065
7242
|
const _showFc = _bill.plan !== 'paid' && Number.isFinite(_fp) && Number.isFinite(_fl);
|
|
7066
7243
|
const _todoNum = _showFc
|
|
7067
7244
|
? `<span class="fcount fstdlabel${_fp >= _fl ? ' cap' : ''}">${_fp}<span class="fc-l">/${_fl}</span></span>`
|
|
7068
|
-
: `<span class="num fstdlabel">${
|
|
7245
|
+
: `<span class="num fstdlabel">${nextCount} next · ${doingCount} doing${later.length ? ` · ${later.length} later` : ''}</span>`;
|
|
7069
7246
|
el.innerHTML = `<div class="lhd sechd" id="fsTodoHd"><span class="hd fstdlabel">To Do</span>${_todoNum}<span class="fold fstdlabel">▾</span></div>
|
|
7070
7247
|
<div class="tl">
|
|
7071
7248
|
${(doingRows || paused) ? `<span class="tlcap tlcap-doing" style="padding-top:0">Doing<button class="tlcap-pause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume — 再開' : 'Pause — 一時停止'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button></span>${doingRows}` : ''}
|
|
@@ -7073,15 +7250,23 @@ function renderFsTodo(){
|
|
|
7073
7250
|
${rejected.length ? `<span class="tlcap rejcap"${(doingRows || paused) || needsRows ? '' : ' style="padding-top:0"'}>Dismissed<span class="num">${rejected.length}</span></span>${rejected.map((g) => `<div class="trow up rej" data-fsgoalrow="${g.id}"><span class="st rej"></span>${noSpan(g.id)}<span class="tt">${tt(g.text, 70)}</span><span class="acts"><button class="ab" data-fsreopen="${g.id}" title="Reopen — back to Review">${FS_ICONS.reopen}</button><button class="ab" data-fsrejdel="${g.id}" title="Delete — discard (closes the PR)">${FS_ICONS.del}</button></span></div>`).join('')}` : ''}
|
|
7074
7251
|
${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
|
|
7075
7252
|
${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
|
|
7076
|
-
|
|
7077
|
-
<
|
|
7253
|
+
<div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
|
|
7254
|
+
<span class="tlcap">Later</span>
|
|
7255
|
+
<div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
|
|
7256
|
+
${laterIdle}
|
|
7257
|
+
${laterRows}
|
|
7078
7258
|
</div>
|
|
7079
7259
|
${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
|
|
7080
7260
|
<div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.map((t) => `<div class="trow up dim done"${t.goalId != null ? ` data-fsgoalrow="${t.goalId}"` : ''}><span class="st ok"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span></div>`).join('')}</div>` : ''}`;
|
|
7081
7261
|
el.classList.toggle('folded', !!fsUI.todoFold);
|
|
7082
7262
|
$('fsTodoHd').onclick = () => { fsUI.todoFold = !fsUI.todoFold; el.classList.toggle('folded', fsUI.todoFold); };
|
|
7083
|
-
const qadd = el.querySelector('.qadd[data-fsnew]');
|
|
7084
|
-
if (qadd) qadd.onclick = () =>
|
|
7263
|
+
const qadd = el.querySelector('.qadd[data-fsnew]');
|
|
7264
|
+
if (qadd) qadd.onclick = (e) => { e.stopPropagation(); fsInlineNewRow(qadd, 'Add to To Do…', (text) => fsCreateGoal(text, false)); };
|
|
7265
|
+
const laterAdd = el.querySelector('.qadd[data-fslaternew]');
|
|
7266
|
+
if (laterAdd) laterAdd.onclick = (e) => {
|
|
7267
|
+
e.stopPropagation();
|
|
7268
|
+
fsInlineNewRow(laterAdd, 'Add to Later…', (text) => fsCreateGoal(text, true));
|
|
7269
|
+
};
|
|
7085
7270
|
const dl = el.querySelector('#fsDoneLine');
|
|
7086
7271
|
if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
|
|
7087
7272
|
for (const r of el.querySelectorAll('[data-fsgoalrow]')) r.onclick = (e) => {
|
|
@@ -7108,12 +7293,10 @@ function renderFsTodo(){
|
|
|
7108
7293
|
};
|
|
7109
7294
|
for (const b of el.querySelectorAll('[data-fspause]')) b.onclick = togglePauseActive; // list版 Doing 一時停止/再生(+Paused行のResume)
|
|
7110
7295
|
for (const b of el.querySelectorAll('[data-fsretry]')) b.onclick = () => fsRetryTask(b.dataset.fsretry);
|
|
7111
|
-
for (const b of el.querySelectorAll('[data-fscodex]')) b.onclick =
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
await refresh();
|
|
7116
|
-
};
|
|
7296
|
+
for (const b of el.querySelectorAll('[data-fscodex]')) b.onclick = (e) => { e.stopPropagation(); fsRunWithCodexGoal(b); };
|
|
7297
|
+
for (const b of el.querySelectorAll('[data-fsconnectworker]')) b.onclick = (e) => { e.stopPropagation(); fsConnectWorker(b); };
|
|
7298
|
+
for (const b of el.querySelectorAll('[data-fscheckworker]')) b.onclick = (e) => { e.stopPropagation(); fsCheckWorker(b); };
|
|
7299
|
+
for (const b of el.querySelectorAll('[data-fsprretry]')) b.onclick = (e) => { e.stopPropagation(); fsRetryPrGoal(b); };
|
|
7117
7300
|
for (const b of el.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
7118
7301
|
for (const b of el.querySelectorAll('[data-fsglog]')) b.onclick = () => toggleGoalDetail(Number(b.dataset.fsglog));
|
|
7119
7302
|
for (const b of el.querySelectorAll('[data-fsreverify]')) b.onclick = async () => {
|
|
@@ -7130,6 +7313,12 @@ function renderFsTodo(){
|
|
|
7130
7313
|
for (const b of el.querySelectorAll('[data-fspstart]')) b.onclick = async (e) => { e.stopPropagation();
|
|
7131
7314
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fspstart}/activate`), { method: 'POST' });
|
|
7132
7315
|
if (!r.ok) showErr('Start failed.'); else await refresh(); };
|
|
7316
|
+
const startNextLater = el.querySelector('[data-fsstartnextlater]');
|
|
7317
|
+
if (startNextLater) startNextLater.onclick = async (e) => { e.stopPropagation();
|
|
7318
|
+
startNextLater.disabled = true;
|
|
7319
|
+
const r = await fetch(withKey(`/api/goals/${startNextLater.dataset.fsstartnextlater}/activate`), { method: 'POST' });
|
|
7320
|
+
if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; }
|
|
7321
|
+
else await refresh(); };
|
|
7133
7322
|
for (const b of el.querySelectorAll('[data-fspdel]')) b.onclick = async (e) => { e.stopPropagation();
|
|
7134
7323
|
const preview = b.closest('.trow')?.querySelector('.tt')?.textContent || '';
|
|
7135
7324
|
if (!(await confirmDelete({ title: 'Delete this shelved goal?', preview }))) return;
|
|
@@ -7153,6 +7342,51 @@ function renderFsTodo(){
|
|
|
7153
7342
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsdel}`), { method: 'DELETE' });
|
|
7154
7343
|
if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
|
|
7155
7344
|
}
|
|
7345
|
+
async function fsCreateGoal(text, pending){
|
|
7346
|
+
const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
7347
|
+
body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending }) });
|
|
7348
|
+
if (r.status === 402) {
|
|
7349
|
+
const data = await r.json().catch(() => ({}));
|
|
7350
|
+
showBillingBlocked(data.blocked);
|
|
7351
|
+
return false;
|
|
7352
|
+
}
|
|
7353
|
+
if (!r.ok) { showErr(pending ? 'Later add failed.' : 'To Do add failed.'); return false; }
|
|
7354
|
+
clearBillingBlocked();
|
|
7355
|
+
const goal = await r.json();
|
|
7356
|
+
if (goal?.id) upsert(state.goals, goal);
|
|
7357
|
+
render();
|
|
7358
|
+
return true;
|
|
7359
|
+
}
|
|
7360
|
+
function fsInlineNewRow(row, placeholder, onSubmit){
|
|
7361
|
+
if (!row) return;
|
|
7362
|
+
const current = row.querySelector('input');
|
|
7363
|
+
if (current) { current.focus(); return; }
|
|
7364
|
+
const label = row.querySelector('span:last-child');
|
|
7365
|
+
if (!label) return;
|
|
7366
|
+
const original = label.textContent;
|
|
7367
|
+
const inp = document.createElement('input');
|
|
7368
|
+
inp.type = 'text'; inp.placeholder = placeholder; inp.setAttribute('aria-label', placeholder);
|
|
7369
|
+
label.replaceWith(inp); row.classList.add('editing'); inp.focus();
|
|
7370
|
+
let done = false;
|
|
7371
|
+
const restore = () => {
|
|
7372
|
+
if (done) return;
|
|
7373
|
+
done = true; row.classList.remove('editing');
|
|
7374
|
+
if (inp.isConnected) { const span = document.createElement('span'); span.textContent = original; inp.replaceWith(span); }
|
|
7375
|
+
};
|
|
7376
|
+
inp.onkeydown = (e) => {
|
|
7377
|
+
if (e.key === 'Enter' && !e.isComposing) {
|
|
7378
|
+
e.preventDefault();
|
|
7379
|
+
const value = inp.value.trim();
|
|
7380
|
+
if (!value) return;
|
|
7381
|
+
done = true; row.classList.remove('editing'); inp.disabled = true;
|
|
7382
|
+
Promise.resolve(onSubmit(value)).then((ok) => {
|
|
7383
|
+
if (ok !== false || !inp.isConnected) return;
|
|
7384
|
+
done = false; row.classList.add('editing'); inp.disabled = false; inp.focus();
|
|
7385
|
+
});
|
|
7386
|
+
} else if (e.key === 'Escape') restore();
|
|
7387
|
+
};
|
|
7388
|
+
inp.onblur = () => setTimeout(restore, 150);
|
|
7389
|
+
}
|
|
7156
7390
|
function fsInlineInput(afterEl, value, placeholder, onSubmit){
|
|
7157
7391
|
if (!afterEl || afterEl.nextElementSibling?.classList.contains('fsinline')) return;
|
|
7158
7392
|
const wrap = document.createElement('div'); wrap.className = 'fsinline';
|
|
@@ -8074,6 +8308,7 @@ async function refresh(){
|
|
|
8074
8308
|
renderModelSelect();
|
|
8075
8309
|
renderEffortSelect();
|
|
8076
8310
|
state.queueOrder = s.queueOrder ?? {}; state.externalActivity = s.externalActivity ?? [];
|
|
8311
|
+
state.runningCounts = s.runningCounts ?? {}; state.parallelLimits = s.parallelLimits ?? null;
|
|
8077
8312
|
state.workflowColumns = s.workflowColumns ?? {};
|
|
8078
8313
|
state.reviewDefinitions = s.reviewDefinitions ?? {};
|
|
8079
8314
|
checkUiVersion(s.uiVersion);
|
|
@@ -8138,7 +8373,7 @@ function connectSSE(){
|
|
|
8138
8373
|
// review→done flip on a goal that has a PR means the user just merged it on GitHub and
|
|
8139
8374
|
// came back. Celebrate (confetti) + a "merged" toast so the merge is unmistakable.
|
|
8140
8375
|
if (prev && prev.status === 'review' && m.goal.status === 'done' && m.goal.pr) {
|
|
8141
|
-
try { celebrateApprove(); } catch {}
|
|
8376
|
+
try { finishOpenReview(m.goal); } catch { try { celebrateApprove(); } catch {} }
|
|
8142
8377
|
const prNo = (String(m.goal.pr).match(/pull\/(\d+)/) ?? [, ''])[1];
|
|
8143
8378
|
showToast(`PR #${prNo} merged — “${(m.goal.text || '').replace(/\s+/g, ' ').slice(0, 48)}” is Done`);
|
|
8144
8379
|
}
|
|
@@ -8376,7 +8611,7 @@ async function postOutboxItem(item){
|
|
|
8376
8611
|
posting.add(item.oid);
|
|
8377
8612
|
try {
|
|
8378
8613
|
const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
8379
|
-
body: JSON.stringify({ projectId: item.projectId, text: item.text, pr: item.pr, model: item.model, effort: item.effort, agent: item.agent, mode: item.mode, skill: item.skill, worktree: item.worktree, images: item.images, pending: item.pending, review: item.review }) });
|
|
8614
|
+
body: JSON.stringify({ projectId: item.projectId, text: item.text, pr: item.pr, model: item.model, effort: item.effort, agent: item.agent, mode: item.mode, skill: item.skill, workspaceMode: item.workspaceMode, worktree: item.worktree, images: item.images, pending: item.pending, review: item.review }) });
|
|
8380
8615
|
if (r.status === 402) {
|
|
8381
8616
|
const data = await r.json().catch(() => ({}));
|
|
8382
8617
|
outbox.mark(item.oid, 'unsent'); // same status as any other failed send — the existing 15s auto-flush retries it (and starts succeeding the moment a license activates), the existing queue resend affordance also works
|
|
@@ -8636,8 +8871,8 @@ async function send(){
|
|
|
8636
8871
|
oid: `o${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
8637
8872
|
projectId: state.active, text, pr: 'auto', model, effort, agent: state.connectedApp,
|
|
8638
8873
|
mode, skill: skill || null,
|
|
8639
|
-
|
|
8640
|
-
//
|
|
8874
|
+
workspaceMode: workspaceMode(),
|
|
8875
|
+
// Keep the old wire contract too: only direct sends worktree:false.
|
|
8641
8876
|
worktree: worktreeOn() ? undefined : false,
|
|
8642
8877
|
pending: later, review,
|
|
8643
8878
|
images: attach.map((a) => a.path), imageUrls: attach.map((a) => a.url),
|
|
@@ -8833,6 +9068,27 @@ function latestGoalOutcomeTask(goalId, tasks){
|
|
|
8833
9068
|
.sort((a, b) => Date.parse(b.finishedAt || b.startedAt || b.createdAt || 0)
|
|
8834
9069
|
- Date.parse(a.finishedAt || a.startedAt || a.createdAt || 0))[0] || null;
|
|
8835
9070
|
}
|
|
9071
|
+
function taskAttemptId(t){ return t?.attemptId || (t?.id != null ? `task-${t.id}` : null); }
|
|
9072
|
+
function taskSortTime(t){ return Date.parse(t?.finishedAt || t?.startedAt || t?.createdAt || 0) || 0; }
|
|
9073
|
+
function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
9074
|
+
if (outcome?.run) return { taskId: outcome.id, attemptId: taskAttemptId(outcome), inherited: false, ...outcome.run };
|
|
9075
|
+
const runs = (tasks || []).filter((t) => t.goalId === goalId && t.run)
|
|
9076
|
+
.sort((a, b) => taskSortTime(b) - taskSortTime(a));
|
|
9077
|
+
if (!runs.length) return null;
|
|
9078
|
+
const cutoff = outcome ? taskSortTime(outcome) : Infinity;
|
|
9079
|
+
const prev = runs.find((t) => t.id !== outcome?.id && taskSortTime(t) <= cutoff) || (!outcome ? runs[0] : null);
|
|
9080
|
+
if (!prev) return null;
|
|
9081
|
+
const note = '前回のpreviewを継承しています: 最新replyは新しいpreviewを宣言しませんでした。 / Inherited previous preview: the latest reply did not declare a new preview.';
|
|
9082
|
+
return {
|
|
9083
|
+
taskId: prev.id,
|
|
9084
|
+
attemptId: taskAttemptId(outcome) || taskAttemptId(prev),
|
|
9085
|
+
inherited: true,
|
|
9086
|
+
inheritedFromTaskId: prev.id,
|
|
9087
|
+
inheritedForTaskId: outcome?.id ?? null,
|
|
9088
|
+
...prev.run,
|
|
9089
|
+
note: prev.run?.note ? `${note} ${prev.run.note}` : note,
|
|
9090
|
+
};
|
|
9091
|
+
}
|
|
8836
9092
|
// One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
|
|
8837
9093
|
// "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
|
|
8838
9094
|
// whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
|
|
@@ -8873,6 +9129,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8873
9129
|
check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
|
|
8874
9130
|
goalText: String(g?.text ?? '').replace(/\s+/g, ' ').trim(),
|
|
8875
9131
|
did, siblings, risk: g?.risk ?? null,
|
|
9132
|
+
verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
|
|
8876
9133
|
note: g?.retest?.passed ? g.retest.note : null,
|
|
8877
9134
|
pr: r.pr,
|
|
8878
9135
|
// P0b (Masa 2026-07-21): the AI's own TodoWrite breakdown for the "Steps" section
|
|
@@ -8918,8 +9175,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8918
9175
|
// written by the worker into .manager-run.json). Absent for asks with nothing to
|
|
8919
9176
|
// run — then no button, which is the honest state, not a missing feature.
|
|
8920
9177
|
run: (() => {
|
|
8921
|
-
|
|
8922
|
-
return t ? { taskId: t.id, ...t.run } : null;
|
|
9178
|
+
return latestGoalRunForOutcome(g?.id, ts, outcome);
|
|
8923
9179
|
})(),
|
|
8924
9180
|
// Explicit review-summary display fields (spec §11: the card's shape follows the
|
|
8925
9181
|
// task). A goal's reviewSummary may carry them directly (e.g. a conversational task
|
|
@@ -8974,6 +9230,7 @@ function saSoloDigestRow(g, ts){
|
|
|
8974
9230
|
return {
|
|
8975
9231
|
goalIds: [g.id], reviewNumber: goalReviewNumber({ goal: g, tasks: ts }), checkLine,
|
|
8976
9232
|
pr: g.pr ?? null, testResult: g.testResult ?? null,
|
|
9233
|
+
verificationInfraError: g.verificationInfraError ?? null,
|
|
8977
9234
|
proof: proofTask?.proof ?? null, proofTaskId: proofTask?.id ?? null, plan: false, planText: null,
|
|
8978
9235
|
};
|
|
8979
9236
|
}
|
|
@@ -9348,6 +9605,11 @@ function saItemHtml(it, i){
|
|
|
9348
9605
|
// compare-only or title-only declaration must not draw the "Start it" button.
|
|
9349
9606
|
const openBody = (it.run?.cmd || it.run?.url)
|
|
9350
9607
|
? `<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>`
|
|
9608
|
+
: it.pr
|
|
9609
|
+
? `<div class="ropen"><a class="olink" href="${esc(it.pr)}" target="_blank" rel="noopener">${esc(it.pr)}<span class="ar">↗</span></a><p class="rbody v">Review and merge this pull request to finish the item.</p></div>`
|
|
9610
|
+
: '';
|
|
9611
|
+
const prRepairBody = it.goal?.prError
|
|
9612
|
+
? `<div class="prfail"><b>${esc(it.goal.prRepair?.title || 'PR blocked')}</b> <span class="dim">${esc(it.goal.prError)}</span>${it.goal.prRepair?.steps?.length ? `<ul>${it.goal.prRepair.steps.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : ''}${it.goal.prRepair?.dirtyFiles?.length ? `<div class="dim">dirty: ${it.goal.prRepair.dirtyFiles.map(esc).join(' · ')}</div>` : ''}<div class="acts2"><button class="errbtn" data-saact="retrypr:${i}">${failureLang() === 'ja' ? 'PR再試行' : 'Retry PR'}</button><button class="errbtn" data-saact="glog:${i}">${failureLang() === 'ja' ? 'ログ確認' : 'View log'}</button></div></div>`
|
|
9351
9613
|
: '';
|
|
9352
9614
|
// ── Also carries (Masa 2026-07-21 → refined 2026-07-23): goals folded onto this review are
|
|
9353
9615
|
// logged HERE, inside the review's own content — NOT as loose rows in the Review column
|
|
@@ -9383,6 +9645,10 @@ function saItemHtml(it, i){
|
|
|
9383
9645
|
// change explains itself here, and an image the AI pasted shows up here as an image.
|
|
9384
9646
|
// We interpret nothing; mdLite only turns Markdown into the shapes it already means. ──
|
|
9385
9647
|
const reportBody = it.report ? `<div class="rreport">${mdLite(it.report, { images: false })}</div>` : '';
|
|
9648
|
+
const infra = it.verificationInfraError;
|
|
9649
|
+
const infraBody = infra?.detail
|
|
9650
|
+
? `<div class="rinfra"><p class="rbody">${esc(infra.detail)}</p><button type="button" class="retrybtn" data-saact="reverify:${i}">再検証</button></div>`
|
|
9651
|
+
: '';
|
|
9386
9652
|
// ── Screenshots (P2, Masa 2026-07-22): the pictures the AI took of its own result.
|
|
9387
9653
|
// Full width, at their own aspect — a four-variant comparison sheet is unreadable
|
|
9388
9654
|
// shrunk into a thumbnail, and that is the shape the AI actually produces. The file
|
|
@@ -9508,9 +9774,11 @@ function saItemHtml(it, i){
|
|
|
9508
9774
|
${meta}
|
|
9509
9775
|
${reqSec}
|
|
9510
9776
|
${S('Open it', openBody)}
|
|
9777
|
+
${S('PR', prRepairBody)}
|
|
9511
9778
|
${S('Also carries', carriesBody)}
|
|
9512
9779
|
${S('What to check', checkBody)}
|
|
9513
9780
|
${S('Result', reportBody)}
|
|
9781
|
+
${S(infra?.title || '検証基盤エラー', infraBody)}
|
|
9514
9782
|
${S('Screenshots', shotsBody)}
|
|
9515
9783
|
${S('Steps', stepsBody)}
|
|
9516
9784
|
${S('Proof', proofInner)}
|
|
@@ -9967,8 +10235,17 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9967
10235
|
}
|
|
9968
10236
|
const it = SA.items[i];
|
|
9969
10237
|
if (!it) return;
|
|
10238
|
+
if (k === 'retrypr') return fsRetryPrGoal({ disabled: false, textContent: '', dataset: { fsprretry: String(it.goalId) } });
|
|
10239
|
+
if (k === 'glog') return toggleGoalDetail(it.goalId);
|
|
9970
10240
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
9971
10241
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
10242
|
+
if (k === 'reverify') {
|
|
10243
|
+
b.disabled = true;
|
|
10244
|
+
b.textContent = '再検証中…';
|
|
10245
|
+
const r = await fetch(withKey(`/api/goals/${it.goalId}/reverify`), { method: 'POST' });
|
|
10246
|
+
if (r.ok) await refresh(); else showErr('再検証に失敗');
|
|
10247
|
+
return;
|
|
10248
|
+
}
|
|
9972
10249
|
// Open it (P1, Masa 2026-07-21): start the thing the worker said to run, then open it.
|
|
9973
10250
|
// Booting a real process takes seconds, so the chip says "Starting…" while it waits —
|
|
9974
10251
|
// and a failure lands in the detail line as a plain reason. It never turns the goal
|
|
@@ -10162,16 +10439,30 @@ function renderCtxBar(){
|
|
|
10162
10439
|
const openRow = `<button type="button" class="appopt ctxopen" id="ctxopenfolder" title="Open the OS folder chooser"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M12 11v6M9 14h6"/></svg><span class="nm">Open folder…</span></button>`;
|
|
10163
10440
|
menu.innerHTML = `${opts}${openRow}<div class="ctxpath"><input type="text" id="ctxdirin" placeholder="~/path/to/repo" aria-label="Folder path" spellcheck="false"><button type="button" class="appopt" id="ctxdiruse" style="width:auto">Use</button></div><div class="ctxmsg" id="ctxdirmsg"></div>`;
|
|
10164
10441
|
}
|
|
10165
|
-
//
|
|
10166
|
-
//
|
|
10167
|
-
//
|
|
10168
|
-
// someone means when they want to watch it happen. Sticky per project, like model and
|
|
10169
|
-
// effort, because it is a working habit rather than a per-message thought.
|
|
10442
|
+
// Workspace mode. isolated worktree is the default clean copy; direct workspace inherits
|
|
10443
|
+
// and edits the current folder. Sticky per project, like model and effort, because it is
|
|
10444
|
+
// a working habit rather than a per-message thought.
|
|
10170
10445
|
function worktreeKey(){ return `sel:worktree:${state.active}`; }
|
|
10171
10446
|
function worktreeOn(){ return localStorage.getItem(worktreeKey()) !== '0'; }
|
|
10447
|
+
function workspaceMode(){ return worktreeOn() ? 'isolated-worktree' : 'direct-workspace'; }
|
|
10448
|
+
function goalWorkspaceMode(g){ return (g?.workspaceMode === 'direct-workspace' || g?.worktree === false) ? 'direct-workspace' : 'isolated-worktree'; }
|
|
10449
|
+
function workspaceModeText(g){ return goalWorkspaceMode(g) === 'direct-workspace' ? 'direct workspace' : 'isolated worktree'; }
|
|
10172
10450
|
function renderWorktreeChip(){
|
|
10173
10451
|
const el = $('ctxwt'); if (!el) return;
|
|
10174
|
-
|
|
10452
|
+
const isolated = worktreeOn();
|
|
10453
|
+
const proj = state.projects.find((p) => p.id === state.active);
|
|
10454
|
+
el.setAttribute('aria-checked', isolated ? 'true' : 'false');
|
|
10455
|
+
const label = $('ctxwtlabel');
|
|
10456
|
+
if (label) label.textContent = isolated ? 'isolated worktree' : 'direct workspace';
|
|
10457
|
+
el.title = isolated
|
|
10458
|
+
? 'isolated worktree: default clean copy; this folder is not edited.'
|
|
10459
|
+
: 'direct workspace: inherits the current state and edits this folder.';
|
|
10460
|
+
const warn = $('ctxworkwarn');
|
|
10461
|
+
if (warn) {
|
|
10462
|
+
const dirty = !isolated && !!proj?.dirty;
|
|
10463
|
+
warn.hidden = !dirty;
|
|
10464
|
+
warn.textContent = dirty ? `Dirty workspace warning: direct workspace will edit ${folderName(proj?.dir) || 'this folder'} as it is now.` : '';
|
|
10465
|
+
}
|
|
10175
10466
|
}
|
|
10176
10467
|
// Asked for once, and only when the menu is first opened — the same "only when you look
|
|
10177
10468
|
// at it" rule the implementation log and the conversation follow.
|