Mike0021 commited on
Commit
193d0f0
·
verified ·
1 Parent(s): 41cdcd3

Pi Web: no blue anywhere (neutral gray accent, underlined links, neutral code keywords and diagrams)

Browse files
Files changed (1) hide show
  1. piweb/hf-design.patch +616 -119
piweb/hf-design.patch CHANGED
@@ -1,16 +1,17 @@
1
  diff --git a/AGENTS.md b/AGENTS.md
2
- index a5c8983..0f7f118 100644
3
  --- a/AGENTS.md
4
  +++ b/AGENTS.md
5
- @@ -138,6 +138,7 @@ lib/
6
  extension-ui-queue.ts FIFO queues for extension dialogs and custom panels, by request id; isBlockingExtensionUiRequest() (which method a run waits on)
7
  markdown.ts shared markdown helpers
8
  markdown-blocks.ts splitMarkdownBlocks(): where a streaming reply may be cut into blocks parsed apart
9
  + step-summary.ts a turn's steps counted by kind (ran, read, edited, …) for their one summary line above the answer
 
10
  highlight-queue.ts code fences past a task's highlight budget get their colors in idle slices
11
  prism-languages.ts loadPrismLanguages(): every Prism language in a chunk of its own, loaded when a highlighter first needs it
12
  gfm-autolink-email-loader.cjs bundler loader: remark-gfm's email regex without a lookbehind literal
13
- @@ -174,7 +175,8 @@ lib/
14
  components/
15
  AppShell.tsx layout, URL state, tab management
