@galda/cli 0.10.118 → 0.10.120
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 +317 -28
- 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}
|
|
@@ -666,6 +674,18 @@
|
|
|
666
674
|
.peratab.on{color:var(--ink);border-color:var(--ink3);background:rgba(var(--tint),.06)}
|
|
667
675
|
.peraask{display:flex;gap:8px;align-items:center;padding:10px 16px 6px}
|
|
668
676
|
.peraask input{flex:1;background:var(--glass);border:1px solid var(--hair2);border-radius:10px;color:var(--ink);font-size:12.5px;padding:8px 12px;outline:none}
|
|
677
|
+
/* First-run nudge + the npx answer. Deliberately not a painted panel or a
|
|
678
|
+
boxed card: a box reads as "tappable" (DESIGN-RULES §5.6). This is a line
|
|
679
|
+
of text and a stroke pointing into the input it is about. */
|
|
680
|
+
.cnudge{display:none;align-items:center;justify-content:center;gap:10px;margin:14px auto 0;opacity:0;transform:translateY(-3px);
|
|
681
|
+
transition:opacity .28s ease,transform .28s ease}
|
|
682
|
+
.cnudge.show{display:flex;opacity:1;transform:none}
|
|
683
|
+
.cnudge-t{font-size:12.5px;line-height:1.5;color:var(--ink2)}
|
|
684
|
+
.cnudge-a{width:12px;height:20px;flex:0 0 auto;border-left:1.5px solid var(--ink3);border-bottom:1.5px solid var(--ink3);
|
|
685
|
+
border-bottom-left-radius:7px;position:relative;opacity:.85}
|
|
686
|
+
.cnudge-a:after{content:'';position:absolute;right:-1px;bottom:-4px;width:6px;height:6px;
|
|
687
|
+
border-right:1.5px solid var(--ink3);border-bottom:1.5px solid var(--ink3);transform:rotate(-45deg)}
|
|
688
|
+
padding:10px 12px;font-family:var(--mono);font-size:12.5px;color:var(--ink);overflow-x:auto;white-space:nowrap}
|
|
669
689
|
.peraanswer{margin:0 16px 8px;font-size:12.5px;line-height:1.55;color:var(--ink2);white-space:pre-wrap;background:rgba(var(--tint),.03);border:1px solid var(--hair2);border-radius:10px;padding:10px 12px}
|
|
670
690
|
.peraanswer.loading{color:var(--ink3);font-style:italic}
|
|
671
691
|
.peraanswer.error{color:var(--danger)}
|
|
@@ -2923,13 +2943,31 @@
|
|
|
2923
2943
|
/* ---- iOS no-zoom: 16px inputs (extends the shared 16px rule to flagship-only fields) ---- */
|
|
2924
2944
|
body[data-layout="flagship"] .sa-chat,
|
|
2925
2945
|
body[data-layout="flagship"] #fsAskbar input{font-size:16px}
|
|
2926
|
-
/* ---- Ledger (#seeall) —
|
|
2946
|
+
/* ---- Ledger (#seeall) — Claude Code-style mobile reading sheet.
|
|
2947
|
+
A tapped Review should land at the top of the phone viewport, with the
|
|
2948
|
+
content column using the width instead of spending it on desktop gutters. ---- */
|
|
2949
|
+
body[data-layout="flagship"] #seeall.open{place-items:start center;z-index:120;pointer-events:auto}
|
|
2927
2950
|
body[data-layout="flagship"] #seeall .sa-panel{width:100%!important;max-width:100%!important;
|
|
2928
|
-
margin-top:
|
|
2929
|
-
|
|
2951
|
+
margin-top:calc(env(safe-area-inset-top) + 6px)!important;
|
|
2952
|
+
max-height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 12px)!important;
|
|
2953
|
+
border-radius:18px 18px 0 0!important}
|
|
2954
|
+
body[data-layout="flagship"] #seeall .sa-hd{padding:14px 18px 10px!important}
|
|
2930
2955
|
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:
|
|
2956
|
+
body[data-layout="flagship"] #seeall .sa-body{padding:10px 8px 16px}
|
|
2932
2957
|
body[data-layout="flagship"] #seeall .sa-item{width:100%}
|
|
2958
|
+
body[data-layout="flagship"] #seeall .sa-item.redesign{padding:22px 18px 28px!important}
|
|
2959
|
+
body[data-layout="flagship"] #seeall .sa-body>.sa-item.redesign+.sa-item.redesign{padding-top:30px!important}
|
|
2960
|
+
body[data-layout="flagship"] #seeall .sa-panel .sa-hd:has(~ .sa-body .sa-item.redesign){padding:14px 18px 10px!important}
|
|
2961
|
+
body[data-layout="flagship"] #seeall .revrail.show{width:9px;margin-right:7px}
|
|
2962
|
+
body[data-layout="flagship"] #seeall .revrail button{font-size:0;padding:6px 0}
|
|
2963
|
+
body[data-layout="flagship"] #seeall .revrail button .bar{width:3px;height:18px}
|
|
2964
|
+
body[data-layout="flagship"] #seeall .revrail button.on .bar{height:28px}
|
|
2965
|
+
body[data-layout="flagship"] #seeall .redesign .rtitle{font-size:17px;line-height:1.32;letter-spacing:0}
|
|
2966
|
+
body[data-layout="flagship"] #seeall .redesign .rsec{margin-top:22px!important}
|
|
2967
|
+
body[data-layout="flagship"] #seeall .redesign .rsec-h{font-size:15px;letter-spacing:0}
|
|
2968
|
+
body[data-layout="flagship"] #seeall .redesign .rbody,
|
|
2969
|
+
body[data-layout="flagship"] #seeall .redesign .rreport{font-size:15px;line-height:1.72;letter-spacing:0;max-width:none}
|
|
2970
|
+
body[data-layout="flagship"] #seeall .redesign .rreport code{font-size:13px}
|
|
2933
2971
|
body[data-layout="flagship"] #seeall .sa-act{flex-wrap:wrap;row-gap:8px}
|
|
2934
2972
|
body[data-layout="flagship"] #seeall .sa-chatwrap{flex:1 1 100%;order:3;min-width:0}
|
|
2935
2973
|
body[data-layout="flagship"] #seeall .sa-app,
|
|
@@ -2963,8 +3001,15 @@
|
|
|
2963
3001
|
max-width:62%;justify-content:center;margin:0}
|
|
2964
3002
|
/* Review/To Do opener (right) = plain ☰ icon (Masa 2026-07-18: ボタン化はやめ、三本線だけ・
|
|
2965
3003
|
タップで To Do/Review ドロワー). The base .fshamb rule above already renders the ☰ svg. */
|
|
2966
|
-
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb
|
|
2967
|
-
|
|
3004
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb{display:none}
|
|
3005
|
+
/* Masa 2026-07-31: たまったレビューが右上から分からない → ☰ の右肩に赤ドット。
|
|
3006
|
+
件数はJS(renderFsReview)がfshamb-nに書き込み済みだったが display:none で常に非表示だった。
|
|
3007
|
+
件数の有無だけをJSからのtext contentで判定して出し分ける(空なら消える)。 */
|
|
3008
|
+
body[data-layout="flagship"] #fsRoot .fshamb{position:relative}
|
|
3009
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n{display:none;position:absolute;top:2px;right:2px;
|
|
3010
|
+
min-width:9px;height:9px;padding:0;border-radius:50%;background:var(--danger);
|
|
3011
|
+
box-shadow:0 0 0 2px var(--panel);font-size:0;line-height:0;color:transparent}
|
|
3012
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n:not(:empty){display:block}
|
|
2968
3013
|
/* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
|
|
2969
3014
|
が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
|
|
2970
3015
|
body[data-layout="flagship"] #fsRoot .fsrail{background:transparent;box-shadow:none}
|
|
@@ -2984,9 +3029,10 @@
|
|
|
2984
3029
|
margin:0 0 4px;padding:0 2px 10px;gap:9px;border-bottom:1px solid var(--hair)}
|
|
2985
3030
|
/* H25③: See-all ledger → scrim + inset panel (a full-bleed sheet let translucent themes show through). */
|
|
2986
3031
|
body[data-layout="flagship"] #seeall{background:rgba(9,12,20,.55)}
|
|
3032
|
+
body[data-layout="flagship"] #seeall.open{z-index:120}
|
|
2987
3033
|
body[data-layout="flagship"] #seeall .sa-panel{width:96%!important;max-width:96%!important;
|
|
2988
3034
|
max-height:calc(100dvh - env(safe-area-inset-top) - 22px)!important;
|
|
2989
|
-
margin-top:calc(env(safe-area-inset-top) +
|
|
3035
|
+
margin-top:calc(env(safe-area-inset-top) + 6px)!important;border-radius:18px!important}
|
|
2990
3036
|
}
|
|
2991
3037
|
/* Billing UI (H16/H22) — self-serve plan. Tokens only; red is a line/label,
|
|
2992
3038
|
never a fill (DESIGN-RULES §5.6), and it is --danger (the app's real token —
|
|
@@ -3249,6 +3295,13 @@
|
|
|
3249
3295
|
<div class="nowdoing" id="fsNowDoing" hidden></div>
|
|
3250
3296
|
<!-- Clawd HERO (§4.5 初期状態): filled by JS (CLAWD generator), shown via #fsRoot.clawdhero -->
|
|
3251
3297
|
<div id="fsClawdHero" aria-hidden="true"></div>
|
|
3298
|
+
<!-- Under the waiting hero, pointing down at the box a new person is meant
|
|
3299
|
+
to use. "CONNECTED / WAITING FOR GOALS" says our state; this says
|
|
3300
|
+
theirs. Shown only while the board has nothing on it. -->
|
|
3301
|
+
<div class="cnudge" id="composerNudge">
|
|
3302
|
+
<span class="cnudge-t">Type what you want done, down there.</span>
|
|
3303
|
+
<span class="cnudge-a" aria-hidden="true"></span>
|
|
3304
|
+
</div>
|
|
3252
3305
|
<!-- H23 mobile empty-state hero (flagship .m-hero) — chips fill the composer (#input) -->
|
|
3253
3306
|
<div class="m-hero"><h2>What should we build?</h2><p>Describe a goal in plain language. Claude Code runs it, checks its own work, and only calls it done with proof.</p><div class="m-chips"><button class="m-chip">Fix the failing test in lib.mjs</button><button class="m-chip">Add a token-usage counter</button><button class="m-chip">Summarize the open PRs</button></div></div>
|
|
3254
3307
|
<div class="fsstream"><div id="fsFeed"></div></div>
|
|
@@ -3440,6 +3493,20 @@
|
|
|
3440
3493
|
<div class="segbtn" id="langSeg"><button data-lang="auto">Auto</button><button data-lang="en">English</button><button data-lang="ja">日本語</button></div></div>
|
|
3441
3494
|
</div>
|
|
3442
3495
|
</div></div>
|
|
3496
|
+
<!-- "What is Galda? / What can it access?" — the orientation panel.
|
|
3497
|
+
WHY IT EXISTS: the first engineer we handed Galda to reached for the gear
|
|
3498
|
+
button before typing anything. He was not lost, he was CHECKING: people
|
|
3499
|
+
want to know what a tool can touch before they hand it a repository. This
|
|
3500
|
+
answers that where they already are, instead of sending them to a website.
|
|
3501
|
+
Rendered from a constant, entirely locally: NO agent run, NO tokens, no
|
|
3502
|
+
network. Spending someone's quota and 40 seconds to produce a text answer
|
|
3503
|
+
they could have read instantly would be a bad trade, and it would teach
|
|
3504
|
+
the wrong idea of what Galda is (it is not a chatbot).
|
|
3505
|
+
Same overlay/modal parts as Settings above — no new component. -->
|
|
3506
|
+
<div class="overlay revfloat" id="aboutOverlay"><div class="modal" style="width:min(560px,94vw)">
|
|
3507
|
+
<div class="perahead" style="border-bottom:1px solid var(--hair)"><div class="peratitle" id="aboutTitle">What is Galda?</div><span style="flex:1"></span><button class="mbtn ghost" id="aboutClose">Close</button></div>
|
|
3508
|
+
<div style="padding:16px;max-height:min(70vh,620px);overflow:auto" id="aboutBody"></div>
|
|
3509
|
+
</div></div>
|
|
3443
3510
|
<!-- Slack-style centered delete confirmation, replacing native confirm() for every goal
|
|
3444
3511
|
delete (restores a lost decision — see docs/confirmed-behaviors.md). Reusable via the
|
|
3445
3512
|
confirmDelete({title,preview}) -> Promise<boolean> helper. -->
|
|
@@ -3710,18 +3777,34 @@ function pickColumnLabel(columns, bucket, fallback){
|
|
|
3710
3777
|
}
|
|
3711
3778
|
function columnsFor(projectId){ return state.workflowColumns[projectId] ?? DEFAULT_WORKFLOW_COLUMNS; }
|
|
3712
3779
|
|
|
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
|
-
|
|
3780
|
+
// Mirrors engine/lib.mjs TASK_PRIORITIES / normalizePriority — kept in sync
|
|
3781
|
+
// manually, see the comment on DEFAULT_WORKFLOW_COLUMNS above. Queue execution
|
|
3782
|
+
// order is high→medium→low, same-priority ties keep creation/drag order
|
|
3783
|
+
// (server.mjs's sortQueueByPriority) — this badge is only for To do (queued)
|
|
3784
|
+
// rows, where that ordering actually matters.
|
|
3785
|
+
//
|
|
3786
|
+
// The values were 高/中/低 until 2026-07-31. Nothing migrates what is already on
|
|
3787
|
+
// disk, so every task written before then still says 高 and must still render as
|
|
3788
|
+
// what it is: reading only the new list would have shown it as Medium — the board
|
|
3789
|
+
// lying about the order the queue really runs in.
|
|
3790
|
+
const TASK_PRIORITIES = ['high', 'medium', 'low'];
|
|
3791
|
+
const LEGACY_PRIORITIES = { 高: 'high', 中: 'medium', 低: 'low' };
|
|
3792
|
+
// One glyph for the 22px square, the whole word for the menu that has room.
|
|
3793
|
+
const PRIORITY_GLYPH = { high: 'H', medium: 'M', low: 'L' };
|
|
3794
|
+
const PRIORITY_NAME = { high: 'High', medium: 'Medium', low: 'Low' };
|
|
3795
|
+
function normalizePriority(value){
|
|
3796
|
+
const v = String(value ?? '').trim();
|
|
3797
|
+
if (TASK_PRIORITIES.includes(v)) return v;
|
|
3798
|
+
if (LEGACY_PRIORITIES[v]) return LEGACY_PRIORITIES[v];
|
|
3799
|
+
const lower = v.toLowerCase();
|
|
3800
|
+
return TASK_PRIORITIES.includes(lower) ? lower : 'medium';
|
|
3801
|
+
}
|
|
3719
3802
|
function prioBadge(t){
|
|
3720
|
-
const p =
|
|
3803
|
+
const p = normalizePriority(t.priority);
|
|
3721
3804
|
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>
|
|
3805
|
+
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority — ${PRIORITY_NAME[p]}">${PRIORITY_GLYPH[p]}</button>
|
|
3723
3806
|
<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>
|
|
3807
|
+
`<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${PRIORITY_NAME[x]}</button>`).join('')}</div>
|
|
3725
3808
|
</div>`;
|
|
3726
3809
|
}
|
|
3727
3810
|
|
|
@@ -6732,6 +6815,7 @@ $('gdRevisionInput').addEventListener('keydown', (e) => {
|
|
|
6732
6815
|
});
|
|
6733
6816
|
|
|
6734
6817
|
function render(){
|
|
6818
|
+
renderComposerNudge();
|
|
6735
6819
|
renderProjects(); renderStream(); renderQueue(); renderTasks(); renderActPanel(); wireWfEditButtons();
|
|
6736
6820
|
renderCtxBar(); // the folder above the composer follows the active project
|
|
6737
6821
|
renderFlagship();
|
|
@@ -9018,6 +9102,12 @@ function connectSSE(){
|
|
|
9018
9102
|
// fsActiveContext() uses to pick which running goal is "now" when several run
|
|
9019
9103
|
// in parallel (§1.5-1: never anchor on stale work when something is live).
|
|
9020
9104
|
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; }
|
|
9105
|
+
// The agent talking, as opposed to the agent working. Everything above this
|
|
9106
|
+
// line is machinery (a status, a tool call, a to-do list); this is a sentence
|
|
9107
|
+
// somebody wrote to the person waiting, so it goes where the person is
|
|
9108
|
+
// looking — the same turn treatment a chat reply gets, because it is the same
|
|
9109
|
+
// thing. Masa's friend waited three minutes for exactly this and gave up.
|
|
9110
|
+
else if (m.ev === 'say') { sayFromAgent(m); return; }
|
|
9021
9111
|
else if (m.ev === 'todos') state.todos[m.taskId] = m.todos ?? [];
|
|
9022
9112
|
else if (m.ev === 'columns') state.workflowColumns[m.projectId] = m.columns;
|
|
9023
9113
|
else if (m.ev === 'review-definition') state.reviewDefinitions[m.projectId] = m.reviewDefinition;
|
|
@@ -9071,7 +9161,8 @@ function bootDisconnected(info){
|
|
|
9071
9161
|
background:rgba(4,4,6,.72);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}
|
|
9072
9162
|
#connectGate.on{display:flex}
|
|
9073
9163
|
#connectGate .cg-card{position:relative;width:100%;max-width:440px;background:var(--panel,#0d0d0f);border:1px solid var(--hair2,rgba(255,255,255,.12));
|
|
9074
|
-
border-radius:18px;padding:
|
|
9164
|
+
border-radius:18px;padding:28px 26px;text-align:left;box-shadow:0 30px 80px -30px #000;
|
|
9165
|
+
max-height:calc(100vh - 44px);overflow:auto}
|
|
9075
9166
|
#connectGate h2{font:700 20px/1.3 var(--ui,system-ui);letter-spacing:-.02em;margin:0 0 8px;color:var(--ink,#e9e7e2)}
|
|
9076
9167
|
#connectGate p{font-size:13.5px;line-height:1.7;color:var(--ink2,#a3a097);margin:0 0 18px}
|
|
9077
9168
|
#connectGate .cg-cmd{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#0a0a0b;
|
|
@@ -9083,6 +9174,14 @@ function bootDisconnected(info){
|
|
|
9083
9174
|
#connectGate .cg-hint{font-size:12px;line-height:1.6;color:var(--ink3,#6b6862);margin:0 0 16px}
|
|
9084
9175
|
#connectGate .cg-hint a{color:var(--blue,#8fb0ff);text-decoration:none}
|
|
9085
9176
|
#connectGate .cg-hint a:hover{text-decoration:underline}
|
|
9177
|
+
#connectGate .cg-trust{margin:18px 0 0;padding:16px 0 0;border-top:1px solid var(--hair2,rgba(255,255,255,.10))}
|
|
9178
|
+
#connectGate .cg-blanket{font-size:12.5px;line-height:1.6;color:var(--ink,#e9e7e2);font-weight:600;margin:0}
|
|
9179
|
+
#connectGate .cg-acchead{font:500 10px/1 var(--mono,monospace);letter-spacing:.09em;text-transform:uppercase;color:var(--ink3,#6b6862);margin:16px 0 6px}
|
|
9180
|
+
#connectGate .cg-accrow{font-size:12px;line-height:1.45;color:var(--ink2,#a3a097);padding:0 0 0 12px;margin:0 0 6px;position:relative}
|
|
9181
|
+
#connectGate .cg-accrow:before{content:'';position:absolute;left:0;top:7px;width:4px;height:1px;background:var(--ink3,#6b6862);opacity:.7}
|
|
9182
|
+
#connectGate .cg-seclink{margin:14px 0 0;font-size:12px}
|
|
9183
|
+
#connectGate .cg-seclink a{color:var(--blue,#8fb0ff);text-decoration:none}
|
|
9184
|
+
#connectGate .cg-acct{margin:18px 0 0;padding:16px 0 0;border-top:1px solid var(--hair2,rgba(255,255,255,.10))}
|
|
9086
9185
|
#connectGate .cg-who{font-size:12px;color:var(--ink3,#6b6862);margin:0 0 4px}
|
|
9087
9186
|
#connectGate .cg-who b{color:var(--ink2,#a3a097)}
|
|
9088
9187
|
#connectGate .cg-switch{font-size:12px;color:var(--ink3,#6b6862);margin:14px 0 0;transition:color .2s}
|
|
@@ -9097,6 +9196,7 @@ function bootDisconnected(info){
|
|
|
9097
9196
|
#connectGate .cg-switch.escalated{color:var(--danger);font-weight:600}
|
|
9098
9197
|
#connectGate .cg-switch.escalated a{color:var(--danger);text-decoration:underline}
|
|
9099
9198
|
#connectGate .cg-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border:0;background:transparent;color:var(--ink3,#6b6862);font-size:20px;line-height:1;cursor:pointer}
|
|
9199
|
+
#connectGate .cg-waitwrap{display:flex;justify-content:center}
|
|
9100
9200
|
#connectGate .cg-wait{display:inline-flex;align-items:center;gap:7px;font:500 10.5px/1 var(--mono,monospace);letter-spacing:.08em;color:var(--ink3,#6b6862);margin-top:16px}
|
|
9101
9201
|
#connectGate .cg-wait i{width:6px;height:6px;border-radius:50%;background:var(--green,#3DDC97);animation:cgpulse 1.8s ease-in-out infinite}
|
|
9102
9202
|
@keyframes cgpulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green,#3DDC97) 50%,transparent)}50%{box-shadow:0 0 6px 2px color-mix(in srgb,var(--green,#3DDC97) 35%,transparent)}}
|
|
@@ -9116,9 +9216,21 @@ function bootDisconnected(info){
|
|
|
9116
9216
|
+ '<a href="https://claude.com/claude-code" target="_blank" rel="noreferrer">Claude Code</a> or '
|
|
9117
9217
|
+ '<a href="https://openai.com/codex/" target="_blank" rel="noreferrer">Codex</a>, then run the same command '
|
|
9118
9218
|
+ '— Galda uses whichever agent you have.</p>'
|
|
9219
|
+
// The gate said what to run and never what it would do. "npx? what else is
|
|
9220
|
+
// it going to touch?" is the actual reason people stop here, so the answer
|
|
9221
|
+
// sits with the command. Facts only, in step with docs/SECURITY.md.
|
|
9222
|
+
+ '<div class="cg-trust">'
|
|
9223
|
+
+ '<p class="cg-blanket">No admin access, no background service, nothing that starts on its own. '
|
|
9224
|
+
+ 'Plain JavaScript you can read before you run it, and one folder you can delete.</p>'
|
|
9225
|
+
+ '<div class="cg-acchead">What it can access</div>'
|
|
9226
|
+
+ NPX_ONBOARDING.access.map(function(t){ return '<div class="cg-accrow">' + esc(t) + '</div>'; }).join('')
|
|
9227
|
+
+ '<p class="cg-seclink"><a href="https://galda.app/security" target="_blank" rel="noreferrer">Security and permissions \u2192</a></p>'
|
|
9228
|
+
+ '</div>'
|
|
9229
|
+
+ '<div class="cg-acct">'
|
|
9119
9230
|
+ '<p class="cg-who">Signed in as <b></b></p>'
|
|
9120
9231
|
+ '<p class="cg-switch">Already running Galda under a different account? Run <code>npx @galda/cli --signin</code> and pick this one. Or <a href="/logout">switch Google account →</a></p>'
|
|
9121
|
-
+ '
|
|
9232
|
+
+ '</div>'
|
|
9233
|
+
+ '<div class="cg-waitwrap"><div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div></div>';
|
|
9122
9234
|
document.body.appendChild(ov);
|
|
9123
9235
|
ov.querySelector('.cg-who b').textContent = email || 'your account';
|
|
9124
9236
|
const open = () => ov.classList.add('on');
|
|
@@ -9323,6 +9435,25 @@ function typeChatTurn(turn){
|
|
|
9323
9435
|
if (turn.revealed >= full.length) { turn.done = true; stopChatTyper(); renderFsFeed(); }
|
|
9324
9436
|
}, 16);
|
|
9325
9437
|
}
|
|
9438
|
+
// A worker's opening words (engine: `ev:'say'`). Rendered as a spoken turn, typed
|
|
9439
|
+
// out, exactly like the one-shot chat reply — Galda writes nothing of its own
|
|
9440
|
+
// here and picks nothing: the sentence is the agent's, and this only puts it
|
|
9441
|
+
// where it can be read. The board layout has no centre feed, so it stays in the
|
|
9442
|
+
// run log there rather than being invented somewhere new.
|
|
9443
|
+
function sayFromAgent({ goalId, text, agent }){
|
|
9444
|
+
const body = String(text ?? '').trim();
|
|
9445
|
+
if (!body || state.layout !== 'flagship') return;
|
|
9446
|
+
const goal = state.goals.find((g) => g.id === goalId);
|
|
9447
|
+
const projectId = goal?.projectId ?? state.active;
|
|
9448
|
+
const turns = (state.chatTurns[projectId] ||= []);
|
|
9449
|
+
// A reconnecting stream can hand us the same opening twice; a person reading it
|
|
9450
|
+
// twice would think the agent had started over.
|
|
9451
|
+
if (turns.some((t) => t.role === 'clawd' && t.text === body)) return;
|
|
9452
|
+
const turn = { role: 'clawd', text: body, revealed: 0, done: false, agent, ts: Date.now() };
|
|
9453
|
+
turns.push(turn);
|
|
9454
|
+
renderFsFeed();
|
|
9455
|
+
if (projectId === state.active) typeChatTurn(turn); else { turn.revealed = body.length; turn.done = true; }
|
|
9456
|
+
}
|
|
9326
9457
|
// A new send starts: finish any in-flight reply cleanly (complete the model turn +
|
|
9327
9458
|
// drop the caret, hide the board bar) so a half-typed reply never lingers.
|
|
9328
9459
|
function finalizeChatTyping(){
|
|
@@ -11510,7 +11641,7 @@ function renderCtxBar(){
|
|
|
11510
11641
|
.join('');
|
|
11511
11642
|
// Redrawing the menu must not hand back a second chooser: carry the disabled
|
|
11512
11643
|
// 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"
|
|
11644
|
+
const openRow = `<button type="button" class="appopt ctxopen${folderPickerBusy ? ' busy' : ''}" id="ctxopenfolder"${folderPickerBusy ? ' disabled' : ''} title="Open the OS folder chooser">${folderRowInner(folderPickerBusy)}</button>`;
|
|
11514
11645
|
menu.innerHTML = `${opts}${openRow}<div class="ctxmsg" id="ctxdirmsg"></div>`;
|
|
11515
11646
|
}
|
|
11516
11647
|
// Workspace mode is fixed to the isolated worktree (clean separate checkout). The
|
|
@@ -11562,17 +11693,37 @@ async function setProjectFolder(dir){
|
|
|
11562
11693
|
// request stays in flight for as long as the dialog is up, so "a request is out"
|
|
11563
11694
|
// is exactly "a dialog is open" — no timer to guess with.
|
|
11564
11695
|
let folderPickerBusy = false;
|
|
11696
|
+
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>';
|
|
11697
|
+
// While a chooser is open the icon spins in its own place. Waiting with nothing
|
|
11698
|
+
// moving is what "it did nothing" looks like — and it is what made Masa's friend
|
|
11699
|
+
// tap ten times (2026-07-31). One source for the row's contents, so the live swap
|
|
11700
|
+
// and the next redraw cannot disagree.
|
|
11701
|
+
function folderRowInner(busy){
|
|
11702
|
+
return `${busy ? '<span class="ctxspin" aria-hidden="true"></span>' : FOLDER_GLYPH}<span class="nm">Open folder…</span>`;
|
|
11703
|
+
}
|
|
11565
11704
|
function setFolderPickerBusy(busy){
|
|
11566
11705
|
folderPickerBusy = busy;
|
|
11567
11706
|
const btn = $('ctxopenfolder');
|
|
11568
|
-
if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); }
|
|
11569
|
-
}
|
|
11707
|
+
if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); btn.innerHTML = folderRowInner(busy); }
|
|
11708
|
+
}
|
|
11709
|
+
// How long to wait before saying the dialog might not be where they are looking.
|
|
11710
|
+
// The engine opens it immediately; on a Mac where the engine was started by launchd
|
|
11711
|
+
// it can open behind everything, and there is nothing on screen to say so — which is
|
|
11712
|
+
// the other half of what happened to Masa's friend. Long enough not to nag someone
|
|
11713
|
+
// who is simply choosing a folder.
|
|
11714
|
+
const FOLDER_CHOOSER_SLOW_MS = 6000;
|
|
11570
11715
|
async function openFolderNative(){
|
|
11571
11716
|
if (folderPickerBusy) return;
|
|
11572
11717
|
const msg = $('ctxdirmsg');
|
|
11573
11718
|
const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
|
|
11574
11719
|
setFolderPickerBusy(true);
|
|
11575
11720
|
say('Opening the folder chooser…');
|
|
11721
|
+
// Still open after a while: say where it might be, and point at the way out that
|
|
11722
|
+
// does not need it. The row stays disabled — a second chooser is the bug this
|
|
11723
|
+
// guard exists to prevent, and offering one here would re-create it.
|
|
11724
|
+
const slow = setTimeout(() => {
|
|
11725
|
+
if (folderPickerBusy) say('Still waiting — the chooser may be behind your other windows. You can also pick a folder from the list above.');
|
|
11726
|
+
}, FOLDER_CHOOSER_SLOW_MS);
|
|
11576
11727
|
try {
|
|
11577
11728
|
const r = await fetch(withKey('/api/choose-folder'), { method: 'POST' });
|
|
11578
11729
|
const body = await r.json().catch(() => ({}));
|
|
@@ -11581,7 +11732,7 @@ async function openFolderNative(){
|
|
|
11581
11732
|
if (body.unsupported) { say('This engine can’t open a folder dialog on this system.', true); return; }
|
|
11582
11733
|
say(body.error || 'Could not open the folder chooser.', true);
|
|
11583
11734
|
} catch { say('Could not reach the engine.', true); }
|
|
11584
|
-
finally { setFolderPickerBusy(false); }
|
|
11735
|
+
finally { clearTimeout(slow); setFolderPickerBusy(false); }
|
|
11585
11736
|
}
|
|
11586
11737
|
$('ctxdirbtn').onclick = (e) => {
|
|
11587
11738
|
e.stopPropagation();
|
|
@@ -11866,6 +12017,13 @@ $('histBtn').onclick = openHistory;
|
|
|
11866
12017
|
$('setBtn').onclick = openSettings;
|
|
11867
12018
|
$('histClose').onclick = () => $('historyOverlay').classList.remove('show');
|
|
11868
12019
|
$('setClose').onclick = () => $('settingsOverlay').classList.remove('show');
|
|
12020
|
+
// About panel: Close, tapping the backdrop, and Esc all dismiss it (DESIGN-RULES
|
|
12021
|
+
// §設定画面 — a dialog must close when you click somewhere unrelated).
|
|
12022
|
+
$('aboutClose').onclick = () => closeAbout();
|
|
12023
|
+
$('aboutOverlay').onclick = (e) => { if (e.target === $('aboutOverlay')) closeAbout(); };
|
|
12024
|
+
document.addEventListener('keydown', (e) => {
|
|
12025
|
+
if (e.key === 'Escape' && $('aboutOverlay')?.classList.contains('show')) { e.stopPropagation(); closeAbout(); }
|
|
12026
|
+
}, true);
|
|
11869
12027
|
$('historyOverlay').addEventListener('click', (e) => { if (e.target === $('historyOverlay')) $('historyOverlay').classList.remove('show'); });
|
|
11870
12028
|
$('settingsOverlay').addEventListener('click', (e) => { if (e.target === $('settingsOverlay')) $('settingsOverlay').classList.remove('show'); });
|
|
11871
12029
|
for (const b of $('langSeg').querySelectorAll('button')) b.onclick = () => saveLang(b.dataset.lang);
|
|
@@ -12517,7 +12675,13 @@ window.__tbink = (i) => { document.body.dataset.tbink = String(((i | 0) % 3 + 3)
|
|
|
12517
12675
|
raf = 0;
|
|
12518
12676
|
if (document.hidden) { clearFx(); return; }
|
|
12519
12677
|
const t = (performance.now() - t0) / 1000, d = Math.min(2, devicePixelRatio || 1);
|
|
12520
|
-
|
|
12678
|
+
// Clear in backing-store pixels *before* installing the CSS-pixel transform.
|
|
12679
|
+
// Mobile Safari can change its visual viewport as a question answer removes a
|
|
12680
|
+
// card (or as the browser chrome expands). Clearing in transformed viewport
|
|
12681
|
+
// coordinates then leaves pixels from the send ring's former location behind.
|
|
12682
|
+
// The canvas's own dimensions are the only stable full-clear bounds.
|
|
12683
|
+
clearFx();
|
|
12684
|
+
fc.setTransform(d, 0, 0, d, 0, 0);
|
|
12521
12685
|
if (MODE < 10) tick(t); // TB edge anim (optional, off by default = 10)
|
|
12522
12686
|
drawSendRing(t); // send ring (always on while btn mode = 0, independent of TB edge anim)
|
|
12523
12687
|
raf = requestAnimationFrame(loop);
|
|
@@ -12676,11 +12840,133 @@ function gearSetHTML(){
|
|
|
12676
12840
|
<button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
|
|
12677
12841
|
<button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>
|
|
12678
12842
|
${billingRow}
|
|
12843
|
+
<div class="srow"><div class="sl">About Galda<span class="sd">What it is, what the agent can touch, and what leaves this machine</span></div>
|
|
12844
|
+
<button class="optchip" data-gabout="1">Open</button></div>
|
|
12845
|
+
${remoteAccessRow()}
|
|
12679
12846
|
${stability}
|
|
12680
12847
|
${gearRulesHTML()}
|
|
12681
12848
|
${hostReadinessRow()}
|
|
12682
12849
|
<div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
|
|
12683
12850
|
}
|
|
12851
|
+
// The npx moment, moved to where the intent is.
|
|
12852
|
+
//
|
|
12853
|
+
// A person who signed up on the web lands on a board with nothing behind it:
|
|
12854
|
+
// Galda drives the Claude Code / Codex on THEIR machine, and that is not
|
|
12855
|
+
// running yet. Until now the only thing they were told was "Galda is offline —
|
|
12856
|
+
// reconnecting…", which is true and useless: it describes our state, not their
|
|
12857
|
+
// next step. That is the "run npx" dead end.
|
|
12858
|
+
//
|
|
12859
|
+
// So the answer arrives when they reach for the composer, which is the moment
|
|
12860
|
+
// they have decided to do something. And it has to answer the real hesitation,
|
|
12861
|
+
// not just print a command: "npx? what else is it going to do to my machine?"
|
|
12862
|
+
//
|
|
12863
|
+
// Every line below is a fact about the shipped package, kept in step with
|
|
12864
|
+
// docs/SECURITY.md and the same block on galda.app. Do not promise here what
|
|
12865
|
+
// is not true there.
|
|
12866
|
+
const NPX_ONBOARDING = {
|
|
12867
|
+
why: 'This is a command, not a website, because Galda runs the Claude Code or Codex already installed on your machine, on your own subscription. A website cannot reach them.',
|
|
12868
|
+
blanket: 'No admin access, no background service, nothing that starts on its own. Plain JavaScript you can read before you run it, and one folder you can delete.',
|
|
12869
|
+
access: [
|
|
12870
|
+
'Your project files and tests, inside a throwaway copy',
|
|
12871
|
+
'Its git is read-only: no commit, no push, no checkout, no reset',
|
|
12872
|
+
'Never your working tree, your branch, or your uncommitted changes',
|
|
12873
|
+
'A fixed allowlist of commands, not arbitrary shell',
|
|
12874
|
+
'Nothing at all at install time',
|
|
12875
|
+
],
|
|
12876
|
+
};
|
|
12877
|
+
|
|
12878
|
+
// The nudge: with no agent behind the board, point at the composer instead of
|
|
12879
|
+
// leaving a new person to guess. Only while there is nothing to do here — it
|
|
12880
|
+
// disappears the moment an agent connects.
|
|
12881
|
+
function renderComposerNudge(){
|
|
12882
|
+
const n = $('composerNudge');
|
|
12883
|
+
if (!n) return;
|
|
12884
|
+
// Only while there is genuinely nothing here. The moment a first goal exists
|
|
12885
|
+
// the board explains itself and a standing instruction becomes clutter.
|
|
12886
|
+
const empty = !(state.goals || []).length && !(state.tasks || []).length;
|
|
12887
|
+
n.classList.toggle('show', empty);
|
|
12888
|
+
}
|
|
12889
|
+
|
|
12890
|
+
// The orientation panel's content. A constant, not a request: this renders with
|
|
12891
|
+
// zero tokens and no network, because the answer never depends on the project.
|
|
12892
|
+
//
|
|
12893
|
+
// Every line here is a fact about the shipped code, kept in step with
|
|
12894
|
+
// docs/SECURITY.md. Do not add a promise here that is not true there.
|
|
12895
|
+
const ABOUT_GALDA = [
|
|
12896
|
+
{ h: 'What Galda is', p: [
|
|
12897
|
+
'Galda hands your work to Claude Code or Codex and brings back something you can review.',
|
|
12898
|
+
'It has no AI of its own. It starts the agent already installed and signed in on this machine, on your own subscription, so there is no extra API cost — and your code is never uploaded to Galda to be run through a model.',
|
|
12899
|
+
] },
|
|
12900
|
+
{ h: 'What the agent can do', p: [
|
|
12901
|
+
'Read and edit files, and run tests, inside a private copy of your project.',
|
|
12902
|
+
'Run named commands only — language runtimes, package managers, test and lint runners, and read-only shell tools. Not arbitrary shell.',
|
|
12903
|
+
] },
|
|
12904
|
+
{ h: 'What the agent cannot do', p: [
|
|
12905
|
+
'It cannot commit, push, checkout, or reset. Its git access is read-only: status, log, diff, show, branch, blame, ls-files, rev-parse. Nothing else.',
|
|
12906
|
+
'It cannot touch your working tree. Every task runs in a throwaway git worktree, so your branch and your uncommitted changes are left alone.',
|
|
12907
|
+
] },
|
|
12908
|
+
{ h: 'What leaves this machine', p: [
|
|
12909
|
+
'Model traffic goes from Claude Code or Codex straight to Anthropic or OpenAI. Galda is not in that path.',
|
|
12910
|
+
'With remote access on, what this board displays passes through Galda’s relay so you can open it from your phone. It is encrypted in transit but not end to end, so treat Galda’s server as being in the path.',
|
|
12911
|
+
'Turn remote access off in Settings and no outbound connection is opened at all.',
|
|
12912
|
+
] },
|
|
12913
|
+
{ h: 'Removing it', p: [
|
|
12914
|
+
'Galda installs no background services and runs nothing at install time. Deleting ~/.manager-for-ai removes everything it keeps.',
|
|
12915
|
+
] },
|
|
12916
|
+
];
|
|
12917
|
+
function openAbout(){
|
|
12918
|
+
const body = $('aboutBody');
|
|
12919
|
+
if (body) {
|
|
12920
|
+
body.innerHTML = ABOUT_GALDA.map((s) => `
|
|
12921
|
+
<div class="setrow" style="display:block">
|
|
12922
|
+
<div class="setlabel">${esc(s.h)}</div>
|
|
12923
|
+
${s.p.map((t) => `<div class="setsub" style="margin-top:4px">${esc(t)}</div>`).join('')}
|
|
12924
|
+
</div>`).join('');
|
|
12925
|
+
}
|
|
12926
|
+
$('aboutOverlay')?.classList.add('show');
|
|
12927
|
+
}
|
|
12928
|
+
function closeAbout(){ $('aboutOverlay')?.classList.remove('show'); }
|
|
12929
|
+
// Reachable from the first-run composer link too, once that lands.
|
|
12930
|
+
window.openAbout = openAbout;
|
|
12931
|
+
|
|
12932
|
+
// Remote access = the relay. On, you can open this board from your phone; the
|
|
12933
|
+
// price is that what the board shows crosses Galda's relay server (it is a
|
|
12934
|
+
// reverse HTTP tunnel, TLS but not end-to-end encrypted — docs/SECURITY.md).
|
|
12935
|
+
// Off, no outbound socket is opened at all.
|
|
12936
|
+
//
|
|
12937
|
+
// This row exists because that switch already worked and nobody could find it:
|
|
12938
|
+
// it was the env var `RELAY_URL=`. The people who most need it are the ones who
|
|
12939
|
+
// will never read a source comment. Cursor puts Privacy Mode in Settings for
|
|
12940
|
+
// the same reason. Same shape as Language above — a server setting that applies
|
|
12941
|
+
// the moment it is chosen, not part of the visual edit session.
|
|
12942
|
+
//
|
|
12943
|
+
// No row at all when this build has no relay configured (§3 段階開示: never show
|
|
12944
|
+
// a switch that controls nothing).
|
|
12945
|
+
function remoteAccessRow(){
|
|
12946
|
+
const rp = state.relayPref;
|
|
12947
|
+
if (!rp || !rp.configured) return '';
|
|
12948
|
+
const on = rp.enabled === true;
|
|
12949
|
+
const desc = on
|
|
12950
|
+
? 'On — reach this board from your phone. What the board shows passes through Galda’s relay.'
|
|
12951
|
+
: 'Off — nothing leaves this machine. This board is not reachable from your phone.';
|
|
12952
|
+
return `<div class="srow"><div class="sl">Remote access<span class="sd">${esc(desc)}</span></div>
|
|
12953
|
+
<button class="optchip${on ? ' on' : ''}" data-grelay="on">On</button>
|
|
12954
|
+
<button class="optchip${on ? '' : ' on'}" data-grelay="off">Off</button></div>`;
|
|
12955
|
+
}
|
|
12956
|
+
async function loadRelayPref(){
|
|
12957
|
+
try { const r = await fetch(withKey('/api/relay-pref')); state.relayPref = r.ok ? await r.json() : null; }
|
|
12958
|
+
catch { state.relayPref = null; }
|
|
12959
|
+
if ($('gearPop') && custTab === 'settings') buildGearPop();
|
|
12960
|
+
}
|
|
12961
|
+
async function saveRelayPref(enabled){
|
|
12962
|
+
const r = await fetch(withKey('/api/relay-pref'), {
|
|
12963
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
12964
|
+
body: JSON.stringify({ enabled }),
|
|
12965
|
+
});
|
|
12966
|
+
if (!r.ok) return showErr('Failed to save.');
|
|
12967
|
+
state.relayPref = await r.json();
|
|
12968
|
+
if ($('gearPop').classList.contains('open')) buildGearPop();
|
|
12969
|
+
}
|
|
12684
12970
|
async function loadStability(){
|
|
12685
12971
|
try { const r = await fetch(withKey('/api/stability')); state.stability = r.ok ? await r.json() : null; }
|
|
12686
12972
|
catch { state.stability = null; }
|
|
@@ -12772,6 +13058,8 @@ function buildGearPop(){
|
|
|
12772
13058
|
pop.innerHTML = gearTabsHTML() + gearSetHTML();
|
|
12773
13059
|
wireGearCommon(pop);
|
|
12774
13060
|
for (const b of pop.querySelectorAll('[data-gslang]')) b.onclick = () => saveLang(b.dataset.gslang);
|
|
13061
|
+
for (const b of pop.querySelectorAll('[data-grelay]')) b.onclick = () => saveRelayPref(b.dataset.grelay === 'on');
|
|
13062
|
+
for (const b of pop.querySelectorAll('[data-gabout]')) b.onclick = () => openAbout();
|
|
12775
13063
|
const signBtn = pop.querySelector('[data-gsignin]');
|
|
12776
13064
|
if (signBtn) signBtn.onclick = async () => {
|
|
12777
13065
|
const msg = pop.querySelector('#licKeyMsg');
|
|
@@ -12797,6 +13085,7 @@ function buildGearPop(){
|
|
|
12797
13085
|
if (stabilityBtn) stabilityBtn.onclick = () => { state.stability = null; buildGearPop(); loadStability(); };
|
|
12798
13086
|
loadProjectRules();
|
|
12799
13087
|
if (!state.stability) loadStability();
|
|
13088
|
+
if (!state.relayPref) loadRelayPref();
|
|
12800
13089
|
return;
|
|
12801
13090
|
}
|
|
12802
13091
|
if (custTab === 'mcp') {
|
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