Mike0021 commited on
Commit
e7eb8a1
·
verified ·
1 Parent(s): e8aeadb

Pi Web fork: Copy only on answers, one step rhythm, no letter spacing

Browse files
Files changed (1) hide show
  1. piweb/hf-design.patch +352 -215
piweb/hf-design.patch CHANGED
@@ -3977,7 +3977,7 @@ index 0000000..8d003fe
3977
  +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
3978
  +OTHER DEALINGS IN THE FONT SOFTWARE.
3979
  diff --git a/app/globals.css b/app/globals.css
3980
- index d6b5535..9f01813 100644
3981
  --- a/app/globals.css
3982
  +++ b/app/globals.css
3983
  @@ -24,46 +24,109 @@
@@ -4235,7 +4235,7 @@ index d6b5535..9f01813 100644
4235
  }
4236
 
4237
  /* Theme toggle: circular wipe via View Transitions API.
4238
- @@ -218,9 +301,9 @@ body {
4239
  margin-bottom: 22px;
4240
  }
4241
 
@@ -4247,7 +4247,14 @@ index d6b5535..9f01813 100644
4247
  }
4248
 
4249
  .web-login-brand h1 {
4250
- @@ -247,15 +330,15 @@ body {
 
 
 
 
 
 
 
4251
  min-width: 0;
4252
  padding: 10px 10px 10px 14px;
4253
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
@@ -4266,7 +4273,7 @@ index d6b5535..9f01813 100644
4266
  }
4267
 
4268
  .web-login-label {
4269
- @@ -307,7 +390,7 @@ html.dark .web-login-error {
4270
  min-height: 32px;
4271
  padding: 7px 14px;
4272
  border: none;
@@ -4275,7 +4282,14 @@ index d6b5535..9f01813 100644
4275
  background: var(--accent);
4276
  color: var(--accent-contrast);
4277
  cursor: pointer;
4278
- @@ -322,7 +405,7 @@ html.dark .web-login-error {
 
 
 
 
 
 
 
4279
  }
4280
 
4281
  .web-login-form button:focus-visible {
@@ -4284,7 +4298,7 @@ index d6b5535..9f01813 100644
4284
  outline-offset: 2px;
4285
  }
4286
 
4287
- @@ -332,110 +415,139 @@ html.dark .web-login-error {
4288
  cursor: not-allowed;
4289
  }
4290
 
@@ -4380,7 +4394,7 @@ index d6b5535..9f01813 100644
4380
  overflow: hidden;
4381
  - text-overflow: ellipsis;
4382
  - white-space: nowrap;
4383
- -}
4384
  -
4385
  -.terminal-status-dot {
4386
  - width: 7px;
@@ -4407,7 +4421,7 @@ index d6b5535..9f01813 100644
4407
  - color: #9ca3af;
4408
  - cursor: pointer;
4409
  - font: inherit;
4410
- }
4411
  -
4412
  -.terminal-panel-exit {
4413
  - padding: 7px 12px;
@@ -4505,7 +4519,7 @@ index d6b5535..9f01813 100644
4505
  }
4506
 
4507
  .terminal-xterm:has(+ .terminal-extra-keys) {
4508
- @@ -451,7 +563,7 @@ html[data-keyboard-open] .terminal-extra-keys {
4509
  padding: 0;
4510
  border: none;
4511
  background: transparent;
@@ -4514,7 +4528,7 @@ index d6b5535..9f01813 100644
4514
  font-family: var(--font-mono);
4515
  font-size: 12px;
4516
  touch-action: manipulation;
4517
- @@ -462,7 +574,7 @@ html[data-keyboard-open] .terminal-extra-keys {
4518
  }
4519
 
4520
  .terminal-extra-keys button:active {
@@ -4523,7 +4537,7 @@ index d6b5535..9f01813 100644
4523
  }
4524
 
4525
  .terminal-extra-keys .is-rotated {
4526
- @@ -471,17 +583,10 @@ html[data-keyboard-open] .terminal-extra-keys {
4527
  }
4528
 
4529
  .terminal-extra-keys button.is-active {
@@ -4544,7 +4558,7 @@ index d6b5535..9f01813 100644
4544
 
4545
  /*
4546
  * Chromium-only scrollbars. Firefox renders its native ones, which the standard
4547
- @@ -502,7 +607,7 @@ html[data-keyboard-open] .terminal-extra-keys {
4548
  background-color: color-mix(in srgb, var(--text-dim) 45%, transparent);
4549
  background-clip: padding-box;
4550
  border: 2px solid transparent;
@@ -4553,7 +4567,7 @@ index d6b5535..9f01813 100644
4553
  }
4554
  /* Only ever set background-color here: the `background` shorthand resets
4555
  background-clip and would snap the thumb to the full 10px on hover. */
4556
- @@ -538,21 +643,10 @@ html[data-keyboard-open] .terminal-extra-keys {
4557
  }
