@firstpick/pi-package-webui 0.4.5 → 0.4.7
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 +153 -23
- package/package.json +10 -3
- package/public/app.js +469 -45
- package/public/index.html +4 -1
- package/public/styles.css +66 -0
- package/tests/fixtures/fake-pi.mjs +0 -0
- package/tests/http-endpoints-harness.test.mjs +48 -0
- package/tests/mobile-static.test.mjs +14 -3
package/public/index.html
CHANGED
|
@@ -539,7 +539,10 @@
|
|
|
539
539
|
<h2 id="gitChangesTitle">Uncommitted Changes</h2>
|
|
540
540
|
<p id="gitChangesSubtitle" class="muted">Current tab git diff</p>
|
|
541
541
|
</div>
|
|
542
|
-
<
|
|
542
|
+
<div class="git-changes-actions">
|
|
543
|
+
<button id="gitChangesRefreshButton" type="button">Refresh</button>
|
|
544
|
+
<button id="gitChangesPullButton" class="primary" type="button" disabled>Pull</button>
|
|
545
|
+
</div>
|
|
543
546
|
</div>
|
|
544
547
|
<p id="gitChangesStatus" class="git-changes-status muted" role="status" aria-live="polite"></p>
|
|
545
548
|
<div id="gitChangesBody" class="git-changes-body"></div>
|
package/public/styles.css
CHANGED
|
@@ -1307,6 +1307,24 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1307
1307
|
isolation: isolate;
|
|
1308
1308
|
background: var(--ctp-crust);
|
|
1309
1309
|
}
|
|
1310
|
+
.terminal-tab[draggable="true"],
|
|
1311
|
+
.terminal-tab-group-item[draggable="true"] {
|
|
1312
|
+
cursor: grab;
|
|
1313
|
+
}
|
|
1314
|
+
.terminal-tab.terminal-tab-dragging,
|
|
1315
|
+
.terminal-tab-group-item.terminal-tab-dragging {
|
|
1316
|
+
cursor: grabbing;
|
|
1317
|
+
opacity: 0.68;
|
|
1318
|
+
transform: scale(0.985);
|
|
1319
|
+
}
|
|
1320
|
+
.terminal-tab-custom-group {
|
|
1321
|
+
border-color: rgba(203, 166, 247, 0.34);
|
|
1322
|
+
}
|
|
1323
|
+
.terminal-tab-custom-group .terminal-tab-group-count {
|
|
1324
|
+
color: var(--ctp-mauve);
|
|
1325
|
+
border-color: rgba(203, 166, 247, 0.30);
|
|
1326
|
+
background: rgba(203, 166, 247, 0.12);
|
|
1327
|
+
}
|
|
1310
1328
|
.terminal-tab-group:hover,
|
|
1311
1329
|
.terminal-tab-group:focus-within {
|
|
1312
1330
|
z-index: 100;
|
|
@@ -1432,6 +1450,15 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1432
1450
|
border-color: rgba(166, 227, 161, 0.52);
|
|
1433
1451
|
box-shadow: 0 0 1rem rgba(166, 227, 161, 0.16), inset 0 1px 0 rgba(255,255,255,0.045);
|
|
1434
1452
|
}
|
|
1453
|
+
.terminal-tab.terminal-tab-drag-over,
|
|
1454
|
+
.terminal-tab-group-item.terminal-tab-drag-over {
|
|
1455
|
+
border-color: rgba(137, 180, 250, 0.82);
|
|
1456
|
+
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.34), 0 0 1.2rem rgba(137, 180, 250, 0.28), inset 0 1px 0 rgba(255,255,255,0.06);
|
|
1457
|
+
}
|
|
1458
|
+
.terminal-tab.terminal-tab-drag-over > .terminal-tab-button,
|
|
1459
|
+
.terminal-tab-group-item.terminal-tab-drag-over > .terminal-tab-button {
|
|
1460
|
+
background: linear-gradient(120deg, rgba(137, 180, 250, 0.18), rgba(203, 166, 247, 0.12));
|
|
1461
|
+
}
|
|
1435
1462
|
.terminal-tab-button,
|
|
1436
1463
|
.terminal-tab-close,
|
|
1437
1464
|
.terminal-new-tab-button {
|
|
@@ -2267,6 +2294,14 @@ button.footer-meta {
|
|
|
2267
2294
|
.git-changes-header > div:first-child {
|
|
2268
2295
|
min-width: 0;
|
|
2269
2296
|
}
|
|
2297
|
+
.git-changes-actions {
|
|
2298
|
+
flex: 0 0 auto;
|
|
2299
|
+
display: flex;
|
|
2300
|
+
align-items: center;
|
|
2301
|
+
justify-content: flex-end;
|
|
2302
|
+
gap: 0.5rem;
|
|
2303
|
+
flex-wrap: wrap;
|
|
2304
|
+
}
|
|
2270
2305
|
.git-changes-kicker {
|
|
2271
2306
|
display: block;
|
|
2272
2307
|
color: var(--ctp-yellow);
|
|
@@ -2287,6 +2322,7 @@ button.footer-meta {
|
|
|
2287
2322
|
.git-changes-empty.error {
|
|
2288
2323
|
color: var(--ctp-red);
|
|
2289
2324
|
}
|
|
2325
|
+
.git-changes-status.success,
|
|
2290
2326
|
.git-changes-empty.success {
|
|
2291
2327
|
color: var(--ctp-green);
|
|
2292
2328
|
}
|
|
@@ -5765,11 +5801,41 @@ button.composer-skill-tag:focus-visible {
|
|
|
5765
5801
|
gap: 0.6rem;
|
|
5766
5802
|
align-items: center;
|
|
5767
5803
|
}
|
|
5804
|
+
.app-runner-custom-item.unavailable {
|
|
5805
|
+
border-color: rgba(243, 139, 168, 0.32);
|
|
5806
|
+
}
|
|
5768
5807
|
.app-runner-custom-item-details {
|
|
5769
5808
|
display: grid;
|
|
5770
5809
|
gap: 0.18rem;
|
|
5771
5810
|
min-width: 0;
|
|
5772
5811
|
}
|
|
5812
|
+
.app-runner-custom-warning,
|
|
5813
|
+
.app-runner-custom-feedback {
|
|
5814
|
+
color: var(--danger);
|
|
5815
|
+
line-height: 1.4;
|
|
5816
|
+
font-size: 0.82rem;
|
|
5817
|
+
font-weight: 700;
|
|
5818
|
+
}
|
|
5819
|
+
.app-runner-custom-feedback {
|
|
5820
|
+
padding: 0.58rem 0.66rem;
|
|
5821
|
+
border: 1px solid rgba(243, 139, 168, 0.26);
|
|
5822
|
+
border-radius: 0.68rem;
|
|
5823
|
+
background: rgba(243, 139, 168, 0.08);
|
|
5824
|
+
}
|
|
5825
|
+
.app-runner-custom-feedback.success {
|
|
5826
|
+
color: var(--ctp-green);
|
|
5827
|
+
border-color: rgba(166, 227, 161, 0.28);
|
|
5828
|
+
background: rgba(166, 227, 161, 0.08);
|
|
5829
|
+
}
|
|
5830
|
+
.app-runner-custom-feedback.warning {
|
|
5831
|
+
color: var(--ctp-yellow);
|
|
5832
|
+
border-color: rgba(249, 226, 175, 0.28);
|
|
5833
|
+
background: rgba(249, 226, 175, 0.08);
|
|
5834
|
+
}
|
|
5835
|
+
.app-runner-custom-diagnostics {
|
|
5836
|
+
display: grid;
|
|
5837
|
+
gap: 0.42rem;
|
|
5838
|
+
}
|
|
5773
5839
|
.app-runner-custom-item-actions,
|
|
5774
5840
|
.app-runner-custom-form-actions {
|
|
5775
5841
|
display: flex;
|
|
File without changes
|
|
@@ -219,6 +219,54 @@ try {
|
|
|
219
219
|
assert.equal(clampedMessages.body?.data?.since, 3, "since beyond the transcript should clamp to the total count");
|
|
220
220
|
assert.equal((clampedMessages.body?.data?.messages || []).length, 0);
|
|
221
221
|
|
|
222
|
+
// Custom app runners: save failures must be explicit, saved runners must be runnable,
|
|
223
|
+
// and stale saved runners must explain why they are not shown in the Run menu.
|
|
224
|
+
await writeFile(path.join(cwd, "custom-runner.mjs"), "console.log('custom runner ok')\n");
|
|
225
|
+
const missingCommandRunner = await request("127.0.0.1", "/api/app-runner-config", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
body: { tab: tabId, runner: { label: "Broken custom", command: "definitely-missing-pi-webui-runner", path: "custom-runner.mjs" } },
|
|
228
|
+
});
|
|
229
|
+
assert.equal(missingCommandRunner.status, 400, "saving a custom runner with a missing command should fail visibly");
|
|
230
|
+
assert.match(String(missingCommandRunner.body?.error || ""), /Command is not available: definitely-missing-pi-webui-runner/);
|
|
231
|
+
|
|
232
|
+
const savedCustomRunner = await request("127.0.0.1", "/api/app-runner-config", {
|
|
233
|
+
method: "POST",
|
|
234
|
+
body: { tab: tabId, runner: { label: "Custom node", command: process.execPath, path: "custom-runner.mjs" } },
|
|
235
|
+
timeoutMs: 10_000,
|
|
236
|
+
});
|
|
237
|
+
assert.equal(savedCustomRunner.status, 200, `saving a valid custom runner should succeed: ${savedCustomRunner.body?.error || ""}`);
|
|
238
|
+
const customConfigRunner = savedCustomRunner.body?.data?.customRunnerConfig?.runners?.find((runner) => runner.label === "Custom node");
|
|
239
|
+
assert.equal(customConfigRunner?.available, true, "saved custom runner config should mark runnable entries available");
|
|
240
|
+
const customRunner = savedCustomRunner.body?.data?.runners?.find((runner) => runner.custom === true && runner.label === "Custom node");
|
|
241
|
+
assert.ok(customRunner?.id, "saved available custom runner should appear in detected app runners");
|
|
242
|
+
|
|
243
|
+
const customRunStart = await request("127.0.0.1", "/api/app-runner", {
|
|
244
|
+
method: "POST",
|
|
245
|
+
body: { tab: tabId, runnerId: customRunner.id },
|
|
246
|
+
timeoutMs: 10_000,
|
|
247
|
+
});
|
|
248
|
+
assert.equal(customRunStart.status, 200, `custom runner start should return ok: ${customRunStart.body?.error || ""}`);
|
|
249
|
+
let customRunState = customRunStart;
|
|
250
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
251
|
+
if (customRunState.body?.data?.activeRun?.status && customRunState.body.data.activeRun.status !== "running") break;
|
|
252
|
+
await delay(100);
|
|
253
|
+
customRunState = await request("127.0.0.1", `/api/app-runners?tab=${encodeURIComponent(tabId)}`, { timeoutMs: 5_000 });
|
|
254
|
+
}
|
|
255
|
+
assert.equal(customRunState.body?.data?.activeRun?.status, "done", "custom runner should finish successfully");
|
|
256
|
+
assert.match((customRunState.body?.data?.activeRun?.lines || []).join("\n"), /custom runner ok/, "custom runner output should be captured");
|
|
257
|
+
await request("127.0.0.1", "/api/app-runner/clear", { method: "POST", body: { tab: tabId } });
|
|
258
|
+
|
|
259
|
+
await writeFile(path.join(cwd, ".pi-webui-runners.json"), `${JSON.stringify({
|
|
260
|
+
version: 1,
|
|
261
|
+
runners: [{ id: "broken-custom", label: "Broken custom", command: "definitely-missing-pi-webui-runner", path: "custom-runner.mjs" }],
|
|
262
|
+
}, null, 2)}\n`);
|
|
263
|
+
const staleCustomRunner = await request("127.0.0.1", `/api/app-runners?tab=${encodeURIComponent(tabId)}`, { timeoutMs: 10_000 });
|
|
264
|
+
assert.equal(staleCustomRunner.status, 200);
|
|
265
|
+
const brokenConfigRunner = staleCustomRunner.body?.data?.customRunnerConfig?.runners?.find((runner) => runner.label === "Broken custom");
|
|
266
|
+
assert.equal(brokenConfigRunner?.available, false, "unavailable saved custom runners should be flagged in config data");
|
|
267
|
+
assert.match(String(brokenConfigRunner?.unavailableReason || ""), /Command is not available: definitely-missing-pi-webui-runner/);
|
|
268
|
+
assert.equal(staleCustomRunner.body?.data?.runners?.some((runner) => runner.label === "Broken custom"), false, "unavailable custom runners should not appear in runnable menu data");
|
|
269
|
+
|
|
222
270
|
// Native slash command routed through the adapter (/copy → get_last_assistant_text).
|
|
223
271
|
const copy = await request("127.0.0.1", "/api/prompt", {
|
|
224
272
|
method: "POST",
|
|
@@ -346,16 +346,20 @@ assert.doesNotMatch(css, /\.footer-(?:metric|meta)-action::after/, "clickable fo
|
|
|
346
346
|
assert.match(css, /\.extension-dialog\.git-changes-dialog \{[\s\S]*?--git-changes-dialog-size:[\s\S]*?width:\s*var\(--git-changes-dialog-size\)[\s\S]*?height:\s*var\(--git-changes-dialog-size\)[\s\S]*?aspect-ratio:\s*1 \/ 1/, "git changes modal should override the base dialog with a square wide diff layout");
|
|
347
347
|
assert.match(css, /\.git-current-file-header \{[\s\S]*?position:\s*sticky[\s\S]*?top:\s*-0\.72rem/, "git changes modal should keep a sticky current-file header inside the diff scroller");
|
|
348
348
|
assert.match(css, /\.git-diff-grid \{[\s\S]*?grid-template-columns:\s*3\.8rem minmax\(22rem, 1fr\) 3\.8rem minmax\(22rem, 1fr\)/, "git changes modal should render a side-by-side diff grid");
|
|
349
|
-
assert.match(html, /id="gitChangesDialog"[\s\S]*id="gitChangesRefreshButton"[\s\S]*id="gitChangesBody"/, "git changes modal should expose refresh controls and a diff body");
|
|
349
|
+
assert.match(html, /id="gitChangesDialog"[\s\S]*id="gitChangesRefreshButton"[\s\S]*id="gitChangesPullButton"[\s\S]*id="gitChangesBody"/, "git changes modal should expose refresh, pull controls, and a diff body");
|
|
350
350
|
assert.match(app, /chip\.key === "changes"[\s\S]*?options\.onClick = openGitChangesDialog/, "footer CHANGES chip should open the git changes modal");
|
|
351
351
|
assert.match(app, /async function loadGitChangesDialog[\s\S]*api\("\/api\/git-changes"/, "git changes modal should load diff data from the server endpoint");
|
|
352
|
+
assert.match(app, /async function pullGitChangesDialog\(\)[\s\S]*api\("\/api\/git-changes\/pull", \{ method: "POST"/, "git changes modal should post to the pull endpoint from the Pull button");
|
|
353
|
+
assert.match(app, /function gitDiffDisplayLine\(row, side\)[\s\S]*`-\$\{text\}`[\s\S]*`\+\$\{text\}`/, "git changes modal should render changed lines with +/- prefixes");
|
|
352
354
|
assert.match(app, /function gitUntrackedEntryToDiffFile\(entry\)[\s\S]*?renderRowLimit:\s*Number\.POSITIVE_INFINITY[\s\S]*?type: "added"/, "untracked files should render as complete added-file diffs without the row preview cap");
|
|
353
355
|
assert.match(app, /async function loadMissingGitUntrackedContent\(entry[\s\S]*?\/api\/git-changes\/untracked-file\?path=/, "untracked path-only payloads should fetch complete file contents instead of rendering as empty files");
|
|
354
356
|
assert.match(app, /function updateGitChangesCurrentFileHeader\(\)[\s\S]*?querySelectorAll\("\.git-diff-file\[data-git-diff-file\]"\)/, "git changes modal should derive the sticky current-file header from visible file cards");
|
|
355
357
|
assert.match(server, /async function readGitUntrackedEntry\(root, file\)[\s\S]*?content: binary \? "" : buffer\.toString\("utf8"\)/, "server should include complete text contents for untracked files");
|
|
356
358
|
assert.match(server, /url\.pathname === "\/api\/git-changes\/untracked-file" && req\.method === "GET"/, "server should expose a focused untracked-file content endpoint for stale path-only payload fallbacks");
|
|
357
|
-
assert.match(server, /async function readGitChanges\(cwd\)[\s\S]*?const diffArgs = \["diff", "--no-ext-diff"[\s\S]*?\["diff", "--cached"/, "server should collect
|
|
359
|
+
assert.match(server, /async function readGitChanges\(cwd\)[\s\S]*?const diffArgs = \["diff", "--no-ext-diff"[\s\S]*?"--unified=0"[\s\S]*?\["diff", "--cached"/, "server should collect compact staged and unstaged git diffs for the changes modal");
|
|
360
|
+
assert.match(server, /async function readGitIncomingChanges\(root, summary\)[\s\S]*?"HEAD\.\.@\{upstream\}"/, "server should collect incoming upstream diffs when remote commits are behind");
|
|
358
361
|
assert.match(server, /url\.pathname === "\/api\/git-changes" && req\.method === "GET"/, "server should expose GET /api/git-changes for the changes modal");
|
|
362
|
+
assert.match(server, /url\.pathname === "\/api\/git-changes\/pull" && req\.method === "POST"[\s\S]*?pullGitChanges\(tab\.cwd\)/, "server should expose POST /api/git-changes/pull for the changes modal Pull button");
|
|
359
363
|
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.footer-line-meta \{[\s\S]*?display:\s*flex;[\s\S]*?flex-wrap:\s*wrap;[\s\S]*?\.footer-line-meta \.footer-meta \{[\s\S]*?flex:\s*1 1 var\(--footer-chip-min-width\);[\s\S]*?width:\s*auto;[\s\S]*?\.footer-workspace,\n\s+\.footer-model,\n\s+\.footer-thinking \{ grid-column:\s*auto; \}/, "narrow git-footer metadata should wrap like the top metric row instead of forcing a two-column grid");
|
|
360
364
|
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)[\s\S]*?\.context-meter-bar \{ display:\s*none !important; \}/, "mobile should hide the WebUI context meter that appears after high context usage");
|
|
361
365
|
assert.match(css, /\.footer-line-tui \{[\s\S]*?white-space:\s*nowrap/, "default Web UI footer should use a minimal TUI-like line");
|
|
@@ -670,6 +674,9 @@ assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided
|
|
|
670
674
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
671
675
|
assert.match(app, /async function refreshAppRunners\(tabContext = activeTabContext\(\)\)/, "frontend should load detected app runners for the active tab cwd");
|
|
672
676
|
assert.match(app, /function renderAppRunnerWidget\(\)/, "frontend should render app runner output in the shared top widget area");
|
|
677
|
+
assert.match(app, /function appRunnerFailureState\(runnerId, error[\s\S]*failed to start app runner/, "frontend should render visible app-runner start failures instead of only logging them");
|
|
678
|
+
assert.match(app, /appRunnerCustomFeedback[\s\S]*Custom app runner was not saved/, "custom app-runner save failures should be shown inline in the dialog");
|
|
679
|
+
assert.match(server, /function customAppRunnerUnavailableReason\(projectRoot, runner\)[\s\S]*Command is not available/, "server should explain why saved custom app runners are unavailable");
|
|
673
680
|
assert.match(server, /url\.pathname === "\/api\/app-runners" && req\.method === "GET"/, "server should expose detected app runners for the active tab cwd");
|
|
674
681
|
assert.match(server, /url\.pathname === "\/api\/app-runner" && req\.method === "POST"/, "server should start selected app runners directly");
|
|
675
682
|
assert.match(server, /function addGoRunner\(runners, cwd\)[\s\S]*Go\/Golang app entry/, "server should detect Go\/Golang app runners");
|
|
@@ -943,7 +950,11 @@ assert.match(app, /classList\.toggle\("terminal-tabs-dense", tabs\.length >= 10\
|
|
|
943
950
|
assert.match(app, /appendTerminalTabContent\(button, \{ title: activeTitle,[\s\S]*?count: groupTabs\.length \}\)/, "group buttons should show the active terminal name instead of only the cwd label");
|
|
944
951
|
assert.match(app, /wrapper\.addEventListener\("pointerenter", \(\) => setOpenTerminalTabGroup\(group\.key\)\)/, "terminal tab groups should mark themselves open while hovered");
|
|
945
952
|
assert.match(app, /if \(openTerminalTabGroupKey\) \{\n\s+scheduleRefreshTabs\(600\);/, "tab polling should defer full tab refreshes while a group menu is open");
|
|
946
|
-
assert.match(app, /function shouldRenderTerminalTabGroup\(group, groupCount\) \{\n\s+return groupCount > 1 && group\.tabs\.length > 1 && Boolean\(group\.cwd\);\n\}/, "terminal tabs should only
|
|
953
|
+
assert.match(app, /function shouldRenderTerminalTabGroup\(group, groupCount\) \{\n\s+if \(group\.custom\) return group\.tabs\.length > 1;\n\s+return groupCount > 1 && group\.tabs\.length > 1 && Boolean\(group\.cwd\);\n\}/, "terminal tabs should always render custom groups while only collapsing cwd groups when multiple groups are available");
|
|
954
|
+
assert.match(app, /TERMINAL_CUSTOM_GROUPS_STORAGE_KEY/, "frontend should persist custom terminal tab groups in browser storage");
|
|
955
|
+
assert.match(app, /function bindTerminalTabDragAndDrop\(/, "terminal tabs should bind drag-and-drop grouping behavior");
|
|
956
|
+
assert.match(app, /function handleTerminalTabDrop\(sourceTabId, target\)[\s\S]*?createTerminalCustomGroup/, "dropping a terminal tab onto another tab or group should create or update a custom group");
|
|
957
|
+
assert.match(css, /\.terminal-tab\.terminal-tab-drag-over,[\s\S]*?\.terminal-tab-group-item\.terminal-tab-drag-over/, "terminal tab drop targets should show drag-over affordance");
|
|
947
958
|
assert.match(app, /function closeTerminalTabGroup\(group\)[\s\S]*?closeTerminalTabs\(group\.tabs\.map\(\(tab\) => tab\.id\)/, "terminal tab groups should be closable as a batch");
|
|
948
959
|
assert.match(app, /function closeAllTerminalTabs\(\)[\s\S]*?closeTerminalTabs\(tabs\.map\(\(tab\) => tab\.id\)/, "tab header should close all terminal tabs as a batch");
|
|
949
960
|
assert.match(app, /WARNING: \$\{activeAgentTabs\.length\}[\s\S]*?still running or waiting for input/, "tab close confirmations should warn when agents are still running");
|