@galda/cli 0.10.122 → 0.10.123

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.
Files changed (2) hide show
  1. package/app/index.html +194 -84
  2. package/package.json +1 -1
package/app/index.html CHANGED
@@ -1292,6 +1292,79 @@
1292
1292
  body[data-layout="flagship"] .gearpop{position:fixed;top:120px;right:auto;width:380px;max-height:calc(100vh - 160px);border-radius:14px;
1293
1293
  background:color-mix(in srgb,var(--panel) 74%,transparent);
1294
1294
  -webkit-backdrop-filter:blur(28px) saturate(1.35);backdrop-filter:blur(28px) saturate(1.35)}
1295
+ /* ── Settings = a centred two-column dialog (CDO 2026-08-02, pattern C) ────────────
1296
+ Four sections that each hold ten-plus controls are a sidebar's job, not a tab
1297
+ strip's: the 380px column beside the rail was showing them 380px at a time, and
1298
+ the panel covered the feed while doing it. Same parts, new frame — the tab
1299
+ buttons keep their data-gctab wiring, the sticky Save/Cancel footer keeps its
1300
+ shape, and everything below 760px still gets the bottom sheet further down.
1301
+ Scoped to flagship only: the legacy anchored popover in other layouts is
1302
+ untouched (it is 320px wide and a left rail would ruin it).
1303
+ No scrim (Masa 2026-08-02): a dimmed board made opening Settings feel like a modal
1304
+ interruption. The dialog just appears — its own shadow separates it from the board,
1305
+ and outside-tap/Esc already close it.
1306
+ ONE HEIGHT for every section (Masa 2026-08-02): the panel used to resize as you moved
1307
+ between Theme (long) and MCP (short), so the thing under your cursor jumped. The frame
1308
+ is fixed and the content column scrolls inside it. */
1309
+ body[data-layout="flagship"] .gearpop.open{display:flex;flex-direction:row;gap:22px;
1310
+ /* Straddles the rail rather than sitting dead centre (Masa 2026-08-02): landing on top
1311
+ of the rail says "this belongs to the gear you just pressed", and it leaves the feed
1312
+ — the thing you were reading — visible on the right instead of split in half. */
1313
+ top:50%;left:96px;transform:translateY(-50%);right:auto;bottom:auto;
1314
+ width:min(880px,calc(100vw - 128px));max-width:none;height:min(720px,86vh);max-height:86vh;
1315
+ border-radius:16px;padding:18px 20px 0;
1316
+ overflow:hidden; /* the .gbody column scrolls, not the frame */
1317
+ /* Lifted off the board instead of matching it: on the dark themes --panel is very near
1318
+ black, so a black dialog on a black board read as a hole rather than a surface. The
1319
+ mix is ink-based, so it lightens in the dark and stays white on the light themes. */
1320
+ background:color-mix(in srgb,var(--ink) 5%,rgb(from var(--panel) r g b));
1321
+ -webkit-backdrop-filter:none;backdrop-filter:none;
1322
+ box-shadow:0 24px 70px rgba(0,0,0,.42),0 0 0 1px var(--hair2)}
1323
+ body[data-layout="flagship"] .gearpop .gnav{flex:0 0 158px;display:flex;flex-direction:column;gap:2px;
1324
+ border-right:1px solid var(--hair);padding:2px 12px 12px 0}
1325
+ body[data-layout="flagship"] .gearpop .gnav .ggrp{font:600 9px/1 var(--mono);letter-spacing:.12em;
1326
+ text-transform:uppercase;color:var(--ink3);padding:14px 10px 6px}
1327
+ body[data-layout="flagship"] .gearpop .gnav .ggrp:first-child{padding-top:2px}
1328
+ body[data-layout="flagship"] .gearpop .gnav button{border:0;background:none;text-align:left;
1329
+ padding:8px 10px;border-radius:8px;cursor:pointer;font:500 12.5px/1 var(--ui);color:var(--ink2)}
1330
+ body[data-layout="flagship"] .gearpop .gnav button:hover{color:var(--ink)} /* glyph only (§5.5) */
1331
+ body[data-layout="flagship"] .gearpop .gnav button.on{background:var(--hover);color:var(--ink);font-weight:600}
1332
+ body[data-layout="flagship"] .gearpop .gcol{flex:1;min-width:0;display:flex;flex-direction:column}
1333
+ /* padding-bottom: the note used to end flush against the frame's edge with nowhere for
1334
+ the eye to land (Masa 2026-08-02). Also the scroll's own breathing room. */
1335
+ body[data-layout="flagship"] .gearpop .gbody{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;padding-bottom:26px}
1336
+ body[data-layout="flagship"] .gearpop .gbody > h4:first-child{margin-top:2px}
1337
+ body[data-layout="flagship"] .gearpop .cfoot{margin:14px -20px 0;padding:14px 20px;bottom:0;border-radius:0;
1338
+ background:color-mix(in srgb,var(--ink) 5%,rgb(from var(--panel) r g b))}
1339
+ /* ONE COLUMN (Masa 2026-08-02). This was two-across, and reading it was worse, not
1340
+ better: a settings row is label + description + control on one line, so pairing two
1341
+ of them puts four things on a line and the eye stops knowing which control belongs to
1342
+ which label. The width now buys long descriptions that no longer wrap, which is what
1343
+ it should have bought in the first place. The class stays a grid so the group keeps
1344
+ one place to change. */
1345
+ .gearpop .gtwo{display:grid;grid-template-columns:1fr}
1346
+ /* The free-tier Upgrade CTA is a bare button, not a .srow — as a full-width grid child
1347
+ it read as a banner. .bx-up-set pins width:100%, right in the 320px popover, wrong
1348
+ here. Keep it the size of its label, on the right edge where every other control in
1349
+ this list sits (Masa 2026-08-02) — a lone left-aligned button broke the column of
1350
+ chips it belongs to. */
1351
+ .gearpop .gtwo > .bx-cta{justify-self:end;align-self:center;margin:9px 0;width:auto}
1352
+ /* ── FEEDBACK: the founder's note, last thing in SETTINGS (Masa 2026-08-02) ────────
1353
+ A letter, not a settings row: hairline above, mono label, plain prose, one mail
1354
+ button. No fill, no badge, no icon block (§5.5). It lives inside the scrolling
1355
+ body so it never covers the sticky Save/Cancel. */
1356
+ .gearpop .gfeed{border-top:1px solid var(--hair);margin-top:13px;padding-top:13px}
1357
+ .gearpop .gfeed.gsolo{border-top:0;margin-top:2px;padding-top:0}
1358
+ .gearpop .gfeed.gsolo p{font-size:13px;line-height:1.8;max-width:56ch;margin-bottom:13px}
1359
+ .gearpop .gfeed.gsolo .gsig{font-size:12px;margin-bottom:18px}
1360
+ .gearpop .gfeed.gsolo .gmail{height:32px;padding:0 15px;font-size:12.5px}
1361
+ .gearpop .gfeed h4{margin:0 0 8px}
1362
+ .gearpop .gfeed p{font-size:11.5px;line-height:1.75;color:var(--ink2);margin:0 0 9px;max-width:54ch}
1363
+ .gearpop .gfeed .gsig{font-size:11px;color:var(--ink3);margin:0 0 11px}
1364
+ .gearpop .gfeed .gmail{display:inline-flex;align-items:center;gap:6px;height:27px;padding:0 12px;
1365
+ border-radius:8px;background:var(--chipbg);color:var(--ink);text-decoration:none;font:500 11.5px/1 var(--mono)}
1366
+ .gearpop .gfeed .gmail:hover{color:var(--ink);background:color-mix(in srgb,var(--ink) 10%,rgb(from var(--panel) r g b))}
1367
+ .gearpop .gfeed .gmail svg{width:12px;height:12px;opacity:.55;fill:none;stroke:currentColor;stroke-width:1.4}
1295
1368
  /* settings = EDIT SESSION (HANDOFF-v45 §3, flagship .cfoot/.mcpbox port): sticky footer,
1296
1369
  always visible over the internal scroll — Save = ink-solid persists, Cancel = ghost
1297
1370
  restores the open-time snapshot. */
