@oharness/server 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/i18n.js +354 -62
  2. package/dist/i18n.js.map +1 -1
  3. package/dist/main.js +4 -0
  4. package/dist/main.js.map +1 -1
  5. package/dist/page/client/account.js +430 -23
  6. package/dist/page/client/account.js.map +1 -1
  7. package/dist/page/client/base.js +20 -1
  8. package/dist/page/client/base.js.map +1 -1
  9. package/dist/page/client/composer.js +693 -54
  10. package/dist/page/client/composer.js.map +1 -1
  11. package/dist/page/client/computer.d.ts +27 -0
  12. package/dist/page/client/computer.js +358 -0
  13. package/dist/page/client/computer.js.map +1 -0
  14. package/dist/page/client/dialogs.js +93 -6
  15. package/dist/page/client/dialogs.js.map +1 -1
  16. package/dist/page/client/events.js +94 -7
  17. package/dist/page/client/events.js.map +1 -1
  18. package/dist/page/client/files.js +86 -1
  19. package/dist/page/client/files.js.map +1 -1
  20. package/dist/page/client/index.js +4 -0
  21. package/dist/page/client/index.js.map +1 -1
  22. package/dist/page/client/panels.js +10 -6
  23. package/dist/page/client/panels.js.map +1 -1
  24. package/dist/page/client/routines.js +296 -41
  25. package/dist/page/client/routines.js.map +1 -1
  26. package/dist/page/client/shell.js +187 -51
  27. package/dist/page/client/shell.js.map +1 -1
  28. package/dist/page/client/state.js +273 -19
  29. package/dist/page/client/state.js.map +1 -1
  30. package/dist/page/client/tasks.d.ts +8 -0
  31. package/dist/page/client/tasks.js +293 -0
  32. package/dist/page/client/tasks.js.map +1 -0
  33. package/dist/page/client/transcript.js +137 -5
  34. package/dist/page/client/transcript.js.map +1 -1
  35. package/dist/page/client/usage.js +1 -11
  36. package/dist/page/client/usage.js.map +1 -1
  37. package/dist/page/style.js +568 -60
  38. package/dist/page/style.js.map +1 -1
  39. package/dist/page.js +90 -11
  40. package/dist/page.js.map +1 -1
  41. package/dist/prefs.d.ts +14 -0
  42. package/dist/prefs.js +8 -1
  43. package/dist/prefs.js.map +1 -1
  44. package/dist/remote-crypto.d.ts +78 -0
  45. package/dist/remote-crypto.js +216 -0
  46. package/dist/remote-crypto.js.map +1 -0
  47. package/dist/remote.d.ts +103 -0
  48. package/dist/remote.js +498 -0
  49. package/dist/remote.js.map +1 -0
  50. package/dist/server.d.ts +70 -0
  51. package/dist/server.js +1291 -49
  52. package/dist/server.js.map +1 -1
  53. package/dist/starters.d.ts +92 -0
  54. package/dist/starters.js +213 -0
  55. package/dist/starters.js.map +1 -0
  56. package/dist/turn-shots.d.ts +27 -0
  57. package/dist/turn-shots.js +71 -0
  58. package/dist/turn-shots.js.map +1 -0
  59. package/package.json +2 -2
@@ -96,7 +96,14 @@ body.inset-titlebar #side-top{-webkit-app-region:drag}
96
96
  width:236px;flex:0 0 236px;background:var(--panel);border-radius:12px;margin:0 8px 8px 0;
97
97
  display:flex;flex-direction:column;overflow:hidden;
98
98
  }
99
- #side-new{padding:0 8px 2px;display:flex;flex-direction:column}
99
+ #side-new{padding:0 8px 2px;display:flex;align-items:center;gap:2px}
100
+ #side-new #new{flex:1;min-width:0}
101
+ #side-search{
102
+ display:grid;place-items:center;flex:0 0 auto;width:28px;height:28px;border:0;border-radius:6px;
103
+ background:transparent;color:var(--dim);cursor:pointer;padding:0;
104
+ }
105
+ #side-search:hover{background:var(--hover);color:var(--fg)}
106
+ #side-search .ico-sm{width:16px;height:16px}
100
107
  /* Folded away on a desktop, the whole sidebar goes, rail and all, and the
101
108
  page has the window: keeping the rail left a strip of chrome and pushed the
102
109
  tabs a rail's width plus the traffic lights in from the edge. */
@@ -161,6 +168,10 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
161
168
  .fold .ico-sm{width:16px;height:16px;flex:0 0 auto;opacity:.5}
162
169
  .fold.open .ico-sm,.fold:hover .ico-sm{opacity:.9}
163
170
  .fold-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
171
+ /* A worktree hangs under the project it came from: indented by its icon's
172
+ * width, no rule or outline to say so. */
173
+ .fold.wt{padding-left:24px;color:var(--dim)}
174
+ .fold.wt.open,.fold.wt:hover{color:var(--fg)}
164
175
  /* The routine group's run count, where a project header keeps its actions. */
165
176
  .fold .count{flex:0 0 auto;font-size:11px;color:var(--dim);padding-right:4px}
166
177
  /* A gap between projects, but not above the first one. The selector names the
@@ -172,9 +183,13 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
172
183
  edge, where the count and the ⋯ already live and a third thing would crowd
173
184
  them; dim, because it marks a row rather than asking for attention. */
174
185
  .pin-mark{
175
- width:11px;height:11px;flex:0 0 auto;margin-left:5px;opacity:.5;
176
- vertical-align:-1px;display:inline-block;
186
+ flex:0 0 auto;margin-left:3px;padding:2px;border-radius:4px;opacity:.5;
187
+ display:inline-grid;place-items:center;cursor:pointer;
177
188
  }
189
+ .pin-mark svg{width:11px;height:11px;display:block}
190
+ /* It is the unpin control, so it answers the pointer like one: full ink and a
191
+ fill on hover, the same as the ⋯ beside it. */
192
+ .pin-mark:hover,.pin-mark:focus-visible{opacity:1;background:var(--sel);outline:none}
178
193
  /* Dragging: the row being carried fades, and the one under the pointer grows
179
194
  the edge it would be dropped against. A line on the row rather than a
180
195
  floating element, because this list is rebuilt on every repaint and a
@@ -257,7 +272,7 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
257
272
  already had two rules in it and a third would have made a list of five
258
273
  entries look like three lists. */
259
274
  .row-menu .menu-head{
260
- padding:9px 10px;margin-bottom:4px;border-radius:8px;background:var(--field);
275
+ padding:12px 12px 11px;margin-bottom:6px;border-radius:12px;background:var(--field);
261
276
  }
262
277
  /* A section label, not a surface: the fill belongs to the account's facts
263
278
  above, and on a one-word caption it read as a second card pressed against
@@ -265,6 +280,25 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
265
280
  .row-menu .menu-head.menu-section{
266
281
  margin:10px 0 2px;padding:4px 10px;background:transparent;
267
282
  }
283
+ /* The workspaces: a heading and its choices on one faint surface — lighter
284
+ than the account card above it, so the card stays the menu's one solid
285
+ thing and this reads as a quiet group rather than a second card. Inside,
286
+ the workspace in use carries a stronger fill, and hover sits between. */
287
+ .row-menu .menu-choices{margin:8px 0 4px;padding:2px 4px 4px;border-radius:12px;background:color-mix(in srgb,var(--fg) 2%,transparent)}
288
+ .row-menu .menu-choices .menu-head.menu-section{margin:4px 0 0;padding-left:6px}
289
+ .row-menu .menu-choices .menu-item{border-radius:9px}
290
+ .row-menu .menu-choices .menu-item:hover{background:color-mix(in srgb,var(--fg) 4%,transparent)}
291
+ .row-menu .menu-choices .menu-item.on{background:color-mix(in srgb,var(--fg) 6%,transparent)}
292
+ .row-menu .menu-choices .menu-item.on .menu-label{font-weight:500}
293
+ .row-menu .menu-choices .menu-section-action:hover,.row-menu .menu-choices .menu-section-action:focus-visible{background:color-mix(in srgb,var(--fg) 5%,transparent)}
294
+ /* A section heading with its one action at the right: the label keeps the
295
+ caption type, the action does not — it is a button, not part of the name. */
296
+ .row-menu .menu-section-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding-right:4px}
297
+ .menu-section-action{
298
+ height:22px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dim);
299
+ font:inherit;font-size:12px;font-weight:500;letter-spacing:0;text-transform:none;cursor:pointer;
300
+ }
301
+ .menu-section-action:hover,.menu-section-action:focus-visible{background:var(--hover);color:var(--fg);outline:none}
268
302
  .menu-head-main{font-size:14px;font-weight:500;color:var(--fg);font-variant-numeric:tabular-nums}
