@galda/cli 0.10.122 → 0.10.124

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 +614 -107
  2. package/package.json +1 -1
package/app/index.html CHANGED
@@ -237,8 +237,11 @@
237
237
  .composer-wrap{flex:0 0 auto;padding:8px 22px 18px}
238
238
  .composer{position:relative;max-width:640px;margin:0 auto;display:flex;flex-direction:column;gap:8px;border:1px solid var(--hair2);
239
239
  border-radius:16px;background:var(--glass);backdrop-filter:blur(22px);padding:10px 12px}
240
+ /* max-height reads the same --compmax dial the JS auto-grow uses. It was a second,
241
+ independent 120 here, so raising the JS cap did nothing — the box stayed at 120 while
242
+ the dial said 280 (measured). One number, two consumers. */
240
243
  .composer textarea{background:transparent;border:0;resize:none;color:var(--ink);font:inherit;font-size:14px;
241
- line-height:1.5;outline:none;max-height:120px;padding:2px 2px 0}
244
+ line-height:1.5;outline:none;max-height:var(--compmax,120px);padding:2px 2px 0}
242
245
  .composer textarea::placeholder{color:var(--ink3)}
243
246
  /* slash-command palette — floats above the whole composer (Claude-Code style).
244
247
  Reuses the .appmenu/.skillmenu popover language: 1px hairline, tokens, no
@@ -1292,6 +1295,79 @@
1292
1295
  body[data-layout="flagship"] .gearpop{position:fixed;top:120px;right:auto;width:380px;max-height:calc(100vh - 160px);border-radius:14px;
1293
1296
  background:color-mix(in srgb,var(--panel) 74%,transparent);
1294
1297
  -webkit-backdrop-filter:blur(28px) saturate(1.35);backdrop-filter:blur(28px) saturate(1.35)}
1298
+ /* ── Settings = a centred two-column dialog (CDO 2026-08-02, pattern C) ────────────
1299
+ Four sections that each hold ten-plus controls are a sidebar's job, not a tab
1300
+ strip's: the 380px column beside the rail was showing them 380px at a time, and
1301
+ the panel covered the feed while doing it. Same parts, new frame — the tab
1302
+ buttons keep their data-gctab wiring, the sticky Save/Cancel footer keeps its
1303
+ shape, and everything below 760px still gets the bottom sheet further down.
1304
+ Scoped to flagship only: the legacy anchored popover in other layouts is
1305
+ untouched (it is 320px wide and a left rail would ruin it).
1306
+ No scrim (Masa 2026-08-02): a dimmed board made opening Settings feel like a modal
1307
+ interruption. The dialog just appears — its own shadow separates it from the board,
1308
+ and outside-tap/Esc already close it.
1309
+ ONE HEIGHT for every section (Masa 2026-08-02): the panel used to resize as you moved
1310
+ between Theme (long) and MCP (short), so the thing under your cursor jumped. The frame
1311
+ is fixed and the content column scrolls inside it. */
1312
+ body[data-layout="flagship"] .gearpop.open{display:flex;flex-direction:row;gap:22px;
1313
+ /* Straddles the rail rather than sitting dead centre (Masa 2026-08-02): landing on top
1314
+ of the rail says "this belongs to the gear you just pressed", and it leaves the feed
1315
+ — the thing you were reading — visible on the right instead of split in half. */
1316
+ top:50%;left:96px;transform:translateY(-50%);right:auto;bottom:auto;
1317
+ width:min(880px,calc(100vw - 128px));max-width:none;height:min(720px,86vh);max-height:86vh;
1318
+ border-radius:16px;padding:18px 20px 0;
1319
+ overflow:hidden; /* the .gbody column scrolls, not the frame */
1320
+ /* Lifted off the board instead of matching it: on the dark themes --panel is very near
1321
+ black, so a black dialog on a black board read as a hole rather than a surface. The
1322
+ mix is ink-based, so it lightens in the dark and stays white on the light themes. */
1323
+ background:color-mix(in srgb,var(--ink) 5%,rgb(from var(--panel) r g b));
1324
+ -webkit-backdrop-filter:none;backdrop-filter:none;
1325
+ box-shadow:0 24px 70px rgba(0,0,0,.42),0 0 0 1px var(--hair2)}
1326
+ body[data-layout="flagship"] .gearpop .gnav{flex:0 0 158px;display:flex;flex-direction:column;gap:2px;
1327
+ border-right:1px solid var(--hair);padding:2px 12px 12px 0}
1328
+ body[data-layout="flagship"] .gearpop .gnav .ggrp{font:600 9px/1 var(--mono);letter-spacing:.12em;
1329
+ text-transform:uppercase;color:var(--ink3);padding:14px 10px 6px}
1330
+ body[data-layout="flagship"] .gearpop .gnav .ggrp:first-child{padding-top:2px}
1331
+ body[data-layout="flagship"] .gearpop .gnav button{border:0;background:none;text-align:left;
1332
+ padding:8px 10px;border-radius:8px;cursor:pointer;font:500 12.5px/1 var(--ui);color:var(--ink2)}
1333
+ body[data-layout="flagship"] .gearpop .gnav button:hover{color:var(--ink)} /* glyph only (§5.5) */
1334
+ body[data-layout="flagship"] .gearpop .gnav button.on{background:var(--hover);color:var(--ink);font-weight:600}
1335
+ body[data-layout="flagship"] .gearpop .gcol{flex:1;min-width:0;display:flex;flex-direction:column}
1336
+ /* padding-bottom: the note used to end flush against the frame's edge with nowhere for
1337
+ the eye to land (Masa 2026-08-02). Also the scroll's own breathing room. */
1338
+ body[data-layout="flagship"] .gearpop .gbody{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;padding-bottom:26px}
1339
+ body[data-layout="flagship"] .gearpop .gbody > h4:first-child{margin-top:2px}
1340
+ body[data-layout="flagship"] .gearpop .cfoot{margin:14px -20px 0;padding:14px 20px;bottom:0;border-radius:0;
1341
+ background:color-mix(in srgb,var(--ink) 5%,rgb(from var(--panel) r g b))}
1342
+ /* ONE COLUMN (Masa 2026-08-02). This was two-across, and reading it was worse, not
1343
+ better: a settings row is label + description + control on one line, so pairing two
1344
+ of them puts four things on a line and the eye stops knowing which control belongs to
1345
+ which label. The width now buys long descriptions that no longer wrap, which is what
1346
+ it should have bought in the first place. The class stays a grid so the group keeps
1347
+ one place to change. */
1348
+ .gearpop .gtwo{display:grid;grid-template-columns:1fr}
1349
+ /* The free-tier Upgrade CTA is a bare button, not a .srow — as a full-width grid child
1350
+ it read as a banner. .bx-up-set pins width:100%, right in the 320px popover, wrong
1351
+ here. Keep it the size of its label, on the right edge where every other control in
1352
+ this list sits (Masa 2026-08-02) — a lone left-aligned button broke the column of
1353
+ chips it belongs to. */
1354
+ .gearpop .gtwo > .bx-cta{justify-self:end;align-self:center;margin:9px 0;width:auto}
1355
+ /* ── FEEDBACK: the founder's note, last thing in SETTINGS (Masa 2026-08-02) ────────
1356
+ A letter, not a settings row: hairline above, mono label, plain prose, one mail
1357
+ button. No fill, no badge, no icon block (§5.5). It lives inside the scrolling
1358
+ body so it never covers the sticky Save/Cancel. */
1359
+ .gearpop .gfeed{border-top:1px solid var(--hair);margin-top:13px;padding-top:13px}
1360
+ .gearpop .gfeed.gsolo{border-top:0;margin-top:2px;padding-top:0}
1361
+ .gearpop .gfeed.gsolo p{font-size:13px;line-height:1.8;max-width:56ch;margin-bottom:13px}
1362
+ .gearpop .gfeed.gsolo .gsig{font-size:12px;margin-bottom:18px}
1363
+ .gearpop .gfeed.gsolo .gmail{height:32px;padding:0 15px;font-size:12.5px}
1364
+ .gearpop .gfeed h4{margin:0 0 8px}
1365
+ .gearpop .gfeed p{font-size:11.5px;line-height:1.75;color:var(--ink2);margin:0 0 9px;max-width:54ch}
1366
+ .gearpop .gfeed .gsig{font-size:11px;color:var(--ink3);margin:0 0 11px}
1367
+ .gearpop .gfeed .gmail{display:inline-flex;align-items:center;gap:6px;height:27px;padding:0 12px;
1368
+ border-radius:8px;background:var(--chipbg);color:var(--ink);text-decoration:none;font:500 11.5px/1 var(--mono)}
1369
+ .gearpop .gfeed .gmail:hover{color:var(--ink);background:color-mix(in srgb,var(--ink) 10%,rgb(from var(--panel) r g b))}
1370
+ .gearpop .gfeed .gmail svg{width:12px;height:12px;opacity:.55;fill:none;stroke:currentColor;stroke-width:1.4}
1295
1371
  /* settings = EDIT SESSION (HANDOFF-v45 §3, flagship .cfoot/.mcpbox port): sticky footer,
1296
1372
  always visible over the internal scroll — Save = ink-solid persists, Cancel = ghost
1297
1373
  restores the open-time snapshot. */
@@ -1333,14 +1409,6 @@
1333
1409
  .gearpop .srow.dis{opacity:.38;pointer-events:none}
1334
1410
  .gearpop .srow .sl{flex:1;min-width:0;font-size:12.5px;color:var(--ink)}
1335
1411
  .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
1412
  /* MCP tab: live status dot + connect command + quiet explanation */
1345
1413
  .gearpop .mcpstat{display:flex;align-items:center;gap:8px;font:600 11px/1 var(--ui);color:var(--ink);padding:2px 0 10px}
1346
1414
  .gearpop .mcpstat i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px rgba(var(--green-rgb),.5)}
