Skip to content

Commit 826caae

Browse files
committed
feat(webui): add direct Windows cwd path entry
- feat: add direct path input and keyboard/button navigation to the working-directory picker - feat: support rooted Windows drive paths for terminal creation and cwd changes - test: cover picker wiring, cache revisions, responsive styling, and Windows path resolution - docs: document direct path entry and Windows drive syntax
1 parent 6525244 commit 826caae

34 files changed

Lines changed: 172 additions & 72 deletions

pi-package-webui/DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ Compact mode keeps live output lightweight, coalesces sustained live DOM/scroll
257257
- A side-panel **Git** section mirrors the repositories represented by every open terminal tab/group. Repository cards are shown directly—without session-name parent disclosures—and always use the Git-root or cwd basename as their title. One repository can be expanded at a time. First/stale expansion refreshes local status after a five-minute cache window, while live filesystem updates use a debounced server watcher and SSE invalidation to refresh visible repositories without periodic Git polling or an automatic network `git fetch`. Its compact Changes tree groups conflicted, staged, modified, and untracked paths with additions/deletions; right-click a repository, folder, or file (or press Shift+F10/the Context Menu key) for View Diff, refresh, stage/unstage, and confirmed discard/delete actions. History lists the latest 30 commits and opens bounded read-only commit diffs.
258258
- Persistent context-window meter with manual compact and auto-compaction controls near the composer; side-panel thinking changes made while a tab is busy are queued for the next prompt.
259259
- Side-panel theme picker backed by optional `@firstpick/pi-themes-bundle` themes plus Pi-native project/global custom themes. **Customize…** provides all 51 required color tokens, variables, optional `thinkingMax`/export colors, synchronized advanced JSON, valid-only live preview, scope badges, and explicit target-bound overwrite confirmation.
260-
- Per-tab cwd changes, a clickable footer cwd picker, directory creation/search in the picker, saved path fast picks, server-persisted fast picks, and restart-safe restoration of open tabs. When the optional stats and git-footer companions are loaded, clicking the footer **PI** token metric dispatches `/calibrate` in the background and refreshes the metric after the isolated calibration sample is recorded.
260+
- Per-tab cwd changes, a clickable footer cwd picker, direct path entry including rooted Windows drive paths, directory creation/search in the picker, saved path fast picks, server-persisted fast picks, and restart-safe restoration of open tabs. When the optional stats and git-footer companions are loaded, clicking the footer **PI** token metric dispatches `/calibrate` in the background and refreshes the metric after the isolated calibration sample is recorded.
261261
- Detected app runner dropdown for the active tab cwd, including Cargo, Bun, npm/npx/pnpm, Python/uv, Go/Golang, Zig, C/C++, Docker Compose, root/dev/scripts shell scripts, and other common project runners with live output pinned at the top of the terminal. Stream chunks pass through a terminal-line reducer: LF/CRLF commit scrollback, bare CR replaces the current live line, backspace edits that line, and ANSI sequences remain intact for the browser's safe SGR renderer. This is line-oriented terminal behavior rather than a full VT100 cursor grid. Running app runners expose line-oriented stdin in the widget for interactive scripts. Projects can add browseable custom runners in `.pi-webui-runners.json` with a command (default `./`) plus a relative path to the file to run. The same dialog also configures project discovery paths: project-relative directories that are scanned one level deep (no subdirectories) for `.sh`, `.bash`, `.zsh`, `.fish`, and `.py` files plus extensionless files with a bash/sh, zsh, fish, or Python shebang. Python candidates use `uv run` and/or the available `python3`/`python` interpreter. Discovered scripts extend the built-in root, `dev/`, `scripts/`, and `dev/scripts/` detection and run from the resolved project root.
262262
- Extension-launched Guided Git workflow for existing repos and new repos with persistent model/reasoning preferences, review-first staging, an optional manual staged repository-review gate from `aur-review`, generated or typed commit messages, explicit push/PR confirmation, and optional PR worktrees. `/git-guided-workflow` emits a transient tab-scoped activation request that the browser validates and consumes before generic status rendering; a named prompt-only direct-launch fallback remains temporarily available only when that command is absent. The HTTP server authoritatively rejects the exact command and fallback admission while the tab is streaming, compacting, has pending messages, or already has a pending Guided Git launch, bypassing generic compaction queueing. A browser-generated UUID is stored in one short-lived server record and added only to the trusted live event envelope; the canonical four-field extension payload remains unchanged, and only the browser holding the matching one-shot permit can consume the event. When the review extension is loaded/enabled, both `git add .` and accepting the current staged set send `/aur-review start --scope staged --origin guided-git` to the same tab. Only its matching approval advances to message generation. The browser and server recheck the approved domain-separated staged-content hash before message generation, commit, and PR-worktree transfer; drift, missing hashes, or hash-check errors return to Stage and require a new review. A decline returns to staging and rejects an unchanged declined staged hash until corrected content is restaged. Guided Git never stages, commits, or pushes remediation automatically. The extension remains the decision authority; direct API callers are not granted an approval by this browser workflow.
263263
- Browser support for Pi extension UI prompts, widgets, status updates, `/btw` side-question output widgets with optional context transfer/live steering, browser notifications when a tab needs an extension UI response, and an optional side-panel toggle for agent-done notifications.

