@galda/cli 0.10.111 → 0.10.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +5 -0
- package/app/index.html +158 -70
- package/engine/lib.mjs +11 -0
- package/engine/server.mjs +6 -2
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
- **UI(app/index.html・app/theme.css 等)に触る前に、必ず `docs/design/DESIGN-RULES.md` を読むこと**。特に §1.5 レイアウトの憲法(10条)と §5.5 美学。条文に反する実装はせず、変えたい場合は理由を添えて仲田さんの決定を仰ぐ。
|
|
58
58
|
- app/ のUIファイル編集時はhook(tools/design_rules_hook.sh)が自動でリマインドする。リマインドが出たら読んだ上で作業すること。
|
|
59
59
|
- 実装時の詳細契約は `docs/design/CTO-HANDOFF.md`(CDO→CTO引き継ぎ書)に従う。**既存パーツで作れない新しい画面・コンポーネントが必要になったら、自作せず実装前にCDOへ依頼すること。**
|
|
60
|
+
- 🔴 **UIの文言は英語(Masa決定 2026-07-31)**。ボタン・ラベル・見出し・プレースホルダ・空状態・折りたたみの開閉ラベル・トースト/エラーなど**製品が書く文字は全て英語**。日本語のUI文字列を新しく足さない。
|
|
61
|
+
- 境界は**「誰の言葉か」**。製品が書くもの=英語。**ユーザーやworkerの発言をそのまま出している所(依頼文・返信・報告・質問の原文)は原文のまま**=翻訳も言語強制もしない。
|
|
62
|
+
- **コードのコメントは対象外**(日本語のままでよい)。
|
|
63
|
+
- 🟡 **例外=日英の出し分け(`failureLang`/`prefersJapaneseText`/`FAILURE_COPY`/`BILLING_COPY`/`AUTH_COPY`/`summarizeAttention`)は現状維持**(Masa決定 2026-07-31)。**依頼文の言語に追従するのは仕様**。「UIは英語」を根拠にこれらの `ja` を削除しない。英語化するのは**無条件に日本語が出る固定ラベル**だけ。
|
|
64
|
+
- 全文=`docs/design/DESIGN-RULES.md` §5.5「文章・コンテンツの品」。
|
|
60
65
|
|
|
61
66
|
## プロダクトの不変条件(実装前に読む・2026-07-30 追加)
|
|
62
67
|
|
package/app/index.html
CHANGED
|
@@ -2325,7 +2325,7 @@
|
|
|
2325
2325
|
.redesign .gh-del .ghm{color:#f85149}
|
|
2326
2326
|
.redesign .gh-hi{border-radius:2px;padding:0 1px}
|
|
2327
2327
|
.redesign .gh-add .gh-hi{background:rgba(46,204,113,.55)} .redesign .gh-del .gh-hi{background:rgba(248,81,73,.5)}
|
|
2328
|
-
/* huge-diff footer (spec §7): "…+N 行 · GitHub
|
|
2328
|
+
/* huge-diff footer (spec §7): "…+N 行 · Open on GitHub ↗" */
|
|
2329
2329
|
.redesign .ghfoot{margin-top:8px;font-family:var(--mono);font-size:11.5px;color:var(--ink3);display:flex;gap:2px;align-items:center;flex-wrap:wrap}
|
|
2330
2330
|
.redesign .ghfoot .ghopen{color:var(--ink2);text-decoration:none}
|
|
2331
2331
|
.redesign .ghfoot .ghopen:hover{color:var(--blue)}
|
|
@@ -2378,6 +2378,10 @@
|
|
|
2378
2378
|
.redesign .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3)}
|
|
2379
2379
|
.redesign .txr.you .w,.redesign .txr.cl .w{color:var(--ink);font-weight:600}
|
|
2380
2380
|
.redesign .txr .m{flex:1;min-width:0;color:var(--ink2)}
|
|
2381
|
+
/* The existing left-hand Activity / Conversation disclosure owns the full text.
|
|
2382
|
+
When it is closed, its preview is capped at three rendered lines; opening the
|
|
2383
|
+
same › control reveals the unchanged transcript. */
|
|
2384
|
+
.redesign .lacc:not([open]) .lfirst .m{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
|
|
2381
2385
|
.redesign .txr.cl .m{color:var(--ink)}
|
|
2382
2386
|
.redesign .txr.tool .m{color:var(--ink2);font-family:var(--mono);font-size:12px}
|
|
2383
2387
|
.redesign .txr.tool .m b{color:var(--ink);font-weight:600}
|
|
@@ -3051,7 +3055,7 @@
|
|
|
3051
3055
|
<div class="spacer"></div>
|
|
3052
3056
|
<!-- Feedback 7: layout switcher — flip the arrangement live. Increment 1:
|
|
3053
3057
|
現行 / タスク中央 / 3列カンバン (CSS reflow). 中央フロー・1列 = increment 2. -->
|
|
3054
|
-
<div class="laysel" id="laysel" title="
|
|
3058
|
+
<div class="laysel" id="laysel" title="Switch layout">
|
|
3055
3059
|
<button class="laybtn" data-layout="bflow">B・Center-flow</button>
|
|
3056
3060
|
<button class="laybtn" data-layout="flagship">Flagship</button>
|
|
3057
3061
|
<button class="laybtn" data-layout="current">現行</button>
|
|
@@ -3060,7 +3064,7 @@
|
|
|
3060
3064
|
</div>
|
|
3061
3065
|
<!-- theme switch — the 5 release themes, English registered names only (v3 §6.5;
|
|
3062
3066
|
Glass archived 2026-07-07 · legacy "dark" no longer selectable). -->
|
|
3063
|
-
<div class="laysel" id="themesel" title="
|
|
3067
|
+
<div class="laysel" id="themesel" title="Switch theme">
|
|
3064
3068
|
<button class="laybtn" data-theme="studio">Studio</button>
|
|
3065
3069
|
<button class="laybtn" data-theme="gradient">Gradient</button>
|
|
3066
3070
|
<button class="laybtn" data-theme="banff">Banff</button>
|
|
@@ -3348,12 +3352,12 @@
|
|
|
3348
3352
|
<div class="overlay revfloat" id="summaryOverlay">
|
|
3349
3353
|
<div class="modal peramodal">
|
|
3350
3354
|
<div class="perahead">
|
|
3351
|
-
<div class="peratitle"
|
|
3355
|
+
<div class="peratitle">Review summary<span class="peracount" id="peraCount">0</span></div>
|
|
3352
3356
|
<div class="peratabs" id="peraTabs"></div>
|
|
3353
3357
|
<button class="mbtn ghost" id="peraClose">Close</button>
|
|
3354
3358
|
</div>
|
|
3355
3359
|
<div class="peraask">
|
|
3356
|
-
<input type="text" id="peraAsk" placeholder="
|
|
3360
|
+
<input type="text" id="peraAsk" placeholder="Ask… e.g. What's critical now? / Just the failures / Sorted by priority">
|
|
3357
3361
|
<button type="button" class="askbtn" id="peraAskBtn" aria-label="Ask"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>
|
|
3358
3362
|
</div>
|
|
3359
3363
|
<div class="peraanswer" id="peraAnswer" hidden></div>
|
|
@@ -3703,7 +3707,7 @@ const TASK_PRIORITIES = ['高', '中', '低'];
|
|
|
3703
3707
|
function prioBadge(t){
|
|
3704
3708
|
const p = TASK_PRIORITIES.includes(t.priority) ? t.priority : '中';
|
|
3705
3709
|
return `<div class="priosel" data-priosel="${t.id}">
|
|
3706
|
-
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="
|
|
3710
|
+
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority">${p}</button>
|
|
3707
3711
|
<div class="priomenu" data-priomenu="${t.id}">${TASK_PRIORITIES.map((x) =>
|
|
3708
3712
|
`<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${x}</button>`).join('')}</div>
|
|
3709
3713
|
</div>`;
|
|
@@ -3721,7 +3725,7 @@ const DEFAULT_REVIEW_DEFINITION = {
|
|
|
3721
3725
|
};
|
|
3722
3726
|
// Shown (faint) under Review when no definition is set yet, so it's obvious the
|
|
3723
3727
|
// line is editable and what it's for.
|
|
3724
|
-
const REVIEW_DEF_HINT = '
|
|
3728
|
+
const REVIEW_DEF_HINT = 'e.g. "Write the pull request in English" — set what done means for a goal';
|
|
3725
3729
|
function reviewDefinitionFor(projectId){ return state.reviewDefinitions[projectId] ?? DEFAULT_REVIEW_DEFINITION; }
|
|
3726
3730
|
// A saved definition on disk may predate the reviewCard/language fields (old
|
|
3727
3731
|
// review-definitions.json entries are never re-validated on server load), so
|
|
@@ -4233,7 +4237,8 @@ function saShotsListHtml(shots){
|
|
|
4233
4237
|
if (!list.length) return '';
|
|
4234
4238
|
if (list.length === 1) return saShotFigHtml(list[0]);
|
|
4235
4239
|
const older = list.slice(0, -1);
|
|
4236
|
-
|
|
4240
|
+
const label = `Show ${older.length} older screenshot${older.length === 1 ? '' : 's'}`;
|
|
4241
|
+
return `${saShotFigHtml(list[list.length - 1])}<details class="rshotfold"><summary>${label}</summary>${older.map(saShotFigHtml).join('')}</details>`;
|
|
4237
4242
|
}
|
|
4238
4243
|
function saPlainPairHtml(pair){
|
|
4239
4244
|
const col = (sh, label) => `<div class="ba-col"><span class="balabel">${label}</span><img class="proofimg" src="${esc(sh.url)}" alt="${esc(label)}" loading="lazy"></div>`;
|
|
@@ -4296,7 +4301,7 @@ function reviewDigestHtml(rows){
|
|
|
4296
4301
|
</div>
|
|
4297
4302
|
</div>`;
|
|
4298
4303
|
};
|
|
4299
|
-
return `<div class="digestcard"><div class="digesthead"
|
|
4304
|
+
return `<div class="digestcard"><div class="digesthead">Pending reviews<span class="digestcount">${rows.length}</span></div>
|
|
4300
4305
|
<div class="digestrows">${rows.map(item).join('')}</div></div>`;
|
|
4301
4306
|
}
|
|
4302
4307
|
|
|
@@ -4411,7 +4416,7 @@ function reviewActionsHtml(g){
|
|
|
4411
4416
|
<div class="rvhead"><span class="dot"></span>レビュー — ${esc(sum.headline)} · ${esc(sum.detail)}</div>
|
|
4412
4417
|
<div class="rvwhat">${esc(sum.what)}</div>
|
|
4413
4418
|
<div class="rvitems">${items}</div>
|
|
4414
|
-
<button type="button" class="rvopen reqopen" data-goal="${g.id}"
|
|
4419
|
+
<button type="button" class="rvopen reqopen" data-goal="${g.id}">Open the review checklist →</button>
|
|
4415
4420
|
</div>`;
|
|
4416
4421
|
}
|
|
4417
4422
|
|
|
@@ -4432,7 +4437,7 @@ function goalRow(g){
|
|
|
4432
4437
|
// shouldShowGoalSummary/GOAL_COMPLETE_STATUSES と揃える(そちらは純関数として
|
|
4433
4438
|
// テスト済み — この行はブラウザ側の複製)。
|
|
4434
4439
|
const summary = GOAL_COMPLETE_STATUSES.includes(g.status) && g.summary
|
|
4435
|
-
? `<div class="gsummary"><div class="gslabel"><span class="dot"></span>Manager
|
|
4440
|
+
? `<div class="gsummary"><div class="gslabel"><span class="dot"></span>Manager summary</div>${esc(g.summary)}</div>` : '';
|
|
4436
4441
|
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>`
|
|
4437
4442
|
: g.prError ? (() => {
|
|
4438
4443
|
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 }; })();
|
|
@@ -4464,7 +4469,7 @@ function goalRow(g){
|
|
|
4464
4469
|
function extRow(c){
|
|
4465
4470
|
const short = esc((c.hash ?? '').slice(0, 7));
|
|
4466
4471
|
const when = c.date ? new Date(c.date).toLocaleString() : '';
|
|
4467
|
-
return `<div class="extrow"><span class="exthash">${short}</span><span class="extsubj">${esc(c.subject ?? '')} · Claude Code CLI
|
|
4472
|
+
return `<div class="extrow"><span class="exthash">${short}</span><span class="extsubj">${esc(c.subject ?? '')} · from Claude Code CLI</span>${when ? `<span class="extdate">${esc(when)}</span>` : ''}</div>`;
|
|
4468
4473
|
}
|
|
4469
4474
|
|
|
4470
4475
|
// Mirrors engine/lib.mjs mergeGoalTimeline() — kept in sync manually since
|
|
@@ -4753,7 +4758,7 @@ function renderTasks(){
|
|
|
4753
4758
|
// half-finished item leaves Attention for good instead of piling up. With
|
|
4754
4759
|
// no parent goal we fall back to skipping just the task.
|
|
4755
4760
|
const clear = g
|
|
4756
|
-
? `<button class="retrybtn skip" data-archivegoal="${g.id}" title="
|
|
4761
|
+
? `<button class="retrybtn skip" data-archivegoal="${g.id}" title="Retire this goal (no rework)">Archive</button>`
|
|
4757
4762
|
: `<button class="retrybtn skip" data-skipbtn="${t.id}">Dismiss</button>`;
|
|
4758
4763
|
cond = `${reason} <button class="retrybtn" data-retrybtn="${t.id}">Retry</button> ${clear}`;
|
|
4759
4764
|
} else if (t.reply && g) {
|
|
@@ -4881,7 +4886,7 @@ function renderTasks(){
|
|
|
4881
4886
|
const dAllClear = dGreen === digestCount && !dBlocked;
|
|
4882
4887
|
// S5 summary (deterministic — no LLM). ⤢ opens the ペライチ dashboard above.
|
|
4883
4888
|
const reviewDigest = digestCount
|
|
4884
|
-
? `<div class="s5sum" id="summaryEntry" title="Open the review summary
|
|
4889
|
+
? `<div class="s5sum" id="summaryEntry" title="Open the review summary">
|
|
4885
4890
|
<span class="s5exp"><svg width="13" height="13" viewBox="0 0 24 24"><path d="M15 4h5v5M20 4l-7 7M9 20H4v-5M4 20l7-7"/></svg></span>
|
|
4886
4891
|
<span class="s5ring" style="background:conic-gradient(var(--green) 0 ${dPct}%, var(--hair2) ${dPct}% 100%)"><span class="s5in">${dGreen}/${digestCount}</span></span>
|
|
4887
4892
|
<span class="s5tx"><span class="s5hl">${dAllClear ? 'Ready to review' : `${dGreen} of ${digestCount} ready`}</span><span class="s5sub">${dAllClear ? 'all green' : dGreen === digestCount ? 'blocked needs attention' : 'some failing'} · ${dBlocked ? `${dBlocked} blocked` : 'none blocked'}</span></span>
|
|
@@ -4905,7 +4910,7 @@ function renderTasks(){
|
|
|
4905
4910
|
// (see saItemHtml's carriesBody) instead — the list row stays just the ask.
|
|
4906
4911
|
return `<div class="task revrow" data-goal="${g.id}"><div class="ic pending"></div><div class="tx"><div class="name">${esc(checkLine)} ${goalSourceBadge(g.source)}</div>
|
|
4907
4912
|
<div class="cond">${prChipHtml(g.pr, g.prClosed, 'prbig')}</div>
|
|
4908
|
-
<div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="
|
|
4913
|
+
<div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="What should change? Sends it back to To Do for rework"><button type="button" class="qreplysend" data-qreplysend="${g.id}" aria-label="Send">${ICONS.up}</button></div></div>
|
|
4909
4914
|
<div class="revquick">
|
|
4910
4915
|
<button type="button" class="qbtn ok" data-qapprove="${g.id}" title="Approve">${ICONS.check}</button>
|
|
4911
4916
|
<button type="button" class="qbtn no" data-qdismiss="${g.id}" title="Send back">${ICONS.close}</button>
|
|
@@ -5057,7 +5062,7 @@ function renderTasks(){
|
|
|
5057
5062
|
// A question keeps the row open: the answer lands under this very box.
|
|
5058
5063
|
if (body.outcome !== 'answer') state.qreplyOpen.delete(Number(gid));
|
|
5059
5064
|
await refresh();
|
|
5060
|
-
} catch { showErr('
|
|
5065
|
+
} catch { showErr('Could not send.'); }
|
|
5061
5066
|
};
|
|
5062
5067
|
for (const b of $('tasklist').querySelectorAll('[data-qreplysend]')) {
|
|
5063
5068
|
b.onclick = (e) => { e.stopPropagation(); sendQReply(b.dataset.qreplysend, $('tasklist').querySelector(`[data-qreplyinput="${b.dataset.qreplysend}"]`)); };
|
|
@@ -5223,8 +5228,8 @@ function openGoalheadReply(id, btn){
|
|
|
5223
5228
|
wrap.appendChild(inp); wrap.appendChild(snd); head.insertAdjacentElement('afterend', wrap); inp.focus();
|
|
5224
5229
|
const send = async () => { const t = inp.value.trim(); if (!t) return;
|
|
5225
5230
|
let r; try { r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text: t })) }); }
|
|
5226
|
-
catch { showErr('
|
|
5227
|
-
if (!r.ok) showErr('
|
|
5231
|
+
catch { showErr("This goal hasn't started — use Edit instead."); return; }
|
|
5232
|
+
if (!r.ok) showErr("This goal hasn't started — use Edit instead."); await refresh(); };
|
|
5228
5233
|
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
5229
5234
|
snd.onclick = send;
|
|
5230
5235
|
}
|
|
@@ -5249,8 +5254,8 @@ function wirePriority(){
|
|
|
5249
5254
|
try {
|
|
5250
5255
|
const r = await fetch(withKey(`/api/tasks/${b.dataset.priotask}/priority`), { method: 'PUT',
|
|
5251
5256
|
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ priority: b.dataset.val }) });
|
|
5252
|
-
if (!r.ok) showErr('
|
|
5253
|
-
} catch { showErr('
|
|
5257
|
+
if (!r.ok) showErr('Could not change the priority.'); else await refresh();
|
|
5258
|
+
} catch { showErr('Could not change the priority.'); }
|
|
5254
5259
|
};
|
|
5255
5260
|
}
|
|
5256
5261
|
}
|
|
@@ -5395,21 +5400,21 @@ function renderRdEditor(){
|
|
|
5395
5400
|
<span class="rdhint">Any task whose headless-browser check didn't pass (or never ran) forces Review too.</span></span></label>
|
|
5396
5401
|
<label class="rdrow rddesc"><span><span class="rdlabel">Review instructions</span>
|
|
5397
5402
|
<span class="rdhint">Goes into the pull request body, so whoever reviews it knows what to look for. The worker doesn't read this while it works — instructions for the AI belong in your project's CLAUDE.md / AGENTS.md.</span>
|
|
5398
|
-
<textarea class="rdtext" id="rdDescription" maxlength="500" rows="2" placeholder="
|
|
5403
|
+
<textarea class="rdtext" id="rdDescription" maxlength="500" rows="2" placeholder="e.g. "Compare the before/after screenshots and check the spacing lines up" — what the reviewer should look at">${esc(state.rdDraft.description ?? '')}</textarea></span></label>
|
|
5399
5404
|
|
|
5400
|
-
<div class="rdsub"
|
|
5405
|
+
<div class="rdsub">Review card fields</div>
|
|
5401
5406
|
<label class="rdrow"><input type="checkbox" id="rdCardScreenshots"${state.rdDraft.reviewCard.screenshots ? ' checked' : ''}>
|
|
5402
|
-
<span><span class="rdlabel"
|
|
5403
|
-
<span class="rdhint"
|
|
5407
|
+
<span><span class="rdlabel">Screenshots</span>
|
|
5408
|
+
<span class="rdhint">Show the evidence (screenshots/GIFs) on the review card.</span></span></label>
|
|
5404
5409
|
|
|
5405
|
-
<div class="rdsub"
|
|
5410
|
+
<div class="rdsub">Output language</div>
|
|
5406
5411
|
<label class="rdrow"><select id="rdLanguage">
|
|
5407
5412
|
<option value="auto"${state.rdDraft.language === 'auto' ? ' selected' : ''}>Auto (match request)</option>
|
|
5408
5413
|
<option value="ja"${state.rdDraft.language === 'ja' ? ' selected' : ''}>日本語</option>
|
|
5409
5414
|
<option value="en"${state.rdDraft.language === 'en' ? ' selected' : ''}>English</option>
|
|
5410
5415
|
</select>
|
|
5411
5416
|
<span><span class="rdlabel">Review language</span>
|
|
5412
|
-
<span class="rdhint"
|
|
5417
|
+
<span class="rdhint">Language for the text the Manager writes, such as the review summary (What to check / What changed).</span></span></label>`;
|
|
5413
5418
|
$('rdDefaultWantsPR').addEventListener('change', (e) => { state.rdDraft.defaultWantsPR = e.target.checked; });
|
|
5414
5419
|
$('rdRequireVerify').addEventListener('change', (e) => { state.rdDraft.requireVerifyPass = e.target.checked; });
|
|
5415
5420
|
$('rdDescription').addEventListener('input', (e) => { state.rdDraft.description = e.target.value; });
|
|
@@ -6315,7 +6320,7 @@ async function sendReviewDismiss(text){
|
|
|
6315
6320
|
// these paths, so passing them in the reply text is enough — no API change.
|
|
6316
6321
|
const imgs = reviewAttach.slice();
|
|
6317
6322
|
const fullText = imgs.length
|
|
6318
|
-
? `${text}${text ? '\n\n' : ''}
|
|
6323
|
+
? `${text}${text ? '\n\n' : ''}Attached images (the worker can open these paths with Read): ${imgs.map((a) => a.path).join(', ')}`
|
|
6319
6324
|
: text;
|
|
6320
6325
|
try {
|
|
6321
6326
|
const body = await replyToGoal(goal.id, fullText, '#reviewFixInput');
|
|
@@ -6431,6 +6436,38 @@ function prefersJapaneseText(...values){
|
|
|
6431
6436
|
return values.some((v) => /[\u3040-\u30ff\u3400-\u9fff]/.test(String(v || '')));
|
|
6432
6437
|
}
|
|
6433
6438
|
function summarizeAttention(goal, tasks){
|
|
6439
|
+
// A pending question / approval request is the one case where we already HAVE the
|
|
6440
|
+
// worker's own words (engine writes goal.question = {kind,text,options} and
|
|
6441
|
+
// goal.approvalRequest). Show them verbatim. The board's question card already does
|
|
6442
|
+
// this (.qq at the question-card render), and the two paths must not disagree about
|
|
6443
|
+
// the same field — the template built below by pattern-matching the log is a last
|
|
6444
|
+
// resort for goals that stopped without asking anything (PRD §0.1.1-5).
|
|
6445
|
+
if (goal.status === 'needsInput' && goal.question?.text) {
|
|
6446
|
+
const opts = (goal.question.options || []).map((o) => String(o).trim()).filter(Boolean);
|
|
6447
|
+
const jq = prefersJapaneseText(goal.question.text, goal.text);
|
|
6448
|
+
return {
|
|
6449
|
+
label: jq ? '質問' : 'Question',
|
|
6450
|
+
happened: String(goal.question.text).trim(),
|
|
6451
|
+
decision: opts.length
|
|
6452
|
+
? (jq ? `次のどれかで答えてください: ${opts.join(' / ')}` : `Answer with one of: ${opts.join(' / ')}`)
|
|
6453
|
+
: (jq ? '下の入力欄に答えを書いて送ってください。' : 'Type your answer in the box below and send it.'),
|
|
6454
|
+
};
|
|
6455
|
+
}
|
|
6456
|
+
if (goal.status === 'needsApproval') {
|
|
6457
|
+
const a = goal.approvalRequest || {};
|
|
6458
|
+
const ask = String(a.question || a.action || '').trim();
|
|
6459
|
+
if (ask) {
|
|
6460
|
+
const scope = (a.scope || []).map((s) => String(s).trim()).filter(Boolean);
|
|
6461
|
+
const jq = prefersJapaneseText(ask, goal.text);
|
|
6462
|
+
return {
|
|
6463
|
+
label: jq ? '許可の依頼' : 'Approval requested',
|
|
6464
|
+
happened: scope.length ? `${ask}\n\n${jq ? '対象' : 'Scope'}: ${scope.join(' · ')}` : ask,
|
|
6465
|
+
decision: jq
|
|
6466
|
+
? 'Proceed で許可、Cancel で断ります。範囲を変えたい場合は Edit scope。'
|
|
6467
|
+
: 'Proceed to allow it, Cancel to refuse. Use Edit scope to narrow what it may touch.',
|
|
6468
|
+
};
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6434
6471
|
const fm = goal.failureMemory?.[goal.failureMemory.length - 1];
|
|
6435
6472
|
if (fm?.whatHappened || fm?.yourCall) {
|
|
6436
6473
|
return {
|
|
@@ -6483,14 +6520,14 @@ function summarizeAttention(goal, tasks){
|
|
|
6483
6520
|
}
|
|
6484
6521
|
return { happened: happened.replace(/\.+$/, '.'), decision };
|
|
6485
6522
|
}
|
|
6486
|
-
function renderGoalBrief({ goal, tasks, isReview, isAttention }){
|
|
6523
|
+
function renderGoalBrief({ goal, tasks, isReview, isAttention, isAsking }){
|
|
6487
6524
|
const brief = $('gdBrief');
|
|
6488
|
-
if (!isReview && !isAttention) { brief.classList.remove('show'); brief.innerHTML = ''; return; }
|
|
6525
|
+
if (!isReview && !isAttention && !isAsking) { brief.classList.remove('show'); brief.innerHTML = ''; return; }
|
|
6489
6526
|
let rows;
|
|
6490
|
-
if (isAttention) {
|
|
6527
|
+
if (isAttention || isAsking) {
|
|
6491
6528
|
const s = summarizeAttention(goal, tasks);
|
|
6492
6529
|
rows = [
|
|
6493
|
-
['What happened', s.happened],
|
|
6530
|
+
[s.label || 'What happened', s.happened],
|
|
6494
6531
|
['Your call', s.decision],
|
|
6495
6532
|
];
|
|
6496
6533
|
} else {
|
|
@@ -6513,9 +6550,17 @@ function renderGoalDetail(){
|
|
|
6513
6550
|
const isReview = goal.status === 'review';
|
|
6514
6551
|
const isAttention = ['blocked', 'partial', 'failed', 'interrupted'].includes(goal.status)
|
|
6515
6552
|
|| tasks.some((t) => ['failed', 'interrupted'].includes(t.status));
|
|
6516
|
-
|
|
6553
|
+
// A goal waiting on an answer/approval is not "stuck" — it is asking. It used to
|
|
6554
|
+
// fall through both flags, so the brief was hidden entirely and the panel showed the
|
|
6555
|
+
// original request with no sign of what the worker actually asked (Masa 2026-07-30).
|
|
6556
|
+
// Kept separate from isAttention so the Discard button's condition does not change.
|
|
6557
|
+
const isAsking = ['needsInput', 'needsApproval'].includes(goal.status);
|
|
6558
|
+
$('gdTitle').textContent = isReview ? 'Review'
|
|
6559
|
+
: goal.status === 'needsInput' ? 'Question'
|
|
6560
|
+
: goal.status === 'needsApproval' ? 'Approval'
|
|
6561
|
+
: 'Attention';
|
|
6517
6562
|
$('gdRequirement').textContent = goal.text ?? '';
|
|
6518
|
-
renderGoalBrief({ goal, tasks, isReview, isAttention });
|
|
6563
|
+
renderGoalBrief({ goal, tasks, isReview, isAttention, isAsking });
|
|
6519
6564
|
// review-only items carry no worker tasks — show the author's note (or a
|
|
6520
6565
|
// plain hint) instead of "No worker reports yet.", and any attached images
|
|
6521
6566
|
// as the thing to look at.
|
|
@@ -6544,10 +6589,22 @@ function renderGoalDetail(){
|
|
|
6544
6589
|
$('gdLog').innerHTML = logLines.length
|
|
6545
6590
|
? `<div class="gdlog">${logLines.map((s) => `<div class="gdlogline">${esc(s)}</div>`).join('')}</div>`
|
|
6546
6591
|
: '<div class="gdempty">No log captured yet.</div>';
|
|
6592
|
+
// A goal that is only ASKING has not produced anything yet, so "No worker reports
|
|
6593
|
+
// yet." / "No proof available yet." / "No log captured yet." are three empty boxes
|
|
6594
|
+
// that push the actual question down and out (Masa 2026-07-30: "高さが少なくてみづらい").
|
|
6595
|
+
// §0.1.1-5: do not show the container for something that does not exist. Scoped to the
|
|
6596
|
+
// asking states so Review — where "no proof" is itself a finding — is unchanged.
|
|
6597
|
+
const hideIfEmpty = isAsking && !tasks.length;
|
|
6598
|
+
for (const [id, empty] of [['gdWhatWasDone', !tasks.length && !goal.note], ['gdProof', !proofTasks.length && !reviewImgs.length], ['gdLog', !logLines.length]]) {
|
|
6599
|
+
const sec = $(id).closest('.gdsec');
|
|
6600
|
+
if (sec) sec.style.display = hideIfEmpty && empty ? 'none' : '';
|
|
6601
|
+
}
|
|
6547
6602
|
$('gdApprove').style.display = isReview ? '' : 'none';
|
|
6548
6603
|
$('gdDismiss').style.display = isReview ? '' : 'none';
|
|
6549
6604
|
$('gdDiscard').style.display = isAttention ? '' : 'none';
|
|
6550
|
-
$('gdRevisionInput').placeholder = isReview ? 'Write revision instructions…'
|
|
6605
|
+
$('gdRevisionInput').placeholder = isReview ? 'Write revision instructions…'
|
|
6606
|
+
: goal.status === 'needsInput' ? 'Answer the question…'
|
|
6607
|
+
: 'Tell the worker what to try next…';
|
|
6551
6608
|
$('gdSend').title = isReview ? 'Send back' : 'Send';
|
|
6552
6609
|
}
|
|
6553
6610
|
$('gdClose').onclick = closeGoalDetail;
|
|
@@ -7152,7 +7209,7 @@ function renderFsBoard(){
|
|
|
7152
7209
|
// flagship mock; a new goal enters via the composer). Earlier this was To-Do-only, which read
|
|
7153
7210
|
// as "position is off" against the Artifact (Masa 2026-07-18).
|
|
7154
7211
|
const col = (name, stg, count, html, stage) =>
|
|
7155
|
-
`<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume
|
|
7212
|
+
`<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" 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 class="fsbcnt">${count}</span><button class="fsbcadd" data-fsnew="1" title="New goal"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button></div><div class="fsbbody">${html || '<div class="fsbempty">—</div>'}</div><button class="fsbadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button></section>`;
|
|
7156
7213
|
// Depth card (CDO, Masa 2026-07-17): #NN + title, colour on the dot only, and a continuous
|
|
7157
7214
|
// stroke + n/m ONLY while the goal is actually in flight. The column already names the
|
|
7158
7215
|
// status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
|
|
@@ -7360,7 +7417,7 @@ function renderFsReview(){
|
|
|
7360
7417
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
7361
7418
|
const incompleteNote = r.incompleteRequirementCount ? `<span class="rnote">${r.incompleteRequirementCount} requirement${r.incompleteRequirementCount === 1 ? '' : 's'} need review</span>` : '';
|
|
7362
7419
|
const infraNote = r.verificationInfraError?.detail
|
|
7363
|
-
? `<span class="rnote"
|
|
7420
|
+
? `<span class="rnote">Verification system error · <button type="button" class="retrybtn" data-goalreverify="${r.goalIds[0]}">Re-verify</button></span>`
|
|
7364
7421
|
: '';
|
|
7365
7422
|
const conflictNote = g0?.conflictsWith?.length
|
|
7366
7423
|
? `<span class="rstat conflict">Potential conflict with ${esc(g0.conflictsWith.map((id) => `#${id}`).join(', '))}</span>` : '<span class="rstat">In review</span>';
|
|
@@ -7608,7 +7665,7 @@ function renderFsTodo(){
|
|
|
7608
7665
|
}).join('')
|
|
7609
7666
|
+ 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('')
|
|
7610
7667
|
// 一時停止したゴールは Doing に「Paused」1行(アンバー)で留める+Resume。走行/中断行は上と needsRows で抑止済み。
|
|
7611
|
-
+ 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
|
|
7668
|
+
+ 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('');
|
|
7612
7669
|
// NEEDS YOU: stopped/errored work that used to fold into Doing, now under its own caption so
|
|
7613
7670
|
// it no longer reads as "still in flight" (Masa 2026-07-19). Three kinds share it, each
|
|
7614
7671
|
// keeping the affordances it already had — nothing about the failed-task and blocked-goal
|
|
@@ -7691,7 +7748,7 @@ function renderFsTodo(){
|
|
|
7691
7748
|
: `<span class="num fstdlabel">${nextCount} next · ${doingCount} doing${later.length ? ` · ${later.length} later` : ''}</span>`;
|
|
7692
7749
|
el.innerHTML = `<div class="lhd sechd" id="fsTodoHd"><span class="hd fstdlabel">To Do</span>${_todoNum}<span class="fold fstdlabel">▾</span></div>
|
|
7693
7750
|
<div class="tl">
|
|
7694
|
-
${(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
|
|
7751
|
+
${(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}` : ''}
|
|
7695
7752
|
${needsRows ? `<span class="tlcap needs"${(doingRows || paused) ? '' : ' style="padding-top:0"'}>Needs you</span>${needsRows}` : ''}
|
|
7696
7753
|
${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('')}` : ''}
|
|
7697
7754
|
${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
|
|
@@ -7781,8 +7838,8 @@ function renderFsTodo(){
|
|
|
7781
7838
|
for (const b of el.querySelectorAll('[data-fsreply]')) b.onclick = (e) => { e.stopPropagation();
|
|
7782
7839
|
fsInlineInput(b.closest('.trow'), '', 'Reply to this goal…', async (text) => {
|
|
7783
7840
|
let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.fsreply}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text })) }); }
|
|
7784
|
-
catch { showErr('
|
|
7785
|
-
if (!r.ok) showErr('
|
|
7841
|
+
catch { showErr("This goal hasn't started — use Edit instead."); return; }
|
|
7842
|
+
if (!r.ok) showErr("This goal hasn't started — use Edit instead."); await refresh();
|
|
7786
7843
|
}); };
|
|
7787
7844
|
for (const b of el.querySelectorAll('[data-fsedit]')) b.onclick = (e) => { e.stopPropagation();
|
|
7788
7845
|
const g = state.goals.find((x) => x.id === Number(b.dataset.fsedit));
|
|
@@ -9340,7 +9397,7 @@ async function send(){
|
|
|
9340
9397
|
if (bind) {
|
|
9341
9398
|
const batt = state.attach.slice();
|
|
9342
9399
|
const fullText = batt.length
|
|
9343
|
-
? `${text}${text ? '\n\n' : ''}
|
|
9400
|
+
? `${text}${text ? '\n\n' : ''}Attached images (the worker can open these paths with Read): ${batt.map((a) => a.path).join(', ')}`
|
|
9344
9401
|
: text;
|
|
9345
9402
|
// A bound send belongs to this goal's persisted thread. Do not also add it
|
|
9346
9403
|
// to the center conversation feed: that made a task reply appear below the
|
|
@@ -9427,14 +9484,14 @@ async function askQuestion(){
|
|
|
9427
9484
|
if (!q) return;
|
|
9428
9485
|
const btn = $('peraAskBtn'), out = $('peraAnswer');
|
|
9429
9486
|
inp.disabled = true; if (btn) btn.disabled = true;
|
|
9430
|
-
out.hidden = false; out.className = 'peraanswer loading'; out.textContent = '
|
|
9487
|
+
out.hidden = false; out.className = 'peraanswer loading'; out.textContent = 'Thinking…';
|
|
9431
9488
|
try {
|
|
9432
9489
|
const r = await fetch(withKey('/api/ask'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ question: q }) });
|
|
9433
9490
|
const body = await r.json().catch(() => ({}));
|
|
9434
|
-
if (!r.ok) throw new Error(body.error || '
|
|
9435
|
-
out.className = 'peraanswer'; out.textContent = body.answer || '(
|
|
9491
|
+
if (!r.ok) throw new Error(body.error || 'Could not ask.');
|
|
9492
|
+
out.className = 'peraanswer'; out.textContent = body.answer || '(no answer)';
|
|
9436
9493
|
} catch (e) {
|
|
9437
|
-
out.className = 'peraanswer error'; out.textContent = e.message || '
|
|
9494
|
+
out.className = 'peraanswer error'; out.textContent = e.message || 'Could not ask.';
|
|
9438
9495
|
} finally {
|
|
9439
9496
|
inp.disabled = false; if (btn) btn.disabled = false;
|
|
9440
9497
|
}
|
|
@@ -9461,8 +9518,8 @@ function peraThumb(r, cls = ''){
|
|
|
9461
9518
|
}
|
|
9462
9519
|
function peraActs(r){
|
|
9463
9520
|
const ids = r.goalIds.join(',');
|
|
9464
|
-
const okTitle = r.plan ? 'Approve plan — execute' : '
|
|
9465
|
-
return `<span class="pgo"><button class="pbtn ok" data-peraok="${ids}" title="${okTitle}">✓</button><button class="pbtn no" data-perano="${ids}" title="
|
|
9521
|
+
const okTitle = r.plan ? 'Approve plan — execute' : 'Approve';
|
|
9522
|
+
return `<span class="pgo"><button class="pbtn ok" data-peraok="${ids}" title="${okTitle}">✓</button><button class="pbtn no" data-perano="${ids}" title="Send back">✕</button></span>`;
|
|
9466
9523
|
}
|
|
9467
9524
|
// Plan-review body: the proposed plan, shown verbatim in the card so the
|
|
9468
9525
|
// reviewer approves (→ execute) or dismisses (→ revise) with the plan in view.
|
|
@@ -9559,7 +9616,12 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9559
9616
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
9560
9617
|
// settles it (see saSync's "returned to review" reset).
|
|
9561
9618
|
// ============================================================================
|
|
9562
|
-
|
|
9619
|
+
// `draft` holds the half-typed reply per goal. It exists because the text used to live
|
|
9620
|
+
// ONLY in the DOM, so the sole way to keep it was for saSync() to refuse to run at all
|
|
9621
|
+
// while the box had text — which froze the To Do / Doing / Review status updates behind
|
|
9622
|
+
// the dialog too. With the text in SA it survives a rebuild on its own, and saSync no
|
|
9623
|
+
// longer has to stop (2026-07-30).
|
|
9624
|
+
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, msgCounts: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, draft: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
|
|
9563
9625
|
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9564
9626
|
const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
|
|
9565
9627
|
// task-971: 白黒(モノクロ)版のレールを ?railmono=1 で見比べられるようにする。
|
|
@@ -9617,7 +9679,7 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
|
9617
9679
|
const cutoff = outcome ? taskSortTime(outcome) : Infinity;
|
|
9618
9680
|
const prev = runs.find((t) => t.id !== outcome?.id && taskSortTime(t) <= cutoff) || (!outcome ? runs[0] : null);
|
|
9619
9681
|
if (!prev) return null;
|
|
9620
|
-
const note = '
|
|
9682
|
+
const note = 'Inherited previous preview: the latest reply did not declare a new preview.';
|
|
9621
9683
|
return {
|
|
9622
9684
|
taskId: prev.id,
|
|
9623
9685
|
attemptId: taskAttemptId(outcome) || taskAttemptId(prev),
|
|
@@ -9837,7 +9899,10 @@ function saLedgerRows(){
|
|
|
9837
9899
|
// (summarizeAttention), mapped into the review card instead of a bespoke "What happened /
|
|
9838
9900
|
// Your call" layout (Masa 2026-07-24). The Request section already shows the ask, so drop
|
|
9839
9901
|
// the summary's "必要だったこと:/Needed:" lead to avoid repeating it.
|
|
9840
|
-
|
|
9902
|
+
// needsInput/needsApproval are included so this path shows the worker's verbatim
|
|
9903
|
+
// question too — summarizeAttention returns it as-is for those. Both renders of the
|
|
9904
|
+
// same goal.question must say the same thing (PRD §0.1.1-5).
|
|
9905
|
+
if (['blocked', 'partial', 'failed', 'interrupted', 'needsInput', 'needsApproval'].includes(g.status)) {
|
|
9841
9906
|
const s = summarizeAttention(g, ts.filter((t) => t.goalId === g.id));
|
|
9842
9907
|
const why = String(s.happened || '').replace(/^\s*(必要だったこと:|Needed:)[^。.]*[。.]\s*/, '').trim();
|
|
9843
9908
|
const merged = [why, s.decision].filter(Boolean).join('\n\n');
|
|
@@ -9868,7 +9933,7 @@ function saLedgerRows(){
|
|
|
9868
9933
|
// 3-for-2 replacement, an add with no matching del, a rewritten line where almost
|
|
9869
9934
|
// nothing survives — all keep today's behaviour exactly (tint only, no span).
|
|
9870
9935
|
// How many body rows the diff really has, ignoring the display cap. A big change used
|
|
9871
|
-
// to be cut at 80 rows with nothing said (SPEC §7 asks for "…+N 行 · GitHub
|
|
9936
|
+
// to be cut at 80 rows with nothing said (SPEC §7 asks for "…+N 行 · Open on GitHub ↗"
|
|
9872
9937
|
// precisely so a reviewer is never shown a slice that looks like the whole thing).
|
|
9873
9938
|
// Same skip rules as saUnifiedToGh below — headers and @@ markers are not rows.
|
|
9874
9939
|
function saDiffRowCount(diff){
|
|
@@ -10032,7 +10097,7 @@ function saMsgRowsHtml(msgs, expanded){
|
|
|
10032
10097
|
}
|
|
10033
10098
|
|
|
10034
10099
|
// Structured GitHub diff (REVIEW-CARD-SPEC §7): renders {o,n,t,c,h} rows with a
|
|
10035
|
-
// word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub
|
|
10100
|
+
// word-level highlight (h) and, for a huge change, a "…+N 行 · Open on GitHub ↗"
|
|
10036
10101
|
// footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
|
|
10037
10102
|
// ghDiffHtml so the app pixel-matches. `more` = extra unshown line count; `pr` = url.
|
|
10038
10103
|
// Request fold threshold (Masa 2026-07-27): Request is supposed to be the user's short
|
|
@@ -10068,7 +10133,7 @@ function saGhDiffRowsHtml(rows, more, pr, goalId){
|
|
|
10068
10133
|
} else if (l.h) { const h = esc(l.h); code = code.replace(h, `<span class="gh-hi">${h}</span>`); }
|
|
10069
10134
|
return `<div class="ghl ${cls}"><span class="gho">${l.o ?? ''}</span><span class="ghn">${l.n ?? ''}</span><span class="ghm">${mk}</span><span class="ghc">${code}</span></div>`;
|
|
10070
10135
|
}).join('');
|
|
10071
|
-
const foot = more ? `<div class="ghfoot"><span>…+${more}
|
|
10136
|
+
const foot = more ? `<div class="ghfoot"><span>…+${more} more lines</span>${pr ? ` · <a class="ghopen" href="${esc(pr)}" target="_blank" rel="noopener">Open on GitHub ↗</a>` : ''}</div>` : '';
|
|
10072
10137
|
if (goalId != null && rows.length > SA_DIFF_FOLD_OVER) {
|
|
10073
10138
|
const open = SA.diffOpen.has(goalId);
|
|
10074
10139
|
const label = open ? 'Collapse' : `Show all <span class="n">${rows.length}</span> lines`;
|
|
@@ -10381,7 +10446,7 @@ function saItemHtml(it, i){
|
|
|
10381
10446
|
const reportBody = it.report ? `<div class="rreport">${mdLite(it.report, { images: false })}</div>` : '';
|
|
10382
10447
|
const infra = it.verificationInfraError;
|
|
10383
10448
|
const infraBody = infra?.detail
|
|
10384
|
-
? `<div class="rinfra"><p class="rbody">${esc(infra.detail)}</p><button type="button" class="retrybtn" data-saact="reverify:${i}"
|
|
10449
|
+
? `<div class="rinfra"><p class="rbody">${esc(infra.detail)}</p><button type="button" class="retrybtn" data-saact="reverify:${i}">Re-verify</button></div>`
|
|
10385
10450
|
: '';
|
|
10386
10451
|
// ── Screenshots (P2, Masa 2026-07-22): the pictures the AI took of its own result.
|
|
10387
10452
|
// Full width, at their own aspect — a four-variant comparison sheet is unreadable
|
|
@@ -10405,7 +10470,7 @@ function saItemHtml(it, i){
|
|
|
10405
10470
|
if (it.recording) proofInner += saVideoFigHtml(it.recordingUrl);
|
|
10406
10471
|
}
|
|
10407
10472
|
// ── What changed (spec §4/§7): description + files summary (always) + the diff, which
|
|
10408
|
-
// peeks then "…+N 行 · GitHub
|
|
10473
|
+
// peeks then "…+N 行 · Open on GitHub ↗" for a huge change (no GitHub link when there's no
|
|
10409
10474
|
// PR). Structured recorded rows (ghRows) win over a git/client unified diff. Dropped
|
|
10410
10475
|
// entirely for a conversational task. ──
|
|
10411
10476
|
// No prose here (P1, Masa 2026-07-22): reviewSummary.changed was the intake AI writing
|
|
@@ -10547,7 +10612,7 @@ function saItemHtml(it, i){
|
|
|
10547
10612
|
${conversationSec}
|
|
10548
10613
|
${activitySec}
|
|
10549
10614
|
${S('Workflow', saWorkflowHtml(it))}
|
|
10550
|
-
<div class="sa-act">${attn ? '' : `<div class="sa-approve-wrap"><button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button>${delivery}</div><button class="sa-dis" data-saact="dis:${i}">Dismiss</button>`}<div class="sa-replybox"><div class="sa-replyattach${SA.attachments[it.goalId]?.length ? ' has' : ''}" data-saattach="${it.goalId}">${saReplyAttachmentsHtml(it.goalId)}</div><div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" data-goal="${it.goalId}" placeholder="Reply, ask, or paste an image…"><button class="sa-send" data-saact="send:${i}" title="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div><button class="sa-ic" title="Archive" data-saact="arch:${i}">${SA_SVG.arch}</button><button class="sa-ic" title="Revert" data-saact="rev:${i}">${SA_SVG.trash}</button></div>
|
|
10615
|
+
<div class="sa-act">${attn ? '' : `<div class="sa-approve-wrap"><button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button>${delivery}</div><button class="sa-dis" data-saact="dis:${i}">Dismiss</button>`}<div class="sa-replybox"><div class="sa-replyattach${SA.attachments[it.goalId]?.length ? ' has' : ''}" data-saattach="${it.goalId}">${saReplyAttachmentsHtml(it.goalId)}</div><div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" data-goal="${it.goalId}" placeholder="Reply, ask, or paste an image…" value="${esc(SA.draft[it.goalId] ?? '')}"><button class="sa-send" data-saact="send:${i}" title="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div><button class="sa-ic" title="Archive" data-saact="arch:${i}">${SA_SVG.arch}</button><button class="sa-ic" title="Revert" data-saact="rev:${i}">${SA_SVG.trash}</button></div>
|
|
10551
10616
|
</div>`;
|
|
10552
10617
|
}
|
|
10553
10618
|
// Scroll the See-all body so a card's top edge (.rhead) sits at the top of the visible
|
|
@@ -10695,9 +10760,12 @@ function saRender(){
|
|
|
10695
10760
|
}) || cards.find((el) => el.getBoundingClientRect().bottom > bodyRect.top);
|
|
10696
10761
|
if (anchor) { anchorGid = anchor.dataset.gid; anchorOffset = anchor.getBoundingClientRect().top - bodyRect.top; }
|
|
10697
10762
|
}
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10763
|
+
// Remember WHERE the caret was too, not just which box had focus: the text now comes
|
|
10764
|
+
// back from SA.draft, and putting it back without the caret would jump the cursor to
|
|
10765
|
+
// the end mid-word on every SSE tick.
|
|
10766
|
+
const activeChat = document.activeElement?.matches?.('#seeall .sa-chat') ? document.activeElement : null;
|
|
10767
|
+
const focusedReplyGoal = activeChat?.dataset.goal ?? null;
|
|
10768
|
+
const focusedCaret = activeChat ? { start: activeChat.selectionStart, end: activeChat.selectionEnd } : null;
|
|
10701
10769
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
10702
10770
|
const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
|
|
10703
10771
|
// Solo mode = a single "Needs you" goal opened as this card. It is not a review batch, so the
|
|
@@ -10828,10 +10896,24 @@ function saRender(){
|
|
|
10828
10896
|
if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
|
|
10829
10897
|
});
|
|
10830
10898
|
}
|
|
10831
|
-
//
|
|
10832
|
-
//
|
|
10833
|
-
//
|
|
10834
|
-
|
|
10899
|
+
// Every reply box carries its draft back from SA.draft via the value attribute, so a
|
|
10900
|
+
// rebuild no longer loses what was typed. Keep SA.draft current as the person types —
|
|
10901
|
+
// this is what lets saSync() run during typing instead of freezing.
|
|
10902
|
+
for (const inp of S.querySelectorAll('.sa-chat[data-goal]')) {
|
|
10903
|
+
inp.addEventListener('input', () => {
|
|
10904
|
+
const gid = inp.dataset.goal;
|
|
10905
|
+
if (inp.value) SA.draft[gid] = inp.value; else delete SA.draft[gid];
|
|
10906
|
+
});
|
|
10907
|
+
}
|
|
10908
|
+
// A relevant status change may legitimately rebuild the card. Put focus AND the caret
|
|
10909
|
+
// back where they were.
|
|
10910
|
+
if (focusedReplyGoal != null) {
|
|
10911
|
+
const inp = S.querySelector(`.sa-chat[data-goal="${focusedReplyGoal}"]`);
|
|
10912
|
+
if (inp) {
|
|
10913
|
+
inp.focus({ preventScroll: true });
|
|
10914
|
+
if (focusedCaret) { try { inp.setSelectionRange(focusedCaret.start, focusedCaret.end); } catch { /* not selectable */ } }
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10835
10917
|
}
|
|
10836
10918
|
// keep the open Ledger in sync with server state (SSE/refresh → render() → here).
|
|
10837
10919
|
// An item whose goal has RETURNED to 'review' (retest ×3 green, rework done,
|
|
@@ -10839,10 +10921,11 @@ function saRender(){
|
|
|
10839
10921
|
// blocks the rebuild (same "vanishes mid-type" guard as renderFsLane).
|
|
10840
10922
|
function saSync(){
|
|
10841
10923
|
if (!SA.open) return;
|
|
10842
|
-
|
|
10843
|
-
//
|
|
10844
|
-
//
|
|
10845
|
-
|
|
10924
|
+
// No typing guard here any more. It used to `return` whenever the focused input had
|
|
10925
|
+
// text, which protected the draft by freezing EVERYTHING — status changes behind the
|
|
10926
|
+
// dialog included, so a card could sit on "Working on it" for as long as someone was
|
|
10927
|
+
// composing a reply. The draft now lives in SA.draft and is restored with its caret by
|
|
10928
|
+
// saRender, so the sync is free to run (2026-07-30).
|
|
10846
10929
|
// Working cards are intentionally absent from the Review ledger rows after the
|
|
10847
10930
|
// server moves them to To Do / Doing. Keep their held dialog snapshot wired to
|
|
10848
10931
|
// live goal/task state so the status changes in place instead of disappearing.
|
|
@@ -10878,11 +10961,15 @@ async function saFetchDiffs(){
|
|
|
10878
10961
|
if (SA.open) saSync();
|
|
10879
10962
|
}
|
|
10880
10963
|
function saOpen(focusGoalId, solo = false){
|
|
10964
|
+
const wasOpen = SA.open;
|
|
10881
10965
|
SA.open = true;
|
|
10882
10966
|
SA.lastMarkup = null; // a newly opened/focused dialog always gets a clean first paint
|
|
10883
10967
|
SA.solo = solo ? focusGoalId : null; // solo = a single "Needs you" goal opened as one card
|
|
10884
10968
|
SA.items = saLedgerRows().map((r) => ({ ...r, st: 0 }));
|
|
10885
|
-
|
|
10969
|
+
// Only a FRESH open starts with everything folded. saOpen also runs when the person taps
|
|
10970
|
+
// a different card while the dialog is already up, and clearing the set there closed the
|
|
10971
|
+
// Activity / Conversation folds they had just opened on the other cards.
|
|
10972
|
+
if (!wasOpen) SA.detOpen = new Set();
|
|
10886
10973
|
// H14: focus a specific goal's card (single-review tap). Map the tapped goal
|
|
10887
10974
|
// to the card that CONTAINS it (goals can group in the digest), then let
|
|
10888
10975
|
// saRender center it + flash a blue ring. Real data keys by goal id.
|
|
@@ -11015,8 +11102,9 @@ async function saSendReply(it, i, text){
|
|
|
11015
11102
|
text = displayText;
|
|
11016
11103
|
await saEnsureThread(it); // fold in any existing history BEFORE this reply (so it isn't hidden)
|
|
11017
11104
|
const seq = saNextSeq(); // reserve send order NOW, so a late-resolving route keeps its place
|
|
11018
|
-
// Clear the box in place — keep it FOCUSED, not blurred
|
|
11019
|
-
//
|
|
11105
|
+
// Clear the box in place — keep it FOCUSED, not blurred. The draft must be dropped from
|
|
11106
|
+
// SA.draft as well, or the next rebuild would re-populate the box from the sent text.
|
|
11107
|
+
delete SA.draft[goalId];
|
|
11020
11108
|
document.querySelectorAll(`#seeall .sa-item[data-gid="${goalId}"] .sa-chat`).forEach((inp) => { inp.value = ''; });
|
|
11021
11109
|
delete SA.attachments[goalId]; saPaintAttachments(goalId);
|
|
11022
11110
|
// Append the line to the card's own thread immediately — this is what the person sees, and
|
package/engine/lib.mjs
CHANGED
|
@@ -1396,6 +1396,17 @@ export function shouldAutoScroll(scrollTop, scrollHeight, clientHeight, threshol
|
|
|
1396
1396
|
return scrollHeight - clientHeight - scrollTop <= threshold;
|
|
1397
1397
|
}
|
|
1398
1398
|
|
|
1399
|
+
// Should an Enter keydown submit an input (send chat / save / answer)? Only when
|
|
1400
|
+
// the user is NOT mid-IME-composition. Pressing Enter to CONFIRM a Japanese
|
|
1401
|
+
// conversion candidate must commit the text, not send the message. Browsers flag
|
|
1402
|
+
// this via `isComposing` (and legacy `keyCode === 229`); every submit-on-Enter
|
|
1403
|
+
// handler in app/index.html shares this guard so IME confirm never mis-sends.
|
|
1404
|
+
export function shouldSubmitOnEnter(event = {}) {
|
|
1405
|
+
if (event.key !== 'Enter') return false;
|
|
1406
|
+
if (event.isComposing || event.keyCode === 229) return false;
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1399
1410
|
// Decide the scrollTop to apply *after* a re-render replaces #stream's HTML
|
|
1400
1411
|
// (task 93). `pre` is the element's geometry captured BEFORE the innerHTML
|
|
1401
1412
|
// swap; `newScrollHeight` is its height AFTER. If the user was near the bottom
|
package/engine/server.mjs
CHANGED
|
@@ -3032,7 +3032,6 @@ async function runTask(task) {
|
|
|
3032
3032
|
task.changedFiles = afterR.filter((l) => !before.includes(l)).map((l) => l.slice(3));
|
|
3033
3033
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
3034
3034
|
notePreviewScope(task);
|
|
3035
|
-
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
3036
3035
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this follow-up.`);
|
|
3037
3036
|
// An explicit worker receipt takes precedence over Galda's legacy prose
|
|
3038
3037
|
// guard. The agent owns this decision; Manager only transports it.
|
|
@@ -3258,7 +3257,6 @@ async function runTask(task) {
|
|
|
3258
3257
|
task.changedFiles = after.filter((l) => !before.includes(l)).map((l) => l.slice(3));
|
|
3259
3258
|
task.unmappedChangedFiles = unmappedChangedFiles(task.changedFiles, task.requirementEvidence);
|
|
3260
3259
|
notePreviewScope(task);
|
|
3261
|
-
task.requirementVerification = { proof: task.proof ?? null, run: task.run ?? null, changedFiles: task.changedFiles };
|
|
3262
3260
|
sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this task.`);
|
|
3263
3261
|
// An explicit worker receipt takes precedence over Galda's legacy prose
|
|
3264
3262
|
// guard. The agent owns this decision; Manager only transports it.
|
|
@@ -3679,6 +3677,12 @@ function goalChangedFileUnion(goalId) {
|
|
|
3679
3677
|
}
|
|
3680
3678
|
function computeWorktreeChangeLocations(dir) {
|
|
3681
3679
|
try {
|
|
3680
|
+
// `git diff` alone never reports untracked files, so goals that only add
|
|
3681
|
+
// new files would get changeLocations=[] and could never match by
|
|
3682
|
+
// position for auto-fold. `add -N` (intent-to-add) registers the paths
|
|
3683
|
+
// without staging their content, which is enough for them to show up as
|
|
3684
|
+
// normal diff hunks.
|
|
3685
|
+
gitOut(dir, ['add', '-N', '-A']);
|
|
3682
3686
|
return parseGitUnifiedDiffLocations(gitOut(dir, ['diff', '--unified=0']) ?? '');
|
|
3683
3687
|
} catch {
|
|
3684
3688
|
return [];
|
package/package.json
CHANGED