4558
  ::-webkit-scrollbar-thumb {
4559
  border-width: 1px;
@@ -4576,7 +4590,7 @@ index d6b5535..9f01813 100644
4576
 
4577
 
4578
  .extension-status-shelf {
4579
- @@ -686,8 +780,9 @@ button.extension-widget-trigger:hover,
4580
  }
4581
 
4582
  button.extension-widget-trigger:focus-visible {
@@ -4588,7 +4602,7 @@ index d6b5535..9f01813 100644
4588
  }
4589
 
4590
  .extension-widget-placement {
4591
- @@ -703,9 +798,9 @@ button.extension-widget-trigger:focus-visible {
4592
 
4593
  .extension-widget-placement-icon {
4594
  display: block;
@@ -4601,7 +4615,7 @@ index d6b5535..9f01813 100644
4601
  fill: currentColor;
4602
  }
4603
 
4604
- @@ -803,8 +898,9 @@ button.extension-widget-trigger:focus-visible {
4605
  }
4606
 
4607
  .extension-status-command:focus-visible {
@@ -4613,7 +4627,7 @@ index d6b5535..9f01813 100644
4613
  }
4614
 
4615
  .extension-status-command:disabled {
4616
- @@ -826,8 +922,8 @@ button.extension-widget-trigger:focus-visible {
4617
  }
4618
 
4619
  :root {
@@ -4624,7 +4638,15 @@ index d6b5535..9f01813 100644
4624
  --font-ui: var(--font-ui-default);
4625
  --font-mono: var(--font-mono-default);
4626
  --font-ui-weight: 400;
4627
- @@ -883,11 +979,11 @@ pre, code {
 
 
 
 
 
 
 
 
4628
  .markdown-body ol > li { list-style-type: decimal; }
4629
  .markdown-body li > p { margin: 0; }
4630
  .markdown-body li::marker {
@@ -4639,7 +4661,7 @@ index d6b5535..9f01813 100644
4639
  font-weight: 700;
4640
  }
4641
  .markdown-body em { color: var(--text-muted); }
4642
- @@ -896,24 +992,23 @@ pre, code {
4643
  text-decoration: underline;
4644
  text-decoration-thickness: 1px;
4645
  text-underline-offset: 3px;
@@ -4668,7 +4690,7 @@ index d6b5535..9f01813 100644
4668
  }
4669
  .markdown-body .contains-task-list {
4670
  padding-left: 0;
4671
- @@ -935,7 +1030,7 @@ pre, code {
4672
  height: 14px;
4673
  margin: 0;
4674
  border: 1px solid var(--border);
@@ -4677,7 +4699,7 @@ index d6b5535..9f01813 100644
4677
  background: var(--bg);
4678
  display: inline-grid;
4679
  place-content: center;
4680
- @@ -967,8 +1062,8 @@ pre, code {
4681
  margin: 8px 0;
4682
  overflow-x: auto;
4683
  overflow-y: hidden;
@@ -4688,7 +4710,7 @@ index d6b5535..9f01813 100644
4688
  }
4689
  .markdown-body table {
4690
  border-collapse: separate;
4691
- @@ -1088,7 +1183,7 @@ pre, code {
4692
  padding: 7px 8px;
4693
  overflow: auto;
4694
  border: 1px solid var(--border);
@@ -4697,7 +4719,7 @@ index d6b5535..9f01813 100644
4697
  background: var(--bg-panel);
4698
  font-family: var(--font-mono);
4699
  font-size: 11px;
4700
- @@ -1108,16 +1203,13 @@ pre, code {
4701
  min-width: 0;
4702
  max-width: 100%;
4703
  margin: 6px 0;
@@ -4717,7 +4739,7 @@ index d6b5535..9f01813 100644
4717
  color: var(--text-dim);
4718
  font-size: 11px;
4719
  display: flex;
4720
- @@ -1137,7 +1229,7 @@ pre, code {
4721
  }
4722
  .markdown-code-action {
4723
  border: 1px solid var(--border);
@@ -4726,7 +4748,7 @@ index d6b5535..9f01813 100644
4726
  padding: 2px 7px;
4727
  background: var(--bg);
4728
  color: var(--text-muted);
4729
- @@ -1191,8 +1283,9 @@ pre, code {
4730
  opacity: 0.85;
4731
  }
4732
  .mermaid-preview-button:focus-visible {
@@ -4738,7 +4760,7 @@ index d6b5535..9f01813 100644
4738
  }
4739
 
4740
  .image-preview-dialog {
4741
- @@ -1228,7 +1321,7 @@ pre, code {
4742
  max-width: 100%;
4743
  max-height: 100%;
4744
  object-fit: contain;
@@ -4747,7 +4769,7 @@ index d6b5535..9f01813 100644
4747
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
4748
  }
4749
 
4750
- @@ -1244,7 +1337,7 @@ pre, code {
4751
  height: 36px;
4752
  padding: 0;
4753
  border: 1px solid var(--border);
@@ -4756,7 +4778,7 @@ index d6b5535..9f01813 100644
4756
  background: var(--bg-panel);
4757
  color: var(--text-muted);
4758
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
4759
- @@ -1259,7 +1352,7 @@ pre, code {
4760
  }
4761
 
4762
  .image-preview-close:focus-visible {
@@ -4765,7 +4787,7 @@ index d6b5535..9f01813 100644
4765
  outline-offset: 2px;
4766
  }
4767
 
4768
- @@ -1320,7 +1413,7 @@ pre, code {
4769
  height: 24px;
4770
  overflow: hidden;
4771
  border: 1px solid var(--border);
@@ -4774,7 +4796,7 @@ index d6b5535..9f01813 100644
4774
  background: var(--bg);
4775
  }
4776
  .mermaid-zoom-stepper button,
4777
- @@ -1347,7 +1440,7 @@ pre, code {
4778
  .mermaid-zoom-icon-button {
4779
  flex: 0 0 24px;
4780
  border: 1px solid var(--border);
@@ -4783,7 +4805,7 @@ index d6b5535..9f01813 100644
4783
  }
4784
  .mermaid-zoom-stepper button:hover:not(:disabled),
4785
  .mermaid-zoom-icon-button:hover {
4786
- @@ -1360,8 +1453,9 @@ pre, code {
4787
  }
4788
  .mermaid-zoom-stepper button:focus-visible,
4789
  .mermaid-zoom-icon-button:focus-visible {
@@ -4795,7 +4817,7 @@ index d6b5535..9f01813 100644
4795
  }
4796
  .mermaid-zoom-value {
4797
  min-width: 48px;
4798
- @@ -1384,7 +1478,7 @@ pre, code {
4799
  .mermaid-zoom-canvas {
4800
  padding: 12px;
4801
  border: 1px solid var(--border);
@@ -4804,7 +4826,7 @@ index d6b5535..9f01813 100644
4804
  background: var(--bg-panel);
4805
  margin: 0 auto;
4806
  transition: width 0.15s ease;
4807
- @@ -1460,7 +1554,7 @@ pre, code {
4808
  height: 24px;
4809
  overflow: hidden;
4810
  border: 1px solid var(--border);
@@ -4813,7 +4835,15 @@ index d6b5535..9f01813 100644
4813
  }
4814
 
4815
  .file-viewer-mode-button {
4816
- @@ -1503,12 +1597,11 @@ pre, code {
 
 
 
 
 
 
 
 
4817
  height: 32px;
4818
  padding: 0;
4819
  box-sizing: border-box;
@@ -4828,7 +4858,7 @@ index d6b5535..9f01813 100644
4828
  opacity: 0;
4829
  visibility: hidden;
4830
  pointer-events: none;
4831
- @@ -1542,7 +1635,7 @@ pre, code {
4832
  }
4833
 
4834
  .chat-scroll-to-bottom:focus-visible {
@@ -4837,7 +4867,7 @@ index d6b5535..9f01813 100644
4837
  outline-offset: 2px;
4838
  }
4839
 
4840
- @@ -1566,11 +1659,13 @@ pre, code {
4841
  }
4842
  }
4843
 
@@ -4855,7 +4885,7 @@ index d6b5535..9f01813 100644
4855
  margin: 0 auto;
4856
  }
4857
 
4858
- @@ -1584,22 +1679,21 @@ pre, code {
4859
 
4860
  .new-session-logo {
4861
  flex: none;
@@ -4880,11 +4910,10 @@ index d6b5535..9f01813 100644
4880
  + color: var(--text-strong);
4881
  + font-size: 20px;
4882
  + font-weight: 600;
4883
- + letter-spacing: -0.02em;
4884
  line-height: 1.15;
4885
  white-space: nowrap;
4886
  }
4887
- @@ -1613,7 +1707,8 @@ pre, code {
4888
  gap: 6px;
4889
  min-width: 0;
4890
  color: var(--text-dim);
@@ -4894,7 +4923,7 @@ index d6b5535..9f01813 100644
4894
  line-height: 1.4;
4895
  white-space: nowrap;
4896
  }
4897
- @@ -1638,7 +1733,7 @@ pre, code {
4898
  gap: 3px;
4899
  margin: -3px -4px;
4900
  padding: 3px 4px;
@@ -4903,7 +4932,7 @@ index d6b5535..9f01813 100644
4903
  color: var(--accent);
4904
  font-weight: 600;
4905
  text-decoration: none;
4906
- @@ -1652,38 +1747,27 @@ pre, code {
4907
 
4908
  /* The ring inside the box, clear of the "→" and "·" beside it. */
4909
  .new-session-update:focus-visible {
@@ -4948,7 +4977,7 @@ index d6b5535..9f01813 100644
4948
  }
4949
  }
4950
 
4951
- @@ -1700,8 +1784,8 @@ pre, code {
4952
  tab's stacked boxes, the row below the sessions), as the sidebar drew it
4953
  on main: two grey boxes in the header buttons' material, 11px, the
4954
  project's whole path and the worktree's branch in code type, cut at their
@@ -4959,7 +4988,7 @@ index d6b5535..9f01813 100644
4959
  .project-picker {
4960
  display: flex;
4961
  align-items: center;
4962
- @@ -1722,52 +1806,14 @@ pre, code {
4963
  min-width: 0;
4964
  height: 29px;
4965
  padding: 0 10px;
@@ -5015,7 +5044,7 @@ index d6b5535..9f01813 100644
5015
  }
5016
 
5017
  /* The project's name (its display name, else its folder's), cut at its
5018
- @@ -1781,8 +1827,9 @@ pre, code {
5019
  max-width: 28ch;
5020
  overflow: hidden;
5021
  color: var(--text-dim);
@@ -5026,7 +5055,7 @@ index d6b5535..9f01813 100644
5026
  }
5027
 
5028
  .project-picker-parent {
5029
- @@ -1800,10 +1847,6 @@ pre, code {
5030
  text-overflow: ellipsis;
5031
  }
5032
 
@@ -5037,7 +5066,7 @@ index d6b5535..9f01813 100644
5037
  .project-picker-button:hover,
5038
  .project-picker-button[aria-expanded="true"] {
5039
  background: var(--bg-selected);
5040
- @@ -1811,8 +1854,9 @@ pre, code {
5041
  }
5042
 
5043
  .project-picker-button:focus-visible {
@@ -5049,7 +5078,7 @@ index d6b5535..9f01813 100644
5049
  }
5050
 
5051
  .project-picker-icon,
5052
- @@ -1851,7 +1895,8 @@ pre, code {
5053
  path's color and code type, cut at its end. */
5054
  .project-picker-label.is-alias {
5055
  color: var(--text);
@@ -5059,7 +5088,7 @@ index d6b5535..9f01813 100644
5059
  }
5060
 
5061
  /* A path or a branch, cut at its left (rtl), the text itself kept left to
5062
- @@ -1901,7 +1946,8 @@ pre, code {
5063
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
5064
  background: color-mix(in srgb, var(--accent) 6%, transparent);
5065
  color: var(--text-dim);
@@ -5069,7 +5098,7 @@ index d6b5535..9f01813 100644
5069
  }
5070
 
5071
  /* No worktree list here (the files tab): a box that says why, and does nothing. */
5072
- @@ -1978,7 +2024,7 @@ pre, code {
5073
  box-sizing: border-box;
5074
  flex: 0 0 24px;
5075
  border: 1px solid var(--border);
@@ -5078,7 +5107,7 @@ index d6b5535..9f01813 100644
5078
  background: transparent;
5079
  color: var(--text-muted);
5080
  cursor: pointer;
5081
- @@ -2003,8 +2049,9 @@ pre, code {
5082
 
5083
  .file-viewer-mode-button:focus-visible,
5084
  .file-viewer-icon-button:focus-visible {
@@ -5090,7 +5119,7 @@ index d6b5535..9f01813 100644
5090
  }
5091
 
5092
  .directory-picker-path:focus-visible {
5093
- @@ -2037,7 +2084,7 @@ pre, code {
5094
  .markdown-file-preview pre {
5095
  background: var(--bg-panel);
5096
  border: 1px solid var(--border);
@@ -5099,7 +5128,7 @@ index d6b5535..9f01813 100644
5099
  padding: 12px 16px;
5100
  overflow-x: auto;
5101
  font-family: var(--font-mono);
5102
- @@ -2049,7 +2096,7 @@ pre, code {
5103
  font-size: 0.9em;
5104
  background: var(--bg-panel);
5105
  padding: 1px 5px;
@@ -5108,7 +5137,7 @@ index d6b5535..9f01813 100644
5108
  }
5109
  .markdown-file-preview pre code {
5110
  background: none;
5111
- @@ -2061,7 +2108,7 @@ pre, code {
5112
  margin-bottom: 20px;
5113
  padding: 14px 16px 12px;
5114
  border: 1px solid var(--border);
@@ -5117,7 +5146,7 @@ index d6b5535..9f01813 100644
5117
  background: var(--bg-panel);
5118
  }
5119
  .markdown-frontmatter-title {
5120
- @@ -2114,18 +2161,6 @@ pre, code {
5121
  text-underline-offset: 2px;
5122
  }
5123
 
@@ -5136,7 +5165,7 @@ index d6b5535..9f01813 100644
5136
  @keyframes drop-zone-in {
5137
  from { opacity: 0; transform: scale(0.97); }
5138
  to { opacity: 1; transform: scale(1); }
5139
- @@ -2230,10 +2265,6 @@ span.token.table {
5140
  .panel-resize-handle.is-resizing::after {
5141
  background: color-mix(in srgb, var(--text-muted) 70%, var(--border));
5142
  }
@@ -5147,7 +5176,7 @@ index d6b5535..9f01813 100644
5147
  /* Desktop sidebar: animate width */
5148
  @media (min-width: 641px) {
5149
  .sidebar-overlay-backdrop {
5150
- @@ -2264,119 +2295,6 @@ span.token.table {
5151
  }
5152
  }
5153
 
@@ -5267,7 +5296,7 @@ index d6b5535..9f01813 100644
5267
  /* Mobile sidebar: slide in/out as overlay */
5268
  @media (max-width: 640px) {
5269
  .chat-stats-center {
5270
- @@ -2401,31 +2319,7 @@ span.token.table {
5271
  box-shadow: none;
5272
  }
5273
 
@@ -5300,7 +5329,7 @@ index d6b5535..9f01813 100644
5300
  display: none;
5301
  }
5302
  .directory-picker-back,
5303
- @@ -2474,3 +2368,479 @@ span.token.table {
5304
  display: none !important;
5305
  }
5306
  }
@@ -5468,7 +5497,10 @@ index d6b5535..9f01813 100644
5468
  + transition: opacity 0.12s;
5469
  +}
5470
  +:is(.turn-user, .turn-reply):is(:hover, :focus-within) .message-actions-reveal,
5471
- +.message-actions-reveal.is-pinned { opacity: 1; }
 
 
 
5472
  +.message-meta {
5473
  + padding: 0 6px;
5474
  + color: var(--text-dim);
@@ -5476,10 +5508,18 @@ index d6b5535..9f01813 100644
5476
  + font-variant-numeric: tabular-nums;
5477
  + white-space: nowrap;
5478
  +}
 
 
 
 
5479
  +.turn-reply { margin: 0 0 22px; }
5480
  +.turn-reply > .message-actions { margin: 4px 0 0 -6px; }
5481
  +.turn-reply.is-step { margin: 0; }
 
 
 
5482
  +.reply-blocks { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
 
5483
  +
5484
  +/* The steps of a turn: one summary line, "Ran 2 commands, read a file", that
5485
  + opens onto the steps themselves along a thin rail. */
@@ -5511,8 +5551,8 @@ index d6b5535..9f01813 100644
5511
  + position: relative;
5512
  + display: flex;
5513
  + flex-direction: column;
5514
- + gap: 4px;
5515
- + margin: 4px 0 4px 1px;
5516
  + padding-left: 16px;
5517
  +}
5518
  +/* A drawing, not a border: the rail the steps hang from. */
@@ -5853,6 +5893,47 @@ index 62c25e4..a921c1d 100644
5853
  <script
5854
  dangerouslySetInnerHTML={{
5855
  __html: THEME_INIT_SCRIPT,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5856
  diff --git a/app/login/page.tsx b/app/login/page.tsx
5857
  index 80264fe..6506201 100644
5858
  --- a/app/login/page.tsx
@@ -5905,7 +5986,7 @@ index 79c8282..97731f1 100644
5905
  lang: "en",
5906
  icons: [
5907
  diff --git a/app/settings.css b/app/settings.css
5908
- index 0bf21eb..f4c84b5 100644
5909
  --- a/app/settings.css
5910
  +++ b/app/settings.css
5911
  @@ -35,14 +35,15 @@
@@ -5980,7 +6061,7 @@ index 0bf21eb..f4c84b5 100644
5980
  .config-sidebar-group-label {
5981
  display: flex;
5982
  align-items: center;
5983
- @@ -129,11 +133,10 @@
5984
  padding: 4px 8px 3px;
5985
  color: var(--text-dim);
5986
  font-family: inherit;
@@ -5989,12 +6070,12 @@ index 0bf21eb..f4c84b5 100644
5989
  + font-size: 12px;
5990
  + font-weight: 400;
5991
  line-height: 1.4;
5992
- letter-spacing: 0;
5993
  - text-transform: uppercase;
5994
  }
5995
 
5996
  .config-sidebar-group-label-text {
5997
- @@ -144,6 +147,13 @@
5998
  white-space: nowrap;
5999
  }
6000
 
@@ -6008,7 +6089,7 @@ index 0bf21eb..f4c84b5 100644
6008
  /* Sits in the heading row, so the sidebar gains no height for it. */
6009
  .config-sidebar-group-switch {
6010
  display: inline-flex;
6011
- @@ -153,8 +163,9 @@
6012
  }
6013
 
6014
  .config-sidebar-group-count {
@@ -6019,7 +6100,7 @@ index 0bf21eb..f4c84b5 100644
6019
  }
6020
 
6021
  /* Three lines, then scroll: the phone layout gives the whole sidebar 190px. */
6022
- @@ -188,7 +199,7 @@
6023
  height: 30px;
6024
  padding: 0 8px;
6025
  border: 0;
@@ -6028,7 +6109,7 @@ index 0bf21eb..f4c84b5 100644
6028
  outline: none;
6029
  background: transparent;
6030
  color: var(--text);
6031
- @@ -202,17 +213,19 @@
6032
 
6033
  .config-sidebar-item:not(:disabled):hover,
6034
  .config-sidebar-item:not(:disabled):focus-visible {
@@ -6053,7 +6134,7 @@ index 0bf21eb..f4c84b5 100644
6053
  }
6054
 
6055
  .config-sidebar-item:disabled {
6056
- @@ -236,8 +249,9 @@
6057
  .models-sidebar-badge {
6058
  flex-shrink: 0;
6059
  color: var(--text-dim);
@@ -6064,7 +6145,7 @@ index 0bf21eb..f4c84b5 100644
6064
  }
6065
 
6066
  .oauth-paste-panel {
6067
- @@ -277,9 +291,8 @@
6068
  /* An input's intrinsic width would otherwise push the button out of a narrow pane. */
6069
  min-width: 0;
6070
  padding: 6px 9px;
@@ -6076,7 +6157,7 @@ index 0bf21eb..f4c84b5 100644
6076
  color: var(--text);
6077
  font-family: var(--font-mono);
6078
  font-size: 12px;
6079
- @@ -290,9 +303,9 @@
6080
  flex-shrink: 0;
6081
  padding: 6px 12px;
6082
  border: none;
@@ -6089,7 +6170,7 @@ index 0bf21eb..f4c84b5 100644
6089
  cursor: pointer;
6090
  font-size: 12px;
6091
  font-weight: 600;
6092
- @@ -304,13 +317,13 @@
6093
  cursor: not-allowed;
6094
  }
6095
 
@@ -6106,7 +6187,7 @@ index 0bf21eb..f4c84b5 100644
6106
  }
6107
 
6108
  .enabled-models-banner-text {
6109
- @@ -370,8 +383,9 @@
6110
  .enabled-models-count {
6111
  margin-right: auto;
6112
  color: var(--text-dim);
@@ -6117,7 +6198,7 @@ index 0bf21eb..f4c84b5 100644
6117
  }
6118
 
6119
  .enabled-models-note {
6120
- @@ -383,7 +397,7 @@
6121
  .enabled-models-error {
6122
  padding: 7px 9px;
6123
  border: 1px solid rgba(239, 68, 68, 0.3);
@@ -6126,7 +6207,7 @@ index 0bf21eb..f4c84b5 100644
6126
  color: #ef4444;
6127
  font-size: 11px;
6128
  line-height: 1.4;
6129
- @@ -401,10 +415,9 @@
6130
  box-sizing: border-box;
6131
  height: 30px;
6132
  padding: 0 9px;
@@ -6139,7 +6220,7 @@ index 0bf21eb..f4c84b5 100644
6139
  color: var(--text);
6140
  font-family: inherit;
6141
  font-size: 12px;
6142
- @@ -413,8 +426,8 @@
6143
  .enabled-models-list {
6144
  max-height: 360px;
6145
  border: 1px solid var(--border);
@@ -6150,7 +6231,7 @@ index 0bf21eb..f4c84b5 100644
6150
  overflow-y: auto;
6151
  }
6152
 
6153
- @@ -457,9 +470,9 @@
6154
  .enabled-models-pin {
6155
  flex-shrink: 0;
6156
  padding: 1px 4px;
@@ -6163,7 +6244,7 @@ index 0bf21eb..f4c84b5 100644
6163
  font-size: 9px;
6164
  }
6165
 
6166
- @@ -496,10 +509,9 @@
6167
  align-items: center;
6168
  justify-content: space-between;
6169
  gap: 18px;
@@ -6177,7 +6258,7 @@ index 0bf21eb..f4c84b5 100644
6177
  }
6178
 
6179
  .agents-feature-copy {
6180
- @@ -541,7 +553,7 @@
6181
  height: 28px;
6182
  padding: 0 6px;
6183
  border: 1px solid var(--border);
@@ -6186,7 +6267,7 @@ index 0bf21eb..f4c84b5 100644
6186
  background: var(--bg);
6187
  color: var(--text);
6188
  font: inherit;
6189
- @@ -566,7 +578,7 @@
6190
  flex: 1;
6191
  min-width: 0;
6192
  min-height: 0;
@@ -6195,7 +6276,7 @@ index 0bf21eb..f4c84b5 100644
6195
  overflow-y: auto;
6196
  font-family: inherit;
6197
  font-size: 12px;
6198
- @@ -628,13 +640,13 @@
6199
  line-height: 1.35;
6200
  }
6201
 
@@ -6207,12 +6288,12 @@ index 0bf21eb..f4c84b5 100644
6207
  + font-size: 12px;
6208
  + font-weight: 400;
6209
  line-height: 1.35;
6210
- letter-spacing: 0;
6211
  - text-transform: uppercase;
6212
  }
6213
 
6214
  .config-field {
6215
- @@ -662,7 +674,7 @@
6216
  background: var(--border);
6217
  border: 5px solid transparent;
6218
  background-clip: padding-box;
@@ -6221,7 +6302,7 @@ index 0bf21eb..f4c84b5 100644
6221
  }
6222
 
6223
  .config-field-label {
6224
- @@ -697,15 +709,15 @@
6225
  .config-scope-tag {
6226
  flex-shrink: 0;
6227
  padding: 1px 5px;
@@ -6240,7 +6321,7 @@ index 0bf21eb..f4c84b5 100644
6240
  }
6241
 
6242
  .config-scope-switch-field {
6243
- @@ -730,7 +742,7 @@
6244
  height: 30px;
6245
  overflow: hidden;
6246
  border: 1px solid var(--border);
@@ -6249,7 +6330,7 @@ index 0bf21eb..f4c84b5 100644
6249
  }
6250
 
6251
  .config-scope-switch-option {
6252
- @@ -760,7 +772,7 @@
6253
  .config-scope-switch.is-small {
6254
  display: flex;
6255
  height: auto;
@@ -6258,7 +6339,7 @@ index 0bf21eb..f4c84b5 100644
6258
  font-size: 12px;
6259
  }
6260
 
6261
- @@ -891,9 +903,8 @@
6262
  width: 100%;
6263
  height: 36px;
6264
  padding: 0 11px;
@@ -6270,7 +6351,7 @@ index 0bf21eb..f4c84b5 100644
6270
  color: var(--text);
6271
  font-family: var(--font-mono);
6272
  font-size: 12px;
6273
- @@ -935,8 +946,8 @@
6274
  min-height: 30px;
6275
  padding: 6px 9px;
6276
  border: 1px solid var(--border);
@@ -6281,7 +6362,7 @@ index 0bf21eb..f4c84b5 100644
6281
  color: var(--text-dim);
6282
  cursor: pointer;
6283
  font-family: var(--font-mono);
6284
- @@ -975,12 +986,15 @@
6285
  white-space: pre-wrap;
6286
  }
6287
 
@@ -6300,7 +6381,7 @@ index 0bf21eb..f4c84b5 100644
6300
  }
6301
 
6302
  .config-notice.has-action {
6303
- @@ -1104,8 +1118,13 @@
6304
  align-items: center;
6305
  gap: 8px;
6306
  min-height: 52px;
@@ -6316,7 +6397,7 @@ index 0bf21eb..f4c84b5 100644
6316
  }
6317
 
6318
  .config-footer-status {
6319
- @@ -1159,7 +1178,7 @@
6320
  gap: 6px;
6321
  border-width: 1px;
6322
  border-style: solid;
@@ -6324,8 +6405,9 @@ index 0bf21eb..f4c84b5 100644
6324
  + border-radius: var(--radius-sm);
6325
  cursor: pointer;
6326
  font-family: inherit;
6327
- letter-spacing: 0;
6328
- @@ -1168,35 +1187,39 @@
 
6329
  transition: background-color 0.14s ease, border-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
6330
  }
6331
 
@@ -6381,7 +6463,7 @@ index 0bf21eb..f4c84b5 100644
6381
  }
6382
 
6383
  .config-button-ghost {
6384
- @@ -1206,24 +1229,31 @@
6385
  }
6386
 
6387
  .config-button-primary:not(:disabled):hover {
@@ -6421,7 +6503,7 @@ index 0bf21eb..f4c84b5 100644
6421
  outline-offset: 2px;
6422
  }
6423
 
6424
- @@ -1248,33 +1278,41 @@
6425
  margin-left: auto;
6426
  }
6427
 
@@ -6475,7 +6557,7 @@ index 0bf21eb..f4c84b5 100644
6475
  .config-switch::before {
6476
  content: "";
6477
  position: absolute;
6478
- @@ -1283,7 +1321,7 @@
6479
 
6480
  .config-switch:disabled {
6481
  cursor: default;
@@ -6484,7 +6566,7 @@ index 0bf21eb..f4c84b5 100644
6484
  }
6485
 
6486
  .config-switch.is-loading:disabled {
6487
- @@ -1294,42 +1332,38 @@
6488
  position: absolute;
6489
  top: 2px;
6490
  left: 2px;
@@ -6541,7 +6623,7 @@ index 0bf21eb..f4c84b5 100644
6541
  }
6542
 
6543
  .config-list-action-button {
6544
- @@ -1342,7 +1376,7 @@
6545
  min-height: 30px;
6546
  padding: 0 8px;
6547
  border: 0;
@@ -6550,7 +6632,7 @@ index 0bf21eb..f4c84b5 100644
6550
  background: transparent;
6551
  color: var(--text-dim);
6552
  cursor: pointer;
6553
- @@ -1358,7 +1392,7 @@
6554
 
6555
  .config-list-action-button[aria-current="page"] {
6556
  background: var(--bg-selected);
@@ -6559,7 +6641,7 @@ index 0bf21eb..f4c84b5 100644
6559
  }
6560
 
6561
  .config-list-action-button:disabled {
6562
- @@ -1382,6 +1416,14 @@
6563
  opacity: 0.7;
6564
  }
6565
 
@@ -6574,7 +6656,7 @@ index 0bf21eb..f4c84b5 100644
6574
  .settings-dialog-backdrop {
6575
  position: fixed;
6576
  inset: 0;
6577
- @@ -1389,7 +1431,7 @@
6578
  display: flex;
6579
  align-items: center;
6580
  justify-content: center;
@@ -6583,7 +6665,7 @@ index 0bf21eb..f4c84b5 100644
6583
  }
6584
 
6585
  /* Focus moves to the dialog itself when Settings opens (tabIndex -1); it is not a control, so no ring. */
6586
- @@ -1398,127 +1440,184 @@
6587
  }
6588
 
6589
  .settings-dialog-surface {
@@ -6839,7 +6921,7 @@ index 0bf21eb..f4c84b5 100644
6839
  }
6840
 
6841
  .settings-dialog-main {
6842
- @@ -1537,136 +1636,197 @@
6843
  min-height: 0;
6844
  }
6845
 
@@ -7116,7 +7198,7 @@ index 0bf21eb..f4c84b5 100644
7116
  font-family: var(--font-mono);
7117
  font-weight: var(--font-mono-weight);
7118
  }
7119
- @@ -1676,197 +1836,324 @@
7120
  }
7121
 
7122
  .settings-font-input:focus-visible {
@@ -7572,7 +7654,7 @@ index 0bf21eb..f4c84b5 100644
7572
  }
7573
 
7574
  @media (max-width: 640px) {
7575
- @@ -1887,13 +2174,13 @@
7576
  width: 100%;
7577
  height: 190px;
7578
  border-right: 0;
@@ -7588,7 +7670,7 @@ index 0bf21eb..f4c84b5 100644
7588
  }
7589
 
7590
  .agents-feature-actions {
7591
- @@ -1905,29 +2192,6 @@
7592
  padding: 14px;
7593
  }
7594
 
@@ -7618,7 +7700,7 @@ index 0bf21eb..f4c84b5 100644
7618
  }
7619
 
7620
  @keyframes saved-pop {
7621
- @@ -2066,7 +2330,7 @@
7622
  max-width: 100%;
7623
  padding: 0 5px;
7624
  border: 1px solid var(--border);
@@ -7627,7 +7709,7 @@ index 0bf21eb..f4c84b5 100644
7627
  background: var(--bg-panel);
7628
  color: var(--text);
7629
  font-family: var(--font-mono);
7630
- @@ -2085,7 +2349,7 @@
7631
  padding: 6px 8px;
7632
  overflow: auto;
7633
  border: 1px solid var(--border);
@@ -7636,7 +7718,7 @@ index 0bf21eb..f4c84b5 100644
7636
  background: var(--bg-panel);
7637
  color: var(--text-muted);
7638
  font-family: var(--font-mono);
7639
- @@ -2172,9 +2436,8 @@
7640
  min-width: 0;
7641
  height: 32px;
7642
  padding: 0 10px;
@@ -7648,7 +7730,7 @@ index 0bf21eb..f4c84b5 100644
7648
  color: var(--text);
7649
  font-family: var(--font-mono);
7650
  font-size: 12px;
7651
- @@ -2266,10 +2529,9 @@ select.mcp-add-input {
7652
  max-width: 100%;
7653
  max-height: 100%;
7654
  overflow: hidden;
@@ -7662,7 +7744,7 @@ index 0bf21eb..f4c84b5 100644
7662
  }
7663
 
7664
  /* Focus moves to the dialog itself when it opens (tabIndex -1); it is not a control, so no ring. */
7665
- @@ -2282,7 +2544,7 @@ select.mcp-add-input {
7666
  flex: 1 1 auto;
7667
  gap: 12px;
7668
  min-height: 0;
@@ -7671,7 +7753,7 @@ index 0bf21eb..f4c84b5 100644
7671
  overflow: auto;
7672
  overscroll-behavior: contain;
7673
  }
7674
- @@ -2315,7 +2577,7 @@ select.mcp-add-input {
7675
  margin-top: 10px;
7676
  padding: 8px 10px;
7677
  border: 1px solid var(--border);
@@ -7680,7 +7762,7 @@ index 0bf21eb..f4c84b5 100644
7680
  background: var(--bg);
7681
  color: var(--text);
7682
  font-family: var(--font-mono);
7683
- @@ -2333,7 +2595,7 @@ select.mcp-add-input {
7684
  .project-trust-error {
7685
  flex-shrink: 0;
7686
  max-height: 30vh;
@@ -7689,7 +7771,7 @@ index 0bf21eb..f4c84b5 100644
7689
  overflow: auto;
7690
  color: #ef4444;
7691
  font-size: 12px;
7692
- @@ -2353,19 +2615,20 @@ select.mcp-add-input {
7693
  flex-shrink: 0;
7694
  justify-content: flex-end;
7695
  gap: 8px;
@@ -7717,7 +7799,7 @@ index 0bf21eb..f4c84b5 100644
7717
  }
7718
 
7719
  .project-trust-button:disabled {
7720
- @@ -2373,10 +2636,8 @@ select.mcp-add-input {
7721
  }
7722
 
7723
  .project-trust-button.is-primary {
@@ -7730,7 +7812,7 @@ index 0bf21eb..f4c84b5 100644
7730
  }
7731
 
7732
  .project-trust-button.is-primary:disabled {
7733
- @@ -2450,7 +2711,7 @@ select.mcp-add-input {
7734
  min-width: 0;
7735
  padding: 8px 10px;
7736
  border: 1px solid var(--border);
@@ -7739,7 +7821,7 @@ index 0bf21eb..f4c84b5 100644
7739
  background: var(--bg);
7740
  }
7741
 
7742
- @@ -2472,7 +2733,7 @@ select.mcp-add-input {
7743
  .project-trust-mcp-tag {
7744
  flex-shrink: 0;
7745
  padding: 1px 5px;
@@ -7748,7 +7830,7 @@ index 0bf21eb..f4c84b5 100644
7748
  background: rgba(120, 120, 120, 0.12);
7749
  color: var(--text-dim);
7750
  font-size: 10px;
7751
- @@ -2493,7 +2754,7 @@ select.mcp-add-input {
7752
  display: inline-block;
7753
  margin: 2px 4px 0 0;
7754
  padding: 0 4px;
@@ -9235,7 +9317,7 @@ index 737e7a5..c098317 100644
9235
 
9236
  .sidebar-sheet .sidebar-menu-question-text {
9237
  diff --git a/app/sidebar.css b/app/sidebar.css
9238
- index 86728a2..d1346be 100644
9239
  --- a/app/sidebar.css
9240
  +++ b/app/sidebar.css
9241
  @@ -91,8 +91,9 @@
@@ -9347,7 +9429,7 @@ index 86728a2..d1346be 100644
9347
  border-radius: 50%;
9348
  opacity: 0.35;
9349
  animation: session-tree-status-pulse 1.6s ease-in-out infinite;
9350
- @@ -297,20 +278,23 @@ a.session-tree-main {
9351
  }
9352
  }
9353
 
@@ -9364,7 +9446,6 @@ index 86728a2..d1346be 100644
9364
  - font-size: 12px;
9365
  - text-overflow: ellipsis;
9366
  + font-size: 13px;
9367
- + letter-spacing: -0.005em;
9368
  white-space: nowrap;
9369
  + -webkit-mask-image: linear-gradient(to right, black calc(100% - 20px), transparent);
9370
  + mask-image: linear-gradient(to right, black calc(100% - 20px), transparent);
@@ -9376,7 +9457,7 @@ index 86728a2..d1346be 100644
9376
  }
9377
 
9378
  .session-tree-session.is-archived .session-tree-title {
9379
- @@ -321,6 +305,8 @@ a.session-tree-main {
9380
  from its source ("… · 3f9a"), never the suffix. */
9381
  .session-tree-title.has-fork-suffix {
9382
  display: flex;
@@ -9385,7 +9466,7 @@ index 86728a2..d1346be 100644
9385
  }
9386
 
9387
  .session-tree-title-base {
9388
- @@ -384,16 +370,23 @@ a.session-tree-main {
9389
  align-items: center;
9390
  }
9391
 
@@ -9413,7 +9494,7 @@ index 86728a2..d1346be 100644
9413
  color: var(--text-dim);
9414
  }
9415
 
9416
- @@ -408,11 +401,18 @@ a.session-tree-main {
9417
  background: var(--bg-hover);
9418
  }
9419
 
@@ -9435,7 +9516,7 @@ index 86728a2..d1346be 100644
9435
  }
9436
 
9437
  .session-tree-meta.is-running {
9438
- @@ -436,7 +436,10 @@ a.session-tree-main {
9439
  * :focus-within fallback in the @supports block after the group rules.
9440
  */
9441
  .session-tree-session:has(:focus-visible) .session-tree-action {
@@ -9447,7 +9528,7 @@ index 86728a2..d1346be 100644
9447
  }
9448
 
9449
  .session-tree-session:has(:focus-visible) .session-tree-meta {
9450
- @@ -454,7 +457,7 @@ a.session-tree-main {
9451
  padding: 0 8px;
9452
  box-sizing: border-box;
9453
  border: 1px solid var(--accent);
@@ -9456,7 +9537,7 @@ index 86728a2..d1346be 100644
9456
  outline: none;
9457
  background: var(--bg);
9458
  color: var(--text);
9459
- @@ -483,7 +486,7 @@ a.session-tree-main {
9460
  gap: 4px;
9461
  height: 24px;
9462
  padding: 0 10px;
@@ -9465,7 +9546,7 @@ index 86728a2..d1346be 100644
9465
  font-size: 12px;
9466
  white-space: nowrap;
9467
  }
9468
- @@ -518,8 +521,8 @@ a.session-tree-main {
9469
  right: var(--session-tree-inset-right);
9470
  left: var(--session-tree-inset-left);
9471
  gap: 2px;
@@ -9476,7 +9557,7 @@ index 86728a2..d1346be 100644
9476
  -webkit-user-select: none;
9477
  user-select: none;
9478
  -webkit-touch-callout: none;
9479
- @@ -548,7 +551,7 @@ a.session-tree-main {
9480
  position: absolute;
9481
  right: var(--session-tree-inset-right);
9482
  left: var(--session-tree-inset-left);
@@ -9485,7 +9566,7 @@ index 86728a2..d1346be 100644
9485
  background: color-mix(in srgb, var(--bg-panel) 55%, transparent);
9486
  pointer-events: none;
9487
  }
9488
- @@ -589,7 +592,7 @@ a.session-tree-main {
9489
  height: 22px;
9490
  padding: 0 10px;
9491
  border: 1px solid var(--border);
@@ -9494,7 +9575,7 @@ index 86728a2..d1346be 100644
9494
  background: var(--bg-panel);
9495
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
9496
  color: var(--text);
9497
- @@ -618,7 +621,7 @@ a.session-tree-main {
9498
  padding: 0 0 0 5px;
9499
  color: var(--text-muted);
9500
  font-size: 12px;
@@ -9503,7 +9584,7 @@ index 86728a2..d1346be 100644
9503
  text-align: left;
9504
  white-space: nowrap;
9505
  }
9506
- @@ -669,19 +672,27 @@ a.session-tree-main {
9507
  width: 6px;
9508
  height: 6px;
9509
  border-radius: 50%;
@@ -9534,7 +9615,7 @@ index 86728a2..d1346be 100644
9534
  }
9535
 
9536
  .session-tree-group:hover .session-tree-summary,
9537
- @@ -690,7 +701,10 @@ a.session-tree-main {
9538
  }
9539
 
9540
  .session-tree-group:has(:focus-visible) .session-tree-group-actions {
@@ -9546,7 +9627,7 @@ index 86728a2..d1346be 100644
9546
  }
9547
 
9548
  .session-tree-group:has(:focus-visible) .session-tree-summary {
9549
- @@ -698,9 +712,18 @@ a.session-tree-main {
9550
  }
9551
 
9552
  @supports not selector(:has(*)) {
@@ -9567,7 +9648,7 @@ index 86728a2..d1346be 100644
9567
  }
9568
 
9569
  .session-tree-session:focus-within .session-tree-meta,
9570
- @@ -726,7 +749,7 @@ a.session-tree-main {
9571
  width: 22px;
9572
  height: 22px;
9573
  padding: 0;
@@ -9576,7 +9657,7 @@ index 86728a2..d1346be 100644
9576
  color: var(--text-dim);
9577
  }
9578
 
9579
- @@ -787,7 +810,7 @@ a.session-tree-main {
9580
  gap: 3px;
9581
  min-width: 0;
9582
  padding: 0 4px 0 5px;
@@ -9585,7 +9666,7 @@ index 86728a2..d1346be 100644
9585
  color: var(--text-dim);
9586
  font-size: 12px;
9587
  text-align: left;
9588
- @@ -838,16 +861,21 @@ a.session-tree-main {
9589
 
9590
  /* Phone layout: 44px rows, a step larger type for titles and project names,
9591
  ⋯ and the group buttons always shown, no archive quick button. */
@@ -9609,7 +9690,7 @@ index 86728a2..d1346be 100644
9609
  }
9610
 
9611
  .session-tree.is-mobile .session-tree-session:hover .session-tree-meta {
9612
- @@ -859,7 +887,10 @@ a.session-tree-main {
9613
  }
9614
 
9615
  .session-tree.is-mobile .session-tree-group-actions {
@@ -9621,7 +9702,7 @@ index 86728a2..d1346be 100644
9621
  }
9622
 
9623
  .session-tree.is-mobile .session-tree-group .session-tree-summary {
9624
- @@ -884,7 +915,10 @@ a.session-tree-main {
9625
  /* Touch screens wider than a phone have no hover: keep ⋯ reachable. */
9626
  @media (hover: none) {
9627
  .session-tree-more-action {
@@ -9633,7 +9714,7 @@ index 86728a2..d1346be 100644
9633
  }
9634
  }
9635
 
9636
- @@ -942,9 +976,10 @@ a.session-tree-main {
9637
  display: flex;
9638
  flex: none;
9639
  align-items: stretch;
@@ -9645,7 +9726,7 @@ index 86728a2..d1346be 100644
9645
  }
9646
 
9647
  /* The tablist itself holds only the two tabs (ARIA allows nothing else in it). */
9648
- @@ -954,44 +989,30 @@ a.session-tree-main {
9649
  align-items: stretch;
9650
  }
9651
 
@@ -9697,7 +9778,7 @@ index 86728a2..d1346be 100644
9697
  .sidebar-tab-icon {
9698
  flex: none;
9699
  color: var(--text-dim);
9700
- @@ -1006,17 +1027,12 @@ a.session-tree-main {
9701
 
9702
  .sidebar-tab.is-selected,
9703
  .sidebar-search-toggle.is-active {
@@ -9717,7 +9798,7 @@ index 86728a2..d1346be 100644
9717
  }
9718
 
9719
  .sidebar-tab-count {
9720
- @@ -1036,27 +1052,37 @@ a.session-tree-main {
9721
  beside the two cells at the sidebar's 180px minimum, and at 11px so do
9722
  the versions with a digit to spare). */
9723
  .sidebar-brand {
@@ -9740,13 +9821,13 @@ index 86728a2..d1346be 100644
9740
  - font-family: var(--font-mono);
9741
  - font-size: 15px;
9742
  - font-weight: 700;
 
 
9743
  + color: var(--text-strong);
9744
  + font-family: var(--font-ui);
9745
  + font-size: 13px;
9746
  + font-weight: 600;
9747
  + font-variant-numeric: tabular-nums;
9748
- letter-spacing: -0.01em;
9749
- - line-height: normal;
9750
  + line-height: 1;
9751
  text-align: left;
9752
  white-space: nowrap;
@@ -9765,7 +9846,7 @@ index 86728a2..d1346be 100644
9765
  }
9766
 
9767
  .sidebar-brand:focus {
9768
- @@ -1064,8 +1090,10 @@ a.session-tree-main {
9769
  }
9770
 
9771
  .sidebar-brand:focus-visible {
@@ -9778,7 +9859,7 @@ index 86728a2..d1346be 100644
9778
  }
9779
 
9780
  .sidebar-header-spacer {
9781
- @@ -1078,7 +1106,7 @@ a.session-tree-main {
9782
  }
9783
 
9784
  .sidebar-search-toggle {
@@ -9787,7 +9868,7 @@ index 86728a2..d1346be 100644
9787
  padding: 0;
9788
  }
9789
 
9790
- @@ -1093,7 +1121,7 @@ a.session-tree-main {
9791
  label is still each one's name. */
9792
  .sidebar-header[data-fit="1"] .sidebar-new-button,
9793
  .sidebar-header[data-fit="2"] .sidebar-new-button {
@@ -9796,7 +9877,7 @@ index 86728a2..d1346be 100644
9796
  padding: 0;
9797
  }
9798
 
9799
- @@ -1105,6 +1133,11 @@ a.session-tree-main {
9800
  font-size: 11px;
9801
  }
9802
 
@@ -9808,7 +9889,7 @@ index 86728a2..d1346be 100644
9808
  .sidebar-header[data-fit="1"] .sidebar-new-label,
9809
  .sidebar-header[data-fit="2"] .sidebar-new-label,
9810
  .sidebar-header[data-fit="2"] .sidebar-tab-label {
9811
- @@ -1122,8 +1155,9 @@ a.session-tree-main {
9812
  .sidebar-archive-back:focus-visible,
9813
  .sidebar-tool-button:focus-visible,
9814
  .sidebar-files-section-toggle:focus-visible {
@@ -9820,7 +9901,7 @@ index 86728a2..d1346be 100644
9820
  }
9821
 
9822
  .sidebar-panel {
9823
- @@ -1202,7 +1236,7 @@ a.session-tree-main {
9824
  the root too), and its time 4px inside the row. The header row's keys and
9825
  the picker's boxes end there. */
9826
  .session-sidebar.is-files-below {
@@ -9829,7 +9910,7 @@ index 86728a2..d1346be 100644
9829
  }
9830
 
9831
  /* The files section's header row (files below the sessions only), one row
9832
- @@ -1246,7 +1280,7 @@ a.session-tree-main {
9833
  padding: 0 5px;
9834
  overflow: hidden;
9835
  border: 0;
@@ -9838,7 +9919,7 @@ index 86728a2..d1346be 100644
9838
  background: transparent;
9839
  color: var(--text-muted);
9840
  font: inherit;
9841
- @@ -1272,10 +1306,8 @@ a.session-tree-main {
9842
  .sidebar-files-section-label {
9843
  min-width: 0;
9844
  overflow: hidden;
@@ -9850,7 +9931,7 @@ index 86728a2..d1346be 100644
9850
  }
9851
 
9852
  /* The changed files' count, as the pinned section's (.session-tree-pinned-count). */
9853
- @@ -1294,7 +1326,7 @@ a.session-tree-main {
9854
  display: flex;
9855
  flex: 1000 0 calc(7 * 18px);
9856
  align-items: center;
@@ -9859,7 +9940,7 @@ index 86728a2..d1346be 100644
9859
  }
9860
 
9861
  /* The section's keys: the files tab's square keys, a step under the row's
9862
- @@ -1302,9 +1334,9 @@ a.session-tree-main {
9863
  .sidebar-files-keys .sidebar-tool-button {
9864
  flex: 1 1 0;
9865
  min-width: 18px;
@@ -9872,7 +9953,7 @@ index 86728a2..d1346be 100644
9872
  }
9873
 
9874
  /* Under that row (or the whole files tab): the head, then the tree filling
9875
- @@ -1317,31 +1349,7 @@ a.session-tree-main {
9876
  min-height: 0;
9877
  }
9878
 
@@ -9905,7 +9986,7 @@ index 86728a2..d1346be 100644
9905
  slides in over the tree, which stays mounted under it (hidden once the
9906
  archive is in), and away again. SessionTree fills each. */
9907
  .sidebar-sessions-views {
9908
- @@ -1413,7 +1421,7 @@ a.session-tree-main {
9909
  height: 26px;
9910
  padding: 0 6px;
9911
  border: 0;
@@ -9914,7 +9995,7 @@ index 86728a2..d1346be 100644
9914
  background: transparent;
9915
  color: var(--text-muted);
9916
  font-size: 12px;
9917
- @@ -1479,10 +1487,11 @@ a.session-tree-main {
9918
  six (the changes view is disabled, not gone, without changes), so nothing
9919
  moves as the count flips. At the sidebar's 180px minimum they give way
9920
  together, down to 21px wide, and the gap before the views closes. */
@@ -9927,7 +10008,7 @@ index 86728a2..d1346be 100644
9927
  margin-top: 4px;
9928
  }
9929
 
9930
- @@ -1492,17 +1501,18 @@ a.session-tree-main {
9931
  margin-left: auto;
9932
  }
9933
 
@@ -9951,7 +10032,7 @@ index 86728a2..d1346be 100644
9951
  background: transparent;
9952
  color: var(--text-dim);
9953
  cursor: pointer;
9954
- @@ -1510,8 +1520,8 @@ a.session-tree-main {
9955
  }
9956
 
9957
  .sidebar-tool-button:not(:disabled):hover {
@@ -9964,10 +10045,10 @@ index 86728a2..d1346be 100644
9964
  .sidebar-tool-button.is-active,
9965
  diff --git a/app/ui.css b/app/ui.css
9966
  new file mode 100644
9967
- index 0000000..3426491
9968
  --- /dev/null
9969
  +++ b/app/ui.css
9970
- @@ -0,0 +1,761 @@
9971
  +/* The shared components on Base UI (components/ui/*): menus, tooltips,
9972
  + popovers and the composer's suggestion list. Popups are portaled to <body>,
9973
  + so they stack by the positioner's z-index, above every pane. */
@@ -10238,7 +10319,6 @@ index 0000000..3426491
10238
  + background: color-mix(in srgb, var(--primary-contrast) 16%, transparent);
10239
  + font-family: inherit;
10240
  + font-size: 11px;
10241
- + letter-spacing: 0.04em;
10242
  +}
10243
  +
10244
  +/* ---------------------------------------------------------------- popovers */
@@ -20817,6 +20897,38 @@ index 25e4a63..171bf7c 100644
20817
  </div>
20818
  );
20819
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20820
  diff --git a/components/MessageView.test.mjs b/components/MessageView.test.mjs
20821
  index 515b033..12d546b 100644
20822
  --- a/components/MessageView.test.mjs
@@ -20856,7 +20968,7 @@ index 515b033..12d546b 100644
20856
 
20857
  test("collapses a displayed custom message from its header alone", () => {
20858
  diff --git a/components/MessageView.tsx b/components/MessageView.tsx
20859
- index f2ff1de..b18cfc6 100644
20860
  --- a/components/MessageView.tsx
20861
  +++ b/components/MessageView.tsx
20862
  @@ -5,13 +5,16 @@ import ReactMarkdown from "react-markdown";
@@ -21318,7 +21430,7 @@ index f2ff1de..b18cfc6 100644
21318
  }: {
21319
  message: AssistantMessage;
21320
  isStreaming?: boolean;
21321
- @@ -669,6 +589,8 @@ function AssistantMessageView({
21322
  onCompact?: () => void;
21323
  isCompacting?: boolean;
21324
  compactError?: string | null;
@@ -21327,7 +21439,14 @@ index f2ff1de..b18cfc6 100644
21327
  }) {
21328
  const { t } = useI18n();
21329
  const time = showTimestamp ? formatTime(message.timestamp) : null;
21330
- @@ -679,7 +601,6 @@ function AssistantMessageView({
 
 
 
 
 
 
 
21331
  const providerError = getAssistantErrorMessage(message, { isStreaming });
21332
  const truncated = isAssistantTruncated(message, { isStreaming });
21333
  const unansweredTruncation = truncated && !hasAssistantAnswer(message);
@@ -21335,7 +21454,22 @@ index f2ff1de..b18cfc6 100644
21335
  const [copied, setCopied] = useState(false);
21336
  const streamStartRef = useRef<StreamRateStart | null>(null);
21337
  const rateKey = streamRateKey(message);
21338
- @@ -803,123 +724,59 @@ function AssistantMessageView({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21339
 
21340
  if (blocks.length === 0 && !isStreaming && !providerError && !truncated) return null;
21341
 
@@ -21354,7 +21488,7 @@ index f2ff1de..b18cfc6 100644
21354
  - style={{ marginBottom: 16 }}
21355
  - onMouseEnter={() => setHovered(true)}
21356
  - onMouseLeave={() => setHovered(false)}
21357
- + className={`turn-reply${inSteps ? " is-step" : ""}`}
21358
  >
21359
  - {/* Model label */}
21360
  - <div
@@ -21484,7 +21618,7 @@ index f2ff1de..b18cfc6 100644
21484
  )}
21485
  </div>
21486
  )}
21487
- @@ -928,51 +785,28 @@ function AssistantMessageView({
21488
  <TurnWrittenFiles files={writtenFiles} onOpenFile={onOpenFile} />
21489
  )}
21490
 
@@ -21497,14 +21631,8 @@ index f2ff1de..b18cfc6 100644
21497
  - </div>
21498
  - )}
21499
  - {textContent && !isStreaming && (
21500
- + {/* Only an answer has an actions row: a step that only called tools has nothing to copy. */}
21501
- + {!inSteps && !isStreaming && textContent && (
21502
- + <div className="message-actions">
21503
- <button
21504
- + type="button"
21505
- + className="ui-btn ui-btn--xs ui-btn--icon"
21506
- + data-done={copied || undefined}
21507
- onClick={copyContent}
21508
  - title={t("i18n.copyMessage")}
21509
  - style={{
21510
  - display: "flex", alignItems: "center", gap: 4,
@@ -21521,9 +21649,7 @@ index f2ff1de..b18cfc6 100644
21521
  - }}
21522
  - onMouseEnter={(e) => { if (!copied) e.currentTarget.style.color = "var(--accent)"; }}
21523
  - onMouseLeave={(e) => { if (!copied) e.currentTarget.style.color = "var(--text-dim)"; }}
21524
- + title={t("i18n.copyMessage")}
21525
- + aria-label={copied ? t("i18n.copied") : t("i18n.copyMessage")}
21526
- >
21527
  - {copied ? (
21528
  - <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
21529
  - <polyline points="20 6 9 17 4 12" />
@@ -21533,20 +21659,32 @@ index f2ff1de..b18cfc6 100644
21533
  - <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
21534
  - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
21535
  - </svg>
21536
- - )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21537
  - {copied ? t("i18n.copied") : t("i18n.copy")}
21538
- + <CarbonIcon icon={copied ? Carbon.Checkmark : Carbon.Copy} size={14} />
21539
- </button>
21540
  - )}
21541
  - {time && !isStreaming && (
21542
  - <span style={{ fontSize: 10, color: "var(--text-dim)", marginLeft: "auto" }}>{time}</span>
21543
  - )}
21544
  - </div>
21545
- + <div className="message-actions-reveal">
21546
- + {modelName && <span className="message-meta">{modelName}</span>}
21547
- + {shownUsage && formatUsageShort(shownUsage) && (
21548
- + <span className="message-meta" title={formatUsage(shownUsage)}>{formatUsageShort(shownUsage)}</span>
21549
- + )}
21550
  + {time && <span className="message-meta">{time}</span>}
21551
  + </div>
21552
  + </div>
@@ -21554,7 +21692,7 @@ index f2ff1de..b18cfc6 100644
21554
  </div>
21555
  );
21556
  }
21557
- @@ -988,7 +822,7 @@ function BlockView({ block, searchTarget, toolResults, isStreaming, streamingDur
21558
  const tc = block as ToolCallContent;
21559
  const result = toolResults?.get(tc.toolCallId);
21560
  const duration = toolCallDurations?.get(tc.toolCallId);
@@ -21563,7 +21701,7 @@ index f2ff1de..b18cfc6 100644
21563
  }
21564
  return null;
21565
  }
21566
- @@ -1052,62 +886,34 @@ export function ThinkingBlock({ block, duration, sessionId, entryId, blockIndex
21567
  }, [expanded, block.deferred, content, sessionId, entryId, blockIndex]);
21568
 
21569
  return (
@@ -21650,7 +21788,7 @@ index f2ff1de..b18cfc6 100644
21650
  </div>
21651
  );
21652
  }
21653
- @@ -1137,7 +943,7 @@ export function formatToolDuration(ms: number): string {
21654
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
21655
  }
21656
 
@@ -21659,7 +21797,7 @@ index f2ff1de..b18cfc6 100644
21659
  const { t } = useI18n();
21660
  const [expanded, setExpanded] = useState(() => isToolCallExpanded(block.toolCallId));
21661
  const toggleExpanded = () => {
21662
- @@ -1146,6 +952,7 @@ function ToolCallBlock({ block, result, duration, onOpenSubagent }: { block: Too
21663
  setExpanded(next);
21664
  };
21665
  const inputStr = getWrittenFileText(block) ?? getToolCallInputText(block);
@@ -21667,7 +21805,7 @@ index f2ff1de..b18cfc6 100644
21667
  const isStreamingInput = block.rawInput !== undefined;
21668
  const isEditTool = isEditToolName(block.toolName);
21669
  const resultDiff = result && !result.isError ? getResultDiff(result) : null;
21670
- @@ -1175,137 +982,154 @@ function ToolCallBlock({ block, result, duration, onOpenSubagent }: { block: Too
21671
  || (isApplyPatchToolName(block.toolName) && applyPatchResultHasFailures(result?.details));
21672
  const subagent = isSubagentToolDetails(result?.details) ? result.details : null;
21673
  const codemodeCallCount = codemode ? codemode.calls.length + codemode.omitted : 0;
@@ -21916,7 +22054,7 @@ index f2ff1de..b18cfc6 100644
21916
  interface ResultDiff {
21917
  text: string;
21918
  }
21919
- @@ -1316,7 +1140,6 @@ function PairedDiffResult({ diff }: {
21920
  return (
21921
  <div
21922
  style={{
@@ -21924,7 +22062,7 @@ index f2ff1de..b18cfc6 100644
21924
  background: "var(--bg)",
21925
  }}
21926
  >
21927
- @@ -1325,148 +1148,16 @@ function PairedDiffResult({ diff }: {
21928
  );
21929
  }
21930
 
@@ -22080,7 +22218,7 @@ index f2ff1de..b18cfc6 100644
22080
  }
22081
 
22082
  function PatchTextView({ text }: { text: string }) {
22083
- @@ -1483,12 +1174,12 @@ function PatchTextView({ text }: { text: string }) {
22084
  const bg =
22085
  kind === "added" ? "rgba(34,197,94,0.12)" :
22086
  kind === "removed" ? "rgba(248,113,113,0.13)" :
@@ -22095,7 +22233,7 @@ index f2ff1de..b18cfc6 100644
22095
  "var(--text)";
22096
 
22097
  return (
22098
- @@ -1502,7 +1193,7 @@ function PatchTextView({ text }: { text: string }) {
22099
  : kind === "removed"
22100
  ? "3px solid #f87171"
22101
  : kind === "hunk"
@@ -22104,7 +22242,7 @@ index f2ff1de..b18cfc6 100644
22104
  : "3px solid transparent",
22105
  }}
22106
  >
22107
- @@ -1578,16 +1269,16 @@ function isRecord(value: unknown): value is Record<string, unknown> {
22108
  return typeof value === "object" && value !== null && !Array.isArray(value);
22109
  }
22110
 
@@ -22123,7 +22261,7 @@ index f2ff1de..b18cfc6 100644
22123
  }}
22124
  >
22125
  {images.map((image, index) => {
22126
- @@ -1606,9 +1297,10 @@ function ResultImages({ images, isError }: { images: ImageContent[]; isError: bo
22127
  loading="lazy"
22128
  style={{
22129
  display: "block",
@@ -22137,7 +22275,7 @@ index f2ff1de..b18cfc6 100644
22137
  objectFit: "contain",
22138
  border: "1px solid var(--border)",
22139
  }}
22140
- @@ -1620,6 +1312,67 @@ function ResultImages({ images, isError }: { images: ImageContent[]; isError: bo
22141
  );
22142
  }
22143
 
@@ -22205,7 +22343,7 @@ index f2ff1de..b18cfc6 100644
22205
  function PairedResult({ text, isEmpty, isError }: {
22206
  text: string;
22207
  isEmpty: boolean;
22208
- @@ -1629,8 +1382,7 @@ function PairedResult({ text, isEmpty, isError }: {
22209
  return (
22210
  <div
22211
  style={{
@@ -22215,7 +22353,7 @@ index f2ff1de..b18cfc6 100644
22215
  }}
22216
  >
22217
  <pre
22218
- @@ -1668,7 +1420,7 @@ function CompactionMessageView({ message }: { message: CustomMessage }) {
22219
  <div
22220
  style={{
22221
  border: "1px solid var(--border)",
@@ -22224,7 +22362,7 @@ index f2ff1de..b18cfc6 100644
22224
  overflow: "hidden",
22225
  background: "var(--bg)",
22226
  }}
22227
- @@ -1710,9 +1462,7 @@ function CompactionMessageView({ message }: { message: CustomMessage }) {
22228
  textAlign: "left",
22229
  }}
22230
  >
@@ -22235,7 +22373,7 @@ index f2ff1de..b18cfc6 100644
22235
  <span style={{ minWidth: 0 }}>
22236
  <span style={{ display: "block", fontSize: "calc(15px + var(--chat-font-size-offset, 0px))", fontWeight: 700, lineHeight: 1.35 }}>
22237
  {t("i18n.conversationCompacted")}
22238
- @@ -1806,7 +1556,7 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
22239
  <div
22240
  style={{
22241
  border: "1px solid var(--border)",
@@ -22244,7 +22382,7 @@ index f2ff1de..b18cfc6 100644
22244
  overflow: "hidden",
22245
  background: "var(--bg)",
22246
  }}
22247
- @@ -1852,9 +1602,7 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
22248
  </span>
22249
  )}
22250
  {time && <span style={{ marginLeft: "auto", color: "var(--text-dim)", fontSize: 10, flexShrink: 0 }}>{time}</span>}
@@ -22255,7 +22393,7 @@ index f2ff1de..b18cfc6 100644
22255
  </button>
22256
  </div>
22257
 
22258
- @@ -1872,7 +1620,7 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
22259
  <img
22260
  src={src}
22261
  alt=""
@@ -22264,7 +22402,7 @@ index f2ff1de..b18cfc6 100644
22264
  />
22265
  </ImagePreview>
22266
  );
22267
- @@ -1999,6 +1747,32 @@ function getWrittenFileText(block: ToolCallContent): string | null {
22268
  return Object.keys(block.input).every((key) => WRITE_VIEW_KEYS.has(key)) ? content : null;
22269
  }
22270
 
@@ -22297,7 +22435,7 @@ index f2ff1de..b18cfc6 100644
22297
  function formatCustomType(type: string): string {
22298
  return type || "extension";
22299
  }
22300
- @@ -2021,13 +1795,16 @@ function getToolPreview(block: ToolCallContent): string {
22301
  return String(first).slice(0, 120);
22302
  }
22303
 
@@ -22557,7 +22695,7 @@ index ba912ee..0283a47 100644
22557
  onSelect={() => choose(option)}
22558
  star={onSetDefault ? {
22559
  diff --git a/components/ModelsConfig.tsx b/components/ModelsConfig.tsx
22560
- index 9f6b556..c1f7dd6 100644
22561
  --- a/components/ModelsConfig.tsx
22562
  +++ b/components/ModelsConfig.tsx
22563
  @@ -54,6 +54,8 @@ import { providerBadgeLabel } from "./enabled-models-helpers";
@@ -22782,7 +22920,7 @@ index 9f6b556..c1f7dd6 100644
22782
  Open the verification page and enter this code:
22783
  </p>
22784
  - <div style={{ padding: "8px 10px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text)", fontSize: 16, fontWeight: 700, fontFamily: "var(--font-mono)", letterSpacing: 0 }}>
22785
- + <div style={{ padding: "8px 10px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: "var(--radius-sm)", color: "var(--text)", fontSize: 16, fontWeight: 700, fontFamily: "var(--font-mono)", letterSpacing: 0 }}>
22786
  {loginState.userCode}
22787
  </div>
22788
  <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
@@ -30943,7 +31081,7 @@ index 25796d6..51c3f1c 100644
30943
  - **Notices.** MCP off, with its reason. Otherwise `hostInactive`: an open session whose `/mcp` is another extension's, naming its folder and that extension (only a session can tell). A project folder the route refuses (`cwd-*`) does not hide the global servers: `loadMcpOverview()` loads again without the cwd and the Project group says "Not listed" with the reason. Trust is reported only when the project has a `.pi/mcp.json` or a problem with one: untrusted (`ConfigTrustNotice`), an inherited `false` naming its folder, and inherited trust as "Trusted through <path>", the only place that trust is visible. A folder that requires no trust gets no trust notice unless a decision marks it untrusted: a dangling `.pi/mcp.json` link is listed (`link-dangling`), but the SDK's `existsSync` finds nothing to trust, so Trust would only meet `trust-not-required`. File problems go to the footer (`ConfigFooterStatus`). A load ends after `MCP_OVERVIEW_TIMEOUT_MS` (15 s, both requests together), since Refresh is disabled while one runs.
30944
  - **The detail pane shows only what GET sends:** the masked command line (`mcpServerTarget()`), working directory, env and header **names** (each only when set), the `!command` fields and host variables (left out for a refused entry, which never connects; kept for a disabled one), the Sign-in row (**Settings › MCP sign-in** in [mcp-test-sign-in.md](mcp-test-sign-in.md)), the exposure, the file. The Code mode pane holds the choice, the sandbox and `-builtin:codemode`.
30945
  diff --git a/docs/agents/sessions.md b/docs/agents/sessions.md
30946
- index 42e1816..5869597 100644
30947
  --- a/docs/agents/sessions.md
30948
  +++ b/docs/agents/sessions.md
30949
  @@ -38,7 +38,7 @@ On mount `useAgentSession` loads the history, then `GET /api/sessions/[id]/state
@@ -30960,7 +31098,7 @@ index 42e1816..5869597 100644
30960
  - Spacing: groups sit right on each other, open or folded (collapsed ones at a 28px pitch); a gap between two of them only pushed the next header away. An 8px spacer row parts the pinned section from the groups (`pinned-spacer`) and the groups from the footer (`footer-spacer`, only when there are groups; a pinned section with no group has the one spacer before the footer).
30961
  - Heights (`SIDEBAR_ROW_HEIGHTS`) keep one rhythm: session rows, every header (a project's, the pinned section's, the archive's projects), the empty group's note and the footer's two links are 28px (44px on a phone, a finger's minimum, headers included); only "show more" is a step shorter (26px, 36px). Every control in a row fits it with 2px to spare (⋯ and archive, the rename field and a delete confirmation's buttons 24px, a header's + and ⋯ 22px; 32px on a phone; `SessionTree.test.mjs` checks it), and the drag ghost (22px) stays shorter than a header.
30962
  -- Rows: nothing before the title; the right column shows the awaiting-input dot (amber, breathing), else the running spinner, else the unread dot, else the short time (a pinned row's project, an archived row's archive time). A running row keeps its spinner beside ⋯ on hover. Hover and selection are a rounded box inset from both edges; titles are 12px `--text` (the file tree's size), the selected one 500, and the selected row (in the archive view too) has a 3px accent bar on its box's left edge, 7px clear of its top and bottom (a 14px bar; 12px on a phone, 20px), a `::before` of the absolutely positioned row, so its rounded corners never clip it; a delete confirmation's red frame has none. A worktree's branch is meta in the rows' own type (11px dim, a 10px `BranchIcon` kept inline so the ellipsis still cuts the name; the tooltip keeps `⑂`), as a pinned row's project is (no code type). A classic scrollbar takes width on the right only (Chromium's and Safari's styled one is 10px, 6px on a coarse pointer; an overlay one none): `SessionTree` measures it into `--session-tree-scrollbar`, and the right inset shrinks by it, so rows end 6px in or on the gutter's edge, whichever is further in, while the left inset stays 6px (moving it too put the tree's two lines 4px right of everything else's in Chromium). It also puts the width on the sidebar's root (only a tree with a box: not one under the archive view or in a hidden tab), where the files below the sessions read it for their right edge. A row's time (or spinner, dot) ends 4px inside its box, where a folded group's summary ends: neither the row nor its always-rendered actions span has a gap, each shown action brings its own 2px (`margin-left`), so the hidden ones leave the time on the edge. The list keeps the scrollbar's room even when everything fits (`scrollbar-gutter: stable`; `overflow-y: scroll` in Safari before 18.2), so rows never narrow when it starts to scroll.
30963
- +- Rows: nothing before the title; the right column shows the awaiting-input dot (amber, breathing), else the running spinner, else the unread dot, else the short time (a pinned row's project, an archived row's archive time). A running row keeps its spinner beside ⋯ on hover. A live row has no hover Archive button (Archive is in its ⋯ menu); the archive view's rows keep Restore one click away. The toolbar's search opens the command bar (`components/SearchCommandBar.tsx`, also ⌘K / Ctrl+K outside a terminal): recent top-level conversations until something is typed, then `/api/sessions/search` matches with their text; arrows move, Enter opens, Esc closes. Hover and selection are a rounded box inset from both edges; titles are 12px `--text` (the file tree's size), the selected one 500, and the selected row (in the archive view too) has a 3px accent bar on its box's left edge, 7px clear of its top and bottom (a 14px bar; 12px on a phone, 20px), a `::before` of the absolutely positioned row, so its rounded corners never clip it; a delete confirmation's red frame has none. A worktree's branch is meta in the rows' own type (11px dim, a 10px `BranchIcon` kept inline so the ellipsis still cuts the name; the tooltip keeps `⑂`), as a pinned row's project is (no code type). A classic scrollbar takes width on the right only (Chromium's and Safari's styled one is 10px, 6px on a coarse pointer; an overlay one none): `SessionTree` measures it into `--session-tree-scrollbar`, and the right inset shrinks by it, so rows end 6px in or on the gutter's edge, whichever is further in, while the left inset stays 6px (moving it too put the tree's two lines 4px right of everything else's in Chromium). It also puts the width on the sidebar's root (only a tree with a box: not one under the archive view or in a hidden tab), where the files below the sessions read it for their right edge. A row's time (or spinner, dot) ends 4px inside its box, where a folded group's summary ends: neither the row nor its always-rendered actions span has a gap, each shown action brings its own 2px (`margin-left`), so the hidden ones leave the time on the edge. The list keeps the scrollbar's room even when everything fits (`scrollbar-gutter: stable`; `overflow-y: scroll` in Safari before 18.2), so rows never narrow when it starts to scroll.
30964
  - Row keys are unique and stable (`session:<context>:<rootId>`, `group:<key>`, `more:<key>`, `empty:<key>`, and fixed keys for the pinned header, the two spacers (`pinned-spacer`, `footer-spacer`) and the footer); focus tracking and `keepMounted` go by them.
30965
  - Menus (`SidebarMenu`) and the toast live in `SessionSidebar`, above the virtualized rows, so a row scrolled away keeps its open menu (the project/worktree picker of the files tab and of the composer's bar uses `SidebarMenu` for its own two menus); the row with focus, a menu, a rename or a delete confirmation stays mounted (`keepMounted`). Menus are portaled to `document.body`: the mobile drawer's `transform` would pin a `position: fixed` menu to the drawer. The archive view is a second tree; the main one stays mounted underneath, the two in one grid cell (`.sidebar-sessions-views`, `grid-area: 1 / 1`; the archive opaque in `--bg-panel` and stacked above the tree for good, `position: relative; z-index: 1`: the tree's rows are positioned and paint over an unpositioned view, which only its animation lifted, so the frame its entrance ended in flashed the tree). Going in and out moves, in both layouts and on phones (`useSidebarMotion()`, `archiveViewState()` in `hooks/useSidebarMotion.ts`; `setArchiveView()` starts it on a change only, and View, Back and a fork's open all go through it): in, the archive slides from 14px right and fades in over the tree (`.is-entering`, 180ms ease-out), the tree `inert` under it until the view is in, then `hidden` as before; out, the tree is back at once and the archive slides and fades out to the right over it (`.is-leaving`, 180ms ease-in, `forwards`), still mounted with its rows (`archiveRows` is built while `archiveMounted`, else it would leave empty), `inert` and `pointer-events: none`, so nothing in it is clicked twice and a click lands on the tree, until its own `animationend` (not one bubbling from a row) unmounts it. A phase that gets no end event (a sidebar not shown: a hidden tab, a closed drawer) ends 300 ms after its start (`SIDEBAR_MOTION_MS` + `SIDEBAR_MOTION_GRACE_MS`). Under `prefers-reduced-motion` no phase starts and the CSS has no animation: the views swap at once, as before.
30966
  - Focus: a menu returns it to the opener it was given when it opened (the parent drops `returnFocusTo` with the menu); a row's ⋯ or a group's buttons are `display: none` by then unless hovered, selected or focused, so `menuFocusReturnTarget()` falls back to the row's first focusable control (its main link or transient-session button, a group's toggle). Controls that go away under the keyboard hand focus on: the archive's Back button when it opens, the footer link (or the selected tab, or with no tabs the toolbar row's search) after Back, the family's row after a toast's Undo or a delete Cancel (`focusIfLost()`, only when focus fell to `<body>`). The archive's two moves make the view they leave `inert` before it is hidden or gone, and Chromium blurs an inert element only at its next frame, after the effect that moves focus has run: `focusIfHidden()` counts focus on something without a box or inside `[inert]` as lost too.
@@ -43686,7 +43824,7 @@ index 0000000..7b6ea92
43686
  + </g>
43687
  +</svg>
43688
  diff --git a/public/offline.html b/public/offline.html
43689
- index 0dac4b2..e07421e 100644
43690
  --- a/public/offline.html
43691
  +++ b/public/offline.html
43692
  @@ -3,14 +3,38 @@
@@ -43745,7 +43883,7 @@ index 0dac4b2..e07421e 100644
43745
  }
43746
 
43747
  main {
43748
- @@ -30,53 +58,66 @@
43749
  text-align: center;
43750
  }
43751
 
@@ -43768,7 +43906,6 @@ index 0dac4b2..e07421e 100644
43768
  + margin: 0 0 6px;
43769
  + font-size: 18px;
43770
  + font-weight: 600;
43771
- + letter-spacing: -0.01em;
43772
  + line-height: 1.3;
43773
  }
43774
 
 
3977
  +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
3978
  +OTHER DEALINGS IN THE FONT SOFTWARE.
3979
  diff --git a/app/globals.css b/app/globals.css
3980
+ index d6b5535..14749e0 100644
3981
  --- a/app/globals.css
3982
  +++ b/app/globals.css
3983
  @@ -24,46 +24,109 @@
 
4235
  }
4236
 
4237
  /* Theme toggle: circular wipe via View Transitions API.
4238
+ @@ -218,16 +301,15 @@ body {
4239
  margin-bottom: 22px;
4240
  }
4241
 
 
4247
  }
4248
 
4249
  .web-login-brand h1 {
4250
+ margin: 0 0 3px;
4251
+ color: var(--text);
4252
+ font-size: 22px;
4253
+ - letter-spacing: 0;
4254
+ }
4255
+
4256
+ .web-login-brand p {
4257
+ @@ -247,15 +329,15 @@ body {
4258
  min-width: 0;
4259
  padding: 10px 10px 10px 14px;
4260
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
 
4273
  }
4274
 
4275
  .web-login-label {
4276
+ @@ -307,14 +389,13 @@ html.dark .web-login-error {
4277
  min-height: 32px;
4278
  padding: 7px 14px;
4279
  border: none;
 
4282
  background: var(--accent);
4283
  color: var(--accent-contrast);
4284
  cursor: pointer;
4285
+ font: inherit;
4286
+ font-size: 13px;
4287
+ font-weight: 600;
4288
+ - letter-spacing: 0;
4289
+ }
4290
+
4291
+ .web-login-form button:not(:disabled):hover {
4292
+ @@ -322,7 +403,7 @@ html.dark .web-login-error {
4293
  }
4294
 
4295
  .web-login-form button:focus-visible {
 
4298
  outline-offset: 2px;
4299
  }
4300
 
4301
+ @@ -332,110 +413,139 @@ html.dark .web-login-error {
4302
  cursor: not-allowed;
4303
  }
4304
 
 
4394
  overflow: hidden;
4395
  - text-overflow: ellipsis;
4396
  - white-space: nowrap;
4397
+ }
4398
  -
4399
  -.terminal-status-dot {
4400
  - width: 7px;
 
4421
  - color: #9ca3af;
4422
  - cursor: pointer;
4423
  - font: inherit;
4424
+ -}
4425
  -
4426
  -.terminal-panel-exit {
4427
  - padding: 7px 12px;
 
4519
  }
4520
 
4521
  .terminal-xterm:has(+ .terminal-extra-keys) {
4522
+ @@ -451,7 +561,7 @@ html[data-keyboard-open] .terminal-extra-keys {
4523
  padding: 0;
4524
  border: none;
4525
  background: transparent;
 
4528
  font-family: var(--font-mono);
4529
  font-size: 12px;
4530
  touch-action: manipulation;
4531
+ @@ -462,7 +572,7 @@ html[data-keyboard-open] .terminal-extra-keys {
4532
  }
4533
 
4534
  .terminal-extra-keys button:active {
 
4537
  }
4538
 
4539
  .terminal-extra-keys .is-rotated {
4540
+ @@ -471,17 +581,10 @@ html[data-keyboard-open] .terminal-extra-keys {
4541
  }
4542
 
4543
  .terminal-extra-keys button.is-active {
 
4558
 
4559
  /*
4560
  * Chromium-only scrollbars. Firefox renders its native ones, which the standard
4561
+ @@ -502,7 +605,7 @@ html[data-keyboard-open] .terminal-extra-keys {
4562
  background-color: color-mix(in srgb, var(--text-dim) 45%, transparent);
4563
  background-clip: padding-box;
4564
  border: 2px solid transparent;
 
4567
  }
4568
  /* Only ever set background-color here: the `background` shorthand resets
4569
  background-clip and would snap the thumb to the full 10px on hover. */
4570
+ @@ -538,21 +641,10 @@ html[data-keyboard-open] .terminal-extra-keys {
4571
  }
4572
  ::-webkit-scrollbar-thumb {
4573
  border-width: 1px;
 
4590
 
4591
 
4592
  .extension-status-shelf {
4593
+ @@ -686,8 +778,9 @@ button.extension-widget-trigger:hover,
4594
  }
4595
 
4596
  button.extension-widget-trigger:focus-visible {
 
4602
  }
4603
 
4604
  .extension-widget-placement {
4605
+ @@ -703,9 +796,9 @@ button.extension-widget-trigger:focus-visible {
4606
 
4607
  .extension-widget-placement-icon {
4608
  display: block;
 
4615
  fill: currentColor;
4616
  }
4617
 
4618
+ @@ -803,8 +896,9 @@ button.extension-widget-trigger:focus-visible {
4619
  }
4620
 
4621
  .extension-status-command:focus-visible {
 
4627
  }
4628
 
4629
  .extension-status-command:disabled {
4630
+ @@ -826,8 +920,8 @@ button.extension-widget-trigger:focus-visible {
4631
  }
4632
 
4633
  :root {
 
4638
  --font-ui: var(--font-ui-default);
4639
  --font-mono: var(--font-mono-default);
4640
  --font-ui-weight: 400;
4641
+ @@ -866,7 +960,6 @@ pre, code {
4642
+ margin: 10px 0 5px;
4643
+ color: var(--text);
4644
+ line-height: 1.35;
4645
+ - letter-spacing: 0;
4646
+ }
4647
+ .markdown-body h1 { font-size: 1.16em; }
4648
+ .markdown-body h2 { font-size: 1.08em; }
4649
+ @@ -883,11 +976,11 @@ pre, code {
4650
  .markdown-body ol > li { list-style-type: decimal; }
4651
  .markdown-body li > p { margin: 0; }
4652
  .markdown-body li::marker {
 
4661
  font-weight: 700;
4662
  }
4663
  .markdown-body em { color: var(--text-muted); }
4664
+ @@ -896,24 +989,23 @@ pre, code {
4665
  text-decoration: underline;
4666
  text-decoration-thickness: 1px;
4667
  text-underline-offset: 3px;
 
4690
  }
4691
  .markdown-body .contains-task-list {
4692
  padding-left: 0;
4693
+ @@ -935,7 +1027,7 @@ pre, code {
4694
  height: 14px;
4695
  margin: 0;
4696
  border: 1px solid var(--border);
 
4699
  background: var(--bg);
4700
  display: inline-grid;
4701
  place-content: center;
4702
+ @@ -967,8 +1059,8 @@ pre, code {
4703
  margin: 8px 0;
4704
  overflow-x: auto;
4705
  overflow-y: hidden;
 
4710
  }
4711
  .markdown-body table {
4712
  border-collapse: separate;
4713
+ @@ -1088,7 +1180,7 @@ pre, code {
4714
  padding: 7px 8px;
4715
  overflow: auto;
4716
  border: 1px solid var(--border);
 
4719
  background: var(--bg-panel);
4720
  font-family: var(--font-mono);
4721
  font-size: 11px;
4722
+ @@ -1108,16 +1200,13 @@ pre, code {
4723
  min-width: 0;
4724
  max-width: 100%;
4725
  margin: 6px 0;
 
4739
  color: var(--text-dim);
4740
  font-size: 11px;
4741
  display: flex;
4742
+ @@ -1137,7 +1226,7 @@ pre, code {
4743
  }
4744
  .markdown-code-action {
4745
  border: 1px solid var(--border);
 
4748
  padding: 2px 7px;
4749
  background: var(--bg);
4750
  color: var(--text-muted);
4751
+ @@ -1191,8 +1280,9 @@ pre, code {
4752
  opacity: 0.85;
4753
  }
4754
  .mermaid-preview-button:focus-visible {
 
4760
  }
4761
 
4762
  .image-preview-dialog {
4763
+ @@ -1228,7 +1318,7 @@ pre, code {
4764
  max-width: 100%;
4765
  max-height: 100%;
4766
  object-fit: contain;
 
4769
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
4770
  }
4771
 
4772
+ @@ -1244,7 +1334,7 @@ pre, code {
4773
  height: 36px;
4774
  padding: 0;
4775
  border: 1px solid var(--border);
 
4778
  background: var(--bg-panel);
4779
  color: var(--text-muted);
4780
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
4781
+ @@ -1259,7 +1349,7 @@ pre, code {
4782
  }
4783
 
4784
  .image-preview-close:focus-visible {
 
4787
  outline-offset: 2px;
4788
  }
4789
 
4790
+ @@ -1320,7 +1410,7 @@ pre, code {
4791
  height: 24px;
4792
  overflow: hidden;
4793
  border: 1px solid var(--border);
 
4796
  background: var(--bg);
4797
  }
4798
  .mermaid-zoom-stepper button,
4799
+ @@ -1347,7 +1437,7 @@ pre, code {
4800
  .mermaid-zoom-icon-button {
4801
  flex: 0 0 24px;
4802
  border: 1px solid var(--border);
 
4805
  }
4806
  .mermaid-zoom-stepper button:hover:not(:disabled),
4807
  .mermaid-zoom-icon-button:hover {
4808
+ @@ -1360,8 +1450,9 @@ pre, code {
4809
  }
4810
  .mermaid-zoom-stepper button:focus-visible,
4811
  .mermaid-zoom-icon-button:focus-visible {
 
4817
  }
4818
  .mermaid-zoom-value {
4819
  min-width: 48px;
4820
+ @@ -1384,7 +1475,7 @@ pre, code {
4821
  .mermaid-zoom-canvas {
4822
  padding: 12px;
4823
  border: 1px solid var(--border);
 
4826
  background: var(--bg-panel);
4827
  margin: 0 auto;
4828
  transition: width 0.15s ease;
4829
+ @@ -1460,7 +1551,7 @@ pre, code {
4830
  height: 24px;
4831
  overflow: hidden;
4832
  border: 1px solid var(--border);
 
4835
  }
4836
 
4837
  .file-viewer-mode-button {
4838
+ @@ -1471,7 +1562,6 @@ pre, code {
4839
+ font: inherit;
4840
+ font-size: 11px;
4841
+ font-weight: 500;
4842
+ - letter-spacing: 0;
4843
+ cursor: pointer;
4844
+ white-space: nowrap;
4845
+ }
4846
+ @@ -1503,12 +1593,11 @@ pre, code {
4847
  height: 32px;
4848
  padding: 0;
4849
  box-sizing: border-box;
 
4858
  opacity: 0;
4859
  visibility: hidden;
4860
  pointer-events: none;
4861
+ @@ -1542,7 +1631,7 @@ pre, code {
4862
  }
4863
 
4864
  .chat-scroll-to-bottom:focus-visible {
 
4867
  outline-offset: 2px;
4868
  }
4869
 
4870
+ @@ -1566,11 +1655,13 @@ pre, code {
4871
  }
4872
  }
4873
 
 
4885
  margin: 0 auto;
4886
  }
4887
 
4888
+ @@ -1584,22 +1675,20 @@ pre, code {
4889
 
4890
  .new-session-logo {
4891
  flex: none;
 
4910
  + color: var(--text-strong);
4911
  + font-size: 20px;
4912
  + font-weight: 600;
 
4913
  line-height: 1.15;
4914
  white-space: nowrap;
4915
  }
4916
+ @@ -1613,7 +1702,8 @@ pre, code {
4917
  gap: 6px;
4918
  min-width: 0;
4919
  color: var(--text-dim);
 
4923
  line-height: 1.4;
4924
  white-space: nowrap;
4925
  }
4926
+ @@ -1638,7 +1728,7 @@ pre, code {
4927
  gap: 3px;
4928
  margin: -3px -4px;
4929
  padding: 3px 4px;
 
4932
  color: var(--accent);
4933
  font-weight: 600;
4934
  text-decoration: none;
4935
+ @@ -1652,38 +1742,27 @@ pre, code {
4936
 
4937
  /* The ring inside the box, clear of the "→" and "·" beside it. */
4938
  .new-session-update:focus-visible {
 
4977
  }
4978
  }
4979
 
4980
+ @@ -1700,8 +1779,8 @@ pre, code {
4981
  tab's stacked boxes, the row below the sessions), as the sidebar drew it
4982
  on main: two grey boxes in the header buttons' material, 11px, the
4983
  project's whole path and the worktree's branch in code type, cut at their
 
4988
  .project-picker {
4989
  display: flex;
4990
  align-items: center;
4991
+ @@ -1722,52 +1801,14 @@ pre, code {
4992
  min-width: 0;
4993
  height: 29px;
4994
  padding: 0 10px;
 
5044
  }
5045
 
5046
  /* The project's name (its display name, else its folder's), cut at its
5047
+ @@ -1781,8 +1822,9 @@ pre, code {
5048
  max-width: 28ch;
5049
  overflow: hidden;
5050
  color: var(--text-dim);
 
5055
  }
5056
 
5057
  .project-picker-parent {
5058
+ @@ -1800,10 +1842,6 @@ pre, code {
5059
  text-overflow: ellipsis;
5060
  }
5061
 
 
5066
  .project-picker-button:hover,
5067
  .project-picker-button[aria-expanded="true"] {
5068
  background: var(--bg-selected);
5069
+ @@ -1811,8 +1849,9 @@ pre, code {
5070
  }
5071
 
5072
  .project-picker-button:focus-visible {
 
5078
  }
5079
 
5080
  .project-picker-icon,
5081
+ @@ -1851,7 +1890,8 @@ pre, code {
5082
  path's color and code type, cut at its end. */
5083
  .project-picker-label.is-alias {
5084
  color: var(--text);
 
5088
  }
5089
 
5090
  /* A path or a branch, cut at its left (rtl), the text itself kept left to
5091
+ @@ -1901,7 +1941,8 @@ pre, code {
5092
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
5093
  background: color-mix(in srgb, var(--accent) 6%, transparent);
5094
  color: var(--text-dim);
 
5098
  }
5099
 
5100
  /* No worktree list here (the files tab): a box that says why, and does nothing. */
5101
+ @@ -1978,7 +2019,7 @@ pre, code {
5102
  box-sizing: border-box;
5103
  flex: 0 0 24px;
5104
  border: 1px solid var(--border);
 
5107
  background: transparent;
5108
  color: var(--text-muted);
5109
  cursor: pointer;
5110
+ @@ -2003,8 +2044,9 @@ pre, code {
5111
 
5112
  .file-viewer-mode-button:focus-visible,
5113
  .file-viewer-icon-button:focus-visible {
 
5119
  }
5120
 
5121
  .directory-picker-path:focus-visible {
5122
+ @@ -2037,7 +2079,7 @@ pre, code {
5123
  .markdown-file-preview pre {
5124
  background: var(--bg-panel);
5125
  border: 1px solid var(--border);
 
5128
  padding: 12px 16px;
5129
  overflow-x: auto;
5130
  font-family: var(--font-mono);
5131
+ @@ -2049,7 +2091,7 @@ pre, code {
5132
  font-size: 0.9em;
5133
  background: var(--bg-panel);
5134
  padding: 1px 5px;
 
5137
  }
5138
  .markdown-file-preview pre code {
5139
  background: none;
5140
+ @@ -2061,7 +2103,7 @@ pre, code {
5141
  margin-bottom: 20px;
5142
  padding: 14px 16px 12px;
5143
  border: 1px solid var(--border);
 
5146
  background: var(--bg-panel);
5147
  }
5148
  .markdown-frontmatter-title {
5149
+ @@ -2114,18 +2156,6 @@ pre, code {
5150
  text-underline-offset: 2px;
5151
  }
5152
 
 
5165
  @keyframes drop-zone-in {
5166
  from { opacity: 0; transform: scale(0.97); }
5167
  to { opacity: 1; transform: scale(1); }
5168
+ @@ -2230,10 +2260,6 @@ span.token.table {
5169
  .panel-resize-handle.is-resizing::after {
5170
  background: color-mix(in srgb, var(--text-muted) 70%, var(--border));
5171
  }
 
5176
  /* Desktop sidebar: animate width */
5177
  @media (min-width: 641px) {
5178
  .sidebar-overlay-backdrop {
5179
+ @@ -2264,119 +2290,6 @@ span.token.table {
5180
  }
5181
  }
5182
 
 
5296
  /* Mobile sidebar: slide in/out as overlay */
5297
  @media (max-width: 640px) {
5298
  .chat-stats-center {
5299
+ @@ -2401,31 +2314,7 @@ span.token.table {
5300
  box-shadow: none;
5301
  }
5302
 
 
5329
  display: none;
5330
  }
5331
  .directory-picker-back,
5332
+ @@ -2474,3 +2363,490 @@ span.token.table {
5333
  display: none !important;
5334
  }
5335
  }
 
5497
  + transition: opacity 0.12s;
5498
  +}
5499
  +:is(.turn-user, .turn-reply):is(:hover, :focus-within) .message-actions-reveal,
5500
+ +.message-actions-reveal.is-pinned,
5501
+ +.message-actions-reveal:has([data-done]) { opacity: 1; }
5502
+ +/* No hover on a touch screen: the actions stay in view there. */
5503
+ +@media (hover: none) { .message-actions-reveal { opacity: 1; } }
5504
  +.message-meta {
5505
  + padding: 0 6px;
5506
  + color: var(--text-dim);
 
5508
  + font-variant-numeric: tabular-nums;
5509
  + white-space: nowrap;
5510
  +}
5511
+ +/* Vertical rhythm. Steps (thoughts, tool calls, a step's text) are one 4px rhythm,
5512
+ + within a message and from one message to the next, folded under their summary or
5513
+ + not (a running turn's are not yet); the answer sits 8px under its steps, open or
5514
+ + closed; its paragraphs keep the reading gap; a turn ends 22px before the next. */
5515
  +.turn-reply { margin: 0 0 22px; }
5516
  +.turn-reply > .message-actions { margin: 4px 0 0 -6px; }
5517
  +.turn-reply.is-step { margin: 0; }
5518
+ +/* Each message sits in a wrapper of its own (ChatWindow): the rhythm is set between those. */
5519
+ +:is(.turn-reply.is-step, :has(> .turn-reply.is-step)) + :is(.turn-reply.is-step, :has(> .turn-reply.is-step)) { margin-top: 4px; }
5520
+ +:is(.turn-reply.is-step, :has(> .turn-reply.is-step)) + :is(.turn-reply:not(.is-step), :has(> .turn-reply:not(.is-step))) { margin-top: 8px; }
5521
  +.reply-blocks { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
5522
+ +.turn-reply.is-step .reply-blocks { gap: 4px; }
5523
  +
5524
  +/* The steps of a turn: one summary line, "Ran 2 commands, read a file", that
5525
  + opens onto the steps themselves along a thin rail. */
 
5551
  + position: relative;
5552
  + display: flex;
5553
  + flex-direction: column;
5554
+ + gap: 0;
5555
+ + margin: 4px 0 0 1px;
5556
  + padding-left: 16px;
5557
  +}
5558
  +/* A drawing, not a border: the rail the steps hang from. */
 
5893
  <script
5894
  dangerouslySetInnerHTML={{
5895
  __html: THEME_INIT_SCRIPT,
5896
+ diff --git a/app/letter-spacing.test.mjs b/app/letter-spacing.test.mjs
5897
+ new file mode 100644
5898
+ index 0000000..099fc49
5899
+ --- /dev/null
5900
+ +++ b/app/letter-spacing.test.mjs
5901
+ @@ -0,0 +1,35 @@
5902
+ +import assert from "node:assert/strict";
5903
+ +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
5904
+ +import test from "node:test";
5905
+ +
5906
+ +// The design system's rule: never tweak letter spacing. Every font keeps its natural spacing,
5907
+ +// so no stylesheet or component sets letter-spacing (not even 0, which only ever undid a tweak).
5908
+ +const PATTERN = /letter-spacing|letterSpacing|\btracking-(?:tighter|tight|normal|wide|wider|widest|\[)/;
5909
+ +
5910
+ +function files(dir, keep) {
5911
+ + const out = [];
5912
+ + for (const name of readdirSync(dir)) {
5913
+ + if (name === "node_modules" || name.startsWith(".")) continue;
5914
+ + const path = `${dir}/${name}`;
5915
+ + if (statSync(path).isDirectory()) out.push(...files(path, keep));
5916
+ + else if (keep(name)) out.push(path);
5917
+ + }
5918
+ + return out;
5919
+ +}
5920
+ +
5921
+ +test("nothing in Pi Web tweaks letter spacing", () => {
5922
+ + const root = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
5923
+ + const ours = [
5924
+ + ...["app", "components", "lib", "hooks"].flatMap((dir) => files(`${root}/${dir}`, (n) => /\.(css|tsx?|mjs)$/.test(n) && !n.endsWith(".test.mjs"))),
5925
+ + `${root}/public/offline.html`,
5926
+ + ];
5927
+ + // The Hugging Face gateway's pages and the design system's sources, where this checkout sits beside them.
5928
+ + for (const dir of ["../space/gateway/static", "../design"]) {
5929
+ + if (existsSync(`${root}/${dir}`)) ours.push(...readdirSync(`${root}/${dir}`).filter((n) => /\.(css|html|js|json|py)$/.test(n)).map((n) => `${root}/${dir}/${n}`));
5930
+ + }
5931
+ + for (const path of ours) {
5932
+ + readFileSync(path, "utf8").split("\n").forEach((line, index) => {
5933
+ + assert.doesNotMatch(line, PATTERN, `${path.replace(root + "/", "")}:${index + 1}`);
5934
+ + });
5935
+ + }
5936
+ +});
5937
  diff --git a/app/login/page.tsx b/app/login/page.tsx
5938
  index 80264fe..6506201 100644
5939
  --- a/app/login/page.tsx
 
5986
  lang: "en",
5987
  icons: [
5988
  diff --git a/app/settings.css b/app/settings.css
5989
+ index 0bf21eb..d2f4031 100644
5990
  --- a/app/settings.css
5991
  +++ b/app/settings.css
5992
  @@ -35,14 +35,15 @@
 
6061
  .config-sidebar-group-label {
6062
  display: flex;
6063
  align-items: center;
6064
+ @@ -129,11 +133,9 @@
6065
  padding: 4px 8px 3px;
6066
  color: var(--text-dim);
6067
  font-family: inherit;
 
6070
  + font-size: 12px;
6071
  + font-weight: 400;
6072
  line-height: 1.4;
6073
+ - letter-spacing: 0;
6074
  - text-transform: uppercase;
6075
  }
6076
 
6077
  .config-sidebar-group-label-text {
6078
+ @@ -144,6 +146,13 @@
6079
  white-space: nowrap;
6080
  }
6081
 
 
6089
  /* Sits in the heading row, so the sidebar gains no height for it. */
6090
  .config-sidebar-group-switch {
6091
  display: inline-flex;
6092
+ @@ -153,8 +162,9 @@
6093
  }
6094
 
6095
  .config-sidebar-group-count {
 
6100
  }
6101
 
6102
  /* Three lines, then scroll: the phone layout gives the whole sidebar 190px. */
6103
+ @@ -188,7 +198,7 @@
6104
  height: 30px;
6105
  padding: 0 8px;
6106
  border: 0;
 
6109
  outline: none;
6110
  background: transparent;
6111
  color: var(--text);
6112
+ @@ -202,17 +212,19 @@
6113
 
6114
  .config-sidebar-item:not(:disabled):hover,
6115
  .config-sidebar-item:not(:disabled):focus-visible {
 
6134
  }
6135
 
6136
  .config-sidebar-item:disabled {
6137
+ @@ -236,8 +248,9 @@
6138
  .models-sidebar-badge {
6139
  flex-shrink: 0;
6140
  color: var(--text-dim);
 
6145
  }
6146
 
6147
  .oauth-paste-panel {
6148
+ @@ -277,9 +290,8 @@
6149
  /* An input's intrinsic width would otherwise push the button out of a narrow pane. */
6150
  min-width: 0;
6151
  padding: 6px 9px;
 
6157
  color: var(--text);
6158
  font-family: var(--font-mono);
6159
  font-size: 12px;
6160
+ @@ -290,9 +302,9 @@
6161
  flex-shrink: 0;
6162
  padding: 6px 12px;
6163
  border: none;
 
6170
  cursor: pointer;
6171
  font-size: 12px;
6172
  font-weight: 600;
6173
+ @@ -304,13 +316,13 @@
6174
  cursor: not-allowed;
6175
  }
6176
 
 
6187
  }
6188
 
6189
  .enabled-models-banner-text {
6190
+ @@ -370,8 +382,9 @@
6191
  .enabled-models-count {
6192
  margin-right: auto;
6193
  color: var(--text-dim);
 
6198
  }
6199
 
6200
  .enabled-models-note {
6201
+ @@ -383,7 +396,7 @@
6202
  .enabled-models-error {
6203
  padding: 7px 9px;
6204
  border: 1px solid rgba(239, 68, 68, 0.3);
 
6207
  color: #ef4444;
6208
  font-size: 11px;
6209
  line-height: 1.4;
6210
+ @@ -401,10 +414,9 @@
6211
  box-sizing: border-box;
6212
  height: 30px;
6213
  padding: 0 9px;
 
6220
  color: var(--text);
6221
  font-family: inherit;
6222
  font-size: 12px;
6223
+ @@ -413,8 +425,8 @@
6224
  .enabled-models-list {
6225
  max-height: 360px;
6226
  border: 1px solid var(--border);
 
6231
  overflow-y: auto;
6232
  }
6233
 
6234
+ @@ -457,9 +469,9 @@
6235
  .enabled-models-pin {
6236
  flex-shrink: 0;
6237
  padding: 1px 4px;
 
6244
  font-size: 9px;
6245
  }
6246
 
6247
+ @@ -496,10 +508,9 @@
6248
  align-items: center;
6249
  justify-content: space-between;
6250
  gap: 18px;
 
6258
  }
6259
 
6260
  .agents-feature-copy {
6261
+ @@ -541,7 +552,7 @@
6262
  height: 28px;
6263
  padding: 0 6px;
6264
  border: 1px solid var(--border);
 
6267
  background: var(--bg);
6268
  color: var(--text);
6269
  font: inherit;
6270
+ @@ -566,7 +577,7 @@
6271
  flex: 1;
6272
  min-width: 0;
6273
  min-height: 0;
 
6276
  overflow-y: auto;
6277
  font-family: inherit;
6278
  font-size: 12px;
6279
+ @@ -628,13 +639,12 @@
6280
  line-height: 1.35;
6281
  }
6282
 
 
6288
  + font-size: 12px;
6289
  + font-weight: 400;
6290
  line-height: 1.35;
6291
+ - letter-spacing: 0;
6292
  - text-transform: uppercase;
6293
  }
6294
 
6295
  .config-field {
6296
+ @@ -662,7 +672,7 @@
6297
  background: var(--border);
6298
  border: 5px solid transparent;
6299
  background-clip: padding-box;
 
6302
  }
6303
 
6304
  .config-field-label {
6305
+ @@ -697,15 +707,15 @@
6306
  .config-scope-tag {
6307
  flex-shrink: 0;
6308
  padding: 1px 5px;
 
6321
  }
6322
 
6323
  .config-scope-switch-field {
6324
+ @@ -730,7 +740,7 @@
6325
  height: 30px;
6326
  overflow: hidden;
6327
  border: 1px solid var(--border);
 
6330
  }
6331
 
6332
  .config-scope-switch-option {
6333
+ @@ -760,7 +770,7 @@
6334
  .config-scope-switch.is-small {
6335
  display: flex;
6336
  height: auto;
 
6339
  font-size: 12px;
6340
  }
6341
 
6342
+ @@ -891,9 +901,8 @@
6343
  width: 100%;
6344
  height: 36px;
6345
  padding: 0 11px;
 
6351
  color: var(--text);
6352
  font-family: var(--font-mono);
6353
  font-size: 12px;
6354
+ @@ -935,8 +944,8 @@
6355
  min-height: 30px;
6356
  padding: 6px 9px;
6357
  border: 1px solid var(--border);
 
6362
  color: var(--text-dim);
6363
  cursor: pointer;
6364
  font-family: var(--font-mono);
6365
+ @@ -975,12 +984,15 @@
6366
  white-space: pre-wrap;
6367
  }
6368
 
 
6381
  }
6382
 
6383
  .config-notice.has-action {
6384
+ @@ -1104,8 +1116,13 @@
6385
  align-items: center;
6386
  gap: 8px;
6387
  min-height: 52px;
 
6397
  }
6398
 
6399
  .config-footer-status {
6400
+ @@ -1159,44 +1176,47 @@
6401
  gap: 6px;
6402
  border-width: 1px;
6403
  border-style: solid;
 
6405
  + border-radius: var(--radius-sm);
6406
  cursor: pointer;
6407
  font-family: inherit;
6408
+ - letter-spacing: 0;
6409
+ line-height: 1;
6410
+ white-space: nowrap;
6411
  transition: background-color 0.14s ease, border-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease;
6412
  }
6413
 
 
6463
  }
6464
 
6465
  .config-button-ghost {
6466
+ @@ -1206,24 +1226,31 @@
6467
  }
6468
 
6469
  .config-button-primary:not(:disabled):hover {
 
6503
  outline-offset: 2px;
6504
  }
6505
 
6506
+ @@ -1248,33 +1275,41 @@
6507
  margin-left: auto;
6508
  }
6509
 
 
6557
  .config-switch::before {
6558
  content: "";
6559
  position: absolute;
6560
+ @@ -1283,7 +1318,7 @@
6561
 
6562
  .config-switch:disabled {
6563
  cursor: default;
 
6566
  }
6567
 
6568
  .config-switch.is-loading:disabled {
6569
+ @@ -1294,42 +1329,38 @@
6570
  position: absolute;
6571
  top: 2px;
6572
  left: 2px;
 
6623
  }
6624
 
6625
  .config-list-action-button {
6626
+ @@ -1342,7 +1373,7 @@
6627
  min-height: 30px;
6628
  padding: 0 8px;
6629
  border: 0;
 
6632
  background: transparent;
6633
  color: var(--text-dim);
6634
  cursor: pointer;
6635
+ @@ -1358,7 +1389,7 @@
6636
 
6637
  .config-list-action-button[aria-current="page"] {
6638
  background: var(--bg-selected);
 
6641
  }
6642
 
6643
  .config-list-action-button:disabled {
6644
+ @@ -1382,6 +1413,14 @@
6645
  opacity: 0.7;
6646
  }
6647
 
 
6656
  .settings-dialog-backdrop {
6657
  position: fixed;
6658
  inset: 0;
6659
+ @@ -1389,7 +1428,7 @@
6660
  display: flex;
6661
  align-items: center;
6662
  justify-content: center;
 
6665
  }
6666
 
6667
  /* Focus moves to the dialog itself when Settings opens (tabIndex -1); it is not a control, so no ring. */
6668
+ @@ -1398,127 +1437,184 @@
6669
  }
6670
 
6671
  .settings-dialog-surface {
 
6921
  }
6922
 
6923
  .settings-dialog-main {
6924
+ @@ -1537,136 +1633,197 @@
6925
  min-height: 0;
6926
  }
6927
 
 
7198
  font-family: var(--font-mono);
7199
  font-weight: var(--font-mono-weight);
7200
  }
7201
+ @@ -1676,197 +1833,324 @@
7202
  }
7203
 
7204
  .settings-font-input:focus-visible {
 
7654
  }
7655
 
7656
  @media (max-width: 640px) {
7657
+ @@ -1887,13 +2171,13 @@
7658
  width: 100%;
7659
  height: 190px;
7660
  border-right: 0;
 
7670
  }
7671
 
7672
  .agents-feature-actions {
7673
+ @@ -1905,29 +2189,6 @@
7674
  padding: 14px;
7675
  }
7676
 
 
7700
  }
7701
 
7702
  @keyframes saved-pop {
7703
+ @@ -2066,7 +2327,7 @@
7704
  max-width: 100%;
7705
  padding: 0 5px;
7706
  border: 1px solid var(--border);
 
7709
  background: var(--bg-panel);
7710
  color: var(--text);
7711
  font-family: var(--font-mono);
7712
+ @@ -2085,7 +2346,7 @@
7713
  padding: 6px 8px;
7714
  overflow: auto;
7715
  border: 1px solid var(--border);
 
7718
  background: var(--bg-panel);
7719
  color: var(--text-muted);
7720
  font-family: var(--font-mono);
7721
+ @@ -2172,9 +2433,8 @@
7722
  min-width: 0;
7723
  height: 32px;
7724
  padding: 0 10px;
 
7730
  color: var(--text);
7731
  font-family: var(--font-mono);
7732
  font-size: 12px;
7733
+ @@ -2266,10 +2526,9 @@ select.mcp-add-input {
7734
  max-width: 100%;
7735
  max-height: 100%;
7736
  overflow: hidden;
 
7744
  }
7745
 
7746
  /* Focus moves to the dialog itself when it opens (tabIndex -1); it is not a control, so no ring. */
7747
+ @@ -2282,7 +2541,7 @@ select.mcp-add-input {
7748
  flex: 1 1 auto;
7749
  gap: 12px;
7750
  min-height: 0;
 
7753
  overflow: auto;
7754
  overscroll-behavior: contain;
7755
  }
7756
+ @@ -2315,7 +2574,7 @@ select.mcp-add-input {
7757
  margin-top: 10px;
7758
  padding: 8px 10px;
7759
  border: 1px solid var(--border);
 
7762
  background: var(--bg);
7763
  color: var(--text);
7764
  font-family: var(--font-mono);
7765
+ @@ -2333,7 +2592,7 @@ select.mcp-add-input {
7766
  .project-trust-error {
7767
  flex-shrink: 0;
7768
  max-height: 30vh;
 
7771
  overflow: auto;
7772
  color: #ef4444;
7773
  font-size: 12px;
7774
+ @@ -2353,19 +2612,20 @@ select.mcp-add-input {
7775
  flex-shrink: 0;
7776
  justify-content: flex-end;
7777
  gap: 8px;
 
7799
  }
7800
 
7801
  .project-trust-button:disabled {
7802
+ @@ -2373,10 +2633,8 @@ select.mcp-add-input {
7803
  }
7804
 
7805
  .project-trust-button.is-primary {
 
7812
  }
7813
 
7814
  .project-trust-button.is-primary:disabled {
7815
+ @@ -2450,7 +2708,7 @@ select.mcp-add-input {
7816
  min-width: 0;
7817
  padding: 8px 10px;
7818
  border: 1px solid var(--border);
 
7821
  background: var(--bg);
7822
  }
7823
 
7824
+ @@ -2472,7 +2730,7 @@ select.mcp-add-input {
7825
  .project-trust-mcp-tag {
7826
  flex-shrink: 0;
7827
  padding: 1px 5px;
 
7830
  background: rgba(120, 120, 120, 0.12);
7831
  color: var(--text-dim);
7832
  font-size: 10px;
7833
+ @@ -2493,7 +2751,7 @@ select.mcp-add-input {
7834
  display: inline-block;
7835
  margin: 2px 4px 0 0;
7836
  padding: 0 4px;
 
9317
 
9318
  .sidebar-sheet .sidebar-menu-question-text {
9319
  diff --git a/app/sidebar.css b/app/sidebar.css
9320
+ index 86728a2..acb525e 100644
9321
  --- a/app/sidebar.css
9322
  +++ b/app/sidebar.css
9323
  @@ -91,8 +91,9 @@
 
9429
  border-radius: 50%;
9430
  opacity: 0.35;
9431
  animation: session-tree-status-pulse 1.6s ease-in-out infinite;
9432
+ @@ -297,20 +278,22 @@ a.session-tree-main {
9433
  }
9434
  }
9435
 
 
9446
  - font-size: 12px;
9447
  - text-overflow: ellipsis;
9448
  + font-size: 13px;
 
9449
  white-space: nowrap;
9450
  + -webkit-mask-image: linear-gradient(to right, black calc(100% - 20px), transparent);
9451
  + mask-image: linear-gradient(to right, black calc(100% - 20px), transparent);
 
9457
  }
9458
 
9459
  .session-tree-session.is-archived .session-tree-title {
9460
+ @@ -321,6 +304,8 @@ a.session-tree-main {
9461
  from its source ("… · 3f9a"), never the suffix. */
9462
  .session-tree-title.has-fork-suffix {
9463
  display: flex;
 
9466
  }
9467
 
9468
  .session-tree-title-base {
9469
+ @@ -384,16 +369,23 @@ a.session-tree-main {
9470
  align-items: center;
9471
  }
9472
 
 
9494
  color: var(--text-dim);
9495
  }
9496
 
9497
+ @@ -408,11 +400,18 @@ a.session-tree-main {
9498
  background: var(--bg-hover);
9499
  }
9500
 
 
9516
  }
9517
 
9518
  .session-tree-meta.is-running {
9519
+ @@ -436,7 +435,10 @@ a.session-tree-main {
9520
  * :focus-within fallback in the @supports block after the group rules.
9521
  */
9522
  .session-tree-session:has(:focus-visible) .session-tree-action {
 
9528
  }
9529
 
9530
  .session-tree-session:has(:focus-visible) .session-tree-meta {
9531
+ @@ -454,7 +456,7 @@ a.session-tree-main {
9532
  padding: 0 8px;
9533
  box-sizing: border-box;
9534
  border: 1px solid var(--accent);
 
9537
  outline: none;
9538
  background: var(--bg);
9539
  color: var(--text);
9540
+ @@ -483,7 +485,7 @@ a.session-tree-main {
9541
  gap: 4px;
9542
  height: 24px;
9543
  padding: 0 10px;
 
9546
  font-size: 12px;
9547
  white-space: nowrap;
9548
  }
9549
+ @@ -518,8 +520,8 @@ a.session-tree-main {
9550
  right: var(--session-tree-inset-right);
9551
  left: var(--session-tree-inset-left);
9552
  gap: 2px;
 
9557
  -webkit-user-select: none;
9558
  user-select: none;
9559
  -webkit-touch-callout: none;
9560
+ @@ -548,7 +550,7 @@ a.session-tree-main {
9561
  position: absolute;
9562
  right: var(--session-tree-inset-right);
9563
  left: var(--session-tree-inset-left);
 
9566
  background: color-mix(in srgb, var(--bg-panel) 55%, transparent);
9567
  pointer-events: none;
9568
  }
9569
+ @@ -589,7 +591,7 @@ a.session-tree-main {
9570
  height: 22px;
9571
  padding: 0 10px;
9572
  border: 1px solid var(--border);
 
9575
  background: var(--bg-panel);
9576
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
9577
  color: var(--text);
9578
+ @@ -618,7 +620,7 @@ a.session-tree-main {
9579
  padding: 0 0 0 5px;
9580
  color: var(--text-muted);
9581
  font-size: 12px;
 
9584
  text-align: left;
9585
  white-space: nowrap;
9586
  }
9587
+ @@ -669,19 +671,27 @@ a.session-tree-main {
9588
  width: 6px;
9589
  height: 6px;
9590
  border-radius: 50%;
 
9615
  }
9616
 
9617
  .session-tree-group:hover .session-tree-summary,
9618
+ @@ -690,7 +700,10 @@ a.session-tree-main {
9619
  }
9620
 
9621
  .session-tree-group:has(:focus-visible) .session-tree-group-actions {
 
9627
  }
9628
 
9629
  .session-tree-group:has(:focus-visible) .session-tree-summary {
9630
+ @@ -698,9 +711,18 @@ a.session-tree-main {
9631
  }
9632
 
9633
  @supports not selector(:has(*)) {
 
9648
  }
9649
 
9650
  .session-tree-session:focus-within .session-tree-meta,
9651
+ @@ -726,7 +748,7 @@ a.session-tree-main {
9652
  width: 22px;
9653
  height: 22px;
9654
  padding: 0;
 
9657
  color: var(--text-dim);
9658
  }
9659
 
9660
+ @@ -787,7 +809,7 @@ a.session-tree-main {
9661
  gap: 3px;
9662
  min-width: 0;
9663
  padding: 0 4px 0 5px;
 
9666
  color: var(--text-dim);
9667
  font-size: 12px;
9668
  text-align: left;
9669
+ @@ -838,16 +860,21 @@ a.session-tree-main {
9670
 
9671
  /* Phone layout: 44px rows, a step larger type for titles and project names,
9672
  ⋯ and the group buttons always shown, no archive quick button. */
 
9690
  }
9691
 
9692
  .session-tree.is-mobile .session-tree-session:hover .session-tree-meta {
9693
+ @@ -859,7 +886,10 @@ a.session-tree-main {
9694
  }
9695
 
9696
  .session-tree.is-mobile .session-tree-group-actions {
 
9702
  }
9703
 
9704
  .session-tree.is-mobile .session-tree-group .session-tree-summary {
9705
+ @@ -884,7 +914,10 @@ a.session-tree-main {
9706
  /* Touch screens wider than a phone have no hover: keep ⋯ reachable. */
9707
  @media (hover: none) {
9708
  .session-tree-more-action {
 
9714
  }
9715
  }
9716
 
9717
+ @@ -942,9 +975,10 @@ a.session-tree-main {
9718
  display: flex;
9719
  flex: none;
9720
  align-items: stretch;
 
9726
  }
9727
 
9728
  /* The tablist itself holds only the two tabs (ARIA allows nothing else in it). */
9729
+ @@ -954,44 +988,30 @@ a.session-tree-main {
9730
  align-items: stretch;
9731
  }
9732
 
 
9778
  .sidebar-tab-icon {
9779
  flex: none;
9780
  color: var(--text-dim);
9781
+ @@ -1006,17 +1026,12 @@ a.session-tree-main {
9782
 
9783
  .sidebar-tab.is-selected,
9784
  .sidebar-search-toggle.is-active {
 
9798
  }
9799
 
9800
  .sidebar-tab-count {
9801
+ @@ -1036,27 +1051,36 @@ a.session-tree-main {
9802
  beside the two cells at the sidebar's 180px minimum, and at 11px so do
9803
  the versions with a digit to spare). */
9804
  .sidebar-brand {
 
9821
  - font-family: var(--font-mono);
9822
  - font-size: 15px;
9823
  - font-weight: 700;
9824
+ - letter-spacing: -0.01em;
9825
+ - line-height: normal;
9826
  + color: var(--text-strong);
9827
  + font-family: var(--font-ui);
9828
  + font-size: 13px;
9829
  + font-weight: 600;
9830
  + font-variant-numeric: tabular-nums;
 
 
9831
  + line-height: 1;
9832
  text-align: left;
9833
  white-space: nowrap;
 
9846
  }
9847
 
9848
  .sidebar-brand:focus {
9849
+ @@ -1064,8 +1088,10 @@ a.session-tree-main {
9850
  }
9851
 
9852
  .sidebar-brand:focus-visible {
 
9859
  }
9860
 
9861
  .sidebar-header-spacer {
9862
+ @@ -1078,7 +1104,7 @@ a.session-tree-main {
9863
  }
9864
 
9865
  .sidebar-search-toggle {
 
9868
  padding: 0;
9869
  }
9870
 
9871
+ @@ -1093,7 +1119,7 @@ a.session-tree-main {
9872
  label is still each one's name. */
9873
  .sidebar-header[data-fit="1"] .sidebar-new-button,
9874
  .sidebar-header[data-fit="2"] .sidebar-new-button {
 
9877
  padding: 0;
9878
  }
9879
 
9880
+ @@ -1105,6 +1131,11 @@ a.session-tree-main {
9881
  font-size: 11px;
9882
  }
9883
 
 
9889
  .sidebar-header[data-fit="1"] .sidebar-new-label,
9890
  .sidebar-header[data-fit="2"] .sidebar-new-label,
9891
  .sidebar-header[data-fit="2"] .sidebar-tab-label {
9892
+ @@ -1122,8 +1153,9 @@ a.session-tree-main {
9893
  .sidebar-archive-back:focus-visible,
9894
  .sidebar-tool-button:focus-visible,
9895
  .sidebar-files-section-toggle:focus-visible {
 
9901
  }
9902
 
9903
  .sidebar-panel {
9904
+ @@ -1202,7 +1234,7 @@ a.session-tree-main {
9905
  the root too), and its time 4px inside the row. The header row's keys and
9906
  the picker's boxes end there. */
9907
  .session-sidebar.is-files-below {
 
9910
  }
9911
 
9912
  /* The files section's header row (files below the sessions only), one row
9913
+ @@ -1246,7 +1278,7 @@ a.session-tree-main {
9914
  padding: 0 5px;
9915
  overflow: hidden;
9916
  border: 0;
 
9919
  background: transparent;
9920
  color: var(--text-muted);
9921
  font: inherit;
9922
+ @@ -1272,10 +1304,8 @@ a.session-tree-main {
9923
  .sidebar-files-section-label {
9924
  min-width: 0;
9925
  overflow: hidden;
 
9931
  }
9932
 
9933
  /* The changed files' count, as the pinned section's (.session-tree-pinned-count). */
9934
+ @@ -1294,7 +1324,7 @@ a.session-tree-main {
9935
  display: flex;
9936
  flex: 1000 0 calc(7 * 18px);
9937
  align-items: center;
 
9940
  }
9941
 
9942
  /* The section's keys: the files tab's square keys, a step under the row's
9943
+ @@ -1302,9 +1332,9 @@ a.session-tree-main {
9944
  .sidebar-files-keys .sidebar-tool-button {
9945
  flex: 1 1 0;
9946
  min-width: 18px;
 
9953
  }
9954
 
9955
  /* Under that row (or the whole files tab): the head, then the tree filling
9956
+ @@ -1317,31 +1347,7 @@ a.session-tree-main {
9957
  min-height: 0;
9958
  }
9959
 
 
9986
  slides in over the tree, which stays mounted under it (hidden once the
9987
  archive is in), and away again. SessionTree fills each. */
9988
  .sidebar-sessions-views {
9989
+ @@ -1413,7 +1419,7 @@ a.session-tree-main {
9990
  height: 26px;
9991
  padding: 0 6px;
9992
  border: 0;
 
9995
  background: transparent;
9996
  color: var(--text-muted);
9997
  font-size: 12px;
9998
+ @@ -1479,10 +1485,11 @@ a.session-tree-main {
9999
  six (the changes view is disabled, not gone, without changes), so nothing
10000
  moves as the count flips. At the sidebar's 180px minimum they give way
10001
  together, down to 21px wide, and the gap before the views closes. */
 
10008
  margin-top: 4px;
10009
  }
10010
 
10011
+ @@ -1492,17 +1499,18 @@ a.session-tree-main {
10012
  margin-left: auto;
10013
  }
10014
 
 
10032
  background: transparent;
10033
  color: var(--text-dim);
10034
  cursor: pointer;
10035
+ @@ -1510,8 +1518,8 @@ a.session-tree-main {
10036
  }
10037
 
10038
  .sidebar-tool-button:not(:disabled):hover {
 
10045
  .sidebar-tool-button.is-active,
10046
  diff --git a/app/ui.css b/app/ui.css
10047
  new file mode 100644
10048
+ index 0000000..ad43ed3
10049
  --- /dev/null
10050
  +++ b/app/ui.css
10051
+ @@ -0,0 +1,760 @@
10052
  +/* The shared components on Base UI (components/ui/*): menus, tooltips,
10053
  + popovers and the composer's suggestion list. Popups are portaled to <body>,
10054
  + so they stack by the positioner's z-index, above every pane. */
 
10319
  + background: color-mix(in srgb, var(--primary-contrast) 16%, transparent);
10320
  + font-family: inherit;
10321
  + font-size: 11px;
 
10322
  +}
10323
  +
10324
  +/* ---------------------------------------------------------------- popovers */
 
20897
  </div>
20898
  );
20899
  });
20900
+ diff --git a/components/MessageView.copy.test.mjs b/components/MessageView.copy.test.mjs
20901
+ new file mode 100644
20902
+ index 0000000..f4d9392
20903
+ --- /dev/null
20904
+ +++ b/components/MessageView.copy.test.mjs
20905
+ @@ -0,0 +1,26 @@
20906
+ +import assert from "node:assert/strict";
20907
+ +import { readFileSync } from "node:fs";
20908
+ +import test from "node:test";
20909
+ +
20910
+ +const view = readFileSync(new URL("./MessageView.tsx", import.meta.url), "utf8");
20911
+ +const css = readFileSync(new URL("../app/globals.css", import.meta.url), "utf8");
20912
+ +
20913
+ +test("only an answer has a Copy, shown on hover like its row; whitespace text blocks draw nothing", () => {
20914
+ + // DeepSeek sends a "\n\n" text block beside each tool call: it is no text, so no block and no Copy of it.
20915
+ + assert.match(view, /!\(block\.type === "text" && !\(block as TextContent\)\.text\.trim\(\)\)/);
20916
+ + assert.match(view, /\.join\("\\n"\)\n\s*\.trim\(\);/);
20917
+ + // An answer is text without tool calls; a running turn's other messages are steps (no actions row).
20918
+ + assert.match(view, /const isAnswer = Boolean\(textContent\) && !blocks\.some\(\(b\) => b\.type === "toolCall"\);/);
20919
+ + assert.match(view, /\{!asStep && !isStreaming && isAnswer && \(/);
20920
+ + // Copy sits inside the row's hover reveal, with the model, usage and time.
20921
+ + const row = view.slice(view.indexOf("{!asStep && !isStreaming && isAnswer && ("));
20922
+ + assert.ok(row.indexOf('<div className="message-actions-reveal">') < row.indexOf("onClick={copyContent}"));
20923
+ + assert.match(css, /@media \(hover: none\) \{ \.message-actions-reveal \{ opacity: 1; \} \}/);
20924
+ +});
20925
+ +
20926
+ +test("steps keep one 4px rhythm, folded or live, and the answer sits 8px under them", () => {
20927
+ + assert.match(css, /\.turn-reply\.is-step \.reply-blocks \{ gap: 4px; \}/);
20928
+ + assert.match(css, /:has\(> \.turn-reply\.is-step\)\) \+ :is\(\.turn-reply\.is-step, :has\(> \.turn-reply\.is-step\)\) \{ margin-top: 4px; \}/);
20929
+ + assert.match(css, /\{ margin-top: 8px; \}/);
20930
+ + assert.match(css, /\.steps-list \{[^}]*gap: 0;[^}]*margin: 4px 0 0 1px;/);
20931
+ +});
20932
  diff --git a/components/MessageView.test.mjs b/components/MessageView.test.mjs
20933
  index 515b033..12d546b 100644
20934
  --- a/components/MessageView.test.mjs
 
20968
 
20969
  test("collapses a displayed custom message from its header alone", () => {
20970
  diff --git a/components/MessageView.tsx b/components/MessageView.tsx
20971
+ index f2ff1de..0dfb320 100644
20972
  --- a/components/MessageView.tsx
20973
  +++ b/components/MessageView.tsx
20974
  @@ -5,13 +5,16 @@ import ReactMarkdown from "react-markdown";
 
21430
  }: {
21431
  message: AssistantMessage;
21432
  isStreaming?: boolean;
21433
+ @@ -669,17 +589,21 @@ function AssistantMessageView({
21434
  onCompact?: () => void;
21435
  isCompacting?: boolean;
21436
  compactError?: string | null;
 
21439
  }) {
21440
  const { t } = useI18n();
21441
  const time = showTimestamp ? formatTime(message.timestamp) : null;
21442
+ const blockItems = useMemo(() => (message.content ?? [])
21443
+ .map((block, originalIndex) => ({ block, originalIndex }))
21444
+ - .filter(({ block }) => !isEmptyThinkingBlock(block, { isStreaming })), [message.content, isStreaming]);
21445
+ + // A text block of whitespace alone (DeepSeek sends "\n\n" beside each tool call) draws
21446
+ + // nothing: rendered, it was an empty element the blocks' gap counted twice.
21447
+ + .filter(({ block }) => !isEmptyThinkingBlock(block, { isStreaming })
21448
+ + && !(block.type === "text" && !(block as TextContent).text.trim())), [message.content, isStreaming]);
21449
+ const blocks = useMemo(() => blockItems.map(({ block }) => block), [blockItems]);
21450
  const providerError = getAssistantErrorMessage(message, { isStreaming });
21451
  const truncated = isAssistantTruncated(message, { isStreaming });
21452
  const unansweredTruncation = truncated && !hasAssistantAnswer(message);
 
21454
  const [copied, setCopied] = useState(false);
21455
  const streamStartRef = useRef<StreamRateStart | null>(null);
21456
  const rateKey = streamRateKey(message);
21457
+ @@ -741,7 +665,13 @@ function AssistantMessageView({
21458
+ const textContent = blocks
21459
+ .filter((b): b is TextContent => b.type === "text")
21460
+ .map((b) => b.text)
21461
+ - .join("\n");
21462
+ + .join("\n")
21463
+ + .trim();
21464
+ + // An answer is text without tool calls. Anything else is a step of its turn, shown as one
21465
+ + // (no actions row, the steps' rhythm) also while the turn runs and its steps are not yet
21466
+ + // folded under their summary: each live step used to carry a Copy of its "\n\n".
21467
+ + const isAnswer = Boolean(textContent) && !blocks.some((b) => b.type === "toolCall");
21468
+ + const asStep = inSteps || (!isAnswer && blocks.length > 0 && !providerError && !truncated);
21469
+
21470
+ const copyContent = () => {
21471
+ copyText(textContent).then(() => {
21472
+ @@ -803,123 +733,59 @@ function AssistantMessageView({
21473
 
21474
  if (blocks.length === 0 && !isStreaming && !providerError && !truncated) return null;
21475
 
 
21488
  - style={{ marginBottom: 16 }}
21489
  - onMouseEnter={() => setHovered(true)}
21490
  - onMouseLeave={() => setHovered(false)}
21491
+ + className={`turn-reply${asStep ? " is-step" : ""}`}
21492
  >
21493
  - {/* Model label */}
21494
  - <div
 
21618
  )}
21619
  </div>
21620
  )}
21621
+ @@ -928,51 +794,29 @@ function AssistantMessageView({
21622
  <TurnWrittenFiles files={writtenFiles} onOpenFile={onOpenFile} />
21623
  )}
21624
 
 
21631
  - </div>
21632
  - )}
21633
  - {textContent && !isStreaming && (
21634
+ - <button
21635
+ - onClick={copyContent}
 
 
 
 
 
 
21636
  - title={t("i18n.copyMessage")}
21637
  - style={{
21638
  - display: "flex", alignItems: "center", gap: 4,
 
21649
  - }}
21650
  - onMouseEnter={(e) => { if (!copied) e.currentTarget.style.color = "var(--accent)"; }}
21651
  - onMouseLeave={(e) => { if (!copied) e.currentTarget.style.color = "var(--text-dim)"; }}
21652
+ - >
 
 
21653
  - {copied ? (
21654
  - <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
21655
  - <polyline points="20 6 9 17 4 12" />
 
21659
  - <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
21660
  - <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
21661
  - </svg>
21662
+ + {/* Only an answer has an actions row; Copy shows with the rest of it, on hover or focus
21663
+ + (always on a touch screen, which cannot hover). */}
21664
+ + {!asStep && !isStreaming && isAnswer && (
21665
+ + <div className="message-actions">
21666
+ + <div className="message-actions-reveal">
21667
+ + <button
21668
+ + type="button"
21669
+ + className="ui-btn ui-btn--xs ui-btn--icon"
21670
+ + data-done={copied || undefined}
21671
+ + onClick={copyContent}
21672
+ + title={t("i18n.copyMessage")}
21673
+ + aria-label={copied ? t("i18n.copied") : t("i18n.copyMessage")}
21674
+ + >
21675
+ + <CarbonIcon icon={copied ? Carbon.Checkmark : Carbon.Copy} size={14} />
21676
+ + </button>
21677
+ + {modelName && <span className="message-meta">{modelName}</span>}
21678
+ + {shownUsage && formatUsageShort(shownUsage) && (
21679
+ + <span className="message-meta" title={formatUsage(shownUsage)}>{formatUsageShort(shownUsage)}</span>
21680
+ )}
21681
  - {copied ? t("i18n.copied") : t("i18n.copy")}
21682
+ - </button>
 
21683
  - )}
21684
  - {time && !isStreaming && (
21685
  - <span style={{ fontSize: 10, color: "var(--text-dim)", marginLeft: "auto" }}>{time}</span>
21686
  - )}
21687
  - </div>
 
 
 
 
 
21688
  + {time && <span className="message-meta">{time}</span>}
21689
  + </div>
21690
  + </div>
 
21692
  </div>
21693
  );
21694
  }
21695
+ @@ -988,7 +832,7 @@ function BlockView({ block, searchTarget, toolResults, isStreaming, streamingDur
21696
  const tc = block as ToolCallContent;
21697
  const result = toolResults?.get(tc.toolCallId);
21698
  const duration = toolCallDurations?.get(tc.toolCallId);
 
21701
  }
21702
  return null;
21703
  }
21704
+ @@ -1052,62 +896,34 @@ export function ThinkingBlock({ block, duration, sessionId, entryId, blockIndex
21705
  }, [expanded, block.deferred, content, sessionId, entryId, blockIndex]);
21706
 
21707
  return (
 
21788
  </div>
21789
  );
21790
  }
21791
+ @@ -1137,7 +953,7 @@ export function formatToolDuration(ms: number): string {
21792
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
21793
  }
21794
 
 
21797
  const { t } = useI18n();
21798
  const [expanded, setExpanded] = useState(() => isToolCallExpanded(block.toolCallId));
21799
  const toggleExpanded = () => {
21800
+ @@ -1146,6 +962,7 @@ function ToolCallBlock({ block, result, duration, onOpenSubagent }: { block: Too
21801
  setExpanded(next);
21802
  };
21803
  const inputStr = getWrittenFileText(block) ?? getToolCallInputText(block);
 
21805
  const isStreamingInput = block.rawInput !== undefined;
21806
  const isEditTool = isEditToolName(block.toolName);
21807
  const resultDiff = result && !result.isError ? getResultDiff(result) : null;
21808
+ @@ -1175,137 +992,154 @@ function ToolCallBlock({ block, result, duration, onOpenSubagent }: { block: Too
21809
  || (isApplyPatchToolName(block.toolName) && applyPatchResultHasFailures(result?.details));
21810
  const subagent = isSubagentToolDetails(result?.details) ? result.details : null;
21811
  const codemodeCallCount = codemode ? codemode.calls.length + codemode.omitted : 0;
 
22054
  interface ResultDiff {
22055
  text: string;
22056
  }
22057
+ @@ -1316,7 +1150,6 @@ function PairedDiffResult({ diff }: {
22058
  return (
22059
  <div
22060
  style={{
 
22062
  background: "var(--bg)",
22063
  }}
22064
  >
22065
+ @@ -1325,148 +1158,16 @@ function PairedDiffResult({ diff }: {
22066
  );
22067
  }
22068
 
 
22218
  }
22219
 
22220
  function PatchTextView({ text }: { text: string }) {
22221
+ @@ -1483,12 +1184,12 @@ function PatchTextView({ text }: { text: string }) {
22222
  const bg =
22223
  kind === "added" ? "rgba(34,197,94,0.12)" :
22224
  kind === "removed" ? "rgba(248,113,113,0.13)" :
 
22233
  "var(--text)";
22234
 
22235
  return (
22236
+ @@ -1502,7 +1203,7 @@ function PatchTextView({ text }: { text: string }) {
22237
  : kind === "removed"
22238
  ? "3px solid #f87171"
22239
  : kind === "hunk"
 
22242
  : "3px solid transparent",
22243
  }}
22244
  >
22245
+ @@ -1578,16 +1279,16 @@ function isRecord(value: unknown): value is Record<string, unknown> {
22246
  return typeof value === "object" && value !== null && !Array.isArray(value);
22247
  }
22248
 
 
22261
  }}
22262
  >
22263
  {images.map((image, index) => {
22264
+ @@ -1606,9 +1307,10 @@ function ResultImages({ images, isError }: { images: ImageContent[]; isError: bo
22265
  loading="lazy"
22266
  style={{
22267
  display: "block",
 
22275
  objectFit: "contain",
22276
  border: "1px solid var(--border)",
22277
  }}
22278
+ @@ -1620,6 +1322,67 @@ function ResultImages({ images, isError }: { images: ImageContent[]; isError: bo
22279
  );
22280
  }
22281
 
 
22343
  function PairedResult({ text, isEmpty, isError }: {
22344
  text: string;
22345
  isEmpty: boolean;
22346
+ @@ -1629,8 +1392,7 @@ function PairedResult({ text, isEmpty, isError }: {
22347
  return (
22348
  <div
22349
  style={{
 
22353
  }}
22354
  >
22355
  <pre
22356
+ @@ -1668,7 +1430,7 @@ function CompactionMessageView({ message }: { message: CustomMessage }) {
22357
  <div
22358
  style={{
22359
  border: "1px solid var(--border)",
 
22362
  overflow: "hidden",
22363
  background: "var(--bg)",
22364
  }}
22365
+ @@ -1710,9 +1472,7 @@ function CompactionMessageView({ message }: { message: CustomMessage }) {
22366
  textAlign: "left",
22367
  }}
22368
  >
 
22373
  <span style={{ minWidth: 0 }}>
22374
  <span style={{ display: "block", fontSize: "calc(15px + var(--chat-font-size-offset, 0px))", fontWeight: 700, lineHeight: 1.35 }}>
22375
  {t("i18n.conversationCompacted")}
22376
+ @@ -1806,7 +1566,7 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
22377
  <div
22378
  style={{
22379
  border: "1px solid var(--border)",
 
22382
  overflow: "hidden",
22383
  background: "var(--bg)",
22384
  }}
22385
+ @@ -1852,9 +1612,7 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
22386
  </span>
22387
  )}
22388
  {time && <span style={{ marginLeft: "auto", color: "var(--text-dim)", fontSize: 10, flexShrink: 0 }}>{time}</span>}
 
22393
  </button>
22394
  </div>
22395
 
22396
+ @@ -1872,7 +1630,7 @@ function CustomMessageView({ message, cwd, onOpenFile }: { message: CustomMessag
22397
  <img
22398
  src={src}
22399
  alt=""
 
22402
  />
22403
  </ImagePreview>
22404
  );
22405
+ @@ -1999,6 +1757,32 @@ function getWrittenFileText(block: ToolCallContent): string | null {
22406
  return Object.keys(block.input).every((key) => WRITE_VIEW_KEYS.has(key)) ? content : null;
22407
  }
22408
 
 
22435
  function formatCustomType(type: string): string {
22436
  return type || "extension";
22437
  }
22438
+ @@ -2021,13 +1805,16 @@ function getToolPreview(block: ToolCallContent): string {
22439
  return String(first).slice(0, 120);
22440
  }
22441
 
 
22695
  onSelect={() => choose(option)}
22696
  star={onSetDefault ? {
22697
  diff --git a/components/ModelsConfig.tsx b/components/ModelsConfig.tsx
22698
+ index 9f6b556..d17e0cf 100644
22699
  --- a/components/ModelsConfig.tsx
22700
  +++ b/components/ModelsConfig.tsx
22701
  @@ -54,6 +54,8 @@ import { providerBadgeLabel } from "./enabled-models-helpers";
 
22920
  Open the verification page and enter this code:
22921
  </p>
22922
  - <div style={{ padding: "8px 10px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 5, color: "var(--text)", fontSize: 16, fontWeight: 700, fontFamily: "var(--font-mono)", letterSpacing: 0 }}>
22923
+ + <div style={{ padding: "8px 10px", background: "var(--bg)", border: "1px solid var(--border)", borderRadius: "var(--radius-sm)", color: "var(--text)", fontSize: 16, fontWeight: 700, fontFamily: "var(--font-mono)" }}>
22924
  {loginState.userCode}
22925
  </div>
22926
  <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
 
31081
  - **Notices.** MCP off, with its reason. Otherwise `hostInactive`: an open session whose `/mcp` is another extension's, naming its folder and that extension (only a session can tell). A project folder the route refuses (`cwd-*`) does not hide the global servers: `loadMcpOverview()` loads again without the cwd and the Project group says "Not listed" with the reason. Trust is reported only when the project has a `.pi/mcp.json` or a problem with one: untrusted (`ConfigTrustNotice`), an inherited `false` naming its folder, and inherited trust as "Trusted through <path>", the only place that trust is visible. A folder that requires no trust gets no trust notice unless a decision marks it untrusted: a dangling `.pi/mcp.json` link is listed (`link-dangling`), but the SDK's `existsSync` finds nothing to trust, so Trust would only meet `trust-not-required`. File problems go to the footer (`ConfigFooterStatus`). A load ends after `MCP_OVERVIEW_TIMEOUT_MS` (15 s, both requests together), since Refresh is disabled while one runs.
31082
  - **The detail pane shows only what GET sends:** the masked command line (`mcpServerTarget()`), working directory, env and header **names** (each only when set), the `!command` fields and host variables (left out for a refused entry, which never connects; kept for a disabled one), the Sign-in row (**Settings › MCP sign-in** in [mcp-test-sign-in.md](mcp-test-sign-in.md)), the exposure, the file. The Code mode pane holds the choice, the sandbox and `-builtin:codemode`.
31083
  diff --git a/docs/agents/sessions.md b/docs/agents/sessions.md
31084
+ index 42e1816..85d70bc 100644
31085
  --- a/docs/agents/sessions.md
31086
  +++ b/docs/agents/sessions.md
31087
  @@ -38,7 +38,7 @@ On mount `useAgentSession` loads the history, then `GET /api/sessions/[id]/state
 
31098
  - Spacing: groups sit right on each other, open or folded (collapsed ones at a 28px pitch); a gap between two of them only pushed the next header away. An 8px spacer row parts the pinned section from the groups (`pinned-spacer`) and the groups from the footer (`footer-spacer`, only when there are groups; a pinned section with no group has the one spacer before the footer).
31099
  - Heights (`SIDEBAR_ROW_HEIGHTS`) keep one rhythm: session rows, every header (a project's, the pinned section's, the archive's projects), the empty group's note and the footer's two links are 28px (44px on a phone, a finger's minimum, headers included); only "show more" is a step shorter (26px, 36px). Every control in a row fits it with 2px to spare (⋯ and archive, the rename field and a delete confirmation's buttons 24px, a header's + and ⋯ 22px; 32px on a phone; `SessionTree.test.mjs` checks it), and the drag ghost (22px) stays shorter than a header.
31100
  -- Rows: nothing before the title; the right column shows the awaiting-input dot (amber, breathing), else the running spinner, else the unread dot, else the short time (a pinned row's project, an archived row's archive time). A running row keeps its spinner beside ⋯ on hover. Hover and selection are a rounded box inset from both edges; titles are 12px `--text` (the file tree's size), the selected one 500, and the selected row (in the archive view too) has a 3px accent bar on its box's left edge, 7px clear of its top and bottom (a 14px bar; 12px on a phone, 20px), a `::before` of the absolutely positioned row, so its rounded corners never clip it; a delete confirmation's red frame has none. A worktree's branch is meta in the rows' own type (11px dim, a 10px `BranchIcon` kept inline so the ellipsis still cuts the name; the tooltip keeps `⑂`), as a pinned row's project is (no code type). A classic scrollbar takes width on the right only (Chromium's and Safari's styled one is 10px, 6px on a coarse pointer; an overlay one none): `SessionTree` measures it into `--session-tree-scrollbar`, and the right inset shrinks by it, so rows end 6px in or on the gutter's edge, whichever is further in, while the left inset stays 6px (moving it too put the tree's two lines 4px right of everything else's in Chromium). It also puts the width on the sidebar's root (only a tree with a box: not one under the archive view or in a hidden tab), where the files below the sessions read it for their right edge. A row's time (or spinner, dot) ends 4px inside its box, where a folded group's summary ends: neither the row nor its always-rendered actions span has a gap, each shown action brings its own 2px (`margin-left`), so the hidden ones leave the time on the edge. The list keeps the scrollbar's room even when everything fits (`scrollbar-gutter: stable`; `overflow-y: scroll` in Safari before 18.2), so rows never narrow when it starts to scroll.
31101
+ +- Rows: nothing before the title; the right column shows the awaiting-input dot (amber, breathing), else the running spinner, else the unread dot, else the short time (a pinned row's project, an archived row's archive time). A running row keeps its spinner beside ⋯ on hover. In the transcript only an answer (text without tool calls) has an actions row, and its Copy shows on hover or focus with the model, usage and time (always on a touch screen); a whitespace-only text block (DeepSeek sends "\n\n" beside each tool call) renders nothing, and a running turn's messages are drawn as steps. Steps are one 4px rhythm, within a message and between messages (set on the messages' wrappers), folded or live; the answer sits 8px under its steps, open or closed. A live row has no hover Archive button (Archive is in its ⋯ menu); the archive view's rows keep Restore one click away. The toolbar's search opens the command bar (`components/SearchCommandBar.tsx`, also ⌘K / Ctrl+K outside a terminal): recent top-level conversations until something is typed, then `/api/sessions/search` matches with their text; arrows move, Enter opens, Esc closes. Hover and selection are a rounded box inset from both edges; titles are 12px `--text` (the file tree's size), the selected one 500, and the selected row (in the archive view too) has a 3px accent bar on its box's left edge, 7px clear of its top and bottom (a 14px bar; 12px on a phone, 20px), a `::before` of the absolutely positioned row, so its rounded corners never clip it; a delete confirmation's red frame has none. A worktree's branch is meta in the rows' own type (11px dim, a 10px `BranchIcon` kept inline so the ellipsis still cuts the name; the tooltip keeps `⑂`), as a pinned row's project is (no code type). A classic scrollbar takes width on the right only (Chromium's and Safari's styled one is 10px, 6px on a coarse pointer; an overlay one none): `SessionTree` measures it into `--session-tree-scrollbar`, and the right inset shrinks by it, so rows end 6px in or on the gutter's edge, whichever is further in, while the left inset stays 6px (moving it too put the tree's two lines 4px right of everything else's in Chromium). It also puts the width on the sidebar's root (only a tree with a box: not one under the archive view or in a hidden tab), where the files below the sessions read it for their right edge. A row's time (or spinner, dot) ends 4px inside its box, where a folded group's summary ends: neither the row nor its always-rendered actions span has a gap, each shown action brings its own 2px (`margin-left`), so the hidden ones leave the time on the edge. The list keeps the scrollbar's room even when everything fits (`scrollbar-gutter: stable`; `overflow-y: scroll` in Safari before 18.2), so rows never narrow when it starts to scroll.
31102
  - Row keys are unique and stable (`session:<context>:<rootId>`, `group:<key>`, `more:<key>`, `empty:<key>`, and fixed keys for the pinned header, the two spacers (`pinned-spacer`, `footer-spacer`) and the footer); focus tracking and `keepMounted` go by them.
31103
  - Menus (`SidebarMenu`) and the toast live in `SessionSidebar`, above the virtualized rows, so a row scrolled away keeps its open menu (the project/worktree picker of the files tab and of the composer's bar uses `SidebarMenu` for its own two menus); the row with focus, a menu, a rename or a delete confirmation stays mounted (`keepMounted`). Menus are portaled to `document.body`: the mobile drawer's `transform` would pin a `position: fixed` menu to the drawer. The archive view is a second tree; the main one stays mounted underneath, the two in one grid cell (`.sidebar-sessions-views`, `grid-area: 1 / 1`; the archive opaque in `--bg-panel` and stacked above the tree for good, `position: relative; z-index: 1`: the tree's rows are positioned and paint over an unpositioned view, which only its animation lifted, so the frame its entrance ended in flashed the tree). Going in and out moves, in both layouts and on phones (`useSidebarMotion()`, `archiveViewState()` in `hooks/useSidebarMotion.ts`; `setArchiveView()` starts it on a change only, and View, Back and a fork's open all go through it): in, the archive slides from 14px right and fades in over the tree (`.is-entering`, 180ms ease-out), the tree `inert` under it until the view is in, then `hidden` as before; out, the tree is back at once and the archive slides and fades out to the right over it (`.is-leaving`, 180ms ease-in, `forwards`), still mounted with its rows (`archiveRows` is built while `archiveMounted`, else it would leave empty), `inert` and `pointer-events: none`, so nothing in it is clicked twice and a click lands on the tree, until its own `animationend` (not one bubbling from a row) unmounts it. A phase that gets no end event (a sidebar not shown: a hidden tab, a closed drawer) ends 300 ms after its start (`SIDEBAR_MOTION_MS` + `SIDEBAR_MOTION_GRACE_MS`). Under `prefers-reduced-motion` no phase starts and the CSS has no animation: the views swap at once, as before.
31104
  - Focus: a menu returns it to the opener it was given when it opened (the parent drops `returnFocusTo` with the menu); a row's ⋯ or a group's buttons are `display: none` by then unless hovered, selected or focused, so `menuFocusReturnTarget()` falls back to the row's first focusable control (its main link or transient-session button, a group's toggle). Controls that go away under the keyboard hand focus on: the archive's Back button when it opens, the footer link (or the selected tab, or with no tabs the toolbar row's search) after Back, the family's row after a toast's Undo or a delete Cancel (`focusIfLost()`, only when focus fell to `<body>`). The archive's two moves make the view they leave `inert` before it is hidden or gone, and Chromium blurs an inert element only at its next frame, after the effect that moves focus has run: `focusIfHidden()` counts focus on something without a box or inside `[inert]` as lost too.
 
43824
  + </g>
43825
  +</svg>
43826
  diff --git a/public/offline.html b/public/offline.html
43827
+ index 0dac4b2..510150c 100644
43828
  --- a/public/offline.html
43829
  +++ b/public/offline.html
43830
  @@ -3,14 +3,38 @@
 
43883
  }
43884
 
43885
  main {
43886
+ @@ -30,53 +58,65 @@
43887
  text-align: center;
43888
  }
43889
 
 
43906
  + margin: 0 0 6px;
43907
  + font-size: 18px;
43908
  + font-weight: 600;
 
43909
  + line-height: 1.3;
43910
  }
43911