269
303
  .menu-head-sub{margin-top:2px;font-size:12px;color:var(--dim)}
270
304
  /* The billing card borrows .menu-head's surface but not its caption type: the
@@ -329,6 +363,15 @@ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:c
329
363
  .row-menu .danger .menu-key{color:var(--dim)}
330
364
  /* A rule, not a gap: the group below it is the one that cannot be undone. */
331
365
  .row-menu .menu-sep{height:1px;margin:5px 8px;background:var(--line)}
366
+ /* The + menu's groups are told apart by space alone: no rule between them,
367
+ the same way nothing in it is outlined. */
368
+ #attach-picker .menu-sep{height:10px;margin:0;background:none}
369
+ /* Each group of the + menu opens with its own heading; the ones after the
370
+ first get room above them, which is what sets the groups apart. */
371
+ #attach-picker .menu-head:not(:first-child){padding-top:12px}
372
+ /* Tall enough for all three groups at once; the list scrolls only on a short
373
+ window, not on every one. */
374
+ #attach-picker .menu-body{max-height:min(72vh,560px)}
332
375
  /* Sized from the title it stands in for, so the row does not jump when a
333
376
  rename starts. */
334
377
  .session .title-edit{
@@ -445,27 +488,19 @@ main{flex:1;display:flex;flex-direction:column;min-width:0}
445
488
  * Same two shapes as the connector list: the vendor's own icon when the
446
489
  * server has one, and a lettered tile when it does not. Sized to the row —
447
490
  * the type is 13px, so the mark is 15px and not the 28px tile a card gets. */
448
- /* One block per vendor, whatever the vendor ships.
449
- *
450
- * A column of these is scanned by its left edge, and the edge has to be the
451
- * same one every time. Left alone, it is not: a vendor shipping a tile fills
452
- * its square, a vendor shipping bare ink covers half of it, and one shipping
453
- * a thin outline covers less than that — the eye finds no common edge and
454
- * reads the lighter rows as hanging off the line, even where the boxes are
455
- * centred on the text to the pixel.
491
+ /* The mark as the vendor drew it, on nothing.
456
492
  *
457
- * So a mark that did not bring a background is given one: the same faint
458
- * fill, the same radius, the same inset. A mark that did bring one keeps it
459
- * and fills the box, because drawing a second shape around a shape is the
460
- * thing this is trying to avoid. Either way the block is identical and the
461
- * ink inside it is the vendor's business. */
462
- .vmark{width:15px;height:15px;flex:0 0 auto;border-radius:4px;object-fit:contain;
463
- padding:1.5px;box-sizing:border-box;background:var(--sel)}
464
- .vmark.filled{padding:0;background:none}
493
+ * A faint tile under every bare mark once gave the column a common edge, but
494
+ * it also shrank the ink to two thirds of the box and put a grey square
495
+ * beside every name — in a list that is all one vendor, a column of the same
496
+ * grey squares. The box is still the same size on every row, so the marks
497
+ * still line up; the ink now fills it. A vendor that ships its own tile keeps
498
+ * it (.filled), since that tile is its mark. */
499
+ .vmark{width:16px;height:16px;flex:0 0 auto;border-radius:3px;object-fit:contain;box-sizing:border-box}
465
500
  /* The fallback, and the reason a missing icon is not a broken row: a lettered
466
501
  tile in a colour of its own still tells two vendors apart at a glance. */
467
502
  .vmono{
468
- width:15px;height:15px;flex:0 0 auto;border-radius:4px;box-sizing:border-box;
503
+ width:16px;height:16px;flex:0 0 auto;border-radius:4px;box-sizing:border-box;
469
504
  display:grid;place-items:center;color:#fff;
470
505
  font-size:9px;font-weight:600;letter-spacing:0;text-transform:uppercase;
471
506
  }
@@ -474,8 +509,7 @@ main{flex:1;display:flex;flex-direction:column;min-width:0}
474
509
  /* Typed as svg.vglyph so it outranks the .ico-sm every drawn icon carries:
475
510
  .6 is right for a chrome glyph and too faint for a vendor's mark. Ours is
476
511
  bare strokes, so it takes the same block as any other bare mark. */
477
- svg.vglyph{width:15px;height:15px;opacity:1;
478
- padding:1.5px;box-sizing:border-box;border-radius:4px;background:var(--sel)}
512
+ svg.vglyph{width:16px;height:16px;opacity:1;box-sizing:border-box}
479
513
  /* Black-on-transparent is what a good half of these vendors ship, and in the
480
514
  dark theme that is a black square on a black row. Inverted only where the
481
515
  bytes were measured to be exactly that. */
@@ -501,9 +535,6 @@ details.provider>summary .vmark,details.provider>summary .vmono,details.provider
501
535
  .pp-item .vmark,.pp-item .vmono,.pp-item .vglyph{
502
536
  width:20px;height:20px;border-radius:5px;font-size:11px;
503
537
  }
504
- details.provider>summary .vmark,.pp-item .vmark,
505
- details.provider>summary .vglyph,.pp-item .vglyph{padding:2.5px}
506
- details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
507
538
  .menu-head{
508
539
  padding:8px 10px 4px;font-size:11px;font-weight:600;letter-spacing:.04em;
509
540
  text-transform:uppercase;color:var(--dim);
@@ -564,18 +595,45 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
564
595
  justify-content:flex-end;flex-wrap:wrap;
565
596
  }
566
597
  /*
567
- * The context meter: how much of the model's window this session is using.
568
- *
569
- * Its own track colour rather than --line. Every other neutral token here is a
570
- * hairline at 6-10% alpha, which at 64x4 pixels disappears into the background
571
- * in both schemes — what was left read as a stray green dash rather than a bar
572
- * with something still to fill. The fill itself is set from the value (green,
573
- * then warn, then err) in applyState.
598
+ * The context meter: how much of the model's window this session is using,
599
+ * as a ring that fills clockwise. A ring rather than a bar because it sits in
600
+ * a row of words and has to read at a glance at the size of one; the numbers
601
+ * are in its tooltip. The sweep is a conic fill and the hole a mask, so there
602
+ * is no stroke anywhere. Its own track colour rather than --line, which at
603
+ * this size disappears into the background. --ctx-pct and --ctx-fill are set
604
+ * from the value in applyState.
574
605
  */
575
- #ctx{width:64px;height:4px;background:var(--track);border-radius:2px;overflow:hidden}
576
- #ctx i{display:block;height:100%;width:0;background:var(--ok);transition:width .3s}
606
+ #ctx{
607
+ width:14px;height:14px;border-radius:50%;flex:0 0 auto;cursor:default;
608
+ background:conic-gradient(var(--ctx-fill,var(--brand)) calc(var(--ctx-pct,0) * 1%),var(--track) 0);
609
+ -webkit-mask:radial-gradient(circle,transparent 4.2px,#000 4.6px);
610
+ mask:radial-gradient(circle,transparent 4.2px,#000 4.6px);
611
+ }
612
+ #ctx{border:0;padding:0}
613
+ /* Focus as a change of size, not a ring: this design draws no outlines. */
614
+ #ctx{transition:transform .12s}
615
+ #ctx:focus-visible,#ctx[aria-expanded="true"]{outline:none;transform:scale(1.2)}
616
+ #ctx:hover{transform:scale(1.2)}
617
+ /* The context panel. Every part is a strength of the brand colour: the two
618
+ things the model is given every turn lighter, the conversation full, as it
619
+ is the part a person can do something about. */
620
+ :root{--ctx-system:color-mix(in srgb,var(--brand) 35%,transparent);--ctx-tools:color-mix(in srgb,var(--brand) 65%,transparent);--ctx-messages:var(--brand)}
621
+ .ctx-panel{width:300px;min-width:0;padding:14px 14px 8px;gap:10px;font-size:12.5px}
622
+ .ctx-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
623
+ .ctx-title{font-weight:600;color:var(--fg)}
624
+ .ctx-total{color:var(--dim);font-variant-numeric:tabular-nums}
625
+ .ctx-bar{position:relative;display:flex;height:8px;border-radius:4px;background:var(--track);overflow:hidden}
626
+ .ctx-bar i{display:block;height:100%;flex:0 0 auto}
627
+ .ctx-tick{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--warn)}
628
+ .ctx-rows{display:flex;flex-direction:column;gap:5px}
629
+ .ctx-row{display:flex;align-items:center;gap:8px}
630
+ .ctx-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
631
+ .ctx-label{flex:1;color:var(--fg)}
632
+ .ctx-value{color:var(--dim);font-variant-numeric:tabular-nums}
633
+ .ctx-notes{display:flex;flex-direction:column;gap:2px;color:var(--dim);font-size:11.5px;line-height:1.5}
634
+ .ctx-compact{margin:0 -8px}
577
635
  #ctx,#ctxtext{display:none}
578
- #meta.has-ctx #ctx,#meta.has-ctx #ctxtext{display:block}
636
+ #meta.has-ctx #ctx{display:block}
579
637
 
580
638
  /*
581
639
  * Over the transcript, not over the whole window.
@@ -856,6 +914,83 @@ body.browser-resizing #browser-pane{transition:none}
856
914
  }
857
915
  #browser-pane.live #browser-frame{cursor:crosshair}
858
916
  .browser-idle{margin:auto;text-align:center;max-width:32ch}
917
+ /* The Computer card: the window the agent is working in, floating over the
918
+ conversation's top-right corner while a turn works in it. Filled surfaces
919
+ and a shadow, no outline — the menus' elevation, since it sits above the
920
+ page the same way. The picture is letterboxed; the agent's cursor and its
921
+ click ripples sit over it in the conversation's accent. */
922
+ #computer-float{
923
+ position:absolute;z-index:30;top:52px;right:16px;width:340px;
924
+ transition:width .18s ease;cursor:grab;touch-action:none;
925
+ }
926
+ #computer-float[hidden]{display:none}
927
+ /* Larger, to read the window rather than glance at it. */
928
+ #computer-float.big{width:min(62%,880px)}
929
+ #computer-float.dragging{transition:none;user-select:none;cursor:grabbing}
930
+ /* The picture is the card: its corners, its shadow, and the window's own
931
+ proportions (set from the stream in computer.ts), so nothing frames it —
932
+ no bar above it, no fill around it, no bands at its sides. */
933
+ #computer-body{
934
+ aspect-ratio:16/10;max-height:70vh;position:relative;overflow:hidden;border-radius:12px;
935
+ background:var(--field);box-shadow:0 1px 2px #0000001a,0 10px 30px #00000033;
936
+ }
937
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]) #computer-body{box-shadow:0 1px 2px #0000004d,0 10px 30px #00000080}}
938
+ :root[data-theme="dark"] #computer-body{box-shadow:0 1px 2px #0000004d,0 10px 30px #00000080}
939
+ /* Over the picture: a pill naming the app on the left, round buttons on the
940
+ right. Translucent dark in both themes, since what is under them is
941
+ somebody else's window, light or dark. The buttons come up with the
942
+ pointer, so a card nobody is reaching for is all picture. */
943
+ #computer-head{
944
+ position:absolute;z-index:2;top:8px;left:8px;right:8px;
945
+ display:flex;align-items:center;gap:6px;pointer-events:none;
946
+ }
947
+ #computer-label{
948
+ display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%;margin-right:auto;
949
+ height:24px;padding:0 9px;border-radius:999px;font-size:11.5px;color:#fff;
950
+ background:#000000a6;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
951
+ }
952
+ #computer-live{width:6px;height:6px;flex:0 0 auto;display:grid;place-items:center}
953
+ #computer-live[hidden]{display:none}
954
+ #computer-live i,#computer-pill i{width:6px;height:6px;border-radius:50%;background:var(--ok);animation:computer-pulse 1.6s ease-in-out infinite}
955
+ #computer-pill i{width:7px;height:7px}
956
+ @keyframes computer-pulse{50%{opacity:.35}}
957
+ #computer-where{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
958
+ #computer-head button{
959
+ pointer-events:auto;width:28px;height:28px;padding:0;border:0;border-radius:50%;flex:0 0 auto;
960
+ display:grid;place-items:center;cursor:pointer;color:#fff;
961
+ background:#000000a6;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
962
+ opacity:0;transition:opacity .15s,background .15s;
963
+ }
964
+ #computer-float:hover #computer-head button,#computer-head button:focus-visible{opacity:1}
965
+ @media (hover:none){#computer-head button{opacity:1}}
966
+ #computer-head button:hover{background:#000000d9}
967
+ #computer-head button svg{width:13px;height:13px}
968
+ #computer-stop:hover{background:#c62828e6!important}
969
+ #computer-stop svg{width:11px!important;height:11px!important}
970
+ .computer-video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
971
+ .computer-cursor{position:absolute;left:0;top:0;width:22px;height:22px;opacity:0;pointer-events:none;transition:transform .26s cubic-bezier(.3,.7,.2,1),opacity .3s}
972
+ .computer-cursor.on{opacity:1}
973
+ .computer-cursor svg{width:22px;height:22px;display:block;filter:drop-shadow(0 1px 2px #00000059)}
974
+ .computer-cursor path{fill:var(--brand);stroke:#fff;stroke-width:1.6;stroke-linejoin:round}
975
+ .computer-ring{position:absolute;width:30px;height:30px;margin:-15px 0 0 -15px;border-radius:50%;background:color-mix(in srgb,var(--brand) 35%,transparent);pointer-events:none;animation:computer-ring .5s ease-out forwards}
976
+ @keyframes computer-ring{from{transform:scale(.3);opacity:.9}to{transform:scale(1.5);opacity:0}}
977
+ .computer-empty{position:absolute;inset:0;display:grid;place-items:center;padding:24px;text-align:center;color:var(--dim);font-size:12.5px}
978
+ .computer-empty[hidden],.computer-empty:empty{display:none}
979
+ /* Folded: where it is working, one click from the card again. */
980
+ #computer-pill{
981
+ position:absolute;z-index:30;top:52px;right:16px;max-width:280px;
982
+ display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 12px;
983
+ border:0;border-radius:999px;background:var(--panel);color:var(--fg);font:inherit;font-size:12px;cursor:pointer;
984
+ box-shadow:0 1px 2px #0000000f,0 4px 14px #0000001f;
985
+ }
986
+ #computer-pill[hidden]{display:none}
987
+ #computer-pill span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
988
+ #computer-pill:hover{background:var(--hover)}
989
+ .att.computer .n.clickable{cursor:pointer}
990
+ @media (max-width:900px){
991
+ #computer-float,#computer-float.big{width:calc(100% - 24px);right:12px}
992
+ }
993
+ .embed #computer-float,.embed #computer-pill{display:none!important}
859
994
  /* One column at a time on a narrow window: two would leave neither readable. */
860
995
  #browser-hide .ico-x{display:none}
861
996
  #browser-scrim{display:none}
@@ -928,7 +1063,16 @@ body.panelled #composer,body.panelled #jump,body.panelled #changes-btn{display:n
928
1063
  #explorer-actions button:hover{background:var(--hover);color:var(--fg)}
929
1064
  #explorer-actions svg{width:15px;height:15px}
930
1065
  #explorer-body{flex:1;display:flex;min-height:0}
931
- #explorer-tree{width:var(--explorer-tree-width,min(40%,260px));flex:0 0 auto;overflow:auto;padding:2px 6px 12px}
1066
+ #explorer-side{width:var(--explorer-tree-width,min(40%,260px));flex:0 0 auto;display:flex;flex-direction:column;min-height:0}
1067
+ #explorer-find{
1068
+ display:flex;align-items:center;gap:6px;margin:2px 8px 6px;padding:0 8px;height:30px;border-radius:8px;
1069
+ background:var(--hover);color:var(--dim);cursor:text;
1070
+ }
1071
+ #explorer-find svg{width:13px;height:13px;flex:0 0 auto}
1072
+ #explorer-q{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--fg);font:inherit;font-size:12.5px;padding:0}
1073
+ #explorer-q::-webkit-search-cancel-button{cursor:pointer}
1074
+ #explorer-tree{flex:1;min-height:0;overflow:auto;padding:2px 6px 12px}
1075
+ .explorer-dir{color:var(--dim);font-size:11.5px;margin-left:6px;overflow:hidden;text-overflow:ellipsis;min-width:0;direction:rtl;text-align:left}
932
1076
  /* The line between the tree and the file, and the handle that moves it: a
933
1077
  gap wide enough to grab, drawn only while it is being pointed at. */
934
1078
  #explorer-split{flex:0 0 7px;margin:0 -3px;position:relative;z-index:1;cursor:col-resize;touch-action:none;outline:0}
@@ -1204,7 +1348,11 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1204
1348
  * positioned from the viewport instead and reuses only the surface styling.
1205
1349
  */
1206
1350
  .menu.pop{position:fixed;bottom:auto;left:auto;min-width:220px}
1207
- #log{padding:28px 24px 8px}
1351
+ /* The scrollbar's gutter is kept on both sides, and the composer below gives
1352
+ up the same width on each side (see #composer), so a message and the box it
1353
+ was typed in share both edges at every window width. Taken from one side
1354
+ only, it pushed every message half a scrollbar left of the box. */
1355
+ #log{padding:28px 24px 8px;scrollbar-gutter:stable both-edges}
1208
1356
  /*
1209
1357
  * One column width, named once.
1210
1358
  *
@@ -1312,6 +1460,8 @@ body.shelf-full #main,body.shelf-full aside,body.shelf-full #top{display:none}
1312
1460
  max-width:220px;max-height:160px;border-radius:8px;cursor:zoom-in;
1313
1461
  background:var(--bg);object-fit:cover;
1314
1462
  }
1463
+ /* A clip a little larger than a picture, since it is watched, not glanced at. */
1464
+ .user .shots video{max-width:min(320px,100%);max-height:200px;border-radius:8px;background:#000;display:block}
1315
1465
  .user{margin-bottom:18px}
1316
1466
  .panel{max-width:720px;margin:8px auto 0}
1317
1467
  .panel-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding-block:48px;font-size:13px}
@@ -1533,6 +1683,9 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
1533
1683
  .setting-status{display:flex;align-items:center;gap:7px;margin-top:8px;color:var(--dim)}
1534
1684
  .status-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--dim);opacity:.6}