@@ -1333,14 +1406,6 @@
1333
1406
  .gearpop .srow.dis{opacity:.38;pointer-events:none}
1334
1407
  .gearpop .srow .sl{flex:1;min-width:0;font-size:12.5px;color:var(--ink)}
1335
1408
  .gearpop .srow .sd{display:block;font-size:10.5px;color:var(--ink3);margin-top:2px}
1336
- /* Project rules read-only view (§1.5a): a quiet framed panel under its row —
1337
- mono filename label + the file body, scrollable. Reuses --canvas/--hair so it
1338
- matches the MCP box; no new surface invented. */
1339
- .gearpop .rulesview{margin:2px 0 9px;display:flex;flex-direction:column;gap:8px}
1340
- .gearpop .rulesfile{background:var(--canvas);border-radius:8px;overflow:hidden}
1341
- .gearpop .rulesname{font:600 9px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);padding:8px 10px 0}
1342
- .gearpop .rulestext{margin:6px 0 0;padding:0 10px 10px;max-height:220px;overflow:auto;font:400 11px/1.6 var(--mono);color:var(--ink2);white-space:pre-wrap;word-break:break-word}
1343
- .gearpop .rulesempty{color:var(--ink3);font-style:italic}
1344
1409
  /* MCP tab: live status dot + connect command + quiet explanation */