pi-package-webui/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Check the server at any time with:
5959
### Your first session
6060

6161
1. Start the Web UI with `/webui-start`.
62-
2. Confirm the working directory in the footer or choose another project.
62+
2. Confirm the working directory in the footer or choose another project. You can paste or type a full path in the picker; on Windows, use a rooted drive path such as `C:/` or `D:/project`.
6363
3. Select a model and thinking effort.
6464
4. Enter a prompt, attach context if needed, and follow the live response. The prompt grows to a bounded height, then scrolls vertically so every line stays reachable.
6565
5. Open the Control Deck for sessions, Git, app runners, themes, updates, and optional features. Under **Controls → Interface**, choose **Top bar** or **Sidebar** for terminal tabs, then choose **Right**, **Left**, or **Both** for the Control Deck. In **Sidebar**, **Both** is unavailable; choosing **Right** or **Left** swaps the Control Deck and terminal/tabs rails across the chat.

pi-package-webui/TECHNICAL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ A short interruption is possible during restart; zero downtime is not promised.
8989

9090
## Session continuity
9191

92+
The working-directory picker accepts a direct path for first, new, and split terminals and for cwd changes. On Windows, enter a rooted drive path such as `C:/` or `D:/project` to switch drives. Web UI loads and validates that directory before it can be selected; an invalid or unavailable path remains in the field so you can correct it.
93+
9294
When tabs span multiple working directories, a directory with one tab stays a normal, non-dropdown tab. Hover over it—or focus controls within it—to reveal **+ Tab** directly below, matching the placement used by grouped terminal menus without repeating the current tab. Directories with multiple tabs continue to use the grouped terminal menu.
9395

9496
Restarting only the Web UI normally keeps managed Pi tabs, working folders, saved sessions, and active work connected to the replacement server.

