@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 +66 -27
- package/engine/lib.mjs +22 -4
- package/engine/server.mjs +8 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
-
//
|
|
9030
|
-
//
|
|
9031
|
-
//
|
|
9032
|
-
//
|
|
9033
|
-
//
|
|
9034
|
-
//
|
|
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
|
-
|
|
9040
|
-
|
|
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
|
|
2158
|
-
//
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
6001
|
+
if (wantsPullRequest(text)) goal.wantsPR = true;
|
|
6000
6002
|
goal.status = result.status;
|
|
6001
6003
|
saveGoal(goal);
|
|
6002
6004
|
// rescope はワーカーを起こさない——「いったん止めて考え直す」なので、走り出したら
|
package/package.json
CHANGED