@firstpick/pi-package-webui 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="manifest" href="/manifest.webmanifest" />
13
13
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
14
14
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
15
- <link rel="stylesheet" href="/styles.css?v=43" />
15
+ <link rel="stylesheet" href="/styles.css?v=50" />
16
16
  </head>
17
17
  <body>
18
18
  <button id="sidePanelExpandButton" class="side-panel-expand-button" type="button" aria-controls="sidePanel" aria-expanded="false" aria-label="Expand side panel" title="Expand side panel">
@@ -74,6 +74,13 @@
74
74
  <button id="closeAllTabsButton" class="terminal-close-all-button" type="button" title="Close all terminal tabs">Close all Tabs</button>
75
75
  </header>
76
76
  <div id="widgetArea" class="widget-area"></div>
77
+ <div id="chatSearchBar" class="chat-search-bar" role="search" hidden>
78
+ <input id="chatSearchInput" class="chat-search-input" type="search" placeholder="Search transcript…" autocomplete="off" spellcheck="false" aria-label="Search transcript" />
79
+ <span id="chatSearchCount" class="chat-search-count muted" aria-live="polite"></span>
80
+ <button id="chatSearchPrevButton" class="chat-search-button" type="button" aria-label="Previous match" title="Previous match (Shift+Enter)">↑</button>
81
+ <button id="chatSearchNextButton" class="chat-search-button" type="button" aria-label="Next match" title="Next match (Enter)">↓</button>
82
+ <button id="chatSearchCloseButton" class="chat-search-button" type="button" aria-label="Close search" title="Close (Escape)">✕</button>
83
+ </div>
77
84
  <div id="chat" class="chat" aria-live="polite">
78
85
  <button id="stickyUserPromptButton" class="sticky-user-prompt-button" type="button" aria-controls="chat" hidden></button>
79
86
  </div>
@@ -86,7 +93,7 @@
86
93
  <section id="gitWorkflowPanel" class="git-workflow-panel" aria-live="polite" hidden>
87
94
  <div class="git-workflow-header">
88
95
  <div>
89
- <span class="git-workflow-kicker">Git workflow</span>
96
+ <span id="gitWorkflowKicker" class="git-workflow-kicker">Git workflow</span>
90
97
  <strong id="gitWorkflowTitle">Ready</strong>
91
98
  <p id="gitWorkflowHint" class="muted">Stage changes, generate a commit message, commit, and push.</p>
92
99
  </div>
@@ -205,6 +212,9 @@
205
212
  <button id="optionsTreeButton" class="composer-publish-menu-item composer-options-menu-item" type="button" role="menuitem" data-command="/tree">
206
213
  <span>Tree</span>
207
214
  </button>
215
+ <button id="optionsStatsButton" class="composer-publish-menu-item composer-options-menu-item" type="button" role="menuitem" data-command="/stats-webui" hidden>
216
+ <span>Stats Dashboard</span>
217
+ </button>
208
218
  <button id="optionsForkButton" class="composer-publish-menu-item composer-options-menu-item" type="button" role="menuitem" data-command="/fork">
209
219
  <span>Fork</span>
210
220
  </button>
@@ -513,6 +523,34 @@
513
523
  </form>
514
524
  </dialog>
515
525
 
526
+ <dialog id="statsOverlayDialog" class="extension-dialog stats-overlay-dialog">
527
+ <form method="dialog">
528
+ <div class="stats-overlay-header">
529
+ <div>
530
+ <span class="stats-overlay-kicker">Pi stats</span>
531
+ <h2>Usage dashboard</h2>
532
+ <p id="statsOverlaySubtitle" class="muted">Run stats to load the browser dashboard.</p>
533
+ </div>
534
+ <div class="stats-overlay-controls">
535
+ <label for="statsOverlayScope">Range</label>
536
+ <select id="statsOverlayScope" title="Stats range" aria-label="Stats range">
537
+ <option value="14">14 days</option>
538
+ <option value="30">30 days</option>
539
+ <option value="90">90 days</option>
540
+ <option value="custom">Custom…</option>
541
+ <option value="all">All</option>
542
+ </select>
543
+ <input id="statsOverlayCustomDays" class="stats-overlay-custom-days" type="number" min="1" max="3650" step="1" value="14" aria-label="Custom stats range in days" hidden />
544
+ <button id="statsOverlayRefreshButton" type="button">Refresh</button>
545
+ <button id="statsOverlayCloseButton" class="stats-overlay-close-button" type="button" aria-label="Close stats dashboard">Close</button>
546
+ </div>
547
+ </div>
548
+ <p id="statsOverlayStatus" class="stats-overlay-status muted" role="status" aria-live="polite" hidden></p>
549
+ <div id="statsOverlayTabs" class="stats-overlay-tabs" role="tablist" aria-label="Stats dashboard views"></div>
550
+ <div id="statsOverlayBody" class="stats-overlay-body"></div>
551
+ </form>
552
+ </dialog>
553
+
516
554
  <dialog id="pathPickerDialog" class="extension-dialog path-picker-dialog">