pi-package-webui/public/app.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,8 @@ const elements = {
541541
pathPickerTitle: $("#pathPickerTitle"),
542542
pathPickerCurrent: $("#pathPickerCurrent"),
543543
pathPickerAddFastPickButton: $("#pathPickerAddFastPickButton"),
544+
pathPickerPathInput: $("#pathPickerPathInput"),
545+
pathPickerPathButton: $("#pathPickerPathButton"),
544546
pathPickerCreateNameInput: $("#pathPickerCreateNameInput"),
545547
pathPickerCreateButton: $("#pathPickerCreateButton"),
546548
pathPickerSearchInput: $("#pathPickerSearchInput"),
@@ -24390,6 +24392,22 @@ function setPathPickerError(message) {
2439024392
elements.pathPickerError.hidden = !message;
2439124393
}
2439224394

24395+
function pathPickerEnteredPath() {
24396+
return elements.pathPickerPathInput.value.trim();
24397+
}
24398+
24399+
function updatePathPickerPathControls() {
24400+
const loading = !!pathPickerState?.loading;
24401+
elements.pathPickerPathInput.disabled = !pathPickerState || loading;
24402+
elements.pathPickerPathButton.disabled = !pathPickerState || loading || !pathPickerEnteredPath();
24403+
}
24404+
24405+
async function openPathPickerEnteredPath() {
24406+
const cwd = pathPickerEnteredPath();
24407+
if (!cwd || pathPickerState?.loading) return;
24408+
await loadPathPickerDirectory(cwd);
24409+
}
24410+
2439324411
function pathPickerCreateName() {
2439424412
return elements.pathPickerCreateNameInput.value.trim();
2439524413
}
@@ -24612,9 +24630,11 @@ function renderPathPicker(data) {
2461224630
elements.pathPickerCurrent.title = data.cwd;
2461324631
elements.pathPickerChooseButton.disabled = false;
2461424632
elements.pathPickerChooseButton.textContent = "Use this directory";
24633+
elements.pathPickerPathInput.value = "";
2461524634
elements.pathPickerCreateNameInput.value = "";
2461624635
elements.pathPickerSearchInput.value = "";
2461724636
setPathPickerError(data.truncated ? "Showing the first 500 directories." : "");
24637+
updatePathPickerPathControls();
2461824638
updateCreateDirectoryControls();
2461924639
renderFastPicks();
2462024640

@@ -24637,6 +24657,7 @@ async function loadPathPickerDirectory(cwd) {
2463724657
elements.pathPickerChooseButton.disabled = true;
2463824658
elements.pathPickerCurrent.textContent = "Loading…";
2463924659
elements.pathPickerSearchStatus.textContent = "";
24660+
updatePathPickerPathControls();
2464024661
updateCreateDirectoryControls();
2464124662
updatePathPickerSearchControls();
2464224663
setPathPickerError("");
@@ -24652,6 +24673,7 @@ async function loadPathPickerDirectory(cwd) {
2465224673
elements.pathPickerChooseButton.disabled = false;
2465324674
elements.pathPickerCurrent.textContent = pathPickerState.cwd || "Unable to load directory";
2465424675
setPathPickerError(error.message);
24676+
updatePathPickerPathControls();
2465524677
updateCreateDirectoryControls();
2465624678
updatePathPickerSearchControls();
2465724679
updateAddFastPickButton();
@@ -24711,6 +24733,7 @@ function pickCwd(tab, initialCwd, { title } = {}) {
2471124733
pathPickerState = { tabId: pickerTab.id, cwd: initialCwd, requestId: 0, loading: false, creatingDirectory: false, directories: [], filteredDirectories: [], resolve };
2471224734
elements.pathPickerTitle.textContent = title || `Choose CWD for ${pickerTab.title}`;
2471324735
elements.pathPickerCurrent.textContent = "Loading…";
24736+
elements.pathPickerPathInput.value = "";
2471424737
elements.pathPickerCreateNameInput.value = "";
2471524738
elements.pathPickerSearchInput.value = "";
2471624739
elements.pathPickerSearchStatus.textContent = "";
@@ -24720,6 +24743,7 @@ function pickCwd(tab, initialCwd, { title } = {}) {
2472024743
setPathPickerError("");
2472124744
elements.pathPickerAddFastPickButton.disabled = true;
2472224745
elements.pathPickerChooseButton.disabled = true;
24746+
updatePathPickerPathControls();
2472324747
updateCreateDirectoryControls();
2472424748
updatePathPickerSearchControls();
2472524749
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
@@ -51046,6 +51070,13 @@ elements.fileSelectionCommentInput?.addEventListener("keydown", (event) => {
5104651070
event.preventDefault();
5104751071
sendFileSelectionToSession();
5104851072
});
51073+
elements.pathPickerPathInput.addEventListener("input", updatePathPickerPathControls);
51074+
elements.pathPickerPathInput.addEventListener("keydown", (event) => {
51075+
if (event.key !== "Enter") return;
51076+
event.preventDefault();
51077+
openPathPickerEnteredPath().catch((error) => addEvent(error.message, "error"));
51078+
});
51079+
elements.pathPickerPathButton.addEventListener("click", () => openPathPickerEnteredPath().catch((error) => addEvent(error.message, "error")));
5104951080
elements.pathPickerCreateNameInput.addEventListener("input", updateCreateDirectoryControls);
5105051081
elements.pathPickerCreateNameInput.addEventListener("keydown", (event) => {
5105151082
if (event.key !== "Enter") return;

pi-package-webui/public/index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<link rel="manifest" href="/manifest.webmanifest" />
1313
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
1414
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
15-
<link rel="stylesheet" href="/styles.css?v=142" />
15+
<link rel="stylesheet" href="/styles.css?v=143" />
1616
<script>
1717
// Public-only issue-bot runtime configuration. Keep disabled unless the gateway,
1818
// Turnstile site key, exact CORS origin, and private-report destination are approved.
@@ -1677,6 +1677,10 @@ <h2 id="pathPickerTitle">Choose working directory</h2>
16771677
<p id="pathPickerCurrent" class="path-picker-current"></p>
16781678
<button id="pathPickerAddFastPickButton" type="button" class="path-picker-add-fast-pick">Pin this folder</button>
16791679
</div>
1680+
<div class="path-picker-path-row" aria-label="Open a directory by path">
1681+
<input id="pathPickerPathInput" class="path-picker-path-input" type="text" placeholder="Path, e.g. C:/ or D:/project" autocomplete="off" spellcheck="false" aria-label="Directory path" />
1682+
<button id="pathPickerPathButton" type="button" class="path-picker-path-button">Go</button>
1683+
</div>
16801684
<div id="pathPickerFastPicks" class="path-picker-fast-picks" aria-label="Pinned folders"></div>
16811685
<div id="pathPickerRoots" class="path-picker-roots"></div>
16821686
<div class="path-picker-create-row" aria-label="Create a new directory in the current directory">
@@ -1885,7 +1889,7 @@ <h2 id="subagentCancelDialogTitle">Cancel subagent</h2>
18851889
</form>
18861890
</dialog>
18871891

1888-
<script id="webuiBootLoader" data-app-src="/app.js?v=172">
1892+
<script id="webuiBootLoader" data-app-src="/app.js?v=173">
18891893
(() => {
18901894
"use strict";
18911895

pi-package-webui/public/service-worker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const CACHE_NAME = "pi-webui-pwa-v139";
1+
const CACHE_NAME = "pi-webui-pwa-v140";
22
const APP_SHELL = [
33
"/",
44
"/index.html",

pi-package-webui/public/styles.css

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14672,12 +14672,19 @@ body.subagent-terminal-active .composer-remote-mic-consent {
1467214672
color: var(--ctp-teal);
1467314673
border-color: rgba(148, 226, 213, 0.30);
1467414674
}
14675+
.path-picker-path-row,
1467514676
.path-picker-create-row {
1467614677
display: grid;
1467714678
grid-template-columns: minmax(0, 1fr) auto;
1467814679
gap: 0.45rem;
14680+
}
14681+
.path-picker-path-row {
14682+
margin: 0.72rem 0 0;
14683+
}
14684+
.path-picker-create-row {
1467914685
margin: 0 0 0.72rem;
1468014686
}
14687+
.path-picker-path-input,
1468114688
.path-picker-create-input {
1468214689
min-width: 0;
1468314690
width: 100%;
@@ -14686,11 +14693,14 @@ body.subagent-terminal-active .composer-remote-mic-consent {
1468614693
border-color: rgba(137, 180, 250, 0.20);
1468714694
background: rgba(var(--ctp-surface-rgb), 0.38);
1468814695
}
14696+
.path-picker-path-button,
1468914697
.path-picker-create-button {
1469014698
align-self: stretch;
1469114699
color: var(--ctp-blue);
1469214700
border-color: rgba(137, 180, 250, 0.32);
1469314701
}
14702+
.path-picker-path-button:hover,
14703+
.path-picker-path-button:focus-visible,
1469414704
.path-picker-create-button:hover,
1469514705
.path-picker-create-button:focus-visible {
1469614706
color: #11111b;
@@ -14701,7 +14711,7 @@ body.subagent-terminal-active .composer-remote-mic-consent {
1470114711
display: grid;
1470214712
grid-template-columns: minmax(0, 1fr) auto;
1470314713
gap: 0.45rem;
14704-
margin: 0 0 0.38rem;
14714+
margin: 0.72rem 0 0.38rem;
1470514715
}
1470614716
.path-picker-search-input {
1470714717
min-width: 0;
@@ -16307,9 +16317,11 @@ button.git-workflow-step { min-height: 1.9rem; }
1630716317
.app-runner-stdin-send,
1630816318
.app-runner-stdin-eof { width: 100%; }
1630916319
.path-picker-current-row,
16320+
.path-picker-path-row,
1631016321
.path-picker-create-row,
1631116322
.path-picker-search-row { grid-template-columns: 1fr; }
1631216323
.path-picker-add-fast-pick,
16324+
.path-picker-path-button,
1631316325
.path-picker-create-button,
1631416326
.path-picker-clear-search-button { width: 100%; }
1631516327
.path-picker-fast-pick { grid-template-columns: minmax(0, 1fr) 44px; }

pi-package-webui/tests/boot-failure-diagnostics.test.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function makeEnvironment({
6565
reload: new FakeElement(),
6666
copyStatus: new FakeElement(),
6767
};
68-
const styleLink = new FakeElement({ href: "http://127.0.0.1:31415/styles.css?v=142", sheet: stylesheetLoaded ? {} : null });
68+
const styleLink = new FakeElement({ href: "http://127.0.0.1:31415/styles.css?v=143", sheet: stylesheetLoaded ? {} : null });
6969
const listeners = new Map();
7070
const timers = new Map();
7171
let timerId = 0;
@@ -84,7 +84,7 @@ function makeEnvironment({
8484
removeEventListener(name) { listeners.delete(name); },
8585
};
8686
const document = {
87-
currentScript: new FakeElement({ dataset: { appSrc: "/app.js?v=172" } }),
87+
currentScript: new FakeElement({ dataset: { appSrc: "/app.js?v=173" } }),
8888
readyState: "complete",
8989
querySelector(selector) {
9090
return {
@@ -232,7 +232,7 @@ for (const moduleName of startupModuleNames) {
232232
assert.ok(serviceWorker.includes(`"/${moduleName}"`), `the PWA app shell should include every app.js startup module: ${moduleName}`);
233233
}
234234
assert.match(serviceWorker, /pi-webui-pwa-v\d+/, "the app-shell cache should retain a versioned identity");
235-
assert.ok(html.indexOf('id="webuiBootLoader"') < html.indexOf("/app.js?v=172"), "the inline loader should own app module startup");
235+
assert.ok(html.indexOf('id="webuiBootLoader"') < html.indexOf("/app.js?v=173"), "the inline loader should own app module startup");
236236
assert.doesNotMatch(html, /<script type="module" src="\/app\.js/, "the app module should not bypass the guarded loader");
237237

238238
console.log("boot-failure-diagnostics.test.mjs passed");

pi-package-webui/tests/codex-fast-mode-static.test.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ assert.match(app, /statusKey === CODEX_FAST_MODE_STATUS_KEY[\s\S]*?applyCodexFas
5555
assert.match(technical, /## Normal and compact output[\s\S]*?select \*\*Compact\*\*[\s\S]*?`compact-v1`/, "technical reference should document Compact mode with the stable identifier");
5656
assert.match(development, /### Codex subscription Fast mode[\s\S]*?service_tier: "priority"[\s\S]*?1\.5× faster[\s\S]*?2× Standard credits[\s\S]*?2\.5×/, "development guide should document Fast-mode integration and cost semantics");
5757
// Intent preserved: browser-asset changes must advance the coherent cache tuple.
58-
assert.match(serviceWorker, /pi-webui-pwa-v139/, "PWA cache identity should be bumped for changed browser assets");
59-
assert.match(html, /data-app-src="\/app\.js\?v=172"/, "browser module URL should be cache-busted for Fast-mode wiring");
58+
assert.match(serviceWorker, /pi-webui-pwa-v140/, "PWA cache identity should be bumped for changed browser assets");
59+
assert.match(html, /data-app-src="\/app\.js\?v=173"/, "browser module URL should be cache-busted for Fast-mode wiring");
6060

6161
console.log("codex-fast-mode-static.test.mjs passed");

pi-package-webui/tests/composer-action-grid-reorder-static.test.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ assert.match(app, /function initializeComposerActionOrdering\(\)[\s\S]*MutationO
5757
assert.match(app, /event\.key === COMPOSER_ACTION_ORDER_STORAGE_KEY\) restoreComposerActionOrder\(\)[\s\S]*event\.key === COMPOSER_ACTION_LAYOUT_STORAGE_KEY\) restoreComposerActionSlotLayout\(\)/, "other tabs should apply persisted composer order and sparse-layout changes");
5858
assert.match(app, /initializeComposerActionOrdering\(\);[\s\S]*restoreSidePanelSectionOrder\(\);/, "composer ordering should initialize during guarded app startup");
5959

60-
assert.match(html, /styles\.css\?v=142/, "changed composer styles should advance the stylesheet revision");
61-
assert.match(html, /app\.js\?v=172/, "changed composer behavior should advance the app revision");
62-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v139"/, "changed browser assets should advance the PWA cache identity");
60+
assert.match(html, /styles\.css\?v=143/, "changed composer styles should advance the stylesheet revision");
61+
assert.match(html, /app\.js\?v=173/, "changed composer behavior should advance the app revision");
62+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v140"/, "changed browser assets should advance the PWA cache identity");
6363

6464
console.log("composer-action-grid-reorder-static.test.mjs passed");

0 commit comments

Comments
 (0)