1345
1410
  .gearpop .mcpstat{display:flex;align-items:center;gap:8px;font:600 11px/1 var(--ui);color:var(--ink);padding:2px 0 10px}
1346
1411
  .gearpop .mcpstat i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px rgba(var(--green-rgb),.5)}
@@ -2987,6 +3052,16 @@
2987
3052
  body[data-layout="flagship"] .gearpop{left:8px!important;right:8px!important;top:auto!important;
2988
3053
  bottom:calc(env(safe-area-inset-bottom) + 8px)!important;width:auto!important;max-width:none!important;
2989
3054
  max-height:82dvh!important;border-radius:16px;padding-top:8px}
3055
+ /* The desktop dialog above centres with a transform and lays its nav out sideways —
3056
+ both have to be undone here or the sheet floats off-centre with a 158px rail. */
3057
+ body[data-layout="flagship"] .gearpop.open{transform:none!important;flex-direction:column!important;
3058
+ padding:8px 13px 0!important;gap:0!important}
3059
+ body[data-layout="flagship"] .gearpop .gnav{flex:0 0 auto;flex-direction:row;gap:6px;overflow-x:auto;
3060
+ scrollbar-width:none;border-right:0;border-bottom:1px solid var(--hair);padding:0 0 8px;margin-bottom:10px}
3061
+ body[data-layout="flagship"] .gearpop .gnav::-webkit-scrollbar{display:none}
3062
+ body[data-layout="flagship"] .gearpop .gnav .ggrp{display:none} /* group labels need a column to sit in */
3063
+ body[data-layout="flagship"] .gearpop .gnav button{white-space:nowrap;min-height:38px;padding:0 12px;font-size:13px}
3064
+ body[data-layout="flagship"] .gearpop .cfoot{margin:14px -13px 0;padding:12px 13px}
2990
3065
  body[data-layout="flagship"] .gearpop::before{content:"";position:sticky;top:0;z-index:2;display:block;
2991
3066
  width:38px;height:5px;margin:0 auto 12px;border-radius:3px;background:var(--hair2)}
2992
3067
  body[data-layout="flagship"] .gearpop .csw{width:30px;height:30px}
@@ -3255,12 +3330,6 @@
3255
3330
  same right column as the fold toggle above it; the menu opens BELOW the gear. -->
3256
3331
  <div class="lbrow"><span class="lb">Projects</span>
3257
3332
  <button class="ghostbtn morebtn" id="fsMoreBtn" title="Settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 00.34 1.87l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.7 1.7 0 00-1.87-.34 1.7 1.7 0 00-1 1.56V21a2 2 0 11-4 0v-.09a1.7 1.7 0 00-1-1.56 1.7 1.7 0 00-1.87.34l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.7 1.7 0 00.34-1.87 1.7 1.7 0 00-1.56-1H3a2 2 0 110-4h.09a1.7 1.7 0 001.56-1 1.7 1.7 0 00-.34-1.87l-.06-.06a2 2 0 112.83-2.83l.06.06a1.7 1.7 0 001.87.34h.09a1.7 1.7 0 001-1.56V3a2 2 0 114 0v.09a1.7 1.7 0 001 1.56h.09a1.7 1.7 0 001.87-.34l.06-.06a2 2 0 112.83 2.83l-.06.06a1.7 1.7 0 00-.34 1.87v.09a1.7 1.7 0 001.56 1H21a2 2 0 110 4h-.09a1.7 1.7 0 00-1.56 1z"/></svg></button>
3258
- <div class="moremenu" id="fsMoreMenu">
3259
- <button class="ghostbtn" id="fsCust">Theme</button>
3260
- <button class="ghostbtn" id="fsConn">Connect MCP</button>
3261
- <button class="ghostbtn" id="fsHist">History</button>
3262
- <button class="ghostbtn" id="fsSet">Settings</button>
3263
- </div>
3264
3333
  </div>
