@galda/cli 0.10.99 → 0.10.100

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
@@ -3475,6 +3475,28 @@ function restoreFocusedAnswerInput(container, attr, saved){
3475
3475
  el.focus();
3476
3476
  try { el.setSelectionRange(saved.start, saved.end); } catch { /* input type may not support it */ }
3477
3477
  }
3478
+ // The capture/restore pair above still loses an in-progress Japanese/Chinese/Korean
3479
+ // IME conversion: rebuilding the DOM node while the browser has an open composition
3480
+ // session aborts that session outright, so the half-typed conversion is dropped even
3481
+ // though the plain .value is restored. Track composition state at the document level
3482
+ // (delegated, since the actual input node gets destroyed and recreated every rebuild)
3483
+ // and let callers skip the destructive innerHTML rebuild while composing, retrying
3484
+ // once composition ends.
3485
+ let composingAnswerInput = null; // { attr, id } while a qansinput/fsansinput IME session is open
3486
+ document.addEventListener('compositionstart', (e) => {
3487
+ const attr = e.target?.getAttribute?.('data-qansinput') != null ? 'data-qansinput'
3488
+ : e.target?.getAttribute?.('data-fsansinput') != null ? 'data-fsansinput' : null;
3489
+ if (attr) composingAnswerInput = { attr, id: e.target.getAttribute(attr) };
3490
+ });
3491
+ document.addEventListener('compositionend', (e) => {
3492
+ if (!composingAnswerInput) return;
3493
+ if (e.target?.getAttribute?.(composingAnswerInput.attr) !== composingAnswerInput.id) return;
3494
+ composingAnswerInput = null;
3495
+ // Catch up on whatever rebuild(s) were skipped mid-composition.
3496
+ if (typeof renderTasks === 'function') renderTasks();
3497
+ if (typeof renderFsFeed === 'function') renderFsFeed();
3498
+ });
3499
+ function answerInputComposing(attr){ return composingAnswerInput?.attr === attr; }
3478
3500
 
