@firstpick/pi-package-webui 0.3.3 → 0.3.5
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/bin/pi-webui.mjs +118 -0
- package/package.json +1 -1
- package/public/app.js +608 -3
- package/public/index.html +45 -2
- package/public/styles.css +307 -0
- package/tests/mobile-static.test.mjs +50 -1
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=36" />
|
|
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">
|
|
@@ -85,6 +85,28 @@
|
|
|
85
85
|
</section>
|
|
86
86
|
<form id="composer" class="composer">
|
|
87
87
|
<div class="composer-input-row">
|
|
88
|
+
<div class="composer-context-tags">
|
|
89
|
+
<button
|
|
90
|
+
id="busyPromptBehaviorTag"
|
|
91
|
+
class="composer-busy-mode-tag"
|
|
92
|
+
type="button"
|
|
93
|
+
aria-haspopup="menu"
|
|
94
|
+
aria-expanded="false"
|
|
95
|
+
aria-controls="busyPromptBehaviorMenu"
|
|
96
|
+
aria-live="polite"
|
|
97
|
+
>Follow-up</button>
|
|
98
|
+
<div id="sessionSkillTags" class="composer-skill-tags" aria-live="polite" hidden></div>
|
|
99
|
+
</div>
|
|
100
|
+
<div id="busyPromptBehaviorMenu" class="composer-busy-mode-menu" role="menu" aria-labelledby="busyPromptBehaviorTag" hidden>
|
|
101
|
+
<button class="composer-busy-mode-menu-item" type="button" role="menuitemradio" data-busy-prompt-behavior="followUp" aria-checked="true">
|
|
102
|
+
<span class="composer-busy-mode-menu-label">Follow-up</span>
|
|
103
|
+
<span class="composer-busy-mode-menu-description">Queue as the next prompt after this run.</span>
|
|
104
|
+
</button>
|
|
105
|
+
<button class="composer-busy-mode-menu-item" type="button" role="menuitemradio" data-busy-prompt-behavior="steer" aria-checked="false">
|
|
106
|
+
<span class="composer-busy-mode-menu-label">Steer</span>
|
|
107
|
+
<span class="composer-busy-mode-menu-description">Guide the active run immediately.</span>
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
88
110
|
<textarea id="promptInput" rows="1" enterkeyhint="enter" placeholder="Ask Pi…" autofocus></textarea>
|
|
89
111
|
<button
|
|
90
112
|
id="attachButton"
|
|
@@ -294,6 +316,14 @@
|
|
|
294
316
|
</span>
|
|
295
317
|
</label>
|
|
296
318
|
</div>
|
|
319
|
+
<div class="control-field terminal-tabs-layout-control-field">
|
|
320
|
+
<label for="terminalTabsLayoutSelect">Tabs layout</label>
|
|
321
|
+
<select id="terminalTabsLayoutSelect" title="Terminal tabs layout" aria-describedby="terminalTabsLayoutStatus">
|
|
322
|
+
<option value="top">Top bar</option>
|
|
323
|
+
<option value="left">Left sidebar</option>
|
|
324
|
+
</select>
|
|
325
|
+
<div id="terminalTabsLayoutStatus" class="terminal-tabs-layout-status toggle-control-hint">Top bar</div>
|
|
326
|
+
</div>
|
|
297
327
|
<div class="control-field">
|
|
298
328
|
<label for="themeSelect">Theme</label>
|
|
299
329
|
<select id="themeSelect" title="Theme"></select>
|
|
@@ -500,6 +530,19 @@
|
|
|
500
530
|
</form>
|
|
501
531
|
</dialog>
|
|
502
532
|
|
|
533
|
+
<dialog id="skillEditorDialog" class="extension-dialog skill-editor-dialog">
|
|
534
|
+
<form method="dialog">
|
|
535
|
+
<h2 id="skillEditorTitle">Edit skill</h2>
|
|
536
|
+
<p id="skillEditorMeta" class="skill-editor-meta muted"></p>
|
|
537
|
+
<textarea id="skillEditorText" class="dialog-editor skill-editor-text" spellcheck="false" aria-label="Skill file contents"></textarea>
|
|
538
|
+
<p id="skillEditorStatus" class="skill-editor-status muted" role="status" aria-live="polite" hidden></p>
|
|
539
|
+
<menu>
|
|
540
|
+
<button id="skillEditorCancelButton" type="button">Cancel</button>
|
|
541
|
+
<button id="skillEditorSaveButton" class="primary" type="button">Save skill</button>
|
|
542
|
+
</menu>
|
|
543
|
+
</form>
|
|
544
|
+
</dialog>
|
|
545
|
+
|
|
503
546
|
<dialog id="promptListDialog" class="extension-dialog prompt-list-dialog">
|
|
504
547
|
<form method="dialog">
|
|
505
548
|
<h2 id="promptListDialogTitle">Create prompt list</h2>
|
|
@@ -541,6 +584,6 @@
|
|
|
541
584
|
</form>
|
|
542
585
|
</dialog>
|
|
543
586
|
|
|
544
|
-
<script type="module" src="/app.js?v=
|
|
587
|
+
<script type="module" src="/app.js?v=37"></script>
|
|
545
588
|
</body>
|
|
546
589
|
</html>
|
package/public/styles.css
CHANGED
|
@@ -1494,6 +1494,102 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1494
1494
|
box-shadow: 0 0 1rem rgba(245, 194, 231, 0.20);
|
|
1495
1495
|
}
|
|
1496
1496
|
|
|
1497
|
+
@media (min-width: 721px) {
|
|
1498
|
+
body.terminal-tabs-left .chat-panel {
|
|
1499
|
+
display: grid;
|
|
1500
|
+
grid-template-columns: clamp(13rem, 18vw, 19rem) minmax(0, 1fr);
|
|
1501
|
+
grid-template-rows: auto minmax(0, 1fr) auto auto auto auto auto;
|
|
1502
|
+
align-items: stretch;
|
|
1503
|
+
}
|
|
1504
|
+
body.terminal-tabs-left .terminal-tabs-shell {
|
|
1505
|
+
grid-column: 1;
|
|
1506
|
+
grid-row: 1 / -1;
|
|
1507
|
+
flex-direction: column;
|
|
1508
|
+
align-items: stretch;
|
|
1509
|
+
gap: 0.58rem;
|
|
1510
|
+
min-width: 0;
|
|
1511
|
+
min-height: 0;
|
|
1512
|
+
padding: 0.76rem;
|
|
1513
|
+
border-right: 1px solid rgba(180, 190, 254, 0.16);
|
|
1514
|
+
border-bottom: 0;
|
|
1515
|
+
background:
|
|
1516
|
+
linear-gradient(180deg, rgba(var(--ctp-crust-rgb), 0.96), rgba(var(--ctp-base-rgb), 0.82), rgba(var(--ctp-mantle-rgb), 0.92)),
|
|
1517
|
+
radial-gradient(circle at 0% 0%, rgba(245, 194, 231, 0.12), transparent 18rem);
|
|
1518
|
+
box-shadow: inset -1px 0 0 rgba(255,255,255,0.035), 0.45rem 0 1rem rgba(var(--ctp-crust-rgb), 0.18);
|
|
1519
|
+
}
|
|
1520
|
+
body.terminal-tabs-left.side-panel-collapsed .terminal-tabs-shell {
|
|
1521
|
+
padding-right: 0.76rem;
|
|
1522
|
+
}
|
|
1523
|
+
body.terminal-tabs-left .terminal-tabs {
|
|
1524
|
+
flex: 1 1 auto;
|
|
1525
|
+
flex-direction: column;
|
|
1526
|
+
align-items: stretch;
|
|
1527
|
+
min-height: 0;
|
|
1528
|
+
padding-right: 0.08rem;
|
|
1529
|
+
overflow-x: hidden;
|
|
1530
|
+
overflow-y: auto;
|
|
1531
|
+
}
|
|
1532
|
+
body.terminal-tabs-left .terminal-tabs.terminal-tabs-dense {
|
|
1533
|
+
flex-wrap: nowrap;
|
|
1534
|
+
max-height: none;
|
|
1535
|
+
overflow-x: hidden;
|
|
1536
|
+
overflow-y: auto;
|
|
1537
|
+
}
|
|
1538
|
+
body.terminal-tabs-left .terminal-tab,
|
|
1539
|
+
body.terminal-tabs-left .terminal-tabs.terminal-tabs-dense .terminal-tab {
|
|
1540
|
+
flex: 0 0 auto;
|
|
1541
|
+
width: 100%;
|
|
1542
|
+
min-width: 0;
|
|
1543
|
+
max-width: none;
|
|
1544
|
+
}
|
|
1545
|
+
body.terminal-tabs-left .terminal-tab-group-menu {
|
|
1546
|
+
--terminal-left-dropdown-bridge: 0.78rem;
|
|
1547
|
+
inset: 0 auto auto 100%;
|
|
1548
|
+
width: clamp(13rem, 18vw, 20rem);
|
|
1549
|
+
min-width: 13rem;
|
|
1550
|
+
max-width: min(22rem, calc(100vw - 2rem));
|
|
1551
|
+
padding-top: 0;
|
|
1552
|
+
padding-left: var(--terminal-left-dropdown-bridge);
|
|
1553
|
+
}
|
|
1554
|
+
body.terminal-tabs-left .terminal-new-tab-menu.composer-publish-menu {
|
|
1555
|
+
width: 100%;
|
|
1556
|
+
}
|
|
1557
|
+
body.terminal-tabs-left .terminal-new-tab-button,
|
|
1558
|
+
body.terminal-tabs-left .terminal-close-all-button {
|
|
1559
|
+
width: 100%;
|
|
1560
|
+
justify-content: flex-start;
|
|
1561
|
+
text-align: left;
|
|
1562
|
+
}
|
|
1563
|
+
body.terminal-tabs-left .terminal-new-tab-menu .composer-publish-menu-panel {
|
|
1564
|
+
--terminal-left-dropdown-bridge: 0.78rem;
|
|
1565
|
+
inset: 0 auto auto 100%;
|
|
1566
|
+
width: clamp(12rem, 16vw, 18rem);
|
|
1567
|
+
min-width: 12rem;
|
|
1568
|
+
padding-top: 0;
|
|
1569
|
+
padding-left: var(--terminal-left-dropdown-bridge);
|
|
1570
|
+
}
|
|
1571
|
+
body.terminal-tabs-left .terminal-close-all-button {
|
|
1572
|
+
margin-top: auto;
|
|
1573
|
+
}
|
|
1574
|
+
body.terminal-tabs-left .widget-area,
|
|
1575
|
+
body.terminal-tabs-left .chat,
|
|
1576
|
+
body.terminal-tabs-left .feedback-tray,
|
|
1577
|
+
body.terminal-tabs-left .jump-to-latest-button,
|
|
1578
|
+
body.terminal-tabs-left .statusbar,
|
|
1579
|
+
body.terminal-tabs-left .git-workflow-panel,
|
|
1580
|
+
body.terminal-tabs-left .composer {
|
|
1581
|
+
grid-column: 2;
|
|
1582
|
+
min-width: 0;
|
|
1583
|
+
}
|
|
1584
|
+
body.terminal-tabs-left .widget-area { grid-row: 1; }
|
|
1585
|
+
body.terminal-tabs-left .chat { grid-row: 2; }
|
|
1586
|
+
body.terminal-tabs-left .feedback-tray { grid-row: 3; }
|
|
1587
|
+
body.terminal-tabs-left .jump-to-latest-button { grid-row: 4; }
|
|
1588
|
+
body.terminal-tabs-left .statusbar { grid-row: 5; }
|
|
1589
|
+
body.terminal-tabs-left .git-workflow-panel { grid-row: 6; }
|
|
1590
|
+
body.terminal-tabs-left .composer { grid-row: 7; }
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1497
1593
|
.widget-area {
|
|
1498
1594
|
flex: 0 0 auto;
|
|
1499
1595
|
border-bottom: 1px solid rgba(180, 190, 254, 0.16);
|
|
@@ -3233,11 +3329,182 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3233
3329
|
opacity: 0.8;
|
|
3234
3330
|
}
|
|
3235
3331
|
.composer-input-row {
|
|
3332
|
+
position: relative;
|
|
3236
3333
|
display: grid;
|
|
3237
3334
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
3238
3335
|
gap: 0.55rem;
|
|
3239
3336
|
align-items: stretch;
|
|
3240
3337
|
}
|
|
3338
|
+
.composer-context-tags {
|
|
3339
|
+
position: absolute;
|
|
3340
|
+
top: -0.48rem;
|
|
3341
|
+
left: 0.75rem;
|
|
3342
|
+
z-index: 3;
|
|
3343
|
+
display: inline-flex;
|
|
3344
|
+
align-items: center;
|
|
3345
|
+
gap: 0.32rem;
|
|
3346
|
+
max-width: calc(100% - 4.5rem);
|
|
3347
|
+
}
|
|
3348
|
+
.composer-busy-mode-tag {
|
|
3349
|
+
position: relative;
|
|
3350
|
+
flex: 0 1 auto;
|
|
3351
|
+
display: inline-flex;
|
|
3352
|
+
align-items: center;
|
|
3353
|
+
max-width: min(12rem, 100%);
|
|
3354
|
+
overflow: hidden;
|
|
3355
|
+
min-width: 0;
|
|
3356
|
+
min-height: 0;
|
|
3357
|
+
margin: 0;
|
|
3358
|
+
padding: 0.14rem 0.52rem;
|
|
3359
|
+
border: 1px solid rgba(137, 180, 250, 0.34);
|
|
3360
|
+
border-radius: 999px;
|
|
3361
|
+
color: var(--ctp-blue);
|
|
3362
|
+
background:
|
|
3363
|
+
linear-gradient(120deg, rgba(137, 180, 250, 0.24), rgba(137, 180, 250, 0.08)),
|
|
3364
|
+
var(--ctp-crust);
|
|
3365
|
+
box-shadow: 0 0.42rem 1rem rgba(var(--ctp-crust-rgb), 0.42), 0 0 0.8rem rgba(137, 180, 250, 0.14);
|
|
3366
|
+
cursor: pointer;
|
|
3367
|
+
font-size: 0.62rem;
|
|
3368
|
+
font-weight: 900;
|
|
3369
|
+
letter-spacing: 0.08em;
|
|
3370
|
+
line-height: 1.1;
|
|
3371
|
+
text-overflow: ellipsis;
|
|
3372
|
+
text-transform: uppercase;
|
|
3373
|
+
white-space: nowrap;
|
|
3374
|
+
}
|
|
3375
|
+
.composer-busy-mode-tag:hover,
|
|
3376
|
+
.composer-busy-mode-tag:focus-visible,
|
|
3377
|
+
.composer-busy-mode-tag.menu-open {
|
|
3378
|
+
border-color: rgba(137, 180, 250, 0.62);
|
|
3379
|
+
box-shadow: 0 0.42rem 1rem rgba(var(--ctp-crust-rgb), 0.42), 0 0 1rem rgba(137, 180, 250, 0.24);
|
|
3380
|
+
transform: translateY(-1px);
|
|
3381
|
+
}
|
|
3382
|
+
.composer-busy-mode-tag.steer {
|
|
3383
|
+
color: var(--ctp-mauve);
|
|
3384
|
+
border-color: rgba(203, 166, 247, 0.38);
|
|
3385
|
+
background:
|
|
3386
|
+
linear-gradient(120deg, rgba(203, 166, 247, 0.26), rgba(203, 166, 247, 0.08)),
|
|
3387
|
+
var(--ctp-crust);
|
|
3388
|
+
box-shadow: 0 0.42rem 1rem rgba(var(--ctp-crust-rgb), 0.42), 0 0 0.8rem rgba(203, 166, 247, 0.16);
|
|
3389
|
+
}
|
|
3390
|
+
.composer-busy-mode-tag.steer:hover,
|
|
3391
|
+
.composer-busy-mode-tag.steer:focus-visible,
|
|
3392
|
+
.composer-busy-mode-tag.steer.menu-open {
|
|
3393
|
+
border-color: rgba(203, 166, 247, 0.66);
|
|
3394
|
+
box-shadow: 0 0.42rem 1rem rgba(var(--ctp-crust-rgb), 0.42), 0 0 1rem rgba(203, 166, 247, 0.26);
|
|
3395
|
+
}
|
|
3396
|
+
.composer-busy-mode-tag.follow-up {
|
|
3397
|
+
color: var(--ctp-blue);
|
|
3398
|
+
border-color: rgba(137, 180, 250, 0.38);
|
|
3399
|
+
}
|
|
3400
|
+
.composer-skill-tags {
|
|
3401
|
+
display: inline-flex;
|
|
3402
|
+
align-items: center;
|
|
3403
|
+
gap: 0.28rem;
|
|
3404
|
+
min-width: 0;
|
|
3405
|
+
overflow: hidden;
|
|
3406
|
+
}
|
|
3407
|
+
.composer-skill-tags[hidden] {
|
|
3408
|
+
display: none !important;
|
|
3409
|
+
}
|
|
3410
|
+
.composer-skill-tag {
|
|
3411
|
+
display: inline-flex;
|
|
3412
|
+
align-items: center;
|
|
3413
|
+
min-width: 0;
|
|
3414
|
+
min-height: 0;
|
|
3415
|
+
max-width: 9.4rem;
|
|
3416
|
+
overflow: hidden;
|
|
3417
|
+
padding: 0.14rem 0.44rem;
|
|
3418
|
+
border: 1px solid rgba(249, 226, 175, 0.34);
|
|
3419
|
+
border-radius: 999px;
|
|
3420
|
+
color: var(--ctp-yellow);
|
|
3421
|
+
background:
|
|
3422
|
+
linear-gradient(120deg, rgba(249, 226, 175, 0.24), rgba(166, 227, 161, 0.08)),
|
|
3423
|
+
var(--ctp-crust);
|
|
3424
|
+
box-shadow: 0 0.35rem 0.9rem rgba(var(--ctp-crust-rgb), 0.40), 0 0 0.6rem rgba(249, 226, 175, 0.14);
|
|
3425
|
+
font-size: 0.58rem;
|
|
3426
|
+
font-weight: 900;
|
|
3427
|
+
letter-spacing: 0.06em;
|
|
3428
|
+
line-height: 1.1;
|
|
3429
|
+
text-align: left;
|
|
3430
|
+
text-overflow: ellipsis;
|
|
3431
|
+
text-transform: uppercase;
|
|
3432
|
+
white-space: nowrap;
|
|
3433
|
+
}
|
|
3434
|
+
button.composer-skill-tag:hover,
|
|
3435
|
+
button.composer-skill-tag:focus-visible {
|
|
3436
|
+
border-color: rgba(148, 226, 213, 0.68);
|
|
3437
|
+
box-shadow: 0 0.35rem 0.9rem rgba(var(--ctp-crust-rgb), 0.40), 0 0 0.9rem rgba(148, 226, 213, 0.24);
|
|
3438
|
+
transform: translateY(-1px);
|
|
3439
|
+
}
|
|
3440
|
+
.composer-skill-tag.read {
|
|
3441
|
+
color: var(--ctp-teal);
|
|
3442
|
+
border-color: rgba(148, 226, 213, 0.36);
|
|
3443
|
+
}
|
|
3444
|
+
.composer-skill-tag.loaded {
|
|
3445
|
+
color: var(--ctp-green);
|
|
3446
|
+
border-color: rgba(166, 227, 161, 0.36);
|
|
3447
|
+
}
|
|
3448
|
+
.composer-skill-tag.overflow {
|
|
3449
|
+
color: var(--ctp-subtext0);
|
|
3450
|
+
border-color: rgba(166, 173, 200, 0.30);
|
|
3451
|
+
background: linear-gradient(120deg, rgba(166, 173, 200, 0.22), rgba(166, 173, 200, 0.08)), var(--ctp-crust);
|
|
3452
|
+
}
|
|
3453
|
+
.composer-busy-mode-menu {
|
|
3454
|
+
position: absolute;
|
|
3455
|
+
top: auto;
|
|
3456
|
+
bottom: calc(100% + 0.22rem);
|
|
3457
|
+
left: 0.75rem;
|
|
3458
|
+
z-index: 120;
|
|
3459
|
+
display: flex;
|
|
3460
|
+
flex-direction: column;
|
|
3461
|
+
gap: 0.34rem;
|
|
3462
|
+
width: min(18rem, calc(100% - 4.5rem));
|
|
3463
|
+
padding: 0.42rem;
|
|
3464
|
+
border: 1px solid rgba(137, 180, 250, 0.32);
|
|
3465
|
+
border-radius: 0.95rem;
|
|
3466
|
+
background: var(--ctp-crust);
|
|
3467
|
+
box-shadow: 0 0.9rem 2.2rem rgba(var(--ctp-crust-rgb), 0.72), 0 0 1rem rgba(137, 180, 250, 0.14), inset 0 1px 0 rgba(255,255,255,0.05);
|
|
3468
|
+
}
|
|
3469
|
+
.composer-busy-mode-menu[hidden] {
|
|
3470
|
+
display: none !important;
|
|
3471
|
+
}
|
|
3472
|
+
.composer-busy-mode-menu-item {
|
|
3473
|
+
display: grid;
|
|
3474
|
+
gap: 0.1rem;
|
|
3475
|
+
width: 100%;
|
|
3476
|
+
margin: 0;
|
|
3477
|
+
padding: 0.45rem 0.58rem;
|
|
3478
|
+
border-color: rgba(137, 180, 250, 0.24);
|
|
3479
|
+
border-radius: 0.72rem;
|
|
3480
|
+
color: var(--ctp-text);
|
|
3481
|
+
background:
|
|
3482
|
+
linear-gradient(120deg, rgba(137, 180, 250, 0.08), rgba(203, 166, 247, 0.06)),
|
|
3483
|
+
var(--ctp-mantle);
|
|
3484
|
+
text-align: left;
|
|
3485
|
+
}
|
|
3486
|
+
.composer-busy-mode-menu-item:hover,
|
|
3487
|
+
.composer-busy-mode-menu-item:focus-visible {
|
|
3488
|
+
border-color: rgba(148, 226, 213, 0.48);
|
|
3489
|
+
box-shadow: 0 0 0.8rem rgba(148, 226, 213, 0.14);
|
|
3490
|
+
transform: translateY(-1px);
|
|
3491
|
+
}
|
|
3492
|
+
.composer-busy-mode-menu-item[aria-checked="true"] {
|
|
3493
|
+
border-color: rgba(148, 226, 213, 0.52);
|
|
3494
|
+
background:
|
|
3495
|
+
linear-gradient(120deg, rgba(148, 226, 213, 0.18), rgba(137, 180, 250, 0.10)),
|
|
3496
|
+
var(--ctp-mantle);
|
|
3497
|
+
}
|
|
3498
|
+
.composer-busy-mode-menu-label {
|
|
3499
|
+
font-size: 0.76rem;
|
|
3500
|
+
font-weight: 900;
|
|
3501
|
+
letter-spacing: 0.04em;
|
|
3502
|
+
}
|
|
3503
|
+
.composer-busy-mode-menu-description {
|
|
3504
|
+
color: var(--muted);
|
|
3505
|
+
font-size: 0.68rem;
|
|
3506
|
+
line-height: 1.25;
|
|
3507
|
+
}
|
|
3241
3508
|
.composer-icon-button {
|
|
3242
3509
|
display: inline-flex;
|
|
3243
3510
|
align-items: center;
|
|
@@ -4033,6 +4300,46 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
4033
4300
|
font-size: 0.72rem;
|
|
4034
4301
|
font-weight: 800;
|
|
4035
4302
|
}
|
|
4303
|
+
.extension-dialog.skill-editor-dialog {
|
|
4304
|
+
--skill-editor-size: min(152rem, calc(100vw - 1.5rem), calc(var(--visual-viewport-height, 100dvh) - 1.5rem));
|
|
4305
|
+
width: var(--skill-editor-size);
|
|
4306
|
+
height: var(--skill-editor-size);
|
|
4307
|
+
max-width: calc(100vw - 1.5rem);
|
|
4308
|
+
max-height: calc(var(--visual-viewport-height, 100dvh) - 1.5rem);
|
|
4309
|
+
aspect-ratio: 1 / 1;
|
|
4310
|
+
}
|
|
4311
|
+
.skill-editor-dialog form {
|
|
4312
|
+
display: grid;
|
|
4313
|
+
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
|
|
4314
|
+
gap: 0.78rem;
|
|
4315
|
+
height: 100%;
|
|
4316
|
+
min-height: 0;
|
|
4317
|
+
}
|
|
4318
|
+
.skill-editor-meta {
|
|
4319
|
+
margin: 0;
|
|
4320
|
+
overflow-wrap: anywhere;
|
|
4321
|
+
font-size: 0.82rem;
|
|
4322
|
+
}
|
|
4323
|
+
.skill-editor-text {
|
|
4324
|
+
min-height: 0;
|
|
4325
|
+
resize: none;
|
|
4326
|
+
overflow: auto;
|
|
4327
|
+
overflow-x: hidden;
|
|
4328
|
+
overflow-wrap: anywhere;
|
|
4329
|
+
word-break: break-word;
|
|
4330
|
+
font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
|
|
4331
|
+
font-size: 0.82rem;
|
|
4332
|
+
line-height: 1.45;
|
|
4333
|
+
white-space: pre-wrap;
|
|
4334
|
+
}
|
|
4335
|
+
.skill-editor-status {
|
|
4336
|
+
margin: 0;
|
|
4337
|
+
min-height: 1.2rem;
|
|
4338
|
+
font-size: 0.84rem;
|
|
4339
|
+
}
|
|
4340
|
+
.skill-editor-status.ok { color: var(--ctp-green); }
|
|
4341
|
+
.skill-editor-status.warn { color: var(--ctp-yellow); }
|
|
4342
|
+
.skill-editor-status.error { color: var(--ctp-red); }
|
|
4036
4343
|
.prompt-list-dialog {
|
|
4037
4344
|
width: min(58rem, calc(100vw - 2rem));
|
|
4038
4345
|
}
|
|
@@ -61,6 +61,8 @@ assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expos
|
|
|
61
61
|
assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
|
|
62
62
|
assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
|
|
63
63
|
assert.match(html, /id="thinkingVisibilityStatus"/, "thinking-output visibility toggle should expose status text");
|
|
64
|
+
assert.match(html, /id="terminalTabsLayoutSelect"[\s\S]*<option value="left">Left sidebar<\/option>/, "side panel controls should expose a terminal-tabs layout selector");
|
|
65
|
+
assert.match(html, /id="terminalTabsLayoutStatus"/, "terminal-tabs layout selector should expose status text");
|
|
64
66
|
assert.match(html, /id="nativeCommandDialog"/, "native slash selector UI should have a dedicated dialog");
|
|
65
67
|
assert.match(html, /id="nativeCommandSearch"[^>]*type="search"/, "native slash selector dialog should expose a filter box");
|
|
66
68
|
assert.match(html, /id="pathPickerCreateNameInput"[^>]*placeholder="New directory name"/, "cwd picker should expose a new-directory name input");
|
|
@@ -104,6 +106,11 @@ assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed la
|
|
|
104
106
|
assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
|
|
105
107
|
assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submittable after the agent finishes");
|
|
106
108
|
assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
|
|
109
|
+
assert.match(html, /id="busyPromptBehaviorTag"[\s\S]*class="composer-busy-mode-tag"[\s\S]*aria-controls="busyPromptBehaviorMenu"/, "composer should expose a clickable busy prompt behavior tag on the input frame");
|
|
110
|
+
assert.doesNotMatch(html, /Busy send:/i, "busy prompt behavior tag should show only the current mode label");
|
|
111
|
+
assert.match(html, /id="sessionSkillTags" class="composer-skill-tags"[\s\S]*hidden/, "composer should expose a hidden-until-used skill tag strip beside the busy mode tag");
|
|
112
|
+
assert.match(html, /id="skillEditorDialog"[\s\S]*id="skillEditorText"[\s\S]*id="skillEditorSaveButton"/, "skill tags should have an in-Web UI SKILL.md editing dialog");
|
|
113
|
+
assert.match(html, /id="busyPromptBehaviorMenu"[\s\S]*data-busy-prompt-behavior="followUp"[\s\S]*data-busy-prompt-behavior="steer"/, "busy prompt behavior dropdown should expose follow-up and steer choices");
|
|
107
114
|
assert.match(app, /const LONG_INPUT_ATTACHMENT_LINE_THRESHOLD = 20/, "long composer text should use a 20-line threshold before becoming an attachment");
|
|
108
115
|
assert.match(app, /function attachLongTextAsFile\(text, source = "input text"\)/, "long composer text should be attachable as a generated text file");
|
|
109
116
|
assert.match(app, /function handleAttachmentPaste\(event\)[\s\S]*attachLongTextAsFile\(text, "clipboard text"\)/, "long pasted text should be attached instead of inserted into the prompt textarea");
|
|
@@ -171,6 +178,14 @@ assert.match(css, /\.composer-abort-button\.long-pressing::after[\s\S]*?animatio
|
|
|
171
178
|
assert.match(css, /body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-abort-button:not\(\[hidden\]\) \{ grid-column: span 2; \}/, "active mobile runs should keep Abort beside Send in the bottom controls");
|
|
172
179
|
assert.match(css, /#promptInput \{[\s\S]*?min-height:\s*calc\(1\.5em \+ 1\.8rem\)/, "prompt input should default to a compact single-line height");
|
|
173
180
|
assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input should be JS-resized instead of showing a scrollbar by default");
|
|
181
|
+
assert.match(css, /\.composer-context-tags \{[\s\S]*?top:\s*-0\.48rem;[\s\S]*?left:\s*0\.75rem;/, "busy prompt behavior and skill tags should sit at the top-left of the input frame");
|
|
182
|
+
assert.match(css, /\.composer-busy-mode-tag \{[\s\S]*?var\(--ctp-crust\)/, "busy prompt behavior tag should use an opaque base background");
|
|
183
|
+
assert.match(css, /\.composer-skill-tag \{[\s\S]*?var\(--ctp-crust\)/, "skill tags should use an opaque base background");
|
|
184
|
+
assert.match(css, /button\.composer-skill-tag:hover,[\s\S]*?button\.composer-skill-tag:focus-visible/, "skill tags should be styled as clickable controls");
|
|
185
|
+
assert.match(css, /\.extension-dialog\.skill-editor-dialog \{[\s\S]*?--skill-editor-size:\s*min\(152rem[\s\S]*?width:\s*var\(--skill-editor-size\);[\s\S]*?height:\s*var\(--skill-editor-size\);[\s\S]*?aspect-ratio:\s*1 \/ 1/, "skill editor should use a square viewport-bounded modal layout");
|
|
186
|
+
assert.match(css, /\.skill-editor-dialog form \{[\s\S]*?height:\s*100%;[\s\S]*?min-height:\s*0/, "skill editor form should fill the square modal without forcing overflow");
|
|
187
|
+
assert.match(css, /\.skill-editor-text \{[\s\S]*?overflow-x:\s*hidden;[\s\S]*?overflow-wrap:\s*anywhere;[\s\S]*?white-space:\s*pre-wrap/, "skill editor text should wrap long lines instead of horizontal scrolling");
|
|
188
|
+
assert.match(css, /\.composer-busy-mode-menu \{[\s\S]*?bottom:\s*calc\(100% \+ 0\.22rem\);[\s\S]*?background:\s*var\(--ctp-crust\)/, "busy prompt behavior dropdown should expand above the tag with an opaque background");
|
|
174
189
|
assert.match(css, /\.sticky-user-prompt-button \{[\s\S]*?grid-template-columns:\s*auto minmax\(0, 1fr\) auto/, "last-user-prompt jump control should render as a fixed transcript header");
|
|
175
190
|
assert.match(css, /\.message\.extension,[\s\S]*?\.message\.native/, "extension and native command output should have visible transcript styling");
|
|
176
191
|
assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
|
|
@@ -253,6 +268,11 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
|
|
|
253
268
|
assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
|
|
254
269
|
assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
|
|
255
270
|
assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
|
|
271
|
+
assert.match(css, /body\.terminal-tabs-left \.chat-panel \{[\s\S]*?grid-template-columns:\s*clamp\(13rem, 18vw, 19rem\) minmax\(0, 1fr\)/, "terminal tabs left layout should split the chat panel into a sidebar and transcript area");
|
|
272
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-tabs-shell \{[\s\S]*?grid-column:\s*1;[\s\S]*?grid-row:\s*1 \/ -1;[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should turn the top tab strip into a vertical sidebar");
|
|
273
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-tabs \{[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should stack tabs vertically");
|
|
274
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-tab-group-menu \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar grouped tab menus should include a hover bridge so they do not vanish between button and dropdown");
|
|
275
|
+
assert.match(css, /body\.terminal-tabs-left \.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar new-tab dropdown should include a hover bridge so it does not vanish between button and dropdown");
|
|
256
276
|
assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*100% 0 auto auto;[\s\S]*?padding-top:\s*0\.38rem/, "new-tab dropdown should reuse the shared composer panel and open below the tab bar");
|
|
257
277
|
assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-item \{[\s\S]*?color:\s*var\(--ctp-pink\)/, "new-tab dropdown items should reuse shared composer menu items with a tab-specific color");
|
|
258
278
|
assert.match(css, /\.terminal-close-all-button \{[\s\S]*?color:\s*var\(--ctp-red\)/, "close-all tabs action should render as a top-right destructive tab action");
|
|
@@ -334,6 +354,9 @@ assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backg
|
|
|
334
354
|
assert.match(app, /const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background"/, "custom backgrounds should prefer IndexedDB persistence for large images");
|
|
335
355
|
assert.match(app, /const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed"/, "side-panel section collapse state should be persisted in browser storage");
|
|
336
356
|
assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
|
|
357
|
+
assert.match(app, /const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout"/, "terminal-tabs layout preference should be persisted in browser storage");
|
|
358
|
+
assert.match(app, /document\.body\.classList\.toggle\("terminal-tabs-left", next === "left"\)/, "terminal-tabs layout should toggle a body class for CSS layout");
|
|
359
|
+
assert.match(app, /terminalTabsLayoutSelect\.addEventListener\("change"/, "terminal-tabs layout selector should update the browser layout immediately");
|
|
337
360
|
assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
|
|
338
361
|
assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
|
|
339
362
|
assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
|
|
@@ -386,7 +409,7 @@ assert.match(app, /function syncMobileChatToBottomForInput\(\)/, "mobile input f
|
|
|
386
409
|
assert.match(app, /function focusPromptInput\(\{ defer = false \} = \{\}\)/, "frontend should focus the prompt composer programmatically after tab/app startup");
|
|
387
410
|
assert.match(app, /async function switchTab\(tabId\)[\s\S]*?restoreActiveDraft\(\);\n\s+focusPromptInput\(\{ defer: true \}\);/, "switching to a newly opened tab should focus the prompt input immediately");
|
|
388
411
|
assert.match(app, /async function initializeTabs\(\)[\s\S]*?restoreActiveDraft\(\);[\s\S]*if \(!loadedTabs\.length\)[\s\S]*focusPromptInput\(\{ defer: true \}\);/, "starting the Web UI should prompt for cwd when needed and focus active tabs");
|
|
389
|
-
assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
|
|
412
|
+
assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nrestoreStoredSkillUsage\(\);\nrestoreBusyPromptBehaviorSetting\(\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus and restore skill tags before waiting for tab state refreshes");
|
|
390
413
|
assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
|
|
391
414
|
assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/, "PWA service worker should be registered by the app");
|
|
392
415
|
assert.match(app, /function serverStartCommandText\(\)[\s\S]*return `pi-webui\$\{currentPortArg\(\)\}`/, "PWA/offline shell should build a pathless pi-webui recovery command");
|
|
@@ -673,9 +696,29 @@ assert.match(app, /function updateComposerModeButtons\(\)/, "composer should rel
|
|
|
673
696
|
assert.match(app, /const target = runActive \? elements\.composerRow : elements\.composerActionsPanel/, "Steer and Follow-up should move into the bottom row only while an agent run is active");
|
|
674
697
|
assert.match(app, /const before = runActive \? elements\.abortButton : null/, "active Steer and Follow-up controls should sit before Abort and Send");
|
|
675
698
|
assert.match(app, /button\.hidden = !runActive;\n\s+button\.disabled = !runActive;/, "Steer and Follow-up should be hidden and disabled when the agent is not running");
|
|
699
|
+
assert.match(app, /renderBusyPromptBehaviorTag\(\);\n\s+document\.body\.classList\.toggle\("pi-run-active", runActive \|\| abortAvailable\)/, "composer mode refresh should keep the busy prompt behavior tag current");
|
|
676
700
|
assert.match(app, /elements\.abortButton\.hidden = !abortAvailable;\n\s+elements\.abortButton\.disabled = !abortAvailable \|\| abortRequestInFlight;/, "Abort should only be exposed in the bottom bar while a run can be aborted");
|
|
677
701
|
assert.match(app, /document\.body\.classList\.toggle\("pi-run-active", runActive \|\| abortAvailable\)/, "run-active or abort-available state should be reflected in CSS for mobile composer layout");
|
|
678
702
|
assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-button taps should show the usage tooltip");
|
|
703
|
+
assert.match(app, /function renderBusyPromptBehaviorTag\(\)[\s\S]*?tag\.textContent = label/, "busy prompt behavior tag should render only the current follow-up\/steer setting");
|
|
704
|
+
assert.doesNotMatch(app, /Busy send: \$\{label\}/, "busy prompt behavior tag should not prefix the current mode label");
|
|
705
|
+
assert.match(app, /function renderSessionSkillTags\(tabId = activeTabId\)[\s\S]*?filter\(\(entry\) => entry\.kinds\.has\("read"\)\)[\s\S]*?make\("button", classes\.join\(" "\), entry\.name\)[\s\S]*?openSkillEditor\(entry\)/, "skill tags should render as clickable buttons only after the full skill context was read");
|
|
706
|
+
assert.ok(app.includes('normalized.match(/\\/skills\\/([^/]+)\\/SKILL\\.md$/i)'), "skill context tracking should require SKILL.md paths");
|
|
707
|
+
assert.match(app, /function trackSkillsFromToolInvocation\(tabId, toolName[\s\S]*?name\.toLowerCase\(\) !== "read"\) return;[\s\S]*?kind: "read"/, "skill context tracking should only follow read-tool invocations");
|
|
708
|
+
assert.match(app, /function trackSkillUsage\(tabId, skillName[\s\S]*?persistSkillUsage\(\);[\s\S]*?renderSessionSkillTags\(tabId\)/, "skill tags should persist and live-update when a read skill is tracked");
|
|
709
|
+
assert.match(app, /const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1"/, "read skill tags should have browser storage for hard-refresh and restart restore");
|
|
710
|
+
assert.match(app, /function persistSkillUsage\(\)[\s\S]*?localStorage\.setItem\(SKILL_USAGE_STORAGE_KEY/, "read skill tags should be persisted to browser storage");
|
|
711
|
+
assert.match(app, /function restoreStoredSkillUsage\(\)[\s\S]*?localStorage\.getItem\(SKILL_USAGE_STORAGE_KEY/, "read skill tags should restore from browser storage");
|
|
712
|
+
assert.match(app, /restoreStoredSkillUsage\(\);[\s\S]*?initializeTabs\(\)/, "stored read skill tags should be restored before tabs initialize");
|
|
713
|
+
assert.match(app, /trackSkillsFromEvent\(event\);[\s\S]*?if \(!eventTargetsActiveTab\(event\)\)/, "skill usage should be tracked as soon as tab events arrive");
|
|
714
|
+
assert.doesNotMatch(app, /trackSkillsFromCommands\(rawAvailableCommands, tabContext\.tabId\)/, "loaded skill commands alone should not populate skill tags");
|
|
715
|
+
assert.match(app, /function openSkillEditor\(entry\)[\s\S]*?api\(skillEditorApiPath\(\{ name, path \}\), \{ tabId \}\)/, "clicking a skill tag should load the corresponding SKILL.md into the editor dialog");
|
|
716
|
+
assert.match(app, /function saveSkillEditor\(\)[\s\S]*?api\("\/api\/skill-file", \{[\s\S]*?method: "POST"[\s\S]*?content: elements\.skillEditorText\.value/, "skill editor should save changed SKILL.md contents through the API");
|
|
717
|
+
assert.match(app, /skillEditorDialog\?\.addEventListener\("keydown"[\s\S]*?saveSkillEditor\(\)/, "skill editor should support Ctrl\/Cmd+S saving");
|
|
718
|
+
assert.match(app, /function setBusyPromptBehaviorMenuOpen\(open,[\s\S]*aria-expanded[\s\S]*busyPromptBehaviorMenu\.hidden/, "busy prompt behavior tag should control a dropdown menu");
|
|
719
|
+
assert.match(app, /busyPromptBehaviorTag\?\.addEventListener\("click"[\s\S]*setBusyPromptBehaviorMenuOpen\(nextOpen\)/, "clicking the busy prompt behavior tag should toggle its dropdown");
|
|
720
|
+
assert.match(app, /busyPromptBehaviorMenu\?\.addEventListener\("click"[\s\S]*chooseBusyPromptBehaviorFromMenu/, "busy prompt behavior dropdown choices should update the setting");
|
|
721
|
+
assert.match(app, /setBusyPromptBehavior\(controls\.busyBehavior\.select\.value\)/, "native settings should update the busy prompt behavior tag immediately");
|
|
679
722
|
assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
|
|
680
723
|
assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
|
|
681
724
|
assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*?sendPrompt\("prompt", command\)/, "Publish workflows should send slash commands directly without replacing the draft");
|
|
@@ -959,6 +1002,12 @@ assert.match(server, /if \(webuiDevServer\) return installRoot/, "source-checkou
|
|
|
959
1002
|
assert.match(server, /Could not determine a safe optional feature install root/, "optional feature installs should fail closed when no declared package root can be found");
|
|
960
1003
|
assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
|
|
961
1004
|
assert.match(server, /Installing optional Web UI features is only allowed from localhost/, "optional feature install endpoint should be localhost-only");
|
|
1005
|
+
assert.match(server, /url\.pathname === "\/api\/skill-file" && req\.method === "GET"[\s\S]*?getSkillFileData/, "server should expose GET /api/skill-file for editable skill content");
|
|
1006
|
+
assert.match(server, /url\.pathname === "\/api\/skill-file" && req\.method === "POST"[\s\S]*?Saving skill files is only allowed from localhost[\s\S]*?saveSkillFileData/, "server should expose localhost-only POST /api/skill-file for saving skill content");
|
|
1007
|
+
assert.match(server, /function resolveEditableSkillFile\(tab, request = \{\}\)[\s\S]*?path\.basename\(skill\.filePath\) !== "SKILL\.md"/, "skill file API should validate that edits target resolved SKILL.md resources");
|
|
1008
|
+
assert.match(server, /function resolveExplicitSkillFilePath\(tab, filePath, requestedName = ""\)[\s\S]*?Skill path must point to \/skills\/<name>\/SKILL\.md[\s\S]*?allowedRoots/, "skill file API should allow exact read SKILL.md paths from trusted Pi skill roots");
|
|
1009
|
+
assert.match(server, /Skill path is outside allowed Pi skill locations/, "explicit skill path fallback should reject paths outside Pi skill roots");
|
|
1010
|
+
assert.match(server, /writeFile\(tmpFile, body\.content[\s\S]*?rename\(tmpFile, skill\.filePath\)/, "skill file saves should use an atomic temp-file rename");
|
|
962
1011
|
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
963
1012
|
assert.match(server, /readBundledThemes\(\)/, "server should read bundled theme JSON files for the browser");
|
|
964
1013
|
assert.match(server, /"apple-touch-icon\.png", "icon-192\.png"/, "server should serve the conventional apple touch icon path");
|