@@ -1690,7 +1758,12 @@
1690
1758
  #fsRoot.lanefold .lanetg{left:50%;margin-left:-14px} /* no rotation (v45 §4) */
1691
1759
  #fsRoot.lanefold .fslane{flex-basis:44px;padding:18px 7px}
1692
1760
  #fsRoot.lanefold .fslane>*:not(.lanetg){display:none}
1693
- #fsRoot .lanegrip{position:absolute;left:-3px;top:0;bottom:0;width:7px;cursor:col-resize;z-index:2}
1761
+ /* The grip sits on the edge that faces the chat. It was pinned to the lane's LEFT,
1762
+ which was right while the lane lived on the right of the chat — once the board took
1763
+ the middle (swapmid) that edge faces the rail and the board could not be widened at
1764
+ all from its own right side (Masa 2026-08-03: 右側の幅が変えられない). */
1765
+ #fsRoot .lanegrip{position:absolute;left:-3px;right:auto;top:0;bottom:0;width:7px;cursor:col-resize;z-index:2}
1766
+ #fsRoot.swapmid .lanegrip{left:auto;right:-3px}
1694
1767
  #fsRoot .lanegrip::after{content:"";position:absolute;left:3px;top:50%;transform:translateY(-50%);width:2px;height:34px;border-radius:2px;background:var(--hair2);opacity:0;transition:opacity .15s}
1695
1768
  #fsRoot .fslane:hover .lanegrip::after{opacity:1}
1696
1769
  #fsRoot .lanebody{display:flex;flex-direction:column;min-height:0;flex:1;overflow-y:auto;overflow-x:hidden;scrollbar-width:none}
@@ -1732,7 +1805,81 @@
1732
1805
  /* ---- kanban board: fills the SAME lane slot the To Do list uses (var(--lanew-board), independent
1733
1806
  from the list's own var(--lanew)) — swapping List⇄Board never resizes the lane or the chat.
1734
1807
  5+ columns don't fit the lane width, so it scrolls horizontally in place instead of growing. ---- */
1735
- #fsRoot .fslane.boardmode{flex-basis:var(--lanew-board,372px)}
1808
+ /* Unset = the board fills the row and the CHAT's min-width is the only thing that
1809
+ stops it (Masa 2026-08-03: 既定を kanban に・添付画像くらいの幅で). Letting flexbox
1810
+ hold the floor beats computing a width in JS: the boot-order guess came out 60px
1811
+ wide and the composer's button row clipped by 72px, and a JS correction loop then
1812
+ overshot the other way (lane 41% of the window at 1280). --lanew-board is still the
1813
+ one dial the drag grip writes, so a dragged width keeps winning. */
1814
+ #fsRoot .fslane.boardmode{flex-basis:var(--lanew-board,100%);flex-shrink:1;min-width:280px}
1815
+ /* 460, not the 440 the JS constant claims. Measured by walking the chat's width in 4px
1816
+ steps and watching .crow overflow: it clears at 452 and still spills 12px at 440, so
1817
+ the old figure had drifted from whatever the composer's button row costs today. 460
1818
+ leaves a little slack. Below this those chips clip off the card's edge. It only bites
1819
+ in board mode — in List the chat is ~848px and nowhere near it. .fscenter sits BEFORE
1820
+ .fslane in the DOM, so no sibling selector reaches it; the state is mirrored onto
1821
+ #fsRoot instead. .fscenter's own min-width:0 stays untouched — that one is load-bearing
1822
+ (it is what stops a wide lane from overflowing the page). */
1823
+ #fsRoot.boardmode .fscenter{min-width:460px}
1824
+
1825
+ /* ── Board view puts the composer UNDER the board (Masa 2026-08-03, variant b) ─────
1826
+ The board took the middle, which left the composer in the 460px right-hand column
1827
+ where it read cramped. It now spans the board's own width, below it, punching through
1828
+ the lane boundary — near where it sat before the board moved.
1829
+ It has to be a DOM move, not just CSS: #fsCompwrap's parent .fscenter is itself
1830
+ position:relative AND overflow:hidden, so an absolutely-positioned composer anchored
1831
+ to the chat column and got clipped by it (measured: host at 1160-1620 on a 1440px
1832
+ window — off-screen and cropped). #fsCompBar is a direct child of #fsRoot instead. */
1833
+ /* How tall the textarea may grow before it scrolls. It was a hard 120 in three
1834
+ separate places; now one dial, draggable by the handle above the card and reset by
1835
+ double-clicking it (Masa 2026-08-03: 自由にかえる・デフォルトに戻す). */
1836
+ :root{--compmax:120px}
1837
+ /* No visible grab line (Masa 2026-08-03: いらなそう). The top edge of the composer is
1838
+ simply a resize zone — the cursor says so on hover, which is how the rail and lane
1839
+ grips already work. Full width so it is easy to land on. */
1840
+ .comphandle{display:none}
1841
+ body[data-layout="flagship"] .comphandle{display:block;height:10px;margin:0 0 -6px;width:100%;
1842
+ cursor:ns-resize;position:relative;touch-action:none}
1843
+ #fsRoot .fscompbar{display:none}
1844
+ /* Drop targets while the composer is being dragged (§5.5: a line, not a filled box). */
1845
+ #fsRoot.compdragging .fscompbar,#fsRoot.compdragging .fscenter{outline:1px dashed var(--hair2);outline-offset:-4px;border-radius:12px}
1846
+ #fsRoot.compdragging .comp-over{outline-color:var(--ink3)}
1847
+ /* Once the composer moves to the chat this bar is empty, so it collapses to zero height
1848
+ and there is nothing left to drop on — the composer could go one way and never come
1849
+ back (Masa 2026-08-03: kanban に戻せなくなってる). It only takes up room WHILE a drag
1850
+ is in progress, so an empty box is never sitting there the rest of the time (§1.5-3). */
1851
+ #fsRoot.compdragging .fscompbar{min-height:72px}
1852
+ /* While it is being carried: it follows the cursor, sits above everything, and stops
1853
+ receiving hits so the column under the pointer is what gets highlighted. */
1854
+ #composerWrap.compdrag-lift{position:relative;z-index:40;pointer-events:none;
1855
+ box-shadow:0 18px 44px rgba(0,0,0,.42);opacity:.94;transition:none}
1856
+ body.compdrag-noselect,body.compdrag-noselect *{user-select:none!important;-webkit-user-select:none!important}
1857
+ #fsRoot.compdragging .fscompbar::after{content:"Composer";position:absolute;left:0;right:0;top:50%;
1858
+ transform:translateY(-50%);text-align:center;font:600 10px/1 var(--mono);letter-spacing:.12em;
1859
+ text-transform:uppercase;color:var(--ink3);pointer-events:none}
1860
+ #fsRoot.compdragging .fscompbar:has(#composerWrap)::after{content:none}
1861
+ /* Moving it: grab the composer anywhere that is not something you can type in or press
1862
+ (Masa 2026-08-03: どこを掴めるか分からない). The cursor is the whole affordance — no
1863
+ extra handle to find. */
1864
+ body[data-layout="flagship"] #fsRoot.boardmode #composerWrap{cursor:grab}
1865
+ body[data-layout="flagship"] #fsRoot.boardmode #composerWrap:active{cursor:grabbing}
1866
+ /* Only the textarea is named. Forcing cursor:pointer on every button in here overrode
1867
+ the ones that deliberately say something else — the folder row goes cursor:default
1868
+ while a chooser is already open so it stops advertising itself as pressable, and this
1869
+ rule was louder than that (caught by folder-picker-single-dialog). Buttons already
1870
+ carry their own cursor; leave them alone. */
1871
+ body[data-layout="flagship"] #fsRoot.boardmode #composerWrap textarea{cursor:text}
1872
+ #fsRoot.boardmode .fscompbar{display:block;position:absolute;z-index:6;
1873
+ left:18px;right:18px;bottom:14px} /* 18px = .fslane's own side padding */
1874
+ /* The board stops above the composer. The reserve is the bar's MEASURED height, not a
1875
+ constant: the box is user-resizable now, so any fixed number is wrong the moment
1876
+ someone drags it (and 136 already left the board's scrollbar 7px underneath). */
1877
+ #fsRoot.boardmode .fslane{padding-bottom:calc(var(--compbarh,124px) + 20px)}
1878
+ @media (max-width: 760px){
1879
+ #fsRoot.boardmode .fscompbar{display:none}
1880
+ #fsRoot.boardmode .fslane{padding-bottom:0}
1881
+ }
1882
+ #fsRoot.boardmode .fscompbar #composerWrap{width:100%;margin:0;max-width:none}
1736
1883
  #fsRoot .fslane.boardmode #fsTodoSec .tl,#fsRoot .fslane.boardmode #fsTodoSec .doneline,#fsRoot .fslane.boardmode #fsTodoSec #fsDonelist{display:none}
1737
1884
  /* Board mode = board ONLY (Masa 2026-07-17): the Review section — proof thumbnails, PR
1738
1885
  links, approve/dismiss, and its "N ready" summary line — is spoken by the board's own
@@ -1747,7 +1894,14 @@
1747
1894
  container-box position (measured identical, 63px both), but the glyph sits differently inside that box. Scoped here, not on <body>, to avoid
1748
1895
  a global typography change. */
1749
1896
  #fsRoot .fslane.boardmode #fsBoard{display:flex}