3265
3334
  <button class="fshamb" id="fsHamburger" title="Open To Do and Review"><svg viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h14"/></svg><span class="fshamb-lb">Review</span><span class="fshamb-n" id="fsHambN"></span></button>
3266
3335
  <div id="fsProjects"></div>
@@ -3565,7 +3634,7 @@ const savedTheme = (() => {
3565
3634
  if (v !== s) localStorage.setItem('theme', v);
3566
3635
  return v;
3567
3636
  })();
3568
- const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, queueWaits: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, hostReadiness: [], connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
3637
+ const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, queueWaits: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, hostReadiness: [], connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
3569
3638
  // per-message send overrides set via slash-commands (reset after each send,
3570
3639
  // except model which is sticky in localStorage). palette = open command list.
3571
3640
  // Scoped per project (msgByProject) so a skill/mode picked in one project can't leak
@@ -8320,8 +8389,14 @@ function renderClawdHero(){
8320
8389
  };
8321
8390
  syncFsTvSeg();
8322
8391
  tgUpdate();
8323
- $('fsMoreBtn').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.toggle('open'); };
8324
- document.addEventListener('click', (e) => { if (!e.target.closest('#fsRoot .lbrow')) $('fsMoreMenu').classList.remove('open'); });
8392
+ // One press, one dialog (Masa 2026-08-02). The gear used to open a four-item menu
8393
+ // (Theme / Connect MCP / History / Settings) whose only job was to pick a tab — and the
8394
+ // dialog now carries those same four in its own left nav, so the menu was asking people
8395
+ // to choose twice. Press the gear, get Settings; switch inside if you want something else.
8396
+ $('fsMoreBtn').onclick = (e) => {
8397
+ e.stopPropagation();
8398
+ if ($('gearPop').classList.contains('open')) custCancel(); else custOpen();
8399
+ };
8325
8400
  const closeMobilePanels = () => {
8326
8401
  document.body.classList.remove('laneopen', 'projectsopen');
8327
8402
  };
@@ -8349,11 +8424,7 @@ function renderClawdHero(){
8349
8424
  // v46 (HANDOFF-v45 §3): the menu entries deep-link into the 4-tab settings panel —
8350
8425
  // Theme→THEME / Connect MCP→MCP / History→HISTORY / Settings→SETTINGS.
8351
8426
  // stopPropagation: the document click-away handler otherwise closes it in the same event.
8352
- $('fsCust').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('theme'); };
8353
- $('fsConn').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('mcp'); };
8354
- $('fsHist').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('history'); };
8355
- $('fsSet').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('settings'); };
8356
- // parallel-runs switcher (§1.5-1/-7): same toggle+click-away shape as fsMoreMenu above.
8427
+ // parallel-runs switcher (§1.5-1/-7): toggle + click-away, the shape the rail's own menus use.
8357
8428
  // Row content is rebuilt by renderFsFeed() (running list changes live); the open/closed
8358
8429
  // class here is left alone by that rebuild so the popover doesn't snap shut mid-render.
8359
8430
  $('fsRunSwitch').onclick = (e) => { e.stopPropagation(); $('fsRunMenu').classList.toggle('open'); };
