@galda/cli 0.10.68 → 0.10.70
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 +434 -118
- package/app/review-merge-celebration-preview.html +21 -0
- package/engine/chrome-safety.mjs +34 -0
- package/engine/lib.mjs +563 -3
- package/engine/server.mjs +479 -113
- package/engine/shot.mjs +4 -0
- package/engine/verify.mjs +27 -6
- 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
|
|
@@ -1758,10 +1772,9 @@
|
|
|
1758
1772
|
/* ---- + New (CDO decision 5, Masa 2026-07-17): a quiet add-row where + sits in the ● column
|
|
1759
1773
|
and New in the text column — aligned to the row's own grid, not a box shoved right. The
|
|
1760
1774
|
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. ---- */
|
|
1775
|
+
that box so the GLYPH lines up, not the box (Masa aesthetic: align glyphs, not boxes). The
|
|
1776
|
+
bottom List row focuses the composer; the Later row below the Later caption posts pending:true
|
|
1777
|
+
directly so it shelves work without spawning a worker. ---- */
|
|
1765
1778
|
#fsRoot .paplus{width:13px;height:13px;flex:0 0 13px;position:relative;display:inline-block}
|
|
1766
1779
|
#fsRoot .paplus::before,#fsRoot .paplus::after{content:"";position:absolute;background:currentColor;border-radius:1px}
|
|
1767
1780
|
#fsRoot .paplus::before{left:5.85px;top:2px;width:1.3px;height:9px}
|
|
@@ -1769,6 +1782,10 @@
|
|
|
1769
1782
|
/* list rail add-row — padding-left:8px puts + on the .trow ● (8px pad + 13px ● + 9px gap grid) */
|
|
1770
1783
|
#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
1784
|
#fsRoot .qadd:hover{opacity:1;color:var(--ink)}
|
|
1785
|
+
#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}
|
|
1786
|
+
#fsRoot .lateridle .msg{flex:1;min-width:0}
|
|
1787
|
+
#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}
|
|
1788
|
+
#fsRoot .lateridle button:hover{border-color:var(--ink3)}
|
|
1772
1789
|
/* board To Do column add-row — 2px body pad + 10px card pad = 12px lines + up with the card's #NN */
|
|
1773
1790
|
#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
1791
|
#fsRoot .fsbadd:hover{opacity:1;color:var(--ink2)}
|
|
@@ -1802,7 +1819,7 @@
|
|
|
1802
1819
|
#fsRoot .sm2 .m-bar{height:3px;border-radius:2px;background:var(--hair);margin:8px 0 6px;overflow:hidden}
|
|
1803
1820
|
#fsRoot .sm2 .m-bar i{display:block;height:100%;background:var(--green)}
|
|
1804
1821
|
#fsRoot .sm2 .s{font-size:10.5px;color:var(--ink3)}
|
|
1805
|
-
/* Review header
|
|
1822
|
+
/* Review header. Review cards use one stable, screenshot-free row design. */
|
|
1806
1823
|
#fsRoot .revhd{display:flex;align-items:baseline;gap:8px;margin-bottom:11px}
|
|
1807
1824
|
#fsRoot .revhd[hidden]{display:none}
|
|
1808
1825
|
#fsRoot .revhd .hd{font-size:13.5px;letter-spacing:-.02em}
|
|
@@ -1816,28 +1833,30 @@
|
|
|
1816
1833
|
it duplicated the sm2 meter directly above. Reviews render as meter + header + cards;
|
|
1817
1834
|
zero reviews render nothing at all (§1.5-3). */
|
|
1818
1835
|
#fsRoot .revlist{display:block}
|
|
1819
|
-
/*
|
|
1820
|
-
|
|
1821
|
-
#fsRoot .rcard
|
|
1836
|
+
/* Approved B Status dot: borderless, title-only Review rows. Details and proof
|
|
1837
|
+
stay in the Review dialog, where they can be inspected without bloating the board. */
|
|
1838
|
+
#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}
|
|
1839
|
+
#fsRoot .rcard::before{content:"";position:absolute;left:4px;top:50%;width:5px;height:5px;border-radius:50%;background:var(--green);transform:translateY(-50%)}
|
|
1840
|
+
#fsRoot .rcard img{display:none}
|
|
1822
1841
|
#fsRoot .rcard .rc{flex:1;min-width:0}
|
|
1823
|
-
#fsRoot .rcard .t{font-size:
|
|
1842
|
+
#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
1843
|
#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}
|
|
1844
|
+
#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}
|
|
1845
|
+
#fsRoot .rcard .rstat.conflict{color:var(--amber)}
|
|
1846
|
+
#fsRoot .rcard .pr,#fsRoot .rcard .rstat,#fsRoot .rcard .rnote{display:none}
|
|
1847
|
+
#fsRoot .rcard.foldrow{border-style:dashed;background:transparent}
|
|
1848
|
+
#fsRoot .rcard.foldrow .rstat{color:var(--ink2)}
|
|
1825
1849
|
/* review row number — #NN PREFIX on the title (HANDOFF-v45 §11), same form as the To Do
|
|
1826
1850
|
rows (PRD §5: numbering matches the sidebar) */
|
|
1827
1851
|
#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). */
|
|
1852
|
+
/* Folded work must stay visible in List view: it is a settled pointer, not a vanished task. */
|
|
1834
1853
|
/* reference numbers everywhere else (HANDOFF-v45 §11): a leading #NN prefix on every
|
|
1835
1854
|
To Do item's name — same goal = same number (goalReviewNumber), distinct from the
|
|
1836
1855
|
PR's own #id shown separately. */
|
|
1837
1856
|
#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
1857
|
#fsRoot .rcard .act{display:flex;gap:5px;opacity:0;transition:opacity .15s} /* hover-only ✓/✕ (§1.5-10) */
|
|
1839
1858
|
#fsRoot .rcard:hover .act{opacity:1}
|
|
1840
|
-
#fsRoot .rcard .b{width:
|
|
1859
|
+
#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
1860
|
#fsRoot .rcard .b.no{color:var(--danger)}
|
|
1842
1861
|
#fsRoot .rcard .b:hover{background:var(--hover)}
|
|
1843
1862
|
/* rv2 compact one-line rows */
|
|
@@ -2141,6 +2160,9 @@
|
|
|
2141
2160
|
/* body prose = the app's normal text: 14px / −.012em, matching .bubble (Masa 2026-07-20:
|
|
2142
2161
|
13px read thinner/lighter for Japanese than the surrounding app). §2. */
|
|
2143
2162
|
.redesign .rbody{font-size:14px;line-height:1.62;letter-spacing:-.012em;color:var(--ink);max-width:70ch;margin:0 !important}
|
|
2163
|
+
.redesign .rinfra{padding:10px 12px;border:1px solid rgba(var(--amber-rgb),.35);border-radius:8px;background:rgba(var(--amber-rgb),.06)}
|
|
2164
|
+
.redesign .rinfra .rbody{color:var(--ink2)}
|
|
2165
|
+
.redesign .rinfra .retrybtn{margin-top:8px;margin-left:0}
|
|
2144
2166
|
.redesign .rdesc,.redesign .rrisk{color:var(--ink2)}
|
|
2145
2167
|
/* Result (P1, Masa 2026-07-21): the AI's own report, rendered as written. Same 14px
|
|
2146
2168
|
body rhythm as .rbody — it is prose the reviewer reads, not a log. Headings/lists/
|
|
@@ -2330,6 +2352,8 @@
|
|
|
2330
2352
|
/* Doing-failure state word (HANDOFF-v45 §11): colour TEXT only, no badge/pill surface —
|
|
2331
2353
|
Failed = red, Interrupted = amber. */
|
|
2332
2354
|
#fsRoot .trow .stword{font-size:10.5px;font-weight:600;color:var(--danger);flex:0 0 auto}
|
|
2355
|
+
#fsRoot .trow .stword.run{color:var(--green)}
|
|
2356
|
+
#fsRoot .trow .stword.wait{color:var(--ink3)}
|
|
2333
2357
|
#fsRoot .trow .stword.int{color:var(--amber)}
|
|
2334
2358
|
#fsRoot .trow .st.ok{border-color:transparent;background:rgba(var(--green-rgb),.12);position:relative}
|
|
2335
2359
|
#fsRoot .trow .st.ok::after{content:"✓";position:absolute;inset:0;display:grid;place-items:center;font-size:8px;color:var(--green)}
|
|
@@ -2986,10 +3010,11 @@
|
|
|
2986
3010
|
<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
3011
|
<span class="ctxdiv"></span>
|
|
2988
3012
|
<button type="button" class="ctxseg ctxwt" id="ctxwt" role="checkbox" aria-checked="true"
|
|
2989
|
-
title="
|
|
2990
|
-
<i class="ctxbox"></i>worktree</button>
|
|
3013
|
+
title="isolated worktree: default clean copy. direct workspace: inherits and edits this folder.">
|
|
3014
|
+
<i class="ctxbox"></i><span id="ctxwtlabel">isolated worktree</span></button>
|
|
2991
3015
|
</div>
|
|
2992
3016
|
</div>
|
|
3017
|
+
<div class="ctxworkwarn" id="ctxworkwarn" hidden></div>
|
|
2993
3018
|
<!-- Conversation-first composer (SLICE 2, docs/HANDOFF-ONBOARDING-conversational.md):
|
|
2994
3019
|
a chat/help/settings message gets a quiet reply HERE instead of
|
|
2995
3020
|
becoming a To-Do. Reuses the .peraanswer surface; hidden until a
|
|
@@ -3081,7 +3106,7 @@
|
|
|
3081
3106
|
Structure ported from docs/design/flagship/flagship.html: rail | Implementing | lane.
|
|
3082
3107
|
The composer (#composerWrap: queue + composer + send/prism ring) is ADOPTED into
|
|
3083
3108
|
#fsCompwrap by positionLayout() — same nodes, same handlers, input-focus fix intact. -->
|
|
3084
|
-
<div id="fsRoot" data-rv="
|
|
3109
|
+
<div id="fsRoot" data-rv="1">
|
|
3085
3110
|
<div class="fscard fsrail">
|
|
3086
3111
|
<button class="railtg" id="fsRailTg" title="Fold the rail"></button><!-- panel icon filled by tgUpdate (v45 §4) -->
|
|
3087
3112
|
<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 +3398,7 @@ const savedTheme = (() => {
|
|
|
3373
3398
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3374
3399
|
return v;
|
|
3375
3400
|
})();
|
|
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,
|
|
3401
|
+
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
3402
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3378
3403
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3379
3404
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -3704,6 +3729,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3704
3729
|
checkLine: plan ? `PLAN — approve to execute: ${checkLine}`.slice(0, 90) : checkLine,
|
|
3705
3730
|
pr: g.pr ?? null,
|
|
3706
3731
|
testResult: g.testResult ?? null,
|
|
3732
|
+
verificationInfraError: g.verificationInfraError ?? null,
|
|
3707
3733
|
proof: proofTask?.proof ?? null,
|
|
3708
3734
|
proofTaskId: proofTask?.id ?? null,
|
|
3709
3735
|
plan,
|
|
@@ -3715,6 +3741,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3715
3741
|
if (row.reviewNumber != null && (existing.reviewNumber == null || row.reviewNumber < existing.reviewNumber)) existing.reviewNumber = row.reviewNumber;
|
|
3716
3742
|
if (existing.proof == null) { existing.proof = row.proof; existing.proofTaskId = row.proofTaskId; }
|
|
3717
3743
|
if (existing.testResult == null) existing.testResult = row.testResult;
|
|
3744
|
+
if (existing.verificationInfraError == null) existing.verificationInfraError = row.verificationInfraError;
|
|
3718
3745
|
continue;
|
|
3719
3746
|
}
|
|
3720
3747
|
if (row.pr) byPr.set(row.pr, row);
|
|
@@ -4247,7 +4274,11 @@ function goalRow(g){
|
|
|
4247
4274
|
const summary = GOAL_COMPLETE_STATUSES.includes(g.status) && g.summary
|
|
4248
4275
|
? `<div class="gsummary"><div class="gslabel"><span class="dot"></span>Manager結論サマリ</div>${esc(g.summary)}</div>` : '';
|
|
4249
4276
|
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 ?
|
|
4277
|
+
: g.prError ? (() => {
|
|
4278
|
+
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 }; })();
|
|
4279
|
+
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>` : '';
|
|
4280
|
+
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>`;
|
|
4281
|
+
})() : '';
|
|
4251
4282
|
// task 96: PRなしのreviewゴールは、チャット内で要約+個別Approve+チェックリスト導線を出す。
|
|
4252
4283
|
const reviewBox = g.status === 'review' && !g.pr ? reviewActionsHtml(g) : '';
|
|
4253
4284
|
const active = replies.some((r) => ['running', 'queued'].includes(r.status));
|
|
@@ -4354,6 +4385,9 @@ function renderStream(){
|
|
|
4354
4385
|
for (const b of $('stream').querySelectorAll('.reqopen')) {
|
|
4355
4386
|
b.onclick = () => openReviewChecklist(Number(b.dataset.goal));
|
|
4356
4387
|
}
|
|
4388
|
+
for (const b of $('stream').querySelectorAll('[data-fsprretry]')) {
|
|
4389
|
+
b.onclick = () => fsRetryPrGoal(b);
|
|
4390
|
+
}
|
|
4357
4391
|
// task 96: chat-side per-requirement Approve — same flow as the checklist
|
|
4358
4392
|
// (setRequirementStatus toggles the approval and auto-advances the goal when
|
|
4359
4393
|
// every requirement is approved, then render() redraws this thread).
|
|
@@ -4422,6 +4456,17 @@ function renderQueue(){
|
|
|
4422
4456
|
// layouts keep the explanatory sentence.
|
|
4423
4457
|
const qn = ob.length + stacked.length + planning.length;
|
|
4424
4458
|
const headHtml = qn > 0 ? `<div class="qhead">${state.layout === 'flagship' ? `Queued · ${qn}` : `${qn} queued · goals become To dos in ~30s`}</div>` : '';
|
|
4459
|
+
const stackedMeta = (g) => {
|
|
4460
|
+
const project = state.projects.find((p) => p.id === g.projectId);
|
|
4461
|
+
if (project?.paused) return 'Paused · press play to start';
|
|
4462
|
+
const activeIds = [...new Set([
|
|
4463
|
+
...state.tasks.filter((t) => t.projectId === g.projectId && t.status === 'running' && t.goalId != null).map((t) => t.goalId),
|
|
4464
|
+
...state.goals.filter((x) => x.projectId === g.projectId && x.status === 'planning').map((x) => x.id),
|
|
4465
|
+
])].filter((id) => id !== g.id);
|
|
4466
|
+
const cap = state.parallelLimits?.project ?? 2;
|
|
4467
|
+
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`;
|
|
4468
|
+
return `Next up · starts automatically · Agent: ${esc(g.agent || state.connectedApp || 'worker')}`;
|
|
4469
|
+
};
|
|
4425
4470
|
q.innerHTML = connHtml + headHtml + obRows + planningRows + stacked.map((g) => {
|
|
4426
4471
|
if (state.editing === g.id) {
|
|
4427
4472
|
return `<textarea class="qedit" id="qedit-${g.id}">${esc(g.text)}</textarea>
|
|
@@ -4434,7 +4479,7 @@ function renderQueue(){
|
|
|
4434
4479
|
<button class="gbtn qsave" data-id="${g.id}">Save</button><button class="gbtn qcancel">Cancel</button>
|
|
4435
4480
|
</div>`;
|
|
4436
4481
|
}
|
|
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>
|
|
4482
|
+
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
4483
|
<button class="qico qe" data-id="${g.id}" title="Edit">${ICONS.pencil}</button>
|
|
4439
4484
|
<button class="qico qp" data-id="${g.id}" title="Run next">${ICONS.up}</button>
|
|
4440
4485
|
<button class="qico del qd" data-id="${g.id}" title="Delete">${ICONS.trash}</button></div>`;
|
|
@@ -5204,7 +5249,13 @@ function wireWfEditButtons(){
|
|
|
5204
5249
|
// /api/goals/:id/reply thread. Approvals live client-side only
|
|
5205
5250
|
// (state.reviewApprovals) since they are a review action, not new work.
|
|
5206
5251
|
function reviewRequirementsFor(goalId){
|
|
5207
|
-
|
|
5252
|
+
const reqs = state.tasks.filter((t) => t.goalId === goalId && !t.reply).sort((a, b) => (a.num ?? 0) - (b.num ?? 0));
|
|
5253
|
+
const outcome = latestGoalOutcomeTask(goalId, state.tasks);
|
|
5254
|
+
if (reqs.length === 1 && outcome?.reply) {
|
|
5255
|
+
const req = reqs[0];
|
|
5256
|
+
return [{ ...req, ...outcome, id: req.id, title: req.title, outcomeTaskId: outcome.id }];
|
|
5257
|
+
}
|
|
5258
|
+
return reqs;
|
|
5208
5259
|
}
|
|
5209
5260
|
function openReviewChecklist(goalId){
|
|
5210
5261
|
// H14 (Masa 2026-07-08): a single review IS the See-all Ledger focused on that
|
|
@@ -5318,6 +5369,8 @@ function renderReviewChecklist(){
|
|
|
5318
5369
|
const hasProof = state.tasks.some((t) => t.goalId === goal.id && (t.proof?.png || t.proof?.gif));
|
|
5319
5370
|
const VF = { pass: 'tests pass', fail: 'tests fail', untested: 'not tested', proof: 'proof', noproof: 'no proof', human: 'needs you' }; // #377-7: chrome is English
|
|
5320
5371
|
const vchips = [];
|
|
5372
|
+
vchips.push(`<span class="vchip none">${esc(workspaceModeText(goal))}</span>`);
|
|
5373
|
+
if (goal.workspaceWarning) vchips.push(`<span class="vchip human high">⚠ ${esc(goal.workspaceWarning.slice(0, 80))}</span>`);
|
|
5321
5374
|
vchips.push(gtr?.ran
|
|
5322
5375
|
? (gtr.ok ? `<span class="vchip ok">✓ ${VF.pass}</span>` : `<span class="vchip no">✗ ${VF.fail}</span>`)
|
|
5323
5376
|
: `<span class="vchip none">— ${VF.untested}</span>`);
|
|
@@ -5329,10 +5382,15 @@ function renderReviewChecklist(){
|
|
|
5329
5382
|
const failureHtml = fm?.whatHappened
|
|
5330
5383
|
? `<div class="rvchanged"><span class="rvlbl">Failure memory</span>${esc(fm.whatHappened)} ${fm.nextPolicy ? `<span class="muted">${esc(fm.nextPolicy)}</span>` : ''}</div>`
|
|
5331
5384
|
: '';
|
|
5385
|
+
const infra = goal.verificationInfraError;
|
|
5386
|
+
const infraHtml = infra?.detail
|
|
5387
|
+
? `<div class="rvinfra"><span class="rvlbl">${esc(infra.title || '検証基盤エラー')}</span>${esc(infra.detail)} <button type="button" class="retrybtn" data-goalreverify="${goal.id}">再検証</button></div>`
|
|
5388
|
+
: '';
|
|
5332
5389
|
$('reviewSummary').innerHTML =
|
|
5333
5390
|
vfyHtml
|
|
5334
5391
|
+ (grs.check ? `<div class="rvcheck2"><span class="rvlbl">${L.check}</span>${esc(grs.check)}</div>` : '')
|
|
5335
5392
|
+ (grs.changed ? `<div class="rvchanged"><span class="rvlbl">${L.changed}</span>${esc(grs.changed)}</div>` : '')
|
|
5393
|
+
+ infraHtml
|
|
5336
5394
|
+ failureHtml
|
|
5337
5395
|
+ optHtml
|
|
5338
5396
|
+ `<div class="rvhead">${testLine}${esc(sum.headline)} · ${esc(sum.detail)}${prLink ? ` · ${prLink}` : ''}</div>`
|
|
@@ -5356,7 +5414,7 @@ function renderReviewChecklist(){
|
|
|
5356
5414
|
}
|
|
5357
5415
|
// P1: one shot when a promised check actually captured one, otherwise nothing at
|
|
5358
5416
|
// all — "No capture yet" was us apologising for a photo we no longer go and take.
|
|
5359
|
-
const media = proofMediaHtml(t.id, t.proof, esc(t.title ?? ''));
|
|
5417
|
+
const media = proofMediaHtml(t.outcomeTaskId ?? t.id, t.proof, esc(t.title ?? ''));
|
|
5360
5418
|
// task 156/234 + 160: show the WORKER'S REPORT (what it actually did/found,
|
|
5361
5419
|
// incl. any test-only verification it describes) — that's what you read to
|
|
5362
5420
|
// decide. Falls back to the instruction if the worker left no result.
|
|
@@ -5395,6 +5453,15 @@ function renderReviewChecklist(){
|
|
|
5395
5453
|
for (const c of $('reviewList').querySelectorAll('[data-reopen]')) {
|
|
5396
5454
|
c.onclick = () => setRequirementStatus(goalId, Number(c.dataset.reopen), 'approved'); // same status toggles it off
|
|
5397
5455
|
}
|
|
5456
|
+
for (const b of $('reviewSummary').querySelectorAll('[data-goalreverify]')) {
|
|
5457
|
+
b.onclick = async (e) => {
|
|
5458
|
+
e.stopPropagation();
|
|
5459
|
+
b.disabled = true;
|
|
5460
|
+
b.textContent = '再検証中…';
|
|
5461
|
+
const r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' });
|
|
5462
|
+
if (r.ok) await refresh(); else showErr('再検証に失敗');
|
|
5463
|
+
};
|
|
5464
|
+
}
|
|
5398
5465
|
for (const b of $('reviewList').querySelectorAll('.reqsend')) {
|
|
5399
5466
|
b.onclick = async () => {
|
|
5400
5467
|
const taskId = Number(b.dataset.task);
|
|
@@ -5450,15 +5517,15 @@ function playReviewFly(capture){
|
|
|
5450
5517
|
}
|
|
5451
5518
|
// task 121: 承認が確定した瞬間に一度だけ紙吹雪を弾けさせる共通フック。両方の
|
|
5452
5519
|
// 承認経路(reviewApprove.onclick / advanceGoalAfterAllApproved)から呼ぶ。
|
|
5453
|
-
//
|
|
5454
|
-
//
|
|
5455
|
-
const CONFETTI_COLORS = ['
|
|
5520
|
+
// Figma's playful primary family: vivid enough to read as a reward on both
|
|
5521
|
+
// light and dark themes while the green check remains the success anchor.
|
|
5522
|
+
const CONFETTI_COLORS = ['#F24E1E', '#FF7262', '#A259FF', '#1ABCFE', '#0ACF83', '#FFD166'];
|
|
5456
5523
|
function celebrateApprove(){
|
|
5457
5524
|
const dm = $('reviewDoneMsg');
|
|
5458
5525
|
const rect = dm && dm.offsetParent ? dm.getBoundingClientRect() : null;
|
|
5459
5526
|
const originX = rect ? rect.left + Math.min(28, rect.width / 2) : window.innerWidth / 2;
|
|
5460
5527
|
const originY = rect ? rect.top + rect.height / 2 : window.innerHeight * 0.3;
|
|
5461
|
-
for (let i = 0; i <
|
|
5528
|
+
for (let i = 0; i < 42; i++){
|
|
5462
5529
|
const el = document.createElement('div');
|
|
5463
5530
|
el.className = 'confetti-piece';
|
|
5464
5531
|
const size = 6 + Math.random() * 6;
|
|
@@ -5472,7 +5539,7 @@ function celebrateApprove(){
|
|
|
5472
5539
|
el.style.transition = 'transform .9s cubic-bezier(.15,.7,.3,1), opacity .9s ease .25s';
|
|
5473
5540
|
document.body.appendChild(el);
|
|
5474
5541
|
const angle = Math.random() * Math.PI * 2;
|
|
5475
|
-
const dist =
|
|
5542
|
+
const dist = 90 + Math.random() * 190;
|
|
5476
5543
|
const dx = Math.cos(angle) * dist;
|
|
5477
5544
|
const dy = Math.sin(angle) * dist * 0.6 + 90; // outward burst, then settle downward like falling confetti
|
|
5478
5545
|
const rot = (Math.random() < 0.5 ? -1 : 1) * (360 + Math.random() * 360);
|
|
@@ -5483,6 +5550,34 @@ function celebrateApprove(){
|
|
|
5483
5550
|
setTimeout(() => el.remove(), 1000);
|
|
5484
5551
|
}
|
|
5485
5552
|
}
|
|
5553
|
+
|
|
5554
|
+
// A merged PR is the terminal Review moment. Show the result in the dialog the
|
|
5555
|
+
// person is looking at, then remove that completed review instead of leaving a
|
|
5556
|
+
// stale modal behind. The state update arrives in the same SSE event, so render()
|
|
5557
|
+
// after close also removes the card from the Review lane.
|
|
5558
|
+
function finishOpenReview(goal){
|
|
5559
|
+
const ids = SA.items.flatMap((it) => it.ids || [it.goalId]);
|
|
5560
|
+
if (!SA.open || !ids.includes(goal.id)) { celebrateApprove(); return; }
|
|
5561
|
+
const layer = document.createElement('div');
|
|
5562
|
+
layer.className = 'sa-finish';
|
|
5563
|
+
layer.setAttribute('role', 'status');
|
|
5564
|
+
layer.setAttribute('aria-label', 'Review complete');
|
|
5565
|
+
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>';
|
|
5566
|
+
const panel = $('seeall').querySelector('.sa-panel')?.getBoundingClientRect();
|
|
5567
|
+
const bounds = panel ?? { left: 0, top: 0, width: innerWidth, height: innerHeight };
|
|
5568
|
+
Object.assign(layer.style, { left: `${bounds.left}px`, top: `${bounds.top}px`, width: `${bounds.width}px`, height: `${bounds.height}px` });
|
|
5569
|
+
// Keep the completion layer outside #seeall: the same SSE schedules a normal
|
|
5570
|
+
// render, which rebuilds the dialog while this short animation is playing.
|
|
5571
|
+
document.body.appendChild(layer);
|
|
5572
|
+
const mark = layer.querySelector('.sa-finish-mark').getBoundingClientRect();
|
|
5573
|
+
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
5574
|
+
if (!reduce) {
|
|
5575
|
+
confettiBurst(mark.left + mark.width / 2, mark.top + mark.height / 2);
|
|
5576
|
+
celebrateApprove();
|
|
5577
|
+
}
|
|
5578
|
+
const delay = reduce ? 350 : 950;
|
|
5579
|
+
setTimeout(() => { layer.remove(); saClose(); render(); }, delay);
|
|
5580
|
+
}
|
|
5486
5581
|
async function setRequirementStatus(goalId, taskId, status){
|
|
5487
5582
|
const approvals = (state.reviewApprovals[goalId] ??= {});
|
|
5488
5583
|
approvals[taskId] = approvals[taskId] === status ? undefined : status; // click again to undo
|
|
@@ -5933,16 +6028,22 @@ $('reviewOverlay').addEventListener('click', (e) => { if (e.target === $('review
|
|
|
5933
6028
|
// result linked to the goal (What was done), and the existing proof
|
|
5934
6029
|
// video/screenshot rendering (Proof). Approve/Dismiss and the revision send
|
|
5935
6030
|
// button are visual-only for now — not wired to any API.
|
|
6031
|
+
const REVIEW_DETAIL_STATUSES = new Set(['review', 'done', 'reverted', 'skipped', 'blocked', 'partial', 'failed', 'interrupted']);
|
|
5936
6032
|
function openGoalDetail(goalId){
|
|
6033
|
+
const g = state.goals.find((x) => x.id === goalId);
|
|
6034
|
+
// Completed and human-attention work always uses the current Review Ledger card.
|
|
6035
|
+
// Keeping this guard at the lowest-level opener prevents Done from falling back to
|
|
6036
|
+
// the legacy overlay when it is opened through a different list/log entry point.
|
|
6037
|
+
if (g && REVIEW_DETAIL_STATUSES.has(g.status) && typeof saOpen === 'function') {
|
|
6038
|
+
saOpen(goalId, true);
|
|
6039
|
+
return;
|
|
6040
|
+
}
|
|
5937
6041
|
state.goalDetailOpen = goalId;
|
|
5938
6042
|
renderGoalDetail();
|
|
5939
6043
|
$('goalDetailOverlay').classList.add('show');
|
|
5940
6044
|
}
|
|
5941
|
-
//
|
|
5942
|
-
// any other non-review goal keeps the goal-detail overlay.
|
|
6045
|
+
// All detail entry points share the routing in openGoalDetail().
|
|
5943
6046
|
function openGoalOrCard(goalId){
|
|
5944
|
-
const g = state.goals.find((x) => x.id === goalId);
|
|
5945
|
-
if (g && ['blocked', 'partial', 'failed', 'interrupted'].includes(g.status) && typeof saOpen === 'function') { saOpen(goalId, true); return; }
|
|
5946
6047
|
openGoalDetail(goalId);
|
|
5947
6048
|
}
|
|
5948
6049
|
// List / board 共通の詳細導線。Review は review ledger、Needs you は attention card、
|
|
@@ -6223,7 +6324,7 @@ function render(){
|
|
|
6223
6324
|
// queueOrder); actions call the SAME endpoints (approve/dismiss/retry/reply…).
|
|
6224
6325
|
// ============================================================================
|
|
6225
6326
|
const fsUI = {
|
|
6226
|
-
rv:
|
|
6327
|
+
rv: 1, // approved screenshot-free B Status dot design; Review has one stable view
|
|
6227
6328
|
band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
|
|
6228
6329
|
revMore: false, // "+N more" fold on the review list (§1.5-8)
|
|
6229
6330
|
todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
|
|
@@ -6328,9 +6429,6 @@ function fsActiveContext(){
|
|
|
6328
6429
|
const focus = pinnedValid ? pinned : running[0] ?? closing[0] ?? failed[failed.length - 1] ?? null;
|
|
6329
6430
|
return { tasks, running, failed, focus, goal: state.goals.find((g) => g.id === focus?.goalId) ?? null };
|
|
6330
6431
|
}
|
|
6331
|
-
// Per-project last non-empty center feed — the whiteout guard (see below): keeps
|
|
6332
|
-
// what the user was looking at when a render pass momentarily produces no lines.
|
|
6333
|
-
const _fsLastFeed = {};
|
|
6334
6432
|
// H-CENTERLANE decision 1: pure merge-sort of timed HTML blocks (the focused task's
|
|
6335
6433
|
// activity block + each chat turn) so the center lane renders in the order things
|
|
6336
6434
|
// actually happened. Stable sort (spec-guaranteed since ES2019) keeps same-ts blocks
|
|
@@ -6404,13 +6502,14 @@ function renderFsFeed(){
|
|
|
6404
6502
|
// it's continuing, so the ⎿ read as floating/orphaned. Now an independent state line:
|
|
6405
6503
|
// colored icon + word only, no continuation glyph.
|
|
6406
6504
|
const isInt = focus.status === 'interrupted';
|
|
6407
|
-
const word = isInt ? 'Interrupted' : 'Failed', icon = isInt ? '⏸' : '✗'
|
|
6505
|
+
const word = isInt ? 'Interrupted' : 'Failed', icon = isInt ? '⏸' : '✗';
|
|
6506
|
+
const meta = taskErrorMeta(focus, goal);
|
|
6408
6507
|
const reason = String(focus.result || '').trim();
|
|
6409
6508
|
// no separate reason text (or it's just a restatement of the status itself) → don't
|
|
6410
6509
|
// print "Interrupted — Interrupted"; the word + icon alone already say it
|
|
6411
6510
|
const reasonSuffix = reason && reason.toLowerCase() !== word.toLowerCase() ? ` — ${esc(reason.replace(/\s+/g, ' ').slice(0, 140))}` : '';
|
|
6412
|
-
html += `<div class="cc-err${isInt ? ' int' : ''}"><div class="msg"><span class="x${isInt ? ' int' : ''}">${icon} ${esc(word)}</span>${reasonSuffix}</div>
|
|
6413
|
-
<div class="acts2"
|
|
6511
|
+
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>
|
|
6512
|
+
<div class="acts2">${errorButtonsHtml(meta, { taskId: focus.id, goalId: focus.goalId, interrupted: isInt, agent: focus.agent || goal?.agent })}</div></div>`;
|
|
6414
6513
|
}
|
|
6415
6514
|
// AI question — while current it lives in the chat feed (cc-ask, §6.5 dynamic rule / q2).
|
|
6416
6515
|
// Free-text answer row + delete (trash) restore lost behaviors from the pre-flagship
|
|
@@ -6440,15 +6539,9 @@ function renderFsFeed(){
|
|
|
6440
6539
|
const app = connectedAppMeta(focus?.agent ?? goal?.agent);
|
|
6441
6540
|
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;
|
|
6442
6541
|
}
|
|
6443
|
-
//
|
|
6444
|
-
//
|
|
6445
|
-
//
|
|
6446
|
-
// a goal finishing before the next starts, a project switch, a refresh() that
|
|
6447
|
-
// briefly reloads state). Gate it PURELY on whether the project has any
|
|
6448
|
-
// goal/task; connection blips never change that, so the giant hero can no
|
|
6449
|
-
// longer flash in and wipe what the user was looking at. While work exists but
|
|
6450
|
-
// this pass produced no lines, keep the last known feed (or a calm one-line
|
|
6451
|
-
// status) instead of blanking to the hero.
|
|
6542
|
+
// The center must describe current state. Keeping the previous task's activity
|
|
6543
|
+
// after it settled made an idle board look 77% active while the server had zero
|
|
6544
|
+
// workers. Empty live output now gets an explicit idle explanation instead.
|
|
6452
6545
|
// Pattern A (H23): the conversation turns — the user's own messages, Claude
|
|
6453
6546
|
// Code's replies (typed out with a caret on the streaming frontier), and
|
|
6454
6547
|
// "→ Added to To Do #NN" markers — rendered from state.chatTurns so the
|
|
@@ -6488,8 +6581,12 @@ function renderFsFeed(){
|
|
|
6488
6581
|
// end (H18/H19 — a pending question must never be lost mid-scroll).
|
|
6489
6582
|
const timelineBlocks = html ? [{ ts: focusTs, html }, ...chatBlocks] : chatBlocks;
|
|
6490
6583
|
let out = fsMergeTimelineBlocks(timelineBlocks) + askHtml;
|
|
6491
|
-
if (out
|
|
6492
|
-
|
|
6584
|
+
if (!out && hasWork) {
|
|
6585
|
+
const laterN = pgoals.filter((g) => g.status === 'pending').length;
|
|
6586
|
+
const queuedN = ptasks.filter((t) => t.status === 'queued').length + pgoals.filter((g) => ['planning', 'stacked'].includes(g.status)).length;
|
|
6587
|
+
const msg = running.length ? 'working…' : queuedN ? `${queuedN} queued · waiting to start` : laterN ? `no active task · ${laterN} saved in Later` : 'no active task';
|
|
6588
|
+
out = `<div class="cc-c">⎿ ${esc(msg)}</div>`;
|
|
6589
|
+
}
|
|
6493
6590
|
$('fsRoot').classList.toggle('clawdhero', !hasWork);
|
|
6494
6591
|
const sc = feed.parentNode;
|
|
6495
6592
|
const nearBottom = sc.scrollHeight - sc.clientHeight - sc.scrollTop <= 80;
|
|
@@ -6500,6 +6597,9 @@ function renderFsFeed(){
|
|
|
6500
6597
|
window.__fsCineSync?.(); // Cinema dynamic rule: content growth may reach the band region
|
|
6501
6598
|
for (const b of feed.querySelectorAll('[data-fsretry]')) b.onclick = () => fsRetryTask(b.dataset.fsretry);
|
|
6502
6599
|
for (const b of feed.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
6600
|
+
for (const b of feed.querySelectorAll('[data-fscodex]')) b.onclick = () => fsRunWithCodexGoal(b);
|
|
6601
|
+
for (const b of feed.querySelectorAll('[data-fsconnectworker]')) b.onclick = () => fsConnectWorker(b);
|
|
6602
|
+
for (const b of feed.querySelectorAll('[data-fscheckworker]')) b.onclick = () => fsCheckWorker(b);
|
|
6503
6603
|
for (const b of feed.querySelectorAll('[data-fsanswer]')) b.onclick = () => fsAnswerQuestion(b.dataset.fsanswer, b.dataset.a);
|
|
6504
6604
|
// Free-text answer (restores 質問カードの自由記述回答欄 — lost in the flagship rewrite,
|
|
6505
6605
|
// see docs/confirmed-behaviors.md): Enter or the ↑ send button answers with typed text.
|
|
@@ -6538,6 +6638,38 @@ async function fsRetryTask(id){
|
|
|
6538
6638
|
const r = await fetch(withKey(`/api/tasks/${id}/retry`), { method: 'POST' });
|
|
6539
6639
|
if (!r.ok) showErr('Retry failed.'); else await refresh();
|
|
6540
6640
|
}
|
|
6641
|
+
async function fsRunWithCodexGoal(b){
|
|
6642
|
+
b.disabled = true; b.textContent = failureLang() === 'ja' ? '開始中…' : 'Starting…';
|
|
6643
|
+
const r = await fetch(withKey(`/api/goals/${b.dataset.fscodex}/run-with-codex`), { method: 'POST' });
|
|
6644
|
+
if (!r.ok) showErr((await r.json().catch(() => ({}))).error || 'Could not start with Codex.');
|
|
6645
|
+
await refresh();
|
|
6646
|
+
}
|
|
6647
|
+
async function fsConnectWorker(b){
|
|
6648
|
+
b.disabled = true;
|
|
6649
|
+
try {
|
|
6650
|
+
const r = await fetch(withKey('/api/worker-auth/connect'), { method: 'POST' });
|
|
6651
|
+
if (!r.ok) throw new Error();
|
|
6652
|
+
showToast(failureLang() === 'ja' ? '接続画面を開きました。完了後に接続確認してください。' : 'Connection opened. Check connection when finished.');
|
|
6653
|
+
} catch { showErr(failureLang() === 'ja' ? '接続画面を開けませんでした。' : 'Could not open the connection.'); }
|
|
6654
|
+
finally { b.disabled = false; }
|
|
6655
|
+
}
|
|
6656
|
+
async function fsCheckWorker(b){
|
|
6657
|
+
b.disabled = true;
|
|
6658
|
+
try {
|
|
6659
|
+
const r = await fetch(withKey('/api/worker-auth/retry'), { method: 'POST' });
|
|
6660
|
+
const body = await r.json().catch(() => ({}));
|
|
6661
|
+
if (!r.ok) throw new Error();
|
|
6662
|
+
renderAuthBanner(body.auth);
|
|
6663
|
+
if (body.auth?.ok) showToast(failureLang() === 'ja' ? '接続できました。' : 'Connected.');
|
|
6664
|
+
} catch { showErr(failureLang() === 'ja' ? 'まだ接続できていません。' : 'Not connected yet.'); }
|
|
6665
|
+
finally { b.disabled = false; }
|
|
6666
|
+
}
|
|
6667
|
+
async function fsRetryPrGoal(b){
|
|
6668
|
+
b.disabled = true; b.textContent = failureLang() === 'ja' ? '再試行中…' : 'Retrying…';
|
|
6669
|
+
const r = await fetch(withKey(`/api/goals/${b.dataset.fsprretry}/retry-pr`), { method: 'POST' });
|
|
6670
|
+
if (!r.ok) { showErr((await r.json().catch(() => ({}))).error || 'PR retry failed.'); b.disabled = false; b.textContent = FAILURE_COPY[failureLang()].actions.pr; }
|
|
6671
|
+
else await refresh();
|
|
6672
|
+
}
|
|
6541
6673
|
function fsToggleLog(taskId){
|
|
6542
6674
|
const task = state.tasks.find((t) => t.id === taskId);
|
|
6543
6675
|
if (task?.goalId == null) return;
|
|
@@ -6801,12 +6933,8 @@ function renderFsReview(){
|
|
|
6801
6933
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
6802
6934
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
6803
6935
|
const rows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
// its work went — and the control to restore it — live on the carrier's side only, in the
|
|
6807
|
-
// opened detail's "Also carries" section (see saItemHtml's carriesBody). The list stays
|
|
6808
|
-
// just the reviews; folded goals are explained where they were merged.
|
|
6809
|
-
if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6936
|
+
const foldedRows = pgoals.filter((g) => g.status === 'folded' && g.foldedInto != null);
|
|
6937
|
+
if (!rows.length && !foldedRows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6810
6938
|
const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
6811
6939
|
const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
|
|
6812
6940
|
const pct = rows.length ? Math.round((green / rows.length) * 100) : 0;
|
|
@@ -6819,12 +6947,6 @@ function renderFsReview(){
|
|
|
6819
6947
|
const RCAP = 3;
|
|
6820
6948
|
const shown = fsUI.revMore ? rows : rows.slice(0, RCAP);
|
|
6821
6949
|
const card = (r) => {
|
|
6822
|
-
const imgFile = r.proof?.png ?? r.proof?.gif;
|
|
6823
|
-
// onerror: a broken proof (404 / expired key) removes the <img> so the card's
|
|
6824
|
-
// dark ground shows through cleanly instead of a broken-image icon + "proof" alt
|
|
6825
|
-
// text. Card height is guaranteed by the base rule's min-height, not the img.
|
|
6826
|
-
const img = imgFile && r.proofTaskId != null
|
|
6827
|
-
? `<img src="${withKey(`/proof/${r.proofTaskId}/${imgFile}`)}" alt="proof" loading="lazy" onerror="this.remove()">` : '';
|
|
6828
6950
|
const pr = r.pr ? `<a class="pr" href="${esc(r.pr)}" target="_blank">PR #${esc((r.pr.match(/pull\/(\d+)/) ?? [, ''])[1])} ↗</a>` : '';
|
|
6829
6951
|
// #NN PREFIX on the title (HANDOFF-v45 §11 — same form/number as the To Do rows;
|
|
6830
6952
|
// goalReviewNumber = goal.id, so a goal keeps the same number in Review and in To
|
|
@@ -6835,6 +6957,11 @@ function renderFsReview(){
|
|
|
6835
6957
|
// retest ×3 note (v45 §5.3): the goal came back green from a Ledger Dismiss
|
|
6836
6958
|
const g0 = pgoals.find((g) => g.id === r.goalIds[0]);
|
|
6837
6959
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
6960
|
+
const infraNote = r.verificationInfraError?.detail
|
|
6961
|
+
? `<span class="rnote">検証基盤エラー · <button type="button" class="retrybtn" data-goalreverify="${r.goalIds[0]}">再検証</button></span>`
|
|
6962
|
+
: '';
|
|
6963
|
+
const conflictNote = g0?.conflictsWith?.length
|
|
6964
|
+
? `<span class="rstat conflict">Potential conflict with ${esc(g0.conflictsWith.map((id) => `#${id}`).join(', '))}</span>` : '<span class="rstat">In review</span>';
|
|
6838
6965
|
// PRD §6.5 → Masa 2026-07-21: goals folded onto this card's PR no longer get an
|
|
6839
6966
|
// inline "+ #901 の仕事もここに入っています" note here — same report-in-the-list
|
|
6840
6967
|
// clutter the old standalone fold row was (Masa: review欄にいらない). It is logged
|
|
@@ -6844,9 +6971,16 @@ function renderFsReview(){
|
|
|
6844
6971
|
// (Masa 2026-07-20: this card showed reviewSummary.check "…が出ない" while the list
|
|
6845
6972
|
// showed the request text). Plan-reviews keep their "PLAN — approve to execute:"
|
|
6846
6973
|
// checkLine, which is the actionable label there, not a title mismatch.
|
|
6847
|
-
return `<div class="rcard
|
|
6974
|
+
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>
|
|
6848
6975
|
<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>`;
|
|
6849
6976
|
};
|
|
6977
|
+
const foldCard = (g) => {
|
|
6978
|
+
const target = pgoals.find((x) => x.id === g.foldedInto);
|
|
6979
|
+
const n = goalReviewNumber({ goal: g, tasks: list });
|
|
6980
|
+
const rid = n != null ? `<span class="rid gno" data-n="${n}">#${n}</span>` : '';
|
|
6981
|
+
const targetNo = target ? (goalReviewNumber({ goal: target, tasks: list }) ?? target.id) : g.foldedInto;
|
|
6982
|
+
return `<div class="rcard noimg foldrow" data-fsrev="${g.id}"><div class="rc"><div class="t">${rid}${esc(saReviewTitle(g))}</div><span class="rstat">Merged into #${esc(String(targetNo))}</span></div></div>`;
|
|
6983
|
+
};
|
|
6850
6984
|
// V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
|
|
6851
6985
|
// above. Reviews render directly: meter + Review header + cards (+N more fold, §1.5-8).
|
|
6852
6986
|
// The meter counts what is reviewable, so it only exists when something is (§1.5-3):
|
|
@@ -6856,13 +6990,8 @@ function renderFsReview(){
|
|
|
6856
6990
|
<div class="m-top"><span class="num mt">${green} / ${rows.length}</span><span> ready</span><button class="detailbtn" id="fsSeeAll">See all</button></div>
|
|
6857
6991
|
<div class="m-bar"><i style="width:${pct}%"></i></div>
|
|
6858
6992
|
<div class="s">${esc(sline)}</div></div>` : ''}
|
|
6859
|
-
<div class="revhd" id="fsRevHd"><span class="hd">Review</span><span class="rnum num">${green} ready</span>
|
|
6860
|
-
|
|
6861
|
-
<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>
|
|
6862
|
-
<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>
|
|
6863
|
-
<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>
|
|
6864
|
-
</span></div>
|
|
6865
|
-
<div class="revlist">${shown.map(card).join('')}
|
|
6993
|
+
<div class="revhd" id="fsRevHd"><span class="hd">Review</span><span class="rnum num">${green} ready</span></div>
|
|
6994
|
+
<div class="revlist">${shown.map(card).join('')}${foldedRows.map(foldCard).join('')}
|
|
6866
6995
|
${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
|
|
6867
6996
|
// Mirror the ready-review count onto the mobile top-bar "Review N" opener (H23).
|
|
6868
6997
|
const _hn = document.getElementById('fsHambN'); if (_hn) _hn.textContent = green > 0 ? String(green) : '';
|
|
@@ -6876,10 +7005,8 @@ function renderFsReview(){
|
|
|
6876
7005
|
saOpen(); };
|
|
6877
7006
|
const moreEl = el.querySelector('#fsRevMore');
|
|
6878
7007
|
if (moreEl) moreEl.onclick = () => { fsUI.revMore = !fsUI.revMore; renderFsReview(); };
|
|
6879
|
-
for (const b of el.querySelectorAll('[data-fsrv]')) b.onclick = (e) => { e.stopPropagation();
|
|
6880
|
-
fsUI.rv = Number(b.dataset.fsrv); localStorage.setItem('fsRv', String(fsUI.rv)); $('fsRoot').dataset.rv = String(fsUI.rv); renderFsReview(); };
|
|
6881
7008
|
for (const c of el.querySelectorAll('[data-fsrev]')) c.onclick = (e) => {
|
|
6882
|
-
if (e.target.closest('a') || e.target.closest('.act')) return;
|
|
7009
|
+
if (e.target.closest('a') || e.target.closest('.act') || e.target.closest('[data-goalreverify]')) return;
|
|
6883
7010
|
openReviewChecklist(Number(c.dataset.fsrev));
|
|
6884
7011
|
};
|
|
6885
7012
|
// Folded goals get no row here (Masa 2026-07-23) — they are shown, and restored, on the
|
|
@@ -6926,6 +7053,96 @@ function fsBucketTodo(){
|
|
|
6926
7053
|
rejected: pgoals.filter((g) => g.status === 'rejected'), // 07-20c: Reject parks goals here (own fold, like Done)
|
|
6927
7054
|
};
|
|
6928
7055
|
}
|
|
7056
|
+
const FAILURE_COPY = {
|
|
7057
|
+
ja: {
|
|
7058
|
+
agent: { title: 'Agentエラー', body: 'Claude Code/Codex 側が作業中に止まりました。依頼内容や実装で詰まった可能性があります。' },
|
|
7059
|
+
infra: {
|
|
7060
|
+
title: 'Galda基盤エラー',
|
|
7061
|
+
timeout: 'Galda側の実行上限や待ち時間で止まりました。変更内容の失敗とは限りません。',
|
|
7062
|
+
auth: '接続や認証確認で止まりました。接続を直してから再開できます。',
|
|
7063
|
+
relay: 'relay接続で止まりました。接続状態を確認してから再試行できます。',
|
|
7064
|
+
chrome: 'Chrome/ブラウザ確認で止まりました。画面確認環境の問題で、実装失敗とは限りません。',
|
|
7065
|
+
'test-runner': 'Galdaのtest runnerで止まりました。マシン負荷や起動待ちの可能性があります。',
|
|
7066
|
+
pr: 'PR作成で止まりました。GitHub/gh認証やpush状態を確認してから再試行できます。',
|
|
7067
|
+
'worker-start': 'worker起動で止まりました。Agentコマンドや実行環境を確認してから再開できます。',
|
|
7068
|
+
unknown: 'Galdaの実行基盤で止まりました。ログを確認し、原因に応じて再試行できます。',
|
|
7069
|
+
},
|
|
7070
|
+
actions: {
|
|
7071
|
+
retry: '再試行', resume: '再開', connect: 'Connect', check: '接続確認',
|
|
7072
|
+
other: '別Agentで実行', log: 'ログ確認', tests: 'テスト再実行', pr: 'PR再試行',
|
|
7073
|
+
},
|
|
7074
|
+
},
|
|
7075
|
+
en: {
|
|
7076
|
+
agent: { title: 'Agent error', body: 'Claude Code/Codex stopped while doing the work. The agent may need a revised instruction or another run.' },
|
|
7077
|
+
infra: {
|
|
7078
|
+
title: 'Galda system issue',
|
|
7079
|
+
timeout: 'Galda stopped this at the run limit or a wait timed out. This is not necessarily a problem with the change.',
|
|
7080
|
+
auth: 'Connection or authentication failed. Reconnect, then continue.',
|
|
7081
|
+
relay: 'The relay connection failed. Check the connection, then retry.',
|
|
7082
|
+
chrome: 'Chrome/browser verification failed. The UI check environment stopped, not necessarily the implementation.',
|
|
7083
|
+
'test-runner': 'Galda’s test runner stopped. Machine load or app startup timing may be the cause.',
|
|
7084
|
+
pr: 'PR creation failed. Check GitHub/gh auth or push state, then retry.',
|
|
7085
|
+
'worker-start': 'The worker could not start. Check the agent command or runtime, then continue.',
|
|
7086
|
+
unknown: 'Galda’s execution layer stopped. Check the log and retry based on the cause.',
|
|
7087
|
+
},
|
|
7088
|
+
actions: {
|
|
7089
|
+
retry: 'Retry', resume: 'Resume', connect: 'Connect', check: 'Check connection',
|
|
7090
|
+
other: 'Run with another Agent', log: 'View log', tests: 'Re-run tests', pr: 'Retry PR',
|
|
7091
|
+
},
|
|
7092
|
+
},
|
|
7093
|
+
};
|
|
7094
|
+
function failureLang(){
|
|
7095
|
+
try { return billingLang(); } catch { return document.documentElement.lang === 'ja' ? 'ja' : 'en'; }
|
|
7096
|
+
}
|
|
7097
|
+
function inferErrorClass(text, agent){
|
|
7098
|
+
const low = String(text || '').toLowerCase();
|
|
7099
|
+
const infra = (reason, actionIds) => ({ owner: 'galda-infra', reason, actionIds });
|
|
7100
|
+
if (/run limit|実行上限|timed out|timeout|タイムアウト/.test(low)) return infra('timeout', ['retry', 'run-with-codex', 'view-log']);
|
|
7101
|
+
if (/spawn|enoent|eacces|command not found|worker tool exited early|作業ツールが途中で終了/.test(low)) return infra('worker-start', ['retry', 'run-with-codex', 'view-log']);
|
|
7102
|
+
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']);
|
|
7103
|
+
if (/relay|websocket|socket|econnreset|econnrefused|billing-api unreachable|cf access|cloudflare access/.test(low)) return infra('relay', ['retry', 'connect', 'view-log']);
|
|
7104
|
+
if (/chrome|chromium|puppeteer|target closed|browser|navigation timeout|waiting failed|screenshot|headless/.test(low)) return infra('chrome', ['retry', 'view-log']);
|
|
7105
|
+
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']);
|
|
7106
|
+
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']);
|
|
7107
|
+
return { owner: 'agent', reason: agent === 'codex' ? 'codex' : 'claude-code', actionIds: ['retry', 'run-with-codex', 'view-log'] };
|
|
7108
|
+
}
|
|
7109
|
+
function taskErrorMeta(t, goal){
|
|
7110
|
+
const meta = t.errorClass || inferErrorClass(t.result, t.agent || goal?.agent);
|
|
7111
|
+
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7112
|
+
const isInfra = meta.owner === 'galda-infra';
|
|
7113
|
+
return {
|
|
7114
|
+
...meta,
|
|
7115
|
+
title: isInfra ? C.infra.title : C.agent.title,
|
|
7116
|
+
body: isInfra ? (C.infra[meta.reason] || C.infra.unknown) : C.agent.body,
|
|
7117
|
+
copy: C,
|
|
7118
|
+
};
|
|
7119
|
+
}
|
|
7120
|
+
function goalBlockedErrorMeta(g){
|
|
7121
|
+
const raw = g.blocked?.errorClass || (g.blocked?.kind === 'test'
|
|
7122
|
+
? { owner: 'galda-infra', reason: 'test-runner', actionIds: ['retry-tests', 'view-log'] }
|
|
7123
|
+
: inferErrorClass(g.blocked?.reason, g.agent));
|
|
7124
|
+
const lang = failureLang(), C = FAILURE_COPY[lang];
|
|
7125
|
+
const isInfra = raw.owner === 'galda-infra';
|
|
7126
|
+
return { ...raw, title: isInfra ? C.infra.title : C.agent.title, body: isInfra ? (C.infra[raw.reason] || C.infra.unknown) : C.agent.body, copy: C };
|
|
7127
|
+
}
|
|
7128
|
+
function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = false, agent = null, pr = false } = {}){
|
|
7129
|
+
const C = meta.copy.actions;
|
|
7130
|
+
const canCodex = goalId != null && agent !== 'codex' && (state.agents || []).includes('codex');
|
|
7131
|
+
const out = [];
|
|
7132
|
+
for (const id of meta.actionIds || []) {
|
|
7133
|
+
if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
|
|
7134
|
+
else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${C.other}</button>`);
|
|
7135
|
+
else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
|
|
7136
|
+
else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
|
|
7137
|
+
else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
|
|
7138
|
+
else if (id === 'retry-pr' && goalId != null) out.push(`<button class="errbtn" data-fsprretry="${goalId}">${C.pr}</button>`);
|
|
7139
|
+
else if (id === 'view-log' && taskId != null) out.push(`<button class="errbtn" data-fslog="${taskId}">${C.log}</button>`);
|
|
7140
|
+
else if (id === 'view-log' && goalId != null) out.push(`<button class="errbtn" data-fsglog="${goalId}">${C.log}</button>`);
|
|
7141
|
+
}
|
|
7142
|
+
if (!out.length && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
|
|
7143
|
+
if (pr && !out.some((h) => h.includes('data-fsprretry'))) out.unshift(`<button class="errbtn" data-fsprretry="${goalId}">${C.pr}</button>`);
|
|
7144
|
+
return out.join('');
|
|
7145
|
+
}
|
|
6929
7146
|
// To Do (below Review): Doing rows with bar + mono %, Next up in queue order,
|
|
6930
7147
|
// error as a small in-row colour element (§1.5-8), quiet Done fold.
|
|
6931
7148
|
function renderFsTodo(){
|
|
@@ -6951,6 +7168,12 @@ function renderFsTodo(){
|
|
|
6951
7168
|
&& !pausedGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id)
|
|
6952
7169
|
&& list.some((t) => t.goalId === g.id && t.reply && t.status === 'queued')
|
|
6953
7170
|
&& !list.some((t) => t.goalId === g.id && !t.reply && ['running', 'queued'].includes(t.status)));
|
|
7171
|
+
const finalizingGoals = pgoals.filter((g) => g.status === 'running'
|
|
7172
|
+
&& !pausedGoalIds.has(g.id)
|
|
7173
|
+
&& !running.some((t) => t.goalId === g.id)
|
|
7174
|
+
&& !parkedReplyGoals.some((x) => x.id === g.id)
|
|
7175
|
+
&& list.some((t) => t.goalId === g.id)
|
|
7176
|
+
&& list.filter((t) => t.goalId === g.id).every((t) => ['done', 'skipped'].includes(t.status)));
|
|
6954
7177
|
const tt = (s, n) => esc(String(s ?? '').replace(/\s+/g, ' ').slice(0, n ?? 70));
|
|
6955
7178
|
// LATER = deliberately shelved goals (composer "Later" send → server status 'pending';
|
|
6956
7179
|
// PRD §8.2 top gap — they were invisible in flagship). Staged disclosure: the cap
|
|
@@ -6972,9 +7195,10 @@ function renderFsTodo(){
|
|
|
6972
7195
|
running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
|
|
6973
7196
|
const goal = pgoals.find((g) => g.id === t.goalId);
|
|
6974
7197
|
const title = t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
|
|
6975
|
-
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>
|
|
7198
|
+
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>
|
|
6976
7199
|
<div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
|
|
6977
7200
|
}).join('')
|
|
7201
|
+
+ 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('')
|
|
6978
7202
|
// 一時停止したゴールは Doing に「Paused」1行(アンバー)で留める+Resume。走行/中断行は上と needsRows で抑止済み。
|
|
6979
7203
|
+ 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('');
|
|
6980
7204
|
// NEEDS YOU: stopped/errored work that used to fold into Doing, now under its own caption so
|
|
@@ -6994,19 +7218,23 @@ function renderFsTodo(){
|
|
|
6994
7218
|
// exists (server-side retry = "resume if possible, else re-run"; there is no separate
|
|
6995
7219
|
// resume endpoint to call).
|
|
6996
7220
|
failed.filter((t) => !pausedGoalIds.has(t.goalId) && !resumedGoalIds.has(t.goalId)).map((t) => { const isInt = t.status === 'interrupted';
|
|
6997
|
-
const
|
|
7221
|
+
const goal = pgoals.find((g) => g.id === t.goalId);
|
|
7222
|
+
const meta = taskErrorMeta(t, goal);
|
|
7223
|
+
const word = isInt ? 'Interrupted' : 'Failed', icon = isInt ? '⏸' : '✗';
|
|
6998
7224
|
// don't restate the state word as the "reason" too (e.g. "Interrupted / Interrupted")
|
|
6999
7225
|
// when the task has no distinct result text — the coloured word already said it, so
|
|
7000
7226
|
// the reason line only prints when there's real detail to add.
|
|
7001
7227
|
const reason = String(t.result || '').trim();
|
|
7002
7228
|
const reasonLine = reason && reason.toLowerCase() !== word.toLowerCase()
|
|
7003
|
-
? `<div class="msg"><span class="x ${isInt ? 'int' : ''}">${icon}</span>${tt(reason,
|
|
7229
|
+
? `<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>`
|
|
7230
|
+
: `<div class="msg"><span class="x ${isInt ? 'int' : ''}">${icon}</span><b>${esc(meta.title)}</b> — ${esc(meta.body)}</div>`;
|
|
7004
7231
|
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>
|
|
7005
7232
|
<div class="errline">${reasonLine}
|
|
7006
|
-
<div class="acts2"
|
|
7007
|
-
+ blocked.map((g) => g.blocked?.kind === 'paused'
|
|
7008
|
-
|
|
7009
|
-
<div class="
|
|
7233
|
+
<div class="acts2">${errorButtonsHtml(meta, { taskId: t.id, goalId: t.goalId, interrupted: isInt, agent: t.agent || goal?.agent })}</div></div></div>`; }).join('')
|
|
7234
|
+
+ blocked.map((g) => { if (g.blocked?.kind === 'paused') return ''; const meta = goalBlockedErrorMeta(g);
|
|
7235
|
+
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>
|
|
7236
|
+
<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>
|
|
7237
|
+
<div class="acts2">${errorButtonsHtml(meta, { goalId: g.id, agent: g.agent })}<button class="errbtn" data-fsarchive="${g.id}">Archive</button></div></div></div>`; }).join('')
|
|
7010
7238
|
+ 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>
|
|
7011
7239
|
<div class="errline"><div class="msg"><span class="x int">?</span>${tt(g.question?.text || 'A question is waiting on you', 180)}</div>
|
|
7012
7240
|
<div class="acts2"><button class="errbtn" data-fsglog="${g.id}">Answer</button></div></div></div>`).join('');
|
|
@@ -7033,16 +7261,16 @@ function renderFsTodo(){
|
|
|
7033
7261
|
}
|
|
7034
7262
|
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}`;
|
|
7035
7263
|
}).join('');
|
|
7036
|
-
//
|
|
7037
|
-
// "
|
|
7038
|
-
//
|
|
7039
|
-
// the whole lane ("To Do"), so it now counts everything the lane actually renders
|
|
7040
|
-
// below it: Doing (running) + Needs you (failed/interrupted tasks + stopped/needsInput
|
|
7041
|
-
// goals) + Next up (queued) + Later (pending goals). "· next-up order" stays keyed off
|
|
7042
|
-
// queued.length specifically, since that suffix describes the Next-up sublist's own sort,
|
|
7043
|
-
// not the lane total.
|
|
7264
|
+
// Keep executable work and deliberately-shelved Later work visibly distinct.
|
|
7265
|
+
// "To Do 6" while all six were in Later made an idle queue look broken: Later
|
|
7266
|
+
// never auto-starts by design. The header now tells the operational truth.
|
|
7044
7267
|
const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
|
|
7045
|
-
const
|
|
7268
|
+
const doingCount = running.length + finalizingGoals.length + pausedGoalIds.size;
|
|
7269
|
+
const nextCount = parkedReplyGoals.length + queued.length;
|
|
7270
|
+
const hasPipelineWork = doingCount > 0 || nextCount > 0 || pgoals.some((g) => ['planning', 'stacked'].includes(g.status));
|
|
7271
|
+
const laterIdle = later.length && !hasPipelineWork
|
|
7272
|
+
? `<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>`
|
|
7273
|
+
: '';
|
|
7046
7274
|
// Free-tier pending counter (H22): on the free plan the header shows the wall
|
|
7047
7275
|
// — pending goals out of the 5-slot cap — reddening at the cap and gone once
|
|
7048
7276
|
// paid. Reads the SAME numbers the gate enforces (state.billing, never a local
|
|
@@ -7053,7 +7281,7 @@ function renderFsTodo(){
|
|
|
7053
7281
|
const _showFc = _bill.plan !== 'paid' && Number.isFinite(_fp) && Number.isFinite(_fl);
|
|
7054
7282
|
const _todoNum = _showFc
|
|
7055
7283
|
? `<span class="fcount fstdlabel${_fp >= _fl ? ' cap' : ''}">${_fp}<span class="fc-l">/${_fl}</span></span>`
|
|
7056
|
-
: `<span class="num fstdlabel">${
|
|
7284
|
+
: `<span class="num fstdlabel">${nextCount} next · ${doingCount} doing${later.length ? ` · ${later.length} later` : ''}</span>`;
|
|
7057
7285
|
el.innerHTML = `<div class="lhd sechd" id="fsTodoHd"><span class="hd fstdlabel">To Do</span>${_todoNum}<span class="fold fstdlabel">▾</span></div>
|
|
7058
7286
|
<div class="tl">
|
|
7059
7287
|
${(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}` : ''}
|
|
@@ -7061,8 +7289,11 @@ function renderFsTodo(){
|
|
|
7061
7289
|
${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('')}` : ''}
|
|
7062
7290
|
${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
|
|
7063
7291
|
${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
|
|
7064
|
-
${laterRows ? `<span class="tlcap">Later</span>${laterRows}` : ''}
|
|
7065
7292
|
<button class="qadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button>
|
|
7293
|
+
<span class="tlcap">Later</span>
|
|
7294
|
+
<button class="qadd lateradd" data-fslaternew="1" title="New Later goal"><span class="paplus"></span>New</button>
|
|
7295
|
+
${laterIdle}
|
|
7296
|
+
${laterRows}
|
|
7066
7297
|
</div>
|
|
7067
7298
|
${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
|
|
7068
7299
|
<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>` : ''}`;
|
|
@@ -7070,6 +7301,11 @@ function renderFsTodo(){
|
|
|
7070
7301
|
$('fsTodoHd').onclick = () => { fsUI.todoFold = !fsUI.todoFold; el.classList.toggle('folded', fsUI.todoFold); };
|
|
7071
7302
|
const qadd = el.querySelector('.qadd[data-fsnew]'); // + New → focus the composer (the real create path)
|
|
7072
7303
|
if (qadd) qadd.onclick = () => $('input')?.focus();
|
|
7304
|
+
const laterAdd = el.querySelector('.qadd[data-fslaternew]');
|
|
7305
|
+
if (laterAdd) laterAdd.onclick = (e) => {
|
|
7306
|
+
e.stopPropagation();
|
|
7307
|
+
fsInlineInput(laterAdd, '', 'Add to Later…', fsCreatePendingGoal);
|
|
7308
|
+
};
|
|
7073
7309
|
const dl = el.querySelector('#fsDoneLine');
|
|
7074
7310
|
if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
|
|
7075
7311
|
for (const r of el.querySelectorAll('[data-fsgoalrow]')) r.onclick = (e) => {
|
|
@@ -7096,12 +7332,10 @@ function renderFsTodo(){
|
|
|
7096
7332
|
};
|
|
7097
7333
|
for (const b of el.querySelectorAll('[data-fspause]')) b.onclick = togglePauseActive; // list版 Doing 一時停止/再生(+Paused行のResume)
|
|
7098
7334
|
for (const b of el.querySelectorAll('[data-fsretry]')) b.onclick = () => fsRetryTask(b.dataset.fsretry);
|
|
7099
|
-
for (const b of el.querySelectorAll('[data-fscodex]')) b.onclick =
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
await refresh();
|
|
7104
|
-
};
|
|
7335
|
+
for (const b of el.querySelectorAll('[data-fscodex]')) b.onclick = (e) => { e.stopPropagation(); fsRunWithCodexGoal(b); };
|
|
7336
|
+
for (const b of el.querySelectorAll('[data-fsconnectworker]')) b.onclick = (e) => { e.stopPropagation(); fsConnectWorker(b); };
|
|
7337
|
+
for (const b of el.querySelectorAll('[data-fscheckworker]')) b.onclick = (e) => { e.stopPropagation(); fsCheckWorker(b); };
|
|
7338
|
+
for (const b of el.querySelectorAll('[data-fsprretry]')) b.onclick = (e) => { e.stopPropagation(); fsRetryPrGoal(b); };
|
|
7105
7339
|
for (const b of el.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
7106
7340
|
for (const b of el.querySelectorAll('[data-fsglog]')) b.onclick = () => toggleGoalDetail(Number(b.dataset.fsglog));
|
|
7107
7341
|
for (const b of el.querySelectorAll('[data-fsreverify]')) b.onclick = async () => {
|
|
@@ -7118,6 +7352,12 @@ function renderFsTodo(){
|
|
|
7118
7352
|
for (const b of el.querySelectorAll('[data-fspstart]')) b.onclick = async (e) => { e.stopPropagation();
|
|
7119
7353
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fspstart}/activate`), { method: 'POST' });
|
|
7120
7354
|
if (!r.ok) showErr('Start failed.'); else await refresh(); };
|
|
7355
|
+
const startNextLater = el.querySelector('[data-fsstartnextlater]');
|
|
7356
|
+
if (startNextLater) startNextLater.onclick = async (e) => { e.stopPropagation();
|
|
7357
|
+
startNextLater.disabled = true;
|
|
7358
|
+
const r = await fetch(withKey(`/api/goals/${startNextLater.dataset.fsstartnextlater}/activate`), { method: 'POST' });
|
|
7359
|
+
if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; }
|
|
7360
|
+
else await refresh(); };
|
|
7121
7361
|
for (const b of el.querySelectorAll('[data-fspdel]')) b.onclick = async (e) => { e.stopPropagation();
|
|
7122
7362
|
const preview = b.closest('.trow')?.querySelector('.tt')?.textContent || '';
|
|
7123
7363
|
if (!(await confirmDelete({ title: 'Delete this shelved goal?', preview }))) return;
|
|
@@ -7141,6 +7381,20 @@ function renderFsTodo(){
|
|
|
7141
7381
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsdel}`), { method: 'DELETE' });
|
|
7142
7382
|
if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
|
|
7143
7383
|
}
|
|
7384
|
+
async function fsCreatePendingGoal(text){
|
|
7385
|
+
const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
7386
|
+
body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending: true }) });
|
|
7387
|
+
if (r.status === 402) {
|
|
7388
|
+
const data = await r.json().catch(() => ({}));
|
|
7389
|
+
showBillingBlocked(data.blocked);
|
|
7390
|
+
return;
|
|
7391
|
+
}
|
|
7392
|
+
if (!r.ok) { showErr('Later add failed.'); return; }
|
|
7393
|
+
clearBillingBlocked();
|
|
7394
|
+
const goal = await r.json();
|
|
7395
|
+
if (goal?.id) upsert(state.goals, goal);
|
|
7396
|
+
render();
|
|
7397
|
+
}
|
|
7144
7398
|
function fsInlineInput(afterEl, value, placeholder, onSubmit){
|
|
7145
7399
|
if (!afterEl || afterEl.nextElementSibling?.classList.contains('fsinline')) return;
|
|
7146
7400
|
const wrap = document.createElement('div'); wrap.className = 'fsinline';
|
|
@@ -8062,6 +8316,7 @@ async function refresh(){
|
|
|
8062
8316
|
renderModelSelect();
|
|
8063
8317
|
renderEffortSelect();
|
|
8064
8318
|
state.queueOrder = s.queueOrder ?? {}; state.externalActivity = s.externalActivity ?? [];
|
|
8319
|
+
state.runningCounts = s.runningCounts ?? {}; state.parallelLimits = s.parallelLimits ?? null;
|
|
8065
8320
|
state.workflowColumns = s.workflowColumns ?? {};
|
|
8066
8321
|
state.reviewDefinitions = s.reviewDefinitions ?? {};
|
|
8067
8322
|
checkUiVersion(s.uiVersion);
|
|
@@ -8126,7 +8381,7 @@ function connectSSE(){
|
|
|
8126
8381
|
// review→done flip on a goal that has a PR means the user just merged it on GitHub and
|
|
8127
8382
|
// came back. Celebrate (confetti) + a "merged" toast so the merge is unmistakable.
|
|
8128
8383
|
if (prev && prev.status === 'review' && m.goal.status === 'done' && m.goal.pr) {
|
|
8129
|
-
try { celebrateApprove(); } catch {}
|
|
8384
|
+
try { finishOpenReview(m.goal); } catch { try { celebrateApprove(); } catch {} }
|
|
8130
8385
|
const prNo = (String(m.goal.pr).match(/pull\/(\d+)/) ?? [, ''])[1];
|
|
8131
8386
|
showToast(`PR #${prNo} merged — “${(m.goal.text || '').replace(/\s+/g, ' ').slice(0, 48)}” is Done`);
|
|
8132
8387
|
}
|
|
@@ -8364,7 +8619,7 @@ async function postOutboxItem(item){
|
|
|
8364
8619
|
posting.add(item.oid);
|
|
8365
8620
|
try {
|
|
8366
8621
|
const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
8367
|
-
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 }) });
|
|
8622
|
+
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 }) });
|
|
8368
8623
|
if (r.status === 402) {
|
|
8369
8624
|
const data = await r.json().catch(() => ({}));
|
|
8370
8625
|
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
|
|
@@ -8624,8 +8879,8 @@ async function send(){
|
|
|
8624
8879
|
oid: `o${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
8625
8880
|
projectId: state.active, text, pr: 'auto', model, effort, agent: state.connectedApp,
|
|
8626
8881
|
mode, skill: skill || null,
|
|
8627
|
-
|
|
8628
|
-
//
|
|
8882
|
+
workspaceMode: workspaceMode(),
|
|
8883
|
+
// Keep the old wire contract too: only direct sends worktree:false.
|
|
8629
8884
|
worktree: worktreeOn() ? undefined : false,
|
|
8630
8885
|
pending: later, review,
|
|
8631
8886
|
images: attach.map((a) => a.path), imageUrls: attach.map((a) => a.url),
|
|
@@ -8816,20 +9071,48 @@ function saRows(){
|
|
|
8816
9071
|
const ts = (state.tasks || []).filter((t) => t.projectId === state.active);
|
|
8817
9072
|
return buildReviewDigest({ goals: gs, tasks: ts }).map((r) => saRowFromDigest(r, gs, ts));
|
|
8818
9073
|
}
|
|
9074
|
+
function latestGoalOutcomeTask(goalId, tasks){
|
|
9075
|
+
return (tasks || []).filter((t) => t.goalId === goalId && t.status === 'done' && String(t.result ?? '').trim())
|
|
9076
|
+
.sort((a, b) => Date.parse(b.finishedAt || b.startedAt || b.createdAt || 0)
|
|
9077
|
+
- Date.parse(a.finishedAt || a.startedAt || a.createdAt || 0))[0] || null;
|
|
9078
|
+
}
|
|
9079
|
+
function taskAttemptId(t){ return t?.attemptId || (t?.id != null ? `task-${t.id}` : null); }
|
|
9080
|
+
function taskSortTime(t){ return Date.parse(t?.finishedAt || t?.startedAt || t?.createdAt || 0) || 0; }
|
|
9081
|
+
function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
9082
|
+
if (outcome?.run) return { taskId: outcome.id, attemptId: taskAttemptId(outcome), inherited: false, ...outcome.run };
|
|
9083
|
+
const runs = (tasks || []).filter((t) => t.goalId === goalId && t.run)
|
|
9084
|
+
.sort((a, b) => taskSortTime(b) - taskSortTime(a));
|
|
9085
|
+
if (!runs.length) return null;
|
|
9086
|
+
const cutoff = outcome ? taskSortTime(outcome) : Infinity;
|
|
9087
|
+
const prev = runs.find((t) => t.id !== outcome?.id && taskSortTime(t) <= cutoff) || (!outcome ? runs[0] : null);
|
|
9088
|
+
if (!prev) return null;
|
|
9089
|
+
const note = '前回のpreviewを継承しています: 最新replyは新しいpreviewを宣言しませんでした。 / Inherited previous preview: the latest reply did not declare a new preview.';
|
|
9090
|
+
return {
|
|
9091
|
+
taskId: prev.id,
|
|
9092
|
+
attemptId: taskAttemptId(outcome) || taskAttemptId(prev),
|
|
9093
|
+
inherited: true,
|
|
9094
|
+
inheritedFromTaskId: prev.id,
|
|
9095
|
+
inheritedForTaskId: outcome?.id ?? null,
|
|
9096
|
+
...prev.run,
|
|
9097
|
+
note: prev.run?.note ? `${note} ${prev.run.note}` : note,
|
|
9098
|
+
};
|
|
9099
|
+
}
|
|
8819
9100
|
// One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
|
|
8820
9101
|
// "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
|
|
8821
9102
|
// whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
|
|
8822
9103
|
function saRowFromDigest(r, gs, ts){
|
|
8823
9104
|
const g = gs.find((x) => x.id === r.goalIds[0]);
|
|
9105
|
+
const outcome = latestGoalOutcomeTask(g?.id, ts);
|
|
8824
9106
|
const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
|
|
8825
9107
|
// "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
|
|
8826
9108
|
// per finished requirement task's own report; falls back to the goal's plan
|
|
8827
9109
|
// steps when nothing has finished yet (e.g. a plan awaiting approval — the plan
|
|
8828
9110
|
// IS "what we're going to do" in that case, which is the honest content to show).
|
|
8829
|
-
const doneTasks = ts.filter((t) => r.goalIds.includes(t.goalId) && !t.reply && t.result)
|
|
9111
|
+
const doneTasks = ts.filter((t) => r.goalIds.includes(t.goalId) && !t.reply && t.result && t.status !== 'skipped')
|
|
8830
9112
|
.sort((a, b) => (a.num ?? 0) - (b.num ?? 0));
|
|
8831
9113
|
const did = doneTasks.length
|
|
8832
9114
|
? doneTasks.map((t) => String(t.result).replace(/\s+/g, ' ').trim().slice(0, 160))
|
|
9115
|
+
: outcome ? [String(outcome.result).replace(/\s+/g, ' ').trim().slice(0, 160)]
|
|
8833
9116
|
: (Array.isArray(g?.plan) ? g.plan.filter(Boolean).map((s) => String(s).replace(/\s+/g, ' ').trim()) : []);
|
|
8834
9117
|
// Sibling goals (H23 §2/§4 "did the AI drop part of what I asked"): buildReviewDigest
|
|
8835
9118
|
// already collapses goals that share one PR into a single row (goalIds.length > 1)
|
|
@@ -8854,6 +9137,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8854
9137
|
check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
|
|
8855
9138
|
goalText: String(g?.text ?? '').replace(/\s+/g, ' ').trim(),
|
|
8856
9139
|
did, siblings, risk: g?.risk ?? null,
|
|
9140
|
+
verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
|
|
8857
9141
|
note: g?.retest?.passed ? g.retest.note : null,
|
|
8858
9142
|
pr: r.pr,
|
|
8859
9143
|
// P0b (Masa 2026-07-21): the AI's own TodoWrite breakdown for the "Steps" section
|
|
@@ -8884,7 +9168,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8884
9168
|
// lines are NOT in state — 1000 of them per task would bloat every page load — so
|
|
8885
9169
|
// this is just the id to ask for when the reviewer opens Activity.
|
|
8886
9170
|
logTaskId: (() => {
|
|
8887
|
-
const t = ts.find((x) => x.goalId === g?.id &&
|
|
9171
|
+
const t = [...ts].reverse().find((x) => x.goalId === g?.id && x.activityLog);
|
|
8888
9172
|
return t?.id ?? null;
|
|
8889
9173
|
})(),
|
|
8890
9174
|
// P1 (Masa 2026-07-21): the AI's own final report, verbatim and untrimmed — the
|
|
@@ -8892,15 +9176,14 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8892
9176
|
// every ask that has no running thing to open ("research this", "explain that"),
|
|
8893
9177
|
// and the explanation for every ask that does.
|
|
8894
9178
|
report: (() => {
|
|
8895
|
-
const t = ts.find((x) => x.goalId === g?.id && !x.reply && x.result);
|
|
9179
|
+
const t = outcome || ts.find((x) => x.goalId === g?.id && !x.reply && x.result);
|
|
8896
9180
|
return String(t?.result ?? '').replace(/^検証(PASS|FAIL)[^\n]*\n+/, '').trim();
|
|
8897
9181
|
})(),
|
|
8898
9182
|
// …and the worker's own "here's how you see it running" declaration (task.run,
|
|
8899
9183
|
// written by the worker into .manager-run.json). Absent for asks with nothing to
|
|
8900
9184
|
// run — then no button, which is the honest state, not a missing feature.
|
|
8901
9185
|
run: (() => {
|
|
8902
|
-
|
|
8903
|
-
return t ? { taskId: t.id, ...t.run } : null;
|
|
9186
|
+
return latestGoalRunForOutcome(g?.id, ts, outcome);
|
|
8904
9187
|
})(),
|
|
8905
9188
|
// Explicit review-summary display fields (spec §11: the card's shape follows the
|
|
8906
9189
|
// task). A goal's reviewSummary may carry them directly (e.g. a conversational task
|
|
@@ -8955,6 +9238,7 @@ function saSoloDigestRow(g, ts){
|
|
|
8955
9238
|
return {
|
|
8956
9239
|
goalIds: [g.id], reviewNumber: goalReviewNumber({ goal: g, tasks: ts }), checkLine,
|
|
8957
9240
|
pr: g.pr ?? null, testResult: g.testResult ?? null,
|
|
9241
|
+
verificationInfraError: g.verificationInfraError ?? null,
|
|
8958
9242
|
proof: proofTask?.proof ?? null, proofTaskId: proofTask?.id ?? null, plan: false, planText: null,
|
|
8959
9243
|
};
|
|
8960
9244
|
}
|
|
@@ -9330,6 +9614,9 @@ function saItemHtml(it, i){
|
|
|
9330
9614
|
const openBody = (it.run?.cmd || it.run?.url)
|
|
9331
9615
|
? `<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>`
|
|
9332
9616
|
: '';
|
|
9617
|
+
const prRepairBody = it.goal?.prError
|
|
9618
|
+
? `<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>`
|
|
9619
|
+
: '';
|
|
9333
9620
|
// ── Also carries (Masa 2026-07-21 → refined 2026-07-23): goals folded onto this review are
|
|
9334
9621
|
// logged HERE, inside the review's own content — NOT as loose rows in the Review column
|
|
9335
9622
|
// (Masa: "ここにはいらない・逆にわかりにくい"). This is the ONE place a folded goal is shown,
|
|
@@ -9364,6 +9651,10 @@ function saItemHtml(it, i){
|
|
|
9364
9651
|
// change explains itself here, and an image the AI pasted shows up here as an image.
|
|
9365
9652
|
// We interpret nothing; mdLite only turns Markdown into the shapes it already means. ──
|
|
9366
9653
|
const reportBody = it.report ? `<div class="rreport">${mdLite(it.report, { images: false })}</div>` : '';
|
|
9654
|
+
const infra = it.verificationInfraError;
|
|
9655
|
+
const infraBody = infra?.detail
|
|
9656
|
+
? `<div class="rinfra"><p class="rbody">${esc(infra.detail)}</p><button type="button" class="retrybtn" data-saact="reverify:${i}">再検証</button></div>`
|
|
9657
|
+
: '';
|
|
9367
9658
|
// ── Screenshots (P2, Masa 2026-07-22): the pictures the AI took of its own result.
|
|
9368
9659
|
// Full width, at their own aspect — a four-variant comparison sheet is unreadable
|
|
9369
9660
|
// shrunk into a thumbnail, and that is the shape the AI actually produces. The file
|
|
@@ -9489,9 +9780,11 @@ function saItemHtml(it, i){
|
|
|
9489
9780
|
${meta}
|
|
9490
9781
|
${reqSec}
|
|
9491
9782
|
${S('Open it', openBody)}
|
|
9783
|
+
${S('PR', prRepairBody)}
|
|
9492
9784
|
${S('Also carries', carriesBody)}
|
|
9493
9785
|
${S('What to check', checkBody)}
|
|
9494
9786
|
${S('Result', reportBody)}
|
|
9787
|
+
${S(infra?.title || '検証基盤エラー', infraBody)}
|
|
9495
9788
|
${S('Screenshots', shotsBody)}
|
|
9496
9789
|
${S('Steps', stepsBody)}
|
|
9497
9790
|
${S('Proof', proofInner)}
|
|
@@ -9948,8 +10241,17 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9948
10241
|
}
|
|
9949
10242
|
const it = SA.items[i];
|
|
9950
10243
|
if (!it) return;
|
|
10244
|
+
if (k === 'retrypr') return fsRetryPrGoal({ disabled: false, textContent: '', dataset: { fsprretry: String(it.goalId) } });
|
|
10245
|
+
if (k === 'glog') return toggleGoalDetail(it.goalId);
|
|
9951
10246
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
9952
10247
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
10248
|
+
if (k === 'reverify') {
|
|
10249
|
+
b.disabled = true;
|
|
10250
|
+
b.textContent = '再検証中…';
|
|
10251
|
+
const r = await fetch(withKey(`/api/goals/${it.goalId}/reverify`), { method: 'POST' });
|
|
10252
|
+
if (r.ok) await refresh(); else showErr('再検証に失敗');
|
|
10253
|
+
return;
|
|
10254
|
+
}
|
|
9953
10255
|
// Open it (P1, Masa 2026-07-21): start the thing the worker said to run, then open it.
|
|
9954
10256
|
// Booting a real process takes seconds, so the chip says "Starting…" while it waits —
|
|
9955
10257
|
// and a failure lands in the detail line as a plain reason. It never turns the goal
|
|
@@ -10143,16 +10445,30 @@ function renderCtxBar(){
|
|
|
10143
10445
|
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>`;
|
|
10144
10446
|
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>`;
|
|
10145
10447
|
}
|
|
10146
|
-
//
|
|
10147
|
-
//
|
|
10148
|
-
//
|
|
10149
|
-
// someone means when they want to watch it happen. Sticky per project, like model and
|
|
10150
|
-
// effort, because it is a working habit rather than a per-message thought.
|
|
10448
|
+
// Workspace mode. isolated worktree is the default clean copy; direct workspace inherits
|
|
10449
|
+
// and edits the current folder. Sticky per project, like model and effort, because it is
|
|
10450
|
+
// a working habit rather than a per-message thought.
|
|
10151
10451
|
function worktreeKey(){ return `sel:worktree:${state.active}`; }
|
|
10152
10452
|
function worktreeOn(){ return localStorage.getItem(worktreeKey()) !== '0'; }
|
|
10453
|
+
function workspaceMode(){ return worktreeOn() ? 'isolated-worktree' : 'direct-workspace'; }
|
|
10454
|
+
function goalWorkspaceMode(g){ return (g?.workspaceMode === 'direct-workspace' || g?.worktree === false) ? 'direct-workspace' : 'isolated-worktree'; }
|
|
10455
|
+
function workspaceModeText(g){ return goalWorkspaceMode(g) === 'direct-workspace' ? 'direct workspace' : 'isolated worktree'; }
|
|
10153
10456
|
function renderWorktreeChip(){
|
|
10154
10457
|
const el = $('ctxwt'); if (!el) return;
|
|
10155
|
-
|
|
10458
|
+
const isolated = worktreeOn();
|
|
10459
|
+
const proj = state.projects.find((p) => p.id === state.active);
|
|
10460
|
+
el.setAttribute('aria-checked', isolated ? 'true' : 'false');
|
|
10461
|
+
const label = $('ctxwtlabel');
|
|
10462
|
+
if (label) label.textContent = isolated ? 'isolated worktree' : 'direct workspace';
|
|
10463
|
+
el.title = isolated
|
|
10464
|
+
? 'isolated worktree: default clean copy; this folder is not edited.'
|
|
10465
|
+
: 'direct workspace: inherits the current state and edits this folder.';
|
|
10466
|
+
const warn = $('ctxworkwarn');
|
|
10467
|
+
if (warn) {
|
|
10468
|
+
const dirty = !isolated && !!proj?.dirty;
|
|
10469
|
+
warn.hidden = !dirty;
|
|
10470
|
+
warn.textContent = dirty ? `Dirty workspace warning: direct workspace will edit ${folderName(proj?.dir) || 'this folder'} as it is now.` : '';
|
|
10471
|
+
}
|
|
10156
10472
|
}
|
|
10157
10473
|
// Asked for once, and only when the menu is first opened — the same "only when you look
|
|
10158
10474
|
// at it" rule the implementation log and the conversation follow.
|