1750
- #fsBoard::-webkit-scrollbar{height:8px}#fsBoard::-webkit-scrollbar-thumb{background:var(--hair);border-radius:6px}
1897
+ /* The horizontal scrollbar is a hint, not furniture (Masa 2026-08-03): the track keeps
1898
+ its 8px so nothing shifts, but the thumb is transparent until you actually scroll —
1899
+ .sbon is added on scroll and dropped ~1.4s after the last one. */
1900
+ #fsBoard{scrollbar-width:none}
1901
+ #fsBoard.sbon{scrollbar-width:thin;scrollbar-color:var(--hair2) transparent}
1902
+ #fsBoard::-webkit-scrollbar{height:8px}
1903
+ #fsBoard::-webkit-scrollbar-thumb{background:transparent;border-radius:6px;transition:background .25s}
1904
+ #fsBoard.sbon::-webkit-scrollbar-thumb{background:var(--hair2)}
1751
1905
  .fsbcol{flex:0 0 236px;max-height:100%;display:flex;flex-direction:column;min-height:0} /* 236 = flagship .labcol (was 200, a prod-side narrowing that made the board read cramped vs the Artifact) */
1752
1906
  .fsbcolhd{display:flex;align-items:center;gap:8px;padding:1px 4px 9px;flex:0 0 auto}
1753
1907
  .fsbdot{width:7px;height:7px;border-radius:50%;background:var(--stg,var(--ink3));flex:0 0 auto;box-shadow:0 0 0 3px color-mix(in srgb,var(--stg,#fff) 14%,transparent)}
@@ -1858,6 +2012,19 @@
1858
2012
  #fsRoot .qadd.editing{opacity:1;color:var(--ink);cursor:text}
1859
2013
  #fsRoot .qadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
1860
2014
  #fsRoot .qadd input::placeholder{color:var(--ink3)}
2015
+ /* A card that has been accepted but not started: faint until the engine picks it up. */
2016
+ #fsRoot .lbc.is-tentative{opacity:.5}
2017
+ /* The board's own add row. It reads and behaves like the To Do list's .qadd — before
2018
+ this the inline field kept the browser's default chrome and landed as a white box on
2019
+ a dark board (Masa 2026-08-03: 白いダイアログでるけどダサい). all:unset is the same
2020
+ trick .qadd uses; the row is the field, there is no second surface. */
2021
+ #fsRoot .fsbadd{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:9px;width:100%;
2022
+ padding:6px 4px 6px 8px;border-radius:8px;color:var(--ink3);font-size:11.5px;cursor:pointer;
2023
+ opacity:.45;transition:opacity .15s,color .15s}
2024
+ #fsRoot .fsbadd:hover{opacity:1;color:var(--ink)}
2025
+ #fsRoot .fsbadd.editing{opacity:1;color:var(--ink);cursor:text}
2026
+ #fsRoot .fsbadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
2027
+ #fsRoot .fsbadd input::placeholder{color:var(--ink3)}
1861
2028
  #fsRoot .lateridle{display:flex;align-items:center;gap:9px;margin:5px 0 2px;padding:9px 10px;border:1px solid var(--hair);border-radius:8px;color:var(--ink2);font-size:10.5px;line-height:1.45}
1862
2029
  #fsRoot .lateridle .msg{flex:1;min-width:0}
1863
2030
  #fsRoot .lateridle button{height:26px;padding:0 10px;border:1px solid var(--hair2);border-radius:7px;background:var(--panel);color:var(--ink);font:600 10px/1 var(--mono);cursor:pointer;white-space:nowrap}
@@ -2987,6 +3154,16 @@
2987
3154
  body[data-layout="flagship"] .gearpop{left:8px!important;right:8px!important;top:auto!important;
2988
3155
  bottom:calc(env(safe-area-inset-bottom) + 8px)!important;width:auto!important;max-width:none!important;
2989
3156
  max-height:82dvh!important;border-radius:16px;padding-top:8px}
3157
+ /* The desktop dialog above centres with a transform and lays its nav out sideways —
3158
+ both have to be undone here or the sheet floats off-centre with a 158px rail. */
3159
+ body[data-layout="flagship"] .gearpop.open{transform:none!important;flex-direction:column!important;
3160
+ padding:8px 13px 0!important;gap:0!important}
3161
+ body[data-layout="flagship"] .gearpop .gnav{flex:0 0 auto;flex-direction:row;gap:6px;overflow-x:auto;
3162
+ scrollbar-width:none;border-right:0;border-bottom:1px solid var(--hair);padding:0 0 8px;margin-bottom:10px}
3163
+ body[data-layout="flagship"] .gearpop .gnav::-webkit-scrollbar{display:none}
3164
+ body[data-layout="flagship"] .gearpop .gnav .ggrp{display:none} /* group labels need a column to sit in */
3165
+ body[data-layout="flagship"] .gearpop .gnav button{white-space:nowrap;min-height:38px;padding:0 12px;font-size:13px}
3166
+ body[data-layout="flagship"] .gearpop .cfoot{margin:14px -13px 0;padding:12px 13px}
2990
3167
  body[data-layout="flagship"] .gearpop::before{content:"";position:sticky;top:0;z-index:2;display:block;
2991
3168
  width:38px;height:5px;margin:0 auto 12px;border-radius:3px;background:var(--hair2)}
2992
3169
  body[data-layout="flagship"] .gearpop .csw{width:30px;height:30px}
@@ -3013,6 +3190,7 @@
3013
3190
  /* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
3014
3191
  が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
3015
3192
  body[data-layout="flagship"] #fsRoot .fsrail{background:transparent;box-shadow:none}
3193
+ body[data-layout="flagship"] #fsRoot.boardmode .fscenter{min-width:0} /* stacked here — no floor to hold */
3016
3194
  /* Masa: on mobile drop the drag-to-swap grip (⠿) — it collides with the lane close ✕ and
3017
3195
  there's nothing to drag on a phone. Keep the ✕. */
3018
3196
  body[data-layout="flagship"] #fsRoot .panelgrip{display:none}
@@ -3141,6 +3319,8 @@
3141
3319
  <div id="tobottom" title="Back to latest">↓</div>
3142
3320
  <div class="errbar" id="errbar"></div>
3143
3321
  <div class="composer-wrap" id="composerWrap">
3322
+ <!-- Drag to set how tall the box may grow; double-click to put it back (Masa 2026-08-03). -->
3323
+ <div class="comphandle" id="compHandle" title="Drag to resize · double-click to reset"></div>
3144
3324
  <div class="queue" id="queue"></div>
3145
3325
  <!-- Context row (Masa 2026-07-22: 「claude の表記に合わせて」). The Claude app
3146
3326
  puts separate rounded chips above the box — repo · branch|worktree · add —
@@ -3255,12 +3435,6 @@
3255
3435
  same right column as the fold toggle above it; the menu opens BELOW the gear. -->
3256
3436
  <div class="lbrow"><span class="lb">Projects</span>
3257
3437
  <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
3438
  </div>
3265
3439
  <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
3440
  <div id="fsProjects"></div>
@@ -3322,6 +3496,10 @@
3322
3496
  </span>
3323
3497
  <div class="fswait" id="fsWait"><!-- waiting badge: "N Needs you / N Review" beside the toggle, board mode only (renderFsWait) --></div>
3324
3498
  <div class="lanegrip" id="fsGrip" title="Drag to resize"></div>
3499
+ <!-- Board-view composer host (Masa 2026-08-03). It lives INSIDE .fslane so its width
3500
+ is the board's width by construction — no second copy of that number to keep in
3501
+ step, and it follows the grip when the board is resized. -->
3502
+ <div class="fscompbar" id="fsCompBar"></div>
3325
3503
  <!-- mobile-only sheet header (H25②): grab handle + close X; display:none on desktop -->
3326
3504
  <div class="lanemtop"><span class="lanemgrab"></span><button class="lanemx" id="fsLaneClose" title="Close" aria-label="Close">✕</button></div>
3327
3505
  <div class="lanebody">
@@ -3565,7 +3743,7 @@ const savedTheme = (() => {
3565
3743
  if (v !== s) localStorage.setItem('theme', v);
3566
3744
  return v;
3567
3745
  })();
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,
3746
+ 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
3747
  // per-message send overrides set via slash-commands (reset after each send,
3570
3748
  // except model which is sticky in localStorage). palette = open command list.
3571
3749
  // Scoped per project (msgByProject) so a skill/mode picked in one project can't leak
@@ -4001,6 +4179,14 @@ function saReviewTitle(g){
4001
4179
  // reviewSummary.title, so they fall back to the request text unchanged (no regression).
4002
4180
  return String(g?.reviewSummary?.title || g?.text || g?.plan?.[0] || g?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim().slice(0, 90);
4003
4181
  }
4182
+ // A reply(follow-up) task's own title IS the reply text ("ok, fix the header too"), not a
4183
+ // task name — showing it as "Doing" reads like a new, unrelated task started (Masa
4184
+ // 2026-07-24). The To Do panel's Doing row already fell back to the goal's own name for a
4185
+ // reply task; the "Now doing" header did not, so the same goal read two different names in
4186
+ // the two places that are supposed to mirror each other. One function, both call sites.
4187
+ function fsDoingName(t, goal){
4188
+ return t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
4189
+ }
4004
4190
  // H23 (2026-07-18) evidence-kind detection. flagship's demo data carries an explicit
4005
4191
  // `kind` field; real goals/tasks have no such field, so the review card derives one
4006
4192
  // from real signals it already has (a captured proof image, whether the changed files
@@ -5252,6 +5438,30 @@ function ensureLayoutAnchors(){
5252
5438
  if (el && !el._anchor) { const a = document.createComment('home:' + id); el.parentNode.insertBefore(a, el); el._anchor = a; }
5253
5439
  }
5254
5440
  }
5441
+ // Board view can host the composer in a full-width bar under both columns (?cmp trial).
5442
+ // appendChild is guarded on a real parent change so a focused #input is never detached
5443
+ // mid-typing — same rule positionLayout follows.
5444
+ // Where the composer lives. In board view it defaults to the bar under the board; drag
5445
+ // its handle onto the chat column (or back) to move it, and that choice is remembered
5446
+ // (Masa 2026-08-03). Outside board view there is only one place for it.
5447
+ // Narrow screens stack the columns, and the bar is absolutely positioned inside the lane —
5448
+ // putting the composer there pushed it clean off the right of a 390px phone (measured at
5449
+ // x=414..682 on a 390px viewport, with the column's own "New" unreachable too). Phones keep
5450
+ // the composer where it has always been.
5451
+ function composerIsStacked(){
5452
+ return window.matchMedia('(max-width: 760px)').matches;
5453
+ }
5454
+ function composerHost(){
5455
+ const bar = document.getElementById('fsCompBar');
5456
+ const chat = document.getElementById('fsCompwrap');
5457
+ const board = document.getElementById('fsRoot')?.classList.contains('boardmode');
5458
+ if (!bar || !board || composerIsStacked()) return chat;
5459
+ return localStorage.getItem('compHome') === 'chat' ? chat : bar;
5460
+ }
5461
+ function placeComposer(){
5462
+ const host = composerHost(), cw = document.getElementById('composerWrap');
5463
+ if (host && cw && cw.parentNode !== host) host.appendChild(cw);
5464
+ }
5255
5465
  function positionLayout(){
5256
5466
  ensureLayoutAnchors();
5257
5467
  // The settings panel escapes the hidden top bar in flagship (.top{display:none} would
@@ -5268,7 +5478,7 @@ function positionLayout(){
5268
5478
  // Composer (queue + input + FREE LIMIT banner) + transient #errbar live at the bottom of
5269
5479
  // the ONE center card — desktop and mobile alike (全面つなげた下地: feed scrolls above,
5270
5480
  // composer sits on the same connected surface, the queue tray shares that 下地).
5271
- const host = $('fsCompwrap'), cw = $('composerWrap');
5481
+ const host = composerHost(), cw = $('composerWrap');
5272
5482
  if (host && cw && cw.parentNode !== host) host.appendChild(cw);
5273
5483
  const eb = $('errbar');
5274
5484
  if (host && eb && eb.parentNode !== host) host.appendChild(eb);
@@ -6844,7 +7054,11 @@ const fsUI = {
6844
7054
  band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
6845
7055
  revMore: false, // "+N more" fold on the review list (§1.5-8)
6846
7056
  todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
6847
- todoView: localStorage.getItem('fsTodoView') === 'board' ? 'board' : 'list', // To Do: list (default) / kanban board (Masa 2026-07-16)
7057
+ // Board is the DEFAULT (Masa 2026-08-03: 明らかに kanban view の方がフックが強い).
7058
+ // Only an explicit 'list' — a choice the person made and we stored — opts out; an
7059
+ // empty localStorage means board, so a first visit lands on the view that shows
7060
+ // what Galda is (columns of work moving) instead of a single list.
7061
+ todoView: localStorage.getItem('fsTodoView') === 'list' ? 'list' : 'board',
6848
7062
  logOpen: new Set(), logFocus: null, // "View log" targets
6849
7063
  repsOpen: new Set(), // reply-group folds
6850
7064
  };
@@ -7018,7 +7232,7 @@ function renderFsFeed(){
7018
7232
  nd.hidden = false;
7019
7233
  nd.innerHTML = `<span class="nd-lb">Doing</span><span class="nd-run"></span>`
7020
7234
  + (n != null ? `<span class="gno nd-no" data-n="${n}">#${n}</span>` : '')
7021
- + `<span class="nd-nm">${esc(String(t.title ?? '').replace(/\s+/g, ' ').slice(0, 90))}</span>`
7235
+ + `<span class="nd-nm">${esc(String(fsDoingName(t, g) ?? '').replace(/\s+/g, ' ').slice(0, 90))}</span>`
7022
7236
  + `<span class="nd-pct">${pct}%</span><div class="nd-bar"><i style="width:${pct}%"></i></div>`;
7023
7237
  } else { nd.hidden = true; nd.innerHTML = ''; }
7024
7238
  }
@@ -7264,6 +7478,8 @@ function renderFsLane(){
7264
7478
  const ae = document.activeElement;
7265
7479
  if (ae && lane.contains(ae) && ['INPUT', 'TEXTAREA'].includes(ae.tagName)) return;
7266
7480
  lane.classList.toggle('boardmode', fsUI.todoView === 'board');
7481
+ lane.closest('#fsRoot')?.classList.toggle('boardmode', fsUI.todoView === 'board');
7482
+ placeComposer();
7267
7483
  syncFsTvSeg();
7268
7484
  renderFsReview(); renderFsTodo();
7269
7485
  if (fsUI.todoView === 'board') renderFsBoard();
@@ -7284,6 +7500,12 @@ function fsBoardBucket(goal, tasks){
7284
7500
  if (s === 'done' || s === 'reverted' || s === 'skipped') return 'done'; // terminal; skipped is finished, not in flight (it used to fall through to the doing default)
7285
7501
  if (s === 'rejected') return 'rejected'; // Reject = closed. Parked in the list's own Dismissed fold; the board drops it entirely (a closed todo must not linger in Doing — Masa 2026-07-21). Both 'folded' and 'rejected' used to fall through to the doing default.
7286
7502
  if (s === 'stacked' || s === 'sending') return 'todo';
7503
+ // NOT moved to 'todo' here, though it was tried (Masa 2026-08-03: キューに入ってる時に
7504
+ // kanban から消える): 'planning' means the agent is already running, and the board must
7505
+ // place a goal in the same column the List does (Masa 2026-07-19, board-list-align).
7506
+ // Breaking that to keep a card visible traded one inconsistency for a worse one. What
7507
+ // actually fixes the disappearance is upstream — the card is inserted locally the moment
7508
+ // you press Enter, and 'running' without startedAt (genuinely queued) already stays here.
7287
7509
  if (s === 'running' && !goal?.startedAt) return 'todo'; // goalChip: planned, nothing started yet
7288
7510
  // Otherwise the goal is in flight (planning / retesting / running-and-started). BUT a
7289
7511
  // goal whose status stuck 'running' after every task already finished is NOT moving —
@@ -7324,8 +7546,14 @@ function renderFsBoard(){
7324
7546
  // (#input) — the real create path (no editable rows / no per-column status invention like the
7325
7547
  // flagship mock; a new goal enters via the composer). Earlier this was To-Do-only, which read
7326
7548
  // as "position is off" against the Artifact (Masa 2026-07-18).
7327
- const col = (name, stg, count, html, stage) =>
7328
- `<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume' : 'Pause'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button>` : ''}<span class="fsbcnt">${count}</span><button class="fsbcadd" data-fsnew="1" title="New goal"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button></div><div class="fsbbody">${html || '<div class="fsbempty">—</div>'}</div><button class="fsbadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button></section>`;
7549
+ // Only To Do and Backlog take a + (Masa 2026-08-03). The other columns are states the
7550
+ // work arrives in you cannot put something straight into Doing or Review by asking for
7551
+ // it — so a + there offered an action that does not exist.
7552
+ const CAN_ADD = new Set(['todo', 'later']);
7553
+ const col = (name, stg, count, html, stage) => {
7554
+ const plus = CAN_ADD.has(stage);
7555
+ return `<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume' : 'Pause'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button>` : ''}<span class="fsbcnt">${count}</span>${plus ? `<button class="fsbcadd" data-fsnew="1" title="New goal"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>` : ''}</div><div class="fsbbody">${html || '<div class="fsbempty">—</div>'}</div>${plus ? `<div class="fsbadd" data-fsnew="1" role="button" tabindex="0" title="${stage === 'later' ? 'New Backlog goal' : 'New goal'}"><span class="paplus"></span><span>${stage === 'later' ? 'New' : 'New'}</span></div>` : ''}</section>`;
7556
+ };
7329
7557
  // Depth card (CDO, Masa 2026-07-17): #NN + title, colour on the dot only, and a continuous
7330
7558
  // stroke + n/m ONLY while the goal is actually in flight. The column already names the
7331
7559
  // status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
@@ -7380,6 +7608,10 @@ function renderFsBoard(){
7380
7608
  else if (healthyRun) dot = 'run';
7381
7609
  // To Do (idle) and Later carry no dot — the column already says it. Quiet = dimmer title.
7382
7610
  const quiet = bkt === 'todo' || bkt === 'done' || bkt === 'later';
7611
+ // Accepted, but the engine has not started it: it stays put and reads faintly rather
7612
+ // than looking as settled as work that is really queued behind a slot (Masa 2026-08-03:
7613
+ // 未確定なら薄く表示されててほしい).
7614
+ const tentative = g.local === true || g.status === 'planning' || (g.status === 'running' && !g.startedAt);
7383
7615
  const n = goalNo.get(g.id);
7384
7616
  let h = '<div class="lbc-hd">'
7385
7617
  + (dot ? `<span class="lbc-dot ${dot}"></span>` : '')
@@ -7405,7 +7637,7 @@ function renderFsBoard(){
7405
7637
  h += `<div class="lbc-meter"><span class="bar"><i style="width:${Math.round(done / total * 100)}%"></i></span><span class="frac">${done}/${total}</span></div>`;
7406
7638
  }
7407
7639
  }
7408
- return `<div class="lbc${quiet ? ' is-quiet' : ''}" data-fsgoal="${g.id}">${h}</div>`;
7640
+ return `<div class="lbc${quiet ? ' is-quiet' : ''}${tentative ? ' is-tentative' : ''}" data-fsgoal="${g.id}">${h}</div>`;
7409
7641
  };
7410
7642
  const no = (g) => goalNo.get(g.id) ?? Infinity;
7411
7643
  const bucket = { todo: [], doing: [], needs: [], review: [], done: [], later: [], rejected: [] };
@@ -7431,8 +7663,23 @@ function renderFsBoard(){
7431
7663
  + (bucket.needs.length ? col('Needs you', '#ffca16', bucket.needs.length, bucket.needs.map(goalCard).join(''), 'needs') : '')
7432
7664
  + col('Review', 'var(--review)', reviewDigest.length, reviewDigest.map(reviewCard).join(''), 'review')
7433
7665
  + col('Done', 'var(--ink3)', bucket.done.length, bucket.done.slice(0, 40).map(goalCard).join(''), 'done')
7434
- + col('Later', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
7435
- for (const b of el.querySelectorAll('[data-fsnew]')) b.onclick = () => $('input')?.focus(); // every + New / header + focuses the composer
7666
+ + col('Backlog', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
7667
+ // + and New ADD something here (Masa 2026-08-03: + をおしてタスクを追加できるように).
7668
+ // They used to only focus the composer, which is a box on the other side of the screen —
7669
+ // measured on Masa's own window: activeElement really was #input, not readOnly, not
7670
+ // disabled, so it "worked", and nothing whatsoever happened where he was looking.
7671
+ // fsInlineNewRow + fsCreateGoal are the same pair the List view has always used for its
7672
+ // own To Do / Backlog rows; nothing new is invented here.
7673
+ for (const b of el.querySelectorAll('[data-fsnew]')) {
7674
+ const colEl = b.closest('.fsbcol');
7675
+ const stage = colEl?.dataset.stage;
7676
+ const anchor = colEl?.querySelector('.fsbadd[data-fsnew]') || b;
7677
+ b.onclick = (e) => {
7678
+ e.stopPropagation();
7679
+ const later = stage === 'later';
7680
+ fsInlineNewRow(anchor, later ? 'Add to Backlog…' : 'Add to To Do…', (text) => fsCreateGoal(text, later));
7681
+ };
7682
+ }
7436
7683
  for (const b of el.querySelectorAll('[data-fspause]')) b.onclick = togglePauseActive; // Doing列の一時停止/再生
7437
7684
  for (const c of el.querySelectorAll('[data-fsgoal]')) c.onclick = (e) => {
7438
7685
  if (e.target.closest('button, a')) return;
@@ -7775,7 +8022,7 @@ function renderFsTodo(){
7775
8022
  const doingRows =
7776
8023
  running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
7777
8024
  const goal = pgoals.find((g) => g.id === t.goalId);
7778
- const title = t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
8025
+ const title = fsDoingName(t, goal);
7779
8026
  return `<div class="drow"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="stword run">Running</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
7780
8027
  <div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
7781
8028
  }).join('')
@@ -7870,8 +8117,8 @@ function renderFsTodo(){
7870
8117
  ${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
7871
8118
  ${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
7872
8119
  <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7873
- <span class="tlcap">Later</span>
7874
- <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
8120
+ <span class="tlcap">Backlog</span>
8121
+ <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Backlog goal"><span class="paplus"></span><span>New</span></div>
7875
8122
  ${laterRows}
7876
8123
  </div>
7877
8124
  ${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
@@ -7883,7 +8130,7 @@ function renderFsTodo(){
7883
8130
  const laterAdd = el.querySelector('.qadd[data-fslaternew]');
7884
8131
  if (laterAdd) laterAdd.onclick = (e) => {
7885
8132
  e.stopPropagation();
7886
- fsInlineNewRow(laterAdd, 'Add to Later…', (text) => fsCreateGoal(text, true));
8133
+ fsInlineNewRow(laterAdd, 'Add to Backlog…', (text) => fsCreateGoal(text, true));
7887
8134
  };
7888
8135
  const dl = el.querySelector('#fsDoneLine');
7889
8136
  if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
@@ -7971,17 +8218,34 @@ function renderFsTodo(){
7971
8218
  catch { showErr('Cannot delete a running/finished goal — reply to append instead'); return; }
7972
8219
  if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
7973
8220
  }
8221
+ // The card appears the moment you press Enter, in the column you typed it into, faint —
8222
+ // then the server's real goal replaces it (Masa 2026-08-03: 通信通さず・キューに入る感じで).
8223
+ // Before this the whole thing waited on the round-trip, so the row you had just typed
8224
+ // simply was not there for as long as the POST took, which read as "it disappeared".
8225
+ // A negative id can never collide with the server's, and `local` marks it as not-yet-real.
8226
+ let localGoalSeq = 0;
7974
8227
  async function fsCreateGoal(text, pending){
8228
+ const tempId = -(++localGoalSeq);
8229
+ const ghost = {
8230
+ id: tempId, projectId: state.active, text,
8231
+ status: pending ? 'pending' : 'stacked', // Backlog / To Do — the columns they were typed into
8232
+ createdAt: new Date().toISOString(), local: true,
8233
+ };
8234
+ state.goals.push(ghost);
8235
+ render();
8236
+ const drop = () => { const i = state.goals.findIndex((g) => g.id === tempId); if (i >= 0) state.goals.splice(i, 1); };
7975
8237
  const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
7976
8238
  body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending }) });
7977
8239
  if (r.status === 402) {
7978
8240
  const data = await r.json().catch(() => ({}));
8241
+ drop(); render();
7979
8242
  showBillingBlocked(data.blocked);
7980
8243
  return false;
7981
8244
  }
7982
- if (!r.ok) { showErr(pending ? 'Later add failed.' : 'To Do add failed.'); return false; }
8245
+ if (!r.ok) { drop(); render(); showErr(pending ? 'Backlog add failed.' : 'To Do add failed.'); return false; }
7983
8246
  clearBillingBlocked();
7984
8247
  const goal = await r.json();
8248
+ drop(); // the real one takes its place
7985
8249
  if (goal?.id) upsert(state.goals, goal);
7986
8250
  render();
7987
8251
  return true;
@@ -8315,13 +8579,22 @@ function renderClawdHero(){
8315
8579
  e.stopPropagation();
8316
8580
  fsUI.todoView = b.dataset.fstv; localStorage.setItem('fsTodoView', fsUI.todoView);
8317
8581
  $('fsLane').classList.toggle('boardmode', fsUI.todoView === 'board');
8582
+ $('fsRoot').classList.toggle('boardmode', fsUI.todoView === 'board');
8583
+ window.__fsApplyMidOrder?.();
8584
+ placeComposer();
8318
8585
  syncFsTvSeg();
8319
8586
  if (fsUI.todoView === 'board') renderFsBoard();
8320
8587
  };
8321
8588
  syncFsTvSeg();
8322
8589
  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'); });
8590
+ // One press, one dialog (Masa 2026-08-02). The gear used to open a four-item menu
8591
+ // (Theme / Connect MCP / History / Settings) whose only job was to pick a tab — and the
8592
+ // dialog now carries those same four in its own left nav, so the menu was asking people
8593
+ // to choose twice. Press the gear, get Settings; switch inside if you want something else.
8594
+ $('fsMoreBtn').onclick = (e) => {
8595
+ e.stopPropagation();
8596
+ if ($('gearPop').classList.contains('open')) custCancel(); else custOpen();
8597
+ };
8325
8598
  const closeMobilePanels = () => {
8326
8599
  document.body.classList.remove('laneopen', 'projectsopen');
8327
8600
  };
@@ -8349,11 +8622,7 @@ function renderClawdHero(){
8349
8622
  // v46 (HANDOFF-v45 §3): the menu entries deep-link into the 4-tab settings panel —
8350
8623
  // Theme→THEME / Connect MCP→MCP / History→HISTORY / Settings→SETTINGS.
8351
8624
  // 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.
8625
+ // parallel-runs switcher (§1.5-1/-7): toggle + click-away, the shape the rail's own menus use.
8357
8626
  // Row content is rebuilt by renderFsFeed() (running list changes live); the open/closed
8358
8627
  // class here is left alone by that rebuild so the popover doesn't snap shut mid-render.
8359
8628
  $('fsRunSwitch').onclick = (e) => { e.stopPropagation(); $('fsRunMenu').classList.toggle('open'); };
@@ -8367,7 +8636,15 @@ function renderClawdHero(){
8367
8636
  // minReadableCenterW, given however wide the lane currently is — no more flat 280px ceiling
8368
8637
  // that stopped rightward drags well short of the window edge (Masa 2026-07-16: "もっと広げ
8369
8638
  // られるようにできる?"). Floors at 72 (the min) on a narrow/laned-out window.
8370
- const railMax = () => Math.max(72, window.innerWidth - 28 - 24 - lane.getBoundingClientRect().width - minReadableCenterW);
8639
+ // Two things were wrong here once the board took the middle (Masa 2026-08-03: 左レーンの
8640
+ // 幅を調整できない). It subtracted (a) the LIST view's 640px reading floor even in board
8641
+ // view, where the floor is minCenterWBoard, and (b) the lane's CURRENT width — but the
8642
+ // lane is flex-shrink:1, so it yields when the rail grows. Together they made railMax()
8643
+ // return the 72px floor on any normal window, so every drag collapsed the rail instead
8644
+ // of resizing it (measured at 1440: 1440-52-760-640 = -12 → clamped to 72).
8645
+ // Reserve the lane's MINIMUM instead, and the floor that actually applies.
8646
+ const centreFloor = () => (lane.classList.contains('boardmode') ? minCenterWBoard : minReadableCenterW);
8647
+ const railMax = () => Math.max(72, window.innerWidth - 28 - 24 - 280 - centreFloor());
8371
8648
  const saved = Number(localStorage.getItem('fsLaneW'));
8372
8649
  const savedRail = Number(localStorage.getItem('fsRailW'));
8373
8650
  const restoreLane = saved >= 280 && saved <= 540;
@@ -8391,10 +8668,45 @@ function renderClawdHero(){
8391
8668
  // those chips get clipped off the edge of the composer card (Masa 2026-07-16 repro:
8392
8669
  // dragged the board grip wide on a 1280px-wide window, "Medium"+send were cut off).
8393
8670
  // 440 is the smallest value that doesn't clip on a real laptop width.
8394
- const minCenterWBoard = 440;
8671
+ // 460: measured, and the same number the CSS floor uses (#fsRoot.boardmode .fscenter).
8672
+ // They were 440 here and 460 there, so a drag could store a width the CSS then refused —
8673
+ // the lane stayed put while localStorage said otherwise.
8674
+ const minCenterWBoard = 460;
8395
8675
  const boardMax = () => Math.max(280, window.innerWidth - 28 - 24
8396
8676
  - (document.querySelector('#fsRoot .fsrail')?.getBoundingClientRect().width || defaultRailW) - minCenterWBoard);
8677
+ // Which panel sits next to the rail. In board view the board takes it (Masa 2026-08-03:
8678
+ // 中央レーンに kanban) — the columns are the thing to look at, and the chat reads fine
8679
+ // on the right. An explicit drag (fsMidLeft) always wins over this.
8680
+ const applyMidOrder = () => {
8681
+ const chosen = localStorage.getItem('fsMidLeft');
8682
+ root.classList.toggle('swapmid', chosen ? chosen === 'lane' : fsUI.todoView === 'board');
8683
+ };
8684
+ applyMidOrder();
8685
+ window.__fsApplyMidOrder = applyMidOrder;
8397
8686
  const savedBoard = Number(localStorage.getItem('fsLaneWBoard'));
8687
+ // Nobody has dragged it yet → open at boardMax(), not the old 372px stub (Masa
8688
+ // 2026-08-03: 添付画像くらい widely). boardMax() is already the honest ceiling — it
8689
+ // reserves the rail's REAL width plus the 440px the composer needs before its own
8690
+ // chips clip — so "as wide as allowed" is the widest the board can be while the
8691
+ // chat still works. Measured 780px on a 1440px window, which is the width in the
8692
+ // screenshot this came from. It follows the window because it is recomputed on
8693
+ // resize for as long as the width is unsaved; the first drag pins it for good.
8694
+ // Nobody has dragged the board yet → open it as wide as it is ALLOWED to be, not at
8695
+ // the old 372px stub (Masa 2026-08-03). Two things had to be measured rather than
8696
+ // reasoned about:
8697
+ // 1. boardMax() reads the rail's real width, but at boot the rail is not laid out
8698
+ // yet, so it fell back to defaultRailW and returned ~60px too much (measured:
8699
+ // lane 840 where the arithmetic wanted 780).
8700
+ // 2. That surplus came straight out of the chat, which dropped to 380px — and the
8701
+ // composer's own button row then overflowed its container by 72px, which is
8702
+ // exactly the clipping minCenterWBoard exists to prevent (main, for comparison:
8703
+ // lane 372 / chat 848 / overflow 0).
8704
+ // So the width is not computed once and trusted. A ResizeObserver watches the chat
8705
+ // column and gives width back to it whenever it falls under the floor — self-correcting
8706
+ // whatever the boot order turns out to be. The first drag writes fsLaneWBoard and the
8707
+ // observer stands down for good.
8708
+ // Unsaved → set nothing: the CSS fallback (100% minus the chat's min-width) sizes it,
8709
+ // and it follows the window with no listener to keep in step.
8398
8710
  if (savedBoard >= 280 && savedBoard <= boardMax()) root.style.setProperty('--lanew-board', `${savedBoard}px`);
8399
8711
  let sx = 0, sw = 0, drag = false, boarding = false;
8400
8712
  grip.addEventListener('mousedown', (e) => {
@@ -8404,7 +8716,10 @@ function renderClawdHero(){
8404
8716
  document.addEventListener('mousemove', (e) => {
8405
8717
  if (!drag) return;
8406
8718
  const max = boarding ? boardMax() : 540;
8407
- const w = Math.max(280, Math.min(max, sw + (sx - e.clientX)));
8719
+ // Grip on the right edge (swapmid) dragging right widens; on the left edge the
8720
+ // sign flips. Same gesture either way: pull the edge you are holding.
8721
+ const dir = root.classList.contains('swapmid') ? (e.clientX - sx) : (sx - e.clientX);
8722
+ const w = Math.max(280, Math.min(max, sw + dir));
8408
8723
  root.style.setProperty(boarding ? '--lanew-board' : '--lanew', `${w}px`);
8409
8724
  });
8410
8725
  document.addEventListener('mouseup', () => {
@@ -8475,6 +8790,10 @@ function renderClawdHero(){
8475
8790
  const side = sideFor(p, e.clientX);
8476
8791
  const wantLeft = side === 'left' ? dragged : (dragged === 'center' ? 'lane' : 'center');
8477
8792
  root.classList.toggle('swapmid', wantLeft === 'lane');
8793
+ // Persisted from here on (Masa 2026-08-03). It used to be session-only, which was
8794
+ // harmless while the default never moved — now that board mode puts the lane on
8795
+ // the left by default, an unsaved drag back would be undone by the next reload.
8796
+ localStorage.setItem('fsMidLeft', wantLeft);
8478
8797
  });
8479
8798
  }
8480
8799
  })();
@@ -11961,7 +12280,7 @@ function acceptPalette(){
11961
12280
  $('input').value = stripLeadingCmd($('input').value);
11962
12281
  closePalette();
11963
12282
  }
11964
- const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 120) + 'px'; ta.focus();
12283
+ const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px'; ta.focus();
11965
12284
  return true;
11966
12285
  }
11967
12286
  // Close the palette on any click outside the composer.
@@ -12086,10 +12405,159 @@ $('input').addEventListener('keydown', (e) => {
12086
12405
  // Up-arrow on an empty input recalls the last sent goal for editing.
12087
12406
  if (e.key === 'ArrowUp' && !state.palette && !$('input').value && _lastSend.text) {
12088
12407
  e.preventDefault(); const ta = $('input'); ta.value = _lastSend.text;
12089
- ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 120) + 'px';
12408
+ ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px';
12090
12409
  }
12091
12410
  });
12092
- $('input').addEventListener('input', () => { const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 120) + 'px'; localStorage.setItem('draft', ta.value); refreshPalette(); });
12411
+ // The textarea grows to a cap; the cap is the dial. Default 120 = the value that was
12412
+ // hard-coded in three places before this. Clamped to a floor that still shows one line
12413
+ // and a ceiling of half the window so the board never disappears behind the composer.
12414
+ const COMP_MAX_DEFAULT = 120;
12415
+ function composerMaxH(){
12416
+ const v = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--compmax'), 10);
12417
+ return Number.isFinite(v) && v > 0 ? v : COMP_MAX_DEFAULT;
12418
+ }
12419
+ function setComposerMax(px, persist){
12420
+ const max = Math.round(Math.max(56, Math.min(px, Math.round(window.innerHeight * 0.5))));
12421
+ document.documentElement.style.setProperty('--compmax', `${max}px`);
12422
+ if (persist) localStorage.setItem('compMax', String(max));
12423
+ const ta = document.getElementById('input');
12424
+ if (ta) { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, max) + 'px'; }
12425
+ }
12426
+ // Keep --compbarh in step with what the composer bar actually measures, so the board's
12427
+ // bottom reserve follows it (including while it is being dragged taller).
12428
+ // The board's scrollbar shows itself while you scroll and for a moment after.
12429
+ // Crossing the phone/desktop boundary changes where the composer belongs.
12430
+ (function watchComposerBreakpoint(){
12431
+ const mq = window.matchMedia('(max-width: 760px)');
12432
+ const on = () => { try { placeComposer(); } catch {} };
12433
+ mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
12434
+ })();
12435
+ (function boardScrollbarHint(){
12436
+ const board = document.getElementById('fsBoard'); if (!board) return;
12437
+ let t = null;
12438
+ board.addEventListener('scroll', () => {
12439
+ board.classList.add('sbon');
12440
+ clearTimeout(t); t = setTimeout(() => board.classList.remove('sbon'), 1400);
12441
+ }, { passive: true });
12442
+ })();
12443
+ (function trackComposerBarHeight(){
12444
+ const bar = document.getElementById('fsCompBar'); if (!bar) return;
12445
+ const sync = () => {
12446
+ // 0 is a real answer, not a glitch to skip: when the composer is dragged over to the
12447
+ // chat this bar is empty, and the board should get that space back instead of holding
12448
+ // a 125px gap under itself for a box that is no longer there.
12449
+ const h = Math.round(bar.getBoundingClientRect().height);
12450
+ document.documentElement.style.setProperty('--compbarh', `${h}px`);
12451
+ };
12452
+ if (window.ResizeObserver) new ResizeObserver(sync).observe(bar);
12453
+ window.addEventListener('resize', sync);
12454
+ sync();
12455
+ })();
12456
+ // Drag the composer between "under the board" and "under Implementing". The handle is
12457
+ // the grab point — the same one that resizes it — so there is one thing to reach for.
12458
+ // A plain HTML5 drag with its own MIME type, like the panel-swap drag already in the rail,
12459
+ // so it can never be confused with a file drop or a card drag.
12460
+ (function initComposerMove(){
12461
+ const cw = document.getElementById('composerWrap'); if (!cw) return;
12462
+ const root = () => document.getElementById('fsRoot');
12463
+ // The whole COLUMN is the target, not the thin bar (Masa 2026-08-03: 移動がうまくいかない
12464
+ // 時がある). Letting go over the board itself — the obvious place to aim for when the
12465
+ // idea is "put it under the kanban" — landed outside the 125px bar and did nothing.
12466
+ const zones = () => [
12467
+ [document.querySelector('#fsRoot .fslane'), 'board', document.getElementById('fsCompBar')],
12468
+ [document.querySelector('#fsRoot .fscenter'), 'chat', document.getElementById('fsCompwrap')],
12469
+ ].filter(([el]) => el);
12470
+
12471
+ // Pointer events, not HTML5 drag-and-drop. The DnD version LOOKED right — dragstart
12472
+ // fired and the outlines appeared — but nothing moved in a real browser, and the
12473
+ // synthetic DragEvents I tested with only proved the handlers were wired (Masa: 枠はでる
12474
+ // けど移動が出来ない). The rail and lane grips here have always been mousedown/mousemove/
12475
+ // mouseup; this matches them, and a real mouse can be driven through it in a test.
12476
+ let dragging = false, armed = false, sx = 0, sy = 0, hot = null;
12477
+ const THRESHOLD = 4; // a click on the card must not read as a drag
12478
+
12479
+ const zoneUnder = (x, y) => {
12480
+ for (const z of zones()) {
12481
+ const r = z[0].getBoundingClientRect();
12482
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return z;
12483
+ }
12484
+ return null;
12485
+ };
12486
+ const paint = (target) => {
12487
+ for (const z of zones()) {
12488
+ z[0].classList.toggle('comp-over', z[0] === target);
12489
+ z[2]?.classList.toggle('comp-over', z[0] === target);
12490
+ }
12491
+ };
12492
+ const stop = () => {
12493
+ dragging = armed = false; hot = null;
12494
+ root()?.classList.remove('compdragging');
12495
+ document.body.classList.remove('compdrag-noselect');
12496
+ cw.classList.remove('compdrag-lift');
12497
+ cw.style.transform = '';
12498
+ paint(null);
12499
+ };
12500
+
12501
+ cw.addEventListener('mousedown', (e) => {
12502
+ if (e.button !== 0) return;
12503
+ if (!root()?.classList.contains('boardmode')) return; // nowhere to move it to
12504
+ if (e.target.closest('textarea,button,select,input,a,[contenteditable],#compHandle')) return;
12505
+ armed = true; sx = e.clientX; sy = e.clientY;
12506
+ // Without this the browser starts a text selection from the card and sweeps it across
12507
+ // everything the pointer passes (Masa 2026-08-03: テキストが広範囲で選択される).
12508
+ e.preventDefault();
12509
+ });
12510
+ document.addEventListener('mousemove', (e) => {
12511
+ if (!armed) return;
12512
+ const dx = e.clientX - sx, dy = e.clientY - sy;
12513
+ if (!dragging) {
12514
+ if (Math.abs(dx) + Math.abs(dy) < THRESHOLD) return;
12515
+ dragging = true;
12516
+ root().classList.add('compdragging');
12517
+ document.body.classList.add('compdrag-noselect');
12518
+ cw.classList.add('compdrag-lift');
12519
+ }
12520
+ e.preventDefault();
12521
+ // It follows the cursor. Without this there was no way to tell it had been picked up
12522
+ // (Masa: 掴めてるかわからない) — the lane grip moves its edge, so this moves the card.
12523
+ cw.style.transform = `translate(${Math.round(dx)}px, ${Math.round(dy)}px)`;
12524
+ hot = zoneUnder(e.clientX, e.clientY);
12525
+ paint(hot ? hot[0] : null);
12526
+ });
12527
+ document.addEventListener('mouseup', () => {
12528
+ if (!armed) return;
12529
+ const home = dragging && hot ? hot[1] : null;
12530
+ stop();
12531
+ if (!home) return;
12532
+ localStorage.setItem('compHome', home);
12533
+ placeComposer();
12534
+ });
12535
+ })();
12536
+ (function initComposerResize(){
12537
+ const saved = Number(localStorage.getItem('compMax'));
12538
+ if (saved >= 56) setComposerMax(saved, false);
12539
+ const h = document.getElementById('compHandle'); if (!h) return;
12540
+ let sy = 0, sh = 0, dragging = false;
12541
+ const down = (e) => { dragging = true; sy = (e.touches ? e.touches[0].clientY : e.clientY);
12542
+ sh = composerMaxH(); e.preventDefault(); };
12543
+ const move = (e) => { if (!dragging) return;
12544
+ const y = (e.touches ? e.touches[0].clientY : e.clientY);
12545
+ setComposerMax(sh + (sy - y), false); }; // drag UP = taller
12546
+ const up = () => { if (!dragging) return; dragging = false;
12547
+ localStorage.setItem('compMax', String(composerMaxH())); };
12548
+ h.addEventListener('mousedown', down); h.addEventListener('touchstart', down, { passive: false });
12549
+ document.addEventListener('mousemove', move); document.addEventListener('touchmove', move, { passive: false });
12550
+ document.addEventListener('mouseup', up); document.addEventListener('touchend', up);
12551
+ // Double-click puts it back — the same gesture the project rows already use for "undo
12552
+ // my fiddling", and the handle says so in its title.
12553
+ h.addEventListener('dblclick', () => {
12554
+ localStorage.removeItem('compMax');
12555
+ document.documentElement.style.removeProperty('--compmax');
12556
+ const ta = document.getElementById('input');
12557
+ if (ta) { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, COMP_MAX_DEFAULT) + 'px'; }
12558
+ });
12559
+ })();
12560
+ $('input').addEventListener('input', () => { const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px'; localStorage.setItem('draft', ta.value); refreshPalette(); });
12093
12561
  $('input').addEventListener('paste', (e) => {
12094
12562
  for (const item of e.clipboardData?.items ?? []) {
12095
12563
  if (item.type.startsWith('image/')) { e.preventDefault(); uploadFile(item.getAsFile()); }
@@ -12762,11 +13230,38 @@ function applyCust(){
12762
13230
  // histHTML/setHTML/mcpHTML port). custTab is session UI state, never persisted; switching
12763
13231
  // tabs re-renders the panel WITHOUT touching the edit-session snapshot (custSnap) and
12764
13232
  // without closing. =====
12765
- let custTab = 'theme';
13233
+ 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
13234
  function gearTabsHTML(){
12767
13235
  return `<div class="ctabs">${[['theme', 'THEME'], ['history', 'HISTORY'], ['settings', 'SETTINGS'], ['mcp', 'MCP']]
12768
13236
  .map(([k, l]) => `<button class="ctab${custTab === k ? ' on' : ''}" data-gctab="${k}">${l}</button>`).join('')}</div>`;
12769
13237
  }
13238
+ // Flagship = the centred dialog (CDO pattern C): the four tabs become a left rail,
13239
+ // grouped by what they are actually about instead of sitting in one flat strip.
13240
+ // Same data-gctab contract as the tab buttons, so wireGearCommon needs no change.
13241
+ // Theme sits LAST (Masa 2026-08-02): it is the section people visit least, and it was
13242
+ // taking the top slot — the position a reader reads first — from the one they came for.
13243
+ const GEAR_NAV = [
13244
+ ['Project', [['settings', 'Settings'], ['history', 'History'], ['mcp', 'MCP']]],
13245
+ // Its own entry under MCP (Masa 2026-08-02) as well as the note at the foot of Settings:
13246
+ // a person who has decided to say something should not have to scroll a settings list to
13247
+ // find where. The heading is Contact, not Feedback — a section labelled the same as its
13248
+ // only item is a label for nothing.
13249
+ ['Contact', [['feedback', 'Feedback']]],
13250
+ ['Appearance', [['theme', 'Theme']]],
13251
+ ];
13252
+ function gearNavHTML(){
13253
+ return `<nav class="gnav">${GEAR_NAV.map(([grp, items]) =>
13254
+ `<div class="ggrp">${grp}</div>` + items.map(([k, l]) =>
13255
+ `<button class="${custTab === k ? 'on' : ''}" data-gctab="${k}">${l}</button>`).join('')).join('')}</nav>`;
13256
+ }
13257
+ // The dialog only exists in flagship; every other layout keeps the anchored popover
13258
+ // and its top tab strip untouched.
13259
+ function gearIsDialog(){ return state.layout === 'flagship'; }
13260
+ function gearShell(html){
13261
+ return gearIsDialog()
13262
+ ? gearNavHTML() + `<div class="gcol"><div class="gbody">${html}</div></div>`
13263
+ : gearTabsHTML() + html;
13264
+ }
12770
13265
  // HISTORY (v46): approve/dismiss history from goal state — deterministic, zero LLM.
12771
13266
  // HONEST DATA NOTE: the engine stamps no decidedAt on approve/dismiss (they only flip
12772
13267
  // goal.status — approveGoal/dismissGoal in lib.mjs), and a dismissed goal goes BACK to
@@ -12834,20 +13329,51 @@ function gearSetHTML(){
12834
13329
  : acct + freeUsage;
12835
13330
  const st = state.stability;
12836
13331
  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>
13332
+ const langRow = `<div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
12839
13333
  <button class="optchip${lang === 'auto' ? ' on' : ''}" data-gslang="auto">Auto</button>
12840
13334
  <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>`;
13335
+ <button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>`;
13336
+ 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>
13337
+ <button class="optchip" data-gabout="1">Open</button></div>`;
13338
+ 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>`;
13339
+ // Grouped headings + two-across rows (CDO pattern C, Masa 2026-08-02). The flat list
13340
+ // gave a reader no way to tell "settings for this project" from "settings for this
13341
+ // account" — the names were already there in the rows, only the structure was missing.
13342
+ //
13343
+ // A group with nothing in it renders nothing, heading included (§1.5-3 no empty box):
13344
+ // remoteAccessRow/shareUsageDataRow/hostReadinessRow all return '' when the engine has
13345
+ // not configured them, and on such a host the heading alone would be a label for air.
13346
+ const group = (title, rows) => {
13347
+ const body = rows.filter(Boolean).join('');
13348
+ return body ? `<h4>${title}</h4><div class="gtwo">${body}</div>` : '';
13349
+ };
13350
+ return group('Project', [langRow, remoteAccessRow(), shareUsageDataRow()])
13351
+ + group('Account', [billingRow])
13352
+ // "System", not "This machine": hostReadinessRow() already labels itself
13353
+ // "This machine" (and a test asserts that exact label), so the heading would
13354
+ // have been the same words twice, three rows apart.
13355
+ + group('System', [stability, aboutRow, hostReadinessRow(), dataRow])
13356
+ + gearFeedbackHTML();
13357
+ }
13358
+ // The founder's note (Masa 2026-08-02). It is the last thing in SETTINGS because it
13359
+ // is the one row that asks something of the reader rather than offering a control,
13360
+ // and because a person who got this far has already formed an opinion.
13361
+ //
13362
+ // The honest limitation is the point of the copy, not an apology: few users so far
13363
+ // means workflows we have not met, and saying so is what earns the reply. Keep the
13364
+ // promise in it something Leon can actually keep.
13365
+ // `solo` = the Contact tab, where the note is the whole screen rather than the last thing
13366
+ // under a list: no hairline above it (there is nothing above it to separate from) and room
13367
+ // to breathe. Same words either way — one place to change them.
13368
+ function gearFeedbackHTML({ solo = false } = {}){
13369
+ return `<div class="gfeed${solo ? ' gsolo' : ''}">
13370
+ <h4>Feedback</h4>
13371
+ <p>Galda is early. Not many people are using it yet, so there are workflows it handles badly — quite possibly parts of yours.</p>
13372
+ <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>
13373
+ <p class="gsig">— Leon, co-founder</p>
13374
+ <a class="gmail" href="mailto:hello@galda.app?subject=Galda%20feedback">
13375
+ <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>
13376
+ </div>`;
12851
13377
  }
12852
13378
  // The npx moment, moved to where the intent is.
12853
13379
  //
@@ -13040,36 +13566,16 @@ function hostReadinessRow(){
13040
13566
  const detail = (missing.length ? missing : checks).map((c) => c.detail).join(' · ');
13041
13567
  return `<div class="srow${missing.length ? '' : ' dis'}"><div class="sl">This machine<span class="sd">${esc(detail)}</span></div></div>`;
13042
13568
  }
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
- }
13569
+ // NO "Project rules" row (Masa 2026-08-02). It used to print CLAUDE.md / AGENTS.md
13570
+ // read-only in Settings. What ONE-CONVERSATION §1.5a actually decided was a NEGATIVE:
13571
+ // Galda must not grow its own "how we work" field, because the same rules would then
13572
+ // live in two places and drift — the repo already holds the answer. That decision does
13573
+ // not need a control to be honoured. The viewer was the incidental part, and it showed
13574
+ // a person their own file, which they can open in an editor. Removed.
13575
+ //
13576
+ // The engine keeps collectProjectRules (workerRunContext still names the files it found)
13577
+ // and its GET /api/projects/:id/rules, which nothing calls now — engine is a separate
13578
+ // lane, so that endpoint is a follow-up, not a silent edit from here.
13073
13579
  // MCP (v46): connection status dot + connectCommand + COPY + quiet explanation.
13074
13580
  // The dot is the app's own live engine link (SSE up/down) — the only real signal here.
13075
13581
  function gearMcpHTML(){
@@ -13086,7 +13592,7 @@ function buildGearPop(){
13086
13592
  // custRender, the sticky Save/Cancel footer belongs to THEME (the live-preview tab);
13087
13593
  // the edit session itself stays open across tabs (Esc/outside still Cancel).
13088
13594
  if (custTab === 'history') {
13089
- pop.innerHTML = gearTabsHTML() + gearHistHTML();
13595
+ pop.innerHTML = gearShell(gearHistHTML());
13090
13596
  wireGearCommon(pop);
13091
13597
  for (const b of pop.querySelectorAll('[data-ghmore]')) b.onclick = () => {
13092
13598
  const r = b.closest('.hrow'); const ex = r.classList.toggle('ex'); b.textContent = ex ? 'Less' : 'More';
@@ -13099,7 +13605,7 @@ function buildGearPop(){
13099
13605
  return;
13100
13606
  }
13101
13607
  if (custTab === 'settings') {
13102
- pop.innerHTML = gearTabsHTML() + gearSetHTML();
13608
+ pop.innerHTML = gearShell(gearSetHTML());
13103
13609
  wireGearCommon(pop);
13104
13610
  for (const b of pop.querySelectorAll('[data-gslang]')) b.onclick = () => saveLang(b.dataset.gslang);
13105
13611
  for (const b of pop.querySelectorAll('[data-grelay]')) b.onclick = () => saveRelayPref(b.dataset.grelay === 'on');
@@ -13124,18 +13630,20 @@ function buildGearPop(){
13124
13630
  catch {}
13125
13631
  finally { await refresh(); if ($('gearPop')) buildGearPop(); }
13126
13632
  };
13127
- const rulesBtn = pop.querySelector('[data-grules]');
13128
- if (rulesBtn) rulesBtn.onclick = () => { state.rulesOpen = !state.rulesOpen; buildGearPop(); };
13129
13633
  const stabilityBtn = pop.querySelector('[data-gstability]');
13130
13634
  if (stabilityBtn) stabilityBtn.onclick = () => { state.stability = null; buildGearPop(); loadStability(); };
13131
- loadProjectRules();
13132
13635
  if (!state.stability) loadStability();
13133
13636
  if (!state.relayPref) loadRelayPref();
13134
13637
  if (!state.analyticsPref) loadAnalyticsPref();
13135
13638
  return;
13136
13639
  }
13640
+ if (custTab === 'feedback') {
13641
+ pop.innerHTML = gearShell(gearFeedbackHTML({ solo: true }));
13642
+ wireGearCommon(pop);
13643
+ return;
13644
+ }
13137
13645
  if (custTab === 'mcp') {
13138
- pop.innerHTML = gearTabsHTML() + gearMcpHTML();
13646
+ pop.innerHTML = gearShell(gearMcpHTML());
13139
13647
  wireGearCommon(pop);
13140
13648
  const mc2 = pop.querySelector('[data-gmcpcopy]'); if (mc2) mc2.onclick = () => {
13141
13649
  navigator.clipboard?.writeText(state.connectCommand || '').then(() => { mc2.textContent = 'COPIED'; setTimeout(() => { mc2.textContent = 'COPY'; }, 1500); });
@@ -13206,7 +13714,7 @@ function buildGearPop(){
13206
13714
  const th = state.theme || 'gradient';
13207
13715
  const sec = (on, html) => `<div class="gsec${on ? '' : ' dis'}">${html}</div>`;
13208
13716
  // FONT exploration section retired (v45 §1): the brand fonts are final — no chips.
13209
- pop.innerHTML = gearTabsHTML() +
13717
+ pop.innerHTML = gearShell(
13210
13718
  `<h4>THEME</h4><div class="swrow">${themeBtns}</div>` +
13211
13719
  `<h4>BOARD</h4><div class="swrow">${boardChips}</div>` +
13212
13720
  sec(th === 'gradient', `<h4>COLOR</h4><div class="swrow">${arcSw}</div>` +
@@ -13224,7 +13732,7 @@ function buildGearPop(){
13224
13732
  (cust.btn === 0 ? `<h4>RING</h4><div class="swrow">${ringSw}</div>` : '') +
13225
13733
  // LOGO row removed (v45 §3: code-fixed) · MCP CONNECT moved to the MCP tab (v46)
13226
13734
  // 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>`;
13735
+ `<div class="cfoot"><button class="cbtn cancel" data-gccancel="1">Cancel</button><button class="cbtn save" data-gcsave="1">Save</button></div>`);
13228
13736
  wireGearCommon(pop);
13229
13737
  // EDIT SESSION (v45 §3): every handler below is LIVE PREVIEW ONLY — mutate cust/state,
13230
13738
  // apply to the DOM, re-render the pop. localStorage is written ONLY in custSave().
@@ -13256,16 +13764,14 @@ function buildGearPop(){
13256
13764
  // live previews that never persist and never close the panel; Save is the only moment
13257
13765
  // localStorage is written; Cancel / Esc / outside-tap restore the snapshot fully. =====
13258
13766
  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 = '';
13767
+ function custPlace(){ // flagship: centred dialog (CDO pattern C) CSS owns the position now,
13768
+ // so the measured rail edge that used to be written here must be cleared, or the inline
13769
+ // left would fight left:50% and the dialog would sit off-centre. Other layouts keep the
13770
+ // .gearwrap anchor and never had an inline left to begin with.
13771
+ $('gearPop').style.left = '';
13265
13772
  }
13266
13773
  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
13774
+ custTab = tab || 'settings'; // v46 deep-link (flagship __custOpen(tab)): menu entries land on their tab
13269
13775
  custSnap = { cust: { ...cust }, theme: state.theme, band: fsUI.band };
13270
13776
  buildGearPop(); custPlace();
13271
13777
  $('gearPop').classList.add('open');
@@ -13320,15 +13826,16 @@ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('gearP
13320
13826
  $('gearBtn').onclick = (e) => { e.stopPropagation(); const p = $('gearPop'); if (p.classList.contains('open')) custCancel(); else custOpen(); };
13321
13827
  // outside-tap = Cancel (v45 §3). e.composedPath() is frozen at dispatch, so a swatch that
13322
13828
  // 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
13829
+ // selections never close the panel. #fsMoreBtn (the rail gear) is excluded so its own
13830
+ // click is never read as an outside tap otherwise pressing it would close and reopen the
13831
+ // dialog in a single press. #deleteConfirmOverlay is excluded
13325
13832
  // too (Masa指示 2026-07-16): it's a separate top-level overlay a settings row can open
13326
13833
  // (e.g. Log out) — without this, confirming/cancelling it read as an outside tap and
13327
13834
  // closed the settings panel underneath it as a surprise side effect.
13328
13835
  document.addEventListener('click', (e) => {
13329
13836
  const p = $('gearPop'); if (!p.classList.contains('open')) return;
13330
13837
  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;
13838
+ if (e.target.closest('.gearwrap') || e.target.closest('#fsMoreBtn') || e.target.closest('#deleteConfirmOverlay')) return;
13332
13839
  custCancel();
13333
13840
  });
13334
13841
  // 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.124",
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": {