@firstpick/pi-package-webui 0.4.6 → 0.4.8
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 -9
- package/bin/pi-webui.mjs +204 -65
- package/package.json +10 -3
- package/public/app.js +268 -113
- package/public/index.html +9 -4
- package/public/styles.css +39 -0
- package/tests/http-endpoints-harness.test.mjs +105 -0
- package/tests/mobile-static.test.mjs +23 -11
package/public/index.html
CHANGED
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
<p id="updateNotificationMessage">A newer Pi version is available.</p>
|
|
53
53
|
<p id="updateNotificationDetail" class="update-notification-detail muted"></p>
|
|
54
54
|
<div class="update-notification-actions">
|
|
55
|
-
<button id="updateNotificationUpdateButton" class="primary" type="button">Update & restart</button>
|
|
55
|
+
<button id="updateNotificationUpdateButton" class="primary" type="button">Update Pi & restart</button>
|
|
56
|
+
<button id="updateNotificationUpdateAllButton" type="button">Update all & restart</button>
|
|
56
57
|
<button id="updateNotificationDismissButton" type="button">Later</button>
|
|
57
58
|
</div>
|
|
58
59
|
</div>
|
|
@@ -379,8 +380,8 @@
|
|
|
379
380
|
</div>
|
|
380
381
|
<div id="backgroundStatus" class="background-status muted">Theme default</div>
|
|
381
382
|
</div>
|
|
382
|
-
<div class="control-field network-control-field">
|
|
383
|
-
<label>
|
|
383
|
+
<div id="networkControlField" class="control-field network-control-field" hidden>
|
|
384
|
+
<label>Remote WebUI</label>
|
|
384
385
|
<div id="networkStatus" class="network-status closed">Local only</div>
|
|
385
386
|
<label class="toggle-control remote-auth-toggle" for="remoteAuthToggle">
|
|
386
387
|
<input id="remoteAuthToggle" type="checkbox" />
|
|
@@ -398,6 +399,7 @@
|
|
|
398
399
|
<option value="" selected>Choose action…</option>
|
|
399
400
|
<option value="restart">Restart Server</option>
|
|
400
401
|
<option value="update">Update Pi & Restart</option>
|
|
402
|
+
<option value="update-all">Update Pi + Packages & Restart</option>
|
|
401
403
|
<option value="stop">Stop Server</option>
|
|
402
404
|
</select>
|
|
403
405
|
<button id="runServerActionButton" type="button" disabled>Run</button>
|
|
@@ -539,7 +541,10 @@
|
|
|
539
541
|
<h2 id="gitChangesTitle">Uncommitted Changes</h2>
|
|
540
542
|
<p id="gitChangesSubtitle" class="muted">Current tab git diff</p>
|
|
541
543
|
</div>
|
|
542
|
-
<
|
|
544
|
+
<div class="git-changes-actions">
|
|
545
|
+
<button id="gitChangesRefreshButton" type="button">Refresh</button>
|
|
546
|
+
<button id="gitChangesPullButton" class="primary" type="button" disabled>Pull</button>
|
|
547
|
+
</div>
|
|
543
548
|
</div>
|
|
544
549
|
<p id="gitChangesStatus" class="git-changes-status muted" role="status" aria-live="polite"></p>
|
|
545
550
|
<div id="gitChangesBody" class="git-changes-body"></div>
|
package/public/styles.css
CHANGED
|
@@ -2294,6 +2294,14 @@ button.footer-meta {
|
|
|
2294
2294
|
.git-changes-header > div:first-child {
|
|
2295
2295
|
min-width: 0;
|
|
2296
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
|
+
}
|
|
2297
2305
|
.git-changes-kicker {
|
|
2298
2306
|
display: block;
|
|
2299
2307
|
color: var(--ctp-yellow);
|
|
@@ -2314,6 +2322,7 @@ button.footer-meta {
|
|
|
2314
2322
|
.git-changes-empty.error {
|
|
2315
2323
|
color: var(--ctp-red);
|
|
2316
2324
|
}
|
|
2325
|
+
.git-changes-status.success,
|
|
2317
2326
|
.git-changes-empty.success {
|
|
2318
2327
|
color: var(--ctp-green);
|
|
2319
2328
|
}
|
|
@@ -5792,11 +5801,41 @@ button.composer-skill-tag:focus-visible {
|
|
|
5792
5801
|
gap: 0.6rem;
|
|
5793
5802
|
align-items: center;
|
|
5794
5803
|
}
|
|
5804
|
+
.app-runner-custom-item.unavailable {
|
|
5805
|
+
border-color: rgba(243, 139, 168, 0.32);
|
|
5806
|
+
}
|
|
5795
5807
|
.app-runner-custom-item-details {
|
|
5796
5808
|
display: grid;
|
|
5797
5809
|
gap: 0.18rem;
|
|
5798
5810
|
min-width: 0;
|
|
5799
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
|
+
}
|
|
5800
5839
|
.app-runner-custom-item-actions,
|
|
5801
5840
|
.app-runner-custom-form-actions {
|
|
5802
5841
|
display: flex;
|
|
@@ -37,6 +37,22 @@ async function request(host, pathname, { method = "GET", body, timeoutMs = 5_000
|
|
|
37
37
|
return { status: response.status, body: payload };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function runGitFixture(args, cwd, message) {
|
|
41
|
+
const result = spawnSync("git", args, {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
env: {
|
|
45
|
+
...process.env,
|
|
46
|
+
GIT_AUTHOR_NAME: "Pi WebUI Test",
|
|
47
|
+
GIT_AUTHOR_EMAIL: "pi-webui-test@example.invalid",
|
|
48
|
+
GIT_COMMITTER_NAME: "Pi WebUI Test",
|
|
49
|
+
GIT_COMMITTER_EMAIL: "pi-webui-test@example.invalid",
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
assert.equal(result.status, 0, `${message}\n$ git ${args.join(" ")}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
53
|
+
return result.stdout.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
const cwd = await mkdtemp(path.join(tmpdir(), "pi-webui-http-harness-"));
|
|
41
57
|
const settingsFile = path.join(cwd, "webui-settings.json");
|
|
42
58
|
await chmod(fakePi, 0o755);
|
|
@@ -152,6 +168,47 @@ try {
|
|
|
152
168
|
assert.equal(gitMain.status, 200);
|
|
153
169
|
assert.equal(gitMain.body?.ok, true, "main branch endpoint should rename the branch");
|
|
154
170
|
|
|
171
|
+
const remoteFixtureRoot = await mkdtemp(path.join(tmpdir(), "pi-webui-git-remote-"));
|
|
172
|
+
const remoteBare = path.join(remoteFixtureRoot, "origin.git");
|
|
173
|
+
const localRepo = path.join(remoteFixtureRoot, "local");
|
|
174
|
+
const remoteWork = path.join(remoteFixtureRoot, "remote-work");
|
|
175
|
+
runGitFixture(["init", "--bare", remoteBare], remoteFixtureRoot, "remote fixture should initialize a bare origin");
|
|
176
|
+
runGitFixture(["init", localRepo], remoteFixtureRoot, "remote fixture should initialize a local repo");
|
|
177
|
+
runGitFixture(["config", "user.name", "Pi WebUI Test"], localRepo, "local repo should set a user name");
|
|
178
|
+
runGitFixture(["config", "user.email", "pi-webui-test@example.invalid"], localRepo, "local repo should set a user email");
|
|
179
|
+
await writeFile(path.join(localRepo, "incoming.txt"), "base\n");
|
|
180
|
+
runGitFixture(["add", "incoming.txt"], localRepo, "local repo should stage base content");
|
|
181
|
+
runGitFixture(["commit", "-m", "base"], localRepo, "local repo should commit base content");
|
|
182
|
+
runGitFixture(["branch", "-M", "main"], localRepo, "local repo should rename main branch");
|
|
183
|
+
runGitFixture(["remote", "add", "origin", remoteBare], localRepo, "local repo should add bare origin");
|
|
184
|
+
runGitFixture(["push", "-u", "origin", "main"], localRepo, "local repo should push main to bare origin");
|
|
185
|
+
runGitFixture(["symbolic-ref", "HEAD", "refs/heads/main"], remoteBare, "bare origin should advertise main as HEAD");
|
|
186
|
+
runGitFixture(["clone", remoteBare, remoteWork], remoteFixtureRoot, "remote worktree should clone bare origin");
|
|
187
|
+
runGitFixture(["config", "user.name", "Pi WebUI Test"], remoteWork, "remote worktree should set a user name");
|
|
188
|
+
runGitFixture(["config", "user.email", "pi-webui-test@example.invalid"], remoteWork, "remote worktree should set a user email");
|
|
189
|
+
await writeFile(path.join(remoteWork, "incoming.txt"), "base\nremote one\n");
|
|
190
|
+
runGitFixture(["commit", "-am", "remote one"], remoteWork, "remote worktree should commit first incoming change");
|
|
191
|
+
await writeFile(path.join(remoteWork, "incoming.txt"), "base\nremote one\nremote two\n");
|
|
192
|
+
runGitFixture(["commit", "-am", "remote two"], remoteWork, "remote worktree should commit second incoming change");
|
|
193
|
+
runGitFixture(["push", "origin", "main"], remoteWork, "remote worktree should push incoming commits");
|
|
194
|
+
runGitFixture(["fetch", "origin"], localRepo, "local repo should fetch incoming commits");
|
|
195
|
+
|
|
196
|
+
const remoteTab = await request("127.0.0.1", "/api/tabs", { method: "POST", body: { cwd: localRepo, title: "remote-behind-fixture" } });
|
|
197
|
+
assert.equal(remoteTab.status, 201);
|
|
198
|
+
const remoteTabId = remoteTab.body?.data?.tab?.id;
|
|
199
|
+
assert.ok(remoteTabId, "remote fixture tab should have an id");
|
|
200
|
+
const incomingChanges = await request("127.0.0.1", `/api/git-changes?tab=${encodeURIComponent(remoteTabId)}`);
|
|
201
|
+
assert.equal(incomingChanges.status, 200);
|
|
202
|
+
assert.equal(incomingChanges.body?.ok, true, "git changes endpoint should load a fetched-behind repo");
|
|
203
|
+
assert.equal(incomingChanges.body?.data?.summary?.behind, 2, "git changes endpoint should report two fetched commits behind");
|
|
204
|
+
assert.equal(incomingChanges.body?.data?.remote?.canPull, true, "git changes endpoint should mark fetched commits as pullable");
|
|
205
|
+
assert.ok(incomingChanges.body?.data?.sections?.some((section) => section.key === "incoming"), "git changes endpoint should include an incoming diff section");
|
|
206
|
+
|
|
207
|
+
const pullIncoming = await request("127.0.0.1", "/api/git-changes/pull", { method: "POST", body: { tab: remoteTabId }, timeoutMs: 20_000 });
|
|
208
|
+
assert.equal(pullIncoming.status, 200);
|
|
209
|
+
assert.equal(pullIncoming.body?.ok, true, "pull endpoint should fast-forward fetched incoming commits");
|
|
210
|
+
assert.equal(pullIncoming.body?.data?.changes?.summary?.behind, 0, "pull endpoint should refresh changes with no remote commits left behind");
|
|
211
|
+
|
|
155
212
|
const gitRemote = await request("127.0.0.1", "/api/git-workflow/remote", { method: "POST", body: { username: "Firstp1ck", repoName: "pi-webui-http-harness", tab: tabId } });
|
|
156
213
|
assert.equal(gitRemote.status, 200);
|
|
157
214
|
assert.equal(gitRemote.body?.ok, true, "remote endpoint should add origin without pushing");
|
|
@@ -219,6 +276,54 @@ try {
|
|
|
219
276
|
assert.equal(clampedMessages.body?.data?.since, 3, "since beyond the transcript should clamp to the total count");
|
|
220
277
|
assert.equal((clampedMessages.body?.data?.messages || []).length, 0);
|
|
221
278
|
|
|
279
|
+
// Custom app runners: save failures must be explicit, saved runners must be runnable,
|
|
280
|
+
// and stale saved runners must explain why they are not shown in the Run menu.
|
|
281
|
+
await writeFile(path.join(cwd, "custom-runner.mjs"), "console.log('custom runner ok')\n");
|
|
282
|
+
const missingCommandRunner = await request("127.0.0.1", "/api/app-runner-config", {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: { tab: tabId, runner: { label: "Broken custom", command: "definitely-missing-pi-webui-runner", path: "custom-runner.mjs" } },
|
|
285
|
+
});
|
|
286
|
+
assert.equal(missingCommandRunner.status, 400, "saving a custom runner with a missing command should fail visibly");
|
|
287
|
+
assert.match(String(missingCommandRunner.body?.error || ""), /Command is not available: definitely-missing-pi-webui-runner/);
|
|
288
|
+
|
|
289
|
+
const savedCustomRunner = await request("127.0.0.1", "/api/app-runner-config", {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: { tab: tabId, runner: { label: "Custom node", command: process.execPath, path: "custom-runner.mjs" } },
|
|
292
|
+
timeoutMs: 10_000,
|
|
293
|
+
});
|
|
294
|
+
assert.equal(savedCustomRunner.status, 200, `saving a valid custom runner should succeed: ${savedCustomRunner.body?.error || ""}`);
|
|
295
|
+
const customConfigRunner = savedCustomRunner.body?.data?.customRunnerConfig?.runners?.find((runner) => runner.label === "Custom node");
|
|
296
|
+
assert.equal(customConfigRunner?.available, true, "saved custom runner config should mark runnable entries available");
|
|
297
|
+
const customRunner = savedCustomRunner.body?.data?.runners?.find((runner) => runner.custom === true && runner.label === "Custom node");
|
|
298
|
+
assert.ok(customRunner?.id, "saved available custom runner should appear in detected app runners");
|
|
299
|
+
|
|
300
|
+
const customRunStart = await request("127.0.0.1", "/api/app-runner", {
|
|
301
|
+
method: "POST",
|
|
302
|
+
body: { tab: tabId, runnerId: customRunner.id },
|
|
303
|
+
timeoutMs: 10_000,
|
|
304
|
+
});
|
|
305
|
+
assert.equal(customRunStart.status, 200, `custom runner start should return ok: ${customRunStart.body?.error || ""}`);
|
|
306
|
+
let customRunState = customRunStart;
|
|
307
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
308
|
+
if (customRunState.body?.data?.activeRun?.status && customRunState.body.data.activeRun.status !== "running") break;
|
|
309
|
+
await delay(100);
|
|
310
|
+
customRunState = await request("127.0.0.1", `/api/app-runners?tab=${encodeURIComponent(tabId)}`, { timeoutMs: 5_000 });
|
|
311
|
+
}
|
|
312
|
+
assert.equal(customRunState.body?.data?.activeRun?.status, "done", "custom runner should finish successfully");
|
|
313
|
+
assert.match((customRunState.body?.data?.activeRun?.lines || []).join("\n"), /custom runner ok/, "custom runner output should be captured");
|
|
314
|
+
await request("127.0.0.1", "/api/app-runner/clear", { method: "POST", body: { tab: tabId } });
|
|
315
|
+
|
|
316
|
+
await writeFile(path.join(cwd, ".pi-webui-runners.json"), `${JSON.stringify({
|
|
317
|
+
version: 1,
|
|
318
|
+
runners: [{ id: "broken-custom", label: "Broken custom", command: "definitely-missing-pi-webui-runner", path: "custom-runner.mjs" }],
|
|
319
|
+
}, null, 2)}\n`);
|
|
320
|
+
const staleCustomRunner = await request("127.0.0.1", `/api/app-runners?tab=${encodeURIComponent(tabId)}`, { timeoutMs: 10_000 });
|
|
321
|
+
assert.equal(staleCustomRunner.status, 200);
|
|
322
|
+
const brokenConfigRunner = staleCustomRunner.body?.data?.customRunnerConfig?.runners?.find((runner) => runner.label === "Broken custom");
|
|
323
|
+
assert.equal(brokenConfigRunner?.available, false, "unavailable saved custom runners should be flagged in config data");
|
|
324
|
+
assert.match(String(brokenConfigRunner?.unavailableReason || ""), /Command is not available: definitely-missing-pi-webui-runner/);
|
|
325
|
+
assert.equal(staleCustomRunner.body?.data?.runners?.some((runner) => runner.label === "Broken custom"), false, "unavailable custom runners should not appear in runnable menu data");
|
|
326
|
+
|
|
222
327
|
// Native slash command routed through the adapter (/copy → get_last_assistant_text).
|
|
223
328
|
const copy = await request("127.0.0.1", "/api/prompt", {
|
|
224
329
|
method: "POST",
|
|
@@ -346,16 +346,21 @@ 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, /\["status", "--porcelain=2", "--branch", "--untracked-files=all"\][\s\S]*?summarizeGitPorcelainStatus\(porcelainStatusText\)/, "server should derive behind/ahead from locale-independent porcelain status so the Pull button activates after fetch");
|
|
361
|
+
assert.match(server, /async function readGitIncomingChanges\(root, summary\)[\s\S]*?"HEAD\.\.@\{upstream\}"/, "server should collect incoming upstream diffs when remote commits are behind");
|
|
358
362
|
assert.match(server, /url\.pathname === "\/api\/git-changes" && req\.method === "GET"/, "server should expose GET /api/git-changes for the changes modal");
|
|
363
|
+
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
364
|
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
365
|
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
366
|
assert.match(css, /\.footer-line-tui \{[\s\S]*?white-space:\s*nowrap/, "default Web UI footer should use a minimal TUI-like line");
|
|
@@ -428,8 +433,9 @@ assert.match(app, /initializeCustomBackground\(\)\.catch/, "startup should resto
|
|
|
428
433
|
assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
|
|
429
434
|
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
430
435
|
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
431
|
-
assert.match(app, /remoteAuthToggle: \$\("#remoteAuthToggle"\)/, "
|
|
432
|
-
assert.match(
|
|
436
|
+
assert.match(app, /remoteAuthToggle: \$\("#remoteAuthToggle"\)/, "Remote WebUI controls should bind the remote PIN auth toggle");
|
|
437
|
+
assert.match(html, /id="networkControlField"[^>]*hidden/, "Remote WebUI browser controls should be hidden until the optional package is loaded and enabled");
|
|
438
|
+
assert.match(app, /remoteWebuiCommand\(enable \? "authOn" : "authOff"/, "remote PIN auth toggle should dispatch through the Remote WebUI package command");
|
|
433
439
|
assert.match(server, /function webuiSettingsFile\(\)[\s\S]*pi-webui[\s\S]*settings\.json/, "server should persist Web UI settings under a pi-webui settings file");
|
|
434
440
|
assert.match(server, /let persistedRemoteAuthEnabled = await readPersistedRemoteAuthEnabled\(\)/, "server should load the saved Remote PIN auth preference before startup");
|
|
435
441
|
assert.match(server, /if \(remoteAuthStartupEnabled\(\)\) enableRemoteAuth\(remoteAuthStartupReason\(\)\)/, "saved Remote PIN auth preference should enable auth on startup");
|
|
@@ -437,7 +443,7 @@ assert.match(server, /await saveRemoteAuthPreference\(true\)/, "enabling Remote
|
|
|
437
443
|
assert.match(server, /await saveRemoteAuthPreference\(false\)/, "disabling Remote PIN auth should persist the off preference");
|
|
438
444
|
assert.match(server, /function pinFromHash\(\)[\s\S]*new URLSearchParams\(String\(window\.location\.hash \|\| ""\)\.replace\(\/\^#\/, ""\)\)/, "remote auth page should read QR-provided PINs from the URL fragment");
|
|
439
445
|
assert.match(server, /window\.history\.replaceState\(null, "", window\.location\.pathname \+ \(window\.location\.search \|\| ""\)\)/, "remote auth page should scrub fragment PINs from the address bar before authenticating");
|
|
440
|
-
assert.match(app, /
|
|
446
|
+
assert.match(app, /remoteWebuiCommand\(open \? "close" : "open"/, "network open\/close button should dispatch through the Remote WebUI package command");
|
|
441
447
|
assert.match(app, /webuiVersionBadge: \$\("#webuiVersionBadge"\)/, "frontend should bind the Control Deck version badge");
|
|
442
448
|
assert.match(app, /webuiDevBadge: \$\("#webuiDevBadge"\)/, "frontend should bind the Control Deck dev badge");
|
|
443
449
|
assert.match(app, /function refreshWebuiVersion\(\)[\s\S]*api\("\/api\/health", \{ scoped: false \}\)[\s\S]*setWebuiVersion\(health\.webuiVersion\)[\s\S]*setWebuiDevServer\(isWebuiDevMetadata\(health\)\)/, "frontend should load Web UI version and dev mode from health metadata");
|
|
@@ -452,7 +458,7 @@ assert.match(server, /cwdExplicit: false/, "server should track whether startup
|
|
|
452
458
|
assert.match(server, /return options\.cwdExplicit \? \[await createTab\(\)\] : \[\]/, "server should wait for UI cwd selection when no --cwd is supplied");
|
|
453
459
|
assert.match(server, /async function resolvedPiCliScript\(\)[\s\S]*require\.resolve\.paths\(PI_CODING_AGENT_PACKAGE\)[\s\S]*nodeModulesRoot[\s\S]*dist[\s\S]*cli\.js/, "server should resolve the bundled Pi CLI through Node resolution roots so hoisted global installs can spawn RPC tabs");
|
|
454
460
|
assert.match(server, /const bundledCli = await resolvedPiCliScript\(\)/, "standalone server should prefer the resolved Pi CLI script before falling back to PATH pi");
|
|
455
|
-
assert.match(server, /if \(options\.piBinExplicit\) \{\n\s+const command = await resolvePiCommand\(\
|
|
461
|
+
assert.match(server, /if \(options\.piBinExplicit\) \{\n\s+const command = await resolvePiCommand\(updateArgs\)/, "explicit --pi JavaScript launchers should also work for update commands");
|
|
456
462
|
assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
|
|
457
463
|
assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
|
|
458
464
|
assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
|
|
@@ -462,10 +468,12 @@ assert.match(app, /api\("\/api\/shutdown", \{ method: "POST", scoped: false \}\)
|
|
|
462
468
|
assert.match(server, /url\.pathname === "\/api\/restart" && req\.method === "POST"/, "server should expose restart endpoint");
|
|
463
469
|
assert.match(server, /PI_WEBUI_RESTORE_TABS: JSON\.stringify\(restorableTabs \|\| \[\]\)/, "server restart should preserve restorable tab metadata");
|
|
464
470
|
assert.match(server, /if \(webuiDevServer\) env\.PI_WEBUI_DEV = "1";/, "server restart should explicitly preserve dev mode");
|
|
465
|
-
assert.match(server, /
|
|
466
|
-
assert.match(server, /function
|
|
467
|
-
assert.match(app, /
|
|
468
|
-
assert.match(
|
|
471
|
+
assert.match(server, /const updateArgs = all \? \["update", "--all"\] : \["update"\]/, "server update should use pi update by default and pi update --all for package-inclusive updates");
|
|
472
|
+
assert.match(server, /async function resolveUpdateTasks\(\{ all = false \} = \{\}\)[\s\S]*await resolvePiUpdateCommand\(\{ all \}\)/, "server update should resolve a single Pi update command with the selected all mode");
|
|
473
|
+
assert.match(app, /const command = all \? "pi update --all" : "pi update"/, "frontend update confirmation should describe self-only and all update commands");
|
|
474
|
+
assert.match(app, /api\(all \? "\/api\/update\?all=1" : "\/api\/update"/, "frontend all update should call the explicit all-mode endpoint");
|
|
475
|
+
assert.match(html, /<option value="update-all">Update Pi \+ Packages & Restart<\/option>/, "side panel should expose pi update --all as a separate server action");
|
|
476
|
+
assert.match(readme, /`pi update` for Pi-only updates[\s\S]*`pi update --all` for Pi plus configured packages/, "README should document self-only and all update modes");
|
|
469
477
|
assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
|
|
470
478
|
assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
|
|
471
479
|
assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
|
|
@@ -622,8 +630,9 @@ assert.match(app, /id: "tuiSkillsCommand"[\s\S]*?@firstpick\/pi-extension-setup-
|
|
|
622
630
|
assert.match(app, /id: "tuiToolsCommand"[\s\S]*?@firstpick\/pi-extension-tools/, "optional features should include the TUI tools command companion");
|
|
623
631
|
assert.match(app, /id: "remoteWebui"[\s\S]*?@firstpick\/pi-package-remote-webui/, "optional features should include the Remote WebUI companion");
|
|
624
632
|
assert.match(app, /function updateOptionalFeatureAvailability\(\)[\s\S]*hasAvailableCommand\("git-staged-msg"\)[\s\S]*hasAvailableCommand\("release-npm"\)[\s\S]*hasAvailableCommand\("release-aur"\)[\s\S]*hasAvailableCommand\("safety-guard"\)[\s\S]*hasLoadedRpcCommand\("skills"\)[\s\S]*hasAvailableCommand\("todo-progress-status"\)[\s\S]*hasLoadedRpcCommand\("tools"\)[\s\S]*hasAvailableCommand\("remote"\)/, "optional feature detection should call RPC-visible commands directly and distinguish native resource selectors from TUI companions");
|
|
625
|
-
assert.match(app, /hasRemoteWebuiCommand = isOptionalFeatureEnabled\("remoteWebui"\) && hasAvailableCommand\("remote"\)[\s\S]*optionsRemoteButton\.hidden = !hasRemoteWebuiCommand/, "Options menu should show
|
|
633
|
+
assert.match(app, /hasRemoteWebuiCommand = isOptionalFeatureEnabled\("remoteWebui"\) && hasAvailableCommand\("remote"\)[\s\S]*optionsRemoteButton\.hidden = !hasRemoteWebuiCommand[\s\S]*networkControlField\.hidden = !hasRemoteWebuiCommand/, "Options menu and browser controls should show Remote WebUI only when /remote is loaded and enabled");
|
|
626
634
|
assert.match(app, /if \(key === "pi-remote-webui"\) return "remoteWebui"/, "optional feature handling should recognize Remote WebUI widget events without rendering them as overlays");
|
|
635
|
+
assert.match(app, /REMOTE_WEBUI_CONTROLS_PAYLOAD_TYPE = "firstpick\.pi-package-remote-webui\.controls"/, "Remote WebUI package should announce browser controls through a package-owned status payload");
|
|
627
636
|
assert.match(app, /function combineIdenticalDuplicateCommands\(commands\)[\s\S]*duplicateGroups[\s\S]*duplicateCount: group\.length/, "identical duplicate RPC commands should be combined into one visible command entry");
|
|
628
637
|
assert.match(app, /if \(kind === "prompt" && attachments\.length === 0\) message = resolveRpcSlashCommandMessage\(message\)/, "manual slash prompts should resolve combined duplicate command aliases before reaching Pi RPC");
|
|
629
638
|
assert.match(app, /if \(!isOptionalFeatureEnabled\("todoProgressWidget"\)\) return String\(text \|\| ""\)/, "todo progress line stripping should only run when the todo feature is detected and enabled");
|
|
@@ -670,6 +679,9 @@ assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided
|
|
|
670
679
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
671
680
|
assert.match(app, /async function refreshAppRunners\(tabContext = activeTabContext\(\)\)/, "frontend should load detected app runners for the active tab cwd");
|
|
672
681
|
assert.match(app, /function renderAppRunnerWidget\(\)/, "frontend should render app runner output in the shared top widget area");
|
|
682
|
+
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");
|
|
683
|
+
assert.match(app, /appRunnerCustomFeedback[\s\S]*Custom app runner was not saved/, "custom app-runner save failures should be shown inline in the dialog");
|
|
684
|
+
assert.match(server, /function customAppRunnerUnavailableReason\(projectRoot, runner\)[\s\S]*Command is not available/, "server should explain why saved custom app runners are unavailable");
|
|
673
685
|
assert.match(server, /url\.pathname === "\/api\/app-runners" && req\.method === "GET"/, "server should expose detected app runners for the active tab cwd");
|
|
674
686
|
assert.match(server, /url\.pathname === "\/api\/app-runner" && req\.method === "POST"/, "server should start selected app runners directly");
|
|
675
687
|
assert.match(server, /function addGoRunner\(runners, cwd\)[\s\S]*Go\/Golang app entry/, "server should detect Go\/Golang app runners");
|