@@ -12762,11 +12833,38 @@ function applyCust(){
12762
12833
  // histHTML/setHTML/mcpHTML port). custTab is session UI state, never persisted; switching
12763
12834
  // tabs re-renders the panel WITHOUT touching the edit-session snapshot (custSnap) and
12764
12835
  // without closing. =====
12765
- let custTab = 'theme';
12836
+ let custTab = 'settings'; // the gear opens on Settings (Masa 2026-08-02): themes are a once-a-month errand, settings are the reason people press it
12766
12837
  function gearTabsHTML(){
12767
12838
  return `<div class="ctabs">${[['theme', 'THEME'], ['history', 'HISTORY'], ['settings', 'SETTINGS'], ['mcp', 'MCP']]
12768
12839
  .map(([k, l]) => `<button class="ctab${custTab === k ? ' on' : ''}" data-gctab="${k}">${l}</button>`).join('')}</div>`;
12769
12840
  }
12841
+ // Flagship = the centred dialog (CDO pattern C): the four tabs become a left rail,
12842
+ // grouped by what they are actually about instead of sitting in one flat strip.
12843
+ // Same data-gctab contract as the tab buttons, so wireGearCommon needs no change.
12844
+ // Theme sits LAST (Masa 2026-08-02): it is the section people visit least, and it was
12845
+ // taking the top slot — the position a reader reads first — from the one they came for.
12846
+ const GEAR_NAV = [
12847
+ ['Project', [['settings', 'Settings'], ['history', 'History'], ['mcp', 'MCP']]],
12848
+ // Its own entry under MCP (Masa 2026-08-02) as well as the note at the foot of Settings:
12849
+ // a person who has decided to say something should not have to scroll a settings list to
12850
+ // find where. The heading is Contact, not Feedback — a section labelled the same as its
12851
+ // only item is a label for nothing.
12852
+ ['Contact', [['feedback', 'Feedback']]],
12853
+ ['Appearance', [['theme', 'Theme']]],
12854
+ ];
12855
+ function gearNavHTML(){
12856
+ return `<nav class="gnav">${GEAR_NAV.map(([grp, items]) =>
12857
+ `<div class="ggrp">${grp}</div>` + items.map(([k, l]) =>
12858
+ `<button class="${custTab === k ? 'on' : ''}" data-gctab="${k}">${l}</button>`).join('')).join('')}</nav>`;
12859
+ }
12860
+ // The dialog only exists in flagship; every other layout keeps the anchored popover
12861
+ // and its top tab strip untouched.
12862
+ function gearIsDialog(){ return state.layout === 'flagship'; }
12863
+ function gearShell(html){
12864
+ return gearIsDialog()
12865
+ ? gearNavHTML() + `<div class="gcol"><div class="gbody">${html}</div></div>`
12866
+ : gearTabsHTML() + html;
12867
+ }
12770
12868
  // HISTORY (v46): approve/dismiss history from goal state — deterministic, zero LLM.
12771
12869
  // HONEST DATA NOTE: the engine stamps no decidedAt on approve/dismiss (they only flip
12772
12870
  // goal.status — approveGoal/dismissGoal in lib.mjs), and a dismissed goal goes BACK to
@@ -12834,20 +12932,51 @@ function gearSetHTML(){
12834
12932
  : acct + freeUsage;
12835
12933
  const st = state.stability;
12836
12934
  const stability = st ? `<div class="srow"><div class="sl">Verification health<span class="sd">Last 24h · implementation ${st.implementationFailures} · environment ${st.verificationInfrastructure} · inconclusive ${st.timeoutsOrInconclusive} · worker holds ${st.workerHolds}</span></div><button class="optchip" data-gstability="1">Refresh</button></div>` : `<div class="srow"><div class="sl">Verification health<span class="sd">Loading…</span></div></div>`;
12837
- return `
12838
- <div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
12935
+ const langRow = `<div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
12839
12936
  <button class="optchip${lang === 'auto' ? ' on' : ''}" data-gslang="auto">Auto</button>
12840
12937
  <button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
12841
- <button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>
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()}
12846
- ${shareUsageDataRow()}
12847
- ${stability}
12848
- ${gearRulesHTML()}
12849
- ${hostReadinessRow()}
12850
- <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>`;
12938
+ <button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>`;
12939
+ const aboutRow = `<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>
12940
+ <button class="optchip" data-gabout="1">Open</button></div>`;
12941
+ const dataRow = `<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>`;
12942
+ // Grouped headings + two-across rows (CDO pattern C, Masa 2026-08-02). The flat list
12943
+ // gave a reader no way to tell "settings for this project" from "settings for this
12944
+ // account" — the names were already there in the rows, only the structure was missing.
12945
+ //
12946
+ // A group with nothing in it renders nothing, heading included (§1.5-3 no empty box):
12947
+ // remoteAccessRow/shareUsageDataRow/hostReadinessRow all return '' when the engine has
12948
+ // not configured them, and on such a host the heading alone would be a label for air.
12949
+ const group = (title, rows) => {
12950
+ const body = rows.filter(Boolean).join('');
12951
+ return body ? `<h4>${title}</h4><div class="gtwo">${body}</div>` : '';
12952
+ };
12953
+ return group('Project', [langRow, remoteAccessRow(), shareUsageDataRow()])
12954
+ + group('Account', [billingRow])
12955
+ // "System", not "This machine": hostReadinessRow() already labels itself
12956
+ // "This machine" (and a test asserts that exact label), so the heading would
12957
+ // have been the same words twice, three rows apart.
12958
+ + group('System', [stability, aboutRow, hostReadinessRow(), dataRow])
12959
+ + gearFeedbackHTML();
12960
+ }
12961
+ // The founder's note (Masa 2026-08-02). It is the last thing in SETTINGS because it
12962
+ // is the one row that asks something of the reader rather than offering a control,
12963
+ // and because a person who got this far has already formed an opinion.
12964
+ //
12965
+ // The honest limitation is the point of the copy, not an apology: few users so far
12966
+ // means workflows we have not met, and saying so is what earns the reply. Keep the
12967
+ // promise in it something Leon can actually keep.
12968
+ // `solo` = the Contact tab, where the note is the whole screen rather than the last thing
12969
+ // under a list: no hairline above it (there is nothing above it to separate from) and room
12970
+ // to breathe. Same words either way — one place to change them.
12971
+ function gearFeedbackHTML({ solo = false } = {}){
12972
+ return `<div class="gfeed${solo ? ' gsolo' : ''}">
12973
+ <h4>Feedback</h4>
12974
+ <p>Galda is early. Not many people are using it yet, so there are workflows it handles badly — quite possibly parts of yours.</p>
12975
+ <p>If it doesn't fit the way you work, write to me. I'd rather hear it straight and make it fit than guess.</p>
12976
+ <p class="gsig">— Leon, co-founder</p>
12977
+ <a class="gmail" href="mailto:hello@galda.app?subject=Galda%20feedback">
12978
+ <svg viewBox="0 0 16 16"><rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path d="M2 4.5l6 4 6-4"/></svg>hello@galda.app</a>
12979
+ </div>`;
12851
12980
  }
