@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/README.md +9 -7
- package/bin/pi-webui.mjs +690 -35
- package/package.json +11 -22
- package/public/app.js +1832 -66
- package/public/index.html +41 -3
- package/public/service-worker.js +1 -1
- package/public/styles.css +415 -0
- package/tests/fixtures/fake-pi.mjs +10 -1
- package/tests/http-endpoints-harness.test.mjs +99 -2
- package/tests/mobile-static.test.mjs +79 -23
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=
|
|
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=
|
|
657
|
+
<script type="module" src="/app.js?v=50"></script>
|
|
620
658
|
</body>
|
|
621
659
|
</html>
|
package/public/service-worker.js
CHANGED
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({
|
|
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",
|