@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.
- package/app/index.html +420 -23
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7397
|
-
|
|
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('
|
|
7504
|
-
|
|
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
|
|
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">
|
|
7943
|
-
<div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New
|
|
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
|
|
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 ? '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
12408
|
+
ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px';
|
|
12161
12409
|
}
|
|
12162
12410
|
});
|
|
12163
|
-
|
|
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