12852
12981
  // The npx moment, moved to where the intent is.
12853
12982
  //
@@ -13040,36 +13169,16 @@ function hostReadinessRow(){
13040
13169
  const detail = (missing.length ? missing : checks).map((c) => c.detail).join(' · ');
13041
13170
  return `<div class="srow${missing.length ? '' : ' dis'}"><div class="sl">This machine<span class="sd">${esc(detail)}</span></div></div>`;
13042
13171
  }
13043
- function gearRulesHTML(){
13044
- const r = state.projectRules[state.active];
13045
- if (!r || !r.files || !r.files.length) return '';
13046
- const names = r.files.map((f) => f.name).join(' · ');
13047
- const view = state.rulesOpen
13048
- ? `<div class="rulesview">${r.files.map((f) =>
13049
- `<div class="rulesfile"><div class="rulesname">${esc(f.name)}</div>`
13050
- + `<pre class="rulestext">${esc(f.content) || '<span class="rulesempty">(empty file)</span>'}</pre></div>`).join('')}</div>`
13051
- : '';
13052
- return `
13053
- <div class="srow"><div class="sl">Project rules<span class="sd">What this project tells the AI — read-only · ${esc(names)}</span></div>
13054
- <button class="optchip${state.rulesOpen ? ' on' : ''}" data-grules="1">${state.rulesOpen ? 'Hide' : 'View'}</button></div>
13055
- ${view}`;
13056
- }
13057
- // Fetch the active project's rules files once (cached per project), then rebuild
13058
- // the settings panel so the row appears. The browser can't read the host FS, so
13059
- // this is the only way in — GET /api/projects/:id/rules (engine, §1.5a).
13060
- let rulesFetching = null;
13061
- async function loadProjectRules(){
13062
- const pid = state.active;
13063
- if (state.projectRules[pid] || rulesFetching === pid) return;
13064
- rulesFetching = pid;
13065
- try {
13066
- const r = await fetch(withKey(`/api/projects/${pid}/rules`));
13067
- state.projectRules[pid] = r.ok ? await r.json() : { files: [] };
13068
- } catch { state.projectRules[pid] = { files: [] }; }
13069
- finally { rulesFetching = null; }
13070
- // Only rebuild if the settings tab is still the one on screen.
13071
- if ($('gearPop') && custTab === 'settings') buildGearPop();
13072
- }
13172
+ // NO "Project rules" row (Masa 2026-08-02). It used to print CLAUDE.md / AGENTS.md
13173
+ // read-only in Settings. What ONE-CONVERSATION §1.5a actually decided was a NEGATIVE:
13174
+ // Galda must not grow its own "how we work" field, because the same rules would then
13175
+ // live in two places and drift — the repo already holds the answer. That decision does
13176
+ // not need a control to be honoured. The viewer was the incidental part, and it showed
13177
+ // a person their own file, which they can open in an editor. Removed.
13178
+ //
13179
+ // The engine keeps collectProjectRules (workerRunContext still names the files it found)
13180
+ // and its GET /api/projects/:id/rules, which nothing calls now — engine is a separate
13181
+ // lane, so that endpoint is a follow-up, not a silent edit from here.
13073
13182
  // MCP (v46): connection status dot + connectCommand + COPY + quiet explanation.