3479
3501
  const STATUS_LABEL = {
3480
3502
  sending: 'Sending…', stacked: 'Queued', planning: 'Planning…', queued: 'To do', running: 'Working…', needsInput: 'Needs input', needsApproval: 'Needs you',
@@ -4807,6 +4829,7 @@ function renderTasks(){
4807
4829
  <div class="cond">shelved — press Start to queue it</div></div>
4808
4830
  <button type="button" class="prmini pstart" data-pstart="${g.id}">Start</button>
4809
4831
  <button type="button" class="pdel" data-pdel="${g.id}" title="Delete">✕</button></div>`).join('');
4832
+ if (answerInputComposing('data-qansinput')) return; // mid-IME conversion — rebuilding now would abort it; compositionend re-runs this
4810
4833
  const savedQAns = captureFocusedAnswerInput($('tasklist'), 'data-qansinput');
4811
4834
  $('tasklist').innerHTML =
4812
4835
  sec('Needs you', needsYou, 'needsyou')
@@ -6768,6 +6791,7 @@ function renderFsFeed(){
6768
6791
  $('fsRoot').classList.toggle('clawdhero', !hasWork);
6769
6792
  const sc = feed.parentNode;
6770
6793
  const nearBottom = sc.scrollHeight - sc.clientHeight - sc.scrollTop <= 80;
6794
+ if (answerInputComposing('data-fsansinput')) return; // mid-IME conversion — rebuilding now would abort it; compositionend re-runs this
6771
6795
  const savedFsAns = captureFocusedAnswerInput(feed, 'data-fsansinput');
6772
6796
  feed.innerHTML = hasWork ? out : '';
6773
6797
  if (nearBottom) sc.scrollTop = sc.scrollHeight;
@@ -8517,15 +8541,40 @@ window.addEventListener('focus', () => { if (signinPending) refreshAfterSignin()
8517
8541
 
8518
8542
  // When the served UI is newer than the one this tab is running (hot-copy
8519
8543
  // or release), reload in place — drafts survive via localStorage.
8544
+ //
8545
+ // A new task/goal running elsewhere routinely touches app/index.html's mtime
8546
+ // (its own edit, or a hot-copy after another lane's merge) — that alone used
8547
+ // to force this reload on every open tab, mid-review, wiping the open Review
8548
+ // dialog with no way back in (Masa 2026-07-29). Two fixes: (1) never reload
8549
+ // while the Review dialog (SA.open) is up — remember a pending version and
8550
+ // apply it the moment the dialog closes instead; (2) whichever card was open
8551
+ // is saved to localStorage first, so if a reload does happen while reviewing,
8552
+ // boot() reopens the same card instead of dropping the user back at nothing.
8553
+ let _pendingUiVersion = null;
8520
8554
  function checkUiVersion(v){
8521
8555
  if (!v) return;
8522
8556
  if (window.__uiv && window.__uiv !== v) {
8523
- try { localStorage.setItem('draft', $('input').value ?? ''); } catch {}
8557
+ if (typeof SA !== 'undefined' && SA.open) { _pendingUiVersion = v; return; }
8558
+ try {
8559
+ localStorage.setItem('draft', $('input').value ?? '');
8560
+ if (typeof SA !== 'undefined' && SA.open) {
8561
+ localStorage.setItem('reopenReview', JSON.stringify({ goalId: SA.solo || SA.focusGid || SA.cur, solo: !!SA.solo }));
8562
+ } else {
8563
+ localStorage.removeItem('reopenReview');
8564
+ }
8565
+ } catch {}
8524
8566
  location.reload();
8525
8567
  return;
8526
8568
  }
8527
8569
  window.__uiv = v;
8528
8570
  }
8571
+ // Called from saClose() so a version that arrived mid-review gets applied
8572
+ // (reloaded) right after the user is done, instead of never at all.
8573
+ function applyPendingUiVersionIfAny(){
8574
+ if (_pendingUiVersion == null) return;
8575
+ const v = _pendingUiVersion; _pendingUiVersion = null;
8576
+ checkUiVersion(v);
8577
+ }
8529
8578
 
8530
8579
  async function refresh(){
8531
8580
  try {
@@ -8823,6 +8872,13 @@ async function boot(){
8823
8872
  if (window.__GALDA_DISCONNECTED) return bootDisconnected(window.__GALDA_DISCONNECTED);
8824
8873
  $('input').value = localStorage.getItem('draft') ?? '';
8825
8874
  await refresh();
8875
+ // Reopen the Review dialog a version-triggered reload had to close mid-review
8876
+ // (see checkUiVersion) so the user lands back where they were, not on the board.
8877
+ try {
8878
+ const reopen = JSON.parse(localStorage.getItem('reopenReview') ?? 'null');
8879
+ localStorage.removeItem('reopenReview');
8880
+ if (reopen && typeof saOpen === 'function') saOpen(reopen.goalId, reopen.solo);
8881
+ } catch {}
8826
8882
  // The folder sits above the composer and is read on every screen, so its list is
8827
8883
  // fetched once at boot rather than on first open: without HOME the bar can only
8828
8884
  // print the absolute path (measured — it read "/var/folders/…/agent-manager"
@@ -9026,32 +9082,15 @@ function resolveComposer(raw){
9026
9082
  review: !!(msg.review || parsed.review),
9027
9083
  };
9028
9084
  }
9029
- // Feature A (ONE-CONVERSATION §1.2/§1.4 "1本の会話"): the composer CONTINUES the
9030
- // goal you're focused on (state.selectedGoal) instead of always making a new goal
9031
- // like Claude Code, where you keep talking to the same session. It binds only
9032
- // when that goal is in the ACTIVE project AND in a repliable status: a review goal
9033
- // routes to the AI-decide reply (continue/rescope/split/answer), an in-flight or
9034
- // finished-rework goal takes a plain follow-up. Anything else (done, needsInput,
9035
- // planning/stacked, or nothing selected) → new-goal mode, exactly as before.
9036
- const REPLY_AUTO = new Set(['review']);
9037
- const REPLY_PLAIN = new Set(['running', 'partial', 'failed', 'interrupted', 'blocked']);
9085
+ // The centre composer is always a NEW task. `selectedGoal` is viewing state: using
9086
+ // it as an implicit reply target made a request typed while reading Review #856 get
9087
+ // appended to that unfinished goal. Replying is intentionally available only from
9088
+ // the goal/review card's dedicated Reply / Fix controls, whose destination is shown
9089
+ // next to the control. Never infer a delivery destination from what happens to be
9090
+ // open in the centre lane.
9038
9091
  function replyBinding(){
9039
- const id = state.selectedGoal;
9040
- if (id == null) return null;
9041
- // Focus and reply-target are separate concepts. A Review reply keeps this goal
9042
- // selected so its live work remains in the centre feed, while the composer goes
9043
- // back to new-goal mode and shows no stale “Replying to” chip.
9044
- if (state.composerDetachedGoal === id) return null;
9045
- const g = state.goals.find((x) => x.id === id);
9046
- if (!g || g.projectId !== state.active) return null; // stale after a project switch → new-goal mode
9047
- // needsInput has NO worker session yet (planGoal() sets it before any task/worker
9048
- // exists — see engine/server.mjs) — there's no "conversation" to resume via /reply
9049
- // (that endpoint 409s on needsInput; engine/lib.mjs:3010). Binding here routes to
9050
- // the SAME /answer endpoint the question card's own free-text box already uses
9051
- // (fsAnswerQuestion), so the composer becomes a second way to answer, not a new path.
9052
- if (g.status === 'needsInput' && g.question) return { goal: g, mode: 'answer' };
9053
- if (REPLY_AUTO.has(g.status)) return { goal: g, mode: 'auto' };
9054
- if (REPLY_PLAIN.has(g.status)) return { goal: g, mode: 'plain' };
9092
+ // Deliberately do not bind the main composer. Keep this one routing boundary so
9093
+ // a future UI change cannot accidentally reintroduce selectedGoal -> /reply.
9055
9094
  return null;
9056
9095
  }
9057
9096
  async function send(){
@@ -10529,7 +10568,7 @@ function saOpen(focusGoalId, solo = false){
10529
10568
  saFetchDiffs();
10530
10569
  saEnhanceShotBoxes();
10531
10570
  }
10532
- function saClose(){ SA.open = false; SA.solo = null; SA.focusGid = null; SA.lastMarkup = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } $('seeall').classList.remove('open'); }
10571
+ function saClose(){ SA.open = false; SA.solo = null; SA.focusGid = null; SA.lastMarkup = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } $('seeall').classList.remove('open'); applyPendingUiVersionIfAny(); }
10533
10572
  // Continuous review (Masa 2026-07-08): move a persistent "current" card with
10534
10573
  // ↑↓ and auto-advance to the next pending after each verdict — same screen, same
10535
10574
  // Ledger, so approving flows without hunting for the next one.
package/engine/lib.mjs CHANGED
@@ -2148,16 +2148,34 @@ export function wantsPullRequest(text) {
2148
2148
  return /プルリク|pull\s*request|(^|[^A-Za-z])PR([^A-Za-z]|$)/i.test(String(text ?? ''));
2149
2149
  }
2150
2150
 
2151
+ // Auto delivery must distinguish a request to CHANGE the connected project
2152
+ // from a request to THINK about it. A project-level PR preference remains the
2153
+ // user's strongest default, and an explicit Deliverable choice below always
2154
+ // wins. This only prevents the silent "implemented, locally previewed, but no
2155
+ // PR" outcome for ordinary implementation requests in projects that have not
2156
+ // configured that preference yet.
2157
+ export function isImplementationRequest(text) {
2158
+ const value = String(text ?? '');
2159
+ const implementation = /実装|修正|直して|直す|不具合|バグ|追加|変更|作って|できるように|直したい|fix\b|implement\b|build\b|add\b|change\b|bug\b/i;
2160
+ const explorationOnly = /リサーチ|調査|分析|アイデア|案(?:を|が|は|ください)|提案|比較|相談|教えて|説明|要約|デザイン|モック|ワイヤーフレーム|資料|スライド|レポート|ドキュメント|research\b|analysis\b|brainstorm\b|ideas?\b|suggest(?:ion)?\b|design\b|mockup\b|wireframe\b|slides?\b|report\b/i;
2161
+ // "実装案を出して" is exploration; "案3を実装して" is implementation.
2162
+ return implementation.test(value) && !(explorationOnly.test(value) && !/実装|修正|直して|直す|追加|変更|fix\b|implement\b|build\b|add\b|change\b/i.test(value));
2163
+ }
2164
+
2151
2165
  // Task 46: the "Deliverable" toggle (pr | none | auto) on task creation and
2152
2166
  // editing. An explicit 'pr'/'none' always wins over the text heuristic, so
2153
2167
  // a user's choice survives editing the goal's text afterwards. `projectDefault`
2154
2168
  // (task 45: a project's reviewDefinition.defaultWantsPR, edited from the same
2155
2169
  // settings panel as the review definition) is only consulted for 'auto' when
2156
2170
  // the text itself gives no signal either way — an explicit "PRを作って" in the
2157
- // text still wins even if the project's default is off, matching the
2158
- // pre-task-45 behavior that new callers who omit this argument keep exactly.
2159
- export function resolveWantsPR(pr, text, projectDefault = false) {
2160
- return pr === 'pr' ? true : pr === 'none' ? false : (wantsPullRequest(text) || !!projectDefault);
2171
+ // text still wins even if the project's default is off. `preferenceKnown`
2172
+ // distinguishes an intentional per-project "No PR" from an older project
2173
+ // which has never made a delivery choice. Only the latter gets the helpful
2174
+ // implementation-request fallback.
2175
+ export function resolveWantsPR(pr, text, projectDefault = false, preferenceKnown = false) {
2176
+ if (pr === 'pr') return true;
2177
+ if (pr === 'none') return false;
2178
+ return wantsPullRequest(text) || !!projectDefault || (!preferenceKnown && isImplementationRequest(text));
2161
2179
  }
2162
2180
 
2163
2181
  // Task 90: which entry point created a goal (Web chat / MCP / future API),
package/engine/server.mjs CHANGED
@@ -25,6 +25,7 @@ import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refineP
25
25
  import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
26
26
  import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
27
27
  import { createSerialQueue } from './lib.mjs';
28
+ import { wantsPullRequest } from './lib.mjs';
28
29
  import { parseRequirementEvidence, unmappedChangedFiles, previewOpensSomethingElse } from './lib.mjs';
29
30
  import { isPrClosed } from './lib.mjs';
30
31
  import { collectProjectRules } from './lib.mjs';
@@ -2447,7 +2448,7 @@ function createSplitGoal(parent, text, identity, targetProjectId = parent.projec
2447
2448
  if (!targetProject) throw new Error(`unknown target project: ${targetProjectId}`);
2448
2449
  const goal = {
2449
2450
  id: nextId++, projectId: targetProjectId, text: text.slice(0, 8000), status: 'planning',
2450
- wantsPR: resolveWantsPR('auto', text, getReviewDefinition(targetProjectId).defaultWantsPR),
2451
+ wantsPR: resolveWantsPR('auto', text, getReviewDefinition(targetProjectId).defaultWantsPR, !!targetProject.reviewPrefAsked),
2451
2452
  model: parent.model, effort: parent.effort, agent: parent.agent, mode: 'auto',
2452
2453
  skill: null, images: [], source: resolveGoalSource('app'), sourceRef: null,
2453
2454
  executionPlan: buildExecutionPlan({ projectId: targetProjectId, text, previousSession: false }),
@@ -4832,7 +4833,7 @@ const server = createServer(async (req, res) => {
4832
4833
  approvalDecision: null,
4833
4834
  reviewOnly: review ? true : undefined,
4834
4835
  note: review && typeof note === 'string' && note.trim() ? note.trim().slice(0, 8000) : undefined,
4835
- wantsPR: (review || approval) ? Boolean(importedPr) : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
4836
+ wantsPR: (review || approval) ? Boolean(importedPr) : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR, !!project.reviewPrefAsked),
4836
4837
  model: workerModel(goalAgent, model),
4837
4838
  effort: workerEffort(goalAgent, effort),
4838
4839
  agent: goalAgent,
@@ -4950,7 +4951,8 @@ const server = createServer(async (req, res) => {
4950
4951
  if (promote) goal.prio = ++prioCounter;
4951
4952
  if (text?.trim()) {
4952
4953
  goal.text = text.trim().slice(0, 8000);
4953
- goal.wantsPR = resolveWantsPR(pr, goal.text, getReviewDefinition(goal.projectId).defaultWantsPR);
4954
+ const project = projects.find((p) => p.id === goal.projectId);
4955
+ goal.wantsPR = resolveWantsPR(pr, goal.text, getReviewDefinition(goal.projectId).defaultWantsPR, !!project?.reviewPrefAsked);
4954
4956
  }
4955
4957
  saveGoal(goal);
4956
4958
  json(res, 200, goal);
@@ -5521,7 +5523,7 @@ const server = createServer(async (req, res) => {
5521
5523
  send({ ev: 'review-definition', projectId: project.id, reviewDefinition: merged.definition });
5522
5524
  }
5523
5525
  }
5524
- goal.wantsPR = resolveWantsPR(undefined, goal.text, patch.defaultWantsPR);
5526
+ goal.wantsPR = resolveWantsPR(undefined, goal.text, patch.defaultWantsPR, true);
5525
5527
  goal.question = null;
5526
5528
  goal.clarified = true;
5527
5529
  goal.status = 'stacked';
@@ -5587,7 +5589,7 @@ const server = createServer(async (req, res) => {
5587
5589
  // both asked twice and got no PR either time.
5588
5590
  // Upgrade only: this can turn the deliverable on, never off, so a later
5589
5591
  // unrelated reply cannot silently withdraw a PR the person already asked for.
5590
- if (resolveWantsPR('auto', text, false) && goal.wantsPR !== true) {
5592
+ if (wantsPullRequest(text) && goal.wantsPR !== true) {
5591
5593
  goal.wantsPR = true;
5592
5594
  saveGoal(goal);
5593
5595
  }
@@ -5996,7 +5998,7 @@ async function answerGoalQuestion(goal, question) {
5996
5998
  // (rescope) or while asking a question (answer) is still a PR they asked
5997
5999
  // for, and it must survive until close-out rather than being dropped
5998
6000
  // because this particular reply happened not to wake a worker.
5999
- if (resolveWantsPR('auto', text, false)) goal.wantsPR = true;
6001
+ if (wantsPullRequest(text)) goal.wantsPR = true;
6000
6002
  goal.status = result.status;
6001
6003
  saveGoal(goal);
6002
6004
  // rescope はワーカーを起こさない——「いったん止めて考え直す」なので、走り出したら
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.99",
3
+ "version": "0.10.100",
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": {