16
  SessionSidebar.tsx toolbar row (Sessions | Files tabs, or the Pi Web brand with the files below them; new, search): tree, archive, menus, toast; files section (resizable, foldable; one header row with the file keys and changed-files count, the picker's row) or tab (picker boxes + file buttons), FileExplorer
@@ -20,7 +21,7 @@ index a5c8983..0f7f118 100644
20
  SessionTree.tsx virtualized rows of the session tree and the archive view
21
  SidebarMenu.tsx sidebar popup menu (bottom sheet on phones), portaled to body: filter field, item secondary actions, the files tab's classic look
22
  ProjectWorktreePicker.tsx project and worktree boxes + menus: the files' (one row below the sessions, stacked in the tab) and the new-session bar's
23
- @@ -274,3 +276,20 @@ Location: `~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl`
24
  --accent --user-bg --tool-bg
25
  --font-mono
26
  ```
@@ -41,6 +42,53 @@ index a5c8983..0f7f118 100644
41
  +`.ui-btn` classes in `app/globals.css` (`--sm`, `--xs`, `--lg`, `--icon`, `--primary`, …); the
42
  +transcript's pieces (`.turn-user`, `.turn-reply`, `.steps-summary`, `.tool-call-row`, `.composer`)
43
  +are styled there too.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  diff --git a/app/favicon.ico b/app/favicon.ico
45
  index 2da6b7dc00c64c59ef23cff153a6051478ad9315..b407f41443e3e94f4204a1862227e0d457f7d823 100644
46
  GIT binary patch
@@ -3080,10 +3128,10 @@ index 0000000..8d003fe
3080
  +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
3081
  +OTHER DEALINGS IN THE FONT SOFTWARE.
3082
  diff --git a/app/globals.css b/app/globals.css
3083
- index d6b5535..69eff6c 100644
3084
  --- a/app/globals.css
3085
  +++ b/app/globals.css
3086
- @@ -24,46 +24,107 @@
3087
  --font-mono-font: var(--font-mono);
3088
  }
3089
 
@@ -3110,10 +3158,11 @@ index d6b5535..69eff6c 100644
3110
  + --text: #2e2e2e;
3111
  + --text-muted: #525252;
3112
  + --text-dim: #666666;
3113
- + --accent: #155dfc;
3114
- + --accent-hover: #1447e6;
3115
  --accent-contrast: #ffffff;
3116
  - --user-bg: #eff6ff;
 
3117
  + --user-bg: #f5f5f5;
3118
  --assistant-bg: #ffffff;
3119
  - --tool-bg: #f9fafb;
@@ -3131,7 +3180,7 @@ index d6b5535..69eff6c 100644
3131
  + --danger-soft: rgba(193, 0, 7, 0.07);
3132
  + --success: #008236;
3133
  + --warning: #bb4d00;
3134
- + --focus-ring: #155dfc;
3135
  + --scrim: rgba(10, 10, 10, 0.32);
3136
  + --border: transparent;
3137
  + --shadow-pop: 0 10px 30px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06);
@@ -3163,9 +3212,10 @@ index d6b5535..69eff6c 100644
3163
  + --text: #d4d4d4;
3164
  + --text-muted: #a3a3a3;
3165
  + --text-dim: #8a8a8a;
3166
- + --accent: #51a2ff;
3167
- + --accent-hover: #8ec5ff;
3168
  + --accent-contrast: #0a0a0a;
 
3169
  + --user-bg: #1a1a1a;
3170
  + --assistant-bg: #0a0a0a;
3171
  + --tool-bg: #1a1a1a;
@@ -3179,7 +3229,7 @@ index d6b5535..69eff6c 100644
3179
  + --danger-soft: rgba(255, 100, 103, 0.10);
3180
  + --success: #05df72;
3181
  + --warning: #ffb900;
3182
- + --focus-ring: #51a2ff;
3183
  + --scrim: rgba(0, 0, 0, 0.6);
3184
  + --border: transparent;
3185
  + --shadow-pop: 0 12px 36px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4);
@@ -3219,7 +3269,7 @@ index d6b5535..69eff6c 100644
3219
  }
3220
 
3221
  [data-theme="mist"] {
3222
- @@ -72,7 +133,7 @@ html.dark,
3223
  --bg-panel: #e9f0ee;
3224
  --bg-hover: #e0eae7;
3225
  --bg-selected: #d7e4df;
@@ -3228,7 +3278,7 @@ index d6b5535..69eff6c 100644
3228
  --text: #202e2b;
3229
  --text-muted: #455f56;
3230
  --text-dim: #52685f;
3231
- @@ -83,6 +144,18 @@ html.dark,
3232
  --assistant-bg: #f4f8f7;
3233
  --tool-bg: #ecf3f0;
3234
  --bg-subtle: rgba(24,65,50,0.04);
@@ -3247,7 +3297,7 @@ index d6b5535..69eff6c 100644
3247
  }
3248
 
3249
  [data-theme="rose"] {
3250
- @@ -91,7 +164,7 @@ html.dark,
3251
  --bg-panel: #f3edef;
3252
  --bg-hover: #eee3e7;
3253
  --bg-selected: #e8dce1;
@@ -3256,7 +3306,7 @@ index d6b5535..69eff6c 100644
3256
  --text: #34282e;
3257
  --text-muted: #65505a;
3258
  --text-dim: #705b65;
3259
- @@ -102,6 +175,18 @@ html.dark,
3260
  --assistant-bg: #fcf7f8;
3261
  --tool-bg: #f7eef2;
3262
  --bg-subtle: rgba(89,39,62,0.04);
@@ -3275,7 +3325,7 @@ index d6b5535..69eff6c 100644
3275
  }
3276
 
3277
  html[data-theme="pine"],
3278
- @@ -111,7 +196,7 @@ html[data-theme="pine"],
3279
  --bg-panel: #212b28;
3280
  --bg-hover: #2b3632;
3281
  --bg-selected: #35433c;
@@ -3284,7 +3334,7 @@ index d6b5535..69eff6c 100644
3284
  --text: #e6ede8;
3285
  --text-muted: #c3d0c6;
3286
  --text-dim: #afc2b5;
3287
- @@ -122,6 +207,18 @@ html[data-theme="pine"],
3288
  --assistant-bg: #19201f;
3289
  --tool-bg: #222d29;
3290
  --bg-subtle: rgba(210,236,219,0.04);
@@ -3303,7 +3353,7 @@ index d6b5535..69eff6c 100644
3303
  }
3304
 
3305
  /* WebKit reports dvh/innerHeight without the status-bar strip for installed
3306
- @@ -135,28 +232,12 @@ html[data-theme="pine"],
3307
  }
3308
  }
3309
 
@@ -3336,7 +3386,7 @@ index d6b5535..69eff6c 100644
3336
  }
3337
 
3338
  /* Theme toggle: circular wipe via View Transitions API.
3339
- @@ -218,9 +299,9 @@ body {
3340
  margin-bottom: 22px;
3341
  }
3342
 
@@ -3348,16 +3398,26 @@ index d6b5535..69eff6c 100644
3348
  }
3349
 
3350
  .web-login-brand h1 {
3351
- @@ -247,7 +328,7 @@ body {
3352
  min-width: 0;
3353
  padding: 10px 10px 10px 14px;
3354
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
3355
  - border-radius: 14px;
3356
  + border-radius: var(--radius-xl);
3357
  background: var(--bg);
3358
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -12px rgba(15, 23, 42, 0.10);
 
3359
  transition: border-color 0.15s, box-shadow 0.15s;
3360
- @@ -307,7 +388,7 @@ html.dark .web-login-error {
 
 
 
 
 
 
 
 
 
3361
  min-height: 32px;
3362
  padding: 7px 14px;
3363
  border: none;
@@ -3366,16 +3426,80 @@ index d6b5535..69eff6c 100644
3366
  background: var(--accent);
3367
  color: var(--accent-contrast);
3368
  cursor: pointer;
3369
- @@ -388,7 +469,7 @@ html.dark .web-login-error {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3370
  flex-shrink: 0;
3371
  padding: 0 8px;
3372
  border: 1px solid #343a46;
3373
  - border-radius: 5px;
3374
  + border-radius: var(--radius-sm);
3375
  background: transparent;
3376
- color: #9ca3af;
 
3377
  cursor: pointer;
3378
- @@ -502,7 +583,7 @@ html[data-keyboard-open] .terminal-extra-keys {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3379
  background-color: color-mix(in srgb, var(--text-dim) 45%, transparent);
3380
  background-clip: padding-box;
3381
  border: 2px solid transparent;
@@ -3384,7 +3508,7 @@ index d6b5535..69eff6c 100644
3384
  }
3385
  /* Only ever set background-color here: the `background` shorthand resets
3386
  background-clip and would snap the thumb to the full 10px on hover. */
3387
- @@ -538,7 +619,7 @@ html[data-keyboard-open] .terminal-extra-keys {
3388
  }
3389
  ::-webkit-scrollbar-thumb {
3390
  border-width: 1px;
@@ -3393,7 +3517,7 @@ index d6b5535..69eff6c 100644
3393
  }
3394
  }
3395
 
3396
- @@ -703,9 +784,9 @@ button.extension-widget-trigger:focus-visible {
3397
 
3398
  .extension-widget-placement-icon {
3399
  display: block;
@@ -3406,7 +3530,7 @@ index d6b5535..69eff6c 100644
3406
  fill: currentColor;
3407
  }
3408
 
3409
- @@ -826,8 +907,8 @@ button.extension-widget-trigger:focus-visible {
3410
  }
3411
 
3412
  :root {
@@ -3417,13 +3541,13 @@ index d6b5535..69eff6c 100644
3417
  --font-ui: var(--font-ui-default);
3418
  --font-mono: var(--font-mono-default);
3419
  --font-ui-weight: 400;
3420
- @@ -883,11 +964,11 @@ pre, code {
3421
  .markdown-body ol > li { list-style-type: decimal; }
3422
  .markdown-body li > p { margin: 0; }
3423
  .markdown-body li::marker {
3424
  - color: color-mix(in srgb, var(--accent) 72%, var(--text-muted));
3425
  - font-weight: 600;
3426
- + color: var(--text-dim);
3427
  + font-weight: 500;
3428
  }
3429
  .markdown-body strong {
@@ -3432,7 +3556,17 @@ index d6b5535..69eff6c 100644
3432
  font-weight: 700;
3433
  }
3434
  .markdown-body em { color: var(--text-muted); }
3435
- @@ -904,16 +985,15 @@ pre, code {
 
 
 
 
 
 
 
 
 
 
3436
  max-width: 100%;
3437
  height: auto;
3438
  margin: 8px 0;
@@ -3451,7 +3585,7 @@ index d6b5535..69eff6c 100644
3451
  }
3452
  .markdown-body .contains-task-list {
3453
  padding-left: 0;
3454
- @@ -935,7 +1015,7 @@ pre, code {
3455
  height: 14px;
3456
  margin: 0;
3457
  border: 1px solid var(--border);
@@ -3460,7 +3594,7 @@ index d6b5535..69eff6c 100644
3460
  background: var(--bg);
3461
  display: inline-grid;
3462
  place-content: center;
3463
- @@ -967,8 +1047,8 @@ pre, code {
3464
  margin: 8px 0;
3465
  overflow-x: auto;
3466
  overflow-y: hidden;
@@ -3471,7 +3605,7 @@ index d6b5535..69eff6c 100644
3471
  }
3472
  .markdown-body table {
3473
  border-collapse: separate;
3474
- @@ -1088,7 +1168,7 @@ pre, code {
3475
  padding: 7px 8px;
3476
  overflow: auto;
3477
  border: 1px solid var(--border);
@@ -3480,7 +3614,7 @@ index d6b5535..69eff6c 100644
3480
  background: var(--bg-panel);
3481
  font-family: var(--font-mono);
3482
  font-size: 11px;
3483
- @@ -1108,16 +1188,13 @@ pre, code {
3484
  min-width: 0;
3485
  max-width: 100%;
3486
  margin: 6px 0;
@@ -3500,7 +3634,7 @@ index d6b5535..69eff6c 100644
3500
  color: var(--text-dim);
3501
  font-size: 11px;
3502
  display: flex;
3503
- @@ -1137,7 +1214,7 @@ pre, code {
3504
  }
3505
  .markdown-code-action {
3506
  border: 1px solid var(--border);
@@ -3509,7 +3643,7 @@ index d6b5535..69eff6c 100644
3509
  padding: 2px 7px;
3510
  background: var(--bg);
3511
  color: var(--text-muted);
3512
- @@ -1228,7 +1305,7 @@ pre, code {
3513
  max-width: 100%;
3514
  max-height: 100%;
3515
  object-fit: contain;
@@ -3518,7 +3652,7 @@ index d6b5535..69eff6c 100644
3518
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
3519
  }
3520
 
3521
- @@ -1244,7 +1321,7 @@ pre, code {
3522
  height: 36px;
3523
  padding: 0;
3524
  border: 1px solid var(--border);
@@ -3527,7 +3661,7 @@ index d6b5535..69eff6c 100644
3527
  background: var(--bg-panel);
3528
  color: var(--text-muted);
3529
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
3530
- @@ -1320,7 +1397,7 @@ pre, code {
3531
  height: 24px;
3532
  overflow: hidden;
3533
  border: 1px solid var(--border);
@@ -3536,7 +3670,7 @@ index d6b5535..69eff6c 100644
3536
  background: var(--bg);
3537
  }
3538
  .mermaid-zoom-stepper button,
3539
- @@ -1347,7 +1424,7 @@ pre, code {
3540
  .mermaid-zoom-icon-button {
3541
  flex: 0 0 24px;
3542
  border: 1px solid var(--border);
@@ -3545,7 +3679,7 @@ index d6b5535..69eff6c 100644
3545
  }
3546
  .mermaid-zoom-stepper button:hover:not(:disabled),
3547
  .mermaid-zoom-icon-button:hover {
3548
- @@ -1384,7 +1461,7 @@ pre, code {
3549
  .mermaid-zoom-canvas {
3550
  padding: 12px;
3551
  border: 1px solid var(--border);
@@ -3554,7 +3688,7 @@ index d6b5535..69eff6c 100644
3554
  background: var(--bg-panel);
3555
  margin: 0 auto;
3556
  transition: width 0.15s ease;
3557
- @@ -1460,7 +1537,7 @@ pre, code {
3558
  height: 24px;
3559
  overflow: hidden;
3560
  border: 1px solid var(--border);
@@ -3563,7 +3697,7 @@ index d6b5535..69eff6c 100644
3563
  }
3564
 
3565
  .file-viewer-mode-button {
3566
- @@ -1503,12 +1580,11 @@ pre, code {
3567
  height: 32px;
3568
  padding: 0;
3569
  box-sizing: border-box;
@@ -3578,7 +3712,7 @@ index d6b5535..69eff6c 100644
3578
  opacity: 0;
3579
  visibility: hidden;
3580
  pointer-events: none;
3581
- @@ -1584,22 +1660,21 @@ pre, code {
3582
 
3583
  .new-session-logo {
3584
  flex: none;
@@ -3607,7 +3741,7 @@ index d6b5535..69eff6c 100644
3607
  line-height: 1.15;
3608
  white-space: nowrap;
3609
  }
3610
- @@ -1613,7 +1688,8 @@ pre, code {
3611
  gap: 6px;
3612
  min-width: 0;
3613
  color: var(--text-dim);
@@ -3617,7 +3751,7 @@ index d6b5535..69eff6c 100644
3617
  line-height: 1.4;
3618
  white-space: nowrap;
3619
  }
3620
- @@ -1638,7 +1714,7 @@ pre, code {
3621
  gap: 3px;
3622
  margin: -3px -4px;
3623
  padding: 3px 4px;
@@ -3626,7 +3760,7 @@ index d6b5535..69eff6c 100644
3626
  color: var(--accent);
3627
  font-weight: 600;
3628
  text-decoration: none;
3629
- @@ -1652,7 +1728,7 @@ pre, code {
3630
 
3631
  /* The ring inside the box, clear of the "→" and "·" beside it. */
3632
  .new-session-update:focus-visible {
@@ -3635,7 +3769,7 @@ index d6b5535..69eff6c 100644
3635
  outline-offset: -2px;
3636
  }
3637
 
3638
- @@ -1678,12 +1754,12 @@ pre, code {
3639
  }
3640
 
3641
  .new-session-logo {
@@ -3651,7 +3785,7 @@ index d6b5535..69eff6c 100644
3651
  }
3652
  }
3653
 
3654
- @@ -1722,14 +1798,14 @@ pre, code {
3655
  min-width: 0;
3656
  height: 29px;
3657
  padding: 0 10px;
@@ -3669,7 +3803,7 @@ index d6b5535..69eff6c 100644
3669
  }
3670
 
3671
  /* Above a fresh composer (inline): no box until hovered or open, 12px, the
3672
- @@ -1752,7 +1828,7 @@ pre, code {
3673
  height: 26px;
3674
  padding: 0 6px;
3675
  border: 0;
@@ -3678,7 +3812,7 @@ index d6b5535..69eff6c 100644
3678
  background: transparent;
3679
  }
3680
 
3681
- @@ -1781,8 +1857,9 @@ pre, code {
3682
  max-width: 28ch;
3683
  overflow: hidden;
3684
  color: var(--text-dim);
@@ -3689,7 +3823,7 @@ index d6b5535..69eff6c 100644
3689
  }
3690
 
3691
  .project-picker-parent {
3692
- @@ -1851,7 +1928,8 @@ pre, code {
3693
  path's color and code type, cut at its end. */
3694
  .project-picker-label.is-alias {
3695
  color: var(--text);
@@ -3699,7 +3833,7 @@ index d6b5535..69eff6c 100644
3699
  }
3700
 
3701
  /* A path or a branch, cut at its left (rtl), the text itself kept left to
3702
- @@ -1901,7 +1979,8 @@ pre, code {
3703
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
3704
  background: color-mix(in srgb, var(--accent) 6%, transparent);
3705
  color: var(--text-dim);
@@ -3709,7 +3843,7 @@ index d6b5535..69eff6c 100644
3709
  }
3710
 
3711
  /* No worktree list here (the files tab): a box that says why, and does nothing. */
3712
- @@ -1978,7 +2057,7 @@ pre, code {
3713
  box-sizing: border-box;
3714
  flex: 0 0 24px;
3715
  border: 1px solid var(--border);
@@ -3718,7 +3852,7 @@ index d6b5535..69eff6c 100644
3718
  background: transparent;
3719
  color: var(--text-muted);
3720
  cursor: pointer;
3721
- @@ -2037,7 +2116,7 @@ pre, code {
3722
  .markdown-file-preview pre {
3723
  background: var(--bg-panel);
3724
  border: 1px solid var(--border);
@@ -3727,7 +3861,7 @@ index d6b5535..69eff6c 100644
3727
  padding: 12px 16px;
3728
  overflow-x: auto;
3729
  font-family: var(--font-mono);
3730
- @@ -2049,7 +2128,7 @@ pre, code {
3731
  font-size: 0.9em;
3732
  background: var(--bg-panel);
3733
  padding: 1px 5px;
@@ -3736,7 +3870,7 @@ index d6b5535..69eff6c 100644
3736
  }
3737
  .markdown-file-preview pre code {
3738
  background: none;
3739
- @@ -2061,7 +2140,7 @@ pre, code {
3740
  margin-bottom: 20px;
3741
  padding: 14px 16px 12px;
3742
  border: 1px solid var(--border);
@@ -3745,7 +3879,7 @@ index d6b5535..69eff6c 100644
3745
  background: var(--bg-panel);
3746
  }
3747
  .markdown-frontmatter-title {
3748
- @@ -2474,3 +2553,428 @@ span.token.table {
3749
  display: none !important;
3750
  }
3751
  }
@@ -4292,7 +4426,7 @@ index 79c8282..97731f1 100644
4292
  lang: "en",
4293
  icons: [
4294
  diff --git a/app/settings.css b/app/settings.css
4295
- index 0bf21eb..4938ff2 100644
4296
  --- a/app/settings.css
4297
  +++ b/app/settings.css
4298
  @@ -40,9 +40,9 @@
@@ -4424,15 +4558,19 @@ index 0bf21eb..4938ff2 100644
4424
  background: var(--bg-panel);
4425
  overflow-y: auto;
4426
  }
4427
- @@ -457,7 +458,7 @@
4428
  .enabled-models-pin {
4429
  flex-shrink: 0;
4430
  padding: 1px 4px;
4431
  - border-radius: 3px;
 
 
4432
  + border-radius: var(--radius-xs);
4433
- background: rgba(99, 102, 241, 0.12);
4434
- color: rgba(99, 102, 241, 0.8);
4435
  font-size: 9px;
 
 
4436
  @@ -497,7 +498,7 @@
4437
  justify-content: space-between;
4438
  gap: 18px;
@@ -4469,7 +4607,7 @@ index 0bf21eb..4938ff2 100644
4469
  }
4470
 
4471
  .config-field-label {
4472
- @@ -697,7 +698,7 @@
4473
  .config-scope-tag {
4474
  flex-shrink: 0;
4475
  padding: 1px 5px;
@@ -4478,6 +4616,16 @@ index 0bf21eb..4938ff2 100644
4478
  background: rgba(120, 120, 120, 0.12);
4479
  color: var(--text-dim);
4480
  font-size: 10px;
 
 
 
 
 
 
 
 
 
 
4481
  @@ -730,7 +731,7 @@
4482
  height: 30px;
4483
  overflow: hidden;
@@ -5119,7 +5267,7 @@ index 737e7a5..19466a2 100644
5119
  color: var(--text-muted);
5120
  font: inherit;
5121
  diff --git a/app/sidebar.css b/app/sidebar.css
5122
- index 86728a2..fd45b6f 100644
5123
  --- a/app/sidebar.css
5124
  +++ b/app/sidebar.css
5125
  @@ -91,7 +91,7 @@
@@ -5131,6 +5279,15 @@ index 86728a2..fd45b6f 100644
5131
  outline-offset: -2px;
5132
  }
5133
 
 
 
 
 
 
 
 
 
 
5134
  @@ -184,7 +184,7 @@
5135
  right: var(--session-tree-inset-right);
5136
  left: var(--session-tree-inset-left);
@@ -5176,6 +5333,23 @@ index 86728a2..fd45b6f 100644
5176
  cursor: default;
5177
  }
5178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5179
  @@ -297,20 +276,23 @@ a.session-tree-main {
5180
  }
5181
  }
@@ -5297,6 +5471,15 @@ index 86728a2..fd45b6f 100644
5297
  text-align: left;
5298
  white-space: nowrap;
5299
  }
 
 
 
 
 
 
 
 
 
5300
  @@ -726,7 +714,7 @@ a.session-tree-main {
5301
  width: 22px;
5302
  height: 22px;
@@ -5646,7 +5829,7 @@ index 861a1c6..d5d466e 100644
5646
  <span>{savedOk ? t("i18n.saved") : saving ? t("agents.saving") : t("agents.save")}</span>
5647
  </ConfigButton>
5648
  diff --git a/components/AppShell.tsx b/components/AppShell.tsx
5649
- index c8c4ba9..fb61efb 100644
5650
  --- a/components/AppShell.tsx
5651
  +++ b/components/AppShell.tsx
5652
  @@ -3,6 +3,8 @@
@@ -6147,6 +6330,15 @@ index c8c4ba9..fb61efb 100644
6147
  </button>
6148
  );
6149
  };
 
 
 
 
 
 
 
 
 
6150
  @@ -2178,13 +2018,9 @@ export function AppShell() {
6151
  onMouseLeave={(e) => { e.currentTarget.style.color = "var(--text-muted)"; }}
6152
  >
@@ -6256,7 +6448,7 @@ index c8c4ba9..fb61efb 100644
6256
  </div>
6257
 
6258
  diff --git a/components/BranchNavigator.tsx b/components/BranchNavigator.tsx
6259
- index 0f8186b..e7fde3d 100644
6260
  --- a/components/BranchNavigator.tsx
6261
  +++ b/components/BranchNavigator.tsx
6262
  @@ -3,6 +3,8 @@
@@ -6268,11 +6460,15 @@ index 0f8186b..e7fde3d 100644
6268
 
6269
  interface Props {
6270
  tree: SessionTreeNode[];
6271
- @@ -209,7 +211,7 @@ function TreeNodeView({ node, activePathIds, depth, isLast, parentLines, onSelec
 
 
6272
  color: role === "user" ? "var(--accent)" : "var(--text-dim)",
6273
- background: role === "user" ? "rgba(37,99,235,0.08)" : "var(--bg-hover)",
6274
- border: `1px solid ${role === "user" ? "rgba(37,99,235,0.2)" : "var(--border)"}`,
6275
  - borderRadius: 3,
 
 
6276
  + borderRadius: "var(--radius-xs)",
6277
  padding: "0 4px",
6278
  marginRight: 5,
@@ -6376,7 +6572,7 @@ index adfcc12..cb17981 100644
6376
  assert.doesNotMatch(html, /type="file"|Attach files|Change tool preset/);
6377
  });
6378
  diff --git a/components/ChatInput.tsx b/components/ChatInput.tsx
6379
- index 9a0a1b7..7dd9112 100644
6380
  --- a/components/ChatInput.tsx
6381
  +++ b/components/ChatInput.tsx
6382
  @@ -35,6 +35,8 @@ import { useFontPreferences } from "@/hooks/useFontPreferences";
@@ -6853,10 +7049,10 @@ index 9a0a1b7..7dd9112 100644
6853
  + style={{
6854
  + display: "flex", alignItems: "center", gap: 5,
6855
  + padding: "7px 12px",
6856
- + background: canQueueStreamingMessage ? "rgba(129,140,248,0.14)" : "var(--bg-subtle)",
6857
  + border: "none",
6858
  + borderRadius: "var(--radius-sm)",
6859
- + color: canQueueStreamingMessage ? "rgba(99,102,241,1)" : "var(--text-dim)",
6860
  + cursor: canQueueStreamingMessage ? "pointer" : "not-allowed",
6861
  + fontSize: 13, fontWeight: 600, letterSpacing: "-0.01em",
6862
  + transition: "background 0.12s",
@@ -7678,7 +7874,7 @@ index 2c78471..12d53ec 100644
7678
 
7679
  {minimapHovered && allNodes.length > 0 && (
7680
  diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx
7681
- index 6c540a4..900d76c 100644
7682
  --- a/components/ChatWindow.tsx
7683
  +++ b/components/ChatWindow.tsx
7684
  @@ -1,19 +1,20 @@
@@ -7818,7 +8014,19 @@ index 6c540a4..900d76c 100644
7818
  export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initialScrollPosition, onScrollPositionChange, sessionRunning, newSessionCwd, newSessionDraftKey, newSessionContextBar, initialNewSessionChoices, onNewSessionChoicesChange, onAgentEnd, onToolEnd, onAttentionNeeded, onSessionCreated, onSessionForked, modelsRefreshKey, chatInputRef, onBranchDataChange, onSystemPromptChange, onSystemToolsChange, onSystemInfoLoaderChange, onSessionStatsChange, onSessionStatsPanelOpen, onOpenSettings, onContextUsageChange, onOpenFile, onFilesUploaded, onOpenSubagent, onAskInNewChat, quoteSelectionEnabled = false, initialPrompt, onInitialPromptConsumed, soundEnabled = true, onSoundToggle, playDoneSound = () => {}, unlockAudio, holdAudio }: Props) {
7819
  const { t } = useI18n();
7820
  const isMobile = useIsMobile();
7821
- @@ -1024,24 +1035,7 @@ export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initi
 
 
 
 
 
 
 
 
 
 
 
 
7822
  />
7823
  ))}
7824
  </div>
@@ -7966,6 +8174,17 @@ index 6c540a4..900d76c 100644
7966
  <div className="new-session-brand-text">
7967
  <span className="new-session-brand-name">Pi Web</span>
7968
  <span className="new-session-meta">
 
 
 
 
 
 
 
 
 
 
 
7969
  @@ -1648,11 +1643,7 @@ function ExtensionWaitingCount({ count }: { count: number }) {
7970
  /** Corner brackets pointing outward; when expanded they point inward (restore). */
7971
  function ExtensionSizeIcon({ expanded }: { expanded: boolean }) {
@@ -8295,7 +8514,7 @@ index 5bd7070..f6586f6 100644
8295
  <span className="extension-widget-key">{widget.key}</span>
8296
  </>
8297
  diff --git a/components/FileExplorer.tsx b/components/FileExplorer.tsx
8298
- index 94cf736..77f0807 100644
8299
  --- a/components/FileExplorer.tsx
8300
  +++ b/components/FileExplorer.tsx
8301
  @@ -18,6 +18,8 @@ import { buildSearchTree, type SearchTreeNode } from "@/lib/search-tree";
@@ -8307,6 +8526,15 @@ index 94cf736..77f0807 100644
8307
  type Translate = ReturnType<typeof useI18n>["t"];
8308
 
8309
  /** Why a listed entry is normally left out; only listings with `hidden=1` report it. */
 
 
 
 
 
 
 
 
 
8310
  @@ -174,20 +176,13 @@ function GitStatusBadge({ status, t }: { status: GitFileStatus; t: Translate })
8311
 
8312
  function MentionIcon({ size = 11 }: { size?: number }) {
@@ -8351,6 +8579,15 @@ index 94cf736..77f0807 100644
8351
  )}
8352
  {!node.isDir && <span style={{ width: 10, flexShrink: 0 }} />}
8353
  <span style={{ flexShrink: 0, display: "flex", alignItems: "center", opacity: hidden ? HIDDEN_ENTRY_OPACITY : 1 }}>
 
 
 
 
 
 
 
 
 
8354
  @@ -419,9 +408,7 @@ export function TreeNode({
8355
  </span>
8356
  )}
@@ -8621,10 +8858,18 @@ index b46d21a..8a8b640 100644
8621
 
8622
  export function FolderIcon({ size = 14, open = false }: IconProps & { open?: boolean }) {
8623
  diff --git a/components/FileViewer.tsx b/components/FileViewer.tsx
8624
- index 9426211..5c5712b 100644
8625
  --- a/components/FileViewer.tsx
8626
  +++ b/components/FileViewer.tsx
8627
- @@ -34,6 +34,8 @@ import {
 
 
 
 
 
 
 
 
8628
  type FileViewerDisplayMode as DisplayMode,
8629
  type FileViewerState,
8630
  } from "@/lib/file-viewer-state";
@@ -8633,7 +8878,24 @@ index 9426211..5c5712b 100644
8633
 
8634
  export type { FileViewerState } from "@/lib/file-viewer-state";
8635
 
8636
- @@ -119,10 +121,7 @@ interface SelectedLineRange {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8637
 
8638
  function MentionIcon() {
8639
  return (
@@ -8645,7 +8907,7 @@ index 9426211..5c5712b 100644
8645
  );
8646
  }
8647
 
8648
- @@ -252,11 +251,7 @@ function DownloadLink({ filePath, sourceSessionId }: { filePath: string; sourceS
8649
  aria-label={t("i18n.downloadFile")}
8650
  className="file-viewer-icon-button"
8651
  >
@@ -8658,7 +8920,16 @@ index 9426211..5c5712b 100644
8658
  </a>
8659
  );
8660
  }
8661
- @@ -1708,12 +1703,7 @@ function TextFileViewer({
 
 
 
 
 
 
 
 
 
8662
  color: wrapLines ? "var(--text)" : "var(--text-muted)",
8663
  }}
8664
  >
@@ -8672,7 +8943,7 @@ index 9426211..5c5712b 100644
8672
  </button>
8673
  </>
8674
  )}
8675
- @@ -1733,7 +1723,7 @@ function TextFileViewer({
8676
  gap: 10,
8677
  padding: "5px 8px",
8678
  border: "1px solid var(--border)",
@@ -8730,19 +9001,29 @@ index 9b81f64..22b3e6d 100644
8730
  </dialog>
8731
  )}
8732
  diff --git a/components/MermaidBlock.tsx b/components/MermaidBlock.tsx
8733
- index 25e4a63..75bd5f2 100644
8734
  --- a/components/MermaidBlock.tsx
8735
  +++ b/components/MermaidBlock.tsx
8736
- @@ -10,6 +10,8 @@ import { usePrismLanguages } from "@/hooks/usePrismLanguages";
8737
  import { copyText } from "@/lib/clipboard";
8738
  import { takeHighlightBudget, waitForHighlight } from "@/lib/highlight-queue";
8739
  import { prismLanguagesLoaded } from "@/lib/prism-languages";
 
8740
  +import { CarbonIcon } from "./CarbonIcon";
8741
  +import * as Carbon from "./carbon/icons";
8742
 
8743
  interface MermaidBlockProps {
8744
  code: string;
8745
- @@ -194,9 +196,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
 
 
 
 
 
 
 
 
 
8746
  title={t("i18n.zoomOut")}
8747
  aria-label={t("i18n.zoomOut")}
8748
  >
@@ -8753,7 +9034,7 @@ index 25e4a63..75bd5f2 100644
8753
  </button>
8754
  <span className="mermaid-zoom-value">{Math.round(zoom * 100)}%</span>
8755
  <button
8756
- @@ -206,9 +206,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
8757
  title={t("i18n.zoomIn")}
8758
  aria-label={t("i18n.zoomIn")}
8759
  >
@@ -8764,7 +9045,7 @@ index 25e4a63..75bd5f2 100644
8764
  </button>
8765
  </div>
8766
  <button
8767
- @@ -218,9 +216,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
8768
  title={t("i18n.fitToWidth")}
8769
  aria-label={t("i18n.fitToWidth")}
8770
  >
@@ -8775,7 +9056,7 @@ index 25e4a63..75bd5f2 100644
8775
  </button>
8776
  <button
8777
  type="button"
8778
- @@ -229,9 +225,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
8779
  title={t("i18n.close")}
8780
  aria-label={t("i18n.close")}
8781
  >
@@ -8786,6 +9067,32 @@ index 25e4a63..75bd5f2 100644
8786
  </button>
8787
  </div>
8788
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8789
  diff --git a/components/MessageView.test.mjs b/components/MessageView.test.mjs
8790
  index 515b033..eeb4c1e 100644
8791
  --- a/components/MessageView.test.mjs
@@ -8812,7 +9119,7 @@ index 515b033..eeb4c1e 100644
8812
 
8813
  test("collapses a displayed custom message from its header alone", () => {
8814
  diff --git a/components/MessageView.tsx b/components/MessageView.tsx
8815
- index f2ff1de..de7e3d6 100644
8816
  --- a/components/MessageView.tsx
8817
  +++ b/components/MessageView.tsx
8818
  @@ -1,10 +1,11 @@
@@ -8914,7 +9221,7 @@ index f2ff1de..de7e3d6 100644
8914
  src={src}
8915
  alt=""
8916
  - style={{ maxWidth: 240, maxHeight: 240, borderRadius: 6, objectFit: "contain", display: "block", border: "1px solid rgba(59,130,246,0.15)" }}
8917
- + style={{ maxWidth: 240, maxHeight: 240, borderRadius: "var(--radius-sm)", objectFit: "contain", display: "block", border: "1px solid rgba(59,130,246,0.15)" }}
8918
  />
8919
  </ImagePreview>
8920
  );
@@ -9895,6 +10202,30 @@ index f2ff1de..de7e3d6 100644
9895
  const { t } = useI18n();
9896
  const showFileHeaders = files.length > 1;
9897
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9898
  @@ -1587,7 +1456,6 @@ function ResultImages({ images, isError }: { images: ImageContent[]; isError: bo
9899
  flexWrap: "wrap",
9900
  padding: "10px",
@@ -10106,7 +10437,7 @@ index ba912ee..db662d8 100644
10106
  <SelectorRow active={!value} gutter={Boolean(onSetDefault)} onSelect={() => {
10107
  setOpen(false);
10108
  diff --git a/components/ModelsConfig.tsx b/components/ModelsConfig.tsx
10109
- index 9f6b556..7063172 100644
10110
  --- a/components/ModelsConfig.tsx
10111
  +++ b/components/ModelsConfig.tsx
10112
  @@ -54,6 +54,8 @@ import { providerBadgeLabel } from "./enabled-models-helpers";
@@ -10192,7 +10523,23 @@ index 9f6b556..7063172 100644
10192
  >
10193
  {selectedCount
10194
  ? t("models.discoveryAddSelectedCount", { count: selectedCount })
10195
- @@ -627,7 +621,7 @@ function ThinkingLevelMapEditor({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10196
  alignItems: "center",
10197
  gap: 8,
10198
  padding: "5px 4px",
@@ -10201,7 +10548,7 @@ index 9f6b556..7063172 100644
10201
  background: "transparent",
10202
  border: "1px solid transparent",
10203
  }}
10204
- @@ -644,7 +638,7 @@ function ThinkingLevelMapEditor({
10205
  </span>
10206
  </div>
10207
 
@@ -10210,7 +10557,7 @@ index 9f6b556..7063172 100644
10210
  <button
10211
  onClick={() => setLevel(level, "omit")}
10212
  style={{ ...btnBase, ...(state === "omit" ? btnActive : {}) }}
10213
- @@ -659,7 +653,7 @@ function ThinkingLevelMapEditor({
10214
  </button>
10215
  </div>
10216
 
@@ -10219,7 +10566,7 @@ index 9f6b556..7063172 100644
10219
  <button
10220
  onClick={() => setLevel(level, strVal || level)}
10221
  style={{ ...btnBase, ...(state === "string" ? btnActive : {}), borderRight: "1px solid var(--border)", flexShrink: 0 }}
10222
- @@ -747,7 +741,7 @@ function HeaderListEditor({ headers, onChange }: {
10223
  padding: "6px 9px",
10224
  background: "none",
10225
  border: "1px solid rgba(239,68,68,0.3)",
@@ -10228,7 +10575,7 @@ index 9f6b556..7063172 100644
10228
  color: "#ef4444",
10229
  cursor: "pointer",
10230
  fontSize: 11,
10231
- @@ -768,7 +762,7 @@ function HeaderListEditor({ headers, onChange }: {
10232
  ...current,
10233
  { id: nextRowIdRef.current++, name: "", value: "" },
10234
  ])}
@@ -10237,16 +10584,20 @@ index 9f6b556..7063172 100644
10237
  + Add header
10238
  </button>
10239
  </div>
10240
- @@ -1051,7 +1045,7 @@ function ModelDetail({
10241
  height: 28,
10242
  padding: "0 8px",
10243
  border: `1px solid ${testState.phase === "error" ? "#fecaca" : testState.phase === "success" ? "#bbf7d0" : "var(--border)"}`,
10244
  - borderRadius: 4,
 
 
10245
  + borderRadius: "var(--radius-xs)",
10246
- background: testState.phase === "error" ? "#fee2e2" : testState.phase === "success" ? "#dcfce7" : "#e5e7eb",
10247
- color: "#111827",
10248
  fontSize: 11,
10249
- @@ -1075,9 +1069,7 @@ function ModelDetail({
 
 
10250
  className={testState.phase === "success" ? "is-success" : undefined}
10251
  >
10252
  {testState.phase === "success" && (
@@ -10257,7 +10608,7 @@ index 9f6b556..7063172 100644
10257
  )}
10258
  {testState.phase === "testing" ? t("i18n.checking") : testState.phase === "success" ? t("common.ok") : t("i18n.test")}
10259
  </ConfigButton>
10260
- @@ -1096,7 +1088,7 @@ function ModelDetail({
10261
  onClick={() => void handleCatalogFill()}
10262
  disabled={!model.id.trim() || catalogState.phase === "loading"}
10263
  style={{
@@ -10266,7 +10617,7 @@ index 9f6b556..7063172 100644
10266
  background: "var(--bg-panel)",
10267
  color: !model.id.trim() || catalogState.phase === "loading" ? "var(--text-dim)" : "var(--text-muted)",
10268
  cursor: !model.id.trim() || catalogState.phase === "loading" ? "not-allowed" : "pointer",
10269
- @@ -1227,20 +1219,7 @@ function ModelDetail({
10270
  {advancedSummary}
10271
  </span>
10272
  </span>
@@ -10288,7 +10639,7 @@ index 9f6b556..7063172 100644
10288
  </button>
10289
 
10290
  {advancedOpen && (
10291
- @@ -1492,7 +1471,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels }: {
10292
  <button
10293
  key={option.id}
10294
  onClick={() => submitSelection(loginState.token, option.id)}
@@ -10297,7 +10648,7 @@ index 9f6b556..7063172 100644
10297
  >
10298
  {option.label}
10299
  </button>
10300
- @@ -1527,7 +1506,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels }: {
10301
  <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
10302
  Open the verification page and enter this code:
10303
  </p>
@@ -10306,7 +10657,7 @@ index 9f6b556..7063172 100644
10306
  {loginState.userCode}
10307
  </div>
10308
  <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
10309
- @@ -1666,7 +1645,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels }: {
10310
  style={{
10311
  padding: "6px 12px",
10312
  background: savedOk ? "#16a34a" : apiKey.trim() ? "var(--accent)" : "var(--bg-panel)",
@@ -10315,7 +10666,7 @@ index 9f6b556..7063172 100644
10315
  color: savedOk ? "#fff" : apiKey.trim() ? "var(--accent-contrast)" : "var(--text-dim)",
10316
  cursor: (saving || !apiKey.trim() || savedOk) ? "not-allowed" : "pointer",
10317
  fontSize: 12, fontWeight: 600, flexShrink: 0,
10318
- @@ -1674,9 +1653,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels }: {
10319
  }}
10320
  >
10321
  {savedOk && (
@@ -10326,7 +10677,7 @@ index 9f6b556..7063172 100644
10326
  )}
10327
  {savedOk ? t("i18n.saved") : saving ? t("i18n.saving") : t("i18n.save")}
10328
  </button>
10329
- @@ -1725,7 +1702,7 @@ function AddProviderPicker({
10330
  padding: "10px 12px",
10331
  background: "var(--bg-panel)",
10332
  border: "1px solid var(--border)",
@@ -10335,7 +10686,7 @@ index 9f6b556..7063172 100644
10335
  boxSizing: "border-box",
10336
  cursor: "pointer",
10337
  minWidth: 0,
10338
- @@ -1747,12 +1724,10 @@ function AddProviderPicker({
10339
  onClose();
10340
  }}
10341
  >
@@ -10350,7 +10701,7 @@ index 9f6b556..7063172 100644
10350
  <input
10351
  ref={inputRef}
10352
  value={search}
10353
- @@ -1782,10 +1757,8 @@ function AddProviderPicker({
10354
  <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>OpenAI / Anthropic compatible</div>
10355
  <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>{t("i18n.customEndpoint")}</div>
10356
  </div>
@@ -10363,7 +10714,7 @@ index 9f6b556..7063172 100644
10363
  </span>
10364
  </button>
10365
  )}
10366
- @@ -2177,13 +2150,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
10367
  onClick={() => setSelection({ type: "provider", name: pName })}
10368
  active={isProviderSelected}
10369
  >
@@ -10378,16 +10729,16 @@ index 9f6b556..7063172 100644
10378
  <ConfigSidebarText className="is-grow">
10379
  {pName}
10380
  </ConfigSidebarText>
10381
- @@ -2204,7 +2171,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
10382
  {m.id || t("i18n.newModel")}
10383
  </ConfigSidebarText>
10384
  {m.reasoning && (
10385
  - <span style={{ fontSize: 9, padding: "1px 4px", background: "rgba(99,102,241,0.12)", color: "rgba(99,102,241,0.8)", borderRadius: 3, flexShrink: 0 }}>T</span>
10386
- + <span style={{ fontSize: 9, padding: "1px 4px", background: "rgba(99,102,241,0.12)", color: "rgba(99,102,241,0.8)", borderRadius: "var(--radius-xs)", flexShrink: 0 }}>T</span>
10387
  )}
10388
  </ConfigSidebarItem>
10389
  );
10390
- @@ -2250,10 +2217,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
10391
  className={savedOk ? "is-success" : undefined}
10392
  >
10393
  {savedOk && (
@@ -10641,7 +10992,7 @@ index cf38be6..c08aaef 100644
10641
  </button>
10642
  {report && (
10643
  diff --git a/components/SelectorRow.tsx b/components/SelectorRow.tsx
10644
- index 555da8f..e64352a 100644
10645
  --- a/components/SelectorRow.tsx
10646
  +++ b/components/SelectorRow.tsx
10647
  @@ -2,12 +2,12 @@
@@ -10698,6 +11049,15 @@ index 555da8f..e64352a 100644
10698
  cursor: "pointer",
10699
  opacity: showSave ? 1 : 0,
10700
  pointerEvents: showSave ? "auto" : "none",
 
 
 
 
 
 
 
 
 
10701
  diff --git a/components/SessionSidebar.test.mjs b/components/SessionSidebar.test.mjs
10702
  index 6bc923f..70b9e80 100644
10703
  --- a/components/SessionSidebar.test.mjs
@@ -10730,7 +11090,7 @@ index 6bc923f..70b9e80 100644
10730
  // search in the toolbar row's place.
10731
  const head = html.slice(html.indexOf('<div class="sidebar-files-head">'), html.indexOf('<div class="sidebar-files-scroll'));
10732
  diff --git a/components/SessionTree.test.mjs b/components/SessionTree.test.mjs
10733
- index fb53e4b..f6596f1 100644
10734
  --- a/components/SessionTree.test.mjs
10735
  +++ b/components/SessionTree.test.mjs
10736
  @@ -119,7 +119,7 @@ test("the pinned header toggles the section and shows activity dots only while c
@@ -10751,12 +11111,14 @@ index fb53e4b..f6596f1 100644
10751
  assert.match(html, /<span class="session-tree-group-name">app<\/span><svg[^>]*class="session-tree-group-pin"[\s\S]*?<\/svg><span class="session-tree-summary">/);
10752
  // No folder: a project group is no second file tree.
10753
  assert.doesNotMatch(html, /session-tree-group-icon/);
10754
- @@ -468,11 +468,12 @@ test("row CSS stays flat, themed and quiet", () => {
 
10755
  assert.doesNotMatch(css, /@starting-style|prefers-color-scheme/);
10756
  const colors = new Set((css.match(/#[0-9a-fA-F]{3,8}\b/g) ?? []).map((color) => color.toLowerCase()));
10757
- for (const color of colors) assert.ok(["#ef4444", "#0891b2", "#d97706", "#f87171", "#fff"].includes(color), `unexpected color ${color}`);
10758
  - // Three text colors: content, labels, meta (plus the accent for states,
10759
  - // the errors' red and the delete button's white). No mixes of their own.
 
10760
  + // Three text colors: content, labels, meta, the selected title a step
10761
  + // stronger (plus the accent for states, the errors' red and the delete
10762
  + // button's white). No mixes of their own.
@@ -11316,7 +11678,7 @@ index 23e0c79..2919906 100644
11316
  </div>
11317
  );
11318
  diff --git a/components/TerminalPanel.tsx b/components/TerminalPanel.tsx
11319
- index d24bb94..28a4582 100644
11320
  --- a/components/TerminalPanel.tsx
11321
  +++ b/components/TerminalPanel.tsx
11322
  @@ -18,6 +18,8 @@ import {
@@ -11328,7 +11690,25 @@ index d24bb94..28a4582 100644
11328
 
11329
  interface Props {
11330
  tab: TerminalTab;
11331
- @@ -260,15 +262,11 @@ export function TerminalPanel({ tab, active, onRestart, onClosed, onCloseError }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11332
  </div>
11333
  {status === "error" && (
11334
  <button type="button" onClick={() => setReconnectKey((key) => key + 1)} disabled={Boolean(tab.closing)} title={t("terminal.reconnect")} aria-label={t("terminal.reconnect")}>
@@ -12061,6 +12441,123 @@ index d8b6d41..e9a45e1 100644
12061
  "chat.filesWritten": "已變更的檔案",
12062
  "chat.openWrittenFile": "開啟 {name}",
12063
  "chat.loadEarlier": "向上捲動以載入較早的訊息",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12064
  diff --git a/lib/step-summary.test.mjs b/lib/step-summary.test.mjs
12065
  new file mode 100644
12066
  index 0000000..6cbe005
 
1
  diff --git a/AGENTS.md b/AGENTS.md
2
+ index a5c8983..c40477c 100644
3
  --- a/AGENTS.md
4
  +++ b/AGENTS.md
5
+ @@ -138,6 +138,8 @@ lib/
6
  extension-ui-queue.ts FIFO queues for extension dialogs and custom panels, by request id; isBlockingExtensionUiRequest() (which method a run waits on)
7
  markdown.ts shared markdown helpers
8
  markdown-blocks.ts splitMarkdownBlocks(): where a streaming reply may be cut into blocks parsed apart
9
  + step-summary.ts a turn's steps counted by kind (ran, read, edited, …) for their one summary line above the answer
10
+ + neutral-code-theme.ts the Prism themes (dark+, vs) with their blue tokens in neutral grays, keywords bold; Mermaid's neutral base theme
11
  highlight-queue.ts code fences past a task's highlight budget get their colors in idle slices
12
  prism-languages.ts loadPrismLanguages(): every Prism language in a chunk of its own, loaded when a highlighter first needs it
13
  gfm-autolink-email-loader.cjs bundler loader: remark-gfm's email regex without a lookbehind literal
14
+ @@ -174,7 +176,8 @@ lib/
15
  components/
16
  AppShell.tsx layout, URL state, tab management
17
  SessionSidebar.tsx toolbar row (Sessions | Files tabs, or the Pi Web brand with the files below them; new, search): tree, archive, menus, toast; files section (resizable, foldable; one header row with the file keys and changed-files count, the picker's row) or tab (picker boxes + file buttons), FileExplorer
 
21
  SessionTree.tsx virtualized rows of the session tree and the archive view
22
  SidebarMenu.tsx sidebar popup menu (bottom sheet on phones), portaled to body: filter field, item secondary actions, the files tab's classic look
23
  ProjectWorktreePicker.tsx project and worktree boxes + menus: the files' (one row below the sessions, stacked in the tab) and the new-session bar's
24
+ @@ -274,3 +277,23 @@ Location: `~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl`
25
  --accent --user-bg --tool-bg
26
  --font-mono
27
  ```
 
42
  +`.ui-btn` classes in `app/globals.css` (`--sm`, `--xs`, `--lg`, `--icon`, `--primary`, …); the
43
  +transcript's pieces (`.turn-user`, `.turn-reply`, `.steps-summary`, `.tool-call-row`, `.composer`)
44
  +are styled there too.
45
+ +`--accent` is a neutral gray in light and dark (no blue anywhere: links are told apart by their
46
+ +underline, focus by a 2px ring), `--accent-soft` its tint; Mist, Rose and Pine keep their own hues.
47
+ +Code blocks and the file viewer use `lib/neutral-code-theme.ts`; the terminal keeps its ANSI hues.
48
+ diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts
49
+ index d4d7df6..25f95bd 100644
50
+ --- a/app/api/files/[...path]/route.ts
51
+ +++ b/app/api/files/[...path]/route.ts
52
+ @@ -394,7 +394,7 @@ function wrapDocxPreviewHtml(bodyHtml: string, fileName: string): string {
53
+ <meta name="viewport" content="width=device-width, initial-scale=1">
54
+ <style>
55
+ :root { color-scheme: light; }
56
+ - html, body { margin: 0; min-height: 100%; background: #eef1f5; color: #171717; }
57
+ + html, body { margin: 0; min-height: 100%; background: #f5f5f5; color: #171717; }
58
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; padding: 28px; }
59
+ main {
60
+ box-sizing: border-box;
61
+ @@ -403,23 +403,24 @@ function wrapDocxPreviewHtml(bodyHtml: string, fileName: string): string {
62
+ margin: 0 auto;
63
+ padding: 56px 64px;
64
+ background: #fff;
65
+ - box-shadow: 0 8px 28px rgba(15, 23, 42, 0.14);
66
+ + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12);
67
+ }
68
+ .file-title {
69
+ margin: 0 0 28px;
70
+ padding-bottom: 10px;
71
+ - border-bottom: 1px solid #e5e7eb;
72
+ - color: #6b7280;
73
+ + border-bottom: 1px solid #e5e5e5;
74
+ + color: #666666;
75
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
76
+ word-break: break-word;
77
+ }
78
+ - h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1.1em 0 0.45em; color: #111827; }
79
+ + h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1.1em 0 0.45em; color: #0a0a0a; }
80
+ p { margin: 0.65em 0; line-height: 1.7; }
81
+ table { border-collapse: collapse; max-width: 100%; margin: 1em 0; }
82
+ - th, td { border: 1px solid #d1d5db; padding: 6px 9px; vertical-align: top; }
83
+ + th, td { border: 1px solid #d4d4d4; padding: 6px 9px; vertical-align: top; }
84
+ img { max-width: 100%; height: auto; }
85
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; }
86
+ - a { color: #2563eb; }
87
+ + a { color: #404040; text-decoration: underline; text-underline-offset: 3px; }
88
+ + a:hover { color: #0a0a0a; }
89
+ @media (max-width: 720px) {
90
+ body { padding: 0; background: #fff; }
91
+ main { min-height: 100vh; padding: 28px 22px; box-shadow: none; }
92
  diff --git a/app/favicon.ico b/app/favicon.ico
93
  index 2da6b7dc00c64c59ef23cff153a6051478ad9315..b407f41443e3e94f4204a1862227e0d457f7d823 100644
94
  GIT binary patch
 
3128
  +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
3129
  +OTHER DEALINGS IN THE FONT SOFTWARE.
3130
  diff --git a/app/globals.css b/app/globals.css
3131
+ index d6b5535..46c0a53 100644
3132
  --- a/app/globals.css
3133
  +++ b/app/globals.css
3134
+ @@ -24,46 +24,109 @@
3135
  --font-mono-font: var(--font-mono);
3136
  }
3137
 
 
3158
  + --text: #2e2e2e;
3159
  + --text-muted: #525252;
3160
  + --text-dim: #666666;
3161
+ + --accent: #404040;
3162
+ + --accent-hover: #0a0a0a;
3163
  --accent-contrast: #ffffff;
3164
  - --user-bg: #eff6ff;
3165
+ + --accent-soft: rgba(10, 10, 10, 0.07);
3166
  + --user-bg: #f5f5f5;
3167
  --assistant-bg: #ffffff;
3168
  - --tool-bg: #f9fafb;
 
3180
  + --danger-soft: rgba(193, 0, 7, 0.07);
3181
  + --success: #008236;
3182
  + --warning: #bb4d00;
3183
+ + --focus-ring: #404040;
3184
  + --scrim: rgba(10, 10, 10, 0.32);
3185
  + --border: transparent;
3186
  + --shadow-pop: 0 10px 30px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06);
 
3212
  + --text: #d4d4d4;
3213
  + --text-muted: #a3a3a3;
3214
  + --text-dim: #8a8a8a;
3215
+ + --accent: #e5e5e5;
3216
+ + --accent-hover: #ffffff;
3217
  + --accent-contrast: #0a0a0a;
3218
+ + --accent-soft: rgba(255, 255, 255, 0.10);
3219
  + --user-bg: #1a1a1a;
3220
  + --assistant-bg: #0a0a0a;
3221
  + --tool-bg: #1a1a1a;
 
3229
  + --danger-soft: rgba(255, 100, 103, 0.10);
3230
  + --success: #05df72;
3231
  + --warning: #ffb900;
3232
+ + --focus-ring: #e5e5e5;
3233
  + --scrim: rgba(0, 0, 0, 0.6);
3234
  + --border: transparent;
3235
  + --shadow-pop: 0 12px 36px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4);
 
3269
  }
3270
 
3271
  [data-theme="mist"] {
3272
+ @@ -72,7 +135,7 @@ html.dark,
3273
  --bg-panel: #e9f0ee;
3274
  --bg-hover: #e0eae7;
3275
  --bg-selected: #d7e4df;
 
3278
  --text: #202e2b;
3279
  --text-muted: #455f56;
3280
  --text-dim: #52685f;
3281
+ @@ -83,6 +146,18 @@ html.dark,
3282
  --assistant-bg: #f4f8f7;
3283
  --tool-bg: #ecf3f0;
3284
  --bg-subtle: rgba(24,65,50,0.04);
 
3297
  }
3298
 
3299
  [data-theme="rose"] {
3300
+ @@ -91,7 +166,7 @@ html.dark,
3301
  --bg-panel: #f3edef;
3302
  --bg-hover: #eee3e7;
3303
  --bg-selected: #e8dce1;
 
3306
  --text: #34282e;
3307
  --text-muted: #65505a;
3308
  --text-dim: #705b65;
3309
+ @@ -102,6 +177,18 @@ html.dark,
3310
  --assistant-bg: #fcf7f8;
3311
  --tool-bg: #f7eef2;
3312
  --bg-subtle: rgba(89,39,62,0.04);
 
3325
  }
3326
 
3327
  html[data-theme="pine"],
3328
+ @@ -111,7 +198,7 @@ html[data-theme="pine"],
3329
  --bg-panel: #212b28;
3330
  --bg-hover: #2b3632;
3331
  --bg-selected: #35433c;
 
3334
  --text: #e6ede8;
3335
  --text-muted: #c3d0c6;
3336
  --text-dim: #afc2b5;
3337
+ @@ -122,6 +209,18 @@ html[data-theme="pine"],
3338
  --assistant-bg: #19201f;
3339
  --tool-bg: #222d29;
3340
  --bg-subtle: rgba(210,236,219,0.04);
 
3353
  }
3354
 
3355
  /* WebKit reports dvh/innerHeight without the status-bar strip for installed
3356
+ @@ -135,28 +234,12 @@ html[data-theme="pine"],
3357
  }
3358
  }
3359
 
 
3386
  }
3387
 
3388
  /* Theme toggle: circular wipe via View Transitions API.
3389
+ @@ -218,9 +301,9 @@ body {
3390
  margin-bottom: 22px;
3391
  }
3392
 
 
3398
  }
3399
 
3400
  .web-login-brand h1 {
3401
+ @@ -247,15 +330,15 @@ body {
3402
  min-width: 0;
3403
  padding: 10px 10px 10px 14px;
3404
  border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
3405
  - border-radius: 14px;
3406
  + border-radius: var(--radius-xl);
3407
  background: var(--bg);
3408
+ - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -12px rgba(15, 23, 42, 0.10);
3409
+ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px -12px rgba(0, 0, 0, 0.10);
3410
  transition: border-color 0.15s, box-shadow 0.15s;
3411
+ }
3412
+
3413
+ .web-login-composer:focus-within {
3414
+ border-color: var(--accent);
3415
+ - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent), 0 8px 24px -12px rgba(15, 23, 42, 0.10);
3416
+ + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent), 0 8px 24px -12px rgba(0, 0, 0, 0.10);
3417
+ }
3418
+
3419
+ .web-login-label {
3420
+ @@ -307,7 +390,7 @@ html.dark .web-login-error {
3421
  min-height: 32px;
3422
  padding: 7px 14px;
3423
  border: none;
 
3426
  background: var(--accent);
3427
  color: var(--accent-contrast);
3428
  cursor: pointer;
3429
+ @@ -339,7 +422,7 @@ html.dark .web-login-error {
3430
+ height: 100%;
3431
+ min-width: 0;
3432
+ min-height: 0;
3433
+ - background: #111318;
3434
+ + background: #111111;
3435
+ }
3436
+
3437
+ .terminal-panel-header {
3438
+ @@ -350,7 +433,7 @@ html.dark .web-login-error {
3439
+ padding: 0 10px 0 13px;
3440
+ border-bottom: 1px solid #2f3540;
3441
+ background: #181b21;
3442
+ - color: #9ca3af;
3443
+ + color: #a3a3a3;
3444
+ font-family: var(--font-mono);
3445
+ font-size: 11px;
3446
+ }
3447
+ @@ -388,23 +471,23 @@ html.dark .web-login-error {
3448
  flex-shrink: 0;
3449
  padding: 0 8px;
3450
  border: 1px solid #343a46;
3451
  - border-radius: 5px;
3452
  + border-radius: var(--radius-sm);
3453
  background: transparent;
3454
+ - color: #9ca3af;
3455
+ + color: #a3a3a3;
3456
  cursor: pointer;
3457
+ font: inherit;
3458
+ }
3459
+
3460
+ .terminal-panel-exit {
3461
+ padding: 7px 12px;
3462
+ - color: #9ca3af;
3463
+ + color: #a3a3a3;
3464
+ font-family: var(--font-mono);
3465
+ font-size: 11px;
3466
+ }
3467
+
3468
+ .terminal-panel-header button:hover {
3469
+ background: #242932;
3470
+ - color: #e5e7eb;
3471
+ + color: #e5e5e5;
3472
+ }
3473
+
3474
+ .terminal-panel-error {
3475
+ @@ -451,7 +534,7 @@ html[data-keyboard-open] .terminal-extra-keys {
3476
+ padding: 0;
3477
+ border: none;
3478
+ background: transparent;
3479
+ - color: #d7dce5;
3480
+ + color: #d4d4d4;
3481
+ font-family: var(--font-mono);
3482
+ font-size: 12px;
3483
+ touch-action: manipulation;
3484
+ @@ -471,7 +554,7 @@ html[data-keyboard-open] .terminal-extra-keys {
3485
+ }
3486
+
3487
+ .terminal-extra-keys button.is-active {
3488
+ - color: #60a5fa;
3489
+ + color: #e5e5e5;
3490
+ }
3491
+
3492
+ .terminal-xterm-host {
3493
+ @@ -480,7 +563,7 @@ html[data-keyboard-open] .terminal-extra-keys {
3494
+ }
3495
+
3496
+ .terminal-xterm .xterm-viewport {
3497
+ - background-color: #111318;
3498
+ + background-color: #111111;
3499
+ }
3500
+
3501
+ /*
3502
+ @@ -502,7 +585,7 @@ html[data-keyboard-open] .terminal-extra-keys {
3503
  background-color: color-mix(in srgb, var(--text-dim) 45%, transparent);
3504
  background-clip: padding-box;
3505
  border: 2px solid transparent;
 
3508
  }
3509
  /* Only ever set background-color here: the `background` shorthand resets
3510
  background-clip and would snap the thumb to the full 10px on hover. */
3511
+ @@ -538,7 +621,7 @@ html[data-keyboard-open] .terminal-extra-keys {
3512
  }
3513
  ::-webkit-scrollbar-thumb {
3514
  border-width: 1px;
 
3517
  }
3518
  }
3519
 
3520
+ @@ -703,9 +786,9 @@ button.extension-widget-trigger:focus-visible {
3521
 
3522
  .extension-widget-placement-icon {
3523
  display: block;
 
3530
  fill: currentColor;
3531
  }
3532
 
3533
+ @@ -826,8 +909,8 @@ button.extension-widget-trigger:focus-visible {
3534
  }
3535
 
3536
  :root {
 
3541
  --font-ui: var(--font-ui-default);
3542
  --font-mono: var(--font-mono-default);
3543
  --font-ui-weight: 400;
3544
+ @@ -883,11 +966,11 @@ pre, code {
3545
  .markdown-body ol > li { list-style-type: decimal; }
3546
  .markdown-body li > p { margin: 0; }
3547
  .markdown-body li::marker {
3548
  - color: color-mix(in srgb, var(--accent) 72%, var(--text-muted));
3549
  - font-weight: 600;
3550
+ + color: var(--text-muted);
3551
  + font-weight: 500;
3552
  }
3553
  .markdown-body strong {
 
3556
  font-weight: 700;
3557
  }
3558
  .markdown-body em { color: var(--text-muted); }
3559
+ @@ -896,24 +979,23 @@ pre, code {
3560
+ text-decoration: underline;
3561
+ text-decoration-thickness: 1px;
3562
+ text-underline-offset: 3px;
3563
+ - text-decoration-color: color-mix(in srgb, var(--accent) 45%, transparent);
3564
+ + text-decoration-color: color-mix(in srgb, var(--accent) 50%, transparent);
3565
+ }
3566
+ -.markdown-body a:hover { text-decoration-color: var(--accent); }
3567
+ +.markdown-body a:hover { color: var(--accent-hover); text-decoration-color: var(--accent-hover); }
3568
+ .markdown-body img {
3569
+ display: block;
3570
  max-width: 100%;
3571
  height: auto;
3572
  margin: 8px 0;
 
3585
  }
3586
  .markdown-body .contains-task-list {
3587
  padding-left: 0;
3588
+ @@ -935,7 +1017,7 @@ pre, code {
3589
  height: 14px;
3590
  margin: 0;
3591
  border: 1px solid var(--border);
 
3594
  background: var(--bg);
3595
  display: inline-grid;
3596
  place-content: center;
3597
+ @@ -967,8 +1049,8 @@ pre, code {
3598
  margin: 8px 0;
3599
  overflow-x: auto;
3600
  overflow-y: hidden;
 
3605
  }
3606
  .markdown-body table {
3607
  border-collapse: separate;
3608
+ @@ -1088,7 +1170,7 @@ pre, code {
3609
  padding: 7px 8px;
3610
  overflow: auto;
3611
  border: 1px solid var(--border);
 
3614
  background: var(--bg-panel);
3615
  font-family: var(--font-mono);
3616
  font-size: 11px;
3617
+ @@ -1108,16 +1190,13 @@ pre, code {
3618
  min-width: 0;
3619
  max-width: 100%;
3620
  margin: 6px 0;
 
3634
  color: var(--text-dim);
3635
  font-size: 11px;
3636
  display: flex;
3637
+ @@ -1137,7 +1216,7 @@ pre, code {
3638
  }
3639
  .markdown-code-action {
3640
  border: 1px solid var(--border);
 
3643
  padding: 2px 7px;
3644
  background: var(--bg);
3645
  color: var(--text-muted);
3646
+ @@ -1228,7 +1307,7 @@ pre, code {
3647
  max-width: 100%;
3648
  max-height: 100%;
3649
  object-fit: contain;
 
3652
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
3653
  }
3654
 
3655
+ @@ -1244,7 +1323,7 @@ pre, code {
3656
  height: 36px;
3657
  padding: 0;
3658
  border: 1px solid var(--border);
 
3661
  background: var(--bg-panel);
3662
  color: var(--text-muted);
3663
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);
3664
+ @@ -1320,7 +1399,7 @@ pre, code {
3665
  height: 24px;
3666
  overflow: hidden;
3667
  border: 1px solid var(--border);
 
3670
  background: var(--bg);
3671
  }
3672
  .mermaid-zoom-stepper button,
3673
+ @@ -1347,7 +1426,7 @@ pre, code {
3674
  .mermaid-zoom-icon-button {
3675
  flex: 0 0 24px;
3676
  border: 1px solid var(--border);
 
3679
  }
3680
  .mermaid-zoom-stepper button:hover:not(:disabled),
3681
  .mermaid-zoom-icon-button:hover {
3682
+ @@ -1384,7 +1463,7 @@ pre, code {
3683
  .mermaid-zoom-canvas {
3684
  padding: 12px;
3685
  border: 1px solid var(--border);
 
3688
  background: var(--bg-panel);
3689
  margin: 0 auto;
3690
  transition: width 0.15s ease;
3691
+ @@ -1460,7 +1539,7 @@ pre, code {
3692
  height: 24px;
3693
  overflow: hidden;
3694
  border: 1px solid var(--border);
 
3697
  }
3698
 
3699
  .file-viewer-mode-button {
3700
+ @@ -1503,12 +1582,11 @@ pre, code {
3701
  height: 32px;
3702
  padding: 0;
3703
  box-sizing: border-box;
 
3712
  opacity: 0;
3713
  visibility: hidden;
3714
  pointer-events: none;
3715
+ @@ -1584,22 +1662,21 @@ pre, code {
3716
 
3717
  .new-session-logo {
3718
  flex: none;
 
3741
  line-height: 1.15;
3742
  white-space: nowrap;
3743
  }
3744
+ @@ -1613,7 +1690,8 @@ pre, code {
3745
  gap: 6px;
3746
  min-width: 0;
3747
  color: var(--text-dim);
 
3751
  line-height: 1.4;
3752
  white-space: nowrap;
3753
  }
3754
+ @@ -1638,7 +1716,7 @@ pre, code {
3755
  gap: 3px;
3756
  margin: -3px -4px;
3757
  padding: 3px 4px;
 
3760
  color: var(--accent);
3761
  font-weight: 600;
3762
  text-decoration: none;
3763
+ @@ -1652,7 +1730,7 @@ pre, code {
3764
 
3765
  /* The ring inside the box, clear of the "→" and "·" beside it. */
3766
  .new-session-update:focus-visible {
 
3769
  outline-offset: -2px;
3770
  }
3771
 
3772
+ @@ -1678,12 +1756,12 @@ pre, code {
3773
  }
3774
 
3775
  .new-session-logo {
 
3785
  }
3786
  }
3787
 
3788
+ @@ -1722,14 +1800,14 @@ pre, code {
3789
  min-width: 0;
3790
  height: 29px;
3791
  padding: 0 10px;
 
3803
  }
3804
 
3805
  /* Above a fresh composer (inline): no box until hovered or open, 12px, the
3806
+ @@ -1752,7 +1830,7 @@ pre, code {
3807
  height: 26px;
3808
  padding: 0 6px;
3809
  border: 0;
 
3812
  background: transparent;
3813
  }
3814
 
3815
+ @@ -1781,8 +1859,9 @@ pre, code {
3816
  max-width: 28ch;
3817
  overflow: hidden;
3818
  color: var(--text-dim);
 
3823
  }
3824
 
3825
  .project-picker-parent {
3826
+ @@ -1851,7 +1930,8 @@ pre, code {
3827
  path's color and code type, cut at its end. */
3828
  .project-picker-label.is-alias {
3829
  color: var(--text);
 
3833
  }
3834
 
3835
  /* A path or a branch, cut at its left (rtl), the text itself kept left to
3836
+ @@ -1901,7 +1981,8 @@ pre, code {
3837
  border-color: color-mix(in srgb, var(--accent) 40%, transparent);
3838
  background: color-mix(in srgb, var(--accent) 6%, transparent);
3839
  color: var(--text-dim);
 
3843
  }
3844
 
3845
  /* No worktree list here (the files tab): a box that says why, and does nothing. */
3846
+ @@ -1978,7 +2059,7 @@ pre, code {
3847
  box-sizing: border-box;
3848
  flex: 0 0 24px;
3849
  border: 1px solid var(--border);
 
3852
  background: transparent;
3853
  color: var(--text-muted);
3854
  cursor: pointer;
3855
+ @@ -2037,7 +2118,7 @@ pre, code {
3856
  .markdown-file-preview pre {
3857
  background: var(--bg-panel);
3858
  border: 1px solid var(--border);
 
3861
  padding: 12px 16px;
3862
  overflow-x: auto;
3863
  font-family: var(--font-mono);
3864
+ @@ -2049,7 +2130,7 @@ pre, code {
3865
  font-size: 0.9em;
3866
  background: var(--bg-panel);
3867
  padding: 1px 5px;
 
3870
  }
3871
  .markdown-file-preview pre code {
3872
  background: none;
3873
+ @@ -2061,7 +2142,7 @@ pre, code {
3874
  margin-bottom: 20px;
3875
  padding: 14px 16px 12px;
3876
  border: 1px solid var(--border);
 
3879
  background: var(--bg-panel);
3880
  }
3881
  .markdown-frontmatter-title {
3882
+ @@ -2474,3 +2555,428 @@ span.token.table {
3883
  display: none !important;
3884
  }
3885
  }
 
4426
  lang: "en",
4427
  icons: [
4428
  diff --git a/app/settings.css b/app/settings.css
4429
+ index 0bf21eb..e8788d8 100644
4430
  --- a/app/settings.css
4431
  +++ b/app/settings.css
4432
  @@ -40,9 +40,9 @@
 
4558
  background: var(--bg-panel);
4559
  overflow-y: auto;
4560
  }
4561
+ @@ -457,9 +458,9 @@
4562
  .enabled-models-pin {
4563
  flex-shrink: 0;
4564
  padding: 1px 4px;
4565
  - border-radius: 3px;
4566
+ - background: rgba(99, 102, 241, 0.12);
4567
+ - color: rgba(99, 102, 241, 0.8);
4568
  + border-radius: var(--radius-xs);
4569
+ + background: var(--bg-subtle);
4570
+ + color: var(--text-muted);
4571
  font-size: 9px;
4572
+ }
4573
+
4574
  @@ -497,7 +498,7 @@
4575
  justify-content: space-between;
4576
  gap: 18px;
 
4607
  }
4608
 
4609
  .config-field-label {
4610
+ @@ -697,15 +698,15 @@
4611
  .config-scope-tag {
4612
  flex-shrink: 0;
4613
  padding: 1px 5px;
 
4616
  background: rgba(120, 120, 120, 0.12);
4617
  color: var(--text-dim);
4618
  font-size: 10px;
4619
+ }
4620
+
4621
+ .config-scope-tag.is-project {
4622
+ - background: rgba(99, 102, 241, 0.12);
4623
+ - color: rgba(99, 102, 241, 0.8);
4624
+ + background: var(--bg-subtle);
4625
+ + color: var(--text-muted);
4626
+ }
4627
+
4628
+ .config-scope-switch-field {
4629
  @@ -730,7 +731,7 @@
4630
  height: 30px;
4631
  overflow: hidden;
 
5267
  color: var(--text-muted);
5268
  font: inherit;
5269
  diff --git a/app/sidebar.css b/app/sidebar.css
5270
+ index 86728a2..1d59f26 100644
5271
  --- a/app/sidebar.css
5272
  +++ b/app/sidebar.css
5273
  @@ -91,7 +91,7 @@
 
5279
  outline-offset: -2px;
5280
  }
5281
 
5282
+ @@ -171,7 +171,7 @@
5283
+ }
5284
+
5285
+ .session-tree-pinned-dot.is-unread {
5286
+ - background: #0891b2;
5287
+ + background: var(--text-strong);
5288
+ }
5289
+
5290
+ /* Session row: title, branch, time; actions take the time's place on
5291
  @@ -184,7 +184,7 @@
5292
  right: var(--session-tree-inset-right);
5293
  left: var(--session-tree-inset-left);
 
5333
  cursor: default;
5334
  }
5335
 
5336
+ @@ -255,14 +234,14 @@ a.session-tree-main {
5337
+ width: 6px;
5338
+ height: 6px;
5339
+ border-radius: 50%;
5340
+ - background: #0891b2;
5341
+ + background: var(--text-strong);
5342
+ }
5343
+
5344
+ .session-tree-unread::after {
5345
+ content: "";
5346
+ position: absolute;
5347
+ inset: -3px;
5348
+ - border: 1.4px solid #0891b2;
5349
+ + border: 1.4px solid var(--text-strong);
5350
+ border-radius: 50%;
5351
+ opacity: 0.35;
5352
+ animation: session-tree-status-pulse 1.6s ease-in-out infinite;
5353
  @@ -297,20 +276,23 @@ a.session-tree-main {
5354
  }
5355
  }
 
5471
  text-align: left;
5472
  white-space: nowrap;
5473
  }
5474
+ @@ -669,7 +657,7 @@ a.session-tree-main {
5475
+ width: 6px;
5476
+ height: 6px;
5477
+ border-radius: 50%;
5478
+ - background: #0891b2;
5479
+ + background: var(--text-strong);
5480
+ }
5481
+
5482
+ .session-tree-group-actions {
5483
  @@ -726,7 +714,7 @@ a.session-tree-main {
5484
  width: 22px;
5485
  height: 22px;
 
5829
  <span>{savedOk ? t("i18n.saved") : saving ? t("agents.saving") : t("agents.save")}</span>
5830
  </ConfigButton>
5831
  diff --git a/components/AppShell.tsx b/components/AppShell.tsx
5832
+ index c8c4ba9..0d90e2b 100644
5833
  --- a/components/AppShell.tsx
5834
  +++ b/components/AppShell.tsx
5835
  @@ -3,6 +3,8 @@
 
6330
  </button>
6331
  );
6332
  };
6333
+ @@ -2033,7 +1873,7 @@ export function AppShell() {
6334
+ transform: translateY(0);
6335
+ filter: blur(0);
6336
+ background: color-mix(in srgb, var(--accent) 8%, var(--bg-panel));
6337
+ - box-shadow: 0 18px 44px rgba(37,99,235,0.16);
6338
+ + box-shadow: 0 18px 44px rgba(0,0,0,0.16);
6339
+ }
6340
+ 100% {
6341
+ opacity: 1;
6342
  @@ -2178,13 +2018,9 @@ export function AppShell() {
6343
  onMouseLeave={(e) => { e.currentTarget.style.color = "var(--text-muted)"; }}
6344
  >
 
6448
  </div>
6449
 
6450
  diff --git a/components/BranchNavigator.tsx b/components/BranchNavigator.tsx
6451
+ index 0f8186b..d31883a 100644
6452
  --- a/components/BranchNavigator.tsx
6453
  +++ b/components/BranchNavigator.tsx
6454
  @@ -3,6 +3,8 @@
 
6460
 
6461
  interface Props {
6462
  tree: SessionTreeNode[];
6463
+ @@ -207,9 +209,9 @@ function TreeNodeView({ node, activePathIds, depth, isLast, parentLines, onSelec
6464
+ fontSize: 9,
6465
+ fontFamily: "var(--font-mono)",
6466
  color: role === "user" ? "var(--accent)" : "var(--text-dim)",
6467
+ - background: role === "user" ? "rgba(37,99,235,0.08)" : "var(--bg-hover)",
6468
+ - border: `1px solid ${role === "user" ? "rgba(37,99,235,0.2)" : "var(--border)"}`,
6469
  - borderRadius: 3,
6470
+ + background: role === "user" ? "var(--accent-soft)" : "var(--bg-hover)",
6471
+ + border: "1px solid var(--border)",
6472
  + borderRadius: "var(--radius-xs)",
6473
  padding: "0 4px",
6474
  marginRight: 5,
 
6572
  assert.doesNotMatch(html, /type="file"|Attach files|Change tool preset/);
6573
  });
6574
  diff --git a/components/ChatInput.tsx b/components/ChatInput.tsx
6575
+ index 9a0a1b7..80465c6 100644
6576
  --- a/components/ChatInput.tsx
6577
  +++ b/components/ChatInput.tsx
6578
  @@ -35,6 +35,8 @@ import { useFontPreferences } from "@/hooks/useFontPreferences";
 
7049
  + style={{
7050
  + display: "flex", alignItems: "center", gap: 5,
7051
  + padding: "7px 12px",
7052
+ + background: canQueueStreamingMessage ? "var(--accent-soft)" : "var(--bg-subtle)",
7053
  + border: "none",
7054
  + borderRadius: "var(--radius-sm)",
7055
+ + color: canQueueStreamingMessage ? "var(--text-strong)" : "var(--text-dim)",
7056
  + cursor: canQueueStreamingMessage ? "pointer" : "not-allowed",
7057
  + fontSize: 13, fontWeight: 600, letterSpacing: "-0.01em",
7058
  + transition: "background 0.12s",
 
7874
 
7875
  {minimapHovered && allNodes.length > 0 && (
7876
  diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx
7877
+ index 6c540a4..57ffd84 100644
7878
  --- a/components/ChatWindow.tsx
7879
  +++ b/components/ChatWindow.tsx
7880
  @@ -1,19 +1,20 @@
 
8014
  export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initialScrollPosition, onScrollPositionChange, sessionRunning, newSessionCwd, newSessionDraftKey, newSessionContextBar, initialNewSessionChoices, onNewSessionChoicesChange, onAgentEnd, onToolEnd, onAttentionNeeded, onSessionCreated, onSessionForked, modelsRefreshKey, chatInputRef, onBranchDataChange, onSystemPromptChange, onSystemToolsChange, onSystemInfoLoaderChange, onSessionStatsChange, onSessionStatsPanelOpen, onOpenSettings, onContextUsageChange, onOpenFile, onFilesUploaded, onOpenSubagent, onAskInNewChat, quoteSelectionEnabled = false, initialPrompt, onInitialPromptConsumed, soundEnabled = true, onSoundToggle, playDoneSound = () => {}, unlockAudio, holdAudio }: Props) {
8015
  const { t } = useI18n();
8016
  const isMobile = useIsMobile();
8017
+ @@ -1014,34 +1025,17 @@ export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initi
8018
+ onDrop={handleDrop}
8019
+ >
8020
+ {isDragOver && (
8021
+ - <div className="pointer-events-none absolute inset-0 z-50 flex animate-[drop-zone-in_0.15s_ease_both] items-center justify-center bg-[rgba(37,99,235,0.06)] backdrop-blur-[1px]">
8022
+ + <div className="pointer-events-none absolute inset-0 z-50 flex animate-[drop-zone-in_0.15s_ease_both] items-center justify-center bg-[var(--accent-soft)] backdrop-blur-[1px]">
8023
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
8024
+ {[0, 0.8, 1.6].map((delay) => (
8025
+ <div
8026
+ key={delay}
8027
+ - className="absolute h-[720px] w-[720px] rounded-full border-[1.5px] border-solid border-[rgba(37,99,235,0.5)] animate-[drop-ripple_2.4s_ease-out_infinite_backwards]"
8028
+ + className="absolute h-[720px] w-[720px] rounded-full border-[1.5px] border-solid border-[var(--text-dim)] animate-[drop-ripple_2.4s_ease-out_infinite_backwards]"
8029
+ style={{ transformOrigin: "center", animationDelay: `${delay}s` }}
8030
  />
8031
  ))}
8032
  </div>
 
8174
  <div className="new-session-brand-text">
8175
  <span className="new-session-brand-name">Pi Web</span>
8176
  <span className="new-session-meta">
8177
+ @@ -1562,8 +1557,8 @@ function NoticeShelf({ notices, floating = false, onPauseChange }: { notices: No
8178
+ width: "fit-content",
8179
+ maxWidth: "min(100%, 620px)",
8180
+ boxShadow: floating
8181
+ - ? "0 1px 2px rgba(15,23,42,0.05), 0 10px 28px -14px rgba(15,23,42,0.24)"
8182
+ - : "0 1px 2px rgba(15,23,42,0.04), 0 8px 24px -12px rgba(15,23,42,0.10)",
8183
+ + ? "0 1px 2px rgba(0,0,0,0.05), 0 10px 28px -14px rgba(0,0,0,0.24)"
8184
+ + : "0 1px 2px rgba(0,0,0,0.04), 0 8px 24px -12px rgba(0,0,0,0.10)",
8185
+ fontSize: 14,
8186
+ lineHeight: 1.5,
8187
+ transformOrigin: "top right",
8188
  @@ -1648,11 +1643,7 @@ function ExtensionWaitingCount({ count }: { count: number }) {
8189
  /** Corner brackets pointing outward; when expanded they point inward (restore). */
8190
  function ExtensionSizeIcon({ expanded }: { expanded: boolean }) {
 
8514
  <span className="extension-widget-key">{widget.key}</span>
8515
  </>
8516
  diff --git a/components/FileExplorer.tsx b/components/FileExplorer.tsx
8517
+ index 94cf736..f4fc956 100644
8518
  --- a/components/FileExplorer.tsx
8519
  +++ b/components/FileExplorer.tsx
8520
  @@ -18,6 +18,8 @@ import { buildSearchTree, type SearchTreeNode } from "@/lib/search-tree";
 
8526
  type Translate = ReturnType<typeof useI18n>["t"];
8527
 
8528
  /** Why a listed entry is normally left out; only listings with `hidden=1` report it. */
8529
+ @@ -144,7 +146,7 @@ const GIT_STATUS_COLORS: Record<GitFileStatusKind, string> = {
8530
+ modified: "#d6a84b",
8531
+ added: "#4ade80",
8532
+ deleted: "#f87171",
8533
+ - renamed: "#60a5fa",
8534
+ + renamed: "var(--text-muted)",
8535
+ untracked: "#4ade80",
8536
+ conflict: "#f87171",
8537
+ };
8538
  @@ -174,20 +176,13 @@ function GitStatusBadge({ status, t }: { status: GitFileStatus; t: Translate })
8539
 
8540
  function MentionIcon({ size = 11 }: { size?: number }) {
 
8579
  )}
8580
  {!node.isDir && <span style={{ width: 10, flexShrink: 0 }} />}
8581
  <span style={{ flexShrink: 0, display: "flex", alignItems: "center", opacity: hidden ? HIDDEN_ENTRY_OPACITY : 1 }}>
8582
+ @@ -396,7 +385,7 @@ export function TreeNode({
8583
+ aria-label={t("files.newlyUploaded")}
8584
+ style={{ width: 14, height: 14, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center" }}
8585
+ >
8586
+ - <span style={{ width: 6, height: 6, borderRadius: "50%", background: "#3b82f6" }} />
8587
+ + <span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--text-strong)" }} />
8588
+ </span>
8589
+ )}
8590
+ {!hovered && !node.isDir && gitStatus && (
8591
  @@ -419,9 +408,7 @@ export function TreeNode({
8592
  </span>
8593
  )}
 
8858
 
8859
  export function FolderIcon({ size = 14, open = false }: IconProps & { open?: boolean }) {
8860
  diff --git a/components/FileViewer.tsx b/components/FileViewer.tsx
8861
+ index 9426211..5f0943f 100644
8862
  --- a/components/FileViewer.tsx
8863
  +++ b/components/FileViewer.tsx
8864
+ @@ -6,6 +6,7 @@ import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/prism-light";
8865
+ import renderSyntaxNode from "react-syntax-highlighter/dist/esm/create-element";
8866
+ import { vs } from "react-syntax-highlighter/dist/cjs/styles/prism";
8867
+ import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
8868
+ +import { neutralCodeTheme } from "@/lib/neutral-code-theme";
8869
+ import ReactMarkdown from "react-markdown";
8870
+ import { useTheme } from "@/hooks/useTheme";
8871
+ import { usePrismLanguages } from "@/hooks/usePrismLanguages";
8872
+ @@ -34,6 +35,8 @@ import {
8873
  type FileViewerDisplayMode as DisplayMode,
8874
  type FileViewerState,
8875
  } from "@/lib/file-viewer-state";
 
8878
 
8879
  export type { FileViewerState } from "@/lib/file-viewer-state";
8880
 
8881
+ @@ -81,12 +84,14 @@ const FILE_CODE_STYLE: CSSProperties = {
8882
+ // dark theme's shorthand is dropped, as CodeBlock does: a theme switch then never
8883
+ // makes React remove one beside the other (it warned, and the view lost its
8884
+ // background).
8885
+ -const fileViewerDarkTheme = {
8886
+ +const fileViewerDarkTheme = neutralCodeTheme({
8887
+ ...vscDarkPlus,
8888
+ 'pre[class*="language-"]': {
8889
+ ...vscDarkPlus['pre[class*="language-"]'],
8890
+ },
8891
+ -};
8892
+ +}, "dark");
8893
+ +// Both without blue: lib/neutral-code-theme.ts.
8894
+ +const fileViewerLightTheme = neutralCodeTheme(vs, "light");
8895
+ delete fileViewerDarkTheme['pre[class*="language-"]'].background;
8896
+
8897
+ const FILE_LINE_NUMBER_STYLE: CSSProperties = {
8898
+ @@ -119,10 +124,7 @@ interface SelectedLineRange {
8899
 
8900
  function MentionIcon() {
8901
  return (
 
8907
  );
8908
  }
8909
 
8910
+ @@ -252,11 +254,7 @@ function DownloadLink({ filePath, sourceSessionId }: { filePath: string; sourceS
8911
  aria-label={t("i18n.downloadFile")}
8912
  className="file-viewer-icon-button"
8913
  >
 
8920
  </a>
8921
  );
8922
  }
8923
+ @@ -1442,7 +1440,7 @@ function TextFileViewer({
8924
+ <SyntaxHighlighter
8925
+ className={wrapLines ? "file-source-view is-wrapped" : "file-source-view"}
8926
+ language={language === "text" || !prismReady ? "plaintext" : language}
8927
+ - style={isDark ? fileViewerDarkTheme : vs}
8928
+ + style={isDark ? fileViewerDarkTheme : fileViewerLightTheme}
8929
+ showLineNumbers
8930
+ lineNumberStyle={{
8931
+ ...FILE_LINE_NUMBER_STYLE,
8932
+ @@ -1708,12 +1706,7 @@ function TextFileViewer({
8933
  color: wrapLines ? "var(--text)" : "var(--text-muted)",
8934
  }}
8935
  >
 
8943
  </button>
8944
  </>
8945
  )}
8946
+ @@ -1733,7 +1726,7 @@ function TextFileViewer({
8947
  gap: 10,
8948
  padding: "5px 8px",
8949
  border: "1px solid var(--border)",
 
9001
  </dialog>
9002
  )}
9003
  diff --git a/components/MermaidBlock.tsx b/components/MermaidBlock.tsx
9004
+ index 25e4a63..8ce9799 100644
9005
  --- a/components/MermaidBlock.tsx
9006
  +++ b/components/MermaidBlock.tsx
9007
+ @@ -10,6 +10,9 @@ import { usePrismLanguages } from "@/hooks/usePrismLanguages";
9008
  import { copyText } from "@/lib/clipboard";
9009
  import { takeHighlightBudget, waitForHighlight } from "@/lib/highlight-queue";
9010
  import { prismLanguagesLoaded } from "@/lib/prism-languages";
9011
+ +import { neutralCodeTheme, neutralMermaidTheme } from "@/lib/neutral-code-theme";
9012
  +import { CarbonIcon } from "./CarbonIcon";
9013
  +import * as Carbon from "./carbon/icons";
9014
 
9015
  interface MermaidBlockProps {
9016
  code: string;
9017
+ @@ -59,7 +62,7 @@ export function MermaidBlock({ code, isStreaming, defaultPreview = false }: Merm
9018
+ startOnLoad: false,
9019
+ securityLevel: "strict",
9020
+ suppressErrorRendering: true,
9021
+ - theme: isDark ? "dark" : "default",
9022
+ + ...neutralMermaidTheme(isDark),
9023
+ });
9024
+
9025
+ const parsed = await mermaid.parse(code, { suppressErrors: true });
9026
+ @@ -194,9 +197,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
9027
  title={t("i18n.zoomOut")}
9028
  aria-label={t("i18n.zoomOut")}
9029
  >
 
9034
  </button>
9035
  <span className="mermaid-zoom-value">{Math.round(zoom * 100)}%</span>
9036
  <button
9037
+ @@ -206,9 +207,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
9038
  title={t("i18n.zoomIn")}
9039
  aria-label={t("i18n.zoomIn")}
9040
  >
 
9045
  </button>
9046
  </div>
9047
  <button
9048
+ @@ -218,9 +217,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
9049
  title={t("i18n.fitToWidth")}
9050
  aria-label={t("i18n.fitToWidth")}
9051
  >
 
9056
  </button>
9057
  <button
9058
  type="button"
9059
+ @@ -229,9 +226,7 @@ function MermaidZoomDialog({ svg, onClose }: { svg: string; onClose: () => void
9060
  title={t("i18n.close")}
9061
  aria-label={t("i18n.close")}
9062
  >
 
9067
  </button>
9068
  </div>
9069
  </div>
9070
+ @@ -262,12 +257,14 @@ interface CodeBlockProps {
9071
+ // Prism's light theme uses backgroundColor, while its dark theme uses the
9072
+ // background shorthand. Keep the pre background solely in customStyle so a
9073
+ // theme switch cannot remove/update one and reset the other in React's diff.
9074
+ -const codeBlockDarkTheme = {
9075
+ +const codeBlockDarkTheme = neutralCodeTheme({
9076
+ ...vscDarkPlus,
9077
+ 'pre[class*="language-"]': {
9078
+ ...vscDarkPlus['pre[class*="language-"]'],
9079
+ },
9080
+ -};
9081
+ +}, "dark");
9082
+ +// Both without blue: lib/neutral-code-theme.ts.
9083
+ +const codeBlockLightTheme = neutralCodeTheme(vs, "light");
9084
+ delete codeBlockDarkTheme['pre[class*="language-"]'].background;
9085
+
9086
+ /**
9087
+ @@ -337,7 +334,7 @@ export const CodeBlock = memo(function CodeBlock({ code, lang, headerAction, isS
9088
+ ) : (
9089
+ <SyntaxHighlighter
9090
+ language={highlighted ? lang || "text" : "text"}
9091
+ - style={isDark ? codeBlockDarkTheme : vs}
9092
+ + style={isDark ? codeBlockDarkTheme : codeBlockLightTheme}
9093
+ showLineNumbers
9094
+ lineNumberStyle={{ color: "var(--text-dim)", fontStyle: "normal" }}
9095
+ customStyle={{
9096
  diff --git a/components/MessageView.test.mjs b/components/MessageView.test.mjs
9097
  index 515b033..eeb4c1e 100644
9098
  --- a/components/MessageView.test.mjs
 
9119
 
9120
  test("collapses a displayed custom message from its header alone", () => {
9121
  diff --git a/components/MessageView.tsx b/components/MessageView.tsx
9122
+ index f2ff1de..d98d368 100644
9123
  --- a/components/MessageView.tsx
9124
  +++ b/components/MessageView.tsx
9125
  @@ -1,10 +1,11 @@
 
9221
  src={src}
9222
  alt=""
9223
  - style={{ maxWidth: 240, maxHeight: 240, borderRadius: 6, objectFit: "contain", display: "block", border: "1px solid rgba(59,130,246,0.15)" }}
9224
+ + style={{ maxWidth: 240, maxHeight: 240, borderRadius: "var(--radius-sm)", objectFit: "contain", display: "block", border: "none" }}
9225
  />
9226
  </ImagePreview>
9227
  );
 
10202
  const { t } = useI18n();
10203
  const showFileHeaders = files.length > 1;
10204
 
10205
+ @@ -1483,12 +1352,12 @@ function PatchTextView({ text }: { text: string }) {
10206
+ const bg =
10207
+ kind === "added" ? "rgba(34,197,94,0.12)" :
10208
+ kind === "removed" ? "rgba(248,113,113,0.13)" :
10209
+ - kind === "hunk" ? "rgba(96,165,250,0.12)" :
10210
+ + kind === "hunk" ? "var(--bg-subtle)" :
10211
+ "transparent";
10212
+ const color =
10213
+ kind === "added" ? "#22c55e" :
10214
+ kind === "removed" ? "#f87171" :
10215
+ - kind === "hunk" ? "var(--accent)" :
10216
+ + kind === "hunk" ? "var(--text-dim)" :
10217
+ "var(--text)";
10218
+
10219
+ return (
10220
+ @@ -1502,7 +1371,7 @@ function PatchTextView({ text }: { text: string }) {
10221
+ : kind === "removed"
10222
+ ? "3px solid #f87171"
10223
+ : kind === "hunk"
10224
+ - ? "3px solid var(--accent)"
10225
+ + ? "3px solid var(--line)"
10226
+ : "3px solid transparent",
10227
+ }}
10228
+ >
10229
  @@ -1587,7 +1456,6 @@ function ResultImages({ images, isError }: { images: ImageContent[]; isError: bo
10230
  flexWrap: "wrap",
10231
  padding: "10px",
 
10437
  <SelectorRow active={!value} gutter={Boolean(onSetDefault)} onSelect={() => {
10438
  setOpen(false);
10439
  diff --git a/components/ModelsConfig.tsx b/components/ModelsConfig.tsx
10440
+ index 9f6b556..470db1f 100644
10441
  --- a/components/ModelsConfig.tsx
10442
  +++ b/components/ModelsConfig.tsx
10443
  @@ -54,6 +54,8 @@ import { providerBadgeLabel } from "./enabled-models-helpers";
 
10523
  >
10524
  {selectedCount
10525
  ? t("models.discoveryAddSelectedCount", { count: selectedCount })
10526
+ @@ -561,10 +555,11 @@ type ThinkingLevel = typeof THINKING_LEVELS[number];
10527
+
10528
+ const LEVEL_COLORS: Record<ThinkingLevel, string> = {
10529
+ off: "var(--text-dim)",
10530
+ - minimal: "#6b7280",
10531
+ - low: "#60a5fa",
10532
+ - medium: "#a78bfa",
10533
+ - high: "#f472b6",
10534
+ + // Neutral grays up to medium, then the warm state colours: no blue or violet.
10535
+ + minimal: "var(--text-dim)",
10536
+ + low: "var(--text-muted)",
10537
+ + medium: "var(--text-strong)",
10538
+ + high: "#f59e0b",
10539
+ xhigh: "#fb923c",
10540
+ max: "#ef4444",
10541
+ };
10542
+ @@ -627,7 +622,7 @@ function ThinkingLevelMapEditor({
10543
  alignItems: "center",
10544
  gap: 8,
10545
  padding: "5px 4px",
 
10548
  background: "transparent",
10549
  border: "1px solid transparent",
10550
  }}
10551
+ @@ -644,7 +639,7 @@ function ThinkingLevelMapEditor({
10552
  </span>
10553
  </div>
10554
 
 
10557
  <button
10558
  onClick={() => setLevel(level, "omit")}
10559
  style={{ ...btnBase, ...(state === "omit" ? btnActive : {}) }}
10560
+ @@ -659,7 +654,7 @@ function ThinkingLevelMapEditor({
10561
  </button>
10562
  </div>
10563
 
 
10566
  <button
10567
  onClick={() => setLevel(level, strVal || level)}
10568
  style={{ ...btnBase, ...(state === "string" ? btnActive : {}), borderRight: "1px solid var(--border)", flexShrink: 0 }}
10569
+ @@ -747,7 +742,7 @@ function HeaderListEditor({ headers, onChange }: {
10570
  padding: "6px 9px",
10571
  background: "none",
10572
  border: "1px solid rgba(239,68,68,0.3)",
 
10575
  color: "#ef4444",
10576
  cursor: "pointer",
10577
  fontSize: 11,
10578
+ @@ -768,7 +763,7 @@ function HeaderListEditor({ headers, onChange }: {
10579
  ...current,
10580
  { id: nextRowIdRef.current++, name: "", value: "" },
10581
  ])}
 
10584
  + Add header
10585
  </button>
10586
  </div>
10587
+ @@ -1051,9 +1046,9 @@ function ModelDetail({
10588
  height: 28,
10589
  padding: "0 8px",
10590
  border: `1px solid ${testState.phase === "error" ? "#fecaca" : testState.phase === "success" ? "#bbf7d0" : "var(--border)"}`,
10591
  - borderRadius: 4,
10592
+ - background: testState.phase === "error" ? "#fee2e2" : testState.phase === "success" ? "#dcfce7" : "#e5e7eb",
10593
+ - color: "#111827",
10594
  + borderRadius: "var(--radius-xs)",
10595
+ + background: testState.phase === "error" ? "#fee2e2" : testState.phase === "success" ? "#dcfce7" : "#e5e5e5",
10596
+ + color: "#0a0a0a",
10597
  fontSize: 11,
10598
+ display: "inline-flex",
10599
+ alignItems: "center",
10600
+ @@ -1075,9 +1070,7 @@ function ModelDetail({
10601
  className={testState.phase === "success" ? "is-success" : undefined}
10602
  >
10603
  {testState.phase === "success" && (
 
10608
  )}
10609
  {testState.phase === "testing" ? t("i18n.checking") : testState.phase === "success" ? t("common.ok") : t("i18n.test")}
10610
  </ConfigButton>
10611
+ @@ -1096,7 +1089,7 @@ function ModelDetail({
10612
  onClick={() => void handleCatalogFill()}
10613
  disabled={!model.id.trim() || catalogState.phase === "loading"}
10614
  style={{
 
10617
  background: "var(--bg-panel)",
10618
  color: !model.id.trim() || catalogState.phase === "loading" ? "var(--text-dim)" : "var(--text-muted)",
10619
  cursor: !model.id.trim() || catalogState.phase === "loading" ? "not-allowed" : "pointer",
10620
+ @@ -1227,20 +1220,7 @@ function ModelDetail({
10621
  {advancedSummary}
10622
  </span>
10623
  </span>
 
10639
  </button>
10640
 
10641
  {advancedOpen && (
10642
+ @@ -1492,7 +1472,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels }: {
10643
  <button
10644
  key={option.id}
10645
  onClick={() => submitSelection(loginState.token, option.id)}
 
10648
  >
10649
  {option.label}
10650
  </button>
10651
+ @@ -1527,7 +1507,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels }: {
10652
  <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
10653
  Open the verification page and enter this code:
10654
  </p>
 
10657
  {loginState.userCode}
10658
  </div>
10659
  <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
10660
+ @@ -1666,7 +1646,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels }: {
10661
  style={{
10662
  padding: "6px 12px",
10663
  background: savedOk ? "#16a34a" : apiKey.trim() ? "var(--accent)" : "var(--bg-panel)",
 
10666
  color: savedOk ? "#fff" : apiKey.trim() ? "var(--accent-contrast)" : "var(--text-dim)",
10667
  cursor: (saving || !apiKey.trim() || savedOk) ? "not-allowed" : "pointer",
10668
  fontSize: 12, fontWeight: 600, flexShrink: 0,
10669
+ @@ -1674,9 +1654,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels }: {
10670
  }}
10671
  >
10672
  {savedOk && (
 
10677
  )}
10678
  {savedOk ? t("i18n.saved") : saving ? t("i18n.saving") : t("i18n.save")}
10679
  </button>
10680
+ @@ -1725,7 +1703,7 @@ function AddProviderPicker({
10681
  padding: "10px 12px",
10682
  background: "var(--bg-panel)",
10683
  border: "1px solid var(--border)",
 
10686
  boxSizing: "border-box",
10687
  cursor: "pointer",
10688
  minWidth: 0,
10689
+ @@ -1747,12 +1725,10 @@ function AddProviderPicker({
10690
  onClose();
10691
  }}
10692
  >
 
10701
  <input
10702
  ref={inputRef}
10703
  value={search}
10704
+ @@ -1782,10 +1758,8 @@ function AddProviderPicker({
10705
  <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>OpenAI / Anthropic compatible</div>
10706
  <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>{t("i18n.customEndpoint")}</div>
10707
  </div>
 
10714
  </span>
10715
  </button>
10716
  )}
10717
+ @@ -2177,13 +2151,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
10718
  onClick={() => setSelection({ type: "provider", name: pName })}
10719
  active={isProviderSelected}
10720
  >
 
10729
  <ConfigSidebarText className="is-grow">
10730
  {pName}
10731
  </ConfigSidebarText>
10732
+ @@ -2204,7 +2172,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
10733
  {m.id || t("i18n.newModel")}
10734
  </ConfigSidebarText>
10735
  {m.reasoning && (
10736
  - <span style={{ fontSize: 9, padding: "1px 4px", background: "rgba(99,102,241,0.12)", color: "rgba(99,102,241,0.8)", borderRadius: 3, flexShrink: 0 }}>T</span>
10737
+ + <span style={{ fontSize: 9, padding: "1px 4px", background: "var(--bg-subtle)", color: "var(--text-muted)", borderRadius: "var(--radius-xs)", flexShrink: 0 }}>T</span>
10738
  )}
10739
  </ConfigSidebarItem>
10740
  );
10741
+ @@ -2250,10 +2218,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
10742
  className={savedOk ? "is-success" : undefined}
10743
  >
10744
  {savedOk && (
 
10992
  </button>
10993
  {report && (
10994
  diff --git a/components/SelectorRow.tsx b/components/SelectorRow.tsx
10995
+ index 555da8f..5bd340c 100644
10996
  --- a/components/SelectorRow.tsx
10997
  +++ b/components/SelectorRow.tsx
10998
  @@ -2,12 +2,12 @@
 
11049
  cursor: "pointer",
11050
  opacity: showSave ? 1 : 0,
11051
  pointerEvents: showSave ? "auto" : "none",
11052
+ @@ -120,7 +123,7 @@ export function SelectorRow({
11053
+ onMouseEnter={(event) => {
11054
+ event.currentTarget.style.background = "var(--bg-selected)";
11055
+ event.currentTarget.style.color = "var(--accent)";
11056
+ - event.currentTarget.style.borderColor = "rgba(37,99,235,0.35)";
11057
+ + event.currentTarget.style.borderColor = "var(--border)";
11058
+ }}
11059
+ onMouseLeave={(event) => {
11060
+ event.currentTarget.style.background = "var(--bg-hover)";
11061
  diff --git a/components/SessionSidebar.test.mjs b/components/SessionSidebar.test.mjs
11062
  index 6bc923f..70b9e80 100644
11063
  --- a/components/SessionSidebar.test.mjs
 
11090
  // search in the toolbar row's place.
11091
  const head = html.slice(html.indexOf('<div class="sidebar-files-head">'), html.indexOf('<div class="sidebar-files-scroll'));
11092
  diff --git a/components/SessionTree.test.mjs b/components/SessionTree.test.mjs
11093
+ index fb53e4b..060f6d0 100644
11094
  --- a/components/SessionTree.test.mjs
11095
  +++ b/components/SessionTree.test.mjs
11096
  @@ -119,7 +119,7 @@ test("the pinned header toggles the section and shows activity dots only while c
 
11111
  assert.match(html, /<span class="session-tree-group-name">app<\/span><svg[^>]*class="session-tree-group-pin"[\s\S]*?<\/svg><span class="session-tree-summary">/);
11112
  // No folder: a project group is no second file tree.
11113
  assert.doesNotMatch(html, /session-tree-group-icon/);
11114
+ @@ -467,12 +467,13 @@ test("row CSS stays flat, themed and quiet", () => {
11115
+ assert.doesNotMatch(css, /&/, "no CSS nesting");
11116
  assert.doesNotMatch(css, /@starting-style|prefers-color-scheme/);
11117
  const colors = new Set((css.match(/#[0-9a-fA-F]{3,8}\b/g) ?? []).map((color) => color.toLowerCase()));
11118
+ - for (const color of colors) assert.ok(["#ef4444", "#0891b2", "#d97706", "#f87171", "#fff"].includes(color), `unexpected color ${color}`);
11119
  - // Three text colors: content, labels, meta (plus the accent for states,
11120
  - // the errors' red and the delete button's white). No mixes of their own.
11121
+ + for (const color of colors) assert.ok(["#ef4444", "#d97706", "#f87171", "#fff"].includes(color), `unexpected color ${color}`);
11122
  + // Three text colors: content, labels, meta, the selected title a step
11123
  + // stronger (plus the accent for states, the errors' red and the delete
11124
  + // button's white). No mixes of their own.
 
11678
  </div>
11679
  );
11680
  diff --git a/components/TerminalPanel.tsx b/components/TerminalPanel.tsx
11681
+ index d24bb94..77b2f58 100644
11682
  --- a/components/TerminalPanel.tsx
11683
  +++ b/components/TerminalPanel.tsx
11684
  @@ -18,6 +18,8 @@ import {
 
11690
 
11691
  interface Props {
11692
  tab: TerminalTab;
11693
+ @@ -94,11 +96,12 @@ export function TerminalPanel({ tab, active, onRestart, onClosed, onCloseError }
11694
+ screenReaderMode: true,
11695
+ disableStdin: true,
11696
+ theme: {
11697
+ - background: "#111318", foreground: "#d7dce5", cursor: "#60a5fa",
11698
+ - selectionBackground: "#365b8a",
11699
+ - black: "#1d222b", red: "#f87171", green: "#4ade80", yellow: "#facc15",
11700
+ - blue: "#60a5fa", magenta: "#c084fc", cyan: "#22d3ee", white: "#e5e7eb",
11701
+ - brightBlack: "#6b7280", brightRed: "#fca5a5", brightGreen: "#86efac",
11702
+ + // Neutral grays like the rest of the app; the ANSI hues stay, they are the programs' own colours.
11703
+ + background: "#111111", foreground: "#d4d4d4", cursor: "#e5e5e5",
11704
+ + selectionBackground: "#3d3d3d",
11705
+ + black: "#1f1f1f", red: "#f87171", green: "#4ade80", yellow: "#facc15",
11706
+ + blue: "#60a5fa", magenta: "#c084fc", cyan: "#22d3ee", white: "#e5e5e5",
11707
+ + brightBlack: "#737373", brightRed: "#fca5a5", brightGreen: "#86efac",
11708
+ brightYellow: "#fde047", brightBlue: "#93c5fd", brightMagenta: "#d8b4fe",
11709
+ brightCyan: "#67e8f9", brightWhite: "#ffffff",
11710
+ },
11711
+ @@ -260,15 +263,11 @@ export function TerminalPanel({ tab, active, onRestart, onClosed, onCloseError }
11712
  </div>
11713
  {status === "error" && (
11714
  <button type="button" onClick={() => setReconnectKey((key) => key + 1)} disabled={Boolean(tab.closing)} title={t("terminal.reconnect")} aria-label={t("terminal.reconnect")}>
 
12441
  "chat.filesWritten": "已變更的檔案",
12442
  "chat.openWrittenFile": "開啟 {name}",
12443
  "chat.loadEarlier": "向上捲動以載入較早的訊息",
12444
+ diff --git a/lib/neutral-code-theme.test.mjs b/lib/neutral-code-theme.test.mjs
12445
+ new file mode 100644
12446
+ index 0000000..a9dc596
12447
+ --- /dev/null
12448
+ +++ b/lib/neutral-code-theme.test.mjs
12449
+ @@ -0,0 +1,45 @@
12450
+ +import assert from "node:assert/strict";
12451
+ +import test from "node:test";
12452
+ +import { createRequire } from "node:module";
12453
+ +
12454
+ +const require = createRequire(import.meta.url);
12455
+ +const { vscDarkPlus, vs } = require("react-syntax-highlighter/dist/cjs/styles/prism");
12456
+ +const load = () => import("./neutral-code-theme.ts");
12457
+ +
12458
+ +// A colour is blue when its hue sits between cyan and violet and it is not a gray.
12459
+ +function isBlue(hex) {
12460
+ + const m = /^#([0-9a-f]{6})$/i.exec(hex ?? "");
12461
+ + if (!m) return false;
12462
+ + const [r, g, b] = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16) / 255);
12463
+ + const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min, l = (max + min) / 2;
12464
+ + if (d < 0.06) return false;
12465
+ + const s = d / (1 - Math.abs(2 * l - 1));
12466
+ + let h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
12467
+ + h = (h * 60 + 360) % 360;
12468
+ + return s > 0.2 && h >= 180 && h <= 275;
12469
+ +}
12470
+ +
12471
+ +test("no token of either code theme is blue; keywords keep standing out by weight", async () => {
12472
+ + const { neutralCodeTheme } = await load();
12473
+ + for (const [theme, mode, keyword] of [[vscDarkPlus, "dark", "keyword"], [vs, "light", "keyword"]]) {
12474
+ + const out = neutralCodeTheme(theme, mode);
12475
+ + const blue = Object.entries(out).filter(([, style]) => isBlue(style.color)).map(([k, style]) => `${k} ${style.color}`);
12476
+ + assert.deepEqual(blue, [], `${mode}: blue tokens left`);
12477
+ + assert.equal(out[keyword].fontWeight, 600, `${mode}: keywords are bold`);
12478
+ + }
12479
+ + // the other hues stay: strings, comments
12480
+ + assert.equal(neutralCodeTheme(vscDarkPlus, "dark").string.color, vscDarkPlus.string.color);
12481
+ + assert.equal(neutralCodeTheme(vs, "light").comment.color, vs.comment.color);
12482
+ + // the source themes are not changed
12483
+ + assert.equal(vscDarkPlus.keyword.color.toLowerCase(), "#569cd6");
12484
+ +});
12485
+ +
12486
+ +test("Mermaid diagrams use the base theme with neutral variables only", async () => {
12487
+ + const { neutralMermaidTheme } = await load();
12488
+ + for (const dark of [true, false]) {
12489
+ + const { theme, themeVariables } = neutralMermaidTheme(dark);
12490
+ + assert.equal(theme, "base");
12491
+ + const blue = Object.entries(themeVariables).filter(([, v]) => typeof v === "string" && isBlue(v));
12492
+ + assert.deepEqual(blue, []);
12493
+ + }
12494
+ +});
12495
+ diff --git a/lib/neutral-code-theme.ts b/lib/neutral-code-theme.ts
12496
+ new file mode 100644
12497
+ index 0000000..ff0d9d6
12498
+ --- /dev/null
12499
+ +++ b/lib/neutral-code-theme.ts
12500
+ @@ -0,0 +1,60 @@
12501
+ +import type { CSSProperties } from "react";
12502
+ +
12503
+ +/**
12504
+ + * The Prism themes Pi Web highlights code with (VS Code's dark+ and light "vs"),
12505
+ + * with their blue tokens turned into the design system's neutral grays: no blue
12506
+ + * anywhere in the app, code blocks included. Keywords keep standing out by weight
12507
+ + * (600) instead of hue; strings, numbers, functions and comments keep their own
12508
+ + * colours. Client-safe and pure.
12509
+ + */
12510
+ +type PrismTheme = Record<string, CSSProperties>;
12511
+ +
12512
+ +const DARK: Record<string, CSSProperties> = {
12513
+ + "#569cd6": { color: "#e5e5e5", fontWeight: 600 }, // keywords, tags, booleans
12514
+ + "#9cdcfe": { color: "#e5e5e5" }, // properties, constants, attribute names
12515
+ +};
12516
+ +
12517
+ +const LIGHT: Record<string, CSSProperties> = {
12518
+ + "#0000ff": { color: "#0a0a0a", fontWeight: 600 }, // keywords, at-rules, attribute values
12519
+ + "#00009f": { color: "#0a0a0a", fontWeight: 600 },
12520
+ + "#2b91af": { color: "#404040", fontWeight: 500 }, // class names, JSON keys
12521
+ + "#36acaa": { color: "#5c5c5c" }, // numbers, booleans, variables
12522
+ +};
12523
+ +
12524
+ +export function neutralCodeTheme(theme: PrismTheme, mode: "dark" | "light"): PrismTheme {
12525
+ + const map = mode === "dark" ? DARK : LIGHT;
12526
+ + const out: PrismTheme = {};
12527
+ + for (const [selector, style] of Object.entries(theme)) {
12528
+ + const color = typeof style.color === "string" ? style.color.toLowerCase() : "";
12529
+ + out[selector] = map[color] ? { ...style, ...map[color] } : { ...style };
12530
+ + }
12531
+ + return out;
12532
+ +}
12533
+ +
12534
+ +/** Mermaid's diagrams in the same neutral grays ("base" theme), never its lavender and blue defaults. */
12535
+ +export function neutralMermaidTheme(isDark: boolean) {
12536
+ + return {
12537
+ + theme: "base" as const,
12538
+ + themeVariables: isDark
12539
+ + ? {
12540
+ + darkMode: true, background: "#0a0a0a", fontFamily: "Geist, ui-sans-serif, system-ui, sans-serif",
12541
+ + primaryColor: "#1f1f1f", primaryTextColor: "#ededed", primaryBorderColor: "#525252",
12542
+ + secondaryColor: "#2a2a2a", secondaryTextColor: "#ededed", secondaryBorderColor: "#525252",
12543
+ + tertiaryColor: "#1a1a1a", tertiaryTextColor: "#d4d4d4", tertiaryBorderColor: "#3d3d3d",
12544
+ + lineColor: "#a3a3a3", textColor: "#d4d4d4", mainBkg: "#1f1f1f", nodeBorder: "#525252",
12545
+ + clusterBkg: "#141414", clusterBorder: "#3d3d3d", edgeLabelBackground: "#1a1a1a",
12546
+ + noteBkgColor: "#2a2a2a", noteTextColor: "#ededed", noteBorderColor: "#525252",
12547
+ + actorBkg: "#1f1f1f", actorBorder: "#525252", actorTextColor: "#ededed", signalColor: "#d4d4d4", signalTextColor: "#d4d4d4",
12548
+ + }
12549
+ + : {
12550
+ + darkMode: false, background: "#ffffff", fontFamily: "Geist, ui-sans-serif, system-ui, sans-serif",
12551
+ + primaryColor: "#f5f5f5", primaryTextColor: "#0a0a0a", primaryBorderColor: "#a3a3a3",
12552
+ + secondaryColor: "#e5e5e5", secondaryTextColor: "#0a0a0a", secondaryBorderColor: "#a3a3a3",
12553
+ + tertiaryColor: "#fafafa", tertiaryTextColor: "#2e2e2e", tertiaryBorderColor: "#d4d4d4",
12554
+ + lineColor: "#525252", textColor: "#2e2e2e", mainBkg: "#f5f5f5", nodeBorder: "#a3a3a3",
12555
+ + clusterBkg: "#fafafa", clusterBorder: "#d4d4d4", edgeLabelBackground: "#ffffff",
12556
+ + noteBkgColor: "#f5f5f5", noteTextColor: "#0a0a0a", noteBorderColor: "#a3a3a3",
12557
+ + actorBkg: "#f5f5f5", actorBorder: "#a3a3a3", actorTextColor: "#0a0a0a", signalColor: "#2e2e2e", signalTextColor: "#2e2e2e",
12558
+ + },
12559
+ + };
12560
+ +}
12561
  diff --git a/lib/step-summary.test.mjs b/lib/step-summary.test.mjs
12562
  new file mode 100644
12563
  index 0000000..6cbe005