@galda/cli 0.10.84 → 0.10.85
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 +16 -0
- package/app/index.html +277 -89
- package/app/theme.css +6 -6
- package/engine/lib.mjs +116 -4
- package/engine/mcp.mjs +33 -1
- package/engine/server.mjs +127 -12
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Galda</title>
|
|
7
|
+
<script>
|
|
8
|
+
// app/index.html is a server-backed SPA, not a standalone artifact. Rendering the
|
|
9
|
+
// product shell from file:// makes missing theme/API/SSE data look like an old UI.
|
|
10
|
+
if (location.protocol === 'file:') {
|
|
11
|
+
document.documentElement.classList.add('galda-invalid-file-preview');
|
|
12
|
+
document.title = 'Galda — invalid preview URL';
|
|
13
|
+
}
|
|
14
|
+
</script>
|
|
15
|
+
<style>
|
|
16
|
+
#galdaInvalidFilePreview{display:none}
|
|
17
|
+
html.galda-invalid-file-preview body{margin:0;background:#0a0a0b;color:#e9e7e2;font-family:system-ui,sans-serif}
|
|
18
|
+
html.galda-invalid-file-preview body>*{display:none!important}
|
|
19
|
+
html.galda-invalid-file-preview body>#galdaInvalidFilePreview{display:grid!important;min-height:100vh;place-items:center;padding:32px}
|
|
20
|
+
.galda-invalid-file-card{max-width:620px;border:1px solid rgba(255,255,255,.12);border-radius:16px;padding:28px;background:#101014}
|
|
21
|
+
.galda-invalid-file-card h1{margin:0 0 12px;font-size:22px;letter-spacing:-.02em}
|
|
22
|
+
.galda-invalid-file-card p{margin:8px 0;color:#a3a097;line-height:1.6}
|
|
23
|
+
.galda-invalid-file-card code{color:#e9e7e2;font-family:ui-monospace,monospace}
|
|
24
|
+
</style>
|
|
7
25
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%233B6BFF'/%3E%3Cstop offset='1' stop-color='%230E2AA8'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='16' cy='16' r='11' fill='none' stroke='url(%23g)' stroke-width='5'/%3E%3C/svg%3E">
|
|
8
26
|
<!-- brand fonts FINAL (v45 §1): the 3 adopted faces self-host in /fonts.css — the old
|
|
9
27
|
Google Fonts Inter/JetBrains request was removed (offline-safe, no stray families) -->
|
|
@@ -463,6 +481,11 @@
|
|
|
463
481
|
.qansrow{display:flex;gap:7px;align-items:center;margin-top:8px}
|
|
464
482
|
.qansrow input{flex:1;background:var(--glass);border:1px solid var(--hair2);border-radius:8px;color:var(--ink);font-size:12.5px;padding:6px 10px;outline:none}
|
|
465
483
|
.qsub{font-size:10.5px;color:var(--ink3);margin-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
484
|
+
.approvalflow .qscope{margin-top:7px;font-size:11px;color:var(--ink2);line-height:1.45;white-space:pre-wrap}
|
|
485
|
+
.approvalflow .qactions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
|
486
|
+
.approvalflow .qaction{border:1px solid rgba(var(--blue-rgb),.5);border-radius:8px;padding:6px 12px;background:transparent;color:var(--blue);font-size:11.5px;cursor:pointer}
|
|
487
|
+
.approvalflow .qaction.primary{background:var(--blue);color:white;border-color:var(--blue)}
|
|
488
|
+
.approvalflow .qaction.cancel{color:var(--ink3);border-color:var(--hair2)}
|
|
466
489
|
/* The one line under a reply box: what the AI asked back, or the answer it gave.
|
|
467
490
|
Same part as .qsub, but an answer is a sentence — it must wrap, not ellipsis. */
|
|
468
491
|
/* The reply box's own short thread (CDO HANDOFF-REPLY-CONVERSATION §2/§5). It uses
|
|
@@ -1368,13 +1391,19 @@
|
|
|
1368
1391
|
#fsRoot .fsproj.on{background:var(--hover);color:var(--ink)}
|
|
1369
1392
|
#fsRoot .fsproj i{width:4px;height:4px;border-radius:50%;background:var(--ink3);flex:0 0 4px;position:relative;top:1.5px} /* top:1.5px = optical nudge so the dot/running-ring centre sits on the name's x-height, not the line-box centre (Masa 2026-07-24, rail=D). Same shift for idle dot + running ring keeps the column aligned. */
|
|
1370
1393
|
#fsRoot .fsproj.on i{background:var(--green)}
|
|
1394
|
+
/* Review-blue comparison B: Aurora Blue #5CC4FF at 78%. This is a dedicated
|
|
1395
|
+
Review token; the app's ordinary blue controls must not inherit it. */
|
|
1396
|
+
#fsRoot .fsproj.rev i{background:rgba(var(--review-rgb),.78);box-shadow:0 0 3px rgba(var(--review-rgb),.24)}
|
|
1371
1397
|
/* running project = solid saturated green dot with a breathing glow — never dims to
|
|
1372
1398
|
grey, independent of selection (HANDOFF-v45 §11, flagship .proj.run port) */
|
|
1373
1399
|
/* running project = a spinning broken green ring (matches the Doing marker · flagship #316).
|
|
1374
1400
|
Was a pulsing dot (fsrundot). Centre WITHOUT transform (spin's rotate would override a centring
|
|
1375
1401
|
translate → drift bottom-right) via top/left:50% + negative-half margin; concentric with the 4px dot. */
|
|
1376
1402
|
#fsRoot .fsproj.run i{background:none;box-shadow:none;position:relative}
|
|
1377
|
-
|
|
1403
|
+
/* Review-blue comparison baseline: the Doing cue is a 7px / 1.3px broken
|
|
1404
|
+
ring, while idle and Review remain 4px dots. */
|
|
1405
|
+
#fsRoot .fsproj.run i::after{content:"";position:absolute;top:50%;left:50%;width:7px;height:7px;margin:-3.5px 0 0 -3.5px;box-sizing:border-box;border-radius:50%;border:1.3px solid var(--green);border-top-color:transparent;animation:spin 1.8s linear infinite}
|
|
1406
|
+
#fsRoot .fsproj.rev i{background:rgba(var(--review-rgb),.78);box-shadow:0 0 3px rgba(var(--review-rgb),.24)}
|
|
1378
1407
|
@keyframes fsrundot{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 50%,transparent)}50%{box-shadow:0 0 5px 1.5px color-mix(in srgb,var(--green) 35%,transparent)}}
|
|
1379
1408
|
@media (prefers-reduced-motion:reduce){#fsRoot .fsproj.run i::after{animation:none}}
|
|
1380
1409
|
#fsRoot .fsproj .pname{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -1719,10 +1748,9 @@
|
|
|
1719
1748
|
.lbc-n{font:600 10px/1 var(--mono);color:var(--ink3);font-variant-numeric:tabular-nums;letter-spacing:.02em}
|
|
1720
1749
|
.lbc-dot{width:6px;height:6px;border-radius:50%;flex:0 0 6px;background:var(--ink3)}
|
|
1721
1750
|
.lbc-dot.run{background:var(--green);animation:lbcpulse 2.4s ease-in-out infinite}
|
|
1722
|
-
/* Status dot palette
|
|
1723
|
-
Doing = green
|
|
1724
|
-
|
|
1725
|
-
.lbc-dot.rev{background:#8390f2}
|
|
1751
|
+
/* Status dot palette: Needs you = amber, Review = dedicated Aurora Blue,
|
|
1752
|
+
Doing = green, Done/To Do/Later = grey, Failed = red. */
|
|
1753
|
+
.lbc-dot.rev{background:rgba(var(--review-rgb),.78)}
|
|
1726
1754
|
.lbc-dot.bad{background:var(--danger)}
|
|
1727
1755
|
.lbc-dot.intr{background:#ffca16}
|
|
1728
1756
|
.lbc-dot.ok{background:var(--ink3);opacity:.6}
|
|
@@ -1918,7 +1946,7 @@
|
|
|
1918
1946
|
.lg-hd .no{flex:0 0 auto;font:600 11px/1 var(--mono);color:var(--ink3);font-variant-numeric:tabular-nums}
|
|
1919
1947
|
.lg-hd .t{flex:1;font:600 15px/1.3 var(--ui);letter-spacing:-.014em}
|
|
1920
1948
|
.lg-state{flex:0 0 auto;font:600 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase} /* state = colour text only (§5.5 no badge) */
|
|
1921
|
-
.lg-state.review{color:var(--
|
|
1949
|
+
.lg-state.review{color:var(--review)}
|
|
1922
1950
|
/* meta row: PR link / token savings (real tap-to-open toggle) / Run locally — a
|
|
1923
1951
|
compact row directly under the title (H23 §1, pattern 1 won over a plain text
|
|
1924
1952
|
line and title-row-right-aligned chips). */
|
|
@@ -2428,6 +2456,7 @@
|
|
|
2428
2456
|
specificity and greys the running dot out on the Gradient wallpaper rail — this
|
|
2429
2457
|
exception restores solid green so "running" never dims to grey. */
|
|
2430
2458
|
body[data-theme="gradient"] #fsRoot .fsproj.run i{background:none}
|
|
2459
|
+
body[data-theme="gradient"] #fsRoot .fsproj.rev i{background:rgba(var(--review-rgb),.78)}
|
|
2431
2460
|
body[data-theme="gradient"] #fsRoot .fsproj .num,body[data-theme="gradient"] #fsRoot .fsproj .pcount{color:rgba(20,23,31,.6)}
|
|
2432
2461
|
body[data-theme="gradient"] #fsRoot .fsproj:not(.on):hover{background:rgba(255,255,255,.34)}
|
|
2433
2462
|
body[data-theme="gradient"] #fsRoot .fsproj.on{background:rgba(255,255,255,.5);color:var(--ink)}
|
|
@@ -2565,6 +2594,7 @@
|
|
|
2565
2594
|
body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj i{background:rgba(255,255,255,.6)}
|
|
2566
2595
|
/* same GOTCHA exception as the light Gradient rail, dark variant (HANDOFF-v45 §11) */
|
|
2567
2596
|
body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj.run i{background:none}
|
|
2597
|
+
body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj.rev i{background:rgba(var(--review-rgb),.78)}
|
|
2568
2598
|
body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj .num,body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj .pcount{color:rgba(255,255,255,.6)}
|
|
2569
2599
|
body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj.on{background:rgba(255,255,255,.15);color:#fff}
|
|
2570
2600
|
body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj:not(.on):hover{background:rgba(255,255,255,.1)}
|
|
@@ -2928,6 +2958,13 @@
|
|
|
2928
2958
|
</style>
|
|
2929
2959
|
</head>
|
|
2930
2960
|
<body>
|
|
2961
|
+
<main id="galdaInvalidFilePreview" aria-live="assertive">
|
|
2962
|
+
<section class="galda-invalid-file-card">
|
|
2963
|
+
<h1>This is not the running Galda app</h1>
|
|
2964
|
+
<p><code>app/index.html</code> was opened with <code>file://</code>. This source file cannot load the theme, live state, Review data, or realtime updates.</p>
|
|
2965
|
+
<p>Start Galda from the target worktree and open the authenticated <code>http://localhost:<port>/?key=<key></code> URL printed in the terminal.</p>
|
|
2966
|
+
</section>
|
|
2967
|
+
</main>
|
|
2931
2968
|
<div id="themebg"></div>
|
|
2932
2969
|
<input type="file" id="bg3File" accept="image/*" style="display:none"><!-- Banff BACKDROP free upload (v3 §6.5) -->
|
|
2933
2970
|
<canvas id="glassgl"></canvas>
|
|
@@ -3371,7 +3408,7 @@ const savedTheme = (() => {
|
|
|
3371
3408
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3372
3409
|
return v;
|
|
3373
3410
|
})();
|
|
3374
|
-
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,
|
|
3411
|
+
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, composerDetachedGoal: 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,
|
|
3375
3412
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3376
3413
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3377
3414
|
// Scoped per project (msgByProject) so a skill/mode picked in one project can't leak
|
|
@@ -3414,7 +3451,7 @@ function restoreFocusedAnswerInput(container, attr, saved){
|
|
|
3414
3451
|
}
|
|
3415
3452
|
|
|
3416
3453
|
const STATUS_LABEL = {
|
|
3417
|
-
sending: 'Sending…', stacked: 'Queued', planning: 'Planning…', queued: 'To do', running: 'Working…', needsInput: 'Needs input',
|
|
3454
|
+
sending: 'Sending…', stacked: 'Queued', planning: 'Planning…', queued: 'To do', running: 'Working…', needsInput: 'Needs input', needsApproval: 'Needs you',
|
|
3418
3455
|
review: 'In review', done: 'Done', failed: 'Failed', partial: 'Partly failed', interrupted: 'Interrupted', skipped: 'Skipped',
|
|
3419
3456
|
rejected: 'Dismissed',
|
|
3420
3457
|
};
|
|
@@ -3437,7 +3474,7 @@ const GOAL_CHIP = (s, columns, startedAt) => s === 'stacked' || s === 'sending'
|
|
|
3437
3474
|
s === 'reverted' ? ['REVERTED', ''] :
|
|
3438
3475
|
s === 'rejected' ? ['DISMISSED', 'rejected'] :
|
|
3439
3476
|
s === 'folded' ? ['FOLDED', ''] :
|
|
3440
|
-
s === 'blocked' ? ['BLOCKED', 'blocked'] : s === 'needsInput' ? ['NEEDS INPUT', 'blocked'] : ['FAIL', ''];
|
|
3477
|
+
s === 'blocked' ? ['BLOCKED', 'blocked'] : s === 'needsInput' ? ['NEEDS INPUT', 'blocked'] : s === 'needsApproval' ? ['NEEDS YOU', 'blocked'] : ['FAIL', ''];
|
|
3441
3478
|
const FINISHED = (s) => ['done', 'partial', 'failed', 'interrupted'].includes(s);
|
|
3442
3479
|
const ICONS = {
|
|
3443
3480
|
pencil: '<svg viewBox="0 0 24 24"><path d="M4 20l1-4L16.5 4.5a2.1 2.1 0 013 3L8 19l-4 1z"/></svg>',
|
|
@@ -3725,6 +3762,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3725
3762
|
verificationInfraError: g.verificationInfraError ?? null,
|
|
3726
3763
|
proof: proofTask?.proof ?? null,
|
|
3727
3764
|
proofTaskId: proofTask?.id ?? null,
|
|
3765
|
+
incompleteRequirementCount: goalTasks.reduce((n, t) => n + (t.requirementEvidence?.requirements ?? []).filter((v) => ['partial', 'not-addressed'].includes(v.status)).length, 0),
|
|
3728
3766
|
plan,
|
|
3729
3767
|
planText: plan ? (g.planText ?? '') : null,
|
|
3730
3768
|
};
|
|
@@ -3735,6 +3773,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3735
3773
|
if (existing.proof == null) { existing.proof = row.proof; existing.proofTaskId = row.proofTaskId; }
|
|
3736
3774
|
if (existing.testResult == null) existing.testResult = row.testResult;
|
|
3737
3775
|
if (existing.verificationInfraError == null) existing.verificationInfraError = row.verificationInfraError;
|
|
3776
|
+
existing.incompleteRequirementCount += row.incompleteRequirementCount;
|
|
3738
3777
|
continue;
|
|
3739
3778
|
}
|
|
3740
3779
|
if (row.pr) byPr.set(row.pr, row);
|
|
@@ -4320,8 +4359,9 @@ function mergeGoalTimeline(goals, externalActivity, projectId){
|
|
|
4320
4359
|
// レビュー画面(checklist/detail overlay)からチャットへ戻った時、そのゴールの
|
|
4321
4360
|
// 末尾がすぐ見える位置へスクロールする(task 86)。ゴールが完了していて結論サマリ
|
|
4322
4361
|
// (.gsummary)が出ていればそこを、まだなければゴールメッセージ全体の末尾を狙う。
|
|
4323
|
-
function goToGoalInChat(goalId){
|
|
4362
|
+
function goToGoalInChat(goalId, bindComposer = true){
|
|
4324
4363
|
state.selectedGoal = goalId;
|
|
4364
|
+
if (bindComposer) state.composerDetachedGoal = null;
|
|
4325
4365
|
renderReplyBind(); // Feature A: the composer's "Replying to #NNN" chip follows the focused goal — renderStream() alone skips it (only render() calls it)
|
|
4326
4366
|
renderStream();
|
|
4327
4367
|
const goalEl = document.querySelector(`#stream [data-goalmsg="${goalId}"]`);
|
|
@@ -4663,8 +4703,17 @@ function renderTasks(){
|
|
|
4663
4703
|
<div class="qansrow"><input type="text" data-qansinput="${g.id}" placeholder="Type your answer…"><button type="button" class="qbtn ok" data-qanssend="${g.id}" aria-label="Send">↑</button></div>
|
|
4664
4704
|
<div class="qsub">${esc((g.text ?? '').replace(/\s+/g, ' ').slice(0, 54))}</div></div>`;
|
|
4665
4705
|
}).join('');
|
|
4666
|
-
const
|
|
4667
|
-
|
|
4706
|
+
const approvalCards = pgoals.filter((g) => g.status === 'needsApproval' && g.approvalRequest).map((g) => {
|
|
4707
|
+
const a = g.approvalRequest;
|
|
4708
|
+
const scope = (a.scope || []).map((x) => `• ${esc(x)}`).join('\n');
|
|
4709
|
+
return `<div class="qflow approvalflow" data-approvalgoal="${g.id}"><div class="qh">◆ Needs your approval</div><div class="qq">${esc(a.question || g.text)}</div>
|
|
4710
|
+
${scope ? `<div class="qscope">${scope}</div>` : ''}
|
|
4711
|
+
<div class="qsub">${esc(a.action || 'continue')}</div>
|
|
4712
|
+
<div class="qactions"><button type="button" class="qaction primary" data-approval="proceed" data-goal="${g.id}">Proceed</button><button type="button" class="qaction" data-approvaledit="${g.id}">Edit scope</button><button type="button" class="qaction cancel" data-approval="cancel" data-goal="${g.id}">Cancel</button></div>
|
|
4713
|
+
<div class="qansrow" data-approvaleditrow="${g.id}" hidden><input type="text" data-approvalinput="${g.id}" placeholder="Describe the allowed scope…"><button type="button" class="qbtn ok" data-approval="edit" data-goal="${g.id}" aria-label="Apply and proceed">↑</button></div></div>`;
|
|
4714
|
+
}).join('');
|
|
4715
|
+
const needsYou = approvalCards + questionCards;
|
|
4716
|
+
const todo = grouped(todoShown, (t, i) => (i === 0 ? '<div class="upnextlbl">UP NEXT</div>' : '') + row(t, { drag: 1, idx: i }))
|
|
4668
4717
|
+ (todoAll.length > TODO_LIMIT ? `<div class="more" data-todomore="1">${state.showAllTodo ? 'show less' : `more · +${todoAll.length - TODO_LIMIT}`}</div>` : '');
|
|
4669
4718
|
// Reviews are numbered by their own original TODO number (task 118), not
|
|
4670
4719
|
// by position in this list — that number matches the dialog header
|
|
@@ -4738,7 +4787,8 @@ function renderTasks(){
|
|
|
4738
4787
|
<button type="button" class="pdel" data-pdel="${g.id}" title="Delete">✕</button></div>`).join('');
|
|
4739
4788
|
const savedQAns = captureFocusedAnswerInput($('tasklist'), 'data-qansinput');
|
|
4740
4789
|
$('tasklist').innerHTML =
|
|
4741
|
-
sec('
|
|
4790
|
+
sec('Needs you', needsYou, 'needsyou')
|
|
4791
|
+
+ sec('Attention', attention, 'attn')
|
|
4742
4792
|
+ sec(pickColumnLabel(cols, 'review', 'Review'), reviewDigest + review, 'reviewsec', reviewDefinitionFor(state.active).description, REVIEW_DEF_HINT)
|
|
4743
4793
|
+ sec(pickColumnLabel(cols, 'doing', 'Doing'), doing)
|
|
4744
4794
|
+ sec(pickColumnLabel(cols, 'todo', 'To do'), todo, 'todosec')
|
|
@@ -4920,6 +4970,25 @@ function renderTasks(){
|
|
|
4920
4970
|
for (const b of $('tasklist').querySelectorAll('[data-qanswer]')) b.onclick = (e) => { e.stopPropagation(); answerQ(Number(b.dataset.qanswer), b.dataset.answer); };
|
|
4921
4971
|
for (const b of $('tasklist').querySelectorAll('[data-qanssend]')) b.onclick = (e) => { e.stopPropagation(); const inp = $('tasklist').querySelector(`[data-qansinput="${b.dataset.qanssend}"]`); answerQ(Number(b.dataset.qanssend), inp && inp.value.trim()); };
|
|
4922
4972
|
for (const inp of $('tasklist').querySelectorAll('[data-qansinput]')) inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); answerQ(Number(inp.dataset.qansinput), inp.value.trim()); } };
|
|
4973
|
+
for (const b of $('tasklist').querySelectorAll('[data-approvaledit]')) b.onclick = (e) => {
|
|
4974
|
+
e.stopPropagation();
|
|
4975
|
+
const row = $('tasklist').querySelector(`[data-approvaleditrow="${b.dataset.approvaledit}"]`);
|
|
4976
|
+
if (row) { row.hidden = !row.hidden; if (!row.hidden) row.querySelector('input')?.focus(); }
|
|
4977
|
+
};
|
|
4978
|
+
const decideApproval = async (goalId, decision, instruction = '') => {
|
|
4979
|
+
const r = await fetch(withKey(`/api/goals/${goalId}/approval`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ decision, instruction }) });
|
|
4980
|
+
if (!r.ok) showErr((await r.json().catch(() => null))?.error || 'Approval decision failed'); else await refresh();
|
|
4981
|
+
};
|
|
4982
|
+
for (const b of $('tasklist').querySelectorAll('[data-approval]')) b.onclick = (e) => {
|
|
4983
|
+
e.stopPropagation();
|
|
4984
|
+
const instruction = b.dataset.approval === 'edit' ? ($('tasklist').querySelector(`[data-approvalinput="${b.dataset.goal}"]`)?.value.trim() || '') : '';
|
|
4985
|
+
if (b.dataset.approval === 'edit' && !instruction) return;
|
|
4986
|
+
decideApproval(Number(b.dataset.goal), b.dataset.approval, instruction);
|
|
4987
|
+
};
|
|
4988
|
+
for (const inp of $('tasklist').querySelectorAll('[data-approvalinput]')) inp.onkeydown = (e) => {
|
|
4989
|
+
if (e.key !== 'Enter' || e.isComposing || !inp.value.trim()) return;
|
|
4990
|
+
e.preventDefault(); decideApproval(Number(inp.dataset.approvalinput), 'edit', inp.value.trim());
|
|
4991
|
+
};
|
|
4923
4992
|
// B mode: tap an "未送信" outbox row to resend it.
|
|
4924
4993
|
for (const el of $('tasklist').querySelectorAll('[data-oresend]')) el.onclick = () => { const item = outbox.load().find((x) => x.oid === el.dataset.oresend); if (item) postOutboxItem(item); };
|
|
4925
4994
|
const moreEl = $('tasklist').querySelector('[data-todomore]');
|
|
@@ -5318,7 +5387,7 @@ function closeReviewChecklist(){
|
|
|
5318
5387
|
state.reviewOpenGoal = null;
|
|
5319
5388
|
// task 86: レビュー画面からチャットへ戻る瞬間に、そのゴールの末尾(完了して
|
|
5320
5389
|
// いれば結論サマリ)がすぐ見える位置へスクロールする。
|
|
5321
|
-
if (goalId != null) goToGoalInChat(goalId);
|
|
5390
|
+
if (goalId != null) goToGoalInChat(goalId, false);
|
|
5322
5391
|
}
|
|
5323
5392
|
function renderReviewChecklist(){
|
|
5324
5393
|
const goalId = state.reviewOpenGoal;
|
|
@@ -6034,9 +6103,11 @@ async function sendReviewDismiss(text){
|
|
|
6034
6103
|
if (body.task) upsert(state.tasks, body.task);
|
|
6035
6104
|
$('reviewFixInput').value = '';
|
|
6036
6105
|
reviewAttach = []; renderReviewAttach();
|
|
6037
|
-
//
|
|
6038
|
-
//
|
|
6039
|
-
|
|
6106
|
+
// Keep the centre conversation focused on this work, but detach only the
|
|
6107
|
+
// composer. Review is a live progress surface and stays open until Close.
|
|
6108
|
+
state.selectedGoal = goal.id;
|
|
6109
|
+
state.composerDetachedGoal = goal.id;
|
|
6110
|
+
state.openThreads.add(goal.id);
|
|
6040
6111
|
render();
|
|
6041
6112
|
} catch { showErr('Dismiss failed.'); }
|
|
6042
6113
|
}
|
|
@@ -6422,13 +6493,23 @@ function renderFsRail(){
|
|
|
6422
6493
|
const pgoals = state.goals.filter((g) => g.projectId === p.id);
|
|
6423
6494
|
// running dot (HANDOFF-v45 §11): a REAL running task, independent of selection —
|
|
6424
6495
|
// .on already means "selected", .run means "actually doing work right now".
|
|
6425
|
-
|
|
6496
|
+
// Was ptasks.some(status==='running') only — missed goals sitting in the board's
|
|
6497
|
+
// 'doing' bucket before their first task exists yet (e.g. still 'planning'), so a
|
|
6498
|
+
// project the To Do/Kanban view showed as Doing kept a static dot in the rail
|
|
6499
|
+
// (Masa report 2026-07-27: "全部doingなのに緑のアイコンが回ってない"). Match the
|
|
6500
|
+
// same bucket the board/list already use, minus Paused (intentionally static).
|
|
6501
|
+
const running = pgoals.some((g) => fsBoardBucket(g, ptasks) === 'doing'
|
|
6502
|
+
&& !(g.status === 'blocked' && g.blocked?.kind === 'paused'));
|
|
6426
6503
|
const td = ptasks.filter((t) => ['running', 'queued'].includes(t.status)).length;
|
|
6427
6504
|
const rev = buildReviewDigest({ goals: pgoals, tasks: ptasks }).length;
|
|
6428
6505
|
const pcount = `<span class="pcount" title="${rev} review${rev === 1 ? '' : 's'} ready · ${td} to-do">`
|
|
6429
6506
|
+ `<span class="pc-item rev${rev ? '' : ' zero'}">${FS_ICO_SCAN}${rev}</span>`
|
|
6430
6507
|
+ `<span class="pc-item td">${FS_ICO_TD}${td}</span></span>`;
|
|
6431
|
-
|
|
6508
|
+
// The left dot itself, not just the count beside it, should read "review waiting"
|
|
6509
|
+
// (Masa 2026-07-27: "青で表示されるのは左の◯もね") — reuses the same Review blue
|
|
6510
|
+
// as pc-item.rev/.lbc-dot.rev. Running (spinning) wins when both are true.
|
|
6511
|
+
const revCls = !running && rev ? ' rev' : '';
|
|
6512
|
+
return `<button class="fsproj${p.id === state.active ? ' on' : ''}${running ? ' run' : ''}${revCls}" data-fsproj="${p.id}"><i></i><span class="pname">${esc(p.name)}</span>${pcount}</button>`;
|
|
6432
6513
|
}).join('')
|
|
6433
6514
|
+ `<button class="fsprojadd" id="fsProjAdd" title="New project"><span class="pa-plus"></span><span class="pa-label">New project</span></button>`;
|
|
6434
6515
|
// Skip the DOM swap when the rail is unchanged: re-creating .fsproj.run would restart the
|
|
@@ -6631,7 +6712,12 @@ function renderFsFeed(){
|
|
|
6631
6712
|
if (!out && hasWork) {
|
|
6632
6713
|
const laterN = pgoals.filter((g) => g.status === 'pending').length;
|
|
6633
6714
|
const queuedN = ptasks.filter((t) => t.status === 'queued').length + pgoals.filter((g) => ['planning', 'stacked'].includes(g.status)).length;
|
|
6634
|
-
const
|
|
6715
|
+
const selectedQueued = [...ptasks].reverse().find((t) => t.status === 'queued' && t.goalId === state.selectedGoal)
|
|
6716
|
+
?? [...ptasks].reverse().find((t) => t.status === 'queued');
|
|
6717
|
+
const queuedText = selectedQueued
|
|
6718
|
+
? `Queued · ${String(selectedQueued.title ?? '').replace(/\s+/g, ' ').slice(0, 90)} · waiting to start`
|
|
6719
|
+
: `${queuedN} queued · waiting to start`;
|
|
6720
|
+
const msg = running.length ? 'working…' : queuedN ? queuedText : laterN ? `no active task · ${laterN} saved in Later` : 'no active task';
|
|
6635
6721
|
out = `<div class="cc-c">⎿ ${esc(msg)}</div>`;
|
|
6636
6722
|
}
|
|
6637
6723
|
$('fsRoot').classList.toggle('clawdhero', !hasWork);
|
|
@@ -6669,6 +6755,13 @@ async function fsAnswerQuestion(goalId, answer){
|
|
|
6669
6755
|
catch { showErr('Failed to send answer'); return; }
|
|
6670
6756
|
if (!r.ok) showErr('Failed to send answer'); else await refresh();
|
|
6671
6757
|
}
|
|
6758
|
+
async function submitApprovalDecision(goalId, decision, instruction = ''){
|
|
6759
|
+
if (decision === 'edit' && !String(instruction).trim()) return;
|
|
6760
|
+
let r;
|
|
6761
|
+
try { r = await fetch(withKey(`/api/goals/${goalId}/approval`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ decision, instruction }) }); }
|
|
6762
|
+
catch { showErr('Approval decision failed'); return; }
|
|
6763
|
+
if (!r.ok) showErr((await r.json().catch(() => null))?.error || 'Approval decision failed'); else await refresh();
|
|
6764
|
+
}
|
|
6672
6765
|
async function fsRetryTask(id){
|
|
6673
6766
|
const r = await fetch(withKey(`/api/tasks/${id}/retry`), { method: 'POST' });
|
|
6674
6767
|
if (!r.ok) showErr('Retry failed.'); else await refresh();
|
|
@@ -6744,6 +6837,7 @@ function fsBoardBucket(goal, tasks){
|
|
|
6744
6837
|
const s = goal?.status;
|
|
6745
6838
|
if (s === 'pending') return 'later'; // shelved by the composer's "Later"
|
|
6746
6839
|
if (s === 'needsInput') return 'needs'; // a clarifying question is waiting on the user
|
|
6840
|
+
if (s === 'needsApproval') return 'needs'; // an explicit go/no-go is waiting; never Review
|
|
6747
6841
|
if (s === 'blocked' && goal?.blocked?.kind === 'paused') return 'doing'; // paused by user — stays in Doing, shown amber "Paused" (Masa 2026-07-19: 1カードのまま黄色に・Needs youへ移動しない)
|
|
6748
6842
|
if (['blocked', 'failed', 'partial', 'interrupted'].includes(s)) return 'needs'; // stopped/errored — needs YOUR action (retry), like the flagship board which puts .drow.err in Needs you. Not "Doing" — Doing is only what's actively moving (Masa 2026-07-18)
|
|
6749
6843
|
if (s === 'review' || s === 'folded') return 'review'; // folded (PR filed for review) reads as Review, not Doing — goalGroupStatus parity (Masa 2026-07-21)
|
|
@@ -6895,7 +6989,7 @@ function renderFsBoard(){
|
|
|
6895
6989
|
col('To Do', 'var(--ink3)', bucket.todo.length, bucket.todo.map(goalCard).join(''), 'todo')
|
|
6896
6990
|
+ col('Doing', 'var(--green)', bucket.doing.length, bucket.doing.map(goalCard).join(''), 'doing')
|
|
6897
6991
|
+ (bucket.needs.length ? col('Needs you', '#ffca16', bucket.needs.length, bucket.needs.map(goalCard).join(''), 'needs') : '')
|
|
6898
|
-
+ col('Review', '
|
|
6992
|
+
+ col('Review', 'var(--review)', reviewDigest.length, reviewDigest.map(reviewCard).join(''), 'review')
|
|
6899
6993
|
+ col('Done', 'var(--ink3)', bucket.done.length, bucket.done.slice(0, 40).map(goalCard).join(''), 'done')
|
|
6900
6994
|
+ col('Later', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
|
|
6901
6995
|
for (const b of el.querySelectorAll('[data-fsnew]')) b.onclick = () => $('input')?.focus(); // every + New / header + focuses the composer
|
|
@@ -6924,7 +7018,7 @@ function togglePauseActive(){
|
|
|
6924
7018
|
// width. They announce themselves beside the List/Board toggle instead — pure read of the same
|
|
6925
7019
|
// buckets the board just rendered (no server call). Colour on the dot only; a column already
|
|
6926
7020
|
// in view sinks. Only the columns with items are drawn — zero waiting shows nothing.
|
|
6927
|
-
const FS_WAIT = [['needs', 'Needs you', '#ffca16'], ['review', 'Review', '
|
|
7021
|
+
const FS_WAIT = [['needs', 'Needs you', '#ffca16'], ['review', 'Review', 'var(--review)']];
|
|
6928
7022
|
function fsWaitPlace(){
|
|
6929
7023
|
const bar = $('fsWait'), seg = $('fsTvSeg'), lane = $('fsLane');
|
|
6930
7024
|
if (!bar || !seg || !lane) return;
|
|
@@ -6997,6 +7091,7 @@ function renderFsReview(){
|
|
|
6997
7091
|
// retest ×3 note (v45 §5.3): the goal came back green from a Ledger Dismiss
|
|
6998
7092
|
const g0 = pgoals.find((g) => g.id === r.goalIds[0]);
|
|
6999
7093
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
7094
|
+
const incompleteNote = r.incompleteRequirementCount ? `<span class="rnote">${r.incompleteRequirementCount} requirement${r.incompleteRequirementCount === 1 ? '' : 's'} need review</span>` : '';
|
|
7000
7095
|
const infraNote = r.verificationInfraError?.detail
|
|
7001
7096
|
? `<span class="rnote">検証基盤エラー · <button type="button" class="retrybtn" data-goalreverify="${r.goalIds[0]}">再検証</button></span>`
|
|
7002
7097
|
: '';
|
|
@@ -7011,7 +7106,7 @@ function renderFsReview(){
|
|
|
7011
7106
|
// (Masa 2026-07-20: this card showed reviewSummary.check "…が出ない" while the list
|
|
7012
7107
|
// showed the request text). Plan-reviews keep their "PLAN — approve to execute:"
|
|
7013
7108
|
// checkLine, which is the actionable label there, not a title mismatch.
|
|
7014
|
-
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>
|
|
7109
|
+
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}${incompleteNote}${infraNote}${pr}${conflictNote}</div>
|
|
7015
7110
|
<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>`;
|
|
7016
7111
|
};
|
|
7017
7112
|
// V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
|
|
@@ -7079,6 +7174,7 @@ function fsBucketTodo(){
|
|
|
7079
7174
|
failed: list.filter((t) => ['failed', 'interrupted'].includes(t.status)),
|
|
7080
7175
|
blocked: pgoals.filter((g) => g.status === 'blocked'),
|
|
7081
7176
|
needsInput: pgoals.filter((g) => g.status === 'needsInput'), // a clarifying question is waiting on the user (board's "Needs you")
|
|
7177
|
+
needsApproval: pgoals.filter((g) => g.status === 'needsApproval'),
|
|
7082
7178
|
reviewGoals: pgoals.filter((g) => g.status === 'review'),
|
|
7083
7179
|
queued: list.filter((t) => t.status === 'queued' && !t.reply).sort(byQueue),
|
|
7084
7180
|
// A reply task's completion is already visible inline in the center-lane timeline
|
|
@@ -7186,7 +7282,7 @@ function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = fa
|
|
|
7186
7282
|
// error as a small in-row colour element (§1.5-8), quiet Done fold.
|
|
7187
7283
|
function renderFsTodo(){
|
|
7188
7284
|
const el = $('fsTodoSec'); if (!el) return;
|
|
7189
|
-
const { pgoals, list, running, failed, blocked, needsInput, queued, doneAll, later, rejected } = fsBucketTodo();
|
|
7285
|
+
const { pgoals, list, running, failed, blocked, needsInput, needsApproval, queued, doneAll, later, rejected } = fsBucketTodo();
|
|
7190
7286
|
const paused = !!state.projects.find((p) => p.id === state.active)?.paused; // list版 Doing トグルの状態
|
|
7191
7287
|
// 一時停止したゴール(blocked kind:'paused')は Doing に「Paused」1行で留める(Masa 2026-07-19)。
|
|
7192
7288
|
// Needs you 側(failed/blocked)へは出さず、その走行/中断タスク行も抑止して二重行を作らない。
|
|
@@ -7276,7 +7372,10 @@ function renderFsTodo(){
|
|
|
7276
7372
|
<div class="acts2">${errorButtonsHtml(meta, { goalId: g.id, agent: g.agent })}<button class="errbtn" data-fsarchive="${g.id}">Archive</button></div></div></div>`; }).join('')
|
|
7277
7373
|
+ 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>
|
|
7278
7374
|
<div class="errline"><div class="msg"><span class="x int">?</span>${tt(g.question?.text || 'A question is waiting on you', 180)}</div>
|
|
7279
|
-
<div class="acts2"><button class="errbtn" data-fsglog="${g.id}">Answer</button></div></div></div>`).join('')
|
|
7375
|
+
<div class="acts2"><button class="errbtn" data-fsglog="${g.id}">Answer</button></div></div></div>`).join('')
|
|
7376
|
+
+ needsApproval.map((g) => { const a = g.approvalRequest || {}; const scope = (a.scope || []).join(' · '); return `<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(a.question || g.text, 80)}</span><span class="stword int">Approval</span></div>
|
|
7377
|
+
<div class="errline"><div class="msg"><span class="x int">?</span>${tt(scope || a.action || 'Permission required', 180)}</div>
|
|
7378
|
+
<div class="acts2"><button class="errbtn" data-fsapproval="proceed" data-goal="${g.id}">Proceed</button><button class="errbtn" data-fsapprovaledit="${g.id}">Edit scope</button><button class="errbtn" data-fsapproval="cancel" data-goal="${g.id}">Cancel</button></div></div></div>`; }).join('');
|
|
7280
7379
|
// Parked over-cap replies ride at the TOP of To Do (above the ordinary Next-up
|
|
7281
7380
|
// queue), as neutral "Queued" rows — the same goal-row shape as a resumed Doing
|
|
7282
7381
|
// goal but without the green "Working on it", because no worker is running yet.
|
|
@@ -7303,7 +7402,7 @@ function renderFsTodo(){
|
|
|
7303
7402
|
// Keep executable work and deliberately-shelved Later work visibly distinct.
|
|
7304
7403
|
// "To Do 6" while all six were in Later made an idle queue look broken: Later
|
|
7305
7404
|
// never auto-starts by design. The header now tells the operational truth.
|
|
7306
|
-
const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
|
|
7405
|
+
const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length + needsApproval.length;
|
|
7307
7406
|
const doingCount = running.length + finalizingGoals.length + pausedGoalIds.size;
|
|
7308
7407
|
const nextCount = parkedReplyGoals.length + queued.length;
|
|
7309
7408
|
// Free-tier pending counter (H22): on the free plan the header shows the wall
|
|
@@ -7374,6 +7473,14 @@ function renderFsTodo(){
|
|
|
7374
7473
|
for (const b of el.querySelectorAll('[data-fsprretry]')) b.onclick = (e) => { e.stopPropagation(); fsRetryPrGoal(b); };
|
|
7375
7474
|
for (const b of el.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
7376
7475
|
for (const b of el.querySelectorAll('[data-fsglog]')) b.onclick = () => toggleGoalDetail(Number(b.dataset.fsglog));
|
|
7476
|
+
for (const b of el.querySelectorAll('[data-fsapproval]')) b.onclick = async (e) => {
|
|
7477
|
+
e.stopPropagation(); await submitApprovalDecision(Number(b.dataset.goal), b.dataset.fsapproval);
|
|
7478
|
+
};
|
|
7479
|
+
for (const b of el.querySelectorAll('[data-fsapprovaledit]')) b.onclick = (e) => {
|
|
7480
|
+
e.stopPropagation(); fsInlineInput(b.closest('.drow'), '', 'Describe the allowed scope…', async (instruction) => {
|
|
7481
|
+
await submitApprovalDecision(Number(b.dataset.fsapprovaledit), 'edit', instruction);
|
|
7482
|
+
});
|
|
7483
|
+
};
|
|
7377
7484
|
for (const b of el.querySelectorAll('[data-fsreverify]')) b.onclick = async () => {
|
|
7378
7485
|
b.disabled = true; b.textContent = 'Starting…';
|
|
7379
7486
|
let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.fsreverify}/reverify`), { method: 'POST' }); }
|
|
@@ -8071,14 +8178,15 @@ function showBillingBlocked(blocked){
|
|
|
8071
8178
|
|
|
8072
8179
|
const AUTH_COPY = {
|
|
8073
8180
|
ja: {
|
|
8074
|
-
connectTitle: '接続が必要です', connectBody: 'Claude Codeとの接続が切れています。ボタンから接続してください。', connect: 'Claude Codeを接続', retry: '接続を確認',
|
|
8181
|
+
connectTitle: '接続が必要です', connectBody: 'Claude Codeとの接続が切れています。ボタンから接続してください。', connect: 'Claude Codeを接続', retry: '接続を確認', checking: '確認中…',
|
|
8075
8182
|
limitTitle: 'Claudeは利用上限です', limitBody: '作業は安全に待機しています。すぐ続ける場合は、待機中のタスクをCodexへ切り替えられます。', limitWait: '作業は安全に待機しています。Claudeが再び使えるようになると自動で再開します。', codex: 'Codexで続ける',
|
|
8076
8183
|
},
|
|
8077
8184
|
en: {
|
|
8078
|
-
connectTitle: 'Connect Claude Code', connectBody: 'Claude Code is disconnected. Use the button to reconnect and continue.', connect: 'Connect Claude Code', retry: 'Check connection',
|
|
8185
|
+
connectTitle: 'Connect Claude Code', connectBody: 'Claude Code is disconnected. Use the button to reconnect and continue.', connect: 'Connect Claude Code', retry: 'Check connection', checking: 'Checking…',
|
|
8079
8186
|
limitTitle: 'Claude limit reached', limitBody: 'Your work is safely waiting. Switch the waiting tasks to Codex if you want to continue now.', limitWait: 'Your work is safely waiting and will resume automatically when Claude is available.', codex: 'Continue with Codex',
|
|
8080
8187
|
},
|
|
8081
8188
|
};
|
|
8189
|
+
let authConnectionOpened = false;
|
|
8082
8190
|
function renderAuthBanner(auth){
|
|
8083
8191
|
const bar = $('errbar');
|
|
8084
8192
|
state.auth = auth ?? state.auth;
|
|
@@ -8092,7 +8200,7 @@ function renderAuthBanner(auth){
|
|
|
8092
8200
|
+ `<div class="bx-q">${limited ? (canUseCodex ? copy.limitBody : copy.limitWait) : copy.connectBody}</div></div>`
|
|
8093
8201
|
+ `<div class="bx-act">${limited && canUseCodex
|
|
8094
8202
|
? `<button class="bx-cta" id="bxUseCodex">${copy.codex}</button>`
|
|
8095
|
-
: limited ? '' : `<button class="bx-cta" id="
|
|
8203
|
+
: limited ? '' : `<button class="bx-cta" id="bxClaudeAuthAction" data-auth-phase="${authConnectionOpened ? 'verify' : 'connect'}"><span class="bx-spin"></span><span data-auth-label>${authConnectionOpened ? copy.retry : copy.connect}</span></button>`}</div></div>`;
|
|
8096
8204
|
bar.style.display = 'block';
|
|
8097
8205
|
clearTimeout(errTimer); // this banner persists — no auto-hide
|
|
8098
8206
|
const codex = bar.querySelector('#bxUseCodex');
|
|
@@ -8108,30 +8216,39 @@ function renderAuthBanner(auth){
|
|
|
8108
8216
|
} catch (e) { showErr(e.message || 'Could not switch to Codex.'); }
|
|
8109
8217
|
finally { codex.disabled = false; }
|
|
8110
8218
|
};
|
|
8111
|
-
const
|
|
8112
|
-
if (
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
} catch { showErr(copy === AUTH_COPY.ja ? '接続画面を開けませんでした。' : 'Could not open the connection.'); }
|
|
8119
|
-
finally { connect.disabled = false; }
|
|
8120
|
-
};
|
|
8121
|
-
const retry = bar.querySelector('#bxRetryClaude');
|
|
8122
|
-
if (retry) retry.onclick = async () => {
|
|
8123
|
-
retry.disabled = true;
|
|
8219
|
+
const authAction = bar.querySelector('#bxClaudeAuthAction');
|
|
8220
|
+
if (authAction) authAction.onclick = async () => {
|
|
8221
|
+
const phase = authAction.dataset.authPhase;
|
|
8222
|
+
authAction.disabled = true;
|
|
8223
|
+
authAction.classList.add('loading');
|
|
8224
|
+
const label = authAction.querySelector('[data-auth-label]');
|
|
8225
|
+
if (label) label.textContent = phase === 'verify' ? copy.checking : copy.connect;
|
|
8124
8226
|
try {
|
|
8125
|
-
const
|
|
8227
|
+
const endpoint = phase === 'verify' ? '/api/worker-auth/retry' : '/api/worker-auth/connect';
|
|
8228
|
+
const r = await fetch(withKey(endpoint), { method: 'POST' });
|
|
8126
8229
|
const body = await r.json().catch(() => ({}));
|
|
8127
8230
|
if (!r.ok) throw new Error();
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8231
|
+
if (phase === 'connect') {
|
|
8232
|
+
authConnectionOpened = true;
|
|
8233
|
+
renderAuthBanner(state.auth);
|
|
8234
|
+
showToast(copy === AUTH_COPY.ja ? '接続画面を開きました。完了後に「接続を確認」を押してください。' : 'Connection opened. When finished, choose “Check connection”.');
|
|
8235
|
+
} else {
|
|
8236
|
+
renderAuthBanner(body.auth);
|
|
8237
|
+
if (body.auth?.ok) showToast(copy === AUTH_COPY.ja ? '接続できました。' : 'Connected.');
|
|
8238
|
+
}
|
|
8239
|
+
} catch {
|
|
8240
|
+
showErr(phase === 'verify'
|
|
8241
|
+
? (copy === AUTH_COPY.ja ? 'まだ接続できていません。' : 'Not connected yet.')
|
|
8242
|
+
: (copy === AUTH_COPY.ja ? '接続画面を開けませんでした。' : 'Could not open the connection.'));
|
|
8243
|
+
} finally {
|
|
8244
|
+
authAction.disabled = false;
|
|
8245
|
+
authAction.classList.remove('loading');
|
|
8246
|
+
if (label) label.textContent = phase === 'verify' ? copy.retry : copy.connect;
|
|
8247
|
+
}
|
|
8132
8248
|
};
|
|
8133
8249
|
} else if (authDown) {
|
|
8134
8250
|
authDown = false;
|
|
8251
|
+
authConnectionOpened = false;
|
|
8135
8252
|
bar.classList.remove('billing');
|
|
8136
8253
|
bar.style.display = 'none';
|
|
8137
8254
|
bar.innerHTML = '';
|
|
@@ -8858,6 +8975,10 @@ const REPLY_PLAIN = new Set(['running', 'partial', 'failed', 'interrupted', 'blo
|
|
|
8858
8975
|
function replyBinding(){
|
|
8859
8976
|
const id = state.selectedGoal;
|
|
8860
8977
|
if (id == null) return null;
|
|
8978
|
+
// Focus and reply-target are separate concepts. A Review reply keeps this goal
|
|
8979
|
+
// selected so its live work remains in the centre feed, while the composer goes
|
|
8980
|
+
// back to new-goal mode and shows no stale “Replying to” chip.
|
|
8981
|
+
if (state.composerDetachedGoal === id) return null;
|
|
8861
8982
|
const g = state.goals.find((x) => x.id === id);
|
|
8862
8983
|
if (!g || g.projectId !== state.active) return null; // stale after a project switch → new-goal mode
|
|
8863
8984
|
// needsInput has NO worker session yet (planGoal() sets it before any task/worker
|
|
@@ -8932,10 +9053,9 @@ async function send(){
|
|
|
8932
9053
|
const body = await replyToGoal(bind.goal.id, fullText, '#input');
|
|
8933
9054
|
if (body) {
|
|
8934
9055
|
upsert(state.goals, body.goal); if (body.task) upsert(state.tasks, body.task);
|
|
8935
|
-
//
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
state.selectedGoal = null;
|
|
9056
|
+
// Keep the goal selected for the centre feed; detach only the composer.
|
|
9057
|
+
state.composerDetachedGoal = bind.goal.id;
|
|
9058
|
+
state.openThreads.add(bind.goal.id);
|
|
8939
9059
|
render();
|
|
8940
9060
|
}
|
|
8941
9061
|
} else {
|
|
@@ -9221,6 +9341,14 @@ function saRowFromDigest(r, gs, ts){
|
|
|
9221
9341
|
title: saReviewTitle(g),
|
|
9222
9342
|
check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
|
|
9223
9343
|
goalText: String(g?.text ?? '').replace(/\s+/g, ' ').trim(),
|
|
9344
|
+
// Server supplies this fact-only ledger for new reviews. Older reviews
|
|
9345
|
+
// simply omit it, so the UI remains backward compatible during rollout.
|
|
9346
|
+
attempt: g?.reviewAttempt ?? null,
|
|
9347
|
+
unmappedChangedFiles: outcome?.unmappedChangedFiles ?? [],
|
|
9348
|
+
requirementVerification: outcome?.requirementVerification ?? null,
|
|
9349
|
+
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String(t.reply ? t.detail : t.title).replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
|
|
9350
|
+
requirementEvidence: outcome?.requirementEvidence?.requirements ?? [],
|
|
9351
|
+
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String(t.reply ? t.detail : t.title).replace(/\s+/g, ' ').trim(), status: t.status })),
|
|
9224
9352
|
did, siblings, risk: g?.risk ?? null,
|
|
9225
9353
|
verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
|
|
9226
9354
|
prNote: g?.prNote ?? null,
|
|
@@ -9726,6 +9854,44 @@ function saItemHtml(it, i){
|
|
|
9726
9854
|
const reqSec = !it.goalText ? '' : reqFold
|
|
9727
9855
|
? `<section class="rsec rreq"><div class="rsec-h">Request</div><details class="lacc" data-detkey="${gk}:req"${reqOpen ? ' open' : ''}><summary><i class="ltog">›</i><p class="rbody rask lfirst">${esc(reqPeek)}</p></summary><div class="lbody"><p class="rbody rask">${esc(it.goalText)}</p></div></details></section>`
|
|
9728
9856
|
: `<section class="rsec rreq"><div class="rsec-h">Request</div><p class="rbody rask">${esc(it.goalText)}</p></section>`;
|
|
9857
|
+
// The active attempt is intentionally a compact fact row, not a second
|
|
9858
|
+
// interpretation of the request. It makes the latest reply, files and
|
|
9859
|
+
// verification visible together while prior attempts stay in Conversation.
|
|
9860
|
+
const attempt = it.attempt;
|
|
9861
|
+
const activeText = String(attempt?.activeRequirement?.text || '').replace(/\s+/g, ' ').trim();
|
|
9862
|
+
const attemptFiles = Array.isArray(attempt?.changedFiles) ? attempt.changedFiles.filter(Boolean) : [];
|
|
9863
|
+
const attemptTests = attempt?.verification?.tests;
|
|
9864
|
+
const attemptProof = attempt?.verification?.proof;
|
|
9865
|
+
const attemptEvidence = [
|
|
9866
|
+
attemptFiles.length ? `Changed: ${attemptFiles.join(' · ')}` : '',
|
|
9867
|
+
attemptTests?.ran ? `Tests: ${attemptTests.passed ?? 0} passed${attemptTests.failed ? ` / ${attemptTests.failed} failed` : ''}` : '',
|
|
9868
|
+
attemptProof?.pass === true ? 'Proof recorded' : '',
|
|
9869
|
+
].filter(Boolean).join(' · ');
|
|
9870
|
+
const attemptSec = activeText || attemptEvidence
|
|
9871
|
+
? `<section class="rsec"><div class="rsec-h">This review</div>${activeText ? `<p class="rbody rask">${esc(activeText)}</p>` : ''}${attemptEvidence ? `<p class="rbody v">${esc(attemptEvidence)}</p>` : ''}</section>`
|
|
9872
|
+
: '';
|
|
9873
|
+
const reqVerify = it.requirementVerification;
|
|
9874
|
+
const reqVerifyParts = [
|
|
9875
|
+
reqVerify?.proof?.verified ? 'Independent proof recorded' : reqVerify?.proof ? 'Proof recorded' : '',
|
|
9876
|
+
reqVerify?.run?.url ? `Open it: ${reqVerify.run.url}` : '',
|
|
9877
|
+
Array.isArray(reqVerify?.changedFiles) && reqVerify.changedFiles.length ? `Files: ${reqVerify.changedFiles.join(' · ')}` : '',
|
|
9878
|
+
].filter(Boolean);
|
|
9879
|
+
const reqVerifySec = reqVerifyParts.length ? `<section class="rsec"><div class="rsec-h">Requirement verification</div><p class="rbody v">${esc(reqVerifyParts.join(' · '))}</p></section>` : '';
|
|
9880
|
+
const coverageRows = (it.requirementCoverage || []).filter((r) => r.text).map((r) => {
|
|
9881
|
+
const status = r.evidenceStatus || r.taskStatus;
|
|
9882
|
+
const tone = status === 'addressed' || status === 'done' ? 'ok' : status === 'partial' ? '' : 'dim';
|
|
9883
|
+
return `<p class="rbody v"><b class="${tone}">${esc(status)}</b> · ${esc(r.text)}</p>`;
|
|
9884
|
+
}).join('');
|
|
9885
|
+
const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div>${coverageRows}</section>` : '';
|
|
9886
|
+
// Worker-declared evidence is displayed verbatim as an auditable label, not
|
|
9887
|
+
// promoted into a Manager verdict. Empty means the worker did not provide it.
|
|
9888
|
+
const evidenceRows = (it.requirementEvidence || []).map((entry) => {
|
|
9889
|
+
const tone = entry.status === 'addressed' ? 'ok' : entry.status === 'partial' ? '' : 'dim';
|
|
9890
|
+
return `<p class="rbody v"><b class="${tone}">${esc(entry.status)}</b> · ${esc(entry.evidence || entry.id)}</p>`;
|
|
9891
|
+
}).join('');
|
|
9892
|
+
const evidenceSec = evidenceRows ? `<section class="rsec"><div class="rsec-h">Requirement evidence</div>${evidenceRows}</section>` : '';
|
|
9893
|
+
const userCoverageRows = (it.requirementCoverage || []).filter((r) => r.text).map((r) => `<p class="rbody v"><b class="${r.status === 'done' ? 'ok' : 'dim'}">${esc(r.status)}</b> · ${esc(r.text)}</p>`).join('');
|
|
9894
|
+
const userCoverageSec = userCoverageRows ? `<section class="rsec"><div class="rsec-h">User requirements</div>${userCoverageRows}</section>` : '';
|
|
9729
9895
|
// ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
|
|
9730
9896
|
// heading + body shape as Request and Result, not a chip in another vocabulary. The
|
|
9731
9897
|
// address the worker declared IS the control (press it → the process starts on demand →
|
|
@@ -9843,7 +10009,11 @@ function saItemHtml(it, i){
|
|
|
9843
10009
|
const diffHtml = it.ghRows
|
|
9844
10010
|
? saGhDiffRowsHtml(it.ghRows, it.diffTotal || 0, it.pr || '', gk)
|
|
9845
10011
|
: hasGitDiff ? saGhDiffHtml(d.diff, it.pr, gk) : clientDiff ? saGhDiffHtml(clientDiff, it.pr, gk) : '';
|
|
9846
|
-
const
|
|
10012
|
+
const unmapped = Array.isArray(it.unmappedChangedFiles) ? it.unmappedChangedFiles.filter(Boolean) : [];
|
|
10013
|
+
// "Unmapped" is a fact: these measured changes were not named in worker
|
|
10014
|
+
// evidence. It is intentionally not labelled unrelated or wrong.
|
|
10015
|
+
const unmappedHtml = unmapped.length ? `<p class="rbody v">Not mapped to a requirement: ${esc(unmapped.join(' · '))}</p>` : '';
|
|
10016
|
+
const changedBody = isConv ? '' : (changedDescHtml + filesListHtml + unmappedHtml + diffHtml);
|
|
9847
10017
|
// ── sensitive-area note (real: classifyChangeRisk forced the pause) ──
|
|
9848
10018
|
const risk = it.risk;
|
|
9849
10019
|
const riskHtml = risk?.shouldPause
|
|
@@ -9928,8 +10098,18 @@ function saItemHtml(it, i){
|
|
|
9928
10098
|
// interrupted goal is amber+resumable, everything else stopped is a red error.
|
|
9929
10099
|
const attnInt = gstatus === 'interrupted' || Boolean(it.interruptedTask);
|
|
9930
10100
|
const attnLabel = attnInt ? 'Interrupted — resume available' : gstatus === 'blocked' ? 'Blocked' : 'Failed';
|
|
10101
|
+
const liveReply = [...state.tasks].reverse().find((t) => t.goalId === it.goalId && t.reply);
|
|
10102
|
+
const liveWorkLabel = liveReply?.status === 'queued'
|
|
10103
|
+
? 'Queued · To Do'
|
|
10104
|
+
: liveReply?.status === 'running'
|
|
10105
|
+
? 'Working on it · Doing'
|
|
10106
|
+
: it.goal?.status === 'review'
|
|
10107
|
+
? 'Ready for review'
|
|
10108
|
+
: it.goal?.status === 'failed' || it.goal?.status === 'partial'
|
|
10109
|
+
? 'Needs attention'
|
|
10110
|
+
: 'Working on it';
|
|
9931
10111
|
const rst = working
|
|
9932
|
-
?
|
|
10112
|
+
? `<span class="rst working"><span class="rdot"></span>${liveWorkLabel}</span>`
|
|
9933
10113
|
: attn
|
|
9934
10114
|
? `<span class="rst ${attnInt ? 'int' : 'err'}"><span class="rdot"></span>${attnLabel}</span>`
|
|
9935
10115
|
: '<span class="rst"><span class="rdot"></span>In review</span>';
|
|
@@ -9938,6 +10118,11 @@ function saItemHtml(it, i){
|
|
|
9938
10118
|
<h2 class="rtitle">${esc(it.title)}</h2>
|
|
9939
10119
|
${meta}
|
|
9940
10120
|
${reqSec}
|
|
10121
|
+
${attemptSec}
|
|
10122
|
+
${coverageSec}
|
|
10123
|
+
${evidenceSec}
|
|
10124
|
+
${reqVerifySec}
|
|
10125
|
+
${userCoverageSec}
|
|
9941
10126
|
${S('Open it', openBody)}
|
|
9942
10127
|
${S('Run state', interruptedBody)}
|
|
9943
10128
|
${S('PR', prRepairBody)}
|
|
@@ -10098,7 +10283,22 @@ function saRender(){
|
|
|
10098
10283
|
function saSync(){
|
|
10099
10284
|
if (!SA.open) return;
|
|
10100
10285
|
const ae = document.activeElement;
|
|
10101
|
-
|
|
10286
|
+
// Do not erase text while the reviewer is typing. Once Send clears the box,
|
|
10287
|
+
// focus alone must not freeze To Do / Doing / Review status updates.
|
|
10288
|
+
if (ae && $('seeall').contains(ae) && ae.tagName === 'INPUT' && ae.value) return;
|
|
10289
|
+
// Working cards are intentionally absent from the Review ledger rows after the
|
|
10290
|
+
// server moves them to To Do / Doing. Keep their held dialog snapshot wired to
|
|
10291
|
+
// live goal/task state so the status changes in place instead of disappearing.
|
|
10292
|
+
for (const ex of SA.items.filter((x) => x.st === 3)) {
|
|
10293
|
+
const liveGoal = state.goals.find((g) => g.id === ex.goalId);
|
|
10294
|
+
if (liveGoal) ex.goal = liveGoal;
|
|
10295
|
+
const activeReply = state.tasks.some((t) => t.goalId === ex.goalId && t.reply && ['queued', 'running'].includes(t.status));
|
|
10296
|
+
if (liveGoal?.status === 'review' && !activeReply) {
|
|
10297
|
+
ex.st = 0;
|
|
10298
|
+
ex.rstatus = null;
|
|
10299
|
+
ex.inflight = false;
|
|
10300
|
+
}
|
|
10301
|
+
}
|
|
10102
10302
|
const rows = saLedgerRows();
|
|
10103
10303
|
for (const row of rows) {
|
|
10104
10304
|
const ex = SA.items.find((x) => x.goalId === row.goalId);
|
|
@@ -10196,42 +10396,24 @@ async function saReplyCall(it, text, outcome){
|
|
|
10196
10396
|
if (!r?.ok) throw new Error('reply failed');
|
|
10197
10397
|
return r.json().catch(() => null);
|
|
10198
10398
|
}
|
|
10199
|
-
// Once feedback becomes rework,
|
|
10200
|
-
//
|
|
10201
|
-
//
|
|
10202
|
-
function
|
|
10399
|
+
// Once feedback becomes rework, keep the card as a live progress surface until the
|
|
10400
|
+
// reviewer closes it. The lane behind the dialog still moves immediately to To Do / Doing,
|
|
10401
|
+
// and the centre conversation follows the same goal; only the Review card snapshot is held.
|
|
10402
|
+
function saKeepFeedbackLive(it, body){
|
|
10203
10403
|
const goal = body?.goal;
|
|
10204
10404
|
const task = body?.task;
|
|
10205
10405
|
if (goal?.id) upsert(state.goals, goal);
|
|
10206
10406
|
if (task?.id) upsert(state.tasks, task);
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
const cardEl = saCardEl(it.goalId);
|
|
10216
|
-
const priorScrollTop = bodyEl?.scrollTop ?? 0;
|
|
10217
|
-
const removedHeight = cardEl ? cardEl.getBoundingClientRect().height : 0;
|
|
10218
|
-
const cardAboveViewport = !!(cardEl && bodyEl && cardEl.getBoundingClientRect().bottom <= bodyEl.getBoundingClientRect().top + 1);
|
|
10219
|
-
if (at >= 0) SA.items.splice(at, 1);
|
|
10407
|
+
it.goal = goal ?? state.goals.find((g) => g.id === it.goalId) ?? it.goal;
|
|
10408
|
+
it.st = 3;
|
|
10409
|
+
it.inflight = false;
|
|
10410
|
+
it.rstatus = 'working';
|
|
10411
|
+
SA.cur = it.goalId;
|
|
10412
|
+
state.selectedGoal = it.goalId;
|
|
10413
|
+
state.composerDetachedGoal = it.goalId;
|
|
10414
|
+
state.openThreads.add(it.goalId);
|
|
10220
10415
|
render();
|
|
10221
|
-
|
|
10222
|
-
SA.cur = next?.goalId ?? SA.items[0]?.goalId ?? null;
|
|
10223
|
-
if (cardAboveViewport) {
|
|
10224
|
-
// Keep reading where we were: compensate for the removed card's height instead of
|
|
10225
|
-
// forcing a scroll-to-top of whatever is now "current".
|
|
10226
|
-
SA.focusGid = null;
|
|
10227
|
-
saRender();
|
|
10228
|
-
const newBody = document.querySelector('#seeall .sa-body');
|
|
10229
|
-
if (newBody) newBody.scrollTop = Math.max(0, priorScrollTop - removedHeight);
|
|
10230
|
-
} else {
|
|
10231
|
-
SA.focusGid = SA.cur;
|
|
10232
|
-
SA.focusScrolled = false;
|
|
10233
|
-
saRender();
|
|
10234
|
-
}
|
|
10416
|
+
saPaintCard(it.goalId);
|
|
10235
10417
|
}
|
|
10236
10418
|
function saCardEl(goalId){ return document.querySelector(`#seeall .sa-item[data-gid="${goalId}"]`); }
|
|
10237
10419
|
// Flip one card to its "Working on it" head without redrawing anything else (決定2).
|
|
@@ -10240,7 +10422,13 @@ function saFlipWorking(goalId){
|
|
|
10240
10422
|
if (!card) return;
|
|
10241
10423
|
card.classList.add('working');
|
|
10242
10424
|
const rst = card.querySelector('.rhead .rst');
|
|
10243
|
-
|
|
10425
|
+
const it = SA.items.find((x) => x.goalId === goalId);
|
|
10426
|
+
const liveReply = [...state.tasks].reverse().find((t) => t.goalId === goalId && t.reply);
|
|
10427
|
+
const label = liveReply?.status === 'queued' ? 'Queued · To Do'
|
|
10428
|
+
: liveReply?.status === 'running' ? 'Working on it · Doing'
|
|
10429
|
+
: it?.goal?.status === 'review' ? 'Ready for review'
|
|
10430
|
+
: 'Working on it';
|
|
10431
|
+
if (rst) { rst.className = 'rst working'; rst.innerHTML = `<span class="rdot"></span>${label}`; }
|
|
10244
10432
|
}
|
|
10245
10433
|
// Repaint one card's reply thread IN PLACE from its client state — no saRender, so nothing
|
|
10246
10434
|
// above the box moves. Header is flipped to "Working on it" iff the card is in that state,
|
|
@@ -10334,7 +10522,7 @@ async function saSendReply(it, i, text){
|
|
|
10334
10522
|
// A change request has left Review. The response is the source of truth for whether it
|
|
10335
10523
|
// is queued (To Do) or already starting (Doing); reveal that transition immediately.
|
|
10336
10524
|
if (body?.goal && body.goal.status !== 'review') {
|
|
10337
|
-
|
|
10525
|
+
saKeepFeedbackLive(it, body);
|
|
10338
10526
|
return;
|
|
10339
10527
|
}
|
|
10340
10528
|
// split keeps the original goal in Review; only the newly-created ticket moves away.
|
|
@@ -11057,7 +11245,7 @@ $('filein').addEventListener('change', () => {
|
|
|
11057
11245
|
$('filein').value = '';
|
|
11058
11246
|
});
|
|
11059
11247
|
// Feature A: ✕ on the "Replying to #NNN" chip detaches → back to new-goal mode.
|
|
11060
|
-
$('replyBindX').onclick = () => { state.
|
|
11248
|
+
$('replyBindX').onclick = () => { state.composerDetachedGoal = state.selectedGoal; render(); };
|
|
11061
11249
|
const IS_MOBILE = () => window.matchMedia('(max-width: 760px)').matches;
|
|
11062
11250
|
// Phones are chat-first: the Tasks sheet starts FOLDED so the first view is the
|
|
11063
11251
|
// conversation + composer (the sheet is an 88vw overlay — default-open buried
|
|
@@ -11699,7 +11887,7 @@ function gearHistHTML(){
|
|
|
11699
11887
|
};
|
|
11700
11888
|
// History = everything you've sent, newest first (not only settled goals —
|
|
11701
11889
|
// that felt empty). Each row gets a status glyph; unknown states fall back.
|
|
11702
|
-
const MARK = { done: ['ok', '✓'], reverted: ['no', '↩'], skipped: ['ar', '—'], failed: ['no', '✕'], interrupted: ['ar', '⏸'], review: ['rv', '●'], running: ['run', '…'], planning: ['run', '…'], stacked: ['ar', '·'], needsInput: ['rv', '?'] };
|
|
11890
|
+
const MARK = { done: ['ok', '✓'], reverted: ['no', '↩'], skipped: ['ar', '—'], failed: ['no', '✕'], interrupted: ['ar', '⏸'], review: ['rv', '●'], running: ['run', '…'], planning: ['run', '…'], stacked: ['ar', '·'], needsInput: ['rv', '?'], needsApproval: ['rv', '?'] };
|
|
11703
11891
|
const evs = (state.goals || [])
|
|
11704
11892
|
.filter((g) => g.projectId === state.active)
|
|
11705
11893
|
.map((g) => ({ g, ts: goalTs(g) })).sort((a, b) => b.ts - a.ts);
|