agentgui 1.0.959 → 1.0.961

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.
@@ -363,8 +363,18 @@ export async function* streamChat(base, { model, messages, signal, agentId, resu
363
363
  else if (block?.type === 'result') push({ type: 'result', block });
364
364
  } else if (ev.type === 'streaming_complete') {
365
365
  finish();
366
+ } else if (ev.type === 'streaming_cancelled') {
367
+ push({ type: 'cancelled' });
368
+ finish();
366
369
  } else if (ev.type === 'streaming_error') {
367
- errored = ev.error || 'streaming error';
370
+ // A remote stop (another tab / dashboard stop-all) is not a failure: the
371
+ // server marks it cancelled; surface it as a distinct event so the app
372
+ // can label the turn 'stopped' instead of a normal finish or error red.
373
+ if (ev.cancelled || ev.error === 'cancelled') {
374
+ push({ type: 'cancelled' });
375
+ } else {
376
+ errored = ev.error || 'streaming error';
377
+ }
368
378
  finish();
369
379
  }
370
380
  });
@@ -381,6 +391,20 @@ export async function* streamChat(base, { model, messages, signal, agentId, resu
381
391
  if (s === 'open') {
382
392
  // Reconnected in time - the open handler already re-subscribed us.
383
393
  if (graceTimer) { clearTimeout(graceTimer); graceTimer = null; }
394
+ // streaming_complete is fire-and-forget with no replay: if the turn
395
+ // finished while the socket was down, no terminal frame will ever
396
+ // arrive. Verify the session is still alive server-side; if it is
397
+ // gone, settle the iterator with a plain incomplete marker instead of
398
+ // hanging busy forever.
399
+ wsCall(base, 'chat.active', {}).then((d) => {
400
+ if (done) return;
401
+ const sessions = (d && d.sessions) || [];
402
+ const alive = Array.isArray(sessions) && sessions.some(x => x && x.sessionId === sessionId);
403
+ if (!alive) {
404
+ errored = errored || 'connection dropped mid-turn - the response may be incomplete; events were not replayed';
405
+ finish();
406
+ }
407
+ }).catch(() => {});
384
408
  return;
385
409
  }
386
410
  if ((s === 'closed' || s === 'error') && !graceTimer) {
@@ -442,7 +442,7 @@
442
442
  -webkit-font-smoothing: antialiased;
443
443
  text-rendering: optimizeLegibility;
444
444
  }
445
- .ds-247420 ::selection { background: var(--mascot); color: var(--ink); }
445
+ .ds-247420 ::selection { background: color-mix(in oklab, var(--accent) 24%, var(--bg-2)); color: var(--fg); }
446
446
 
447
447
  /* Every root has a CQ container so fluid type can resolve to a meaningful inline-size. */
448
448
  .ds-247420 .app, .ds-247420[class*="kit-"], .ds-247420 .ds-stage {
@@ -555,6 +555,9 @@
555
555
  ============================================================ */
556
556
  .ds-247420 .app {
557
557
  display: flex; flex-direction: column;
558
+ /* Offset parent for the sidebar drawer/scrim/toggle, which are absolute so
559
+ they overlay THIS shell (a thebird WM window) rather than the viewport. */
560
+ position: relative;
558
561
  min-height: 100vh;
559
562
  /* Definite height so the flex column resolves height:100% for descendants
560
563
  (e.g. a full-height chat) at every breakpoint, not just where a desktop
@@ -932,41 +935,61 @@
932
935
  grid-template-columns: minmax(80px, 12ch) minmax(0, 1fr) auto;
933
936
  gap: var(--space-3);
934
937
  align-items: baseline;
935
- padding: 18px 22px;
938
+ padding: 16px 22px;
936
939
  background: var(--bg);
937
940
  border-radius: var(--r-2);
938
941
  color: var(--fg);
939
942
  position: relative;
940
- transition: background var(--dur-snap) var(--ease), padding var(--dur-base) var(--ease);
943
+ /* Only background + the leading rail animate; padding/size never change on
944
+ interaction, so they are deliberately absent (a padding transition here was
945
+ dead and misleading). */
946
+ transition: background var(--dur-snap) var(--ease);
941
947
  }
942
- .ds-247420 .row + .row { margin-top: 4px; }
948
+ .ds-247420 .row + .row { margin-top: 2px; }
943
949
  .ds-247420 .row::before {
944
950
  content: '';
945
- position: absolute; left: 6px; top: 22%; bottom: 22%; width: 3px;
946
- background: transparent; border-radius: 3px;
947
- transition: background var(--dur-snap) var(--ease);
951
+ position: absolute; left: 7px; top: 0; bottom: 0; width: 3px;
952
+ margin: auto 0;
953
+ height: 0; /* collapsed by default; reveals from centre */
954
+ background: var(--rule-strong);
955
+ border-radius: var(--r-pill);
956
+ opacity: 0;
957
+ transition: height var(--dur-base) var(--ease), opacity var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease);
948
958
  }
949
959
  /* A clickable row (onClick -> role=button, or a link row) is interactive, so it
950
960
  carries a pointer cursor; without this the click affordance is invisible. */
951
961
  .ds-247420 .row[role="button"], .ds-247420 a.row { cursor: pointer; }
952
- .ds-247420 .row:hover { background: var(--bg-2); }
953
- .ds-247420 .row:hover::before { background: var(--rule-strong); }
962
+ /* Hover reveals the rail (grows from centre) only on interactive rows — a static
963
+ row should not signal an affordance it does not have. */
964
+ .ds-247420 .row[role="button"]:hover, .ds-247420 a.row:hover { background: var(--bg-2); }
965
+ .ds-247420 .row[role="button"]:hover::before, .ds-247420 a.row:hover::before { height: 56%; opacity: 1; }
954
966
  .ds-247420 .row.active { background: color-mix(in oklab, var(--accent) 10%, var(--bg-2)); }
955
- .ds-247420 .row.active::before { background: var(--accent); }
967
+ .ds-247420 .row.active::before { height: 64%; opacity: 1; background: var(--accent); }
968
+ .ds-247420 .row.active .title { color: var(--accent); }
956
969
  .ds-247420 .row.row-state-disabled { opacity: 0.5; pointer-events: none; }
957
970
  .ds-247420 .row.row-state-error { background: color-mix(in oklab, var(--flame) 10%, var(--bg-2)); }
958
- .ds-247420 .row.row-state-error::before { background: var(--flame); }
971
+ .ds-247420 .row.row-state-error::before { height: 64%; opacity: 1; background: var(--flame); }
959
972
  /* rail tones — a persistent status colour on the leading rail (the ::before
960
- bar). Active/hover/error still win via their own rules below source order. */
973
+ bar). Always shown (a status indicator, not a hover affordance). Active/error
974
+ still win via their own rules below in source order. */
975
+ .ds-247420 .row.rail-green::before,
976
+ .ds-247420 .row.rail-purple::before,
977
+ .ds-247420 .row.rail-flame::before { height: 56%; opacity: 1; }
961
978
  .ds-247420 .row.rail-green::before { background: var(--accent); }
962
979
  .ds-247420 .row.rail-purple::before { background: var(--purple-2, #7F18A4); }
963
980
  .ds-247420 .row.rail-flame::before { background: var(--flame); }
964
981
  .ds-247420 .row-grid { /* explicit grid-template-columns set inline */ }
982
+ /* A row with no leading code/index drops the empty first column so the title
983
+ takes the full width instead of wrapping in the narrow code gutter. */
984
+ .ds-247420 .row.row-nocode { grid-template-columns: minmax(0, 1fr) auto; }
965
985
 
966
- .ds-247420 .row .code { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); }
967
- .ds-247420 .row .title { font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-lg); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
986
+ .ds-247420 .row .code { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
987
+ .ds-247420 .row .title { font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-lg); line-height: var(--lh-snug, 1.3); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
988
+ .ds-247420 .row .title .sub { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
968
989
  .ds-247420 .row .sub { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
969
- .ds-247420 .row .meta { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); text-align: right; }
990
+ .ds-247420 .row .meta { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; align-self: center; }
991
+ /* WorksList meta pairs a label with a disclosure chevron, inline-aligned. */
992
+ .ds-247420 .ds-works-meta { display: inline-flex; align-items: center; gap: .4em; }
970
993
 
971
994
  .ds-247420 a.row { text-decoration: none; }
972
995
 
@@ -1700,6 +1723,19 @@
1700
1723
  .ds-247420 .ds-upload-item.done .ds-upload-pct { color: var(--success); }
1701
1724
  .ds-247420 .ds-upload-item.error .ds-upload-fill { background: var(--warn); }
1702
1725
  .ds-247420 .ds-upload-item.error .ds-upload-pct { color: var(--warn); }
1726
+ /* Per-row upload recovery actions (replace / dismiss) — error rows are not dead ends. */
1727
+ .ds-247420 .ds-upload-item { grid-template-columns: minmax(0, 1fr) 120px 44px auto; }
1728
+ .ds-247420 .ds-upload-actions { display: inline-flex; gap: var(--space-1); justify-content: flex-end; }
1729
+ .ds-247420 .ds-upload-act {
1730
+ padding: 2px 10px; min-height: 24px; cursor: pointer;
1731
+ background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
1732
+ color: var(--fg-2); font-family: var(--ff-body); font-size: var(--fs-micro);
1733
+ }
1734
+ .ds-247420 .ds-upload-act:hover { background: var(--bg-3); color: var(--fg); }
1735
+ .ds-247420 .ds-upload-act:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
1736
+ @media (hover: none), (pointer: coarse) {
1737
+ .ds-247420 .ds-upload-act { min-height: 44px; min-width: 44px; }
1738
+ }
1703
1739
 
1704
1740
  /* Empty state */
1705
1741
  .ds-247420 .ds-file-empty {
@@ -1757,6 +1793,14 @@
1757
1793
  font-family: inherit; font-size: var(--fs-sm);
1758
1794
  }
1759
1795
  .ds-247420 .ds-modal-input:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
1796
+ /* In-body modal error (role=alert): mutation failures (409/403) surface INSIDE
1797
+ the dialog, not behind the fixed overlay. */
1798
+ .ds-247420 .ds-modal-error {
1799
+ margin: var(--space-2) 0 0; padding: 8px 10px;
1800
+ border: var(--bw-hair) solid var(--flame); border-radius: var(--r-2);
1801
+ background: color-mix(in oklab, var(--flame) 12%, var(--bg));
1802
+ color: var(--flame); font-size: var(--fs-xs);
1803
+ }
1760
1804
  .ds-247420 .btn-primary.danger {
1761
1805
  background: var(--warn);
1762
1806
  border-color: var(--warn);
@@ -1886,10 +1930,12 @@
1886
1930
  .ds-247420 .ds-file-row:hover .ds-file-check,
1887
1931
  .ds-247420 .ds-file-row:focus-within .ds-file-check,
1888
1932
  .ds-247420 .ds-file-check.on { opacity: 1; }
1889
- /* Touch / coarse-pointer devices have no hover — keep checkboxes visible so
1890
- multi-select is reachable without a pointer. */
1933
+ /* Touch / coarse-pointer devices have no hover — keep checkboxes AND the
1934
+ per-row action buttons (rename/delete/download) visible so the file manager
1935
+ is fully reachable without a pointer (e.g. an iPad in landscape). */
1891
1936
  @media (hover: none), (pointer: coarse) {
1892
1937
  .ds-247420 .ds-file-check { opacity: 1; }
1938
+ .ds-247420 .ds-file-actions { opacity: 1; }
1893
1939
  }
1894
1940
  .ds-247420 .ds-file-check.on { background: var(--accent); border-color: var(--accent); }
1895
1941
  .ds-247420 .ds-file-row.selected {
@@ -2295,36 +2341,43 @@
2295
2341
  .ds-247420 .chat-msg:nth-child(n+4) { animation-delay: 0.15s; }
2296
2342
  }
2297
2343
 
2344
+ /* Calm composer (claude.ai/code): ONE bordered rounded-rect shell holding the
2345
+ context line, a borderless textarea, the hint, and an inline send button -
2346
+ not a stadium pill + a floating round send. The shell carries the border and
2347
+ the focus ring; the textarea fills it transparently. flex-wrap lets the
2348
+ context line and hint occupy their own rows above/below the input row. */
2298
2349
  .ds-247420 .chat-composer {
2299
- display: flex; align-items: flex-end; gap: 8px;
2300
- padding: var(--space-3) 0 0;
2301
- border-top: 1px solid color-mix(in oklab, var(--fg) 8%, transparent);
2350
+ display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--space-2);
2351
+ padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
2352
+ background: var(--bg);
2353
+ border: var(--bw-hair) solid var(--rule);
2354
+ border-radius: var(--r-2);
2302
2355
  flex-shrink: 0;
2356
+ transition: border-color var(--dur-snap) var(--ease), box-shadow var(--dur-snap) var(--ease);
2357
+ }
2358
+ .ds-247420 .chat-composer:focus-within {
2359
+ border-color: var(--accent);
2360
+ box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 18%, transparent);
2303
2361
  }
2304
2362
  .ds-247420 .chat-composer textarea {
2305
- flex: 1; padding: 12px 16px;
2306
- background: var(--bg-2); color: var(--fg);
2307
- border: 1px solid transparent; border-radius: var(--r-pill);
2363
+ flex: 1; min-width: 0; padding: 8px 4px;
2364
+ background: none; color: var(--fg);
2365
+ border: none; border-radius: 0;
2308
2366
  font-family: inherit; font-size: var(--fs-sm);
2309
- line-height: 1.4; resize: none;
2310
- min-height: 44px; max-height: 200px;
2367
+ line-height: 1.45; resize: none;
2368
+ min-height: 28px; max-height: 200px;
2311
2369
  box-sizing: border-box; overflow-y: auto;
2312
2370
  scrollbar-width: thin;
2313
2371
  }
2314
2372
  .ds-247420 .chat-composer textarea::placeholder { color: var(--fg-3); }
2315
- .ds-247420 .chat-composer textarea:focus {
2316
- background: var(--bg-2);
2317
- border-color: var(--accent);
2318
- box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 20%, transparent);
2319
- outline: none;
2320
- }
2373
+ .ds-247420 .chat-composer textarea:focus { background: none; border: none; box-shadow: none; outline: none; }
2321
2374
  .ds-247420 .chat-composer .send {
2322
- width: 44px; height: 44px; border-radius: 50%;
2375
+ width: 36px; height: 36px; border-radius: var(--r-1);
2323
2376
  background: var(--accent); color: var(--accent-fg);
2324
2377
  border: 0; cursor: pointer;
2325
2378
  display: inline-flex; align-items: center; justify-content: center;
2326
2379
  flex-shrink: 0;
2327
- transition: background var(--dur-snap, .15s) var(--ease, ease), transform .1s ease;
2380
+ transition: background var(--dur-snap, .15s) var(--ease, ease);
2328
2381
  }
2329
2382
  .ds-247420 .chat-composer .send:disabled { opacity: .5; cursor: not-allowed; }
2330
2383
  /* Ghost icon buttons in the toolbar (attach / emoji / more) — NOT the round
@@ -2338,8 +2391,8 @@
2338
2391
  transition: background var(--dur-snap, .15s) var(--ease, ease), color var(--dur-snap, .15s) var(--ease, ease);
2339
2392
  }
2340
2393
  .ds-247420 .composer-btn:hover { background: color-mix(in oklab, var(--fg) 8%, transparent); color: var(--fg); }
2341
- .ds-247420 .chat-composer .send:hover { transform: scale(1.04); }
2342
- .ds-247420 .chat-composer .send:active { transform: scale(0.96); }
2394
+ .ds-247420 .chat-composer .send:hover { filter: brightness(1.08); }
2395
+ .ds-247420 .chat-composer .send:active { filter: brightness(0.94); }
2343
2396
  .ds-247420 .chat-composer .send:focus-visible,
2344
2397
  .ds-247420 .composer-btn:focus-visible {
2345
2398
  outline: 2px solid var(--accent);
@@ -2449,6 +2502,12 @@
2449
2502
  from { transform: rotate(0deg); }
2450
2503
  to { transform: rotate(360deg); }
2451
2504
  }
2505
+ /* Reusable spin utility: rotates the element (e.g. a refresh Icon used as a
2506
+ busy indicator) via the shared spinner-rotate keyframe. Respects reduced
2507
+ motion. Use instead of a decorative rotation glyph. */
2508
+ .ds-247420 .ds-spin { animation: spinner-rotate 0.8s linear infinite; transform-origin: center; }
2509
+ @media (prefers-reduced-motion: reduce) { .ds-247420 .ds-spin { animation: none; } }
2510
+
2452
2511
  .ds-247420 .loading { position: relative; }
2453
2512
  .ds-247420 .loading::after {
2454
2513
  content: '';
@@ -3031,14 +3090,20 @@
3031
3090
  ============================================================ */
3032
3091
 
3033
3092
  /* Tablet + Mobile: side rail becomes off-canvas drawer (≤900px) */
3034
- @media (max-width: 900px) {
3093
+ /* Sidebar drawer collapse keys on the SHELL width (@container on .app, which is
3094
+ container-type:inline-size) — NOT the viewport. Full-page (freddie) the shell
3095
+ == viewport so this is identical to the old @media(max-width:900px); embedded
3096
+ in a thebird WM window the drawer + hamburger appear when the WINDOW is narrow
3097
+ even on a wide desktop. Drawer/scrim/toggle are absolute (anchored to .app), so
3098
+ they overlay the window, not the page. */
3099
+ @container (max-width: 900px) {
3035
3100
  .ds-247420 .app-body,
3036
3101
  .ds-247420 .app-body.no-side { grid-template-columns: 1fr !important; }
3037
3102
  .ds-247420 .app-side-shell {
3038
- position: fixed;
3103
+ position: absolute;
3039
3104
  left: 0; top: var(--app-topbar-h);
3040
- height: calc(100vh - var(--app-topbar-h) - var(--app-status-h));
3041
- width: 280px; max-width: 80vw;
3105
+ height: calc(100% - var(--app-topbar-h) - var(--app-status-h));
3106
+ width: 280px; max-width: 80%;
3042
3107
  transform: translateX(-100%);
3043
3108
  transition: transform var(--dur-base, 240ms) var(--ease, ease);
3044
3109
  z-index: 50;
@@ -3072,21 +3137,24 @@
3072
3137
  .ds-247420 .app-main { padding-left: var(--space-3); padding-right: var(--space-3); }
3073
3138
  }
3074
3139
 
3075
- /* Drawer scrim — lives inside .app-body, shown when drawer open ≤900px */
3140
+ /* Drawer scrim — absolute so it covers THIS shell, shown when drawer open and
3141
+ the shell is narrow (@container on .app). */
3076
3142
  .ds-247420 .app-side-scrim {
3077
3143
  display: none;
3078
- position: fixed; inset: 0;
3144
+ position: absolute; inset: 0;
3079
3145
  background: color-mix(in oklab, var(--bg) 60%, transparent);
3080
3146
  z-index: 49;
3081
3147
  }
3082
- @media (max-width: 900px) {
3148
+ @container (max-width: 900px) {
3083
3149
  .ds-247420 .app-body.side-open .app-side-scrim { display: block; }
3084
3150
  }
3085
3151
 
3086
- /* Mobile nav toggle (hamburger) — hidden on desktop, shown ≤900px */
3152
+ /* Nav toggle (hamburger) — hidden when the shell is wide, shown when the shell
3153
+ itself is narrow (@container on .app). Absolute so it sits in THIS shell's
3154
+ topbar, not pinned to the viewport corner. */
3087
3155
  .ds-247420 .app-side-toggle {
3088
3156
  display: none;
3089
- position: fixed; top: calc((var(--app-topbar-h) - 44px) / 2); left: 10px;
3157
+ position: absolute; top: calc((var(--app-topbar-h) - 44px) / 2); left: 10px;
3090
3158
  z-index: 51;
3091
3159
  /* 44x44 minimum hit area (WCAG 2.5.5 / Apple HIG) - 36px is below the
3092
3160
  reliable-tap threshold on touch. */
@@ -3099,7 +3167,7 @@
3099
3167
  cursor: pointer;
3100
3168
  }
3101
3169
  .ds-247420 .app-side-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
3102
- @media (max-width: 900px) {
3170
+ @container (max-width: 900px) {
3103
3171
  .ds-247420 .app-side-toggle { display: inline-flex; }
3104
3172
  .ds-247420 .app-topbar .brand { margin-left: 44px; }
3105
3173
  }
@@ -3172,6 +3240,12 @@
3172
3240
  .ds-247420 .ws-shell.ws-no-pane.ws-pane-collapsed.ws-no-sessions { grid-template-columns: var(--ws-rail-w) 1fr var(--ws-pane-w); grid-template-areas: "rail content pane"; }
3173
3241
  .ds-247420 .ws-shell.ws-rail-collapsed { --ws-rail-w: var(--ws-rail-w-collapsed); }
3174
3242
  .ds-247420 .ws-shell.ws-pane-collapsed { --ws-pane-w: 0px; }
3243
+ /* Ease the rail/pane collapse. Track COUNT stays stable on collapse (only a
3244
+ width var flips), so grid-template-columns is animatable; reduced-motion
3245
+ drops it to an instant swap. */
3246
+ @media (prefers-reduced-motion: no-preference) {
3247
+ .ds-247420 .ws-shell { transition: grid-template-columns var(--dur-base) var(--ease); }
3248
+ }
3175
3249
 
3176
3250
  /* Rail (left nav) */
3177
3251
  .ds-247420 .ws-rail {
@@ -3242,9 +3316,17 @@
3242
3316
 
3243
3317
  /* Content column */
3244
3318
  .ds-247420 .ws-content { grid-area: content; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
3245
- .ds-247420 .ws-crumb { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-1); border-bottom: var(--bw-hair) solid var(--bg-3); }
3319
+ /* Crumb band is the top chrome of the content column: a stable height so the
3320
+ top edge aligns with the rail head, and a left gutter matching .ws-main so
3321
+ the trail text is not flush against the rail border. */
3322
+ .ds-247420 .ws-crumb { flex: 0 0 auto; min-height: 48px; padding: 0 var(--space-4); display: flex; align-items: center; gap: var(--space-1); border-bottom: var(--bw-hair) solid var(--bg-3); }
3246
3323
  .ds-247420 .ws-crumb-main { flex: 1 1 auto; min-width: 0; }
3247
- .ds-247420 .ws-main { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
3324
+ /* Content column gutter. Claude-Code-class calm: file grid / dashboard / event
3325
+ list / history get a generous, consistent inner gutter instead of butting
3326
+ against the 1px column borders. The chat tab opts out via .ws-main--flush
3327
+ because the chat thread carries its own --measure gutter. */
3328
+ .ds-247420 .ws-main { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; padding: var(--space-4) var(--space-5); }
3329
+ .ds-247420 .ws-main.ws-main--flush { padding: 0; }
3248
3330
  .ds-247420 .ws-main:focus { outline: none; }
3249
3331
 
3250
3332
  /* Right context pane */
@@ -3289,8 +3371,11 @@
3289
3371
  .ds-247420 .ws-rail-collapsed .ws-rail-item-label, .ds-247420 .ws-rail-collapsed .ws-rail-brand { display: none; }
3290
3372
  /* Tap-scrim behind an open drawer; dismiss on click. Hidden unless a drawer
3291
3373
  is open so it never blocks the content area on desktop. */
3292
- .ds-247420 .ws-scrim { display: none; position: fixed; inset: 0; z-index: 41; background: color-mix(in srgb, var(--fg) 38%, transparent); }
3293
- .ds-247420 .ws-shell.ws-sessions-open .ws-scrim, .ds-247420 .ws-shell.ws-pane-open .ws-scrim { display: block; }
3374
+ /* Scrim is always present on mobile so it can FADE with the drawer instead of
3375
+ hard-appearing via a display toggle; pointer-events gate keeps it inert
3376
+ while closed. Reduced-motion users get the global transition kill -> instant. */
3377
+ .ds-247420 .ws-scrim { display: block; opacity: 0; pointer-events: none; position: fixed; inset: 0; z-index: 41; background: color-mix(in srgb, var(--fg) 38%, transparent); transition: opacity var(--dur-base) var(--ease); }
3378
+ .ds-247420 .ws-shell.ws-sessions-open .ws-scrim, .ds-247420 .ws-shell.ws-pane-open .ws-scrim { opacity: 1; pointer-events: auto; }
3294
3379
  /* The mobile drawer-toggle affordances live in the crumb bar. */
3295
3380
  .ds-247420 .ws-drawer-toggle { display: inline-flex; }
3296
3381
  }
@@ -5337,7 +5422,10 @@
5337
5422
  /* --- C5: active dashboard card (current conversation). --- */
5338
5423
  .ds-247420 .ds-dash-card.is-active { box-shadow: inset 2px 0 0 var(--accent); }
5339
5424
  .ds-247420 .ds-dash-card.is-selected { background: var(--accent-tint); }
5340
- .ds-247420 .ds-dash-card.is-stale { opacity: .92; }
5425
+ /* Stale/idle card: a positive amber inset bar (mirrors is-active) so a stuck
5426
+ agent is flagged in a dense grid, not merely faded near-invisibly. The word
5427
+ 'idle' co-carries state, so this stays colour-blind safe. */
5428
+ .ds-247420 .ds-dash-card.is-stale { box-shadow: inset 2px 0 0 var(--amber, #d9a93a); }
5341
5429
 
5342
5430
  /* --- H3: dashboard live disc pulses; stale/error do not (handled by H1). --- */
5343
5431
 
@@ -5415,6 +5503,231 @@
5415
5503
  .ds-247420 .ds-session-meta-copy { min-height: 44px; min-width: 44px; }
5416
5504
  }
5417
5505
 
5506
+ /* ============================================================================
5507
+ Logic/predictability sweep (2026-06-10): composer ergonomics, turn notices,
5508
+ windowed thread, dashboard stopping/external states, cwd validation.
5509
+ ============================================================================ */
5510
+
5511
+ /* Enter-to-send hint: visible while the composer is focused or carries a
5512
+ draft; hidden under 420px to save rows. */
5513
+ .ds-247420 .chat-composer-hint {
5514
+ display: none; width: 100%; order: 5; padding: 2px var(--space-1) 0;
5515
+ font-size: var(--fs-micro); color: var(--fg-3);
5516
+ }
5517
+ .ds-247420 .chat-composer:focus-within .chat-composer-hint,
5518
+ .ds-247420 .chat-composer.has-draft .chat-composer-hint { display: block; }
5519
+ @media (max-width: 420px) {
5520
+ .ds-247420 .chat-composer .chat-composer-hint { display: none; }
5521
+ }
5522
+
5523
+ /* Transient composer note (paste/drop not supported), aria-live polite. */
5524
+ .ds-247420 .chat-composer-note {
5525
+ padding: 4px var(--space-3); font-size: var(--fs-tiny); color: var(--fg-2);
5526
+ }
5527
+
5528
+ /* Drag-over ring: token accent, never a hex. */
5529
+ .ds-247420 .chat-composer.dragover { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: var(--r-2); }
5530
+
5531
+ /* Per-bit composer context: only the bit that owns a control is a button. */
5532
+ .ds-247420 .chat-composer-context-bit {
5533
+ display: inline; padding: 0; margin: 0;
5534
+ background: none; border: none; cursor: pointer;
5535
+ font: inherit; color: inherit; text-decoration: underline dotted;
5536
+ }
5537
+ .ds-247420 .chat-composer-context-bit:hover { color: var(--fg-2); }
5538
+ .ds-247420 .chat-composer-context-bit:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
5539
+ @media (pointer: coarse) {
5540
+ .ds-247420 .chat-composer-context-bit { min-height: 44px; }
5541
+ }
5542
+
5543
+ /* Out-of-band turn notices: neutral tone (NOT error red) — a stopped or
5544
+ incomplete turn must not read as a finished answer. */
5545
+ .ds-247420 .chat-msg-notice {
5546
+ margin-top: var(--space-1); padding: 6px 10px;
5547
+ border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
5548
+ background: var(--bg-2); color: var(--fg-2); font-size: var(--fs-tiny);
5549
+ }
5550
+ .ds-247420 .chat-msg-notice.is-incomplete { border-style: dashed; }
5551
+
5552
+ /* Tail-window streaming head ('streaming · N KB so far'). */
5553
+ .ds-247420 .chat-stream-head {
5554
+ padding: 2px 0 6px; font-family: var(--ff-mono);
5555
+ font-size: var(--fs-micro); color: var(--fg-3);
5556
+ }
5557
+
5558
+ /* Windowed thread: 'show N earlier turns' control at the top. */
5559
+ .ds-247420 .agentchat-earlier {
5560
+ display: flex; align-items: center; justify-content: center;
5561
+ gap: var(--space-3); padding: var(--space-2) var(--space-3);
5562
+ }
5563
+ .ds-247420 .agentchat-earlier-count { font-size: var(--fs-tiny); color: var(--fg-3); }
5564
+ .ds-247420 .agentchat-earlier-btn {
5565
+ padding: 4px 12px; min-height: 32px; border: var(--bw-hair) solid var(--bg-3);
5566
+ border-radius: var(--r-1); background: var(--bg-2); color: var(--fg-2); cursor: pointer;
5567
+ font-family: var(--ff-body); font-size: var(--fs-tiny);
5568
+ }
5569
+ .ds-247420 .agentchat-earlier-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
5570
+ @media (pointer: coarse) {
5571
+ .ds-247420 .agentchat-earlier-btn { min-height: 44px; }
5572
+ }
5573
+
5574
+ /* Inline cwd validation line (checking / error) under the cwd input. */
5575
+ .ds-247420 .agentchat-cwd-hint { font-size: var(--fs-tiny); color: var(--fg-3); }
5576
+ .ds-247420 .agentchat-cwd-hint.is-error { color: var(--flame); }
5577
+
5578
+ /* Dashboard: shared session title heading (same string as the rails). */
5579
+ .ds-247420 .ds-dash-title {
5580
+ font-size: var(--fs-body); font-weight: 600; color: var(--fg);
5581
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
5582
+ }
5583
+
5584
+ /* Stopping state: in-flight cancel reads distinctly from running/error. */
5585
+ .ds-247420 .ds-dash-status.is-stopping { color: var(--amber, #d9a93a); }
5586
+
5587
+ /* External (observed, not owned) session card: no stop control exists. */
5588
+ .ds-247420 .ds-dash-external {
5589
+ padding: 1px 6px; border: var(--bw-hair) solid var(--bg-3); border-radius: var(--r-pill);
5590
+ font-size: var(--fs-micro); color: var(--fg-3); text-transform: uppercase; letter-spacing: .03em;
5591
+ }
5592
+ .ds-247420 .ds-dash-card.is-external { border-style: dashed; }
5593
+
5594
+ /* One connection vocabulary: offline (is-lost kept as legacy alias). */
5595
+ .ds-247420 .ds-dash-stream.is-offline { color: var(--flame); }
5596
+
5597
+ /* ============================================================
5598
+ 10th run - Claude-Code-web design-language parity
5599
+ ============================================================ */
5600
+
5601
+ /* Flat full-width chat turns (claude.ai/code), replacing the messenger
5602
+ avatar-disc + colored-bubble layout. AgentChat passes flat:true; the demo
5603
+ Chat keeps the bubble layout. Turns are full-width to --measure; the user vs
5604
+ assistant distinction is a role label + a faint assistant background. */
5605
+ .ds-247420 .chat-msg-flat { display: block; padding: var(--space-3) var(--space-4); margin: 0; background: none; border-radius: var(--r-2); }
5606
+ .ds-247420 .chat-msg-flat.them { background: color-mix(in oklab, var(--fg) 3%, transparent); }
5607
+ .ds-247420 .chat-msg-flat.you { background: none; }
5608
+ .ds-247420 .chat-msg-flat .chat-stack { max-width: var(--measure); width: 100%; margin: 0; align-items: stretch; }
5609
+ .ds-247420 .chat-msg-flat.you .chat-stack { align-items: stretch; }
5610
+ .ds-247420 .chat-role { font-size: var(--fs-tiny); font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-3); margin-bottom: var(--space-1); }
5611
+ .ds-247420 .chat-msg-flat.you .chat-role { color: var(--accent); }
5612
+ .ds-247420 .chat-msg-flat .chat-bubble { background: none; border: none; border-radius: 0; padding: 0; max-width: 100%; box-shadow: none; transform: none; }
5613
+ .ds-247420 .chat-msg-flat.you .chat-bubble { background: none; color: inherit; }
5614
+ .ds-247420 .chat-msg-flat:hover { background: color-mix(in oklab, var(--fg) 3%, transparent); }
5615
+ .ds-247420 .chat-msg-flat.you:hover { background: color-mix(in oklab, var(--fg) 2%, transparent); }
5616
+ .ds-247420 .chat-msg-flat:hover .chat-bubble { transform: none; box-shadow: none; background: none; }
5617
+ .ds-247420 .chat-msg-flat .chat-avatar { display: none; }
5618
+
5619
+ /* Tool-call card: a bordered, status-toned, scannable card (claude.ai/code),
5620
+ not the raw mascot-tinted details dump. The node keeps the .chat-bubble
5621
+ class, so these rules de-frame it and restyle. Placed after the flat-bubble
5622
+ reset so they win on equal specificity. */
5623
+ .ds-247420 .chat-msg .chat-tool {
5624
+ display: block; max-width: 100%; padding: 0; margin: var(--space-1) 0;
5625
+ background: var(--bg); color: var(--fg);
5626
+ border: var(--bw-hair) solid var(--rule); border-radius: var(--r-2);
5627
+ box-shadow: none; overflow: hidden;
5628
+ }
5629
+ .ds-247420 .chat-msg .chat-tool[open] { background: var(--bg); }
5630
+ .ds-247420 .chat-msg .chat-tool .chat-tool-head {
5631
+ display: flex; align-items: center; gap: var(--space-2);
5632
+ padding: var(--space-2) var(--space-3); cursor: pointer;
5633
+ font-size: var(--fs-sm); list-style: none;
5634
+ }
5635
+ .ds-247420 .chat-msg .chat-tool .chat-tool-head::-webkit-details-marker { display: none; }
5636
+ .ds-247420 .chat-msg .chat-tool .chat-tool-head:hover { background: var(--bg-2); }
5637
+ .ds-247420 .chat-tool-icon { display: inline-flex; color: var(--fg-3); }
5638
+ .ds-247420 .chat-tool-name { font-family: var(--ff-mono); font-weight: 600; color: var(--fg); }
5639
+ .ds-247420 .chat-tool-label { color: var(--fg-3); font-size: var(--fs-tiny); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
5640
+ .ds-247420 .chat-tool-status {
5641
+ margin-left: auto; flex: 0 0 auto;
5642
+ font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
5643
+ color: var(--fg-3);
5644
+ padding: 2px var(--space-2); border-radius: var(--r-pill);
5645
+ background: color-mix(in oklab, var(--fg) 6%, transparent);
5646
+ }
5647
+ .ds-247420 .chat-msg .chat-tool.tool-running .chat-tool-status { color: var(--accent); background: color-mix(in oklab, var(--accent) 12%, transparent); }
5648
+ .ds-247420 .chat-msg .chat-tool.tool-error .chat-tool-status { color: var(--flame); background: color-mix(in oklab, var(--flame) 12%, transparent); }
5649
+ .ds-247420 .chat-tool-body { border-top: var(--bw-hair) solid var(--rule); padding: var(--space-2) var(--space-3); display: flex; flex-direction: column; gap: var(--space-2); }
5650
+ .ds-247420 .chat-tool-section { display: flex; flex-direction: column; gap: var(--space-1); }
5651
+ .ds-247420 .chat-tool-section-label { font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--fg-3); }
5652
+ .ds-247420 .chat-tool-pre { margin: 0; padding: var(--space-2); background: var(--bg-2); border-radius: var(--r-1); font-family: var(--ff-mono); font-size: var(--fs-tiny); line-height: 1.45; overflow-x: auto; max-height: 320px; overflow-y: auto; }
5653
+ .ds-247420 .chat-tool-pre.is-error { color: var(--flame); }
5654
+ .ds-247420 .chat-tool-pre.chat-tool-empty { color: var(--fg-3); }
5655
+
5656
+ /* Fenced-code language tab on every rendered-markdown block (not just the
5657
+ structured CodeNode). injectCodeCopy reads the language- class. */
5658
+ .ds-247420 .chat-code-block { position: relative; }
5659
+ .ds-247420 .chat-code-lang {
5660
+ position: absolute; top: 0; left: 0; z-index: 2;
5661
+ padding: 2px var(--space-2); border-bottom-right-radius: var(--r-1);
5662
+ font-family: var(--ff-mono); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .04em;
5663
+ color: var(--fg-3); background: color-mix(in oklab, var(--fg) 6%, transparent);
5664
+ pointer-events: none;
5665
+ }
5666
+ .ds-247420 .chat-code-block pre { padding-top: calc(var(--space-3) + 4px); }
5667
+
5668
+ /* Motion + microinteraction polish (all reduced-motion guarded). */
5669
+ .ds-247420 .ds-session-row { transition: background var(--dur-snap) var(--ease), box-shadow var(--dur-snap) var(--ease); }
5670
+ @media (prefers-reduced-motion: no-preference) {
5671
+ .ds-247420 .agentchat-thread { scroll-behavior: smooth; }
5672
+ .ds-247420 .ds-alert { animation: ds-alert-in var(--dur-base) var(--ease); }
5673
+ }
5674
+ @keyframes ds-alert-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
5675
+ .ds-247420 .chat-code-copy { transition: opacity var(--dur-snap, .12s) var(--ease, ease), color var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease), background var(--dur-snap) var(--ease); }
5676
+ .ds-247420 .chat-msg-action { transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease); }
5677
+ .ds-247420 .agentchat-install-copy { transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease); }
5678
+ @media (prefers-reduced-motion: no-preference) {
5679
+ .ds-247420 .btn.is-armed, .ds-247420 .btn-primary.is-armed { animation: ds-arm-pulse var(--dur-slow) var(--ease); }
5680
+ }
5681
+ @keyframes ds-arm-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--warn) 55%, transparent); } 100% { box-shadow: 0 0 0 6px color-mix(in oklab, var(--warn) 0%, transparent); } }
5682
+ .ds-247420 .ds-dash-card.is-new { box-shadow: inset 2px 0 0 var(--accent), 0 0 0 1px color-mix(in oklab, var(--accent) 40%, transparent); }
5683
+ @media (prefers-reduced-motion: no-preference) {
5684
+ .ds-247420 .ds-dash-card.is-new { animation: ds-card-in var(--dur-slow) var(--ease); }
5685
+ }
5686
+ @keyframes ds-card-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
5687
+
5688
+ /* Live command-center: status-bucket groups + header breakdown + heartbeat. */
5689
+ .ds-247420 .ds-dash-group { display: flex; flex-direction: column; gap: var(--space-2); }
5690
+ .ds-247420 .ds-dash-group-label { font-size: var(--fs-micro); font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-3); padding: 0 var(--space-1); }
5691
+ .ds-247420 .ds-dash-breakdown { display: flex; align-items: center; gap: var(--space-1); font-size: var(--fs-sm); color: var(--fg-2); }
5692
+ .ds-247420 .ds-dash-breakdown .seg.is-running { color: var(--green); font-weight: 600; }
5693
+ .ds-247420 .ds-dash-breakdown .seg.is-error { color: var(--flame); font-weight: 600; }
5694
+ .ds-247420 .ds-dash-breakdown .seg.is-idle { color: var(--amber, #d9a93a); }
5695
+ .ds-247420 .ds-dash-stream-disc { display: inline-flex; align-items: center; gap: var(--space-1); }
5696
+ .ds-247420 .ds-dash-selectall { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--fs-tiny); color: var(--fg-2); cursor: pointer; background: none; border: none; padding: var(--space-1); }
5697
+ .ds-247420 .ds-dash-selectall:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
5698
+ .ds-247420 .ds-dash-clear { background: none; border: none; color: var(--fg-3); cursor: pointer; font-size: var(--fs-tiny); text-decoration: underline dotted; padding: var(--space-1); }
5699
+
5700
+ /* Conversation-rail loading skeleton (cold ccsniff walk). */
5701
+ .ds-247420 .ds-session-row-skeleton { display: flex; flex-direction: column; gap: 6px; padding: var(--space-2) var(--space-3); }
5702
+ .ds-247420 .ds-session-row-skeleton .ds-skel { background: var(--bg-3); border-radius: var(--r-1); }
5703
+ .ds-247420 .ds-session-row-skeleton .ds-skel-title { height: 12px; width: 70%; }
5704
+ .ds-247420 .ds-session-row-skeleton .ds-skel-meta { height: 9px; width: 45%; }
5705
+ @media (prefers-reduced-motion: no-preference) {
5706
+ .ds-247420 .ds-session-row-skeleton .ds-skel { animation: ds-skel-shimmer 1.3s ease-in-out infinite; background: linear-gradient(90deg, var(--bg-3) 0%, var(--bg-2) 50%, var(--bg-3) 100%); background-size: 200% 100%; }
5707
+ }
5708
+ @keyframes ds-skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
5709
+
5710
+ /* File code preview: paint Prism tokens (the bundle scoped them to the chat
5711
+ block only) + an optional non-selectable line-number gutter. */
5712
+ .ds-247420 .ds-preview-code code { color: var(--fg); }
5713
+ .ds-247420 .ds-preview-code .token.comment, .ds-247420 .ds-preview-code .token.prolog, .ds-247420 .ds-preview-code .token.doctype, .ds-247420 .ds-preview-code .token.cdata { color: var(--fg-3); }
5714
+ .ds-247420 .ds-preview-code .token.punctuation { color: var(--fg-2); }
5715
+ .ds-247420 .ds-preview-code .token.property, .ds-247420 .ds-preview-code .token.tag, .ds-247420 .ds-preview-code .token.boolean, .ds-247420 .ds-preview-code .token.number, .ds-247420 .ds-preview-code .token.constant, .ds-247420 .ds-preview-code .token.symbol { color: var(--purple-2, #7F18A4); }
5716
+ .ds-247420 .ds-preview-code .token.selector, .ds-247420 .ds-preview-code .token.attr-name, .ds-247420 .ds-preview-code .token.string, .ds-247420 .ds-preview-code .token.char, .ds-247420 .ds-preview-code .token.builtin { color: var(--green-2, #3A9A34); }
5717
+ .ds-247420 .ds-preview-code .token.atrule, .ds-247420 .ds-preview-code .token.attr-value, .ds-247420 .ds-preview-code .token.keyword { color: var(--sky, #3A6EFF); }
5718
+ .ds-247420 .ds-preview-code .token.function, .ds-247420 .ds-preview-code .token.class-name { color: var(--flame); }
5719
+ .ds-247420 .ds-preview-code.has-gutter { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 var(--space-2); }
5720
+ .ds-247420 .ds-preview-code.has-gutter code { display: block; min-width: 0; overflow-x: auto; }
5721
+ .ds-247420 .ds-preview-gutter { user-select: none; text-align: right; padding: 0 var(--space-2) 0 0; color: var(--fg-3); border-right: var(--bw-hair) solid var(--rule); font-family: var(--ff-mono); white-space: pre; line-height: inherit; }
5722
+
5723
+ /* Image preview: fit/actual control + alpha checkerboard. */
5724
+ .ds-247420 .ds-preview-media-controls { display: flex; gap: var(--space-2); padding: var(--space-2); justify-content: center; }
5725
+ .ds-247420 .ds-preview-media-alpha {
5726
+ background-image: linear-gradient(45deg, var(--bg-3) 25%, transparent 25%), linear-gradient(-45deg, var(--bg-3) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, var(--bg-3) 75%), linear-gradient(-45deg, transparent 75%, var(--bg-3) 75%);
5727
+ background-size: 16px 16px; background-position: 0 0, 0 8px, 8px -8px, -8px 0;
5728
+ }
5729
+ .ds-247420 .ds-preview-media.is-actual { max-width: none; max-height: none; }
5730
+
5418
5731
  /* editor-primitives.css */
5419
5732
  /* editor-primitives.css — chrome for in-engine editors, inspectors, IDEs,
5420
5733
  debug HUDs. All rules under .ds-247420 scope (build prefixes). Tokens