517
555
  <form method="dialog">
518
556
  <h2 id="pathPickerTitle">Choose working directory</h2>
@@ -616,6 +654,6 @@
616
654
  </form>
617
655
  </dialog>
618
656
 
619
- <script type="module" src="/app.js?v=43"></script>
657
+ <script type="module" src="/app.js?v=50"></script>
620
658
  </body>
621
659
  </html>
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "pi-webui-pwa-v26";
1
+ const CACHE_NAME = "pi-webui-pwa-v27";
2
2
  const APP_SHELL = [
3
3
  "/",
4
4
  "/index.html",
package/public/styles.css CHANGED
@@ -1167,10 +1167,19 @@ body.side-panel-collapsed .terminal-tabs-shell {
1167
1167
  color: rgba(var(--ctp-teal-rgb), 0.86);
1168
1168
  font-size: 0.68rem;
1169
1169
  }
1170
+ .optional-feature-actions {
1171
+ display: grid;
1172
+ gap: 0.42rem;
1173
+ justify-items: end;
1174
+ }
1170
1175
  .optional-feature-action {
1171
1176
  min-width: 5.2rem;
1172
1177
  white-space: nowrap;
1173
1178
  }
1179
+ .optional-feature-action.setup {
1180
+ color: var(--ctp-teal);
1181
+ border-color: rgba(148, 226, 213, 0.32);
1182
+ }
1174
1183
  .optional-feature-action.install {
1175
1184
  color: var(--ctp-yellow);
1176
1185
  border-color: rgba(249, 226, 175, 0.32);
@@ -2609,6 +2618,31 @@ button.git-workflow-step:hover:not(:disabled) {
2609
2618
  line-height: 1.45;
2610
2619
  white-space: pre-wrap;
2611
2620
  }
2621
+ .git-workflow-message-input-row {
2622
+ flex: 1 1 100%;
2623
+ display: flex;
2624
+ gap: 0.5rem;
2625
+ align-items: flex-end;
2626
+ min-width: min(100%, 22rem);
2627
+ }
2628
+ .git-workflow-message-input-field {
2629
+ flex: 1 1 18rem;
2630
+ display: grid;
2631
+ gap: 0.28rem;
2632
+ min-width: min(100%, 15rem);
2633
+ }
2634
+ .git-workflow-message-input-label {
2635
+ color: rgba(var(--ctp-subtext-rgb), 0.82);
2636
+ font-size: 0.72rem;
2637
+ font-weight: 900;
2638
+ letter-spacing: 0.08em;
2639
+ text-transform: uppercase;
2640
+ }
2641
+ .git-workflow-message-input {
2642
+ width: 100%;
2643
+ min-height: 2.15rem;
2644
+ padding: 0.45rem 0.72rem;
2645
+ }
2612
2646
  .git-workflow-actions button {
2613
2647
  min-height: 2.15rem;
2614
2648
  padding: 0.45rem 0.72rem;
@@ -5152,6 +5186,297 @@ button.composer-skill-tag:focus-visible {
5152
5186
  padding: 0;
5153
5187
  margin: 1rem 0 0;
5154
5188
  }
5189
+ .extension-dialog.stats-overlay-dialog {
5190
+ width: min(92rem, calc(100vw - 1.5rem));
5191
+ height: min(54rem, calc(var(--visual-viewport-height, 100dvh) - 1.5rem));
5192
+ max-height: calc(var(--visual-viewport-height, 100dvh) - 1.5rem);
5193
+ overflow: hidden;
5194
+ border-color: rgba(137, 180, 250, 0.48);
5195
+ box-shadow: 0 2rem 5rem var(--shadow), 0 0 2rem rgba(137, 180, 250, 0.22);
5196
+ }
5197
+ .stats-overlay-dialog form {
5198
+ display: grid;
5199
+ grid-template-rows: auto auto auto minmax(0, 1fr);
5200
+ gap: 0.82rem;
5201
+ height: 100%;
5202
+ min-height: 0;
5203
+ min-width: 0;
5204
+ max-height: 100%;
5205
+ overflow: hidden;
5206
+ }
5207
+ .stats-overlay-header {
5208
+ display: flex;
5209
+ align-items: flex-start;
5210
+ justify-content: space-between;
5211
+ gap: 1rem;
5212
+ }
5213
+ .stats-overlay-kicker {
5214
+ display: block;
5215
+ color: var(--ctp-sky);
5216
+ font-size: 0.72rem;
5217
+ font-weight: 900;
5218
+ letter-spacing: 0.12em;
5219
+ text-transform: uppercase;
5220
+ }
5221
+ .stats-overlay-header h2 {
5222
+ margin: 0.12rem 0 0;
5223
+ color: var(--ctp-text);
5224
+ }
5225
+ .stats-overlay-header p { margin: 0.25rem 0 0; }
5226
+ .stats-overlay-controls {
5227
+ display: flex;
5228
+ align-items: end;
5229
+ gap: 0.5rem;
5230
+ flex-wrap: wrap;
5231
+ justify-content: flex-end;
5232
+ }
5233
+ .stats-overlay-controls label {
5234
+ color: rgba(var(--ctp-subtext-rgb), 0.74);
5235
+ font-size: 0.68rem;
5236
+ font-weight: 900;
5237
+ letter-spacing: 0.08em;
5238
+ text-transform: uppercase;
5239
+ }
5240
+ .stats-overlay-controls select { min-width: 7.5rem; }
5241
+ .stats-overlay-close-button {
5242
+ border-color: rgba(249, 226, 175, 0.36);
5243
+ color: var(--ctp-yellow);
5244
+ }
5245
+ .stats-overlay-custom-days {
5246
+ width: 7rem;
5247
+ min-width: 0;
5248
+ }
5249
+ .stats-overlay-status {
5250
+ margin: 0;
5251
+ padding: 0.55rem 0.7rem;
5252
+ border: 1px solid rgba(137, 180, 250, 0.22);
5253
+ border-radius: 0.75rem;
5254
+ background: rgba(var(--ctp-surface-rgb), 0.30);
5255
+ }
5256
+ .stats-overlay-status.error {
5257
+ color: var(--ctp-red);
5258
+ border-color: rgba(243, 139, 168, 0.38);
5259
+ }
5260
+ .stats-overlay-tabs {
5261
+ display: flex;
5262
+ gap: 0.42rem;
5263
+ overflow-x: auto;
5264
+ padding-bottom: 0.12rem;
5265
+ scrollbar-width: thin;
5266
+ }
5267
+ .stats-overlay-tabs button {
5268
+ flex: 0 0 auto;
5269
+ border-color: rgba(137, 180, 250, 0.24);
5270
+ background: rgba(var(--ctp-surface-rgb), 0.28);
5271
+ color: rgba(var(--ctp-subtext-rgb), 0.86);
5272
+ font-weight: 800;
5273
+ }
5274
+ .stats-overlay-tabs button.active {
5275
+ color: var(--ctp-sky);
5276
+ border-color: rgba(137, 180, 250, 0.56);
5277
+ box-shadow: 0 0 1rem rgba(137, 180, 250, 0.14), inset 0 0 1rem rgba(137, 180, 250, 0.08);
5278
+ }
5279
+ .stats-overlay-body {
5280
+ min-height: 0;
5281
+ overflow: auto;
5282
+ padding: 0.1rem 0.18rem 0.75rem 0;
5283
+ overscroll-behavior: contain;
5284
+ -webkit-overflow-scrolling: touch;
5285
+ }
5286
+ .stats-overlay-pane {
5287
+ display: grid;
5288
+ gap: 0.82rem;
5289
+ }
5290
+ .stats-overlay-pane h3 {
5291
+ margin: 0.25rem 0 0;
5292
+ color: var(--ctp-blue);
5293
+ font-size: 0.9rem;
5294
+ }
5295
+ .stats-overlay-cards {
5296
+ display: grid;
5297
+ grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
5298
+ gap: 0.72rem;
5299
+ }
5300
+ .stats-overlay-cards.compact { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
5301
+ .stats-overlay-card {
5302
+ display: grid;
5303
+ gap: 0.22rem;
5304
+ min-width: 0;
5305
+ padding: 0.78rem 0.85rem;
5306
+ border: 1px solid rgba(137, 180, 250, 0.22);
5307
+ border-radius: 0.9rem;
5308
+ background: linear-gradient(145deg, rgba(var(--ctp-surface-rgb), 0.38), rgba(var(--ctp-crust-rgb), 0.48));
5309
+ box-shadow: inset 0 0 1.4rem rgba(137, 180, 250, 0.04);
5310
+ }
5311
+ .stats-overlay-card-label,
5312
+ .stats-overlay-card-detail {
5313
+ min-width: 0;
5314
+ color: rgba(var(--ctp-subtext-rgb), 0.72);
5315
+ font-size: 0.72rem;
5316
+ line-height: 1.25;
5317
+ }
5318
+ .stats-overlay-card-label {
5319
+ font-weight: 900;
5320
+ letter-spacing: 0.08em;
5321
+ text-transform: uppercase;
5322
+ }
5323
+ .stats-overlay-card strong {
5324
+ min-width: 0;
5325
+ color: var(--ctp-text);
5326
+ font-size: 1.35rem;
5327
+ overflow-wrap: anywhere;
5328
+ }
5329
+ .stats-overlay-card.tone-blue { border-color: rgba(137, 180, 250, 0.38); }
5330
+ .stats-overlay-card.tone-green { border-color: rgba(166, 227, 161, 0.38); }
5331
+ .stats-overlay-card.tone-mauve { border-color: rgba(203, 166, 247, 0.38); }
5332
+ .stats-overlay-card.tone-yellow { border-color: rgba(249, 226, 175, 0.38); }
5333
+ .stats-overlay-card.tone-teal { border-color: rgba(148, 226, 213, 0.38); }
5334
+ .stats-overlay-card.tone-pink { border-color: rgba(245, 194, 231, 0.38); }
5335
+ .stats-overlay-calibration-panel {
5336
+ display: flex;
5337
+ align-items: center;
5338
+ justify-content: space-between;
5339
+ gap: 0.8rem;
5340
+ padding: 0.82rem 0.9rem;
5341
+ border: 1px solid rgba(249, 226, 175, 0.26);
5342
+ border-radius: 0.9rem;
5343
+ background: linear-gradient(135deg, rgba(249, 226, 175, 0.08), rgba(var(--ctp-crust-rgb), 0.48));
5344
+ }
5345
+ .stats-overlay-calibration-copy {
5346
+ display: grid;
5347
+ gap: 0.2rem;
5348
+ min-width: 0;
5349
+ }
5350
+ .stats-overlay-calibration-copy strong {
5351
+ color: var(--ctp-yellow);
5352
+ font-size: 0.86rem;
5353
+ }
5354
+ .stats-overlay-calibration-copy span {
5355
+ color: rgba(var(--ctp-subtext-rgb), 0.78);
5356
+ font-size: 0.76rem;
5357
+ line-height: 1.35;
5358
+ }
5359
+ .stats-overlay-calibration-copy .warning { color: var(--ctp-yellow); }
5360
+ .stats-overlay-calibration-actions {
5361
+ display: flex;
5362
+ flex: 0 0 auto;
5363
+ gap: 0.5rem;
5364
+ flex-wrap: wrap;
5365
+ justify-content: flex-end;
5366
+ }
5367
+ .stats-overlay-bars {
5368
+ display: grid;
5369
+ gap: 0.42rem;
5370
+ }
5371
+ .stats-overlay-bar-row {
5372
+ display: grid;
5373
+ grid-template-columns: 6.2rem minmax(7rem, 1fr) 5.5rem 4.8rem;
5374
+ gap: 0.55rem;
5375
+ align-items: center;
5376
+ padding: 0.42rem 0.5rem;
5377
+ border: 1px solid rgba(var(--ctp-overlay-rgb), 0.16);
5378
+ border-radius: 0.72rem;
5379
+ background: rgba(var(--ctp-surface-rgb), 0.20);
5380
+ }
5381
+ .stats-overlay-bar {
5382
+ height: 0.55rem;
5383
+ overflow: hidden;
5384
+ border-radius: 999px;
5385
+ background: rgba(var(--ctp-crust-rgb), 0.78);
5386
+ box-shadow: inset 0 0 0 1px rgba(137, 180, 250, 0.12);
5387
+ }
5388
+ .stats-overlay-bar-fill {
5389
+ display: block;
5390
+ height: 100%;
5391
+ border-radius: inherit;
5392
+ background: linear-gradient(90deg, var(--ctp-blue), var(--ctp-sky), var(--ctp-teal));
5393
+ box-shadow: 0 0 0.9rem rgba(137, 180, 250, 0.32);
5394
+ }
5395
+ .stats-overlay-bar-day,
5396
+ .stats-overlay-bar-value,
5397
+ .stats-overlay-bar-cost {
5398
+ color: rgba(var(--ctp-subtext-rgb), 0.86);
5399
+ font-size: 0.78rem;
5400
+ white-space: nowrap;
5401
+ }
5402
+ .stats-overlay-bar-value,
5403
+ .stats-overlay-bar-cost { text-align: right; }
5404
+ .stats-overlay-table-wrap {
5405
+ overflow: auto;
5406
+ border: 1px solid rgba(137, 180, 250, 0.16);
5407
+ border-radius: 0.88rem;
5408
+ }
5409
+ .stats-overlay-table {
5410
+ width: 100%;
5411
+ min-width: 42rem;
5412
+ border-collapse: collapse;
5413
+ font-size: 0.82rem;
5414
+ }
5415
+ .stats-overlay-table th,
5416
+ .stats-overlay-table td {
5417
+ padding: 0.58rem 0.64rem;
5418
+ border-bottom: 1px solid rgba(var(--ctp-overlay-rgb), 0.12);
5419
+ text-align: left;
5420
+ vertical-align: top;
5421
+ }
5422
+ .stats-overlay-table th {
5423
+ position: sticky;
5424
+ top: 0;
5425
+ z-index: 1;
5426
+ color: var(--ctp-sky);
5427
+ background: rgba(var(--ctp-crust-rgb), 0.96);
5428
+ font-size: 0.68rem;
5429
+ letter-spacing: 0.08em;
5430
+ text-transform: uppercase;
5431
+ }
5432
+ .stats-overlay-table td:nth-child(n+3) { white-space: nowrap; }
5433
+ .stats-overlay-command-section {
5434
+ display: grid;
5435
+ gap: 0.52rem;
5436
+ padding: 0.78rem;
5437
+ border: 1px solid rgba(137, 180, 250, 0.16);
5438
+ border-radius: 0.9rem;
5439
+ background: rgba(var(--ctp-surface-rgb), 0.16);
5440
+ }
5441
+ .stats-overlay-command-header {
5442
+ display: flex;
5443
+ align-items: flex-start;
5444
+ justify-content: space-between;
5445
+ gap: 0.75rem;
5446
+ }
5447
+ .stats-overlay-command-title {
5448
+ display: grid;
5449
+ gap: 0.18rem;
5450
+ min-width: 0;
5451
+ }
5452
+ .stats-overlay-command-title h3,
5453
+ .stats-overlay-command-title p {
5454
+ margin: 0;
5455
+ }
5456
+ .stats-overlay-command-pill {
5457
+ flex: 0 0 auto;
5458
+ padding: 0.28rem 0.46rem;
5459
+ border: 1px solid rgba(137, 180, 250, 0.24);
5460
+ border-radius: 999px;
5461
+ background: rgba(var(--ctp-crust-rgb), 0.55);
5462
+ color: var(--ctp-sky);
5463
+ font-size: 0.72rem;
5464
+ white-space: nowrap;
5465
+ }
5466
+ .stats-overlay-lines {
5467
+ max-height: 24rem;
5468
+ margin: 0;
5469
+ padding: 0.78rem;
5470
+ overflow: auto;
5471
+ border: 1px solid rgba(137, 180, 250, 0.18);
5472
+ border-radius: 0.82rem;
5473
+ background: rgba(var(--ctp-crust-rgb), 0.62);
5474
+ color: rgba(var(--ctp-text-rgb), 0.92);
5475
+ font-size: 0.78rem;
5476
+ line-height: 1.42;
5477
+ white-space: pre;
5478
+ }
5479
+ .stats-overlay-empty { margin: 0.4rem 0; }
5155
5480
  .dialog-options {
5156
5481
  display: grid;
5157
5482
  gap: 0.5rem;
@@ -6506,6 +6831,46 @@ button.composer-skill-tag:focus-visible {
6506
6831
  overflow: auto;
6507
6832
  border-radius: 1rem 1rem 0 0;
6508
6833
  }
6834
+ .extension-dialog.stats-overlay-dialog {
6835
+ inset: calc(0.5rem + env(safe-area-inset-top)) 0 0 0;
6836
+ width: 100vw;
6837
+ height: calc(var(--visual-viewport-height, 100dvh) - 0.5rem - env(safe-area-inset-top));
6838
+ max-height: calc(var(--visual-viewport-height, 100dvh) - 0.5rem - env(safe-area-inset-top));
6839
+ }
6840
+ .stats-overlay-dialog form {
6841
+ height: 100%;
6842
+ max-height: 100%;
6843
+ }
6844
+ .stats-overlay-header {
6845
+ flex-direction: column;
6846
+ gap: 0.65rem;
6847
+ }
6848
+ .stats-overlay-controls,
6849
+ .stats-overlay-controls select,
6850
+ .stats-overlay-custom-days,
6851
+ .stats-overlay-controls button {
6852
+ width: 100%;
6853
+ }
6854
+ .stats-overlay-calibration-panel,
6855
+ .stats-overlay-calibration-actions {
6856
+ display: grid;
6857
+ grid-template-columns: 1fr;
6858
+ width: 100%;
6859
+ }
6860
+ .stats-overlay-command-header {
6861
+ display: grid;
6862
+ grid-template-columns: 1fr;
6863
+ }
6864
+ .stats-overlay-command-pill {
6865
+ justify-self: start;
6866
+ white-space: normal;
6867
+ }
6868
+ .stats-overlay-bar-row {
6869
+ grid-template-columns: 1fr;
6870
+ gap: 0.32rem;
6871
+ }
6872
+ .stats-overlay-bar-value,
6873
+ .stats-overlay-bar-cost { text-align: left; }
6509
6874
  .extension-dialog.release-dialog form {
6510
6875
  max-height: calc(var(--visual-viewport-height, 100dvh) - 2rem - env(safe-area-inset-top));
6511
6876
  }
@@ -6561,6 +6926,11 @@ button.composer-skill-tag:focus-visible {
6561
6926
  .composer-row button[data-tooltip].tooltip-open::after { display: block; }
6562
6927
  .git-workflow-actions button,
6563
6928
  #gitWorkflowCancelButton { min-height: 44px; }
6929
+ .git-workflow-message-input-row {
6930
+ flex-direction: column;
6931
+ align-items: stretch;
6932
+ }
6933
+ .git-workflow-message-input-commit { width: 100%; }
6564
6934
  .native-command-body { max-height: min(28rem, 54dvh); }
6565
6935
  .native-settings-grid,
6566
6936
  .native-tree-options { grid-template-columns: 1fr; }
@@ -6584,3 +6954,48 @@ button.composer-skill-tag:focus-visible {
6584
6954
  }
6585
6955
  .path-picker-list { max-height: min(18rem, 42dvh); }
6586
6956
  }
6957
+
6958
+ /* Transcript search (Ctrl/Cmd+F) */
6959
+ .chat-search-bar {
6960
+ display: flex;
6961
+ align-items: center;
6962
+ gap: 0.4rem;
6963
+ padding: 0.4rem 0.75rem;
6964
+ border-bottom: 1px solid var(--border);
6965
+ background: var(--panel-3);
6966
+ }
6967
+ .chat-search-bar[hidden] { display: none; }
6968
+ .chat-search-input {
6969
+ flex: 1 1 auto;
6970
+ min-width: 0;
6971
+ padding: 0.3rem 0.55rem;
6972
+ border: 1px solid var(--border);
6973
+ border-radius: 0.5rem;
6974
+ background: var(--panel);
6975
+ color: inherit;
6976
+ font: inherit;
6977
+ }
6978
+ .chat-search-input:focus-visible {
6979
+ outline: 2px solid var(--accent);
6980
+ outline-offset: 1px;
6981
+ }
6982
+ .chat-search-count {
6983
+ flex: 0 0 auto;
6984
+ min-width: 3.2em;
6985
+ text-align: center;
6986
+ font-variant-numeric: tabular-nums;
6987
+ }
6988
+ .chat-search-button {
6989
+ flex: 0 0 auto;
6990
+ padding: 0.25rem 0.55rem;
6991
+ border: 1px solid var(--border);
6992
+ border-radius: 0.5rem;
6993
+ background: var(--panel-2);
6994
+ color: inherit;
6995
+ cursor: pointer;
6996
+ }
6997
+ .chat-search-button:hover { background: var(--panel); }
6998
+ .message.search-current {
6999
+ outline: 2px solid var(--accent-2);
7000
+ outline-offset: 2px;
7001
+ }
@@ -47,7 +47,16 @@ rl.on("line", (line) => {
47
47
  });
48
48
  return;
49
49
  case "get_messages":
50
- respond({ ...base, data: { messages: [] } });
50
+ respond({
51
+ ...base,
52
+ data: {
53
+ messages: [
54
+ { role: "user", content: "fake prompt", timestamp: 1000 },
55
+ { role: "assistant", content: [{ type: "text", text: "fake answer" }], timestamp: 2000 },
56
+ { role: "user", content: "fake follow-up", timestamp: 3000 },
57
+ ],
58
+ },
59
+ });
51
60
  return;
52
61
  case "get_available_models":
53
62
  respond({ ...base, data: { models: [{ provider: "fake", id: "fake-model", name: "Fake Model" }] } });
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
- import { spawn } from "node:child_process";
3
- import { chmod, mkdtemp, rm } from "node:fs/promises";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { chmod, mkdtemp, rm, stat } from "node:fs/promises";
4
4
  import { networkInterfaces, tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { setTimeout as delay } from "node:timers/promises";
@@ -42,6 +42,13 @@ await chmod(fakePi, 0o755);
42
42
 
43
43
  const child = spawn(process.execPath, [serverScript, "--cwd", cwd, "--host", "0.0.0.0", "--port", String(port), "--pi", fakePi], {
44
44
  stdio: ["ignore", "pipe", "pipe"],
45
+ env: {
46
+ ...process.env,
47
+ GIT_AUTHOR_NAME: "Pi WebUI Test",
48
+ GIT_AUTHOR_EMAIL: "pi-webui-test@example.invalid",
49
+ GIT_COMMITTER_NAME: "Pi WebUI Test",
50
+ GIT_COMMITTER_EMAIL: "pi-webui-test@example.invalid",
51
+ },
45
52
  });
46
53
  let serverOutput = "";
47
54
  child.stdout.on("data", (chunk) => {
@@ -68,6 +75,38 @@ try {
68
75
  assert.equal(health.body.ok, true);
69
76
  assert.equal(health.body.piRunning, true, "fake pi RPC process should be attached and running");
70
77
 
78
+ // Static assets: brotli/gzip compression plus ETag revalidation (P0-2).
79
+ const brotliResponse = await fetch(`http://127.0.0.1:${port}/app.js`, {
80
+ headers: { "accept-encoding": "br, gzip" },
81
+ signal: AbortSignal.timeout(5_000),
82
+ });
83
+ assert.equal(brotliResponse.status, 200);
84
+ assert.equal(brotliResponse.headers.get("content-encoding"), "br", "app.js should be served brotli-compressed");
85
+ assert.equal(brotliResponse.headers.get("cache-control"), "no-cache", "static assets should allow ETag revalidation");
86
+ assert.equal(brotliResponse.headers.get("vary"), "Accept-Encoding");
87
+ const appEtag = brotliResponse.headers.get("etag");
88
+ assert.ok(appEtag, "app.js response should carry an ETag");
89
+ // Node fetch transparently decompresses; equal size proves the brotli
90
+ // round-trip reproduced the exact raw asset.
91
+ const appBody = await brotliResponse.arrayBuffer();
92
+ const rawAppSize = (await stat(join(root, "public", "app.js"))).size;
93
+ assert.equal(appBody.byteLength, rawAppSize, "decompressed app.js should match the raw file byte-for-byte in size");
94
+
95
+ const conditionalResponse = await fetch(`http://127.0.0.1:${port}/app.js`, {
96
+ headers: { "if-none-match": appEtag },
97
+ signal: AbortSignal.timeout(5_000),
98
+ });
99
+ assert.equal(conditionalResponse.status, 304, "matching If-None-Match should return 304");
100
+ await conditionalResponse.arrayBuffer();
101
+
102
+ const gzipResponse = await fetch(`http://127.0.0.1:${port}/styles.css`, {
103
+ headers: { "accept-encoding": "gzip" },
104
+ signal: AbortSignal.timeout(5_000),
105
+ });
106
+ assert.equal(gzipResponse.status, 200);
107
+ assert.equal(gzipResponse.headers.get("content-encoding"), "gzip", "styles.css should fall back to gzip");
108
+ await gzipResponse.arrayBuffer();
109
+
71
110
  const tabsResponse = await request("127.0.0.1", "/api/tabs");
72
111
  assert.equal(tabsResponse.status, 200);
73
112
  const tabList = tabsResponse.body?.data?.tabs || tabsResponse.body?.tabs || [];
@@ -79,6 +118,64 @@ try {
79
118
  assert.equal(state.status, 200);
80
119
  assert.equal(state.body?.data?.model?.provider, "fake", "state should come from the fake pi RPC");
81
120
 
121
+ const gitAvailable = spawnSync("git", ["--version"], { encoding: "utf8" }).status === 0;
122
+ if (gitAvailable) {
123
+ const gitInit = await request("127.0.0.1", "/api/git-workflow/init", { method: "POST", body: { tab: tabId } });
124
+ assert.equal(gitInit.status, 200);
125
+ assert.equal(gitInit.body?.ok, true, "git init endpoint should initialize a temp repository");
126
+
127
+ const initFileStatus = await request("127.0.0.1", `/api/git-workflow/init-files-status?tab=${encodeURIComponent(tabId)}`);
128
+ assert.equal(initFileStatus.status, 200);
129
+ assert.equal(initFileStatus.body?.ok, true, "init files status endpoint should check README.md and .gitignore");
130
+ assert.equal(initFileStatus.body?.data?.readmeExists, false);
131
+ assert.equal(initFileStatus.body?.data?.gitignoreExists, false);
132
+
133
+ const gitReadme = await request("127.0.0.1", "/api/git-workflow/readme", { method: "POST", body: { repoName: "pi-webui-http-harness", stack: "Node.js / TypeScript", tab: tabId } });
134
+ assert.equal(gitReadme.status, 200);
135
+ assert.equal(gitReadme.body?.ok, true, "README endpoint should create/stage README.md and .gitignore");
136
+ assert.equal(gitReadme.body?.data?.readme?.created, true);
137
+ assert.equal(gitReadme.body?.data?.gitignore?.created, true);
138
+
139
+ const gitReadmeAgain = await request("127.0.0.1", "/api/git-workflow/readme", { method: "POST", body: { repoName: "pi-webui-http-harness", stack: "Node.js / TypeScript", tab: tabId } });
140
+ assert.equal(gitReadmeAgain.status, 200);
141
+ assert.equal(gitReadmeAgain.body?.ok, true, "README endpoint should re-check existing files without overwriting");
142
+ assert.equal(gitReadmeAgain.body?.data?.readme?.created, false);
143
+ assert.equal(gitReadmeAgain.body?.data?.gitignore?.created, false);
144
+
145
+ const gitCommit = await request("127.0.0.1", "/api/git-workflow/initial-commit", { method: "POST", body: { tab: tabId } });
146
+ assert.equal(gitCommit.status, 200);
147
+ assert.equal(gitCommit.body?.ok, true, "initial commit endpoint should commit the staged README.md");
148
+
149
+ const gitMain = await request("127.0.0.1", "/api/git-workflow/main-branch", { method: "POST", body: { tab: tabId } });
150
+ assert.equal(gitMain.status, 200);
151
+ assert.equal(gitMain.body?.ok, true, "main branch endpoint should rename the branch");
152
+
153
+ const gitRemote = await request("127.0.0.1", "/api/git-workflow/remote", { method: "POST", body: { username: "Firstp1ck", repoName: "pi-webui-http-harness", tab: tabId } });
154
+ assert.equal(gitRemote.status, 200);
155
+ assert.equal(gitRemote.body?.ok, true, "remote endpoint should add origin without pushing");
156
+ assert.equal(gitRemote.body?.data?.remoteUrl, "https://github.com/Firstp1ck/pi-webui-http-harness.git");
157
+ } else {
158
+ console.log("http-endpoints-harness: git not available; skipping git init workflow endpoint checks");
159
+ }
160
+
161
+ // Delta transcript endpoint (P1-1): ?since= returns only the tail plus merge metadata.
162
+ const fullMessages = await request("127.0.0.1", `/api/messages?tab=${encodeURIComponent(tabId)}`);
163
+ assert.equal(fullMessages.status, 200);
164
+ assert.equal((fullMessages.body?.data?.messages || []).length, 3, "fake pi should provide a 3-message transcript");
165
+ assert.equal(fullMessages.body?.data?.totalCount, undefined, "full fetches should keep the legacy payload shape");
166
+
167
+ const deltaMessages = await request("127.0.0.1", `/api/messages?since=2&tab=${encodeURIComponent(tabId)}`);
168
+ assert.equal(deltaMessages.status, 200);
169
+ assert.equal(deltaMessages.body?.data?.since, 2);
170
+ assert.equal(deltaMessages.body?.data?.totalCount, 3);
171
+ assert.equal((deltaMessages.body?.data?.messages || []).length, 1, "since=2 should return only the tail message");
172
+ assert.equal(deltaMessages.body?.data?.messages?.[0]?.content, "fake follow-up");
173
+
174
+ const clampedMessages = await request("127.0.0.1", `/api/messages?since=99&tab=${encodeURIComponent(tabId)}`);
175
+ assert.equal(clampedMessages.status, 200);
176
+ assert.equal(clampedMessages.body?.data?.since, 3, "since beyond the transcript should clamp to the total count");
177
+ assert.equal((clampedMessages.body?.data?.messages || []).length, 0);
178
+
82
179
  // Native slash command routed through the adapter (/copy → get_last_assistant_text).
83
180
  const copy = await request("127.0.0.1", "/api/prompt", {
84
181
  method: "POST",