@galda/cli 0.10.16 → 0.10.19
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/CLAUDE.md +30 -1
- package/app/index.html +418 -53
- package/bin/manager-for-ai.mjs +18 -12
- package/engine/lib.mjs +536 -12
- package/engine/relay-client.mjs +101 -32
- package/engine/server.mjs +478 -20
- package/package.json +2 -2
package/app/index.html
CHANGED
|
@@ -610,10 +610,14 @@
|
|
|
610
610
|
.confetti-piece{position:fixed;pointer-events:none;z-index:9999;will-change:transform,opacity}
|
|
611
611
|
|
|
612
612
|
/* connect modal */
|
|
613
|
-
.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
613
|
+
/* A confirm dialog (deleteConfirmOverlay etc.) must always sit above other
|
|
614
|
+
floating chrome it can be opened from — was 60, then 90 (cleared .gearpop's
|
|
615
|
+
80 for the customize-panel discard confirm), but that still sat under the
|
|
616
|
+
context menu (95) and the settings panel rendered in front of its own Log
|
|
617
|
+
out confirm (Masa 指示 2026-07-16). 100 clears every other floating layer
|
|
618
|
+
(gearpop 80, context menu 95) while staying under true full-screen gates
|
|
619
|
+
(lightbox 200, #connectGate 1000). */
|
|
620
|
+
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);backdrop-filter:blur(6px);z-index:100;display:none;place-items:center}
|
|
617
621
|
.overlay.show{display:grid}
|
|
618
622
|
.modal{width:640px;max-width:92vw;background:var(--surface);border:1px solid var(--hair2);border-radius:16px;padding:20px 22px;box-shadow:0 24px 70px rgba(0,0,0,.6)}
|
|
619
623
|
.modal h3{margin:0 0 6px;font-size:15px;letter-spacing:-.01em}
|
|
@@ -634,6 +638,10 @@
|
|
|
634
638
|
.mbtn.danger{background:color-mix(in srgb,var(--danger) 10%,var(--panel));color:color-mix(in srgb,var(--danger) 78%,var(--ink));
|
|
635
639
|
box-shadow:inset 0 0 0 1px rgba(var(--danger-rgb),.35)}
|
|
636
640
|
.mbtn.danger:hover{box-shadow:inset 0 0 0 1px var(--danger);background:color-mix(in srgb,var(--danger) 16%,var(--panel))}
|
|
641
|
+
/* Neutral confirm (not destructive/AI — e.g. Log out): same ink-solid tone as
|
|
642
|
+
the settings Save button (.cfoot .cbtn.save), not the iri default (AI-only, §5.5). */
|
|
643
|
+
.mbtn.solid{background:var(--ink);color:var(--invink)}
|
|
644
|
+
.mbtn.solid:hover{opacity:.9}
|
|
637
645
|
|
|
638
646
|
/* Slack-style centered delete confirmation (replaces native confirm() — restores a
|
|
639
647
|
decision lost in the flagship rewrite; see docs/confirmed-behaviors.md). Deliberately
|
|
@@ -980,7 +988,7 @@
|
|
|
980
988
|
.gearbtn{display:grid;place-items:center;width:30px;height:28px;border:1px solid var(--hair2);border-radius:8px;background:transparent;color:var(--ink2);cursor:pointer}
|
|
981
989
|
.gearbtn:hover{background:var(--surface2);color:var(--ink)}
|
|
982
990
|
.gearbtn svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
|
|
983
|
-
.gearpop{display:none;position:absolute;top:36px;right:0;z-index:80;background:var(--panel);border-radius:12px;box-shadow:0 10px 30px rgba(20,23,31,.18),0 0 0 1px var(--hair);padding:12px 13px;width:
|
|
991
|
+
.gearpop{display:none;position:absolute;top:36px;right:0;z-index:80;background:var(--panel);border-radius:12px;box-shadow:0 10px 30px rgba(20,23,31,.18),0 0 0 1px var(--hair);padding:12px 13px;width:320px;
|
|
984
992
|
max-height:78vh;overflow-y:auto;scrollbar-width:thin; /* TB edge + colour sections made it taller than the viewport */
|
|
985
993
|
/* opaque chip fill (HANDOFF-v45 §9): a translucent --canvas would let the wallpaper
|
|
986
994
|
bleed through and colour each row differently — this token mixes off the OPAQUE
|
|
@@ -1017,7 +1025,7 @@
|
|
|
1017
1025
|
the rail (left is set on open from the measured rail edge), max-height = viewport −
|
|
1018
1026
|
margins, internal scroll. White-translucent per §3: color-mix 74% + blur28 (theme-tinted).
|
|
1019
1027
|
The node is adopted to <body> in flagship (positionLayout) — .top is display:none there. */
|
|
1020
|
-
body[data-layout="flagship"] .gearpop{position:fixed;top:120px;right:auto;width:
|
|
1028
|
+
body[data-layout="flagship"] .gearpop{position:fixed;top:120px;right:auto;width:380px;max-height:calc(100vh - 160px);border-radius:14px;
|
|
1021
1029
|
background:color-mix(in srgb,var(--panel) 74%,transparent);
|
|
1022
1030
|
-webkit-backdrop-filter:blur(28px) saturate(1.35);backdrop-filter:blur(28px) saturate(1.35)}
|
|
1023
1031
|
/* settings = EDIT SESSION (HANDOFF-v45 §3, flagship .cfoot/.mcpbox port): sticky footer,
|
|
@@ -1267,6 +1275,29 @@
|
|
|
1267
1275
|
#fsRoot .fsstream::-webkit-scrollbar{display:none}
|
|
1268
1276
|
#fsFeed{display:flex;flex-direction:column;gap:8px;padding-top:2px}
|
|
1269
1277
|
#fsRoot .cc-say{font-size:13.5px;color:var(--ink)}
|
|
1278
|
+
/* Pattern A (H23 / AMENDMENT-center-feed-2026-07-16, CDO): the center feed is
|
|
1279
|
+
a conversation — the user's own turns live here too, not just Claude Code's.
|
|
1280
|
+
器を増やさない(§1.5-1): no bubbles — a speaker label + a hairline turn border. */
|
|
1281
|
+
#fsRoot .cc-you{padding-top:11px;border-top:1px solid var(--hair)}
|
|
1282
|
+
#fsRoot .cc-you:first-child{padding-top:0;border-top:0}
|
|
1283
|
+
#fsRoot .cc-you .cy-n{display:block;font:600 8.5px/1 var(--mono);letter-spacing:.13em;text-transform:uppercase;color:var(--ink3);margin-bottom:5px}
|
|
1284
|
+
#fsRoot .cc-you .cy-t{font-size:13.5px;color:var(--ink);line-height:1.45}
|
|
1285
|
+
/* Typing cursor on the streaming reply frontier (rides the client-side reveal —
|
|
1286
|
+
the CLI returns the reply as one block, not token deltas). Solid block, hard
|
|
1287
|
+
terminal blink (steps(1)); --ink adapts across all themes. */
|
|
1288
|
+
#fsRoot .cc-caret{display:inline-block;width:6px;height:14px;margin-left:2px;transform:translateY(2px);
|
|
1289
|
+
background:var(--ink);animation:ccblink 1.05s steps(1) infinite}
|
|
1290
|
+
@keyframes ccblink{50%{opacity:0}}
|
|
1291
|
+
@media (prefers-reduced-motion:reduce){#fsRoot .cc-caret{animation:none}}
|
|
1292
|
+
/* "→ Added to To Do #NN" — shown quietly in the flow when a turn became a task.
|
|
1293
|
+
Undo appears on hover only (§1.5-10). No filled surface. */
|
|
1294
|
+
#fsRoot .cc-mark{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;color:var(--ink3);padding:1px 0}
|
|
1295
|
+
#fsRoot .cc-mark .cm-ar{color:var(--ink2);font-size:12.5px;line-height:1}
|
|
1296
|
+
#fsRoot .cc-mark .cm-t{color:var(--ink2)}
|
|
1297
|
+
#fsRoot .cc-mark .cm-no{color:var(--ink3);font-variant-numeric:tabular-nums}
|
|
1298
|
+
#fsRoot .cc-mark .cm-undo{margin-left:2px;padding:0;border:0;background:none;font:inherit;color:var(--ink3);cursor:pointer;opacity:0;transition:opacity .15s,color .15s}
|
|
1299
|
+
#fsRoot .cc-mark:hover .cm-undo{opacity:1}
|
|
1300
|
+
#fsRoot .cc-mark .cm-undo:hover{color:var(--blue)}
|
|
1270
1301
|
/* Claude Code CLI language (HANDOFF-v45 §12, flagship .cc-t port): gap:0 — the bullet's
|
|
1271
1302
|
own margin-right does the spacing, not a flex gap (keeps ⏺ tight against the tool name
|
|
1272
1303
|
the way the real CLI reads). */
|
|
@@ -1313,6 +1344,11 @@
|
|
|
1313
1344
|
discard path. */
|
|
1314
1345
|
#fsRoot .cc-ask .acts{position:absolute;top:8px;right:8px;display:flex;opacity:0;transition:opacity .15s}
|
|
1315
1346
|
#fsRoot .cc-ask:hover .acts{opacity:1}
|
|
1347
|
+
/* Touch devices have no hover → the hover-only trash was permanently invisible
|
|
1348
|
+
and the「Needs your answer」card had no reachable discard path (Masa
|
|
1349
|
+
2026-07-16: 右上のdeleteが押せない). Reveal it (dimmed, so it still reads as
|
|
1350
|
+
secondary per §1.5-10) whenever the pointer can't hover. */
|
|
1351
|
+
@media (hover: none){ #fsRoot .cc-ask .acts{opacity:.55} }
|
|
1316
1352
|
#fsRoot .qchips{display:flex;gap:6px;flex-wrap:wrap}
|
|
1317
1353
|
#fsRoot .achip{height:28px;padding:0 12px;border-radius:8px;border:1px solid var(--hair2);background:var(--panel);
|
|
1318
1354
|
color:var(--ink2);font:inherit;font-size:12.5px;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
|
|
@@ -1321,6 +1357,10 @@
|
|
|
1321
1357
|
#fsRoot .fs-askbar{display:none;border:1px solid var(--hair2);border-bottom:0;border-left:2px solid var(--blue);border-radius:14px 14px 0 0;padding:9px 13px 11px} /* 枠1px (v3 §6.5) */
|
|
1322
1358
|
#fsRoot .compwrap{max-width:var(--fscenterw,760px);width:100%;margin:10px auto 0;flex:0 0 auto;position:relative;z-index:1;
|
|
1323
1359
|
transition:max-width .28s cubic-bezier(.4,0,.2,1)}
|
|
1360
|
+
/* #errbar is adopted into .compwrap by positionLayout() (flagship hides .main
|
|
1361
|
+
outright, its original home) — float it just above the input, matching the
|
|
1362
|
+
legacy layout's "just above the composer" placement (CDO判定 2026-07-16). */
|
|
1363
|
+
#fsRoot .compwrap>.errbar{position:absolute;left:50%;bottom:calc(100% + 10px);transform:translateX(-50%)}
|
|
1324
1364
|
#fsRoot.lanefold{--fscenterw:1020px} /* folded space flows to the center (§1.5-7) — one var flip keeps feed+composer in lockstep */
|
|
1325
1365
|
/* adopted composer (#composerWrap moves in — same node, same handlers, focus fix intact) */
|
|
1326
1366
|
body[data-layout="flagship"] #composerWrap{padding:0;border-top:0;max-width:none;width:auto;margin:0}
|
|
@@ -1937,7 +1977,7 @@
|
|
|
1937
1977
|
#fsClawdHero span{display:block;margin-top:5px;font:500 10px/1.4 var(--mono);letter-spacing:.08em;color:var(--ink3)}
|
|
1938
1978
|
/* Welcome P1 (H17 item 4, flagship f9c75ad): grounded hop + clean-cut eyes on settle + our CSS confetti.
|
|
1939
1979
|
Two pixel frames (neutral black-eyes / laughing), no arm-raise, no official confetti. reduce-motion=static. */
|
|
1940
|
-
#fsClawdHero .cstage{position:relative;width:170px;height:132px;display:flex;align-items:flex-end;justify-content:center;margin:0 auto 2px}#fsClawdHero .cw{position:relative;width:150px;transform-origin:50% 100%;will-change:transform;animation:chHop 2.4s linear infinite}#fsClawdHero .cw img{position:absolute;left:0;bottom:0;width:100%;display:block;image-rendering:pixelated}#fsClawdHero .cw .chNeu{animation:chNeu 2.4s linear infinite}#fsClawdHero .cw .chLau{opacity:0;animation:chLau 2.4s linear infinite}#fsClawdHero .chConf{position:absolute;left:50%;top:34px;width:0;height:0;pointer-events:none;z-index:3}#fsClawdHero .chConf i{position:absolute;width:4px;height:7px;border-radius:1.5px;opacity:0;transform:translate(-50%,-50%);animation:chPop 2.4s cubic-bezier(.2,.7,.3,1) infinite}@keyframes chHop{0%,52%{transform:translateY(0) scale(1,1)}55%{transform:translateY(0) scale(1.08,.85)}58%{transform:translateY(-16px) scale(.95,1.14)}61%{transform:translateY(-27px) scale(.98,1.05)}64%{transform:translateY(-31px) scale(1,1)}67%{transform:translateY(-26px) scale(.98,1.05)}70%{transform:translateY(-14px) scale(.95,1.12)}73%{transform:translateY(0) scale(1.1,.82)}77%{transform:translateY(-4px) scale(.99,1.02)}80%,100%{transform:translateY(0) scale(1,1)}}@keyframes chLau{0%,53.8%{opacity:0}54%,85.8%{opacity:1}86%,100%{opacity:0}}@keyframes chNeu{0%,53.8%{opacity:1}54%,85.8%{opacity:0}86%,100%{opacity:1}}@keyframes chPop{0%,54%{opacity:0;transform:translate(0,6px) rotate(0) scale(.5)}60%{opacity:1;transform:translate(calc(var(--dx)*.5),calc(var(--dy)*.5)) rotate(calc(var(--rot)*.5)) scale(1)}72%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot))}100%{opacity:0;transform:translate(calc(var(--dx)*1.1),calc(var(--dy) + 44px)) rotate(calc(var(--rot)*1.4)) scale(.9)}}@media (prefers-reduced-motion:reduce){#fsClawdHero .cw,#fsClawdHero .cw .chNeu,#fsClawdHero .cw .chLau{animation:none}#fsClawdHero .cw .chLau{opacity:0}#fsClawdHero .chConf{display:none}}
|
|
1980
|
+
#fsClawdHero .cstage{position:relative;left:-13px;width:170px;height:132px;display:flex;align-items:flex-end;justify-content:center;margin:0 auto 2px}#fsClawdHero .cw{position:relative;width:150px;transform-origin:50% 100%;will-change:transform;animation:chHop 2.4s linear infinite}#fsClawdHero .cw img{position:absolute;left:0;bottom:0;width:100%;display:block;image-rendering:pixelated}#fsClawdHero .cw .chNeu{animation:chNeu 2.4s linear infinite}#fsClawdHero .cw .chLau{opacity:0;animation:chLau 2.4s linear infinite}#fsClawdHero .chConf{position:absolute;left:50%;top:34px;width:0;height:0;pointer-events:none;z-index:3}#fsClawdHero .chConf i{position:absolute;width:4px;height:7px;border-radius:1.5px;opacity:0;transform:translate(-50%,-50%);animation:chPop 2.4s cubic-bezier(.2,.7,.3,1) infinite}@keyframes chHop{0%,52%{transform:translateY(0) scale(1,1)}55%{transform:translateY(0) scale(1.08,.85)}58%{transform:translateY(-16px) scale(.95,1.14)}61%{transform:translateY(-27px) scale(.98,1.05)}64%{transform:translateY(-31px) scale(1,1)}67%{transform:translateY(-26px) scale(.98,1.05)}70%{transform:translateY(-14px) scale(.95,1.12)}73%{transform:translateY(0) scale(1.1,.82)}77%{transform:translateY(-4px) scale(.99,1.02)}80%,100%{transform:translateY(0) scale(1,1)}}@keyframes chLau{0%,53.8%{opacity:0}54%,85.8%{opacity:1}86%,100%{opacity:0}}@keyframes chNeu{0%,53.8%{opacity:1}54%,85.8%{opacity:0}86%,100%{opacity:1}}@keyframes chPop{0%,54%{opacity:0;transform:translate(0,6px) rotate(0) scale(.5)}60%{opacity:1;transform:translate(calc(var(--dx)*.5),calc(var(--dy)*.5)) rotate(calc(var(--rot)*.5)) scale(1)}72%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot))}100%{opacity:0;transform:translate(calc(var(--dx)*1.1),calc(var(--dy) + 44px)) rotate(calc(var(--rot)*1.4)) scale(.9)}}@media (prefers-reduced-motion:reduce){#fsClawdHero .cw,#fsClawdHero .cw .chNeu,#fsClawdHero .cw .chLau{animation:none}#fsClawdHero .cw .chLau{opacity:0}#fsClawdHero .chConf{display:none}}
|
|
1941
1981
|
#fsRoot.clawdhero #fsClawdHero{display:block}
|
|
1942
1982
|
/* Non-Claude agent (e.g. Codex) hero: no Clawd mascot (that's a Claude Code brand mark,
|
|
1943
1983
|
DESIGN-RULES v3 §6.5) — a plain glyph badge stands in so the hero still names the
|
|
@@ -2047,6 +2087,11 @@
|
|
|
2047
2087
|
.bx-lb{font:600 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--red)}
|
|
2048
2088
|
.bx-q{font-size:13px;color:var(--ink);margin-top:4px}
|
|
2049
2089
|
.bx-act{flex:none}
|
|
2090
|
+
/* neutral ask (not a limit/error — e.g. the sign-in nudge): same card, --blue
|
|
2091
|
+
accent instead of --red, matching the existing AI-confirmation blue
|
|
2092
|
+
(§7決定ログ「AI確認:色は青系」) rather than inventing a new color. */
|
|
2093
|
+
.bx-ask.info{border-left-color:var(--blue)}
|
|
2094
|
+
.bx-ask.info .bx-lb{color:var(--blue)}
|
|
2050
2095
|
/* ② post-checkout return bar */
|
|
2051
2096
|
.bx-cobar{box-shadow:inset 0 0 0 1px var(--hair2);border-radius:11px;padding:10px 13px;display:flex;align-items:center;gap:10px;font-size:12.5px;margin:0 auto 12px;width:100%;max-width:var(--fscenterw,760px)}
|
|
2052
2097
|
.bx-cobar[hidden]{display:none}
|
|
@@ -2092,7 +2137,7 @@
|
|
|
2092
2137
|
<button class="laybtn" data-theme="cinema">Cinema</button>
|
|
2093
2138
|
</div>
|
|
2094
2139
|
<div class="gearwrap">
|
|
2095
|
-
<button class="gearbtn" id="gearBtn" title="
|
|
2140
|
+
<button class="gearbtn" id="gearBtn" title="Settings" aria-label="Settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
|
|
2096
2141
|
<div class="gearpop" id="gearPop"></div>
|
|
2097
2142
|
</div>
|
|
2098
2143
|
<button class="pill-btn" id="connect">Connect MCP</button>
|
|
@@ -2107,6 +2152,12 @@
|
|
|
2107
2152
|
<div class="errbar" id="errbar"></div>
|
|
2108
2153
|
<div class="composer-wrap" id="composerWrap">
|
|
2109
2154
|
<div class="queue" id="queue"></div>
|
|
2155
|
+
<!-- Conversation-first composer (SLICE 2, docs/HANDOFF-ONBOARDING-conversational.md):
|
|
2156
|
+
a chat/help/settings message gets a quiet reply HERE instead of
|
|
2157
|
+
becoming a To-Do. Reuses the .peraanswer surface; hidden until a
|
|
2158
|
+
reply exists (progressive disclosure — DESIGN-RULES §1.5-3, "存在
|
|
2159
|
+
しないものの器を先に見せない"). -->
|
|
2160
|
+
<div class="peraanswer" id="chatReply" hidden></div>
|
|
2110
2161
|
<div class="composer">
|
|
2111
2162
|
<div class="attachbar" id="attachbar" style="display:none"></div>
|
|
2112
2163
|
<!-- Slash-command palette (Masa決定 2026-07-07: chips→slash). Floats
|
|
@@ -2199,9 +2250,8 @@
|
|
|
2199
2250
|
<div class="lbrow"><span class="lb">Projects</span>
|
|
2200
2251
|
<button class="ghostbtn morebtn" id="fsMoreBtn" title="Settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 00.34 1.87l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.7 1.7 0 00-1.87-.34 1.7 1.7 0 00-1 1.56V21a2 2 0 11-4 0v-.09a1.7 1.7 0 00-1-1.56 1.7 1.7 0 00-1.87.34l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.7 1.7 0 00.34-1.87 1.7 1.7 0 00-1.56-1H3a2 2 0 110-4h.09a1.7 1.7 0 001.56-1 1.7 1.7 0 00-.34-1.87l-.06-.06a2 2 0 112.83-2.83l.06.06a1.7 1.7 0 001.87.34h.09a1.7 1.7 0 001-1.56V3a2 2 0 114 0v.09a1.7 1.7 0 001 1.56h.09a1.7 1.7 0 001.87-.34l.06-.06a2 2 0 112.83 2.83l-.06.06a1.7 1.7 0 00-.34 1.87v.09a1.7 1.7 0 001.56 1H21a2 2 0 110 4h-.09a1.7 1.7 0 00-1.56 1z"/></svg></button>
|
|
2201
2252
|
<div class="moremenu" id="fsMoreMenu">
|
|
2202
|
-
<button class="ghostbtn" id="fsCust">
|
|
2253
|
+
<button class="ghostbtn" id="fsCust">Theme</button>
|
|
2203
2254
|
<button class="ghostbtn" id="fsConn">Connect MCP</button>
|
|
2204
|
-
<button class="ghostbtn" id="fsTodoBd">To Do board</button>
|
|
2205
2255
|
<button class="ghostbtn" id="fsHist">History</button>
|
|
2206
2256
|
<button class="ghostbtn" id="fsSet">Settings</button>
|
|
2207
2257
|
</div>
|
|
@@ -2455,7 +2505,7 @@ const savedTheme = (() => {
|
|
|
2455
2505
|
if (v !== s) localStorage.setItem('theme', v);
|
|
2456
2506
|
return v;
|
|
2457
2507
|
})();
|
|
2458
|
-
const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, active: 'galda2', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
2508
|
+
const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'galda2', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
2459
2509
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
2460
2510
|
// except model which is sticky in localStorage). palette = open command list.
|
|
2461
2511
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -2463,6 +2513,28 @@ const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {
|
|
|
2463
2513
|
window.state = state; // top-level const isn't a window property; external checks probe window.state
|
|
2464
2514
|
const $ = (id) => document.getElementById(id);
|
|
2465
2515
|
|
|
2516
|
+
// Question free-text answer boxes (qansinput/fsansinput) are rebuilt from
|
|
2517
|
+
// scratch (container.innerHTML = ...) on every SSE-driven render, which — same
|
|
2518
|
+
// class of bug as the composer's c32fda1 fix — detaches whatever the user is
|
|
2519
|
+
// mid-typing into and drops focus + cursor position + the in-progress text.
|
|
2520
|
+
// Since these nodes can't be kept alive like the composer (they're generated
|
|
2521
|
+
// HTML strings, not a persistent DOM subtree), save the focused box's value
|
|
2522
|
+
// and selection before the rebuild and restore them onto the freshly-rendered
|
|
2523
|
+
// node with the same data-attribute id afterward.
|
|
2524
|
+
function captureFocusedAnswerInput(container, attr){
|
|
2525
|
+
const el = document.activeElement;
|
|
2526
|
+
if (!el || !container || !container.contains(el) || !el.hasAttribute(attr)) return null;
|
|
2527
|
+
return { id: el.getAttribute(attr), value: el.value, start: el.selectionStart, end: el.selectionEnd };
|
|
2528
|
+
}
|
|
2529
|
+
function restoreFocusedAnswerInput(container, attr, saved){
|
|
2530
|
+
if (!saved || !container) return;
|
|
2531
|
+
const el = container.querySelector(`[${attr}="${saved.id}"]`);
|
|
2532
|
+
if (!el) return;
|
|
2533
|
+
el.value = saved.value;
|
|
2534
|
+
el.focus();
|
|
2535
|
+
try { el.setSelectionRange(saved.start, saved.end); } catch { /* input type may not support it */ }
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2466
2538
|
const STATUS_LABEL = {
|
|
2467
2539
|
sending: 'Sending…', stacked: 'Queued', planning: 'Planning…', queued: 'To do', running: 'Working…', needsInput: 'Needs input',
|
|
2468
2540
|
review: 'In review', done: 'Done', failed: 'Failed', partial: 'Partly failed', interrupted: 'Interrupted', skipped: 'Skipped',
|
|
@@ -2553,10 +2625,15 @@ function setAgentEffort(agent){
|
|
|
2553
2625
|
localStorage.setItem('sel:effort', state.stickyEffort);
|
|
2554
2626
|
}
|
|
2555
2627
|
function renderAppSelect(){
|
|
2556
|
-
|
|
2557
|
-
|
|
2628
|
+
// Only offer agents whose CLI is actually installed (state.agents comes from the
|
|
2629
|
+
// server = AVAILABLE_AGENTS). A claude-only machine never shows Codex; a
|
|
2630
|
+
// Codex-only one never shows Claude Code. Never render an empty picker.
|
|
2631
|
+
const apps = CONNECTED_APPS.filter((a) => (state.agents || []).includes(a.id));
|
|
2632
|
+
const list = apps.length ? apps : CONNECTED_APPS.slice(0, 1);
|
|
2633
|
+
if (!list.some((a) => a.id === state.connectedApp)) state.connectedApp = list[0].id;
|
|
2634
|
+
const app = list.find((a) => a.id === state.connectedApp) ?? list[0];
|
|
2558
2635
|
$('appbtn').innerHTML = `${app.icon}<span>${esc(app.label)}</span>${ICONS.caret}`;
|
|
2559
|
-
$('appmenu').innerHTML =
|
|
2636
|
+
$('appmenu').innerHTML = list.map((a) =>
|
|
2560
2637
|
`<button type="button" class="appopt${a.id === state.connectedApp ? ' active' : ''}" data-val="${a.id}">${a.icon}<span>${esc(a.label)}</span></button>`
|
|
2561
2638
|
).join('');
|
|
2562
2639
|
}
|
|
@@ -2768,6 +2845,14 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
2768
2845
|
// Mirrors engine/lib.mjs buildReviewSummary()/requirementSummaryLine() (task 96)
|
|
2769
2846
|
// — kept in sync manually since the browser script isn't a module. Used by both
|
|
2770
2847
|
// the review checklist top summary and the chat-side review导线.
|
|
2848
|
+
// task 6 (2026-07-16): mirrors engine/lib.mjs goalTokenHistory() — one
|
|
2849
|
+
// {weightedTokens, requirementCount} sample per finished goal (excluding the
|
|
2850
|
+
// one being summarized), oldest-first, for the token-savings baseline below.
|
|
2851
|
+
function goalTokenHistoryLocal(goals, tasks, excludeGoalId){
|
|
2852
|
+
return (goals ?? [])
|
|
2853
|
+
.filter((g) => g && g.id !== excludeGoalId && GOAL_COMPLETE_STATUSES.includes(g.status) && g.runTokens != null)
|
|
2854
|
+
.map((g) => ({ weightedTokens: g.runTokens, requirementCount: (tasks ?? []).filter((t) => t.goalId === g.id && !t.reply).length }));
|
|
2855
|
+
}
|
|
2771
2856
|
function buildReviewSummary({ goal, tasks, approvedN = 0 } = {}){
|
|
2772
2857
|
const reqs = (tasks ?? []).filter((t) => !t.reply);
|
|
2773
2858
|
const total = reqs.length;
|
|
@@ -2777,6 +2862,9 @@ function buildReviewSummary({ goal, tasks, approvedN = 0 } = {}){
|
|
|
2777
2862
|
const source = plan.length ? plan : reqs.map((t) => t.title);
|
|
2778
2863
|
const what = source.map((s) => String(s ?? '').replace(/\s+/g, ' ').trim()).filter(Boolean).slice(0, 3).join(' / ');
|
|
2779
2864
|
const fallback = String(goal?.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
2865
|
+
// History is scoped to the same project as this goal — a goal in a small
|
|
2866
|
+
// toy project shouldn't be judged against a large project's average.
|
|
2867
|
+
const sameProjectGoals = (state.goals || []).filter((g) => g.projectId === goal?.projectId);
|
|
2780
2868
|
return { approvedN, total, headline: `${approvedN} / ${total} approved`, what: what || fallback,
|
|
2781
2869
|
detail: `${done}/${total} done${proofs ? ` · proof ×${proofs}` : ''}`,
|
|
2782
2870
|
failureMemory: goal?.failureMemory?.length ? goal.failureMemory[goal.failureMemory.length - 1] : null,
|
|
@@ -2789,6 +2877,8 @@ function buildReviewSummary({ goal, tasks, approvedN = 0 } = {}){
|
|
|
2789
2877
|
usedCompact: goal?.executionPlan?.sessionMode === 'compact',
|
|
2790
2878
|
usedClear: Boolean(goal?.sessionClosedAt),
|
|
2791
2879
|
agentModel: goal?.model,
|
|
2880
|
+
requirementCount: total,
|
|
2881
|
+
history: goalTokenHistoryLocal(sameProjectGoals, state.tasks || [], goal?.id),
|
|
2792
2882
|
}) };
|
|
2793
2883
|
}
|
|
2794
2884
|
function requirementSummaryLine(task){
|
|
@@ -2856,28 +2946,65 @@ function usageBudgetTokensLocal(usage, cacheReadWeight = 0.1){
|
|
|
2856
2946
|
const weight = Number.isFinite(Number(cacheReadWeight)) ? Math.max(0, Number(cacheReadWeight)) : 0.1;
|
|
2857
2947
|
return Math.round((u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + ((u.cache_read_input_tokens ?? 0) * weight));
|
|
2858
2948
|
}
|
|
2859
|
-
|
|
2949
|
+
// task 6 (2026-07-16): mirrors engine/lib.mjs tokenBaselinePerRequirement() —
|
|
2950
|
+
// moving average of tokens-per-requirement over up to the last 20 finished
|
|
2951
|
+
// goals (needs at least 3 to be meaningful).
|
|
2952
|
+
const TOKEN_BASELINE_MIN_HISTORY = 3;
|
|
2953
|
+
const TOKEN_BASELINE_MAX_HISTORY = 20;
|
|
2954
|
+
function tokenBaselinePerRequirementLocal(history){
|
|
2955
|
+
const samples = (history ?? [])
|
|
2956
|
+
.map((h) => {
|
|
2957
|
+
if (typeof h === 'number') return h;
|
|
2958
|
+
const tokens = Number(h?.weightedTokens);
|
|
2959
|
+
const reqN = Math.max(1, Number(h?.requirementCount) || 1);
|
|
2960
|
+
return Number.isFinite(tokens) ? tokens / reqN : null;
|
|
2961
|
+
})
|
|
2962
|
+
.filter((n) => Number.isFinite(n) && n > 0)
|
|
2963
|
+
.slice(-TOKEN_BASELINE_MAX_HISTORY);
|
|
2964
|
+
if (samples.length < TOKEN_BASELINE_MIN_HISTORY) return null;
|
|
2965
|
+
return samples.reduce((a, b) => a + b, 0) / samples.length;
|
|
2966
|
+
}
|
|
2967
|
+
// Mirrors engine/lib.mjs estimateTokenOptimization() — see the comment there
|
|
2968
|
+
// for why "saved X%" now compares against a real per-project baseline
|
|
2969
|
+
// (basis: 'baseline') instead of always reusing this goal's own cache-hit
|
|
2970
|
+
// ratio as a stand-in for "cheaper than usual" (basis: 'cache-weight',
|
|
2971
|
+
// used only until there's enough finished-goal history).
|
|
2972
|
+
function estimateTokenOptimizationLocal({ usage = null, weightedTokens = null, cacheReadWeight = 0.1, sessionMode = null, usedHandoff = false, usedCompact = false, usedClear = false, agentModel = null, requirementCount = 1, history = [] } = {}){
|
|
2860
2973
|
const u = usage ?? {};
|
|
2861
2974
|
const raw = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0);
|
|
2975
|
+
if (raw <= 0) return null;
|
|
2862
2976
|
const weighted = weightedTokens != null && Number.isFinite(Number(weightedTokens)) ? Number(weightedTokens) : usageBudgetTokensLocal(u, cacheReadWeight);
|
|
2863
|
-
const
|
|
2864
|
-
const
|
|
2977
|
+
const reqN = Math.max(1, Number(requirementCount) || 1);
|
|
2978
|
+
const perReq = weighted / reqN;
|
|
2979
|
+
const baseline = tokenBaselinePerRequirementLocal(history);
|
|
2980
|
+
let percent, basis, savedTokens;
|
|
2981
|
+
if (baseline != null) {
|
|
2982
|
+
percent = Math.max(-100, Math.min(100, Math.round(((baseline - perReq) / baseline) * 100)));
|
|
2983
|
+
basis = 'baseline';
|
|
2984
|
+
savedTokens = Math.max(0, Math.round((baseline - perReq) * reqN));
|
|
2985
|
+
} else {
|
|
2986
|
+
savedTokens = Math.max(0, raw - weighted);
|
|
2987
|
+
percent = Math.round((savedTokens / raw) * 100);
|
|
2988
|
+
basis = 'cache-weight';
|
|
2989
|
+
}
|
|
2865
2990
|
const techniques = [];
|
|
2866
|
-
|
|
2867
|
-
if (
|
|
2868
|
-
if (usedHandoff) techniques.push('
|
|
2869
|
-
if (usedCompact) techniques.push('/compact');
|
|
2870
|
-
if (usedClear) techniques.push('/clear');
|
|
2871
|
-
if (agentModel) techniques.push(
|
|
2872
|
-
if (
|
|
2873
|
-
|
|
2991
|
+
const cacheRatio = Math.round(((u.cache_read_input_tokens ?? 0) / raw) * 100);
|
|
2992
|
+
if (cacheRatio > 0) techniques.push(`${cacheRatio}% of tokens served from cache`);
|
|
2993
|
+
if (usedHandoff || sessionMode === 'cold-handoff') techniques.push('fresh handoff instead of a bloated session');
|
|
2994
|
+
if (usedCompact || sessionMode === 'compact') techniques.push('/compact before continuing');
|
|
2995
|
+
if (usedClear) techniques.push('/clear between unrelated goals');
|
|
2996
|
+
if (agentModel) techniques.push(`${agentModel} for this run`);
|
|
2997
|
+
if (basis === 'baseline' && percent > 0) techniques.push(`${Math.round(perReq / 1000)}k tokens/requirement vs your ${Math.round(baseline / 1000)}k average`);
|
|
2998
|
+
if (!techniques.length) techniques.push(`${Math.round(raw / 1000)}k tokens this run (no cache reuse yet)`);
|
|
2999
|
+
return { rawTokens: raw, weightedTokens: weighted, savedTokens, percent, basis, techniques: [...new Set(techniques)] };
|
|
2874
3000
|
}
|
|
2875
3001
|
function tokenOptimizationHtml(opt){
|
|
2876
3002
|
if (!opt) return '';
|
|
2877
|
-
const techniques = opt.techniques?.length ? opt.techniques.slice(0, 4).join(' / ') : '
|
|
3003
|
+
const techniques = opt.techniques?.length ? opt.techniques.slice(0, 4).join(' / ') : 'this run';
|
|
2878
3004
|
const pct = Number(opt.percent);
|
|
3005
|
+
const cmp = opt.basis === 'baseline' ? 'vs your recent average' : 'from prompt caching';
|
|
2879
3006
|
const text = pct > 0
|
|
2880
|
-
? `Token optimized: saved about ${pct}%
|
|
3007
|
+
? `Token optimized: saved about ${pct}% ${cmp} (${techniques})`
|
|
2881
3008
|
: `Token optimized: ${techniques}`;
|
|
2882
3009
|
return `<div class="rvtokenopt">${esc(text)}</div>`;
|
|
2883
3010
|
}
|
|
@@ -3500,6 +3627,7 @@ function renderTasks(){
|
|
|
3500
3627
|
<div class="cond">shelved — press Start to queue it</div></div>
|
|
3501
3628
|
<button type="button" class="prmini pstart" data-pstart="${g.id}">Start</button>
|
|
3502
3629
|
<button type="button" class="pdel" data-pdel="${g.id}" title="Delete">✕</button></div>`).join('');
|
|
3630
|
+
const savedQAns = captureFocusedAnswerInput($('tasklist'), 'data-qansinput');
|
|
3503
3631
|
$('tasklist').innerHTML =
|
|
3504
3632
|
sec('Attention', attention, 'attn')
|
|
3505
3633
|
+ sec(pickColumnLabel(cols, 'review', 'Review'), reviewDigest + review, 'reviewsec', reviewDefinitionFor(state.active).description, REVIEW_DEF_HINT)
|
|
@@ -3507,6 +3635,7 @@ function renderTasks(){
|
|
|
3507
3635
|
+ sec(pickColumnLabel(cols, 'todo', 'To do'), todo, 'todosec')
|
|
3508
3636
|
+ sec('Pending', pendingRows, 'dim')
|
|
3509
3637
|
+ sec(pickColumnLabel(cols, 'done', 'Done'), doneRows, 'dim');
|
|
3638
|
+
restoreFocusedAnswerInput($('tasklist'), 'data-qansinput', savedQAns);
|
|
3510
3639
|
// faint pencil beside the Review subtitle → open the review-definition editor
|
|
3511
3640
|
$('tasklist').querySelector('[data-editreviewdef]')?.addEventListener('click', (e) => { e.stopPropagation(); openWfEditor(); });
|
|
3512
3641
|
for (const el of $('tasklist').querySelectorAll('.task[data-task]')) {
|
|
@@ -3663,7 +3792,7 @@ function renderTasks(){
|
|
|
3663
3792
|
// event listeners, so all wiring above still works. Anchor comments mark each
|
|
3664
3793
|
// node's home so other layouts restore it exactly.
|
|
3665
3794
|
function ensureLayoutAnchors(){
|
|
3666
|
-
for (const id of ['tasklist', 'composerWrap', 'actpanel']) {
|
|
3795
|
+
for (const id of ['tasklist', 'composerWrap', 'actpanel', 'errbar']) {
|
|
3667
3796
|
const el = $(id);
|
|
3668
3797
|
if (el && !el._anchor) { const a = document.createComment('home:' + id); el.parentNode.insertBefore(a, el); el._anchor = a; }
|
|
3669
3798
|
}
|
|
@@ -3683,11 +3812,19 @@ function positionLayout(){
|
|
|
3683
3812
|
// is never detached mid-typing on SSE-driven renders.
|
|
3684
3813
|
const host = $('fsCompwrap'), cw = $('composerWrap');
|
|
3685
3814
|
if (host && cw && cw.parentNode !== host) host.appendChild(cw);
|
|
3815
|
+
// #errbar (showErr/showBillingBlocked/showSignInNudge) lives inside .main,
|
|
3816
|
+
// which flagship hides outright (display:none) — every transient message
|
|
3817
|
+
// silently never rendered. Adopt it beside the composer, same technique as
|
|
3818
|
+
// #composerWrap above; a flagship CSS rule floats it just above the input
|
|
3819
|
+
// (CDO判定 2026-07-16 — same fix as #gearPop's body[data-layout] escape).
|
|
3820
|
+
const eb = $('errbar');
|
|
3821
|
+
if (host && eb && eb.parentNode !== host) host.appendChild(eb);
|
|
3686
3822
|
// tasklist / actpanel go back home (they live hidden inside .main here)
|
|
3687
3823
|
for (const id of ['tasklist', 'actpanel']) { const el = $(id); if (el && el._anchor && el.parentNode !== el._anchor.parentNode) el._anchor.parentNode.insertBefore(el, el._anchor); }
|
|
3688
3824
|
return;
|
|
3689
3825
|
}
|
|
3690
3826
|
if (state.layout === 'bflow') {
|
|
3827
|
+
{ const el = $('errbar'); if (el && el._anchor && el.parentNode !== el._anchor.parentNode) el._anchor.parentNode.insertBefore(el, el._anchor); }
|
|
3691
3828
|
const bc = $('bcenter'), bt = $('brightTop'), bb = $('brightBottom');
|
|
3692
3829
|
if (!bc) return;
|
|
3693
3830
|
const tl = $('tasklist'), cw = $('composerWrap'), ap = $('actpanel');
|
|
@@ -3707,7 +3844,7 @@ function positionLayout(){
|
|
|
3707
3844
|
if (!bb.querySelector('.brbot-head')) { const h = document.createElement('div'); h.className = 'brbot-head'; h.textContent = 'Implementing'; bb.appendChild(h); }
|
|
3708
3845
|
if (ap.parentNode !== bb) bb.appendChild(ap); // right-bottom: Log → Implementing
|
|
3709
3846
|
} else {
|
|
3710
|
-
for (const id of ['tasklist', 'composerWrap', 'actpanel']) { const el = $(id); if (el && el._anchor && el.parentNode !== el._anchor.parentNode) el._anchor.parentNode.insertBefore(el, el._anchor); }
|
|
3847
|
+
for (const id of ['tasklist', 'composerWrap', 'actpanel', 'errbar']) { const el = $(id); if (el && el._anchor && el.parentNode !== el._anchor.parentNode) el._anchor.parentNode.insertBefore(el, el._anchor); }
|
|
3711
3848
|
}
|
|
3712
3849
|
}
|
|
3713
3850
|
|
|
@@ -4866,12 +5003,15 @@ function renderFsFeed(){
|
|
|
4866
5003
|
// dogfood PRs (質問カードの自由記述回答欄 / 「Needs your answer」カードを削除できるように
|
|
4867
5004
|
// — see docs/confirmed-behaviors.md): the chips-only card left no way to answer a question
|
|
4868
5005
|
// with no matching preset option, or to discard a question you don't want to answer.
|
|
5006
|
+
// Questions render at the FEED BOTTOM (composer-adjacent) — built into askHtml
|
|
5007
|
+
// so they sit AFTER the conversation turns (H18/H19 / Pattern A: 質問は末尾固定).
|
|
5008
|
+
let askHtml = '';
|
|
4869
5009
|
for (const g of state.goals.filter((g) => g.projectId === state.active && g.status === 'needsInput' && g.question)) {
|
|
4870
5010
|
const chips = (g.question.options || []).map((o) => `<button class="achip" data-fsanswer="${g.id}" data-a="${esc(o)}">${esc(o)}</button>`).join('');
|
|
4871
5011
|
// QUESTION card (H18): mono label + prompt + option chips + always-on free-text
|
|
4872
5012
|
// answer + hover trash. Data hooks (data-fsanswer / -ansinput / -anssend /
|
|
4873
5013
|
// -answerdel) are unchanged so the existing wiring below keeps working.
|
|
4874
|
-
|
|
5014
|
+
askHtml += `<div class="cc-ask"><div class="cc-ask-lb">Question</div>`
|
|
4875
5015
|
+ `<div class="cc-ask-q">${esc(g.question.text)}</div>`
|
|
4876
5016
|
+ (chips ? `<div class="qchips">${chips}</div>` : '')
|
|
4877
5017
|
+ `<div class="answ-inline"><input type="text" data-fsansinput="${g.id}" placeholder="Type your answer…" aria-label="Type your answer">`
|
|
@@ -4895,17 +5035,44 @@ function renderFsFeed(){
|
|
|
4895
5035
|
// longer flash in and wipe what the user was looking at. While work exists but
|
|
4896
5036
|
// this pass produced no lines, keep the last known feed (or a calm one-line
|
|
4897
5037
|
// status) instead of blanking to the hero.
|
|
5038
|
+
// Pattern A (H23): the conversation turns — the user's own messages, Claude
|
|
5039
|
+
// Code's replies (typed out with a caret on the streaming frontier), and
|
|
5040
|
+
// "→ Added to To Do #NN" markers — rendered from state.chatTurns so the
|
|
5041
|
+
// whole-feed re-render (SSE act events) can't drop them. Order per turn:
|
|
5042
|
+
// You → (mark, if it became a task) and, for a reply, speaker line → reply.
|
|
5043
|
+
const turns = (state.chatTurns[state.active] || []).slice(-40);
|
|
5044
|
+
let chatHtml = '';
|
|
5045
|
+
for (const turn of turns) {
|
|
5046
|
+
if (turn.role === 'you') {
|
|
5047
|
+
chatHtml += `<div class="cc-you"><span class="cy-n">You</span><div class="cy-t">${esc(turn.text)}</div></div>`;
|
|
5048
|
+
if (turn.markGoalId != null && !turn.undone) {
|
|
5049
|
+
const mg = state.goals.find((x) => x.id === turn.markGoalId);
|
|
5050
|
+
const n = mg ? goalReviewNumber({ goal: mg, tasks: state.tasks }) : turn.markGoalId;
|
|
5051
|
+
chatHtml += `<div class="cc-mark"><span class="cm-ar">→</span><span class="cm-t">Added to To Do</span><span class="cm-no">#${n}</span><button class="cm-undo" type="button" data-ccundo="${turn.markGoalId}">Undo</button></div>`;
|
|
5052
|
+
}
|
|
5053
|
+
} else { // Claude Code reply — speaker line first (You → speaker → reply), then the text
|
|
5054
|
+
const app = connectedAppMeta(turn.agent);
|
|
5055
|
+
chatHtml += `<div class="cc-who">${app.icon}<span class="cs-n">${esc(app.label)}</span></div>`;
|
|
5056
|
+
const shown = esc((turn.text || '').slice(0, turn.revealed || 0));
|
|
5057
|
+
chatHtml += turn.done
|
|
5058
|
+
? `<div class="cc-say">${shown}</div>`
|
|
5059
|
+
: `<div class="cc-say"><span id="ccTyping">${shown}</span><span class="cc-caret"></span></div>`;
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
4898
5062
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
4899
5063
|
const ptasks = state.tasks.filter((t) => t.projectId === state.active);
|
|
4900
|
-
const hasWork = pgoals.length > 0 || ptasks.length > 0;
|
|
4901
|
-
|
|
5064
|
+
const hasWork = pgoals.length > 0 || ptasks.length > 0 || turns.length > 0;
|
|
5065
|
+
// task feed (with its own speaker line) → conversation turns → question (bottom)
|
|
5066
|
+
let out = html + chatHtml + askHtml;
|
|
4902
5067
|
if (out) _fsLastFeed[state.active] = out;
|
|
4903
5068
|
else if (hasWork) out = _fsLastFeed[state.active] || `<div class="cc-c">⎿ ${esc(running.length ? 'working…' : 'no active task')}</div>`;
|
|
4904
5069
|
$('fsRoot').classList.toggle('clawdhero', !hasWork);
|
|
4905
5070
|
const sc = feed.parentNode;
|
|
4906
5071
|
const nearBottom = sc.scrollHeight - sc.clientHeight - sc.scrollTop <= 80;
|
|
5072
|
+
const savedFsAns = captureFocusedAnswerInput(feed, 'data-fsansinput');
|
|
4907
5073
|
feed.innerHTML = hasWork ? out : '';
|
|
4908
5074
|
if (nearBottom) sc.scrollTop = sc.scrollHeight;
|
|
5075
|
+
restoreFocusedAnswerInput(feed, 'data-fsansinput', savedFsAns);
|
|
4909
5076
|
window.__fsCineSync?.(); // Cinema dynamic rule: content growth may reach the band region
|
|
4910
5077
|
for (const b of feed.querySelectorAll('[data-fsretry]')) b.onclick = () => fsRetryTask(b.dataset.fsretry);
|
|
4911
5078
|
for (const b of feed.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
@@ -4923,6 +5090,19 @@ function renderFsFeed(){
|
|
|
4923
5090
|
// Delete a "needs your answer" card outright (restores 「Needs your answer」カードを削除
|
|
4924
5091
|
// できるように — a question you don't want to answer had no discard path in flagship).
|
|
4925
5092
|
for (const b of feed.querySelectorAll('[data-fsanswerdel]')) b.onclick = (e) => { e.stopPropagation(); fsTaskDelete(Number(b.dataset.fsanswerdel)); };
|
|
5093
|
+
// Pattern A (H23): Undo a "→ Added to To Do" marker — cancel the created goal
|
|
5094
|
+
// and drop the mark. A just-created (planning) goal is deletable; if it already
|
|
5095
|
+
// started the server returns non-OK (canDeleteGoal) and we say so quietly.
|
|
5096
|
+
for (const b of feed.querySelectorAll('[data-ccundo]')) b.onclick = async (e) => {
|
|
5097
|
+
e.stopPropagation();
|
|
5098
|
+
const gid = Number(b.dataset.ccundo);
|
|
5099
|
+
const r = await fetch(withKey(`/api/goals/${gid}`), { method: 'DELETE' });
|
|
5100
|
+
if (!r.ok) { showErr('Could not undo — the task may already be running'); return; }
|
|
5101
|
+
const turn = (state.chatTurns[state.active] || []).find((t) => t.markGoalId === gid);
|
|
5102
|
+
if (turn) turn.undone = true;
|
|
5103
|
+
state.goals = state.goals.filter((g) => g.id !== gid);
|
|
5104
|
+
render();
|
|
5105
|
+
};
|
|
4926
5106
|
}
|
|
4927
5107
|
async function fsAnswerQuestion(goalId, answer){
|
|
4928
5108
|
if (!answer) return;
|
|
@@ -5367,10 +5547,15 @@ function applyFsBand(){
|
|
|
5367
5547
|
const CLAWD_HERO_IMGS = '<img class="chNeu" src="data:image/png;base64,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" alt=""><img class="chLau" src="data:image/png;base64,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" alt=""></div></div>';
|
|
5368
5548
|
function clawdHeroHTML(agent){
|
|
5369
5549
|
const app = connectedAppMeta(agent);
|
|
5550
|
+
// Codex: wordmark + status only — no Clawd mascot (that's a Claude Code brand
|
|
5551
|
+
// mark, §6.5) and no glyph badge either (Masa 2026-07-16: 「codexの時は文字だけでよい」).
|
|
5370
5552
|
const stage = agent === 'codex'
|
|
5371
|
-
?
|
|
5553
|
+
? ''
|
|
5372
5554
|
: `<div class="cstage"><div class="chConf"></div><div class="cw">${CLAWD_HERO_IMGS}`;
|
|
5373
|
-
|
|
5555
|
+
// .hstat: the status line is the ONLY element renderClawdHero()/bootDisconnected()
|
|
5556
|
+
// may rewrite — never target it by bare `span` (the codex badge used to carry an
|
|
5557
|
+
// .appglyph <span>, so querySelector('span') clobbered the glyph with the status).
|
|
5558
|
+
return stage + `<b>${esc(app.label)}</b><span class="hstat">CONNECTED · WAITING FOR GOALS</span>`;
|
|
5374
5559
|
}
|
|
5375
5560
|
function spawnHeroConfetti(){
|
|
5376
5561
|
const conf = document.querySelector('#fsClawdHero .chConf'); if (!conf) return;
|
|
@@ -5391,9 +5576,9 @@ function spawnHeroConfetti(){
|
|
|
5391
5576
|
}
|
|
5392
5577
|
function renderClawdHero(){
|
|
5393
5578
|
const el = $('fsClawdHero'); if (!el) return;
|
|
5394
|
-
const prevStatus = el.querySelector('
|
|
5579
|
+
const prevStatus = el.querySelector('.hstat')?.textContent;
|
|
5395
5580
|
el.innerHTML = clawdHeroHTML(state.connectedApp);
|
|
5396
|
-
if (prevStatus) { const s = el.querySelector('
|
|
5581
|
+
if (prevStatus) { const s = el.querySelector('.hstat'); if (s) s.textContent = prevStatus; }
|
|
5397
5582
|
if (state.connectedApp !== 'codex') spawnHeroConfetti(); // Welcome P1 confetti (H17 item 4)
|
|
5398
5583
|
}
|
|
5399
5584
|
// one-time static wiring for the flagship shell (folds, lane resize, rail menu, band picker)
|
|
@@ -5440,11 +5625,10 @@ function renderClawdHero(){
|
|
|
5440
5625
|
});
|
|
5441
5626
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMobilePanels(); });
|
|
5442
5627
|
// v46 (HANDOFF-v45 §3): the menu entries deep-link into the 4-tab settings panel —
|
|
5443
|
-
//
|
|
5628
|
+
// Theme→THEME / Connect MCP→MCP / History→HISTORY / Settings→SETTINGS.
|
|
5444
5629
|
// stopPropagation: the document click-away handler otherwise closes it in the same event.
|
|
5445
5630
|
$('fsCust').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('theme'); };
|
|
5446
5631
|
$('fsConn').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('mcp'); };
|
|
5447
|
-
$('fsTodoBd').onclick = () => { $('fsMoreMenu').classList.remove('open'); $('toggle').click(); }; // To Do board stays reachable from the menu; ⌘K itself now opens the project switcher (HANDOFF-v45 §11)
|
|
5448
5632
|
$('fsHist').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('history'); };
|
|
5449
5633
|
$('fsSet').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('settings'); };
|
|
5450
5634
|
// parallel-runs switcher (§1.5-1/-7): same toggle+click-away shape as fsMoreMenu above.
|
|
@@ -5551,12 +5735,13 @@ function fsGearAlignX(){
|
|
|
5551
5735
|
function upsert(arr, item){ const i = arr.findIndex((x) => x.id === item.id); if (i >= 0) arr[i] = item; else arr.push(item); }
|
|
5552
5736
|
|
|
5553
5737
|
let errTimer = null;
|
|
5738
|
+
let authDown = false; // true while the worker-auth banner is up → keep auto-hide timers from clearing it
|
|
5554
5739
|
function showErr(msg){
|
|
5555
5740
|
const bar = $('errbar');
|
|
5556
5741
|
bar.classList.remove('billing'); // shed the paywall styling if it was last used for that
|
|
5557
5742
|
bar.textContent = msg; bar.style.display = 'block';
|
|
5558
5743
|
clearTimeout(errTimer);
|
|
5559
|
-
errTimer = setTimeout(() => { bar.style.display = 'none'; }, 6000);
|
|
5744
|
+
errTimer = setTimeout(() => { if (authDown) return; bar.style.display = 'none'; }, 6000);
|
|
5560
5745
|
}
|
|
5561
5746
|
|
|
5562
5747
|
// Minimal placeholder for the free-tier paywall pending CDO design
|
|
@@ -5583,6 +5768,55 @@ function showBillingBlocked(blocked){
|
|
|
5583
5768
|
const btn = bar.querySelector('#bxUpgrade');
|
|
5584
5769
|
if (btn) btn.onclick = () => startCheckout();
|
|
5585
5770
|
clearTimeout(errTimer);
|
|
5771
|
+
errTimer = setTimeout(() => { if (authDown) return; bar.style.display = 'none'; bar.classList.remove('billing'); }, 20000);
|
|
5772
|
+
}
|
|
5773
|
+
|
|
5774
|
+
// Worker sign-in banner. The server preflights `claude` at boot (/api/state.auth
|
|
5775
|
+
// + SSE ev:'auth'); if it isn't signed in, every worker would fail with "Not
|
|
5776
|
+
// logged in", so the queue is held server-side and this standing banner tells
|
|
5777
|
+
// the user how to fix it. Reuses #errbar + the .bx-ask card (bordered, red as a
|
|
5778
|
+
// left rule only — no new chrome), but WITHOUT an auto-hide timer since the
|
|
5779
|
+
// condition persists until the user relaunches signed-in. Chrome is English to
|
|
5780
|
+
// match the rest of the app.
|
|
5781
|
+
const AUTH_REASON_TEXT = {
|
|
5782
|
+
'logged-out': 'Not signed in to claude. Run `claude` in a terminal and /login, then relaunch the manager from that terminal — tasks are paused until then.',
|
|
5783
|
+
'bad-credentials': 'Worker sign-in failed. Check ANTHROPIC_API_KEY, or run `claude` and /login, then relaunch the manager from that terminal.',
|
|
5784
|
+
'probe-failed': 'Couldn’t verify claude sign-in. Ensure the `claude` CLI is installed and signed in, then relaunch the manager.',
|
|
5785
|
+
};
|
|
5786
|
+
function renderAuthBanner(auth){
|
|
5787
|
+
const bar = $('errbar');
|
|
5788
|
+
if (auth && auth.ok === false){
|
|
5789
|
+
authDown = true;
|
|
5790
|
+
const text = AUTH_REASON_TEXT[auth.reason] || AUTH_REASON_TEXT['logged-out'];
|
|
5791
|
+
bar.classList.add('billing'); // strips #errbar's error fill so the card stands alone
|
|
5792
|
+
bar.innerHTML = `<div class="bx-ask"><div class="bx-qt"><div class="bx-lb">Sign in</div>`
|
|
5793
|
+
+ `<div class="bx-q">${esc(text)}</div></div></div>`;
|
|
5794
|
+
bar.style.display = 'block';
|
|
5795
|
+
clearTimeout(errTimer); // this banner persists — no auto-hide
|
|
5796
|
+
} else if (authDown) {
|
|
5797
|
+
authDown = false;
|
|
5798
|
+
bar.classList.remove('billing');
|
|
5799
|
+
bar.style.display = 'none';
|
|
5800
|
+
bar.innerHTML = '';
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
// Post-logout nudge (Masa指示 2026-07-16): shown once, the next time a signed-out
|
|
5805
|
+
// send() actually lands, so it reads as "you just logged out" rather than nagging
|
|
5806
|
+
// every anonymous user (the free tier needs no account — this never blocks send).
|
|
5807
|
+
// Same #errbar "ask card" look as showBillingBlocked, Sign in instead of Upgrade.
|
|
5808
|
+
// .bx-ask.info (blue, not red — CDO判定 2026-07-16): this isn't a limit/error,
|
|
5809
|
+
// red is reserved for those (§5.6).
|
|
5810
|
+
function showSignInNudge(){
|
|
5811
|
+
const bar = $('errbar');
|
|
5812
|
+
bar.classList.add('billing');
|
|
5813
|
+
bar.innerHTML = `<div class="bx-ask info"><div class="bx-qt"><div class="bx-lb">Signed out</div>`
|
|
5814
|
+
+ `<div class="bx-q">Sign in with Google to keep your usage synced across devices.</div></div>`
|
|
5815
|
+
+ `<div class="bx-act"><button class="bx-cta" id="bxSignin">Sign in</button></div></div>`;
|
|
5816
|
+
bar.style.display = 'block';
|
|
5817
|
+
const btn = bar.querySelector('#bxSignin');
|
|
5818
|
+
if (btn) btn.onclick = () => beginGoogleSignin((err) => showErr(err));
|
|
5819
|
+
clearTimeout(errTimer);
|
|
5586
5820
|
errTimer = setTimeout(() => { bar.style.display = 'none'; bar.classList.remove('billing'); }, 20000);
|
|
5587
5821
|
}
|
|
5588
5822
|
|
|
@@ -5654,6 +5888,21 @@ function startGoogleSignin(url){
|
|
|
5654
5888
|
const w = window.open(url, 'galda-signin', 'width=520,height=680');
|
|
5655
5889
|
if (!w) location.href = url; // popup blocked → same-tab fallback
|
|
5656
5890
|
}
|
|
5891
|
+
// Shared by the Settings "Sign in with Google" button and the post-logout nudge
|
|
5892
|
+
// (showSignInNudge below) so both go through the same signin-url → popup path.
|
|
5893
|
+
async function beginGoogleSignin(onError){
|
|
5894
|
+
try {
|
|
5895
|
+
const r = await fetch(withKey('/api/signin-url'));
|
|
5896
|
+
const data = await r.json().catch(() => ({}));
|
|
5897
|
+
if (r.ok && data.url) { startGoogleSignin(data.url); return true; }
|
|
5898
|
+
onError?.(data.error || 'Sign-in is not available.');
|
|
5899
|
+
} catch { onError?.('Sign-in failed — check your connection.'); }
|
|
5900
|
+
return false;
|
|
5901
|
+
}
|
|
5902
|
+
// Set right after a confirmed Log out; consumed once by the next successful
|
|
5903
|
+
// send() while still signed out, to nudge the user back toward signing in
|
|
5904
|
+
// (Masa指示 2026-07-16) — never blocks the send itself (free tier needs no account).
|
|
5905
|
+
let recentlySignedOut = false;
|
|
5657
5906
|
async function refreshAfterSignin(){
|
|
5658
5907
|
await refresh();
|
|
5659
5908
|
if (state.billing?.licensed) signinPending = false;
|
|
@@ -5697,6 +5946,8 @@ async function refresh(){
|
|
|
5697
5946
|
for (const t of state.tasks) { if (t.todos?.length) state.todos[t.id] = t.todos; }
|
|
5698
5947
|
state.connectCommand = s.connectCommand ?? ''; state.models = s.models ?? ['sonnet']; state.agentModels = s.agentModels ?? null; state.agentEfforts = s.agentEfforts ?? null; state.agents = s.agents ?? ['claude-code', 'codex']; state.uiVersion = s.uiVersion;
|
|
5699
5948
|
state.billing = s.billing ?? { licensed: false, email: null };
|
|
5949
|
+
renderAppSelect(); // picker follows the server's installed-agent set; may correct a stale connectedApp
|
|
5950
|
+
renderAuthBanner(s.auth); // show/clear the worker sign-in banner from the boot preflight
|
|
5700
5951
|
setAgentModel(state.connectedApp);
|
|
5701
5952
|
setAgentEffort(state.connectedApp);
|
|
5702
5953
|
renderModelSelect();
|
|
@@ -5753,6 +6004,7 @@ function connectSSE(){
|
|
|
5753
6004
|
if (!state.projects.some((p) => p.id === state.active)) state.active = state.projects[0]?.id;
|
|
5754
6005
|
}
|
|
5755
6006
|
else if (m.ev === 'ping') { checkUiVersion(m.uiVersion); return; }
|
|
6007
|
+
else if (m.ev === 'auth') { renderAuthBanner(m.auth); return; } // worker sign-in preflight result
|
|
5756
6008
|
scheduleRender();
|
|
5757
6009
|
};
|
|
5758
6010
|
es.onerror = () => {
|
|
@@ -5782,7 +6034,7 @@ function bootDisconnected(info){
|
|
|
5782
6034
|
state.active = '__onboard';
|
|
5783
6035
|
state.billing = { licensed: false, email };
|
|
5784
6036
|
try { render(); } catch (e) { console.warn('disconnected render', e); }
|
|
5785
|
-
const heroSpan = document.querySelector('#fsClawdHero
|
|
6037
|
+
const heroSpan = document.querySelector('#fsClawdHero .hstat');
|
|
5786
6038
|
if (heroSpan) heroSpan.textContent = 'CONNECT YOUR MACHINE TO BEGIN';
|
|
5787
6039
|
|
|
5788
6040
|
const style = document.createElement('style');
|
|
@@ -5800,10 +6052,13 @@ function bootDisconnected(info){
|
|
|
5800
6052
|
#connectGate .cg-cmd button{font:500 11px/1 var(--mono,monospace);color:#a3a097;background:transparent;
|
|
5801
6053
|
border:1px solid rgba(255,255,255,.18);border-radius:7px;padding:6px 9px;cursor:pointer}
|
|
5802
6054
|
#connectGate .cg-cmd button:hover{color:#fff}
|
|
6055
|
+
#connectGate .cg-hint{font-size:12px;line-height:1.6;color:var(--ink3,#6b6862);margin:0 0 16px}
|
|
6056
|
+
#connectGate .cg-hint a{color:var(--blue,#8fb0ff);text-decoration:none}
|
|
6057
|
+
#connectGate .cg-hint a:hover{text-decoration:underline}
|
|
5803
6058
|
#connectGate .cg-who{font-size:12px;color:var(--ink3,#6b6862);margin:0 0 4px}
|
|
5804
6059
|
#connectGate .cg-who b{color:var(--ink2,#a3a097)}
|
|
5805
6060
|
#connectGate .cg-switch{font-size:12px;color:var(--ink3,#6b6862);margin:14px 0 0}
|
|
5806
|
-
#connectGate .cg-switch a{color
|
|
6061
|
+
#connectGate .cg-switch a{color:var(--blue,#8fb0ff);text-decoration:none}
|
|
5807
6062
|
#connectGate .cg-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border:0;background:transparent;color:var(--ink3,#6b6862);font-size:20px;line-height:1;cursor:pointer}
|
|
5808
6063
|
#connectGate .cg-wait{display:inline-flex;align-items:center;gap:7px;font:500 10.5px/1 var(--mono,monospace);letter-spacing:.08em;color:var(--ink3,#6b6862);margin-top:16px}
|
|
5809
6064
|
#connectGate .cg-wait i{width:6px;height:6px;border-radius:50%;background:var(--green,#3DDC97);animation:cgpulse 1.8s ease-in-out infinite}
|
|
@@ -5817,6 +6072,13 @@ function bootDisconnected(info){
|
|
|
5817
6072
|
+ '<h2>Connect this device to start</h2>'
|
|
5818
6073
|
+ '<p>Galda runs on your own machine. Run this where you use Claude Code or Codex — this board comes alive the moment it connects, no refresh needed:</p>'
|
|
5819
6074
|
+ '<div class="cg-cmd"><code>npx @galda/cli</code><button class="cg-copy">Copy</button></div>'
|
|
6075
|
+
// One command connects the machine, not an agent: the CLI probes PATH and hands
|
|
6076
|
+
// the whole detected set to the server. So we never split this per agent — we
|
|
6077
|
+
// only point people without either one at the official installers.
|
|
6078
|
+
+ '<p class="cg-hint">Don\'t have either yet? Install '
|
|
6079
|
+
+ '<a href="https://claude.com/claude-code" target="_blank" rel="noreferrer">Claude Code</a> or '
|
|
6080
|
+
+ '<a href="https://openai.com/codex/" target="_blank" rel="noreferrer">Codex</a>, then run the same command '
|
|
6081
|
+
+ '— Galda uses whichever agent you have.</p>'
|
|
5820
6082
|
+ '<p class="cg-who">Signed in as <b></b></p>'
|
|
5821
6083
|
+ '<p class="cg-switch">Already running Galda under a different account? Run <code>npx @galda/cli --signin</code> and pick this one. Or <a href="/logout">switch this browser →</a></p>'
|
|
5822
6084
|
+ '<div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div>';
|
|
@@ -5853,7 +6115,17 @@ function bootDisconnected(info){
|
|
|
5853
6115
|
const es = new EventSource('/presence');
|
|
5854
6116
|
es.addEventListener('presence', (e) => {
|
|
5855
6117
|
let d; try { d = JSON.parse(e.data); } catch { return; }
|
|
5856
|
-
if (d.state === 'connected') {
|
|
6118
|
+
if (d.state === 'connected') {
|
|
6119
|
+
// Confirm the engine is REALLY reachable before reloading. A stale/spurious
|
|
6120
|
+
// 'connected' presence otherwise reloads us into a white flash that lands
|
|
6121
|
+
// right back on this same connect screen — the welcome animation plays, then
|
|
6122
|
+
// 一瞬真っ白 (Masa 2026-07-16). Reuse the proven 6d71c32 contract: the relay
|
|
6123
|
+
// 503s every engine-backed path while offline and 200s once proxied, so
|
|
6124
|
+
// status !== 503 flips exactly at a genuine connect.
|
|
6125
|
+
fetch('/api/state', { cache: 'no-store', credentials: 'same-origin' })
|
|
6126
|
+
.then((r) => { if (r.status !== 503) { es.close(); location.reload(); } })
|
|
6127
|
+
.catch(() => {}); // engine unreachable → not connected yet; keep waiting
|
|
6128
|
+
}
|
|
5857
6129
|
else if (cgWait) cgWait.innerHTML = '<i></i>WAITING FOR THIS DEVICE';
|
|
5858
6130
|
});
|
|
5859
6131
|
window.addEventListener('beforeunload', () => { try { es.close(); } catch {} });
|
|
@@ -5917,9 +6189,38 @@ async function postOutboxItem(item){
|
|
|
5917
6189
|
}
|
|
5918
6190
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
5919
6191
|
const goal = await r.json();
|
|
6192
|
+
if (goal.kind === 'chat') {
|
|
6193
|
+
// Conversation-first (SLICE 2 + Pattern A H23): a chat/help/settings turn,
|
|
6194
|
+
// not a To-Do. Retire the optimistic outbox item — a chat turn is ephemeral
|
|
6195
|
+
// by design, so never-lose (which protects real tasks/goals) is untouched.
|
|
6196
|
+
// In flagship the reply is a turn in the center feed (typed out with a
|
|
6197
|
+
// caret); the board layout has no feed, so it falls back to the quiet
|
|
6198
|
+
// .peraanswer bar. Never land()/upsert(): a kind:'chat' response has no id.
|
|
6199
|
+
outbox.remove(item.oid);
|
|
6200
|
+
if (state.layout === 'flagship') {
|
|
6201
|
+
const turn = { role: 'clawd', text: String(goal.reply || ''), revealed: 0, done: false, agent: item.agent };
|
|
6202
|
+
(state.chatTurns[item.projectId] ||= []).push(turn);
|
|
6203
|
+
renderFsFeed();
|
|
6204
|
+
typeChatTurn(turn);
|
|
6205
|
+
} else {
|
|
6206
|
+
showChatReply(goal.reply);
|
|
6207
|
+
}
|
|
6208
|
+
render();
|
|
6209
|
+
return; // finally still runs posting.delete(); skip land()/upsert()
|
|
6210
|
+
}
|
|
5920
6211
|
goal.imageUrls = item.imageUrls;
|
|
5921
6212
|
outbox.land(item.oid, goal.id);
|
|
5922
6213
|
upsert(state.goals, goal);
|
|
6214
|
+
if (recentlySignedOut && !state.billing?.licensed) { recentlySignedOut = false; showSignInNudge(); }
|
|
6215
|
+
// Pattern A (H23): this send became a real To-Do — mark the most recent
|
|
6216
|
+
// unmarked user turn "→ Added to To Do #NN" in the feed. Skip a needsInput
|
|
6217
|
+
// goal (it's still asking; shown as the question card at the feed bottom).
|
|
6218
|
+
if (goal.status !== 'needsInput') {
|
|
6219
|
+
const turns = state.chatTurns[item.projectId] || [];
|
|
6220
|
+
for (let k = turns.length - 1; k >= 0; k--) {
|
|
6221
|
+
if (turns[k].role === 'you' && turns[k].markGoalId == null) { turns[k].markGoalId = goal.id; break; }
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
5923
6224
|
} catch {
|
|
5924
6225
|
outbox.mark(item.oid, 'unsent');
|
|
5925
6226
|
} finally {
|
|
@@ -5928,6 +6229,58 @@ async function postOutboxItem(item){
|
|
|
5928
6229
|
render();
|
|
5929
6230
|
}
|
|
5930
6231
|
|
|
6232
|
+
// SLICE 2 / Pattern A (H23): the conversational reply TYPES out — "read along as
|
|
6233
|
+
// it types" (ChatGPT / Claude Code). The claude CLI returns the reply as one
|
|
6234
|
+
// block, NOT token deltas (verified), so this is a client-side reveal of the full
|
|
6235
|
+
// text, not network streaming. Reveal progress lives on the turn MODEL
|
|
6236
|
+
// (turn.revealed), so the whole-feed re-render (SSE `act` events rebuild #fsFeed)
|
|
6237
|
+
// can't reset it; between re-renders we patch just the #ccTyping text node so we
|
|
6238
|
+
// don't rebuild the feed 60×/sec. Duration-normalized (~2s) so short/long both
|
|
6239
|
+
// feel right.
|
|
6240
|
+
let _chatTyper = null;
|
|
6241
|
+
function stopChatTyper(){ if (_chatTyper) { clearInterval(_chatTyper); _chatTyper = null; } }
|
|
6242
|
+
// Flagship: reveal a reply turn in the center feed. The caret sits next to
|
|
6243
|
+
// #ccTyping (a separate span) so patching the text node never wipes it.
|
|
6244
|
+
function typeChatTurn(turn){
|
|
6245
|
+
stopChatTyper();
|
|
6246
|
+
const full = turn.text || '';
|
|
6247
|
+
if (!full) { turn.done = true; return; }
|
|
6248
|
+
const step = Math.max(1, Math.round(full.length / 120)); // ~120 frames ≈ 2s
|
|
6249
|
+
_chatTyper = setInterval(() => {
|
|
6250
|
+
turn.revealed = Math.min(full.length, turn.revealed + step);
|
|
6251
|
+
const node = $('ccTyping');
|
|
6252
|
+
if (node) node.textContent = full.slice(0, turn.revealed);
|
|
6253
|
+
if (turn.revealed >= full.length) { turn.done = true; stopChatTyper(); renderFsFeed(); }
|
|
6254
|
+
}, 16);
|
|
6255
|
+
}
|
|
6256
|
+
// A new send starts: finish any in-flight reply cleanly (complete the model turn +
|
|
6257
|
+
// drop the caret, hide the board bar) so a half-typed reply never lingers.
|
|
6258
|
+
function finalizeChatTyping(){
|
|
6259
|
+
stopChatTyper();
|
|
6260
|
+
const turns = state.chatTurns[state.active] || [];
|
|
6261
|
+
for (let k = turns.length - 1; k >= 0; k--) {
|
|
6262
|
+
if (turns[k].role === 'clawd' && !turns[k].done) { turns[k].revealed = (turns[k].text || '').length; turns[k].done = true; break; }
|
|
6263
|
+
}
|
|
6264
|
+
const el = $('chatReply'); if (el) { el.hidden = true; el.textContent = ''; }
|
|
6265
|
+
}
|
|
6266
|
+
// Board layout (no center feed) keeps the quiet .peraanswer bar; same reveal.
|
|
6267
|
+
function showChatReply(text){
|
|
6268
|
+
const el = $('chatReply'); if (!el) return;
|
|
6269
|
+
stopChatTyper();
|
|
6270
|
+
const full = (text || '').trim();
|
|
6271
|
+
if (!full) { el.hidden = true; el.textContent = ''; return; }
|
|
6272
|
+
el.className = 'peraanswer'; el.textContent = ''; el.hidden = false;
|
|
6273
|
+
let i = 0;
|
|
6274
|
+
const step = Math.max(1, Math.round(full.length / 120));
|
|
6275
|
+
_chatTyper = setInterval(() => {
|
|
6276
|
+
i = Math.min(full.length, i + step);
|
|
6277
|
+
el.textContent = full.slice(0, i);
|
|
6278
|
+
el.scrollTop = el.scrollHeight;
|
|
6279
|
+
if (i >= full.length) stopChatTyper();
|
|
6280
|
+
}, 16);
|
|
6281
|
+
}
|
|
6282
|
+
function clearChatReply(){ finalizeChatTyping(); }
|
|
6283
|
+
|
|
5931
6284
|
async function flushOutbox(){
|
|
5932
6285
|
// Only (re)send items that still need it — never re-POST 'landed' memos that
|
|
5933
6286
|
// are merely waiting for reconcile() (that was the double-send path).
|
|
@@ -5998,12 +6351,17 @@ async function send(){
|
|
|
5998
6351
|
status: 'sending', ts: Date.now(),
|
|
5999
6352
|
};
|
|
6000
6353
|
outbox.add(item); // saved locally BEFORE any network call
|
|
6354
|
+
// Pattern A (H23): the user's own turn enters the center feed immediately, so
|
|
6355
|
+
// the composer reads like a conversation. If this send becomes a To-Do, a
|
|
6356
|
+
// .cc-mark is attached to this turn later; if it's chat, a reply turn follows.
|
|
6357
|
+
(state.chatTurns[state.active] ||= []).push({ role: 'you', text, ts: Date.now() });
|
|
6001
6358
|
state.attach = [];
|
|
6002
6359
|
// mode/skill/later/review are per-message → clear after send (model stays sticky).
|
|
6003
6360
|
state.msg = { mode: null, model: null, effort: null, skill: null, later: false, review: false };
|
|
6004
6361
|
renderMsgHint();
|
|
6005
6362
|
|
|
6006
6363
|
$('input').value = ''; $('input').style.height = 'auto';
|
|
6364
|
+
clearChatReply(); // a new turn starts — drop any prior conversational reply
|
|
6007
6365
|
closePalette();
|
|
6008
6366
|
localStorage.removeItem('draft');
|
|
6009
6367
|
renderAttach(); render();
|
|
@@ -6193,6 +6551,8 @@ function saRows(){
|
|
|
6193
6551
|
usedCompact: g?.executionPlan?.sessionMode === 'compact',
|
|
6194
6552
|
usedClear: Boolean(g?.sessionClosedAt),
|
|
6195
6553
|
agentModel: g?.model,
|
|
6554
|
+
requirementCount: ts.filter((t) => t.goalId === g?.id && !t.reply).length,
|
|
6555
|
+
history: goalTokenHistoryLocal(gs, ts, g?.id),
|
|
6196
6556
|
}),
|
|
6197
6557
|
};
|
|
6198
6558
|
});
|
|
@@ -7504,11 +7864,11 @@ function gearSetHTML(){
|
|
|
7504
7864
|
<button class="optchip" data-gsignin="1">Sign in with Google</button></div>
|
|
7505
7865
|
<div class="srow dis" id="licKeyMsg" style="display:none"></div>`;
|
|
7506
7866
|
} else if (bill.plan === 'paid') {
|
|
7507
|
-
billingRow = `<div class="srow"><div class="sl">Account<span class="sd">Signed in with Google</span></div><
|
|
7867
|
+
billingRow = `<div class="srow"><div class="sl">Account<span class="sd">Signed in with Google · ${esc(bill.email)}</span></div><button class="optchip" data-gsignout="1">Log out</button></div>
|
|
7508
7868
|
<div class="srow"><div class="sl">Plan</div><span class="bx-pdot"><i></i>Paid</span></div>
|
|
7509
7869
|
<div class="srow"><div class="sl">Billing<span class="sd">manage or cancel in Stripe</span></div><button class="bx-cchip" data-gportal="1">Manage</button></div>`;
|
|
7510
7870
|
} else {
|
|
7511
|
-
billingRow = `<div class="srow"><div class="sl">Account<span class="sd">Signed in with Google</span></div><
|
|
7871
|
+
billingRow = `<div class="srow"><div class="sl">Account<span class="sd">Signed in with Google · ${esc(bill.email)}</span></div><button class="optchip" data-gsignout="1">Log out</button></div>
|
|
7512
7872
|
<div class="srow"><div class="sl">Plan</div><span class="bx-sval">Free</span></div>
|
|
7513
7873
|
<div class="srow"><div class="sl">To Dos<span class="sd">pending, this install</span></div><span class="bx-sval bx-num">${u.pending ?? 0} / ${lim.pending ?? 5}</span></div>
|
|
7514
7874
|
<div class="srow"><div class="sl">Lifetime</div><span class="bx-sval bx-num">${u.cumulative ?? 0} / ${lim.cumulative ?? 19}</span></div>
|
|
@@ -7558,18 +7918,20 @@ function buildGearPop(){
|
|
|
7558
7918
|
if (signBtn) signBtn.onclick = async () => {
|
|
7559
7919
|
const msg = pop.querySelector('#licKeyMsg');
|
|
7560
7920
|
signBtn.disabled = true;
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
const data = await r.json().catch(() => ({}));
|
|
7564
|
-
if (r.ok && data.url) { startGoogleSignin(data.url); }
|
|
7565
|
-
else { msg.style.display = ''; msg.textContent = data.error || 'Sign-in is not available.'; }
|
|
7566
|
-
} catch { msg.style.display = ''; msg.textContent = 'Sign-in failed — check your connection.'; }
|
|
7567
|
-
finally { signBtn.disabled = false; }
|
|
7921
|
+
await beginGoogleSignin((err) => { msg.style.display = ''; msg.textContent = err; });
|
|
7922
|
+
signBtn.disabled = false;
|
|
7568
7923
|
};
|
|
7569
7924
|
const upBtn = pop.querySelector('[data-gupgrade]');
|
|
7570
7925
|
if (upBtn) upBtn.onclick = () => startCheckout();
|
|
7571
7926
|
const portalBtn = pop.querySelector('[data-gportal]');
|
|
7572
7927
|
if (portalBtn) portalBtn.onclick = () => openBillingPortal();
|
|
7928
|
+
const signOutBtn = pop.querySelector('[data-gsignout]');
|
|
7929
|
+
if (signOutBtn) signOutBtn.onclick = async () => {
|
|
7930
|
+
if (!(await confirmDelete({ title: 'Log out of Google?', body: 'You can sign back in anytime.', confirmLabel: 'Log out', confirmDanger: false }))) return;
|
|
7931
|
+
signOutBtn.disabled = true;
|
|
7932
|
+
try { await fetch(withKey('/api/signout'), { method: 'POST' }); recentlySignedOut = true; }
|
|
7933
|
+
finally { await refresh(); if ($('gearPop')) buildGearPop(); }
|
|
7934
|
+
};
|
|
7573
7935
|
return;
|
|
7574
7936
|
}
|
|
7575
7937
|
if (custTab === 'mcp') {
|
|
@@ -7753,11 +8115,14 @@ $('gearBtn').onclick = (e) => { e.stopPropagation(); const p = $('gearPop'); if
|
|
|
7753
8115
|
// outside-tap = Cancel (v45 §3). e.composedPath() is frozen at dispatch, so a swatch that
|
|
7754
8116
|
// buildGearPop() re-rendered (detached) mid-click still reads as INSIDE the pop —
|
|
7755
8117
|
// selections never close the panel. Known trap (v45 §3): #fsMoreMenu clicks are excluded
|
|
7756
|
-
// so a menu item is never swallowed as an outside tap.
|
|
8118
|
+
// so a menu item is never swallowed as an outside tap. #deleteConfirmOverlay is excluded
|
|
8119
|
+
// too (Masa指示 2026-07-16): it's a separate top-level overlay a settings row can open
|
|
8120
|
+
// (e.g. Log out) — without this, confirming/cancelling it read as an outside tap and
|
|
8121
|
+
// closed the settings panel underneath it as a surprise side effect.
|
|
7757
8122
|
document.addEventListener('click', (e) => {
|
|
7758
8123
|
const p = $('gearPop'); if (!p.classList.contains('open')) return;
|
|
7759
8124
|
if (e.composedPath().includes(p)) return;
|
|
7760
|
-
if (e.target.closest('.gearwrap') || e.target.closest('#fsCust') || e.target.closest('#fsMoreMenu') || e.target.closest('#fsMoreBtn')) return;
|
|
8125
|
+
if (e.target.closest('.gearwrap') || e.target.closest('#fsCust') || e.target.closest('#fsMoreMenu') || e.target.closest('#fsMoreBtn') || e.target.closest('#deleteConfirmOverlay')) return;
|
|
7761
8126
|
custCancel();
|
|
7762
8127
|
});
|
|
7763
8128
|
// Banff BACKDROP free upload (v3 §6.5): file → dataURL → #themebg. Persists via
|