13074
13183
  // The dot is the app's own live engine link (SSE up/down) — the only real signal here.
13075
13184
  function gearMcpHTML(){
@@ -13086,7 +13195,7 @@ function buildGearPop(){
13086
13195
  // custRender, the sticky Save/Cancel footer belongs to THEME (the live-preview tab);
13087
13196
  // the edit session itself stays open across tabs (Esc/outside still Cancel).
13088
13197
  if (custTab === 'history') {
13089
- pop.innerHTML = gearTabsHTML() + gearHistHTML();
13198
+ pop.innerHTML = gearShell(gearHistHTML());
13090
13199
  wireGearCommon(pop);
13091
13200
  for (const b of pop.querySelectorAll('[data-ghmore]')) b.onclick = () => {
13092
13201
  const r = b.closest('.hrow'); const ex = r.classList.toggle('ex'); b.textContent = ex ? 'Less' : 'More';
@@ -13099,7 +13208,7 @@ function buildGearPop(){
13099
13208
  return;
13100
13209
  }
13101
13210
  if (custTab === 'settings') {
13102
- pop.innerHTML = gearTabsHTML() + gearSetHTML();
13211
+ pop.innerHTML = gearShell(gearSetHTML());
13103
13212
  wireGearCommon(pop);
13104
13213
  for (const b of pop.querySelectorAll('[data-gslang]')) b.onclick = () => saveLang(b.dataset.gslang);
13105
13214
  for (const b of pop.querySelectorAll('[data-grelay]')) b.onclick = () => saveRelayPref(b.dataset.grelay === 'on');
@@ -13124,18 +13233,20 @@ function buildGearPop(){
13124
13233
  catch {}
13125
13234
  finally { await refresh(); if ($('gearPop')) buildGearPop(); }
13126
13235
  };
13127
- const rulesBtn = pop.querySelector('[data-grules]');
13128
- if (rulesBtn) rulesBtn.onclick = () => { state.rulesOpen = !state.rulesOpen; buildGearPop(); };
13129
13236
  const stabilityBtn = pop.querySelector('[data-gstability]');
13130
13237
  if (stabilityBtn) stabilityBtn.onclick = () => { state.stability = null; buildGearPop(); loadStability(); };
13131
- loadProjectRules();
13132
13238
  if (!state.stability) loadStability();
13133
13239
  if (!state.relayPref) loadRelayPref();
13134
13240
  if (!state.analyticsPref) loadAnalyticsPref();
13135
13241
  return;
13136
13242
  }
13243
+ if (custTab === 'feedback') {
13244
+ pop.innerHTML = gearShell(gearFeedbackHTML({ solo: true }));
13245
+ wireGearCommon(pop);
13246
+ return;
13247
+ }
13137
13248
  if (custTab === 'mcp') {
13138
- pop.innerHTML = gearTabsHTML() + gearMcpHTML();
13249
+ pop.innerHTML = gearShell(gearMcpHTML());
13139
13250
  wireGearCommon(pop);
13140
13251
  const mc2 = pop.querySelector('[data-gmcpcopy]'); if (mc2) mc2.onclick = () => {
13141
13252
  navigator.clipboard?.writeText(state.connectCommand || '').then(() => { mc2.textContent = 'COPIED'; setTimeout(() => { mc2.textContent = 'COPY'; }, 1500); });
@@ -13206,7 +13317,7 @@ function buildGearPop(){
13206
13317
  const th = state.theme || 'gradient';
13207
13318
  const sec = (on, html) => `<div class="gsec${on ? '' : ' dis'}">${html}</div>`;
13208
13319
  // FONT exploration section retired (v45 §1): the brand fonts are final — no chips.
13209
- pop.innerHTML = gearTabsHTML() +
13320
+ pop.innerHTML = gearShell(
13210
13321
  `<h4>THEME</h4><div class="swrow">${themeBtns}</div>` +
13211
13322
  `<h4>BOARD</h4><div class="swrow">${boardChips}</div>` +
13212
13323
  sec(th === 'gradient', `<h4>COLOR</h4><div class="swrow">${arcSw}</div>` +
@@ -13224,7 +13335,7 @@ function buildGearPop(){
13224
13335
  (cust.btn === 0 ? `<h4>RING</h4><div class="swrow">${ringSw}</div>` : '') +
13225
13336
  // LOGO row removed (v45 §3: code-fixed) · MCP CONNECT moved to the MCP tab (v46)
13226
13337
  // sticky footer (v45 §3, flagship .cfoot): Save = persist NOW / Cancel = snapshot restore
13227
- `<div class="cfoot"><button class="cbtn cancel" data-gccancel="1">Cancel</button><button class="cbtn save" data-gcsave="1">Save</button></div>`;
13338
+ `<div class="cfoot"><button class="cbtn cancel" data-gccancel="1">Cancel</button><button class="cbtn save" data-gcsave="1">Save</button></div>`);
13228
13339
  wireGearCommon(pop);
13229
13340
  // EDIT SESSION (v45 §3): every handler below is LIVE PREVIEW ONLY — mutate cust/state,
13230
13341
  // apply to the DOM, re-render the pop. localStorage is written ONLY in custSave().
@@ -13256,16 +13367,14 @@ function buildGearPop(){
13256
13367
  // live previews that never persist and never close the panel; Save is the only moment
13257
13368
  // localStorage is written; Cancel / Esc / outside-tap restore the snapshot fully. =====
13258
13369
  let custSnap = null;
13259
- function custPlace(){ // flagship: top-anchored band beside the rail (v45 §2); other layouts keep the .gearwrap anchor
13260
- const pop = $('gearPop');
13261
- if (state.layout === 'flagship') {
13262
- const rail = document.querySelector('#fsRoot .fsrail');
13263
- if (rail) pop.style.left = Math.round(rail.getBoundingClientRect().right + 8) + 'px';
13264
- } else pop.style.left = '';
13370
+ function custPlace(){ // flagship: centred dialog (CDO pattern C) CSS owns the position now,
13371
+ // so the measured rail edge that used to be written here must be cleared, or the inline
13372
+ // left would fight left:50% and the dialog would sit off-centre. Other layouts keep the
13373
+ // .gearwrap anchor and never had an inline left to begin with.
13374
+ $('gearPop').style.left = '';
13265
13375
  }
13266
13376
  function custOpen(tab){
13267
- custTab = tab || 'theme'; // v46 deep-link (flagship __custOpen(tab)): menu entries land on their tab
13268
- state.rulesOpen = false; // rules panel starts collapsed each time the gear opens
13377
+ custTab = tab || 'settings'; // v46 deep-link (flagship __custOpen(tab)): menu entries land on their tab
13269
13378
  custSnap = { cust: { ...cust }, theme: state.theme, band: fsUI.band };
13270
13379
  buildGearPop(); custPlace();
13271
13380
  $('gearPop').classList.add('open');
@@ -13320,15 +13429,16 @@ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('gearP
13320
13429
  $('gearBtn').onclick = (e) => { e.stopPropagation(); const p = $('gearPop'); if (p.classList.contains('open')) custCancel(); else custOpen(); };
13321
13430
  // outside-tap = Cancel (v45 §3). e.composedPath() is frozen at dispatch, so a swatch that
13322
13431
  // buildGearPop() re-rendered (detached) mid-click still reads as INSIDE the pop —
13323
- // selections never close the panel. Known trap (v45 §3): #fsMoreMenu clicks are excluded
13324
- // so a menu item is never swallowed as an outside tap. #deleteConfirmOverlay is excluded
13432
+ // selections never close the panel. #fsMoreBtn (the rail gear) is excluded so its own
13433
+ // click is never read as an outside tap otherwise pressing it would close and reopen the
13434
+ // dialog in a single press. #deleteConfirmOverlay is excluded
13325
13435
  // too (Masa指示 2026-07-16): it's a separate top-level overlay a settings row can open
13326
13436
  // (e.g. Log out) — without this, confirming/cancelling it read as an outside tap and
13327
13437
  // closed the settings panel underneath it as a surprise side effect.
13328
13438
  document.addEventListener('click', (e) => {
13329
13439
  const p = $('gearPop'); if (!p.classList.contains('open')) return;
13330
13440
  if (e.composedPath().includes(p)) return;
13331
- if (e.target.closest('.gearwrap') || e.target.closest('#fsCust') || e.target.closest('#fsMoreMenu') || e.target.closest('#fsMoreBtn') || e.target.closest('#deleteConfirmOverlay')) return;
13441
+ if (e.target.closest('.gearwrap') || e.target.closest('#fsMoreBtn') || e.target.closest('#deleteConfirmOverlay')) return;
13332
13442
  custCancel();
13333
13443
  });
13334
13444
  // Banff BACKDROP free upload (v3 §6.5): file → dataURL → #themebg. Persists via
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.122",
3
+ "version": "0.10.123",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {