@galda/cli 0.10.123 → 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 +420 -23
  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
@@ -1755,7 +1758,12 @@
1755
1758
  #fsRoot.lanefold .lanetg{left:50%;margin-left:-14px} /* no rotation (v45 §4) */
1756
1759
  #fsRoot.lanefold .fslane{flex-basis:44px;padding:18px 7px}
1757
1760
  #fsRoot.lanefold .fslane>*:not(.lanetg){display:none}
1758
- #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}
1759
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}
1760
1768
  #fsRoot .fslane:hover .lanegrip::after{opacity:1}
1761
1769
  #fsRoot .lanebody{display:flex;flex-direction:column;min-height:0;flex:1;overflow-y:auto;overflow-x:hidden;scrollbar-width:none}
@@ -1797,7 +1805,81 @@
1797
1805
  /* ---- kanban board: fills the SAME lane slot the To Do list uses (var(--lanew-board), independent
1798
1806
  from the list's own var(--lanew)) — swapping List⇄Board never resizes the lane or the chat.
1799
1807
  5+ columns don't fit the lane width, so it scrolls horizontally in place instead of growing. ---- */
1800
- #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}
1801
1883
  #fsRoot .fslane.boardmode #fsTodoSec .tl,#fsRoot .fslane.boardmode #fsTodoSec .doneline,#fsRoot .fslane.boardmode #fsTodoSec #fsDonelist{display:none}
1802
1884
  /* Board mode = board ONLY (Masa 2026-07-17): the Review section — proof thumbnails, PR
1803
1885
  links, approve/dismiss, and its "N ready" summary line — is spoken by the board's own
@@ -1812,7 +1894,14 @@
1812
1894
  container-box position (measured identical, 63px both), but the glyph sits differently inside that box. Scoped here, not on <body>, to avoid
1813
1895
  a global typography change. */
1814
1896
  #fsRoot .fslane.boardmode #fsBoard{display:flex}
1815
- #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)}
1816
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) */
1817
1906
  .fsbcolhd{display:flex;align-items:center;gap:8px;padding:1px 4px 9px;flex:0 0 auto}
1818
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)}
@@ -1923,6 +2012,19 @@
1923
2012
  #fsRoot .qadd.editing{opacity:1;color:var(--ink);cursor:text}
1924
2013
  #fsRoot .qadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
1925
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)}
1926
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}
1927
2029
  #fsRoot .lateridle .msg{flex:1;min-width:0}
1928
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}
@@ -3088,6 +3190,7 @@
3088
3190
  /* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
3089
3191
  が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
3090
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 */
3091
3194
  /* Masa: on mobile drop the drag-to-swap grip (⠿) — it collides with the lane close ✕ and
3092
3195
  there's nothing to drag on a phone. Keep the ✕. */
3093
3196
  body[data-layout="flagship"] #fsRoot .panelgrip{display:none}
@@ -3216,6 +3319,8 @@
3216
3319
  <div id="tobottom" title="Back to latest">↓</div>
3217
3320
  <div class="errbar" id="errbar"></div>
3218
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>
3219
3324
  <div class="queue" id="queue"></div>
3220
3325
  <!-- Context row (Masa 2026-07-22: 「claude の表記に合わせて」). The Claude app
3221
3326
  puts separate rounded chips above the box — repo · branch|worktree · add —
@@ -3391,6 +3496,10 @@
3391
3496
  </span>
3392
3497
  <div class="fswait" id="fsWait"><!-- waiting badge: "N Needs you / N Review" beside the toggle, board mode only (renderFsWait) --></div>
3393
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>
3394
3503
  <!-- mobile-only sheet header (H25②): grab handle + close X; display:none on desktop -->
3395
3504
  <div class="lanemtop"><span class="lanemgrab"></span><button class="lanemx" id="fsLaneClose" title="Close" aria-label="Close">✕</button></div>
3396
3505
  <div class="lanebody">
@@ -4070,6 +4179,14 @@ function saReviewTitle(g){
4070
4179
  // reviewSummary.title, so they fall back to the request text unchanged (no regression).
4071
4180
  return String(g?.reviewSummary?.title || g?.text || g?.plan?.[0] || g?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim().slice(0, 90);
4072
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
+ }
4073
4190
  // H23 (2026-07-18) evidence-kind detection. flagship's demo data carries an explicit
4074
4191
  // `kind` field; real goals/tasks have no such field, so the review card derives one
4075
4192
  // from real signals it already has (a captured proof image, whether the changed files
@@ -5321,6 +5438,30 @@ function ensureLayoutAnchors(){
5321
5438
  if (el && !el._anchor) { const a = document.createComment('home:' + id); el.parentNode.insertBefore(a, el); el._anchor = a; }
5322
5439
  }
5323
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
+ }
5324
5465
  function positionLayout(){
5325
5466
  ensureLayoutAnchors();
5326
5467
  // The settings panel escapes the hidden top bar in flagship (.top{display:none} would
@@ -5337,7 +5478,7 @@ function positionLayout(){
5337
5478
  // Composer (queue + input + FREE LIMIT banner) + transient #errbar live at the bottom of
5338
5479
  // the ONE center card — desktop and mobile alike (全面つなげた下地: feed scrolls above,
5339
5480
  // composer sits on the same connected surface, the queue tray shares that 下地).
5340
- const host = $('fsCompwrap'), cw = $('composerWrap');
5481
+ const host = composerHost(), cw = $('composerWrap');
5341
5482
  if (host && cw && cw.parentNode !== host) host.appendChild(cw);
5342
5483
  const eb = $('errbar');
5343
5484
  if (host && eb && eb.parentNode !== host) host.appendChild(eb);
@@ -6913,7 +7054,11 @@ const fsUI = {
6913
7054
  band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
6914
7055
  revMore: false, // "+N more" fold on the review list (§1.5-8)
6915
7056
  todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
6916
- 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',
6917
7062
  logOpen: new Set(), logFocus: null, // "View log" targets
6918
7063
  repsOpen: new Set(), // reply-group folds
6919
7064
  };
@@ -7087,7 +7232,7 @@ function renderFsFeed(){
7087
7232
  nd.hidden = false;
7088
7233
  nd.innerHTML = `<span class="nd-lb">Doing</span><span class="nd-run"></span>`
7089
7234
  + (n != null ? `<span class="gno nd-no" data-n="${n}">#${n}</span>` : '')
7090
- + `<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>`
7091
7236
  + `<span class="nd-pct">${pct}%</span><div class="nd-bar"><i style="width:${pct}%"></i></div>`;
7092
7237
  } else { nd.hidden = true; nd.innerHTML = ''; }
7093
7238
  }
@@ -7333,6 +7478,8 @@ function renderFsLane(){
7333
7478
  const ae = document.activeElement;
7334
7479
  if (ae && lane.contains(ae) && ['INPUT', 'TEXTAREA'].includes(ae.tagName)) return;
7335
7480
  lane.classList.toggle('boardmode', fsUI.todoView === 'board');
7481
+ lane.closest('#fsRoot')?.classList.toggle('boardmode', fsUI.todoView === 'board');
7482
+ placeComposer();
7336
7483
  syncFsTvSeg();
7337
7484
  renderFsReview(); renderFsTodo();
7338
7485
  if (fsUI.todoView === 'board') renderFsBoard();
@@ -7353,6 +7500,12 @@ function fsBoardBucket(goal, tasks){
7353
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)
7354
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.
7355
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.
7356
7509
  if (s === 'running' && !goal?.startedAt) return 'todo'; // goalChip: planned, nothing started yet
7357
7510
  // Otherwise the goal is in flight (planning / retesting / running-and-started). BUT a
7358
7511
  // goal whose status stuck 'running' after every task already finished is NOT moving —
@@ -7393,8 +7546,14 @@ function renderFsBoard(){
7393
7546
  // (#input) — the real create path (no editable rows / no per-column status invention like the
7394
7547
  // flagship mock; a new goal enters via the composer). Earlier this was To-Do-only, which read
7395
7548
  // as "position is off" against the Artifact (Masa 2026-07-18).
7396
- const col = (name, stg, count, html, stage) =>
7397
- `<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
+ };
7398
7557
  // Depth card (CDO, Masa 2026-07-17): #NN + title, colour on the dot only, and a continuous
7399
7558
  // stroke + n/m ONLY while the goal is actually in flight. The column already names the
7400
7559
  // status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
@@ -7449,6 +7608,10 @@ function renderFsBoard(){
7449
7608
  else if (healthyRun) dot = 'run';
7450
7609
  // To Do (idle) and Later carry no dot — the column already says it. Quiet = dimmer title.
7451
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);
7452
7615
  const n = goalNo.get(g.id);
7453
7616
  let h = '<div class="lbc-hd">'
7454
7617
  + (dot ? `<span class="lbc-dot ${dot}"></span>` : '')
@@ -7474,7 +7637,7 @@ function renderFsBoard(){
7474
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>`;
7475
7638
  }
7476
7639
  }
7477
- 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>`;
7478
7641
  };
7479
7642
  const no = (g) => goalNo.get(g.id) ?? Infinity;
7480
7643
  const bucket = { todo: [], doing: [], needs: [], review: [], done: [], later: [], rejected: [] };
@@ -7500,8 +7663,23 @@ function renderFsBoard(){
7500
7663
  + (bucket.needs.length ? col('Needs you', '#ffca16', bucket.needs.length, bucket.needs.map(goalCard).join(''), 'needs') : '')
7501
7664
  + col('Review', 'var(--review)', reviewDigest.length, reviewDigest.map(reviewCard).join(''), 'review')
7502
7665
  + col('Done', 'var(--ink3)', bucket.done.length, bucket.done.slice(0, 40).map(goalCard).join(''), 'done')
7503
- + col('Later', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
7504
- 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
+ }
7505
7683
  for (const b of el.querySelectorAll('[data-fspause]')) b.onclick = togglePauseActive; // Doing列の一時停止/再生
7506
7684
  for (const c of el.querySelectorAll('[data-fsgoal]')) c.onclick = (e) => {
7507
7685
  if (e.target.closest('button, a')) return;
@@ -7844,7 +8022,7 @@ function renderFsTodo(){
7844
8022
  const doingRows =
7845
8023
  running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
7846
8024
  const goal = pgoals.find((g) => g.id === t.goalId);
7847
- const title = t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
8025
+ const title = fsDoingName(t, goal);
7848
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>
7849
8027
  <div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
7850
8028
  }).join('')
@@ -7939,8 +8117,8 @@ function renderFsTodo(){
7939
8117
  ${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
7940
8118
  ${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
7941
8119
  <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7942
- <span class="tlcap">Later</span>
7943
- <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>
7944
8122
  ${laterRows}
7945
8123
  </div>
7946
8124
  ${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
@@ -7952,7 +8130,7 @@ function renderFsTodo(){
7952
8130
  const laterAdd = el.querySelector('.qadd[data-fslaternew]');
7953
8131
  if (laterAdd) laterAdd.onclick = (e) => {
7954
8132
  e.stopPropagation();
7955
- fsInlineNewRow(laterAdd, 'Add to Later…', (text) => fsCreateGoal(text, true));
8133
+ fsInlineNewRow(laterAdd, 'Add to Backlog…', (text) => fsCreateGoal(text, true));
7956
8134
  };
7957
8135
  const dl = el.querySelector('#fsDoneLine');
7958
8136
  if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
@@ -8040,17 +8218,34 @@ function renderFsTodo(){
8040
8218
  catch { showErr('Cannot delete a running/finished goal — reply to append instead'); return; }
8041
8219
  if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
8042
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;
8043
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); };
8044
8237
  const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
8045
8238
  body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending }) });
8046
8239
  if (r.status === 402) {
8047
8240
  const data = await r.json().catch(() => ({}));
8241
+ drop(); render();
8048
8242
  showBillingBlocked(data.blocked);
8049
8243
  return false;
8050
8244
  }
8051
- 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; }
8052
8246
  clearBillingBlocked();
8053
8247
  const goal = await r.json();
8248
+ drop(); // the real one takes its place
8054
8249
  if (goal?.id) upsert(state.goals, goal);
8055
8250
  render();
8056
8251
  return true;
@@ -8384,6 +8579,9 @@ function renderClawdHero(){
8384
8579
  e.stopPropagation();
8385
8580
  fsUI.todoView = b.dataset.fstv; localStorage.setItem('fsTodoView', fsUI.todoView);
8386
8581
  $('fsLane').classList.toggle('boardmode', fsUI.todoView === 'board');
8582
+ $('fsRoot').classList.toggle('boardmode', fsUI.todoView === 'board');
8583
+ window.__fsApplyMidOrder?.();
8584
+ placeComposer();
8387
8585
  syncFsTvSeg();
8388
8586
  if (fsUI.todoView === 'board') renderFsBoard();
8389
8587
  };
@@ -8438,7 +8636,15 @@ function renderClawdHero(){
8438
8636
  // minReadableCenterW, given however wide the lane currently is — no more flat 280px ceiling
8439
8637
  // that stopped rightward drags well short of the window edge (Masa 2026-07-16: "もっと広げ
8440
8638
  // られるようにできる?"). Floors at 72 (the min) on a narrow/laned-out window.
8441
- 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());
8442
8648
  const saved = Number(localStorage.getItem('fsLaneW'));
8443
8649
  const savedRail = Number(localStorage.getItem('fsRailW'));
8444
8650
  const restoreLane = saved >= 280 && saved <= 540;
@@ -8462,10 +8668,45 @@ function renderClawdHero(){
8462
8668
  // those chips get clipped off the edge of the composer card (Masa 2026-07-16 repro:
8463
8669
  // dragged the board grip wide on a 1280px-wide window, "Medium"+send were cut off).
8464
8670
  // 440 is the smallest value that doesn't clip on a real laptop width.
8465
- 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;
8466
8675
  const boardMax = () => Math.max(280, window.innerWidth - 28 - 24
8467
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;
8468
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.
8469
8710
  if (savedBoard >= 280 && savedBoard <= boardMax()) root.style.setProperty('--lanew-board', `${savedBoard}px`);
8470
8711
  let sx = 0, sw = 0, drag = false, boarding = false;
8471
8712
  grip.addEventListener('mousedown', (e) => {
@@ -8475,7 +8716,10 @@ function renderClawdHero(){
8475
8716
  document.addEventListener('mousemove', (e) => {
8476
8717
  if (!drag) return;
8477
8718
  const max = boarding ? boardMax() : 540;
8478
- 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));
8479
8723
  root.style.setProperty(boarding ? '--lanew-board' : '--lanew', `${w}px`);
8480
8724
  });
8481
8725
  document.addEventListener('mouseup', () => {
@@ -8546,6 +8790,10 @@ function renderClawdHero(){
8546
8790
  const side = sideFor(p, e.clientX);
8547
8791
  const wantLeft = side === 'left' ? dragged : (dragged === 'center' ? 'lane' : 'center');
8548
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);
8549
8797
  });
8550
8798
  }
8551
8799
  })();
@@ -12032,7 +12280,7 @@ function acceptPalette(){
12032
12280
  $('input').value = stripLeadingCmd($('input').value);
12033
12281
  closePalette();
12034
12282
  }
12035
- 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();
12036
12284
  return true;
12037
12285
  }
12038
12286
  // Close the palette on any click outside the composer.
@@ -12157,10 +12405,159 @@ $('input').addEventListener('keydown', (e) => {
12157
12405
  // Up-arrow on an empty input recalls the last sent goal for editing.
12158
12406
  if (e.key === 'ArrowUp' && !state.palette && !$('input').value && _lastSend.text) {
12159
12407
  e.preventDefault(); const ta = $('input'); ta.value = _lastSend.text;
12160
- 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';
12161
12409
  }
12162
12410
  });
12163
- $('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(); });
12164
12561
  $('input').addEventListener('paste', (e) => {
12165
12562
  for (const item of e.clipboardData?.items ?? []) {
12166
12563
  if (item.type.startsWith('image/')) { e.preventDefault(); uploadFile(item.getAsFile()); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.123",
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": {