1535
1685
  .status-dot.ok{background:var(--ok);opacity:1}
1686
+ /* A paired phone: its name, when, and the way to forget it, on one line. */
1687
+ .remote-phone{display:flex;align-items:center;gap:10px;min-height:28px}
1688
+ .remote-phone .remote-phone-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1536
1689
  /* The presets and the calendar on one line, on the same baseline. */
1537
1690
  .usage-window{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
1538
1691
  .usage-kind-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
@@ -1791,13 +1944,41 @@ details.provider>summary:hover{background:var(--hover)}
1791
1944
  * with it. */
1792
1945
  .row-head .ghost+.ghost,.row-head .ghost+.switch,.row-head .switch+.ghost{margin-left:0}
1793
1946
  .routine-where{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
1794
- .routine-history{margin-top:6px}
1795
- .routine-history>summary{cursor:pointer;width:max-content}
1796
- .routine-runs{display:flex;flex-direction:column;gap:2px;margin-top:6px}
1797
- .routine-run{display:flex;gap:10px;align-items:baseline;text-align:left;border:0;background:transparent;color:var(--fg);font:inherit;padding:4px 6px;border-radius:6px}
1798
- button.routine-run{cursor:pointer}
1799
- button.routine-run:hover{background:var(--hover)}
1800
- .routine-run.failed{color:var(--err)}
1947
+ .routine-history{margin-top:8px}
1948
+ .routine-history-head{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;
1949
+ font:inherit;cursor:pointer;padding:3px 6px;margin:0 -6px 4px;border-radius:6px}
1950
+ .routine-history-head:hover{background:var(--hover);color:var(--fg)}
1951
+ .routine-history-head .chev{display:inline-block;transition:transform .15s;font-size:10px}
1952
+ .routine-history.folded .chev{transform:rotate(-90deg)}
1953
+ .routine-history.folded .routine-runs{display:none}
1954
+ .routine-runs{display:flex;flex-direction:column;gap:2px}
1955
+ /* Each run a quiet row that lights on hover: status, time, facts on one line,
1956
+ and the first line of what it said beneath. No rules between them. */
1957
+ .routine-run{display:flex;flex-direction:column;gap:3px;text-align:left;border:0;background:transparent;
1958
+ color:var(--fg);font:inherit;font-size:12.5px;padding:7px 8px;margin:0 -8px;border-radius:8px;min-width:0}
1959
+ .routine-run[role=button]{cursor:pointer}
1960
+ .routine-run[role=button]:hover,.routine-run.open{background:var(--hover)}
1961
+ .routine-run:focus-visible{outline:none;background:var(--hover)}
1962
+ .routine-run-detail{display:none;flex-direction:column;align-items:flex-start;gap:8px;margin-top:4px;cursor:default}
1963
+ .routine-run.open .routine-run-detail{display:flex}
1964
+ .routine-run.open:not(.running) .routine-run-text{display:none}
1965
+ .routine-run-full{word-break:break-word;max-height:360px;overflow:auto;width:100%;
1966
+ line-height:1.55;user-select:text;color:var(--fg);font-size:13px}
1967
+ .routine-run-full.err{color:var(--err);white-space:pre-wrap}
1968
+ .routine-run-full.md>:first-child{margin-top:0}
1969
+ .routine-run.running .routine-run-text{color:var(--fg)}
1970
+ .routine-run-top{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
1971
+ .routine-run-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
1972
+ .routine-run-text.err{color:var(--err)}
1973
+ .run-status{font-size:11px;font-weight:550;padding:1px 8px;border-radius:999px;flex:0 0 auto}
1974
+ .run-status.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
1975
+ .run-status.failed{color:var(--err);background:color-mix(in srgb,var(--err) 14%,transparent)}
1976
+ .run-status.skipped{color:var(--dim);background:var(--hover)}
1977
+ .run-status.running{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}
1978
+ .run-status.running::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;
1979
+ background:currentColor;margin-right:5px;vertical-align:1px;animation:run-pulse 1.2s ease-in-out infinite}
1980
+ @keyframes run-pulse{50%{opacity:.25}}
1981
+ .routine-more{align-self:flex-start;margin-top:4px}
1801
1982
  .small{font-size:12px;white-space:pre-wrap;word-break:break-all}
1802
1983
  .keyrow{display:flex;gap:6px;margin:8px 0 6px}
1803
1984
  .keyrow input{
@@ -1906,6 +2087,54 @@ button.signin-site:disabled{opacity:.4;cursor:default}
1906
2087
  #browser-import-dialog .dlg-actions{padding:16px 22px 20px}
1907
2088
  #browser-import-dialog .dlg-actions button:disabled{opacity:.4;cursor:default}
1908
2089
  #browser-import-dialog input[type=text]{height:38px;padding:0 13px;font-size:13px}
2090
+ /* Feedback. Same frame as the import dialog; the kind is a row of fills, the
2091
+ chosen one taking the selection fill rather than a ring. */
2092
+ #feedback-dialog{width:min(520px,94vw);max-width:min(520px,94vw)}
2093
+ #feedback-dialog .cn-head{padding:20px 22px 2px}
2094
+ #feedback-dialog .cn-body{padding:14px 22px 4px;gap:10px}
2095
+ #feedback-dialog .dlg-actions{padding:16px 22px 20px}
2096
+ #feedback-dialog .dlg-actions button:disabled{opacity:.4;cursor:default}
2097
+ #feedback-dialog textarea{width:100%;box-sizing:border-box;min-height:140px;padding:11px 13px;border:0;border-radius:10px;outline:none;background:var(--field);color:var(--fg);font:inherit;font-size:13px;line-height:1.55;resize:vertical}
2098
+ #feedback-dialog textarea:focus-visible{background:var(--hover)}
2099
+ .fb-kinds{display:flex;gap:4px}
2100
+ .fb-kind{height:30px;padding:0 13px;border:0;border-radius:999px;background:var(--field);color:var(--dim);font:inherit;font-size:12.5px;cursor:pointer}
2101
+ .fb-kind:hover{background:var(--hover);color:var(--fg)}
2102
+ .fb-kind.on{background:var(--sel);color:var(--fg);font-weight:550}
2103
+ .fb-note{margin:0;font-size:11.5px;color:var(--dim);line-height:1.5}
2104
+ .fb-note:empty{display:none}
2105
+ /* Attachments as square tiles in a row, the last one adding another. Fills
2106
+ only: the add tile is a softer fill, not a dashed outline. */
2107
+ .fb-media{display:flex;flex-wrap:wrap;gap:8px}
2108
+ .fb-tile{position:relative;width:76px;height:76px;flex:0 0 auto;border-radius:14px;overflow:hidden;background:var(--field)}
2109
+ .fb-thumb{display:block;width:100%;height:100%;object-fit:cover}
2110
+ .fb-tile.is-video .fb-thumb{background:#000}
2111
+ /* A play mark over a recording's first frame, so it reads as video at a glance. */
2112
+ .fb-play{position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;background:rgba(0,0,0,.5);backdrop-filter:blur(6px)}
2113
+ .fb-play::after{content:"";position:absolute;left:10px;top:8px;border-style:solid;border-width:5px 0 5px 8px;border-color:transparent transparent transparent #fff}
2114
+ .fb-remove{
2115
+ position:absolute;top:5px;right:5px;width:20px;height:20px;padding:0;border:0;border-radius:50%;
2116
+ background:rgba(0,0,0,.55);backdrop-filter:blur(6px);color:#fff;font-size:14px;line-height:20px;text-align:center;cursor:pointer;
2117
+ opacity:.9;transition:opacity .15s,background .15s;
2118
+ }
2119
+ .fb-remove:hover{opacity:1;background:rgba(0,0,0,.75)}
2120
+ .fb-remove:disabled{display:none}
2121
+ .fb-tile.is-uploading::before,.fb-tile.is-done::before{content:"";position:absolute;inset:0;background:rgba(0,0,0,.35)}
2122
+ .fb-spinner{position:absolute;left:50%;top:50%;width:20px;height:20px;margin:-10px 0 0 -10px;border-radius:50%;
2123
+ border:2px solid rgba(255,255,255,.35);border-top-color:#fff;animation:fb-spin .8s linear infinite}
2124
+ @keyframes fb-spin{to{transform:rotate(360deg)}}
2125
+ .fb-done{position:absolute;left:50%;top:50%;width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:#fff;color:#111;font-size:12px;font-weight:700;line-height:22px;text-align:center}
2126
+ .fb-add{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;padding:0;border:0;color:var(--dim);font:inherit;cursor:pointer;transition:background .15s,color .15s}
2127
+ .fb-add:hover,.fb-add:focus-visible{background:var(--hover);color:var(--fg);outline:none}
2128
+ .fb-add .fb-add-icon{width:20px;height:20px}
2129
+ .fb-add-label{font-size:11.5px;font-weight:500}
2130
+ /* Beside the tiles, centred on them: what goes here and the limits. It takes
2131
+ the rest of the row and drops below the tiles when the row is full. */
2132
+ .fb-media{align-items:center}
2133
+ .fb-caption{display:flex;flex-direction:column;gap:3px;flex:1;min-width:170px;padding-left:4px}
2134
+ .fb-caption-title{font-size:12.5px;font-weight:500;color:var(--fg)}
2135
+ .fb-caption-sub{font-size:11.5px;color:var(--dim);line-height:1.45}
2136
+ /* Something is being dragged over: the whole dialog is the target, said with a fill. */
2137
+ #feedback-dialog.fb-dropping{background:var(--sel)}
1909
2138
  .bi-label{font-size:12px;color:var(--dim)}
1910
2139
  .bi-sites{display:flex;flex-direction:column;gap:4px;max-height:240px;overflow:auto}
1911
2140
  .bi-site{display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:9px;background:var(--field);cursor:pointer}
@@ -1990,6 +2219,23 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
1990
2219
  }
1991
2220
  .pw-tab:hover{color:var(--fg)}
1992
2221
  .pw-tab.on{background:var(--bg);color:var(--fg);font-weight:500}
2222
+ .pw-tab{display:inline-flex;align-items:center;justify-content:center;gap:6px}
2223
+ /* The site's badge on the same tab, at this dialog's size. */
2224
+ .pw-tab-save{
2225
+ font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:999px;
2226
+ background:var(--fg);color:var(--bg);font-variant-numeric:tabular-nums;
2227
+ }
2228
+ /* Twelve months at the monthly price, struck, just left of what the year costs. */
2229
+ .pw-was{color:var(--dim);font-weight:400;margin-right:6px;text-decoration-thickness:1px}
2230
+ /* What the credits reach: a quiet heading, then one row per kind of model —
2231
+ the kind in a narrow column, its marks and names beside it. */
2232
+ .pw-media{display:grid;grid-template-columns:auto 1fr;align-items:center;gap:7px 14px;font-size:12.5px}
2233
+ .pw-media-head{grid-column:1/-1;color:var(--dim);font-size:11.5px}
2234
+ .pw-media-kind{color:var(--dim);white-space:nowrap}
2235
+ .pw-media-models{display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}
2236
+ .pw-media-model{display:inline-flex;align-items:center;gap:5px;color:var(--fg);font-weight:500;white-space:nowrap}
2237
+ .pw-media-mark{display:inline-flex;width:14px;height:14px}
2238
+ .pw-media-mark svg{width:100%;height:100%}
1993
2239
  /* A labelled break, not a rule: what follows is a different kind of purchase,
1994
2240
  and a bare line would read as the list simply continuing. */
1995
2241
  .pw-sep{
@@ -2138,26 +2384,27 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2138
2384
  * is right for browsing and wrong for "the one about the parser, two days
2139
2385
  * ago". So this is a search field over every session in every tree opened
2140
2386
  * here, the projects themselves, and the few actions worth a shortcut. */
2141
- #palette{width:min(560px,92vw);max-height:min(60vh,520px)}
2387
+ #palette{width:min(600px,92vw);max-height:min(64vh,560px)}
2142
2388
  #palette[open]{display:flex;flex-direction:column}
2143
2389
  /* One left edge for everything: the field's magnifier, each row's icon, the
2144
- group labels and the key hints all start 22px in, and the field's text
2145
- starts where the row labels do. Rows sit 8px in so their highlight has room;
2390
+ group labels and the key hints all start 26px in, and the field's text
2391
+ starts where the row labels do. Rows sit 12px in so their highlight has room;
2146
2392
  their own 14px padding brings the content back to the shared edge. */
2147
- .cmdk-head{position:relative;padding:12px 12px 8px}
2148
- .cmdk-head .ico-sm{position:absolute;left:22px;top:calc(50% - 5.5px);pointer-events:none}
2149
- #palette .cmdk-head input{height:40px;padding:0 10px 0 35px;font-size:14px}
2150
- #cmdk-list{overflow-y:auto;padding:0 8px 8px;flex:1;scroll-padding-block:4px}
2393
+ .cmdk-head{position:relative;padding:16px 16px 10px}
2394
+ .cmdk-head .ico-sm{position:absolute;left:26px;top:calc(50% - 4.5px);pointer-events:none}
2395
+ #palette .cmdk-head input{height:42px;padding:0 12px 0 35px;font-size:14px}
2396
+ #cmdk-list{overflow-y:auto;padding:0 12px 12px;flex:1;scroll-padding-block:6px}
2151
2397
  .cmdk-group{
2152
- padding:12px 14px 5px;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
2398
+ padding:16px 14px 6px;font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;
2153
2399
  color:var(--dim);
2154
2400
  }
2155
- .cmdk-group:first-child{padding-top:6px}
2401
+ .cmdk-group:first-child{padding-top:8px}
2156
2402
  .cmdk-item{
2157
2403
  width:100%;display:flex;align-items:center;gap:10px;border:0;border-radius:8px;
2158
2404
  background:transparent;color:var(--fg);font:inherit;font-size:13px;cursor:pointer;
2159
- padding:8px 14px;text-align:left;
2405
+ padding:10px 14px;text-align:left;
2160
2406
  }
2407
+ .cmdk-item + .cmdk-item{margin-top:2px}
2161
2408
  /* Hover and the keyboard cursor are the same highlight: a list driven by both
2162
2409
  at once needs one answer to "which row does Enter take". */
2163
2410
  .cmdk-item.cursor{background:var(--sel)}
@@ -2169,8 +2416,7 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2169
2416
  }
2170
2417
  .cmdk-empty{padding:12px 14px;font-size:12.5px;color:var(--dim)}
2171
2418
  .cmdk-foot{
2172
- display:flex;gap:16px;padding:9px 22px 11px;font-size:11px;color:var(--dim);
2173
- box-shadow:0 -1px 0 var(--line);
2419
+ display:flex;gap:18px;padding:10px 26px 16px;font-size:11px;color:var(--dim);
2174
2420
  }
2175
2421
  .cmdk-foot kbd{
2176
2422
  font:inherit;font-size:10.5px;border:1px solid currentColor;border-radius:4px;
@@ -2190,7 +2436,10 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2190
2436
  #connect:has(#cn-aside:not([hidden])){width:min(760px,94vw)}
2191
2437
  .cn-grid{display:grid;grid-template-columns:1fr}
2192
2438
  #connect:has(#cn-aside:not([hidden])) .cn-grid{grid-template-columns:minmax(0,.85fr) minmax(0,1fr)}
2439
+ /* The sidebar's bare aside rule (a fixed width, contents pinned right) would
2440
+ * otherwise reach this one too, and set the wordmark against the divider. */
2193
2441
  .cn-aside{
2442
+ width:auto;flex:initial;align-items:stretch;
2194
2443
  display:flex;flex-direction:column;gap:20px;padding:30px 28px;
2195
2444
  background:var(--hover);border-right:1px solid var(--line);
2196
2445
  border-radius:16px 0 0 16px;
@@ -2347,6 +2596,10 @@ button.pw-plan:disabled{opacity:.4;cursor:default}
2347
2596
  three sentences side by side either wrap mid-phrase or shrink the type. Down
2348
2597
  the page they read as a short list of offers, which is what they are. */
2349
2598
  #starters{display:flex;flex-direction:column;align-items:flex-start;gap:8px}
2599
+ /* A rotated or newly written set gets a short fade, so the change is seen
2600
+ rather than noticed as a flicker. */
2601
+ #starters.fresh button{animation:starters-in .35s ease-out both}
2602
+ @keyframes starters-in{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
2350
2603
  #starters button{
2351
2604
  border:0;background:var(--panel);color:var(--fg);border-radius:999px;
2352
2605
  padding:8px 15px;font:inherit;font-size:13px;cursor:pointer;text-align:left;
@@ -2424,6 +2677,34 @@ details.tool>summary:hover::after{opacity:1}
2424
2677
  .tool-body{padding:2px 12px 10px}
2425
2678
  .tool-body pre{margin:0;overflow-x:auto;font-size:12px;max-height:340px}
2426
2679
  .tool-body .label{font-size:11px;color:var(--dim);margin:0 0 4px}
2680
+ /* The screenshots a tool took, under its result: the same thumbnail as a
2681
+ pasted image, a click away from full size. */
2682
+ .tool-shots{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
2683
+ .tool-shots img{
2684
+ max-width:220px;max-height:160px;border-radius:8px;cursor:zoom-in;
2685
+ background:var(--bg);object-fit:cover;
2686
+ }
2687
+ /* The windows a Computer use turn ended on: the result, so out in the turn
2688
+ rather than folded into a step, and a little larger than a step's shots. */
2689
+ .turn-shots{display:flex;flex-wrap:wrap;gap:10px;margin:10px 0 4px}
2690
+ .turn-shot{margin:0;display:flex;flex-direction:column;gap:5px;max-width:280px}
2691
+ .turn-shot img{
2692
+ display:block;max-width:280px;max-height:200px;border-radius:10px;cursor:zoom-in;
2693
+ background:var(--bg);object-fit:cover;
2694
+ }
2695
+ .turn-shot figcaption{font-size:11.5px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2696
+ /* The preview: the image as large as the window allows, over a dark scrim.
2697
+ Dark in both themes — a screenshot reads against black, not the page. */
2698
+ .img-preview{
2699
+ position:fixed;inset:0;z-index:90;display:grid;place-items:center;padding:32px;
2700
+ background:rgba(0,0,0,.82);cursor:zoom-out;
2701
+ }
2702
+ .img-preview img{max-width:100%;max-height:100%;border-radius:8px;object-fit:contain}
2703
+ .img-preview-count{
2704
+ position:absolute;bottom:14px;left:50%;transform:translateX(-50%);
2705
+ color:#fff;font-size:12px;opacity:.7;
2706
+ }
2707
+ .img-preview-count:empty{display:none}
2427
2708
  .tool-body .kv{margin:0 0 8px}
2428
2709
  .tool-summary{font-size:12.5px;line-height:1.5;margin:0 0 6px}
2429
2710
  details.tool-raw>summary{font-size:11px;color:var(--dim);cursor:pointer;margin:0 0 4px}
@@ -2547,6 +2828,8 @@ details.thinking[open]>summary::after{transform:translateY(-2px) rotate(45deg)}
2547
2828
 
2548
2829
  /* composer */
2549
2830
  form{padding:6px 24px 18px;background:var(--bg)}
2831
+ /* 24px plus the 11px the transcript's scrollbar gutter takes on each side. */
2832
+ @media (min-width:761px){#composer{padding-left:35px;padding-right:35px}}
2550
2833
  .composer{
2551
2834
  position:relative;
2552
2835
  max-width:720px;margin:0 auto;border-radius:16px;
@@ -2667,6 +2950,57 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2667
2950
  }
2668
2951
  .icon-btn svg{width:14px;height:14px}
2669
2952
  .icon-btn:hover{background:var(--hover)}
2953
+ /* Dictation. Listening is the harness alive, so it takes the brand: the mic
2954
+ becomes a soft lime well holding a stop square, breathing, and the panel
2955
+ above the box shows the voice in lime. Actions (done) keep --accent, as
2956
+ every action does. Fills and shadows only, never a ring. */
2957
+ .mic-btn{width:30px;height:30px;border-radius:999px;color:var(--dim);transition:background .2s,color .2s}
2958
+ .mic-btn svg{width:16px;height:16px}
2959
+ .mic-btn .mic-stop{display:none;width:14px;height:14px}
2960
+ .mic-btn:hover{color:var(--fg)}
2961
+ .mic-btn.is-recording{background:var(--brand-tint);color:var(--brand-ink);animation:mic-breathe 1.8s ease-in-out infinite}
2962
+ .mic-btn.is-recording .mic-idle{display:none}
2963
+ .mic-btn.is-recording .mic-stop{display:block}
2964
+ .mic-btn:disabled{opacity:.45;cursor:default}
2965
+ @keyframes mic-breathe{50%{background:color-mix(in srgb,var(--brand) 34%,transparent)}}
2966
+ .composer-bar .mic-btn,.composer-bar #send,.composer-bar #stop{flex-shrink:0}
2967
+ /* The panel: centred over the box's top edge, a pill of its own. One row:
2968
+ what is going on, then the voice and the two ways out while listening. */
2969
+ .dictate-pop{
2970
+ position:absolute;left:50%;bottom:calc(100% + 12px);z-index:30;transform:translateX(-50%);
2971
+ display:flex;align-items:center;gap:12px;min-height:52px;width:max-content;max-width:min(480px,calc(100% - 24px));
2972
+ padding:8px 8px 8px 20px;border-radius:999px;background:var(--panel);color:var(--fg);
2973
+ box-shadow:0 18px 44px rgba(0,0,0,.26),0 3px 10px rgba(0,0,0,.12);
2974
+ font-size:13.5px;font-weight:500;letter-spacing:.01em;animation:dp-in .18s cubic-bezier(.2,.8,.2,1);
2975
+ }
2976
+ .dictate-pop[hidden]{display:none}
2977
+ @keyframes dp-in{from{opacity:0;transform:translate(-50%,8px) scale(.97)}to{opacity:1;transform:translateX(-50%)}}
2978
+ .dp-dot{width:8px;height:8px;border-radius:50%;background:var(--brand);flex:0 0 auto;animation:dp-dot 1.4s ease-in-out infinite}
2979
+ @keyframes dp-dot{50%{opacity:.35}}
2980
+ .dp-spin{width:14px;height:14px;border-radius:50%;border:2px solid var(--brand-tint);border-top-color:var(--brand);flex:0 0 auto;animation:dp-spin .8s linear infinite}
2981
+ @keyframes dp-spin{to{transform:rotate(360deg)}}
2982
+ .dp-text{white-space:nowrap}
2983
+ .dp-time{color:var(--dim);font-weight:400;font-variant-numeric:tabular-nums;min-width:2.4em}
2984
+ .dp-wave{width:168px;height:32px;flex:0 1 auto;min-width:64px;color:var(--brand)}
2985
+ .dp-btn{width:36px;height:36px;border:0;border-radius:50%;display:grid;place-items:center;cursor:pointer;flex:0 0 auto;padding:0;font:inherit;transition:background .15s,transform .15s}
2986
+ .dp-btn:active{transform:scale(.94)}
2987
+ .dp-btn svg{width:15px;height:15px}
2988
+ .dp-cancel,.dp-close{background:var(--hover);color:var(--fg)}
2989
+ .dp-done{background:var(--accent);color:var(--bg)}
2990
+ .dp-btn:focus-visible{outline:2px solid var(--fg);outline-offset:2px}
2991
+ /* Which parts show is the state's: listening, working, or saying what went wrong. */
2992
+ .dictate-pop:not([data-state="listening"]) .dp-dot,
2993
+ .dictate-pop:not([data-state="listening"]) .dp-time,
2994
+ .dictate-pop:not([data-state="listening"]) .dp-wave,
2995
+ .dictate-pop:not([data-state="listening"]) .dp-cancel,
2996
+ .dictate-pop:not([data-state="listening"]) .dp-done,
2997
+ .dictate-pop:not([data-state="working"]) .dp-spin,
2998
+ .dictate-pop:not([data-state="error"]) .dp-close{display:none}
2999
+ .dictate-pop[data-state="working"]{padding-right:20px}
3000
+ /* The error red mixed toward the text colour: bright enough on the dark
3001
+ theme, still deep on the light one. */
3002
+ .dictate-pop[data-state="error"] .dp-text{color:color-mix(in srgb,var(--err) 62%,var(--fg));white-space:normal;line-height:1.45}
3003
+ @media (prefers-reduced-motion:reduce){.dictate-pop,.dp-dot,.dp-spin,.mic-btn.is-recording{animation:none}}
2670
3004
  /* The folder it runs in, set at the same weight as the model beside it: three
2671
3005
  settings on one bar, none of them shouting over the others. */
2672
3006
  #workspace-picker .picker-btn{gap:6px}
@@ -2708,6 +3042,54 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2708
3042
  .effort-name.on{color:var(--fg);font-weight:600}
2709
3043
  #attachments{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
2710
3044
  #attachments:empty{display:none}
3045
+ /* The tray: what the message carries, as cards sitting just above the box.
3046
+ Pictures at a size that can be recognised, files as a name and a line;
3047
+ the same fill as the box below and no outline, with the × on the card's
3048
+ corner where a thumbnail leaves no room beside it. */
3049
+ /* The box's own width and centring, so the cards start where the box does
3050
+ rather than at the column's edge. */
3051
+ #tray{display:flex;flex-wrap:wrap;gap:8px;max-width:720px;margin:0 auto 8px}
3052
+ #tray:empty{display:none}
3053
+ .tray-card{
3054
+ position:relative;display:flex;align-items:center;gap:10px;height:64px;
3055
+ max-width:240px;padding:0 30px 0 10px;border-radius:12px;background:var(--panel);
3056
+ }
3057
+ .tray-card.tray-media{width:64px;padding:0;overflow:hidden;background:var(--field)}
3058
+ .tray-card img,.tray-card video{width:100%;height:100%;object-fit:cover;display:block}
3059
+ .tray-card img{cursor:zoom-in}
3060
+ /* A clip: its first frame where a file's icon goes, with its length and size
3061
+ beside the name. */
3062
+ .tray-thumb{position:relative;width:44px;height:44px;flex:0 0 auto;border-radius:9px;overflow:hidden;background:var(--field)}
3063
+ .tray-thumb video{width:100%;height:100%;object-fit:cover;display:block}
3064
+ .tray-duration:not(:empty)::after{content:" · "}
3065
+ .tray-play{position:absolute;left:50%;top:50%;margin:-9px 0 0 -9px;width:18px;height:18px;border-radius:50%;display:grid;place-items:center;background:#0000008c;color:#fff;pointer-events:none;transition:opacity .12s}
3066
+ .tray-card.video:hover .tray-play{opacity:0}
3067
+ .tray-play svg{width:10px;height:10px}
3068
+ .tray-icon{width:36px;height:36px;flex:0 0 auto;border-radius:9px;display:grid;place-items:center;background:var(--field);color:var(--dim)}
3069
+ .tray-icon svg{width:16px;height:16px}
3070
+ .tray-text{display:flex;flex-direction:column;gap:2px;min-width:0}
3071
+ .tray-name{font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3072
+ .tray-sub{font-size:11px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3073
+ .tray-card .x{
3074
+ position:absolute;top:4px;right:4px;width:20px;height:20px;padding:0;border:0;border-radius:50%;
3075
+ display:grid;place-items:center;font-size:13px;line-height:1;cursor:pointer;
3076
+ background:var(--field);color:var(--dim);
3077
+ }
3078
+ .tray-card.tray-media .x{background:#0000008c;color:#fff;opacity:0;transition:opacity .12s}
3079
+ .tray-card.tray-media:hover .x,.tray-card .x:focus-visible{opacity:1}
3080
+ /* No hover on a touch screen: the × is always there. */
3081
+ @media (hover:none){.tray-card.tray-media .x{opacity:1}}
3082
+ .tray-card .x:hover{color:var(--fg)}
3083
+ /* A phone: one row that scrolls sideways rather than rows that push the box
3084
+ up the screen, and cards a little smaller so two or three still show. */
3085
+ @media (max-width:760px){
3086
+ #tray{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;margin-bottom:6px}
3087
+ #tray::-webkit-scrollbar{display:none}
3088
+ .tray-card{flex:0 0 auto;height:56px;max-width:200px}
3089
+ .tray-card.tray-media{width:56px}
3090
+ .tray-icon,.tray-thumb{width:36px;height:36px}
3091
+ }
3092
+ .tray-card.tray-media .x:hover{color:#fff;background:#000000b3}
2711
3093
  /* Outside the composer, tracking its width so the rows line up with the box
2712
3094
  they are waiting on. min-width:0 twice over — here and on the line inside
2713
3095
  each row — because a flex item defaults to refusing to shrink below its
@@ -2743,6 +3125,91 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2743
3125
  :root[data-theme="dark"] .q .go:hover{color:var(--brand)}
2744
3126
  .q .x{border:0;background:transparent;color:var(--dim);cursor:pointer;font:inherit;padding:0 2px}
2745
3127
  .q .x:hover{color:var(--err)}
3128
+ /* The goal being worked toward: the reason the turn keeps going, in the same
3129
+ brand ink that marks the harness at work. Its words in full colour, since
3130
+ unlike a queued line it is what everything below is for. */
3131
+ .q.goal .tag{color:var(--brand-ink)}
3132
+ :root[data-theme="dark"] .q.goal .tag{color:var(--brand)}
3133
+ .q.goal .n{color:var(--fg)}
3134
+ /* A background task: work this conversation set going. Running ones say so
3135
+ in the brand ink that marks the harness at work; ended ones step back, but
3136
+ stay a click from what they said. */
3137
+ .q.task .n{cursor:pointer}
3138
+ .q.task-running .tag,.q.task-queued .tag{color:var(--brand-ink)}
3139
+ :root[data-theme="dark"] .q.task-running .tag,:root[data-theme="dark"] .q.task-queued .tag{color:var(--brand)}
3140
+ .q.task-failed .tag{color:var(--err)}
3141
+ .q.task-completed .n,.q.task-cancelled .n,.q.task-failed .n{color:var(--dim)}
3142
+ .q.task-more{border:0;color:var(--dim);cursor:pointer;font:inherit;font-size:12px;text-align:left}
3143
+ .q.task-more:hover{color:var(--fg)}
3144
+ /* Background results read into the conversation: a card, set apart from
3145
+ what the person typed by its fill and its place on the left. */
3146
+ .task-card-row-wrap{margin-bottom:26px}
3147
+ .task-card{display:flex;flex-direction:column;gap:4px;max-width:520px;background:var(--panel);border-radius:12px;padding:10px 12px;font-size:12px}
3148
+ .task-card-head{color:var(--dim);font-size:11px;margin-bottom:2px}
3149
+ .task-card-row{display:flex;align-items:center;gap:8px;min-width:0}
3150
+ .task-card-row .tag{flex:0 0 auto;color:var(--dim);font-size:11px}
3151
+ .task-card-row.task-completed .tag{color:var(--ok)}
3152
+ .task-card-row.task-failed .tag{color:var(--err)}
3153
+ .task-card-row .n{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3154
+ .task-card-row .go{flex:0 0 auto;height:22px;padding:0 9px;border:0;border-radius:7px;background:var(--hover);color:var(--fg);cursor:pointer;font:inherit;font-size:11px}
3155
+ .task-card-row .go:hover{background:var(--brand-tint);color:var(--brand-ink)}
3156
+ /* The sheet a task opens into: down the right edge, over the conversation,
3157
+ which stays where it was underneath. Set apart by its fill, never a line. */
3158
+ #task-sheet{
3159
+ position:fixed;top:0;right:0;bottom:0;z-index:30;width:min(560px,100vw);
3160
+ display:flex;flex-direction:column;background:var(--panel);
3161
+ box-shadow:-12px 0 32px #00000014;
3162
+ }
3163
+ #task-sheet[hidden]{display:none}
3164
+ /* In the desktop app the tab strip under the sheet's top edge is the window's
3165
+ drag handle, and a drag region takes clicks whatever is drawn over it: the
3166
+ sheet's close and stop buttons would only ever move the window. */
3167
+ #task-sheet{-webkit-app-region:no-drag}
3168
+ .task-head{display:flex;align-items:flex-start;gap:10px;padding:16px 16px 10px}
3169
+ .task-titles{flex:1;min-width:0}
3170
+ .task-title{font-weight:600;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3171
+ .task-meta{color:var(--dim);font-size:12px;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3172
+ .task-stop{
3173
+ flex:0 0 auto;height:28px;padding:0 10px;border:0;border-radius:8px;
3174
+ background:var(--hover);color:var(--fg);cursor:pointer;font:inherit;font-size:12px;
3175
+ }
3176
+ .task-stop:hover{color:var(--err)}
3177
+ .task-close{flex:0 0 auto;width:28px;height:28px;border:0;border-radius:8px;background:transparent;color:var(--dim);cursor:pointer;font:inherit;font-size:18px;line-height:1}
3178
+ .task-close:hover{background:var(--hover);color:var(--fg)}
3179
+ .task-chips{position:relative;display:flex;flex-wrap:nowrap;gap:6px;overflow-x:auto;padding:0 16px 10px;scrollbar-width:none;overscroll-behavior-x:contain}
3180
+ .task-chips::-webkit-scrollbar{display:none}
3181
+ .task-chips[hidden]{display:none}
3182
+ .task-chip{
3183
+ flex:0 0 auto;height:26px;padding:0 10px;border:0;border-radius:13px;
3184
+ background:var(--hover);color:var(--dim);cursor:pointer;font:inherit;font-size:12px;
3185
+ white-space:nowrap;
3186
+ }
3187
+ .task-chip.on{background:var(--brand-tint);color:var(--brand-ink)}
3188
+ :root[data-theme="dark"] .task-chip.on{color:var(--brand)}
3189
+ .task-log{flex:1;min-height:0;overflow-y:auto;padding:4px 16px 16px;display:flex;flex-direction:column;gap:10px;font-size:13px;line-height:1.55}
3190
+ .task-msg.user{align-self:flex-end;max-width:85%;background:var(--bg);border-radius:12px;padding:8px 12px;white-space:pre-wrap;overflow-wrap:anywhere}
3191
+ .task-msg.assistant{overflow-wrap:anywhere}
3192
+ .task-tool{background:var(--bg);border-radius:10px;padding:6px 10px;font-size:12px}
3193
+ .task-tool summary{display:flex;gap:8px;cursor:pointer;list-style:none;min-width:0}
3194
+ .task-tool summary::-webkit-details-marker{display:none}
3195
+ .task-tool-name{flex:0 0 auto;font-weight:600}
3196
+ .task-tool-input{flex:1;min-width:0;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono,ui-monospace,monospace)}
3197
+ .task-tool.err .task-tool-name{color:var(--err)}
3198
+ .task-tool-result{margin:6px 0 0;max-height:240px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dim);font-size:11.5px}
3199
+ .task-note{color:var(--dim);font-size:12px}
3200
+ .task-error{color:var(--err);font-size:12px}
3201
+ .task-empty{color:var(--dim)}
3202
+ .task-working{display:flex;align-items:center;gap:8px;color:var(--dim)}
3203
+ /* The sheet's own surface runs to its bottom edge: no band behind the box. */
3204
+ .task-form{padding:8px 16px 16px;background:transparent}
3205
+ /* Field and send button as one box, the send arrow inside at its corner. */
3206
+ .task-box{display:flex;align-items:flex-end;gap:8px;background:var(--bg);border-radius:14px;padding:8px 8px 8px 12px}
3207
+ .task-input{
3208
+ flex:1;min-width:0;resize:none;border:0;padding:5px 0;max-height:160px;
3209
+ background:transparent;color:var(--fg);font:inherit;font-size:13px;line-height:1.5;outline:none;
3210
+ }
3211
+ .task-input:disabled{opacity:.6}
3212
+ .task-send{flex:0 0 auto}
2746
3213
  .att{
2747
3214
  display:flex;align-items:center;gap:7px;background:var(--bg);border-radius:8px;
2748
3215
  padding:5px 8px;font-size:12px;max-width:260px;
@@ -2756,6 +3223,37 @@ textarea.boxed:focus{outline:none;background:var(--field-focus)}
2756
3223
  the chip carries a mark for what it points at instead of a size. */
2757
3224
  .att .ico-sm{width:13px;height:13px;opacity:.65}
2758
3225
  .att .where{color:var(--dim);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3226
+ /* Goal mode is how this message is sent, not a thing attached to it: a
3227
+ tinted fill, never an outline, sets it apart from the files. */
3228
+ .att.goal{background:var(--brand-tint);color:var(--brand-ink)}
3229
+ :root[data-theme="dark"] .att.goal{color:var(--brand)}
3230
+ .att.goal .ico-sm{opacity:1}
3231
+ /* Computer use stays on for the whole conversation, so it sits in the chip
3232
+ row on every message: a full brand fill there shouted louder than anything
3233
+ the person typed. The file chips' own quiet fill, with the colour kept to
3234
+ the mark. Turning it off is not destructive, so the × does not go red. */
3235
+ .att.computer{color:var(--fg)}
3236
+ .att.computer .ico-sm{opacity:1;color:var(--brand-ink)}
3237
+ .att.computer .x:hover{color:var(--fg)}
3238
+ .att.computer-perm{flex-basis:100%;display:flex;flex-direction:column;align-items:stretch;gap:6px;padding:10px 12px;background:var(--field);border-radius:10px;max-width:none}
3239
+ .computer-perm .cp-head{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--fg)}
3240
+ .computer-perm .cp-body{color:var(--dim);font-size:12px;white-space:normal}
3241
+ .computer-perm .cp-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12.5px;color:var(--fg)}
3242
+ .computer-perm .cp-ok{width:26px;text-align:center;color:var(--dim);font-size:13px;line-height:24px}
3243
+ /* The app's primary button, inverted on either theme: white on the brand
3244
+ green was unreadable in dark mode. */
3245
+ .computer-perm .cp-btn{border:0;border-radius:7px;padding:4px 12px;background:var(--accent);color:var(--bg);font:inherit;font-size:12px;font-weight:500;cursor:pointer}
3246
+ .computer-perm .cp-btn:hover{background:color-mix(in srgb,var(--accent) 85%,var(--bg))}
3247
+ /* The card's own actions, after the rows: buttons the size of Allow, but
3248
+ secondary — the page's fill on the card's, no outline — and set right, where
3249
+ Allow already sits. Loose green words read as links to somewhere else, and
3250
+ sat under the rows as if they were a third one. */
3251
+ .computer-perm .cp-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
3252
+ .computer-perm .cp-link{
3253
+ border:0;border-radius:7px;padding:4px 12px;background:var(--bg);color:var(--fg);
3254
+ font:inherit;font-size:12px;font-weight:500;cursor:pointer;
3255
+ }
3256
+ .computer-perm .cp-link:hover{background:color-mix(in srgb,var(--fg) 8%,var(--bg))}
2759
3257
  /* Wider than the model picker's: these rows carry a name and the folder it
2760
3258
  sits in, and truncating the folder away leaves three files called index.ts. */
2761
3259
  #attach-picker .menu{min-width:min(380px,86vw)}
@@ -2843,8 +3341,11 @@ button.ghost,a.ghost{
2843
3341
  background:var(--sel);color:var(--fg);font:inherit;font-size:12.5px;cursor:pointer;
2844
3342
  }
2845
3343
  button.ghost:hover,a.ghost:hover{background:var(--hover)}
3344
+ /* Just above the composer, however tall it is: a fixed offset was covered as
3345
+ soon as attachment cards or a longer draft grew the form under it.
3346
+ --form-h is measured in transcript.ts. */
2846
3347
  #jump{
2847
- position:absolute;right:24px;bottom:118px;border-radius:999px;padding:7px 15px;border:0;
3348
+ position:absolute;right:24px;bottom:calc(var(--form-h, 108px) + 10px);border-radius:999px;padding:7px 15px;border:0;
2848
3349
  background:var(--bg);color:var(--fg);cursor:pointer;font:inherit;font-size:12px;
2849
3350
  box-shadow:0 1px 2px #0000000f,0 6px 20px #0000001f;
2850
3351
  }
@@ -2855,6 +3356,11 @@ dialog{
2855
3356
  box-shadow:0 2px 6px #00000014,0 20px 60px #00000033;
2856
3357
  }
2857
3358
  dialog::backdrop{background:rgba(0,0,0,.55)}
3359
+ /* A phone asking to pair: the code is the thing to read, so it is the largest. */
3360
+ .pair-dialog{width:min(420px,92vw);padding-bottom:16px}
3361
+ .pair-title{padding:22px 24px 6px;font-size:16px;font-weight:600}
3362
+ .pair-body{padding:0 24px;color:var(--dim);font-size:13.5px;line-height:1.5}
3363
+ .pair-code{margin:18px 24px 6px;padding:14px 0;border-radius:12px;background:var(--sel);text-align:center;font-size:30px;letter-spacing:.12em}
2858
3364
  /* The approval dialog.
2859
3365
  *
2860
3366
  * Everything here is arranged around one question: what exactly is about to
@@ -3032,7 +3538,9 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
3032
3538
  #explorer-head{padding-top:max(10px,env(safe-area-inset-top))}
3033
3539
  #explorer-body{flex-direction:column}
3034
3540
  #explorer-resizer{display:none}
3035
- #explorer-tree{width:auto;height:var(--explorer-tree-height,38%);max-height:none}
3541
+ #explorer-side{width:auto;height:var(--explorer-tree-height,38%);max-height:none}
3542
+ #explorer-find{height:36px}
3543
+ #explorer-q{font-size:16px}
3036
3544
  /* Stacked, so the handle between them drags up and down — and is a strip
3037
3545
  a thumb can find rather than a hairline. */
3038
3546
  #explorer-split{flex:0 0 18px;margin:0;cursor:row-resize}
@@ -3095,7 +3603,7 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
3095
3603
  .effort-stop,.effort-name{width:40px}
3096
3604
  .effort-stop{height:40px}
3097
3605
  .effort-knob{width:28px;height:28px}
3098
- #jump{right:14px;bottom:160px}
3606
+ #jump{right:14px}
3099
3607
 
3100
3608
  /* Dialogs, with the keyboard up: vh is the height without it. */
3101
3609
  #market{max-height:82dvh}