@galda/cli 0.10.110 → 0.10.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +127 -26
- package/engine/lib.mjs +11 -0
- package/engine/server.mjs +6 -2
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -3485,7 +3485,7 @@ const savedTheme = (() => {
|
|
|
3485
3485
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3486
3486
|
return v;
|
|
3487
3487
|
})();
|
|
3488
|
-
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, hostReadiness: [], 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,
|
|
3488
|
+
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: {}, queueWaits: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, hostReadiness: [], 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,
|
|
3489
3489
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3490
3490
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3491
3491
|
// Scoped per project (msgByProject) so a skill/mode picked in one project can't leak
|
|
@@ -4617,7 +4617,25 @@ function renderQueue(){
|
|
|
4617
4617
|
// layouts keep the explanatory sentence.
|
|
4618
4618
|
const qn = ob.length + stacked.length + planning.length;
|
|
4619
4619
|
const headHtml = qn > 0 ? `<div class="qhead">${state.layout === 'flagship' ? `Queued · ${qn}` : `${qn} queued · goals become To dos in ~30s`}</div>` : '';
|
|
4620
|
+
// Why this queued goal is not moving, in the SERVER's words (state.queueWaits,
|
|
4621
|
+
// keyed by goal id — engine/lib.mjs buildQueueWaits). The server owns the slot
|
|
4622
|
+
// accounting, so it is the only thing that can answer this without disagreeing
|
|
4623
|
+
// with itself.
|
|
4624
|
+
//
|
|
4625
|
+
// The fallback below is what this used to do always: re-derive the answer in the
|
|
4626
|
+
// browser from running TASKS plus planning goals. That is the accounting #541
|
|
4627
|
+
// replaced — a goal whose task sat in the queue counted for nothing — so the
|
|
4628
|
+
// browser could print "Next up · starts automatically" about a goal the server
|
|
4629
|
+
// had blocked. Kept only for a server older than queueWaits; when the field is
|
|
4630
|
+
// there, the row repeats the server rather than guessing alongside it.
|
|
4620
4631
|
const stackedMeta = (g) => {
|
|
4632
|
+
const wait = state.queueWaits?.[g.id];
|
|
4633
|
+
if (wait) {
|
|
4634
|
+
const targets = (wait.targetGoalIds ?? []).filter((id) => id !== g.id);
|
|
4635
|
+
const active = targets.length ? ` · active ${targets.map((id) => `#${id}`).join(', ')}` : '';
|
|
4636
|
+
const ops = targets.length ? ' · Run next / switch Agent' : '';
|
|
4637
|
+
return `Waiting · ${wait.reason}${active} · starts when ${wait.condition}${ops}`;
|
|
4638
|
+
}
|
|
4621
4639
|
const project = state.projects.find((p) => p.id === g.projectId);
|
|
4622
4640
|
if (project?.paused) return 'Paused · press play to start';
|
|
4623
4641
|
const activeIds = [...new Set([
|
|
@@ -6413,6 +6431,38 @@ function prefersJapaneseText(...values){
|
|
|
6413
6431
|
return values.some((v) => /[\u3040-\u30ff\u3400-\u9fff]/.test(String(v || '')));
|
|
6414
6432
|
}
|
|
6415
6433
|
function summarizeAttention(goal, tasks){
|
|
6434
|
+
// A pending question / approval request is the one case where we already HAVE the
|
|
6435
|
+
// worker's own words (engine writes goal.question = {kind,text,options} and
|
|
6436
|
+
// goal.approvalRequest). Show them verbatim. The board's question card already does
|
|
6437
|
+
// this (.qq at the question-card render), and the two paths must not disagree about
|
|
6438
|
+
// the same field — the template built below by pattern-matching the log is a last
|
|
6439
|
+
// resort for goals that stopped without asking anything (PRD §0.1.1-5).
|
|
6440
|
+
if (goal.status === 'needsInput' && goal.question?.text) {
|
|
6441
|
+
const opts = (goal.question.options || []).map((o) => String(o).trim()).filter(Boolean);
|
|
6442
|
+
const jq = prefersJapaneseText(goal.question.text, goal.text);
|
|
6443
|
+
return {
|
|
6444
|
+
label: jq ? '質問' : 'Question',
|
|
6445
|
+
happened: String(goal.question.text).trim(),
|
|
6446
|
+
decision: opts.length
|
|
6447
|
+
? (jq ? `次のどれかで答えてください: ${opts.join(' / ')}` : `Answer with one of: ${opts.join(' / ')}`)
|
|
6448
|
+
: (jq ? '下の入力欄に答えを書いて送ってください。' : 'Type your answer in the box below and send it.'),
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
if (goal.status === 'needsApproval') {
|
|
6452
|
+
const a = goal.approvalRequest || {};
|
|
6453
|
+
const ask = String(a.question || a.action || '').trim();
|
|
6454
|
+
if (ask) {
|
|
6455
|
+
const scope = (a.scope || []).map((s) => String(s).trim()).filter(Boolean);
|
|
6456
|
+
const jq = prefersJapaneseText(ask, goal.text);
|
|
6457
|
+
return {
|
|
6458
|
+
label: jq ? '許可の依頼' : 'Approval requested',
|
|
6459
|
+
happened: scope.length ? `${ask}\n\n${jq ? '対象' : 'Scope'}: ${scope.join(' · ')}` : ask,
|
|
6460
|
+
decision: jq
|
|
6461
|
+
? 'Proceed で許可、Cancel で断ります。範囲を変えたい場合は Edit scope。'
|
|
6462
|
+
: 'Proceed to allow it, Cancel to refuse. Use Edit scope to narrow what it may touch.',
|
|
6463
|
+
};
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
6416
6466
|
const fm = goal.failureMemory?.[goal.failureMemory.length - 1];
|
|
6417
6467
|
if (fm?.whatHappened || fm?.yourCall) {
|
|
6418
6468
|
return {
|
|
@@ -6465,14 +6515,14 @@ function summarizeAttention(goal, tasks){
|
|
|
6465
6515
|
}
|
|
6466
6516
|
return { happened: happened.replace(/\.+$/, '.'), decision };
|
|
6467
6517
|
}
|
|
6468
|
-
function renderGoalBrief({ goal, tasks, isReview, isAttention }){
|
|
6518
|
+
function renderGoalBrief({ goal, tasks, isReview, isAttention, isAsking }){
|
|
6469
6519
|
const brief = $('gdBrief');
|
|
6470
|
-
if (!isReview && !isAttention) { brief.classList.remove('show'); brief.innerHTML = ''; return; }
|
|
6520
|
+
if (!isReview && !isAttention && !isAsking) { brief.classList.remove('show'); brief.innerHTML = ''; return; }
|
|
6471
6521
|
let rows;
|
|
6472
|
-
if (isAttention) {
|
|
6522
|
+
if (isAttention || isAsking) {
|
|
6473
6523
|
const s = summarizeAttention(goal, tasks);
|
|
6474
6524
|
rows = [
|
|
6475
|
-
['What happened', s.happened],
|
|
6525
|
+
[s.label || 'What happened', s.happened],
|
|
6476
6526
|
['Your call', s.decision],
|
|
6477
6527
|
];
|
|
6478
6528
|
} else {
|
|
@@ -6495,9 +6545,17 @@ function renderGoalDetail(){
|
|
|
6495
6545
|
const isReview = goal.status === 'review';
|
|
6496
6546
|
const isAttention = ['blocked', 'partial', 'failed', 'interrupted'].includes(goal.status)
|
|
6497
6547
|
|| tasks.some((t) => ['failed', 'interrupted'].includes(t.status));
|
|
6498
|
-
|
|
6548
|
+
// A goal waiting on an answer/approval is not "stuck" — it is asking. It used to
|
|
6549
|
+
// fall through both flags, so the brief was hidden entirely and the panel showed the
|
|
6550
|
+
// original request with no sign of what the worker actually asked (Masa 2026-07-30).
|
|
6551
|
+
// Kept separate from isAttention so the Discard button's condition does not change.
|
|
6552
|
+
const isAsking = ['needsInput', 'needsApproval'].includes(goal.status);
|
|
6553
|
+
$('gdTitle').textContent = isReview ? 'Review'
|
|
6554
|
+
: goal.status === 'needsInput' ? 'Question'
|
|
6555
|
+
: goal.status === 'needsApproval' ? 'Approval'
|
|
6556
|
+
: 'Attention';
|
|
6499
6557
|
$('gdRequirement').textContent = goal.text ?? '';
|
|
6500
|
-
renderGoalBrief({ goal, tasks, isReview, isAttention });
|
|
6558
|
+
renderGoalBrief({ goal, tasks, isReview, isAttention, isAsking });
|
|
6501
6559
|
// review-only items carry no worker tasks — show the author's note (or a
|
|
6502
6560
|
// plain hint) instead of "No worker reports yet.", and any attached images
|
|
6503
6561
|
// as the thing to look at.
|
|
@@ -6526,10 +6584,22 @@ function renderGoalDetail(){
|
|
|
6526
6584
|
$('gdLog').innerHTML = logLines.length
|
|
6527
6585
|
? `<div class="gdlog">${logLines.map((s) => `<div class="gdlogline">${esc(s)}</div>`).join('')}</div>`
|
|
6528
6586
|
: '<div class="gdempty">No log captured yet.</div>';
|
|
6587
|
+
// A goal that is only ASKING has not produced anything yet, so "No worker reports
|
|
6588
|
+
// yet." / "No proof available yet." / "No log captured yet." are three empty boxes
|
|
6589
|
+
// that push the actual question down and out (Masa 2026-07-30: "高さが少なくてみづらい").
|
|
6590
|
+
// §0.1.1-5: do not show the container for something that does not exist. Scoped to the
|
|
6591
|
+
// asking states so Review — where "no proof" is itself a finding — is unchanged.
|
|
6592
|
+
const hideIfEmpty = isAsking && !tasks.length;
|
|
6593
|
+
for (const [id, empty] of [['gdWhatWasDone', !tasks.length && !goal.note], ['gdProof', !proofTasks.length && !reviewImgs.length], ['gdLog', !logLines.length]]) {
|
|
6594
|
+
const sec = $(id).closest('.gdsec');
|
|
6595
|
+
if (sec) sec.style.display = hideIfEmpty && empty ? 'none' : '';
|
|
6596
|
+
}
|
|
6529
6597
|
$('gdApprove').style.display = isReview ? '' : 'none';
|
|
6530
6598
|
$('gdDismiss').style.display = isReview ? '' : 'none';
|
|
6531
6599
|
$('gdDiscard').style.display = isAttention ? '' : 'none';
|
|
6532
|
-
$('gdRevisionInput').placeholder = isReview ? 'Write revision instructions…'
|
|
6600
|
+
$('gdRevisionInput').placeholder = isReview ? 'Write revision instructions…'
|
|
6601
|
+
: goal.status === 'needsInput' ? 'Answer the question…'
|
|
6602
|
+
: 'Tell the worker what to try next…';
|
|
6533
6603
|
$('gdSend').title = isReview ? 'Send back' : 'Send';
|
|
6534
6604
|
}
|
|
6535
6605
|
$('gdClose').onclick = closeGoalDetail;
|
|
@@ -8784,7 +8854,7 @@ async function refresh(){
|
|
|
8784
8854
|
setAgentEffort(state.connectedApp);
|
|
8785
8855
|
renderModelSelect();
|
|
8786
8856
|
renderEffortSelect();
|
|
8787
|
-
state.queueOrder = s.queueOrder ?? {}; state.externalActivity = s.externalActivity ?? [];
|
|
8857
|
+
state.queueOrder = s.queueOrder ?? {}; state.queueWaits = s.queueWaits ?? {}; state.externalActivity = s.externalActivity ?? [];
|
|
8788
8858
|
state.runningCounts = s.runningCounts ?? {}; state.parallelLimits = s.parallelLimits ?? null;
|
|
8789
8859
|
state.workflowColumns = s.workflowColumns ?? {};
|
|
8790
8860
|
state.reviewDefinitions = s.reviewDefinitions ?? {};
|
|
@@ -9541,7 +9611,12 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9541
9611
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
9542
9612
|
// settles it (see saSync's "returned to review" reset).
|
|
9543
9613
|
// ============================================================================
|
|
9544
|
-
|
|
9614
|
+
// `draft` holds the half-typed reply per goal. It exists because the text used to live
|
|
9615
|
+
// ONLY in the DOM, so the sole way to keep it was for saSync() to refuse to run at all
|
|
9616
|
+
// while the box had text — which froze the To Do / Doing / Review status updates behind
|
|
9617
|
+
// the dialog too. With the text in SA it survives a rebuild on its own, and saSync no
|
|
9618
|
+
// longer has to stop (2026-07-30).
|
|
9619
|
+
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 };
|
|
9545
9620
|
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9546
9621
|
const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
|
|
9547
9622
|
// task-971: 白黒(モノクロ)版のレールを ?railmono=1 で見比べられるようにする。
|
|
@@ -9819,7 +9894,10 @@ function saLedgerRows(){
|
|
|
9819
9894
|
// (summarizeAttention), mapped into the review card instead of a bespoke "What happened /
|
|
9820
9895
|
// Your call" layout (Masa 2026-07-24). The Request section already shows the ask, so drop
|
|
9821
9896
|
// the summary's "必要だったこと:/Needed:" lead to avoid repeating it.
|
|
9822
|
-
|
|
9897
|
+
// needsInput/needsApproval are included so this path shows the worker's verbatim
|
|
9898
|
+
// question too — summarizeAttention returns it as-is for those. Both renders of the
|
|
9899
|
+
// same goal.question must say the same thing (PRD §0.1.1-5).
|
|
9900
|
+
if (['blocked', 'partial', 'failed', 'interrupted', 'needsInput', 'needsApproval'].includes(g.status)) {
|
|
9823
9901
|
const s = summarizeAttention(g, ts.filter((t) => t.goalId === g.id));
|
|
9824
9902
|
const why = String(s.happened || '').replace(/^\s*(必要だったこと:|Needed:)[^。.]*[。.]\s*/, '').trim();
|
|
9825
9903
|
const merged = [why, s.decision].filter(Boolean).join('\n\n');
|
|
@@ -10529,7 +10607,7 @@ function saItemHtml(it, i){
|
|
|
10529
10607
|
${conversationSec}
|
|
10530
10608
|
${activitySec}
|
|
10531
10609
|
${S('Workflow', saWorkflowHtml(it))}
|
|
10532
|
-
<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>
|
|
10610
|
+
<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>
|
|
10533
10611
|
</div>`;
|
|
10534
10612
|
}
|
|
10535
10613
|
// Scroll the See-all body so a card's top edge (.rhead) sits at the top of the visible
|
|
@@ -10677,9 +10755,12 @@ function saRender(){
|
|
|
10677
10755
|
}) || cards.find((el) => el.getBoundingClientRect().bottom > bodyRect.top);
|
|
10678
10756
|
if (anchor) { anchorGid = anchor.dataset.gid; anchorOffset = anchor.getBoundingClientRect().top - bodyRect.top; }
|
|
10679
10757
|
}
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10758
|
+
// Remember WHERE the caret was too, not just which box had focus: the text now comes
|
|
10759
|
+
// back from SA.draft, and putting it back without the caret would jump the cursor to
|
|
10760
|
+
// the end mid-word on every SSE tick.
|
|
10761
|
+
const activeChat = document.activeElement?.matches?.('#seeall .sa-chat') ? document.activeElement : null;
|
|
10762
|
+
const focusedReplyGoal = activeChat?.dataset.goal ?? null;
|
|
10763
|
+
const focusedCaret = activeChat ? { start: activeChat.selectionStart, end: activeChat.selectionEnd } : null;
|
|
10683
10764
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
10684
10765
|
const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
|
|
10685
10766
|
// Solo mode = a single "Needs you" goal opened as this card. It is not a review batch, so the
|
|
@@ -10810,10 +10891,24 @@ function saRender(){
|
|
|
10810
10891
|
if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
|
|
10811
10892
|
});
|
|
10812
10893
|
}
|
|
10813
|
-
//
|
|
10814
|
-
//
|
|
10815
|
-
//
|
|
10816
|
-
|
|
10894
|
+
// Every reply box carries its draft back from SA.draft via the value attribute, so a
|
|
10895
|
+
// rebuild no longer loses what was typed. Keep SA.draft current as the person types —
|
|
10896
|
+
// this is what lets saSync() run during typing instead of freezing.
|
|
10897
|
+
for (const inp of S.querySelectorAll('.sa-chat[data-goal]')) {
|
|
10898
|
+
inp.addEventListener('input', () => {
|
|
10899
|
+
const gid = inp.dataset.goal;
|
|
10900
|
+
if (inp.value) SA.draft[gid] = inp.value; else delete SA.draft[gid];
|
|
10901
|
+
});
|
|
10902
|
+
}
|
|
10903
|
+
// A relevant status change may legitimately rebuild the card. Put focus AND the caret
|
|
10904
|
+
// back where they were.
|
|
10905
|
+
if (focusedReplyGoal != null) {
|
|
10906
|
+
const inp = S.querySelector(`.sa-chat[data-goal="${focusedReplyGoal}"]`);
|
|
10907
|
+
if (inp) {
|
|
10908
|
+
inp.focus({ preventScroll: true });
|
|
10909
|
+
if (focusedCaret) { try { inp.setSelectionRange(focusedCaret.start, focusedCaret.end); } catch { /* not selectable */ } }
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10817
10912
|
}
|
|
10818
10913
|
// keep the open Ledger in sync with server state (SSE/refresh → render() → here).
|
|
10819
10914
|
// An item whose goal has RETURNED to 'review' (retest ×3 green, rework done,
|
|
@@ -10821,10 +10916,11 @@ function saRender(){
|
|
|
10821
10916
|
// blocks the rebuild (same "vanishes mid-type" guard as renderFsLane).
|
|
10822
10917
|
function saSync(){
|
|
10823
10918
|
if (!SA.open) return;
|
|
10824
|
-
|
|
10825
|
-
//
|
|
10826
|
-
//
|
|
10827
|
-
|
|
10919
|
+
// No typing guard here any more. It used to `return` whenever the focused input had
|
|
10920
|
+
// text, which protected the draft by freezing EVERYTHING — status changes behind the
|
|
10921
|
+
// dialog included, so a card could sit on "Working on it" for as long as someone was
|
|
10922
|
+
// composing a reply. The draft now lives in SA.draft and is restored with its caret by
|
|
10923
|
+
// saRender, so the sync is free to run (2026-07-30).
|
|
10828
10924
|
// Working cards are intentionally absent from the Review ledger rows after the
|
|
10829
10925
|
// server moves them to To Do / Doing. Keep their held dialog snapshot wired to
|
|
10830
10926
|
// live goal/task state so the status changes in place instead of disappearing.
|
|
@@ -10860,11 +10956,15 @@ async function saFetchDiffs(){
|
|
|
10860
10956
|
if (SA.open) saSync();
|
|
10861
10957
|
}
|
|
10862
10958
|
function saOpen(focusGoalId, solo = false){
|
|
10959
|
+
const wasOpen = SA.open;
|
|
10863
10960
|
SA.open = true;
|
|
10864
10961
|
SA.lastMarkup = null; // a newly opened/focused dialog always gets a clean first paint
|
|
10865
10962
|
SA.solo = solo ? focusGoalId : null; // solo = a single "Needs you" goal opened as one card
|
|
10866
10963
|
SA.items = saLedgerRows().map((r) => ({ ...r, st: 0 }));
|
|
10867
|
-
|
|
10964
|
+
// Only a FRESH open starts with everything folded. saOpen also runs when the person taps
|
|
10965
|
+
// a different card while the dialog is already up, and clearing the set there closed the
|
|
10966
|
+
// Activity / Conversation folds they had just opened on the other cards.
|
|
10967
|
+
if (!wasOpen) SA.detOpen = new Set();
|
|
10868
10968
|
// H14: focus a specific goal's card (single-review tap). Map the tapped goal
|
|
10869
10969
|
// to the card that CONTAINS it (goals can group in the digest), then let
|
|
10870
10970
|
// saRender center it + flash a blue ring. Real data keys by goal id.
|
|
@@ -10997,8 +11097,9 @@ async function saSendReply(it, i, text){
|
|
|
10997
11097
|
text = displayText;
|
|
10998
11098
|
await saEnsureThread(it); // fold in any existing history BEFORE this reply (so it isn't hidden)
|
|
10999
11099
|
const seq = saNextSeq(); // reserve send order NOW, so a late-resolving route keeps its place
|
|
11000
|
-
// Clear the box in place — keep it FOCUSED, not blurred
|
|
11001
|
-
//
|
|
11100
|
+
// Clear the box in place — keep it FOCUSED, not blurred. The draft must be dropped from
|
|
11101
|
+
// SA.draft as well, or the next rebuild would re-populate the box from the sent text.
|
|
11102
|
+
delete SA.draft[goalId];
|
|
11002
11103
|
document.querySelectorAll(`#seeall .sa-item[data-gid="${goalId}"] .sa-chat`).forEach((inp) => { inp.value = ''; });
|
|
11003
11104
|
delete SA.attachments[goalId]; saPaintAttachments(goalId);
|
|
11004
11105
|
// 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