@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 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
- $('gdTitle').textContent = isReview ? 'Review' : 'Attention';
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…' : 'Tell the worker what to try next…';
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
- const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, msgCounts: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
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
- if (['blocked', 'partial', 'failed', 'interrupted'].includes(g.status)) {
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
- const focusedReplyGoal = document.activeElement?.matches?.('#seeall .sa-chat')
10681
- ? document.activeElement.dataset.goal
10682
- : null;
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
- // A relevant status change may legitimately rebuild the card. Restore the
10814
- // empty reply box's focus afterwards; non-empty input is protected earlier
10815
- // by saSync and is never rebuilt at all.
10816
- if (focusedReplyGoal != null) S.querySelector(`.sa-chat[data-goal="${focusedReplyGoal}"]`)?.focus({ preventScroll: true });
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
- const ae = document.activeElement;
10825
- // Do not erase text while the reviewer is typing. Once Send clears the box,
10826
- // focus alone must not freeze To Do / Doing / Review status updates.
10827
- if (ae && $('seeall').contains(ae) && ae.tagName === 'INPUT' && ae.value) return;
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
- SA.detOpen = new Set();
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 (saSync skips a rebuild while an
11001
- // input in #seeall has focus, which is what keeps an SSE tick from flashing the panel).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.110",
3
+ "version": "0.10.112",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {