@galda/cli 0.10.118 → 0.10.119
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 +33 -6
- package/engine/server.mjs +44 -13
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -328,6 +328,11 @@
|
|
|
328
328
|
hairline-separated from the detected list, matching .appopt grammar. */
|
|
329
329
|
.ctxmenu .appopt.ctxopen{flex-direction:row;align-items:center;gap:7px;border-top:1px solid var(--hair);margin-top:4px;padding-top:8px}
|
|
330
330
|
.ctxmenu .appopt.ctxopen svg{width:14px;height:14px;flex:0 0 14px;stroke:currentColor;stroke-width:1.7;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
331
|
+
/* Working: the folder icon becomes a spinner in its own place, so the row
|
|
332
|
+
itself says a chooser is opening rather than only the line of text below it.
|
|
333
|
+
Reuses @keyframes spin — no new motion vocabulary (DESIGN-RULES §5.5). */
|
|
334
|
+
.ctxmenu .appopt.ctxopen .ctxspin{width:12px;height:12px;flex:0 0 14px;margin:1px;border-radius:50%;
|
|
335
|
+
border:1.5px solid var(--ink3);border-top-color:transparent;animation:spin 1s linear infinite}
|
|
331
336
|
.ctxmenu .appopt.ctxopen .nm{color:var(--ink);font-size:12.5px;line-height:1.3}
|
|
332
337
|
.appopt{display:flex;align-items:center;gap:7px;height:28px;padding:0 8px;border:0;border-radius:6px;background:transparent;
|
|
333
338
|
color:var(--ink2);font:inherit;font-size:12px;cursor:pointer;text-align:left;width:100%}
|
|
@@ -529,15 +534,18 @@
|
|
|
529
534
|
.badge.upnext{color:var(--invink);background:rgba(var(--tint),.75);border:0;font-weight:700}
|
|
530
535
|
.retrybtn{margin-left:6px;padding:1.5px 9px;border:1px solid var(--hair2);border-radius:6px;background:transparent;color:var(--ink);font-size:10.5px;cursor:pointer}
|
|
531
536
|
.retrybtn.skip{color:var(--ink3)} /* Dismiss reads as the secondary/clear action next to Retry */
|
|
532
|
-
/* Queue priority badge (To do rows only) — click opens a
|
|
533
|
-
No new hues:
|
|
534
|
-
on the neutral ink scale, so priority never reads as another rainbow.
|
|
537
|
+
/* Queue priority badge (To do rows only) — click opens a High/Medium/Low popup.
|
|
538
|
+
No new hues: high borrows the existing --amber attention accent, medium/low
|
|
539
|
+
stay on the neutral ink scale, so priority never reads as another rainbow.
|
|
540
|
+
One mono glyph in a 22px square, unchanged since it held 高: the square is
|
|
541
|
+
the shape, H/M/L is what fits in it, and the whole word is in the menu where
|
|
542
|
+
there is room for it. */
|
|
535
543
|
.priosel{position:relative;flex:0 0 auto;align-self:flex-start;margin-top:1px}
|
|
536
544
|
.priobtn{width:22px;height:20px;border:1px solid var(--hair2);border-radius:6px;background:transparent;
|
|
537
545
|
color:var(--ink3);font-family:var(--mono);font-size:10.5px;cursor:pointer;display:grid;place-items:center}
|
|
538
546
|
.priobtn:hover{color:var(--ink);border-color:rgba(var(--tint),.2)}
|
|
539
|
-
.priobtn.prio
|
|
540
|
-
.priobtn.prio
|
|
547
|
+
.priobtn.prio-high{color:var(--amber);border-color:rgba(var(--amber-rgb),.4)}
|
|
548
|
+
.priobtn.prio-medium{color:var(--ink2)}
|
|
541
549
|
.priomenu{position:absolute;top:calc(100% + 4px);right:0;min-width:64px;background:var(--surface);border:1px solid var(--hair2);
|
|
542
550
|
border-radius:9px;padding:4px;display:none;flex-direction:column;gap:1px;box-shadow:0 10px 28px rgba(0,0,0,.5);z-index:50}
|
|
543
551
|
.priomenu.show{display:flex}
|
|
@@ -2923,13 +2931,31 @@
|
|
|
2923
2931
|
/* ---- iOS no-zoom: 16px inputs (extends the shared 16px rule to flagship-only fields) ---- */
|
|
2924
2932
|
body[data-layout="flagship"] .sa-chat,
|
|
2925
2933
|
body[data-layout="flagship"] #fsAskbar input{font-size:16px}
|
|
2926
|
-
/* ---- Ledger (#seeall) —
|
|
2934
|
+
/* ---- Ledger (#seeall) — Claude Code-style mobile reading sheet.
|
|
2935
|
+
A tapped Review should land at the top of the phone viewport, with the
|
|
2936
|
+
content column using the width instead of spending it on desktop gutters. ---- */
|
|
2937
|
+
body[data-layout="flagship"] #seeall.open{place-items:start center;z-index:120;pointer-events:auto}
|
|
2927
2938
|
body[data-layout="flagship"] #seeall .sa-panel{width:100%!important;max-width:100%!important;
|
|
2928
|
-
margin-top:
|
|
2929
|
-
|
|
2939
|
+
margin-top:calc(env(safe-area-inset-top) + 6px)!important;
|
|
2940
|
+
max-height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 12px)!important;
|
|
2941
|
+
border-radius:18px 18px 0 0!important}
|
|
2942
|
+
body[data-layout="flagship"] #seeall .sa-hd{padding:14px 18px 10px!important}
|
|
2930
2943
|
body[data-layout="flagship"] #seeall .sa-hd .keys{display:none} /* keyboard shortcuts — no keyboard on a phone */
|
|
2931
|
-
body[data-layout="flagship"] #seeall .sa-body{padding:
|
|
2944
|
+
body[data-layout="flagship"] #seeall .sa-body{padding:10px 8px 16px}
|
|
2932
2945
|
body[data-layout="flagship"] #seeall .sa-item{width:100%}
|
|
2946
|
+
body[data-layout="flagship"] #seeall .sa-item.redesign{padding:22px 18px 28px!important}
|
|
2947
|
+
body[data-layout="flagship"] #seeall .sa-body>.sa-item.redesign+.sa-item.redesign{padding-top:30px!important}
|
|
2948
|
+
body[data-layout="flagship"] #seeall .sa-panel .sa-hd:has(~ .sa-body .sa-item.redesign){padding:14px 18px 10px!important}
|
|
2949
|
+
body[data-layout="flagship"] #seeall .revrail.show{width:9px;margin-right:7px}
|
|
2950
|
+
body[data-layout="flagship"] #seeall .revrail button{font-size:0;padding:6px 0}
|
|
2951
|
+
body[data-layout="flagship"] #seeall .revrail button .bar{width:3px;height:18px}
|
|
2952
|
+
body[data-layout="flagship"] #seeall .revrail button.on .bar{height:28px}
|
|
2953
|
+
body[data-layout="flagship"] #seeall .redesign .rtitle{font-size:17px;line-height:1.32;letter-spacing:0}
|
|
2954
|
+
body[data-layout="flagship"] #seeall .redesign .rsec{margin-top:22px!important}
|
|
2955
|
+
body[data-layout="flagship"] #seeall .redesign .rsec-h{font-size:15px;letter-spacing:0}
|
|
2956
|
+
body[data-layout="flagship"] #seeall .redesign .rbody,
|
|
2957
|
+
body[data-layout="flagship"] #seeall .redesign .rreport{font-size:15px;line-height:1.72;letter-spacing:0;max-width:none}
|
|
2958
|
+
body[data-layout="flagship"] #seeall .redesign .rreport code{font-size:13px}
|
|
2933
2959
|
body[data-layout="flagship"] #seeall .sa-act{flex-wrap:wrap;row-gap:8px}
|
|
2934
2960
|
body[data-layout="flagship"] #seeall .sa-chatwrap{flex:1 1 100%;order:3;min-width:0}
|
|
2935
2961
|
body[data-layout="flagship"] #seeall .sa-app,
|
|
@@ -2963,8 +2989,15 @@
|
|
|
2963
2989
|
max-width:62%;justify-content:center;margin:0}
|
|
2964
2990
|
/* Review/To Do opener (right) = plain ☰ icon (Masa 2026-07-18: ボタン化はやめ、三本線だけ・
|
|
2965
2991
|
タップで To Do/Review ドロワー). The base .fshamb rule above already renders the ☰ svg. */
|
|
2966
|
-
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb
|
|
2967
|
-
|
|
2992
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb{display:none}
|
|
2993
|
+
/* Masa 2026-07-31: たまったレビューが右上から分からない → ☰ の右肩に赤ドット。
|
|
2994
|
+
件数はJS(renderFsReview)がfshamb-nに書き込み済みだったが display:none で常に非表示だった。
|
|
2995
|
+
件数の有無だけをJSからのtext contentで判定して出し分ける(空なら消える)。 */
|
|
2996
|
+
body[data-layout="flagship"] #fsRoot .fshamb{position:relative}
|
|
2997
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n{display:none;position:absolute;top:2px;right:2px;
|
|
2998
|
+
min-width:9px;height:9px;padding:0;border-radius:50%;background:var(--danger);
|
|
2999
|
+
box-shadow:0 0 0 2px var(--panel);font-size:0;line-height:0;color:transparent}
|
|
3000
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n:not(:empty){display:block}
|
|
2968
3001
|
/* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
|
|
2969
3002
|
が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
|
|
2970
3003
|
body[data-layout="flagship"] #fsRoot .fsrail{background:transparent;box-shadow:none}
|
|
@@ -2984,9 +3017,10 @@
|
|
|
2984
3017
|
margin:0 0 4px;padding:0 2px 10px;gap:9px;border-bottom:1px solid var(--hair)}
|
|
2985
3018
|
/* H25③: See-all ledger → scrim + inset panel (a full-bleed sheet let translucent themes show through). */
|
|
2986
3019
|
body[data-layout="flagship"] #seeall{background:rgba(9,12,20,.55)}
|
|
3020
|
+
body[data-layout="flagship"] #seeall.open{z-index:120}
|
|
2987
3021
|
body[data-layout="flagship"] #seeall .sa-panel{width:96%!important;max-width:96%!important;
|
|
2988
3022
|
max-height:calc(100dvh - env(safe-area-inset-top) - 22px)!important;
|
|
2989
|
-
margin-top:calc(env(safe-area-inset-top) +
|
|
3023
|
+
margin-top:calc(env(safe-area-inset-top) + 6px)!important;border-radius:18px!important}
|
|
2990
3024
|
}
|
|
2991
3025
|
/* Billing UI (H16/H22) — self-serve plan. Tokens only; red is a line/label,
|
|
2992
3026
|
never a fill (DESIGN-RULES §5.6), and it is --danger (the app's real token —
|
|
@@ -3710,18 +3744,34 @@ function pickColumnLabel(columns, bucket, fallback){
|
|
|
3710
3744
|
}
|
|
3711
3745
|
function columnsFor(projectId){ return state.workflowColumns[projectId] ?? DEFAULT_WORKFLOW_COLUMNS; }
|
|
3712
3746
|
|
|
3713
|
-
// Mirrors engine/lib.mjs TASK_PRIORITIES — kept in sync
|
|
3714
|
-
// comment on DEFAULT_WORKFLOW_COLUMNS above. Queue execution
|
|
3715
|
-
//
|
|
3716
|
-
// sortQueueByPriority) — this badge is only for To do (queued)
|
|
3717
|
-
// that ordering actually matters.
|
|
3718
|
-
|
|
3747
|
+
// Mirrors engine/lib.mjs TASK_PRIORITIES / normalizePriority — kept in sync
|
|
3748
|
+
// manually, see the comment on DEFAULT_WORKFLOW_COLUMNS above. Queue execution
|
|
3749
|
+
// order is high→medium→low, same-priority ties keep creation/drag order
|
|
3750
|
+
// (server.mjs's sortQueueByPriority) — this badge is only for To do (queued)
|
|
3751
|
+
// rows, where that ordering actually matters.
|
|
3752
|
+
//
|
|
3753
|
+
// The values were 高/中/低 until 2026-07-31. Nothing migrates what is already on
|
|
3754
|
+
// disk, so every task written before then still says 高 and must still render as
|
|
3755
|
+
// what it is: reading only the new list would have shown it as Medium — the board
|
|
3756
|
+
// lying about the order the queue really runs in.
|
|
3757
|
+
const TASK_PRIORITIES = ['high', 'medium', 'low'];
|
|
3758
|
+
const LEGACY_PRIORITIES = { 高: 'high', 中: 'medium', 低: 'low' };
|
|
3759
|
+
// One glyph for the 22px square, the whole word for the menu that has room.
|
|
3760
|
+
const PRIORITY_GLYPH = { high: 'H', medium: 'M', low: 'L' };
|
|
3761
|
+
const PRIORITY_NAME = { high: 'High', medium: 'Medium', low: 'Low' };
|
|
3762
|
+
function normalizePriority(value){
|
|
3763
|
+
const v = String(value ?? '').trim();
|
|
3764
|
+
if (TASK_PRIORITIES.includes(v)) return v;
|
|
3765
|
+
if (LEGACY_PRIORITIES[v]) return LEGACY_PRIORITIES[v];
|
|
3766
|
+
const lower = v.toLowerCase();
|
|
3767
|
+
return TASK_PRIORITIES.includes(lower) ? lower : 'medium';
|
|
3768
|
+
}
|
|
3719
3769
|
function prioBadge(t){
|
|
3720
|
-
const p =
|
|
3770
|
+
const p = normalizePriority(t.priority);
|
|
3721
3771
|
return `<div class="priosel" data-priosel="${t.id}">
|
|
3722
|
-
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority">${p}</button>
|
|
3772
|
+
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority — ${PRIORITY_NAME[p]}">${PRIORITY_GLYPH[p]}</button>
|
|
3723
3773
|
<div class="priomenu" data-priomenu="${t.id}">${TASK_PRIORITIES.map((x) =>
|
|
3724
|
-
`<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${x}</button>`).join('')}</div>
|
|
3774
|
+
`<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${PRIORITY_NAME[x]}</button>`).join('')}</div>
|
|
3725
3775
|
</div>`;
|
|
3726
3776
|
}
|
|
3727
3777
|
|
|
@@ -9018,6 +9068,12 @@ function connectSSE(){
|
|
|
9018
9068
|
// fsActiveContext() uses to pick which running goal is "now" when several run
|
|
9019
9069
|
// in parallel (§1.5-1: never anchor on stale work when something is live).
|
|
9020
9070
|
else if (m.ev === 'act') { (state.act[m.taskId] ??= []).push(m.line); state.act[m.taskId] = state.act[m.taskId].slice(-1000); state.actAt[m.taskId] = Date.now(); scheduleActRender(); return; }
|
|
9071
|
+
// The agent talking, as opposed to the agent working. Everything above this
|
|
9072
|
+
// line is machinery (a status, a tool call, a to-do list); this is a sentence
|
|
9073
|
+
// somebody wrote to the person waiting, so it goes where the person is
|
|
9074
|
+
// looking — the same turn treatment a chat reply gets, because it is the same
|
|
9075
|
+
// thing. Masa's friend waited three minutes for exactly this and gave up.
|
|
9076
|
+
else if (m.ev === 'say') { sayFromAgent(m); return; }
|
|
9021
9077
|
else if (m.ev === 'todos') state.todos[m.taskId] = m.todos ?? [];
|
|
9022
9078
|
else if (m.ev === 'columns') state.workflowColumns[m.projectId] = m.columns;
|
|
9023
9079
|
else if (m.ev === 'review-definition') state.reviewDefinitions[m.projectId] = m.reviewDefinition;
|
|
@@ -9323,6 +9379,25 @@ function typeChatTurn(turn){
|
|
|
9323
9379
|
if (turn.revealed >= full.length) { turn.done = true; stopChatTyper(); renderFsFeed(); }
|
|
9324
9380
|
}, 16);
|
|
9325
9381
|
}
|
|
9382
|
+
// A worker's opening words (engine: `ev:'say'`). Rendered as a spoken turn, typed
|
|
9383
|
+
// out, exactly like the one-shot chat reply — Galda writes nothing of its own
|
|
9384
|
+
// here and picks nothing: the sentence is the agent's, and this only puts it
|
|
9385
|
+
// where it can be read. The board layout has no centre feed, so it stays in the
|
|
9386
|
+
// run log there rather than being invented somewhere new.
|
|
9387
|
+
function sayFromAgent({ goalId, text, agent }){
|
|
9388
|
+
const body = String(text ?? '').trim();
|
|
9389
|
+
if (!body || state.layout !== 'flagship') return;
|
|
9390
|
+
const goal = state.goals.find((g) => g.id === goalId);
|
|
9391
|
+
const projectId = goal?.projectId ?? state.active;
|
|
9392
|
+
const turns = (state.chatTurns[projectId] ||= []);
|
|
9393
|
+
// A reconnecting stream can hand us the same opening twice; a person reading it
|
|
9394
|
+
// twice would think the agent had started over.
|
|
9395
|
+
if (turns.some((t) => t.role === 'clawd' && t.text === body)) return;
|
|
9396
|
+
const turn = { role: 'clawd', text: body, revealed: 0, done: false, agent, ts: Date.now() };
|
|
9397
|
+
turns.push(turn);
|
|
9398
|
+
renderFsFeed();
|
|
9399
|
+
if (projectId === state.active) typeChatTurn(turn); else { turn.revealed = body.length; turn.done = true; }
|
|
9400
|
+
}
|
|
9326
9401
|
// A new send starts: finish any in-flight reply cleanly (complete the model turn +
|
|
9327
9402
|
// drop the caret, hide the board bar) so a half-typed reply never lingers.
|
|
9328
9403
|
function finalizeChatTyping(){
|
|
@@ -11510,7 +11585,7 @@ function renderCtxBar(){
|
|
|
11510
11585
|
.join('');
|
|
11511
11586
|
// Redrawing the menu must not hand back a second chooser: carry the disabled
|
|
11512
11587
|
// state that setFolderPickerBusy() put on the live button.
|
|
11513
|
-
const openRow = `<button type="button" class="appopt ctxopen${folderPickerBusy ? ' busy' : ''}" id="ctxopenfolder"${folderPickerBusy ? ' disabled' : ''} title="Open the OS folder chooser"
|
|
11588
|
+
const openRow = `<button type="button" class="appopt ctxopen${folderPickerBusy ? ' busy' : ''}" id="ctxopenfolder"${folderPickerBusy ? ' disabled' : ''} title="Open the OS folder chooser">${folderRowInner(folderPickerBusy)}</button>`;
|
|
11514
11589
|
menu.innerHTML = `${opts}${openRow}<div class="ctxmsg" id="ctxdirmsg"></div>`;
|
|
11515
11590
|
}
|
|
11516
11591
|
// Workspace mode is fixed to the isolated worktree (clean separate checkout). The
|
|
@@ -11562,17 +11637,37 @@ async function setProjectFolder(dir){
|
|
|
11562
11637
|
// request stays in flight for as long as the dialog is up, so "a request is out"
|
|
11563
11638
|
// is exactly "a dialog is open" — no timer to guess with.
|
|
11564
11639
|
let folderPickerBusy = false;
|
|
11640
|
+
const FOLDER_GLYPH = '<svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M12 11v6M9 14h6"/></svg>';
|
|
11641
|
+
// While a chooser is open the icon spins in its own place. Waiting with nothing
|
|
11642
|
+
// moving is what "it did nothing" looks like — and it is what made Masa's friend
|
|
11643
|
+
// tap ten times (2026-07-31). One source for the row's contents, so the live swap
|
|
11644
|
+
// and the next redraw cannot disagree.
|
|
11645
|
+
function folderRowInner(busy){
|
|
11646
|
+
return `${busy ? '<span class="ctxspin" aria-hidden="true"></span>' : FOLDER_GLYPH}<span class="nm">Open folder…</span>`;
|
|
11647
|
+
}
|
|
11565
11648
|
function setFolderPickerBusy(busy){
|
|
11566
11649
|
folderPickerBusy = busy;
|
|
11567
11650
|
const btn = $('ctxopenfolder');
|
|
11568
|
-
if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); }
|
|
11569
|
-
}
|
|
11651
|
+
if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); btn.innerHTML = folderRowInner(busy); }
|
|
11652
|
+
}
|
|
11653
|
+
// How long to wait before saying the dialog might not be where they are looking.
|
|
11654
|
+
// The engine opens it immediately; on a Mac where the engine was started by launchd
|
|
11655
|
+
// it can open behind everything, and there is nothing on screen to say so — which is
|
|
11656
|
+
// the other half of what happened to Masa's friend. Long enough not to nag someone
|
|
11657
|
+
// who is simply choosing a folder.
|
|
11658
|
+
const FOLDER_CHOOSER_SLOW_MS = 6000;
|
|
11570
11659
|
async function openFolderNative(){
|
|
11571
11660
|
if (folderPickerBusy) return;
|
|
11572
11661
|
const msg = $('ctxdirmsg');
|
|
11573
11662
|
const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
|
|
11574
11663
|
setFolderPickerBusy(true);
|
|
11575
11664
|
say('Opening the folder chooser…');
|
|
11665
|
+
// Still open after a while: say where it might be, and point at the way out that
|
|
11666
|
+
// does not need it. The row stays disabled — a second chooser is the bug this
|
|
11667
|
+
// guard exists to prevent, and offering one here would re-create it.
|
|
11668
|
+
const slow = setTimeout(() => {
|
|
11669
|
+
if (folderPickerBusy) say('Still waiting — the chooser may be behind your other windows. You can also pick a folder from the list above.');
|
|
11670
|
+
}, FOLDER_CHOOSER_SLOW_MS);
|
|
11576
11671
|
try {
|
|
11577
11672
|
const r = await fetch(withKey('/api/choose-folder'), { method: 'POST' });
|
|
11578
11673
|
const body = await r.json().catch(() => ({}));
|
|
@@ -11581,7 +11676,7 @@ async function openFolderNative(){
|
|
|
11581
11676
|
if (body.unsupported) { say('This engine can’t open a folder dialog on this system.', true); return; }
|
|
11582
11677
|
say(body.error || 'Could not open the folder chooser.', true);
|
|
11583
11678
|
} catch { say('Could not reach the engine.', true); }
|
|
11584
|
-
finally { setFolderPickerBusy(false); }
|
|
11679
|
+
finally { clearTimeout(slow); setFolderPickerBusy(false); }
|
|
11585
11680
|
}
|
|
11586
11681
|
$('ctxdirbtn').onclick = (e) => {
|
|
11587
11682
|
e.stopPropagation();
|
|
@@ -12517,7 +12612,13 @@ window.__tbink = (i) => { document.body.dataset.tbink = String(((i | 0) % 3 + 3)
|
|
|
12517
12612
|
raf = 0;
|
|
12518
12613
|
if (document.hidden) { clearFx(); return; }
|
|
12519
12614
|
const t = (performance.now() - t0) / 1000, d = Math.min(2, devicePixelRatio || 1);
|
|
12520
|
-
|
|
12615
|
+
// Clear in backing-store pixels *before* installing the CSS-pixel transform.
|
|
12616
|
+
// Mobile Safari can change its visual viewport as a question answer removes a
|
|
12617
|
+
// card (or as the browser chrome expands). Clearing in transformed viewport
|
|
12618
|
+
// coordinates then leaves pixels from the send ring's former location behind.
|
|
12619
|
+
// The canvas's own dimensions are the only stable full-clear bounds.
|
|
12620
|
+
clearFx();
|
|
12621
|
+
fc.setTransform(d, 0, 0, d, 0, 0);
|
|
12521
12622
|
if (MODE < 10) tick(t); // TB edge anim (optional, off by default = 10)
|
|
12522
12623
|
drawSendRing(t); // send ring (always on while btn mode = 0, independent of TB edge anim)
|
|
12523
12624
|
raf = requestAnimationFrame(loop);
|
package/engine/lib.mjs
CHANGED
|
@@ -1869,8 +1869,8 @@ export function reorderQueue(waiting, ids) {
|
|
|
1869
1869
|
return [...orderByIds(queued, ids), ...rest];
|
|
1870
1870
|
}
|
|
1871
1871
|
|
|
1872
|
-
// Priority queue ordering: '
|
|
1873
|
-
// priority (or an unrecognized value) is treated as '
|
|
1872
|
+
// Priority queue ordering: 'high' runs before 'medium' before 'low'. A task with
|
|
1873
|
+
// no priority (or an unrecognized value) is treated as 'medium', matching the
|
|
1874
1874
|
// default new tasks are created with. Array.prototype.sort is a stable sort
|
|
1875
1875
|
// (guaranteed by the spec), so this never disturbs the relative order tasks
|
|
1876
1876
|
// already have within the same priority — that incoming order already
|
|
@@ -1879,12 +1879,39 @@ export function reorderQueue(waiting, ids) {
|
|
|
1879
1879
|
// for everything it doesn't touch). Callers should feed this the array
|
|
1880
1880
|
// *after* applying a drag reorder, so a drag only ever re-ranks within a
|
|
1881
1881
|
// priority band rather than fighting it.
|
|
1882
|
-
|
|
1883
|
-
|
|
1882
|
+
//
|
|
1883
|
+
// The values were 高/中/低 until 2026-07-31 (Masa: the UI writes its own labels
|
|
1884
|
+
// in English — DESIGN-RULES §5.5). They are stored, sorted on, and sent over the
|
|
1885
|
+
// API, so the old ones cannot simply be swapped out: every task written before
|
|
1886
|
+
// today still says 高 on disk, and a browser tab left open on the old app will
|
|
1887
|
+
// keep PUTting 高 for as long as it stays open. Both are accepted forever and
|
|
1888
|
+
// normalised on the way in; nothing has to be migrated, and nothing that already
|
|
1889
|
+
// exists loses its place in the queue.
|
|
1890
|
+
export const TASK_PRIORITIES = ['high', 'medium', 'low'];
|
|
1891
|
+
const LEGACY_PRIORITIES = { 高: 'high', 中: 'medium', 低: 'low' };
|
|
1892
|
+
export const DEFAULT_PRIORITY = 'medium';
|
|
1893
|
+
|
|
1894
|
+
export function normalizePriority(value) {
|
|
1895
|
+
const v = String(value ?? '').trim();
|
|
1896
|
+
if (TASK_PRIORITIES.includes(v)) return v;
|
|
1897
|
+
if (LEGACY_PRIORITIES[v]) return LEGACY_PRIORITIES[v];
|
|
1898
|
+
const lower = v.toLowerCase();
|
|
1899
|
+
if (TASK_PRIORITIES.includes(lower)) return lower; // 'High' from a hand-written call
|
|
1900
|
+
return DEFAULT_PRIORITY;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Whether a value is one this product accepts at all — the API's gate. Deliberately
|
|
1904
|
+
// wider than TASK_PRIORITIES: an old client sending 高 is not making a mistake.
|
|
1905
|
+
export function isAcceptedPriority(value) {
|
|
1906
|
+
const v = String(value ?? '').trim();
|
|
1907
|
+
return TASK_PRIORITIES.includes(v) || Boolean(LEGACY_PRIORITIES[v]) || TASK_PRIORITIES.includes(v.toLowerCase());
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
const PRIORITY_WEIGHT = { high: 0, medium: 1, low: 2 };
|
|
1884
1911
|
export function sortQueueByPriority(tasks) {
|
|
1885
1912
|
return tasks
|
|
1886
1913
|
.map((t, i) => ({ t, i }))
|
|
1887
|
-
.sort((a, b) => (PRIORITY_WEIGHT[a.t.priority] ?? 1) - (PRIORITY_WEIGHT[b.t.priority] ?? 1) || a.i - b.i)
|
|
1914
|
+
.sort((a, b) => (PRIORITY_WEIGHT[normalizePriority(a.t.priority)] ?? 1) - (PRIORITY_WEIGHT[normalizePriority(b.t.priority)] ?? 1) || a.i - b.i)
|
|
1888
1915
|
.map(({ t }) => t);
|
|
1889
1916
|
}
|
|
1890
1917
|
|
|
@@ -3270,7 +3297,7 @@ export function buildGoalTask(goal, { id, num, passCondition = null, createdAt }
|
|
|
3270
3297
|
mode: goal.mode ?? 'auto', skill: goal.skill ?? null,
|
|
3271
3298
|
workspaceMode: resolveWorkspaceMode(goal.workspaceMode ?? goal.worktree),
|
|
3272
3299
|
worktree: goal.worktree === false ? false : undefined,
|
|
3273
|
-
status: 'queued', createdAt, priority:
|
|
3300
|
+
status: 'queued', createdAt, priority: DEFAULT_PRIORITY,
|
|
3274
3301
|
result: null, changedFiles: [], secs: null, activity: [], proof: null, usage: null,
|
|
3275
3302
|
};
|
|
3276
3303
|
}
|
package/engine/server.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { homedir } from 'node:os';
|
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { bootBanner, bootDestination, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath, hostReadiness, parseRequestedWorkspaceDir } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, DEFAULT_PRIORITY, normalizePriority, isAcceptedPriority, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
25
25
|
import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
|
|
26
26
|
import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
|
|
27
27
|
import { readRelayPref, writeRelayPref, resolveRelayEnabled } from './relay-pref.mjs';
|
|
@@ -2357,7 +2357,7 @@ function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {})
|
|
|
2357
2357
|
title: text.replace(/\s+/g, ' ').slice(0, 60), detail: promptText.slice(0, 4000), replyFrom: from,
|
|
2358
2358
|
workspaceMode: resolveWorkspaceMode(goal.workspaceMode ?? goal.worktree),
|
|
2359
2359
|
worktree: goal.worktree === false ? false : undefined,
|
|
2360
|
-
passCondition: null, status: 'queued', createdAt: new Date().toISOString(), priority:
|
|
2360
|
+
passCondition: null, status: 'queued', createdAt: new Date().toISOString(), priority: DEFAULT_PRIORITY,
|
|
2361
2361
|
result: null, changedFiles: [], secs: null, activity: [], proof: null, usage: null,
|
|
2362
2362
|
userRequirementHistory: tasks.filter((t) => t.goalId === goal.id && (!t.reply || t.replyFrom === 'you')).slice(-12).map((t) => ({ text: t.reply ? t.detail : t.title, status: t.status })),
|
|
2363
2363
|
// Transport the prior attempts as facts; the worker, not Manager, decides
|
|
@@ -4921,11 +4921,22 @@ const server = createServer(async (req, res) => {
|
|
|
4921
4921
|
// guarded here.
|
|
4922
4922
|
const runnable = !pending && !review && !approval;
|
|
4923
4923
|
const lang = /[-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en';
|
|
4924
|
-
//
|
|
4925
|
-
// runnable goal
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4924
|
+
// Galda no longer asks "do you want PRs?" (Masa 2026-07-31). It used to ask once
|
|
4925
|
+
// per project, before the first runnable goal, WITHOUT looking at what was asked —
|
|
4926
|
+
// needsReviewPreference() was `return !askedBefore`. A first-time user typing
|
|
4927
|
+
// 「テスト」 got interrogated about pull requests, and because the folder question
|
|
4928
|
+
// had claimed the needsInput slot on message 1, it landed on message 2, out of
|
|
4929
|
+
// nowhere. (That folder question is gone too, as of #642 — this is the other half
|
|
4930
|
+
// of the same interrogation.)
|
|
4931
|
+
//
|
|
4932
|
+
// Nothing needs the answer: resolveWantsPR() already falls back to
|
|
4933
|
+
// `!preferenceKnown && isImplementationRequest(text)`, i.e. a PR for work that looks
|
|
4934
|
+
// like implementation and none for 「テスト」 — which is the behaviour the question
|
|
4935
|
+
// was there to obtain. Projects that already answered keep their setting, and an
|
|
4936
|
+
// agent that genuinely needs to know can ask for itself: the question layer belongs
|
|
4937
|
+
// to Claude Code / Codex, not to Galda.
|
|
4938
|
+
const needsReviewPref = false;
|
|
4939
|
+
const reviewPrefQuestion = undefined;
|
|
4929
4940
|
// Clamp to whatever agent CLIs are actually installed on this machine
|
|
4930
4941
|
// (connected-agents Phase A) — a requested/default agent that isn't
|
|
4931
4942
|
// present would otherwise spawn a worker doomed to fail at launch.
|
|
@@ -5317,7 +5328,7 @@ const server = createServer(async (req, res) => {
|
|
|
5317
5328
|
return;
|
|
5318
5329
|
}
|
|
5319
5330
|
|
|
5320
|
-
// change a queued task's priority (
|
|
5331
|
+
// change a queued task's priority (high/medium/low) — re-sorts its project's queue
|
|
5321
5332
|
// so a bumped-up task actually runs sooner, not just displays that way.
|
|
5322
5333
|
const priorityMatch = url.pathname.match(/^\/api\/tasks\/(\d+)\/priority$/);
|
|
5323
5334
|
if (priorityMatch && req.method === 'PUT') {
|
|
@@ -5329,8 +5340,11 @@ const server = createServer(async (req, res) => {
|
|
|
5329
5340
|
req.on('end', () => {
|
|
5330
5341
|
try {
|
|
5331
5342
|
const { priority } = JSON.parse(body);
|
|
5332
|
-
|
|
5333
|
-
|
|
5343
|
+
// A browser left open on yesterday's app still sends 高/中/低. Refusing it
|
|
5344
|
+
// would break a tab nobody reloaded, for a rename that is ours, so the old
|
|
5345
|
+
// words are accepted and stored as the new ones.
|
|
5346
|
+
if (!isAcceptedPriority(priority)) return json(res, 400, { error: `priority must be one of: ${TASK_PRIORITIES.join(', ')}` });
|
|
5347
|
+
task.priority = normalizePriority(priority);
|
|
5334
5348
|
saveTask(task);
|
|
5335
5349
|
const q = queues.get(task.projectId);
|
|
5336
5350
|
if (q) { q.waiting = sortQueueByPriority(q.waiting); for (const t of q.waiting) saveTask(t); }
|
|
@@ -5506,9 +5520,26 @@ const server = createServer(async (req, res) => {
|
|
|
5506
5520
|
const existing = projects.find((p) => p.dir === dir);
|
|
5507
5521
|
let proj = existing;
|
|
5508
5522
|
if (!proj) {
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5523
|
+
// The free tier allows one project, and POST /api/projects enforces that — but
|
|
5524
|
+
// this path never did, so every folder someone named here minted another one
|
|
5525
|
+
// (measured 2026-07-31: a first-time user on the free tier ended up with three).
|
|
5526
|
+
// At the cap, "pick a folder" means "this is the folder I work in", so point the
|
|
5527
|
+
// project someone already has at it instead of inventing a second.
|
|
5528
|
+
const atCap = checkFreeTierLimit({ ...computeFreeTierUsage(goal.owner ?? 'owner'), projectCount: projects.length + 1 }).reason === 'project-limit';
|
|
5529
|
+
// Only ever the goal's OWN project. A folder question only fires when that
|
|
5530
|
+
// project's dir is HOME or not a repo, so it is by definition the unusable one
|
|
5531
|
+
// and pointing it at the chosen folder is what the person just asked for. Never
|
|
5532
|
+
// repoint somebody else's project to make room.
|
|
5533
|
+
const reusable = atCap ? projects.find((p) => p.id === goal.projectId) : null;
|
|
5534
|
+
if (reusable) {
|
|
5535
|
+
reusable.dir = dir;
|
|
5536
|
+
saveProjects();
|
|
5537
|
+
proj = reusable;
|
|
5538
|
+
} else {
|
|
5539
|
+
proj = { id: uniqueProjectId(basename(dir)), name: basename(dir), dir };
|
|
5540
|
+
projects.push(proj); saveProjects();
|
|
5541
|
+
queues.set(proj.id, { running: new Set(), waiting: [] });
|
|
5542
|
+
}
|
|
5512
5543
|
}
|
|
5513
5544
|
goal.projectId = proj.id;
|
|
5514
5545
|
goal.question = null;
|
package/package.json
CHANGED