Reviewed PR and linked issue.
UnderstandResolved merge conflict and applied adjustments.
EditRan tests and confirmed success.
VerifyCommitted changes and merged to main.
EditPushed changes, closed PR, and cleaned up branch.
CleanupYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/816 pull this into a local branch so i can test it
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
The user wants me to review a GitHub PR and then pull it into a local branch for testing. Let me start by reading the PR details.
>gh pr view 816 --json title,body,comments,labels,state,author,commits,files,headRefName,baseRefName,mergeable,additions,deletions{"additions":529,"author":{"id":"MDQ6VXNlcjIwNDczOTg=","is_bot":false,"login":"w-winter","name":"warren"},"baseRefName":"main","body":"The `/resume` picker is already a capable session discovery tool (search, scope toggling, sort modes), so it's a natural place to surface `.jsonl` paths for disambiguation and to offer in-UI deletion for keeping results uncluttered; see [#813](https://github.com/badlogic/pi-mono/issues/813) for the full motivation. This change adds both, and also fixes latent scope-loading races and missing error handling that became apparent during implementation.\r\n\r\nChanges:\r\n- adds `Ctrl+P` to toggle showing the session `.jsonl` file path in the list\r\n- adds session deletion via `Ctrl+D` with inline confirmation\r\n - uses `trash` when available, falls back to `unlink`\r\n - blocks deletion of the currently active session\r\n - includes trash stderr info in the error message if both trash + unlink fail\r\n- improves `/resume` scope switching reliability:\r\n - prevents redundant concurrent “All sessions” loads\r\n - ignores stale “All sessions” load results when toggling back to “Current”\r\n - adds `.catch(...)` handling so loader failures don’t leave the UI stuck in “loading”\r\n- improves `/resume` header UX:\r\n - status messages auto-hide and force a re-render\r\n - hint line is split into two lines (each width-truncated independently) to keep search discoverability without hint clutter\r\n- adds `packages/coding-agent/test/session-selector-path-delete.test.ts` covering:\r\n - delete confirmation behavior (`Ctrl+D`)\r\n - `Ctrl+Backspace` doesn't delete when search query is non-empty\r\n - scope load race protection (All resolves after switching back to Current)\r\n - redundant All-load suppression while an All load is inflight\r\n\r\nFiles:\r\n- `packages/coding-agent/src/modes/interactive/components/session-selector.ts`\r\n- `packages/coding-agent/test/session-selector-path-delete.test.ts` (new)\r\n- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`\r\n- `packages/coding-agent/CHANGELOG.md`\r\n- `packages/coding-agent/docs/session.md`\r\n- `packages/coding-agent/README.md`\r\n\r\nManual testing (macOS; iTerm2 and Terminal):\r\n- `/resume`: `Ctrl+P` toggles paths; layout stays stable on narrow terminals\r\n- `/resume`: `Ctrl+D` → confirm/cancel; deleted sessions are removed from the list\r\n- `/resume`: deletion of active session is blocked with an error status\r\n- `/resume`: toggling Tab rapidly while “All” is loading does not flip scope back unexpectedly\r\n- `/resume`: `Ctrl+Backspace` → confirm/cancel *only* when the search query is empty; deleted sessions are removed from the list\r\n\r\nUndocumented behavior:\r\n- I added an additional convenience binding: `Ctrl+Backspace` enters delete-confirmation only when the search query is empty (it's forwarded to the input when the query is non-empty)\r\n - Let me know if you'd rather not have this and I'll remove it\r\n - If you want to keep it, let me know if you'd like it briefly documented (e.g. in `docs/session.md`)\r\n\r\nScreenshots:\r\n<img width=\"450\" alt=\"Screenshot 2026-01-17 at 8 50 48 PM\" src=\"https://github.com/user-attachments/assets/c5755574-e744-4015-90cd-428d1ccf19f6\" />\r\n^ new hint lines layout\r\n<img width=\"450\" alt=\"Screenshot 2026-01-17 at 8 51 23 PM\" src=\"https://github.com/user-attachments/assets/99cb749c-098a-413a-98d1-30fee49a34e2\" />\r\n^ paths toggled on: \r\n<img width=\"450\" alt=\"Screenshot 2026-01-17 at 8 51 04 PM\" src=\"https://github.com/user-attachments/assets/8d332bfe-8782-4cf3-bba5-4d9cc2a55222\" />\r\n^ delete session confirmation after Ctrl+D with non-empty search query: \r\n<img width=\"450\" alt=\"Screenshot 2026-01-17 at 8 52 09 PM\" src=\"https://github.com/user-attachments/assets/d4032e2a-b158-4698-8441-d8aeb9107edb\" />\r\n^ blocked attempt to delete the current active session: \r\n<img width=\"450\" alt=\"Screenshot 2026-01-17 at 8 51 33 PM\" src=\"https://github.com/user-attachments/assets/4045633e-8b69-404d-90a5-e442ac68adee\" />\r\n^ delete session confirmation after Ctrl+Backspace or Ctrl+D with empty search query (and paths toggled on): ","comments":[],"commits":[{"authoredDate":"2026-01-18T01:29:16Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjIwNDczOTg=","login":"w-winter","name":"warren"}],"committedDate":"2026-01-18T17:48:50Z","messageBody":"","messageHeadline":"feat(coding-agent): add session path toggle and deletion to /resume","oid":"a81aeee88ce55adae27b2fde2952f7875da5d288"},{"authoredDate":"2026-01-18T02:07:40Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjIwNDczOTg=","login":"w-winter","name":"warren"}],"committedDate":"2026-01-18T17:48:54Z","messageBody":"","messageHeadline":"fix(coding-agent): init theme in session selector tests","oid":"41ca1eacb50faa9eb4569cb24d0efe28efc4022a"}],"deletions":51,"files":[{"path":"packages/coding-agent/CHANGELOG.md","additions":5,"deletions":0},{"path":"packages/coding-agent/README.md","additions":4,"deletions":0},{"path":"packages/coding-agent/docs/session.md","additions":6,"deletions":0},{"path":"packages/coding-agent/src/modes/interactive/components/session-selector.ts","additions":328,"deletions":51},{"path":"packages/coding-agent/src/modes/interactive/interactive-mode.ts","additions":1,"deletions":0},{"path":"packages/coding-agent/test/session-selector-path-delete.test.ts","additions":185,"deletions":0}],"headRefName":"resume-additions","labels":[],"mergeable":"CONFLICTING","state":"OPEN","title":"feat(coding-agent): add session path toggle and deletion to /resume"}>gh pr diff 816diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index dbc607396..2cbd1bffa 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -2,6 +2,11 @@
## [Unreleased]
+### Added
+
+- Session selector (`/resume`) now supports path display toggle (`Ctrl+P`) to show/hide `.jsonl` file paths for each session
+- Session selector (`/resume`) now supports session deletion (`Ctrl+D`) with inline confirmation. Uses `trash` command if available, falls back to permanent deletion. Prevents deletion of the currently active session
+
### Changed
- Share URLs now use hash fragments (`#`) instead of query strings (`?`) to prevent session IDs from being sent to buildwithpi.ai ([#828](https://github.com/badlogic/pi-mono/issues/828))
diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md
index 7ad4e94c4..e18d01fd3 100644
--- a/packages/coding-agent/README.md
+++ b/packages/coding-agent/README.md
@@ -541,6 +541,10 @@ pi --session /path/to/file.jsonl # Use specific session file
pi --session a8ec1c2a # Resume by session ID (partial UUID)
```
+In the `/resume` picker:
+- `Ctrl+P` toggles display of the session `.jsonl` file path
+- `Ctrl+D` deletes the selected session (inline confirmation; uses `trash` if available and cannot delete the active session)
+
**Resuming by session ID:** The `--session` flag accepts a session UUID (or prefix). Session IDs are visible in filenames under `~/.pi/agent/sessions/<project>/` (e.g., `2025-12-13T17-47-46-817Z_a8ec1c2a-5a5f-4699-88cb-03e7d3cb9292.jsonl`). The UUID is the part after the underscore. You can also search by session ID in the `pi -r` picker.
### Context Compaction
diff --git a/packages/coding-agent/docs/session.md b/packages/coding-agent/docs/session.md
index 281fec48b..10d0a2686 100644
--- a/packages/coding-agent/docs/session.md
+++ b/packages/coding-agent/docs/session.md
@@ -10,6 +10,12 @@ Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with
Where `<path>` is the working directory with `/` replaced by `-`.
+## Deleting Sessions
+
+Sessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.
+
+Pi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.
+
## Session Version
Sessions have a version field in the header:
diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts
index 918e3dade..6e67ac495 100644
--- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts
+++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts
@@ -1,3 +1,6 @@
+import { spawnSync } from "node:child_process";
+import { existsSync } from "node:fs";
+import { unlink } from "node:fs/promises";
import * as os from "node:os";
import {
type Component,
@@ -45,12 +48,18 @@ function formatSessionDate(date: Date): string {
class SessionSelectorHeader implements Component {
private scope: SessionScope;
private sortMode: SortMode;
+ private requestRender: () => void;
private loading = false;
private loadProgress: { loaded: number; total: number } | null = null;
+ private showPath = false;
+ private confirmingDeletePath: string | null = null;
+ private statusMessage: { type: "info" | "error"; message: string } | null = null;
+ private statusTimeout: ReturnType<typeof setTimeout> | null = null;
- constructor(scope: SessionScope, sortMode: SortMode) {
+ constructor(scope: SessionScope, sortMode: SortMode, requestRender: () => void) {
this.scope = scope;
this.sortMode = sortMode;
+ this.requestRender = requestRender;
}
setScope(scope: SessionScope): void {
@@ -63,15 +72,40 @@ class SessionSelectorHeader implements Component {
setLoading(loading: boolean): void {
this.loading = loading;
- if (!loading) {
- this.loadProgress = null;
- }
+ // Progress is scoped to the current load; clear whenever the loading state is set
+ this.loadProgress = null;
}
setProgress(loaded: number, total: number): void {
this.loadProgress = { loaded, total };
}
+ setShowPath(showPath: boolean): void {
+ this.showPath = showPath;
+ }
+
+ setConfirmingDeletePath(path: string | null): void {
+ this.confirmingDeletePath = path;
+ }
+
+ private clearStatusTimeout(): void {
+ if (!this.statusTimeout) return;
+ clearTimeout(this.statusTimeout);
+ this.statusTimeout = null;
+ }
+
+ setStatusMessage(msg: { type: "info" | "error"; message: string } | null, autoHideMs?: number): void {
+ this.clearStatusTimeout();
+ this.statusMessage = msg;
+ if (!msg || !autoHideMs) return;
+
+ this.statusTimeout = setTimeout(() => {
+ this.statusMessage = null;
+ this.statusTimeout = null;
+ this.requestRender();
+ }, autoHideMs);
+ }
+
invalidate(): void {}
render(width: number): string[] {
@@ -85,21 +119,37 @@ class SessionSelectorHeader implements Component {
if (this.loading) {
const progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : "...";
scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", `Loading ${progressText}`)}`;
+ } else if (this.scope === "current") {
+ scopeText = `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`;
} else {
- scopeText =
- this.scope === "current"
- ? `${theme.fg("accent", "◉ Current Folder")}${theme.fg("muted", " | ○ All")}`
- : `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
+ scopeText = `${theme.fg("muted", "○ Current Folder | ")}${theme.fg("accent", "◉ All")}`;
}
const rightText = truncateToWidth(`${scopeText} ${sortText}`, width, "");
const availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);
const left = truncateToWidth(leftText, availableLeft, "");
const spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));
- const hintText = 'Tab: scope · Ctrl+R: sort · re:<pattern> for regex · "phrase" for exact phrase';
- const truncatedHint = truncateToWidth(hintText, width, "…");
- const hint = theme.fg("muted", truncatedHint);
- return [`${left}${" ".repeat(spacing)}${rightText}`, hint];
+
+ // Build hint lines - changes based on state (all branches truncate to width)
+ let hintLine1: string;
+ let hintLine2: string;
+ if (this.confirmingDeletePath !== null) {
+ const confirmHint = "Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel";
+ hintLine1 = theme.fg("error", truncateToWidth(confirmHint, width, "…"));
+ hintLine2 = "";
+ } else if (this.statusMessage) {
+ const color = this.statusMessage.type === "error" ? "error" : "accent";
+ hintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, "…"));
+ hintLine2 = "";
+ } else {
+ const pathState = this.showPath ? "(on)" : "(off)";
+ const hint1 = `Tab: scope · re:<pattern> for regex · "phrase" for exact phrase`;
+ const hint2 = `Ctrl+R: sort · Ctrl+D: delete · Ctrl+P: path ${pathState}`;
+ hintLine1 = theme.fg("muted", truncateToWidth(hint1, width, "…"));
+ hintLine2 = theme.fg("muted", truncateToWidth(hint2, width, "…"));
+ }
+
+ return [`${left}${" ".repeat(spacing)}${rightText}`, hintLine1, hintLine2];
}
}
@@ -113,12 +163,19 @@ class SessionList implements Component, Focusable {
private searchInput: Input;
private showCwd = false;
private sortMode: SortMode = "relevance";
+ private showPath = false;
+ private confirmingDeletePath: string | null = null;
+ private currentSessionFilePath?: string;
public onSelect?: (sessionPath: string) => void;
public onCancel?: () => void;
public onExit: () => void = () => {};
public onToggleScope?: () => void;
public onToggleSort?: () => void;
- private maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
+ public onTogglePath?: (showPath: boolean) => void;
+ public onDeleteConfirmationChange?: (path: string | null) => void;
+ public onDeleteSession?: (sessionPath: string) => Promise<void>;
+ public onError?: (message: string) => void;
+ private maxVisible: number = 5; // Max sessions visible (each session: message + metadata + optional path + blank)
// Focusable implementation - propagate to searchInput for IME cursor positioning
private _focused = false;
@@ -130,12 +187,13 @@ class SessionList implements Component, Focusable {
this.searchInput.focused = value;
}
- constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode) {
+ constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {
this.allSessions = sessions;
this.filteredSessions = sessions;
this.searchInput = new Input();
this.showCwd = showCwd;
this.sortMode = sortMode;
+ this.currentSessionFilePath = currentSessionFilePath;
// Handle Enter in search input - select current item
this.searchInput.onSubmit = () => {
@@ -164,6 +222,24 @@ class SessionList implements Component, Focusable {
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
}
+ private setConfirmingDeletePath(path: string | null): void {
+ this.confirmingDeletePath = path;
+ this.onDeleteConfirmationChange?.(path);
+ }
+
+ private startDeleteConfirmationForSelectedSession(): void {
+ const selected = this.filteredSessions[this.selectedIndex];
+ if (!selected) return;
+
+ // Prevent deleting current session
+ if (this.currentSessionFilePath && selected.path === this.currentSessionFilePath) {
+ this.onError?.("Cannot delete the currently active session");
+ return;
+ }
+
+ this.setConfirmingDeletePath(selected.path);
+ }
+
invalidate(): void {}
render(width: number): string[] {
@@ -196,10 +272,11 @@ class SessionList implements Component, Focusable {
);
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
- // Render visible sessions (2 lines per session + blank line)
+ // Render visible sessions (message + metadata + optional path + blank line)
for (let i = startIndex; i < endIndex; i++) {
const session = this.filteredSessions[i];
const isSelected = i === this.selectedIndex;
+ const isConfirmingDelete = session.path === this.confirmingDeletePath;
// Use session name if set, otherwise first message
const hasName = !!session.name;
@@ -211,10 +288,13 @@ class SessionList implements Component, Focusable {
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
- let styledMsg = truncatedMsg;
- if (hasName) {
- styledMsg = theme.fg("warning", truncatedMsg);
+ let messageColor: "error" | "warning" | null = null;
+ if (isConfirmingDelete) {
+ messageColor = "error";
+ } else if (hasName) {
+ messageColor = "warning";
}
+ let styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;
if (isSelected) {
styledMsg = theme.bold(styledMsg);
}
@@ -228,10 +308,20 @@ class SessionList implements Component, Focusable {
metadataParts.push(shortenPath(session.cwd));
}
const metadata = ` ${metadataParts.join(" · ")}`;
- const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, ""));
+ const truncatedMetadata = truncateToWidth(metadata, width, "");
+ const metadataLine = theme.fg(isConfirmingDelete ? "error" : "dim", truncatedMetadata);
lines.push(messageLine);
lines.push(metadataLine);
+
+ // Optional third line: file path (when showPath is enabled)
+ if (this.showPath) {
+ const pathText = ` ${shortenPath(session.path)}`;
+ const truncatedPath = truncateToWidth(pathText, width, "…");
+ const pathLine = theme.fg(isConfirmingDelete ? "error" : "muted", truncatedPath);
+ lines.push(pathLine);
+ }
+
lines.push(""); // Blank line between sessions
}
@@ -247,6 +337,24 @@ class SessionList implements Component, Focusable {
handleInput(keyData: string): void {
const kb = getEditorKeybindings();
+
+ // Handle delete confirmation state first - intercept all keys
+ if (this.confirmingDeletePath !== null) {
+ if (kb.matches(keyData, "selectConfirm")) {
+ const pathToDelete = this.confirmingDeletePath;
+ this.setConfirmingDeletePath(null);
+ void this.onDeleteSession?.(pathToDelete);
+ return;
+ }
+ // Allow both Escape and Ctrl+C to cancel (consistent with pi UX)
+ if (kb.matches(keyData, "selectCancel") || matchesKey(keyData, "ctrl+c")) {
+ this.setConfirmingDeletePath(null);
+ return;
+ }
+ // Ignore all other keys while confirming
+ return;
+ }
+
if (kb.matches(keyData, "tab")) {
if (this.onToggleScope) {
this.onToggleScope();
@@ -259,6 +367,32 @@ class SessionList implements Component, Focusable {
return;
}
+ // Ctrl+P: toggle path display
+ if (matchesKey(keyData, "ctrl+p")) {
+ this.showPath = !this.showPath;
+ this.onTogglePath?.(this.showPath);
+ return;
+ }
+
+ // Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)
+ if (matchesKey(keyData, "ctrl+d")) {
+ this.startDeleteConfirmationForSelectedSession();
+ return;
+ }
+
+ // Ctrl+Backspace: non-invasive convenience alias for delete
+ // Only triggers deletion when the query is empty; otherwise it is forwarded to the input
+ if (matchesKey(keyData, "ctrl+backspace")) {
+ if (this.searchInput.getValue().length > 0) {
+ this.searchInput.handleInput(keyData);
+ this.filterSessions(this.searchInput.getValue());
+ return;
+ }
+
+ this.startDeleteConfirmationForSelectedSession();
+ return;
+ }
+
// Up arrow
if (kb.matches(keyData, "selectUp")) {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
@@ -298,6 +432,46 @@ class SessionList implements Component, Focusable {
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
+/**
+ * Delete a session file, trying the `trash` CLI first, then falling back to unlink
+ */
+async function deleteSessionFile(
+ sessionPath: string,
+): Promise<{ ok: boolean; method: "trash" | "unlink"; error?: string }> {
+ // Try `trash` first (if installed)
+ const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
+ const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
+
+ const getTrashErrorHint = (): string | null => {
+ const parts: string[] = [];
+ if (trashResult.error) {
+ parts.push(trashResult.error.message);
+ }
+ const stderr = trashResult.stderr?.trim();
+ if (stderr) {
+ parts.push(stderr.split("\n")[0] ?? stderr);
+ }
+ if (parts.length === 0) return null;
+ return `trash: ${parts.join(" · ").slice(0, 200)}`;
+ };
+
+ // If trash reports success, or the file is gone afterwards, treat it as successful
+ if (trashResult.status === 0 || !existsSync(sessionPath)) {
+ return { ok: true, method: "trash" };
+ }
+
+ // Fallback to permanent deletion
+ try {
+ await unlink(sessionPath);
+ return { ok: true, method: "unlink" };
+ } catch (err) {
+ const unlinkError = err instanceof Error ? err.message : String(err);
+ const trashErrorHint = getTrashErrorHint();
+ const error = trashErrorHint ? `${unlinkError} (${trashErrorHint})` : unlinkError;
+ return { ok: false, method: "unlink", error };
+ }
+}
+
/**
* Component that renders a session selector
*/
@@ -312,6 +486,9 @@ export class SessionSelectorComponent extends Container implements Focusable {
private allSessionsLoader: SessionsLoader;
private onCancel: () => void;
private requestRender: () => void;
+ private currentLoading = false;
+ private allLoading = false;
+ private allLoadSeq = 0;
// Focusable implementation - propagate to sessionList for IME cursor positioning
private _focused = false;
@@ -330,13 +507,14 @@ export class SessionSelectorComponent extends Container implements Focusable {
onCancel: () => void,
onExit: () => void,
requestRender: () => void,
+ currentSessionFilePath?: string,
) {
super();
this.currentSessionsLoader = currentSessionsLoader;
this.allSessionsLoader = allSessionsLoader;
this.onCancel = onCancel;
this.requestRender = requestRender;
- this.header = new SessionSelectorHeader(this.scope, this.sortMode);
+ this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.requestRender);
// Add header
this.addChild(new Spacer(1));
@@ -346,13 +524,65 @@ export class SessionSelectorComponent extends Container implements Focusable {
this.addChild(new Spacer(1));
// Create session list (starts empty, will be populated after load)
- this.sessionList = new SessionList([], false, this.sortMode);
- this.sessionList.onSelect = onSelect;
- this.sessionList.onCancel = onCancel;
- this.sessionList.onExit = onExit;
+ this.sessionList = new SessionList([], false, this.sortMode, currentSessionFilePath);
+
+ // Ensure header status timeouts are cleared when leaving the selector
+ const clearStatusMessage = () => this.header.setStatusMessage(null);
+ this.sessionList.onSelect = (sessionPath) => {
+ clearStatusMessage();
+ onSelect(sessionPath);
+ };
+ this.sessionList.onCancel = () => {
+ clearStatusMessage();
+ onCancel();
+ };
+ this.sessionList.onExit = () => {
+ clearStatusMessage();
+ onExit();
+ };
this.sessionList.onToggleScope = () => this.toggleScope();
this.sessionList.onToggleSort = () => this.toggleSortMode();
+ // Sync list events to header
+ this.sessionList.onTogglePath = (showPath) => {
+ this.header.setShowPath(showPath);
+ this.requestRender();
+ };
+ this.sessionList.onDeleteConfirmationChange = (path) => {
+ this.header.setConfirmingDeletePath(path);
+ this.requestRender();
+ };
+ this.sessionList.onError = (msg) => {
+ this.header.setStatusMessage({ type: "error", message: msg }, 3000);
+ this.requestRender();
+ };
+
+ // Handle session deletion
+ this.sessionList.onDeleteSession = async (sessionPath: string) => {
+ const result = await deleteSessionFile(sessionPath);
+
+ if (result.ok) {
+ if (this.currentSessions) {
+ this.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);
+ }
+ if (this.allSessions) {
+ this.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);
+ }
+
+ const sessions = this.scope === "all" ? (this.allSessions ?? []) : (this.currentSessions ?? []);
+ const showCwd = this.scope === "all";
+ this.sessionList.setSessions(sessions, showCwd);
+
+ const msg = result.method === "trash" ? "Session moved to trash" : "Session deleted";
+ this.header.setStatusMessage({ type: "info", message: msg }, 2000);
+ } else {
+ const errorMessage = result.error ?? "Unknown error";
+ this.header.setStatusMessage({ type: "error", message: `Failed to delete: ${errorMessage}` }, 3000);
+ }
+
+ this.requestRender();
+ };
+
this.addChild(this.sessionList);
// Add bottom border
@@ -364,17 +594,37 @@ export class SessionSelectorComponent extends Container implements Focusable {
}
private loadCurrentSessions(): void {
+ this.currentLoading = true;
+ this.header.setScope("current");
this.header.setLoading(true);
this.requestRender();
+
this.currentSessionsLoader((loaded, total) => {
+ if (this.scope !== "current") return;
this.header.setProgress(loaded, total);
this.requestRender();
- }).then((sessions) => {
- this.currentSessions = sessions;
- this.header.setLoading(false);
- this.sessionList.setSessions(sessions, false);
- this.requestRender();
- });
+ })
+ .then((sessions) => {
+ this.currentSessions = sessions;
+ this.currentLoading = false;
+
+ if (this.scope !== "current") return;
+
+ this.header.setLoading(false);
+ this.sessionList.setSessions(sessions, false);
+ this.requestRender();
+ })
+ .catch((error: unknown) => {
+ this.currentLoading = false;
+ const message = error instanceof Error ? error.message : String(error);
+
+ if (this.scope !== "current") return;
+
+ this.header.setLoading(false);
+ this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
+ this.sessionList.setSessions([], false);
+ this.requestRender();
+ });
}
private toggleSortMode(): void {
@@ -386,37 +636,64 @@ export class SessionSelectorComponent extends Container implements Focusable {
private toggleScope(): void {
if (this.scope === "current") {
- // Switching to "all" - load if not already loaded
- if (this.allSessions === null) {
- this.header.setLoading(true);
- this.header.setScope("all");
- this.sessionList.setSessions([], true); // Clear list while loading
+ this.scope = "all";
+ this.header.setScope(this.scope);
+
+ if (this.allSessions !== null) {
+ this.header.setLoading(false);
+ this.sessionList.setSessions(this.allSessions, true);
this.requestRender();
- // Load asynchronously with progress updates
- this.allSessionsLoader((loaded, total) => {
- this.header.setProgress(loaded, total);
- this.requestRender();
- }).then((sessions) => {
+ return;
+ }
+
+ this.header.setLoading(true);
+ this.sessionList.setSessions([], true);
+ this.requestRender();
+
+ if (this.allLoading) return;
+
+ this.allLoading = true;
+ const seq = ++this.allLoadSeq;
+
+ this.allSessionsLoader((loaded, total) => {
+ if (seq !== this.allLoadSeq) return;
+ if (this.scope !== "all") return;
+ this.header.setProgress(loaded, total);
+ this.requestRender();
+ })
+ .then((sessions) => {
this.allSessions = sessions;
+ this.allLoading = false;
+
+ if (seq !== this.allLoadSeq) return;
+ if (this.scope !== "all") return;
+
this.header.setLoading(false);
- this.scope = "all";
- this.sessionList.setSessions(this.allSessions, true);
+ this.sessionList.setSessions(sessions, true);
this.requestRender();
- // If no sessions in All scope either, cancel
- if (this.allSessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {
+
+ if (sessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) {
this.onCancel();
}
+ })
+ .catch((error: unknown) => {
+ this.allLoading = false;
+ const message = error instanceof Error ? error.message : String(error);
+
+ if (seq !== this.allLoadSeq) return;
+ if (this.scope !== "all") return;
+
+ this.header.setLoading(false);
+ this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
+ this.sessionList.setSessions([], true);
+ this.requestRender();
});
- } else {
- this.scope = "all";
- this.sessionList.setSessions(this.allSessions, true);
- this.header.setScope(this.scope);
- }
} else {
- // Switching back to "current"
this.scope = "current";
- this.sessionList.setSessions(this.currentSessions ?? [], false);
this.header.setScope(this.scope);
+ this.header.setLoading(this.currentLoading);
+ this.sessionList.setSessions(this.currentSessions ?? [], false);
+ this.requestRender();
}
}
diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
index 5d923fa03..204322ee1 100644
--- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts
+++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
@@ -3035,6 +3035,7 @@ export class InteractiveMode {
void this.shutdown();
},
() => this.ui.requestRender(),
+ this.sessionManager.getSessionFile(),
);
return { component: selector, focus: selector.getSessionList() };
});
diff --git a/packages/coding-agent/test/session-selector-path-delete.test.ts b/packages/coding-agent/test/session-selector-path-delete.test.ts
new file mode 100644
index 000000000..f1b2e96ac
--- /dev/null
+++ b/packages/coding-agent/test/session-selector-path-delete.test.ts
@@ -0,0 +1,185 @@
+import { beforeAll, describe, expect, it } from "vitest";
+import type { SessionInfo } from "../src/core/session-manager.js";
+import { SessionSelectorComponent } from "../src/modes/interactive/components/session-selector.js";
+import { initTheme } from "../src/modes/interactive/theme/theme.js";
+
+type Deferred<T> = {
+ promise: Promise<T>;
+ resolve: (value: T) => void;
+ reject: (err: unknown) => void;
+};
+
+function createDeferred<T>(): Deferred<T> {
+ let resolve: (value: T) => void = () => {};
+ let reject: (err: unknown) => void = () => {};
+ const promise = new Promise<T>((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ return { promise, resolve, reject };
+}
+
+async function flushPromises(): Promise<void> {
+ await new Promise<void>((resolve) => {
+ setImmediate(resolve);
+ });
+}
+
+function makeSession(overrides: Partial<SessionInfo> & { id: string }): SessionInfo {
+ return {
+ path: overrides.path ?? `/tmp/${overrides.id}.jsonl`,
+ id: overrides.id,
+ cwd: overrides.cwd ?? "",
+ name: overrides.name,
+ created: overrides.created ?? new Date(0),
+ modified: overrides.modified ?? new Date(0),
+ messageCount: overrides.messageCount ?? 1,
+ firstMessage: overrides.firstMessage ?? "hello",
+ allMessagesText: overrides.allMessagesText ?? "hello",
+ };
+}
+
+const CTRL_D = "\x04";
+const CTRL_BACKSPACE = "\x1b[127;5u";
+
+describe("session selector path/delete interactions", () => {
+ beforeAll(() => {
+ // session selector uses the global theme instance
+ initTheme("dark");
+ });
+ it("does not treat Ctrl+Backspace as delete when search query is non-empty", async () => {
+ const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
+
+ const selector = new SessionSelectorComponent(
+ async () => sessions,
+ async () => [],
+ () => {},
+ () => {},
+ () => {},
+ () => {},
+ );
+ await flushPromises();
+
+ const list = selector.getSessionList();
+ const confirmationChanges: Array<string | null> = [];
+ list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
+
+ list.handleInput("a");
+ list.handleInput(CTRL_BACKSPACE);
+
+ expect(confirmationChanges).toEqual([]);
+ });
+
+ it("enters confirmation mode on Ctrl+D even with a non-empty search query", async () => {
+ const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
+
+ const selector = new SessionSelectorComponent(
+ async () => sessions,
+ async () => [],
+ () => {},
+ () => {},
+ () => {},
+ () => {},
+ );
+ await flushPromises();
+
+ const list = selector.getSessionList();
+ const confirmationChanges: Array<string | null> = [];
+ list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
+
+ list.handleInput("a");
+ list.handleInput(CTRL_D);
+
+ expect(confirmationChanges).toEqual([sessions[0]!.path]);
+ });
+
+ it("enters confirmation mode on Ctrl+Backspace when search query is empty", async () => {
+ const sessions = [makeSession({ id: "a" }), makeSession({ id: "b" })];
+
+ const selector = new SessionSelectorComponent(
+ async () => sessions,
+ async () => [],
+ () => {},
+ () => {},
+ () => {},
+ () => {},
+ );
+ await flushPromises();
+
+ const list = selector.getSessionList();
+ const confirmationChanges: Array<string | null> = [];
+ list.onDeleteConfirmationChange = (path) => confirmationChanges.push(path);
+
+ let deletedPath: string | null = null;
+ list.onDeleteSession = async (sessionPath) => {
+ deletedPath = sessionPath;
+ };
+
+ list.handleInput(CTRL_BACKSPACE);
+ expect(confirmationChanges).toEqual([sessions[0]!.path]);
+
+ list.handleInput("\r");
+ expect(confirmationChanges).toEqual([sessions[0]!.path, null]);
+ expect(deletedPath).toBe(sessions[0]!.path);
+ });
+
+ it("does not switch scope back to All when All load resolves after toggling back to Current", async () => {
+ const currentSessions = [makeSession({ id: "current" })];
+ const allDeferred = createDeferred<SessionInfo[]>();
+ let allLoadCalls = 0;
+
+ const selector = new SessionSelectorComponent(
+ async () => currentSessions,
+ async () => {
+ allLoadCalls++;
+ return allDeferred.promise;
+ },
+ () => {},
+ () => {},
+ () => {},
+ () => {},
+ );
+ await flushPromises();
+
+ const list = selector.getSessionList();
+ list.handleInput("\t"); // current -> all (starts async load)
+ list.handleInput("\t"); // all -> current
+
+ allDeferred.resolve([makeSession({ id: "all" })]);
+ await flushPromises();
+
+ expect(allLoadCalls).toBe(1);
+ const output = selector.render(120).join("\n");
+ expect(output).toContain("Resume Session (Current Folder)");
+ expect(output).not.toContain("Resume Session (All)");
+ });
+
+ it("does not start redundant All loads when toggling scopes while All is already loading", async () => {
+ const currentSessions = [makeSession({ id: "current" })];
+ const allDeferred = createDeferred<SessionInfo[]>();
+ let allLoadCalls = 0;
+
+ const selector = new SessionSelectorComponent(
+ async () => currentSessions,
+ async () => {
+ allLoadCalls++;
+ return allDeferred.promise;
+ },
+ () => {},
+ () => {},
+ () => {},
+ () => {},
+ );
+ await flushPromises();
+
+ const list = selector.getSessionList();
+ list.handleInput("\t"); // current -> all (starts async load)
+ list.handleInput("\t"); // all -> current
+ list.handleInput("\t"); // current -> all again while load pending
+
+ expect(allLoadCalls).toBe(1);
+
+ allDeferred.resolve([makeSession({ id: "all" })]);
+ await flushPromises();
+ });
+});This is a comprehensive PR. Let me read the linked issue #813 mentioned in the PR body.
Let me read the linked issue #813 and the existing source files:
>gh issue view 813 --json title,body,comments,labels,state{"body":"The `/resume` picker already provides a wonderful interaction model for discovering sessions (keyboard navigation, search, scope toggling, recent/fuzzy sort modes). I regularly encounter scenarios with it where I want to see the session's .jsonl path to disambiguate (without committing to resumption) between sessions with similar names or first messages, and where I wish I could do quick cleanup passes so that junk sessions don't clutter the `/resume` search results over time.\n\nWhile I'm wary of feature creep in something nominally about *resuming* sessions, `/resume` is a relatively sophisticated session discovery tool, and being able to clean up noise from that is directly useful to \"session discovery in service of session resumption.\"\n\nTo that end I think these would be helpful additions to `/resume`:\n- toggle on/off (e.g. via `ctrl+P`) display of the `.jsonl` file path for each session\n- session deletion (e.g. via `ctrl+backspace`), with inline confirmation\n\nDoes this belong as an extension? I did prototype this as an extension first. The path toggle and delete UI work fine, but an extension can't actually *resume* a session—`ExtensionCommandContext` has `newSession()` and `fork()` but no `resumeSession(path)` / `switchSession(path)`. So such an extension would need a core API change anyway, and at that point we'd have two places doing overlapping things (the extension reimplementing `/resume`'s navigation just to add delete).\n\nGiven that, it seems cleaner and less duplicative to enhance the canonical `/resume` UI directly rather than creating an extension that mostly re-implements it. So, I also have a working implementation (see screenshots below) with `/resume`.\n\nWould you welcome a PR in this direction? If yes, I'll polish the patch I have and open it.\n\n<img width=\"600\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/d5283214-058a-48e2-9689-c1dc721fe1a5\" />\n<img width=\"600\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/4d6b334d-83c9-42a1-8bcf-6571107bcf4c\" />\n<img width=\"600\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/2548d867-a58c-449f-b87c-b32f64eff34c\" />\n<img width=\"600\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/83dd9095-9c78-4053-9d3a-731403c1c511\" />\n\nNote that my implementation uses `trash` when available and prevents deletion of the active session. I consider these thoughtful safeguards against mistakes made by my future self or others, but happy to strip them back if they're more than you'd want.\n\nChanged files:\n- `packages/coding-agent/src/modes/interactive/components/session-selector.ts`\n- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`","comments":[{"id":"IC_kwDOPbFNk87gXaPf","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"This sounds great, happy to review and merge a corresponding PR. Closing this out.","createdAt":"2026-01-17T19:23:05Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"THUMBS_UP","users":{"totalCount":1}}],"url":"https://github.com/badlogic/pi-mono/issues/813#issuecomment-3764233183","viewerDidAuthor":true}],"labels":[],"state":"CLOSED","title":"Proposal: path display toggle + session deletion in /resume"}Now let me check the docs files to see if they need any additional updates, and also check the CHANGELOG format:
Let me check the existing documentation and verify the CHANGELOG format:
Now I have all the information I need to complete the review. Let me check the README for any additional context and then provide my structured review.
Looking at the PR:
The PR also updates:
Key observations:
### Added and doesn't include the PR link and author attribution required for external contributions per AGENTS.mdLet me check if there are any code issues:
spawnSync for trash, which is synchronous but wrapped in an async function - fine since it's a quick operationcurrentSessionFilePath to prevent deleting the active sessionThe PR is from an external contributor (@w-winter), so per AGENTS.md the changelog should include PR link and author.
Now let me fetch this PR into a local branch for testing as the user requested.
Now let me pull this PR into a local branch for testing:
>cd /Users/badlogic/workspaces/pi-mono && git fetch origin pull/816/head:pr-816-resume-additions && git checkout pr-816-resume-additionsFrom https://github.com/badlogic/pi-mono
* [new ref] refs/pull/816/head -> pr-816-resume-additions
Switched to branch 'pr-816-resume-additions'Successfully pulled the PR into a local branch. Now let me provide the structured review.
Successfully checked out the PR branch. Here's my review:
PR: https://github.com/badlogic/pi-mono/pull/816
Changelog:
### Added in packages/coding-agent/CHANGELOG.md- Added feature ([#816](https://github.com/badlogic/pi-mono/pull/816) by [@w-winter](https://github.com/w-winter))Good:
allLoadSeq sequencing to ignore stale resultsallLoading flag.catch() for loader failures (previously could leave UI stuck)trash CLI when available for safer deletion with fallback to unlinkBad:
mergeable: "CONFLICTING") that need resolution before mergespawnSync is used synchronously in an async function. While acceptable for a quick trash call, it briefly blocks the event loopUgly:
Questions or Assumptions:
Ctrl+Backspace convenience binding only triggers delete when search query is empty. Is this behavior desirable to keep? The PR author asks for feedback on this.Ctrl+Backspace be documented if kept?Change summary:
Ctrl+P: Toggle display of session .jsonl file paths in /resume pickerCtrl+D: Delete selected session with inline confirmation (uses trash when available)Ctrl+Backspace: Same as Ctrl+D but only when search query is emptyTests:
packages/coding-agent/test/session-selector-path-delete.test.tsCtrl+D)Ctrl+Backspace not triggering delete when search query is non-emptyThe branch pr-816-resume-additions is now checked out locally for testing. Note the PR has merge conflicts that will need resolution. You can test with:
npm run check # verify it compilesnpm test -- packages/coding-agent/test/session-selector-path-delete.test.ts # run the new testslove it. could we move the dynamic border up above the "Resume Session" line?
Resume Session (Current Folder) ◉ Current Folder | ○ All Sort: Fuzzy Tab: scope · re: for regex · "phrase" for exact phrase Ctrl+R: sort · Ctrl+D: delete · Ctrl+P: path (off)
───────────────────────────────────────────────────────────────────────────────────────────────────────────
The user wants to move the dynamic border from below the header to above it. Let me look at the current structure in the SessionSelectorComponent constructor.
Currently it's:
The user wants:
Let me make that edit.
I see the structure. I need to move the DynamicBorder above the header:
Current:
this.addChild(new Spacer(1));this.addChild(this.header);this.addChild(new Spacer(1));this.addChild(new DynamicBorder());this.addChild(new Spacer(1));New:
this.addChild(new Spacer(1));this.addChild(new DynamicBorder());this.addChild(new Spacer(1));this.addChild(this.header);this.addChild(new Spacer(1));Done. Let me verify the change and run a quick check.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1 | head -50> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 408 files in 237ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.0 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. The change is done. Now the border will appear above the header instead of between the header and the session list.
Done. The layout is now:
───────────────────────────────────────────────────────────────────────────────────────────────────────────
Resume Session (Current Folder) ◉ Current Folder | ○ All Sort: FuzzyTab: scope · re:<pattern> for regex · "phrase" for exact phraseCtrl+R: sort · Ctrl+D: delete · Ctrl+P: path (off)
>Checks pass. You can test it with /resume in interactive mode.
Tab: scope · re: for regex · "phrase" for exact phrase Ctrl+R: sort · Ctrl+D: delete · Ctrl+P: path (off)
does this align with other selector hint text we have? specifically keys etc.?
The user is asking about consistency with other selector hint text in the codebase. Let me check other selectors to see how they format their hints.
>cd /Users/badlogic/workspaces/pi-mono && grep -r "hint" --include="*.ts" packages/coding-agent/src/modes/interactive/components/ | grep -v ".test." | head -30packages/coding-agent/src/modes/interactive/components/extension-selector.ts:import { keyHint, rawKeyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/bordered-loader.ts:import { keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/login-dialog.ts:import { keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/footer.ts: // Add thinking level hint if model supports reasoning and thinking is enabled
packages/coding-agent/src/modes/interactive/components/extension-editor.ts:import { appKeyHint, keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/extension-editor.ts: // Add hint
packages/coding-agent/src/modes/interactive/components/extension-editor.ts: const hint =
packages/coding-agent/src/modes/interactive/components/extension-editor.ts: this.addChild(new Text(hint, 1, 0));
packages/coding-agent/src/modes/interactive/components/tree-selector.ts:import { keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/bash-execution.ts:import { editorKey, keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/tool-execution.ts:import { keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/tool-execution.ts: const hint =
packages/coding-agent/src/modes/interactive/components/tool-execution.ts: return ["", hint, ...cachedLines];
packages/coding-agent/src/modes/interactive/components/index.ts:export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts: * Utilities for formatting keybinding hints in the UI.
packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts: * Format a keybinding hint with consistent styling: dim key, muted description.
packages/coding-agent/src/modes/interactive/components/keybinding-hints.ts: * Format a keybinding hint for app-level actions.
packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts:import { editorKey } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts:import { editorKey } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/extension-input.ts:import { keyHint } from "./keybinding-hints.js";
packages/coding-agent/src/modes/interactive/components/model-selector.ts: // Add hint about model filtering
packages/coding-agent/src/modes/interactive/components/model-selector.ts: const hintText =
packages/coding-agent/src/modes/interactive/components/model-selector.ts: this.addChild(new Text(theme.fg("warning", hintText), 0, 0));
packages/coding-agent/src/modes/interactive/components/session-selector.ts: // Build hint lines - changes based on state (all branches truncate to width)
packages/coding-agent/src/modes/interactive/components/session-selector.ts: let hintLine1: string;
packages/coding-agent/src/modes/interactive/components/session-selector.ts: let hintLine2: string;
packages/coding-agent/src/modes/interactive/components/session-selector.ts: hintLine1 = theme.fg("error", truncateToWidth(confirmHint, width, "…"));
packages/coding-agent/src/modes/interactive/components/session-selector.ts: hintLine2 = "";
packages/coding-agent/src/modes/interactive/components/session-selector.ts: hintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, "…"));
packages/coding-agent/src/modes/interactive/components/session-selector.ts: hintLine2 = "";>cd /Users/badlogic/workspaces/pi-mono && grep -rn "Tab:" --include="*.ts" packages/coding-agent/src/modes/interactive/packages/coding-agent/src/modes/interactive/components/session-selector.ts:146: const hint1 = `Tab: scope · re:<pattern> for regex · "phrase" for exact phrase`;>cd /Users/badlogic/workspaces/pi-mono && grep -rn "Ctrl+" --include="*.ts" packages/coding-agent/src/modes/interactive/components/ | grep -v "matches\|keyData" | head -20packages/coding-agent/src/modes/interactive/components/oauth-selector.ts:116: // Escape or Ctrl+C
packages/coding-agent/src/modes/interactive/components/login-dialog.ts:89: const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
packages/coding-agent/src/modes/interactive/components/extension-editor.ts:3: * Supports Ctrl+G for external editor.
packages/coding-agent/src/modes/interactive/components/extension-editor.ts:87: // Escape or Ctrl+C to cancel
packages/coding-agent/src/modes/interactive/components/custom-editor.ts:59: // Exit (Ctrl+D) - only when editor is empty
packages/coding-agent/src/modes/interactive/components/model-selector.ts:254: // Escape or Ctrl+C
packages/coding-agent/src/modes/interactive/components/session-selector.ts:137: const confirmHint = "Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel";
packages/coding-agent/src/modes/interactive/components/session-selector.ts:147: const hint2 = `Ctrl+R: sort · Ctrl+D: delete · Ctrl+P: path ${pathState}`;
packages/coding-agent/src/modes/interactive/components/session-selector.ts:349: // Allow both Escape and Ctrl+C to cancel (consistent with pi UX)
packages/coding-agent/src/modes/interactive/components/session-selector.ts:370: // Ctrl+P: toggle path display
packages/coding-agent/src/modes/interactive/components/session-selector.ts:377: // Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)
packages/coding-agent/src/modes/interactive/components/session-selector.ts:383: // Ctrl+Backspace: non-invasive convenience alias for delete
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:93: * Component for enabling/disabling models for Ctrl+P cycling.
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:94: * Changes are session-only until explicitly persisted with Ctrl+S.
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:136: this.addChild(new Text(theme.fg("muted", "Session-only. Ctrl+S to save to settings."), 0, 0));
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:267: // Ctrl+A - Enable all (filtered if search active, otherwise all)
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:277: // Ctrl+X - Clear all (filtered if search active, otherwise all)
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:287: // Ctrl+P - Toggle provider of current item
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:304: // Ctrl+S - Save/persist to settings
packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:312: // Ctrl+C - clear search or cancel if emptyThere's a keybinding-hints.ts file with utilities for formatting hints. Let me look at that and see how other selectors use it.
>cd /Users/badlogic/workspaces/pi-mono && grep -A5 "keyHint\|rawKeyHint" packages/coding-agent/src/modes/interactive/components/tree-selector.ts | head -30import { keyHint } from "./keybinding-hints.js";
/** Gutter info: position (displayIndent where connector was) and whether to show │ */
interface GutterInfo {
position: number; // displayIndent level where the connector was shown
show: boolean; // true = show │, false = show spaces
--
truncateToWidth(`${indent}${keyHint("selectConfirm", "save")} ${keyHint("selectCancel", "cancel")}`, width),
);
return lines;
}
handleInput(keyData: string): void {>cd /Users/badlogic/workspaces/pi-mono && grep -A5 "keyHint\|rawKeyHint" packages/coding-agent/src/modes/interactive/components/extension-selector.ts | head -40import { keyHint, rawKeyHint } from "./keybinding-hints.js";
export interface ExtensionSelectorOptions {
tui?: TUI;
timeout?: number;
}
--
rawKeyHint("↑↓", "navigate") +
" " +
keyHint("selectConfirm", "select") +
" " +
keyHint("selectCancel", "cancel"),
1,
0,
),
);
this.addChild(new Spacer(1));I see. The other selectors use keyHint() and rawKeyHint() from keybinding-hints.js which:
theme.fg("dim", key) for the keytheme.fg("muted", description) for the descriptionThe session selector is using hardcoded strings like "Tab: scope" and "Ctrl+R: sort" instead of using the keybinding hints system.
Let me check how Tab, Ctrl+R, Ctrl+D, Ctrl+P are handled - are they configurable keybindings or hardcoded?
>cd /Users/badlogic/workspaces/pi-mono && grep -n "tab\|ctrl+r\|ctrl+d\|ctrl+p" packages/coding-agent/src/modes/interactive/components/session-selector.ts | head -20358: if (kb.matches(keyData, "tab")) {
365: if (matchesKey(keyData, "ctrl+r")) {
371: if (matchesKey(keyData, "ctrl+p")) {
378: if (matchesKey(keyData, "ctrl+d")) {>cd /Users/badlogic/workspaces/pi-mono && grep -n "EditorAction\|AppAction" packages/tui/src/keybindings.ts | head -206:export type EditorAction =
51: [K in EditorAction]?: KeyId | KeyId[];
100: private actionToKeys: Map<EditorAction, KeyId[]>;
113: this.actionToKeys.set(action as EditorAction, [...keyArray]);
120: this.actionToKeys.set(action as EditorAction, keyArray);
127: matches(data: string, action: EditorAction): boolean {
139: getKeys(action: EditorAction): KeyId[] {I see. The session selector uses:
tab - which is a configurable EditorActionctrl+r, ctrl+p, ctrl+d - which are hardcoded with matchesKey()Looking at how other selectors do it, they use keyHint() and rawKeyHint() for consistent styling. The session selector should:
keyHint("tab", "scope") for TabrawKeyHint("ctrl+r", "sort") for the hardcoded onesLet me check the format more closely - in the extension-selector it's:
rawKeyHint("↑↓", "navigate") + " " + keyHint("selectConfirm", "select") + " " + keyHint("selectCancel", "cancel")So the format is "key description" not "key: description". The session selector uses "Tab: scope" but it should be "tab scope" with the colons removed.
Let me update the session selector to use the keybinding hints system for consistency.
The session selector uses hardcoded strings like "Tab: scope" but other selectors use the keyHint()/rawKeyHint() system with format "key description" (no colon). Let me fix this:
I need to:
keyHint and rawKeyHint from ./keybinding-hints.jsLet me make the edits.
Now I need to update the hint lines. Let me find the current hint lines and update them to use keyHint/rawKeyHint.
I see the current code. The hint lines are using hardcoded strings and applying muted color to the whole thing. But keyHint and rawKeyHint return already-styled strings with dim key and muted description.
Looking at the extension-selector usage:
rawKeyHint("↑↓", "navigate") + " " + keyHint("selectConfirm", "select") + " " + keyHint("selectCancel", "cancel")For the session selector hints:
Tab: scope → keyHint("tab", "scope") (tab is an EditorAction)Ctrl+R: sort → rawKeyHint("ctrl+r", "sort") (hardcoded key)Ctrl+D: delete → rawKeyHint("ctrl+d", "delete")Ctrl+P: path (on) → rawKeyHint("ctrl+p", "path") + " " + pathState or similarThe regex and phrase hints don't have keys, they're just informational text. I'll keep them as muted text.
Let me update the code.
Let me run the check to make sure it compiles.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1 | head -50> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 408 files in 269ms. Fixed 1 file.
> @mariozechner/pi-web-ui@0.49.0 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 42ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.Good, checks pass. Now the hints use the consistent styling system:
keyHint("tab", "scope") → dim "tab" + muted " scope"rawKeyHint("ctrl+r", "sort") → dim "ctrl+r" + muted " sort"The format now matches other selectors like extension-selector.
Done. Now the hints use the consistent keyHint()/rawKeyHint() system:
tab scope · re:<pattern> regex · "phrase" exactctrl+r sort · ctrl+d delete · ctrl+p path (off)Keys are styled with dim color, descriptions with muted (matching extension-selector and tree-selector patterns). Checks pass.
ok, what merge conflicts need resolving? everything looks good to me otherwise
The user wants to know what merge conflicts exist. Let me check by trying to rebase or merge with main.
>cd /Users/badlogic/workspaces/pi-mono && git fetch origin main && git merge-base HEAD origin/mainFrom https://github.com/badlogic/pi-mono
* branch main -> FETCH_HEAD
ad337e30be87ecc8794468fd03fc264dfe23cfc9>cd /Users/badlogic/workspaces/pi-mono && git diff origin/main...HEAD --name-onlypackages/coding-agent/CHANGELOG.md
packages/coding-agent/README.md
packages/coding-agent/docs/session.md
packages/coding-agent/src/modes/interactive/components/session-selector.ts
packages/coding-agent/src/modes/interactive/interactive-mode.ts
packages/coding-agent/test/session-selector-path-delete.test.tsLet me try rebasing on main to see the conflicts.
>cd /Users/badlogic/workspaces/pi-mono && git rebase origin/main 2>&1 || trueerror: cannot rebase: You have unstaged changes.
error: Please commit or stash them.Ah, there are unstaged changes from my edits. Let me check what those are.
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch pr-816-resume-additions
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/src/modes/interactive/components/session-selector.ts
no changes added to commit (use "git add" and/or "git commit -a")My changes are in session-selector.ts. Let me stash those, do a rebase to see what conflicts exist, then restore my changes.
>cd /Users/badlogic/workspaces/pi-mono && git stash && git rebase origin/main 2>&1 || trueSaved working directory and index state WIP on pr-816-resume-additions: 41ca1eac fix(coding-agent): init theme in session selector tests
Rebasing (1/2)
Auto-merging packages/coding-agent/CHANGELOG.md
CONFLICT (content): Merge conflict in packages/coding-agent/CHANGELOG.md
Auto-merging packages/coding-agent/README.md
Auto-merging packages/coding-agent/src/modes/interactive/interactive-mode.ts
error: could not apply a81aeee8... feat(coding-agent): add session path toggle and deletion to /resume
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply a81aeee8... # feat(coding-agent): add session path toggle and deletion to /resumeThe conflict is in CHANGELOG.md. Let me see what the conflict looks like.
>cd /Users/badlogic/workspaces/pi-mono && cat packages/coding-agent/CHANGELOG.md | head -40# Changelog
## [Unreleased]
### Added
<<<<<<< HEAD
- Added `strictResponsesPairing` compat option for custom OpenAI Responses models on Azure ([#768](https://github.com/badlogic/pi-mono/pull/768) by [@nicobako](https://github.com/nicobako))
=======
- Session selector (`/resume`) now supports path display toggle (`Ctrl+P`) to show/hide `.jsonl` file paths for each session
- Session selector (`/resume`) now supports session deletion (`Ctrl+D`) with inline confirmation. Uses `trash` command if available, falls back to permanent deletion. Prevents deletion of the currently active session
>>>>>>> a81aeee8 (feat(coding-agent): add session path toggle and deletion to /resume)
### Changed
- Share URLs now use hash fragments (`#`) instead of query strings (`?`) to prevent session IDs from being sent to buildwithpi.ai ([#828](https://github.com/badlogic/pi-mono/issues/828))
- API keys in `models.json` can now be retrieved via shell command using `!` prefix (e.g., `"apiKey": "!security find-generic-password -ws 'anthropic'"` for macOS Keychain) ([#697](https://github.com/badlogic/pi-mono/issues/697))
### Fixed
- Fixed IME candidate window appearing in wrong position when filtering menus with Input Method Editor (e.g., Chinese IME). Components with search inputs now properly propagate focus state for cursor positioning. ([#827](https://github.com/badlogic/pi-mono/issues/827))
- Fixed extension shortcut conflicts to respect user keybindings when built-in actions are remapped. ([#826](https://github.com/badlogic/pi-mono/pull/826) by [@richardgill](https://github.com/richardgill))
## [0.49.0] - 2026-01-17
### Added
- `pi.setLabel(entryId, label)` in ExtensionAPI for setting per-entry labels from extensions ([#806](https://github.com/badlogic/pi-mono/issues/806))
- Export `keyHint`, `appKeyHint`, `editorKey`, `appKey`, `rawKeyHint` for extensions to format keybinding hints consistently ([#802](https://github.com/badlogic/pi-mono/pull/802) by [@dannote](https://github.com/dannote))
- Exported `VERSION` from the package index and updated the custom-header example. ([#798](https://github.com/badlogic/pi-mono/pull/798) by [@tallshort](https://github.com/tallshort))
- Added `showHardwareCursor` setting to control cursor visibility while still positioning it for IME support. ([#800](https://github.com/badlogic/pi-mono/pull/800) by [@ghoulr](https://github.com/ghoulr))
- Added Emacs-style kill ring editing with yank and yank-pop keybindings, plus legacy Alt+letter handling and Alt+D delete word forward support in the interactive editor. ([#810](https://github.com/badlogic/pi-mono/pull/810) by [@Perlence](https://github.com/Perlence))
- Added `ctx.compact()` and `ctx.getContextUsage()` to extension contexts for programmatic compaction and context usage checks.
- Added documentation for delete word forward and kill ring keybindings in interactive mode. ([#810](https://github.com/badlogic/pi-mono/pull/810) by [@Perlence](https://github.com/Perlence))
### Changed
- Updated the default system prompt wording to clarify the pi harness and documentation scope.
- Simplified Codex system prompt handling to use the default system prompt directly for Codex instructions.Simple conflict in CHANGELOG.md. We need to keep both entries under ### Added. Let me resolve this conflict by keeping both entries. Also, I need to add the PR link and author attribution for the external contribution.
Simple conflict in CHANGELOG.md - just needs both entries combined. I'll resolve it and also add the required PR attribution for the external contribution:
>cd /Users/badlogic/workspaces/pi-mono && cat > /tmp/changelog_fix.txt << 'EOF'
# Changelog
## [Unreleased]
### Added
- Added `strictResponsesPairing` compat option for custom OpenAI Responses models on Azure ([#768](https://github.com/badlogic/pi-mono/pull/768) by [@nicobako](https://github.com/nicobako))
- Session selector (`/resume`) now supports path display toggle (`Ctrl+P`) and session deletion (`Ctrl+D`) with inline confirmation ([#816](https://github.com/badlogic/pi-mono/pull/816) by [@w-winter](https://github.com/w-winter))
### Changed
EOF
tail -n +14 packages/coding-agent/CHANGELOG.md >> /tmp/changelog_fix.txt && mv /tmp/changelog_fix.txt packages/coding-agent/CHANGELOG.md(no output)Let me verify the changelog looks correct.
There's a duplicate ### Changed header. Let me fix that.
Now let me continue the rebase.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/CHANGELOG.md && git rebase --continue 2>&1[detached HEAD 26fe0483] feat(coding-agent): add session path toggle and deletion to /resume
Author: warren <[REDACTED]>
6 files changed, 520 insertions(+), 51 deletions(-)
create mode 100644 packages/coding-agent/test/session-selector-path-delete.test.ts
Rebasing (2/2)
Successfully rebased and updated refs/heads/pr-816-resume-additions.Rebase succeeded. Now let me restore my stashed changes (the border reorder and keybinding hints updates).
>cd /Users/badlogic/workspaces/pi-mono && git stash popOn branch pr-816-resume-additions
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/src/modes/interactive/components/session-selector.ts
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (db97a58272f20f0e60a8107167e175cba56584cb)My changes are restored. Now let me verify everything still works.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1 | head -20> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 408 files in 240ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.0 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.All checks pass. Let me run the tests for the new file as well.
>cd /Users/badlogic/workspaces/pi-mono && npm test -- packages/coding-agent/test/session-selector-path-delete.test.ts 2>&1> pi-monorepo@0.0.3 test
> npm run test --workspaces --if-present packages/coding-agent/test/session-selector-path-delete.test.ts
> @mariozechner/pi-agent-core@0.49.0 test
> vitest --run packages/coding-agent/test/session-selector-path-delete.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/agent
No test files found, exiting with code 1
filter: packages/coding-agent/test/session-selector-path-delete.test.ts
include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/agent
npm error workspace @mariozechner/pi-agent-core@0.49.0
npm error location /Users/badlogic/workspaces/pi-mono/packages/agent
npm error command failed
npm error command sh -c vitest --run packages/coding-agent/test/session-selector-path-delete.test.ts
> @mariozechner/pi-ai@0.49.0 test
> vitest --run packages/coding-agent/test/session-selector-path-delete.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/ai
No test files found, exiting with code 1
filter: packages/coding-agent/test/session-selector-path-delete.test.ts
include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/ai
npm error workspace @mariozechner/pi-ai@0.49.0
npm error location /Users/badlogic/workspaces/pi-mono/packages/ai
npm error command failed
npm error command sh -c vitest --run packages/coding-agent/test/session-selector-path-delete.test.ts
> @mariozechner/pi-coding-agent@0.49.0 test
> vitest --run packages/coding-agent/test/session-selector-path-delete.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
No test files found, exiting with code 1
filter: packages/coding-agent/test/session-selector-path-delete.test.ts
include: **/*.{test,spec}.?(c|m)[jt]s?(x)
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error workspace @mariozechner/pi-coding-agent@0.49.0
npm error location /Users/badlogic/workspaces/pi-mono/packages/coding-agent
npm error command failed
npm error command sh -c vitest --run packages/coding-agent/test/session-selector-path-delete.test.ts
> @mariozechner/pi-tui@0.49.0 test
> node --test --import tsx test/*.test.ts packages/coding-agent/test/session-selector-path-delete.test.ts
Result: null
Result: null
Result: {
items: [
{ value: '/.nofollow/', label: '.nofollow/' },
{ value: '/.resolve/', label: '.resolve/' },
{ value: '/.vol/', label: '.vol/' },
{ value: '/Applications/', label: 'Applications/' },
{ value: '/bin/', label: 'bin/' },
{ value: '/cores/', label: 'cores/' },
{ value: '/dev/', label: 'dev/' },
{ value: '/etc/', label: 'etc/' },
{ value: '/home/', label: 'home/' },
{ value: '/Library/', label: 'Library/' },
{ value: '/opt/', label: 'opt/' },
{ value: '/private/', label: 'private/' },
{ value: '/sbin/', label: 'sbin/' },
{ value: '/System/', label: 'System/' },
{ value: '/tmp/', label: 'tmp/' },
{ value: '/Users/', label: 'Users/' },
{ value: '/usr/', label: 'usr/' },
{ value: '/var/', label: 'var/' },
{ value: '/Volumes/', label: 'Volumes/' },
{ value: '/.file', label: '.file' },
{ value: '/.VolumeIcon.icns', label: '.VolumeIcon.icns' }
],
prefix: '/'
}
▶ CombinedAutocompleteProvider
▶ extractPathPrefix
✔ extracts / from 'hey /' when forced (9.229084ms)
✔ extracts /A from '/A' when forced (0.6955ms)
✔ does not trigger for slash commands (0.092209ms)
✔ triggers for absolute paths after slash command argument (1.213583ms)
✔ extractPathPrefix (11.658334ms)
✔ CombinedAutocompleteProvider (11.891ms)
▶ Editor component
▶ Prompt history navigation
✔ does nothing on Up arrow when history is empty (4.258333ms)
✔ shows most recent history entry on Up arrow when editor is empty (0.625708ms)
✔ cycles through history entries on repeated Up arrow (3.214458ms)
✔ returns to empty editor on Down arrow after browsing history (0.557875ms)
✔ navigates forward through history with Down arrow (0.599625ms)
✔ exits history mode when typing a character (0.457708ms)
✔ exits history mode on setText (0.332875ms)
✔ does not add empty strings to history (0.293208ms)
✔ does not add consecutive duplicates to history (0.335375ms)
✔ allows non-consecutive duplicates in history (5.389708ms)
✔ uses cursor movement instead of history when editor has content (0.772625ms)
✔ limits history to 100 entries (2.806875ms)
✔ allows cursor movement within multi-line history entry with Down (0.46525ms)
✔ allows cursor movement within multi-line history entry with Up (0.451417ms)
✔ navigates from multi-line entry back to newer via Down after cursor movement (0.47175ms)
✔ Prompt history navigation (21.784917ms)
▶ public state accessors
✔ returns cursor position (1.357125ms)
✔ returns lines as a defensive copy (0.425458ms)
✔ public state accessors (1.853541ms)
▶ Shift+Enter handling
✔ treats split VS Code Shift+Enter as a newline (0.367167ms)
✔ inserts a literal backslash when not followed by Enter (0.31475ms)
✔ Shift+Enter handling (0.74725ms)
▶ Unicode text editing behavior
✔ inserts mixed ASCII, umlauts, and emojis as literal text (0.7655ms)
✔ deletes single-code-unit unicode characters (umlauts) with Backspace (19.6015ms)
✔ deletes multi-code-unit emojis with single Backspace (0.402667ms)
✔ inserts characters at the correct position after cursor movement over umlauts (0.308375ms)
✔ moves cursor across multi-code-unit emojis with single arrow key (0.290875ms)
✔ preserves umlauts across line breaks (0.286791ms)
✔ replaces the entire document with unicode text via setText (paste simulation) (0.2045ms)
✔ moves cursor to document start on Ctrl+A and inserts at the beginning (0.279167ms)
✔ deletes words correctly with Ctrl+W and Alt+Backspace (0.487375ms)
✔ navigates words correctly with Ctrl+Left/Right (0.436583ms)
✔ Unicode text editing behavior (23.221958ms)
▶ Grapheme-aware text wrapping
✔ wraps lines correctly when text contains wide emojis (28.524167ms)
✔ wraps long text with emojis at correct positions (0.510625ms)
✔ wraps CJK characters correctly (each is 2 columns wide) (0.658709ms)
✔ handles mixed ASCII and wide characters in wrapping (0.278709ms)
✔ renders cursor correctly on wide characters (0.279084ms)
✔ does not exceed terminal width with emoji at wrap boundary (4.155166ms)
✔ Grapheme-aware text wrapping (34.536167ms)
▶ Word wrapping
✔ wraps at word boundaries instead of mid-word (0.505042ms)
✔ does not start lines with leading whitespace after word wrap (0.325416ms)
✔ breaks long words (URLs) at character level (0.369292ms)
✔ preserves multiple spaces within words on same line (0.249625ms)
✔ handles empty string (0.235583ms)
✔ handles single word that fits exactly (0.246041ms)
✔ Word wrapping (2.029792ms)
▶ Kill ring
✔ Ctrl+W saves deleted text to kill ring and Ctrl+Y yanks it (0.445625ms)
✔ Ctrl+U saves deleted text to kill ring (0.385708ms)
✔ Ctrl+K saves deleted text to kill ring (0.24075ms)
✔ Ctrl+Y does nothing when kill ring is empty (0.194916ms)
✔ Alt+Y cycles through kill ring after Ctrl+Y (0.324333ms)
✔ Alt+Y does nothing if not preceded by yank (0.221666ms)
✔ Alt+Y does nothing if kill ring has ≤1 entry (0.210709ms)
✔ consecutive Ctrl+W accumulates into one kill ring entry (0.224959ms)
✔ Ctrl+U accumulates multiline deletes including newlines (0.232083ms)
✔ backward deletions prepend, forward deletions append during accumulation (0.302916ms)
✔ non-delete actions break kill accumulation (0.255084ms)
✔ non-yank actions break Alt+Y chain (3.057833ms)
✔ kill ring rotation persists after cycling (0.383208ms)
✔ consecutive deletions across lines coalesce into one entry (0.307083ms)
✔ Ctrl+K at line end deletes newline and coalesces (0.328875ms)
✔ handles yank in middle of text (0.300209ms)
✔ handles yank-pop in middle of text (0.306459ms)
✔ multiline yank and yank-pop in middle of text (0.316917ms)
✔ Alt+D deletes word forward and saves to kill ring (0.278ms)
✔ Alt+D at end of line deletes newline (0.228458ms)
✔ Kill ring (8.822834ms)
✔ Editor component (93.4315ms)
▶ fuzzyMatch
✔ empty query matches everything with score 0 (0.641042ms)
✔ query longer than text does not match (0.061625ms)
✔ exact match has good score (0.135542ms)
✔ characters must appear in order (0.060583ms)
✔ case insensitive matching (0.058542ms)
✔ consecutive matches score better than scattered matches (0.060083ms)
✔ word boundary matches score better (0.0485ms)
✔ fuzzyMatch (1.654417ms)
▶ fuzzyFilter
✔ empty query returns all items unchanged (0.697292ms)
✔ filters out non-matching items (0.12ms)
✔ sorts results by match quality (0.128959ms)
✔ works with custom getText function (0.095ms)
✔ fuzzyFilter (1.177625ms)
▶ Input component
✔ treats split VS Code Shift+Enter as submit (0.647042ms)
✔ inserts a literal backslash when not followed by Enter (0.718917ms)
✔ Input component (1.885875ms)
▶ matchesKey
▶ Kitty protocol with alternate keys (non-Latin layouts)
✔ should match Ctrl+c when pressing Ctrl+С (Cyrillic) with base layout key (0.922666ms)
✔ should match Ctrl+d when pressing Ctrl+В (Cyrillic) with base layout key (0.154041ms)
✔ should match Ctrl+z when pressing Ctrl+Я (Cyrillic) with base layout key (0.065916ms)
✔ should match Ctrl+Shift+p with base layout key (0.065833ms)
✔ should still match direct codepoint when no base layout key (0.077375ms)
✔ should handle shifted key in format (0.060125ms)
✔ should handle event type in format (0.056167ms)
✔ should handle full format with shifted key, base key, and event type (0.055041ms)
✔ should not match wrong key even with base layout (0.072875ms)
✔ should not match wrong modifiers even with base layout (0.101708ms)
✔ Kitty protocol with alternate keys (non-Latin layouts) (2.190084ms)
▶ Legacy key matching
✔ should match legacy Ctrl+c (0.13125ms)
✔ should match legacy Ctrl+d (0.046333ms)
✔ should match escape key (0.045333ms)
✔ should match legacy linefeed as enter (0.209792ms)
✔ should treat linefeed as shift+enter when kitty active (0.157125ms)
✔ should parse ctrl+space (0.034208ms)
✔ should parse legacy alt-prefixed sequences when kitty inactive (0.094416ms)
✔ should match arrow keys (0.04425ms)
✔ should match SS3 arrows and home/end (0.042708ms)
✔ should match legacy function keys and clear (0.036417ms)
✔ should match alt+arrows (0.033084ms)
✔ should match rxvt modifier sequences (0.052958ms)
✔ Legacy key matching (1.100167ms)
✔ matchesKey (3.591292ms)
▶ parseKey
▶ Kitty protocol with alternate keys
✔ should return Latin key name when base layout key is present (0.063125ms)
✔ should return key name from codepoint when no base layout (0.036667ms)
✔ Kitty protocol with alternate keys (0.1495ms)
▶ Legacy key parsing
✔ should parse legacy Ctrl+letter (0.077208ms)
✔ should parse special keys (0.040958ms)
✔ should parse arrow keys (0.035833ms)
✔ should parse SS3 arrows and home/end (0.035583ms)
✔ should parse legacy function and modifier sequences (0.038208ms)
✔ should parse double bracket pageUp (0.030084ms)
✔ Legacy key parsing (0.400917ms)
✔ parseKey (0.620708ms)
▶ Markdown component
▶ Nested lists
✔ should render simple nested list (10.20275ms)
✔ should render deeply nested list (0.451333ms)
✔ should render ordered nested list (0.689167ms)
✔ should render mixed ordered and unordered nested lists (0.333125ms)
✔ should maintain numbering when code blocks are not indented (LLM output) (0.501334ms)
✔ Nested lists (12.63825ms)
▶ Tables
✔ should render simple table (1.308292ms)
✔ should render table with alignment (0.523708ms)
✔ should handle tables with varying column widths (0.500875ms)
✔ should wrap table cells when table exceeds available width (0.804125ms)
✔ should wrap long cell content to multiple lines (0.436292ms)
✔ should wrap long unbroken tokens inside table cells (not only at line start) (0.979375ms)
✔ should wrap styled inline code inside table cells without breaking borders (20.600667ms)
✔ should handle extremely narrow width gracefully (0.229ms)
✔ should render table correctly when it fits naturally (0.187125ms)
✔ should respect paddingX when calculating table width (0.260584ms)
✔ Tables (26.07ms)
▶ Combined features
✔ should render lists and tables together (0.719125ms)
✔ Combined features (0.775417ms)
▶ Pre-styled text (thinking traces)
✔ should preserve gray italic styling after inline code (1.326625ms)
✔ should preserve gray italic styling after bold text (3.032208ms)
✔ should not leak styles into following lines when rendered in TUI (16.841333ms)
✔ Pre-styled text (thinking traces) (21.323542ms)
▶ Spacing after code blocks
✔ should have only one blank line between code block and following paragraph (0.22475ms)
✔ Spacing after code blocks (0.263167ms)
▶ Spacing after dividers
✔ should have only one blank line between divider and following paragraph (0.174167ms)
✔ Spacing after dividers (0.201916ms)
▶ Spacing after headings
✔ should have only one blank line between heading and following paragraph (0.144ms)
✔ Spacing after headings (0.167583ms)
▶ Spacing after blockquotes
✔ should have only one blank line between blockquote and following paragraph (0.26525ms)
✔ Spacing after blockquotes (0.2895ms)
▶ HTML-like tags in text
✔ should render content with HTML-like tags as text (0.1695ms)
✔ should render HTML tags in code blocks correctly (0.079167ms)
✔ HTML-like tags in text (0.281792ms)
✔ Markdown component (62.477292ms)
▶ TUI overlay options
▶ width overflow protection
✔ should truncate overlay lines that exceed declared width (14.306959ms)
✔ should handle overlay with complex ANSI sequences without crashing (6.42125ms)
✔ should handle overlay composited on styled base content (1.933375ms)
✔ should handle wide characters at overlay boundary (1.788708ms)
✔ should handle overlay positioned at terminal edge (1.206583ms)
✔ should handle overlay on base content with OSC sequences (1.802583ms)
✔ width overflow protection (28.133042ms)
▶ width percentage
✔ should render overlay at percentage of terminal width (58.749709ms)
✔ should respect minWidth when widthPercent results in smaller width (0.934084ms)
✔ width percentage (59.832583ms)
▶ anchor positioning
✔ should position overlay at top-left (0.870416ms)
✔ should position overlay at bottom-right (0.838292ms)
✔ should position overlay at top-center (3.486583ms)
✔ anchor positioning (5.326ms)
▶ margin
✔ should clamp negative margins to zero (25.845833ms)
✔ should respect margin as number (1.757ms)
✔ should respect margin object (9.245833ms)
✔ margin (37.015041ms)
▶ offset
✔ should apply offsetX and offsetY from anchor position (3.984ms)
✔ offset (4.072833ms)
▶ percentage positioning
✔ should position with rowPercent and colPercent (2.917666ms)
✔ rowPercent 0 should position at top (7.178875ms)
✔ rowPercent 100 should position at bottom (0.935084ms)
✔ percentage positioning (11.49825ms)
▶ maxHeight
✔ should truncate overlay to maxHeight (2.068833ms)
✔ should truncate overlay to maxHeightPercent (1.433625ms)
✔ maxHeight (3.615833ms)
▶ absolute positioning
✔ row and col should override anchor (1.23675ms)
✔ absolute positioning (1.296416ms)
▶ stacked overlays
✔ should render multiple overlays with later ones on top (1.313625ms)
✔ should handle overlays at different positions without interference (4.071125ms)
✔ should properly hide overlays in stack order (3.777375ms)
✔ stacked overlays (9.310959ms)
✔ TUI overlay options (160.627167ms)
Terminal rows: 24
Content lines: 3
Overlay visible: true
▶ TUI overlay with short content
✔ should render overlay when content is shorter than terminal height (10.412667ms)
✔ TUI overlay with short content (10.950459ms)
▶ SelectList
✔ normalizes multiline descriptions to single line (0.788583ms)
✔ SelectList (1.269167ms)
▶ StdinBuffer
▶ Regular Characters
✔ should pass through regular characters immediately (1.485417ms)
✔ should pass through multiple regular characters (0.128667ms)
✔ should handle unicode characters (0.106333ms)
✔ Regular Characters (2.102709ms)
▶ Complete Escape Sequences
✔ should pass through complete mouse SGR sequences (0.236791ms)
✔ should pass through complete arrow key sequences (0.13525ms)
✔ should pass through complete function key sequences (0.123167ms)
✔ should pass through meta key sequences (0.120875ms)
✔ should pass through SS3 sequences (0.074834ms)
✔ Complete Escape Sequences (0.841666ms)
▶ Partial Escape Sequences
✔ should buffer incomplete mouse SGR sequence (0.438375ms)
✔ should buffer incomplete CSI sequence (0.114583ms)
✔ should buffer split across many chunks (0.197958ms)
✔ should flush incomplete sequence after timeout (15.759333ms)
✔ Partial Escape Sequences (16.655417ms)
▶ Mixed Content
✔ should handle characters followed by escape sequence (0.096125ms)
✔ should handle escape sequence followed by characters (0.05175ms)
✔ should handle multiple complete sequences (0.04725ms)
✔ should handle partial sequence with preceding characters (0.066875ms)
✔ Mixed Content (0.324ms)
▶ Kitty Keyboard Protocol
✔ should handle Kitty CSI u press events (0.086708ms)
✔ should handle Kitty CSI u release events (0.041959ms)
✔ should handle batched Kitty press and release (0.048083ms)
✔ should handle multiple batched Kitty events (0.051917ms)
✔ should handle Kitty arrow keys with event type (0.042333ms)
✔ should handle Kitty functional keys with event type (0.042083ms)
✔ should handle plain characters mixed with Kitty sequences (0.041167ms)
✔ should handle Kitty sequence followed by plain characters (0.080334ms)
✔ should handle rapid typing simulation with Kitty protocol (0.060625ms)
✔ Kitty Keyboard Protocol (0.613834ms)
▶ Mouse Events
✔ should handle mouse press event (0.088791ms)
✔ should handle mouse release event (0.047792ms)
✔ should handle mouse move event (0.055583ms)
✔ should handle split mouse events (0.057916ms)
✔ should handle multiple mouse events (0.046208ms)
✔ should handle old-style mouse sequence (ESC[M + 3 bytes) (0.038708ms)
✔ should buffer incomplete old-style mouse sequence (0.050542ms)
✔ Mouse Events (0.468292ms)
▶ Edge Cases
✔ should handle empty input (0.061375ms)
✔ should handle lone escape character with timeout (16.207416ms)
✔ should handle lone escape character with explicit flush (0.155125ms)
✔ should handle buffer input (0.069833ms)
✔ should handle very long sequences (0.087083ms)
✔ Edge Cases (16.672625ms)
▶ Flush
✔ should flush incomplete sequences (0.075458ms)
✔ should return empty array if nothing to flush (0.041333ms)
✔ should emit flushed data via timeout (15.6415ms)
✔ Flush (15.841083ms)
▶ Clear
✔ should clear buffered content without emitting (0.113708ms)
✔ Clear (0.146916ms)
▶ Bracketed Paste
✔ should emit paste event for complete bracketed paste (0.140375ms)
✔ should handle paste arriving in chunks (0.059833ms)
✔ should handle paste with input before and after (0.061916ms)
✔ should handle paste with newlines (0.051375ms)
✔ should handle paste with unicode (0.054583ms)
✔ Bracketed Paste (0.430833ms)
▶ Destroy
✔ should clear buffer on destroy (0.05575ms)
✔ should clear pending timeouts on destroy (16.221917ms)
✔ Destroy (16.354166ms)
✔ StdinBuffer (71.005375ms)
▶ TruncatedText component
✔ pads output lines to exactly match width (0.744583ms)
✔ pads output with vertical padding lines to width (0.082125ms)
✔ truncates long text and pads to width (0.9645ms)
✔ preserves ANSI codes in output and pads correctly (0.253166ms)
✔ truncates styled text and adds reset code before ellipsis (0.201875ms)
✔ handles text that fits exactly (0.144708ms)
✔ handles empty text (0.11ms)
✔ stops at newline and only shows first line (0.079541ms)
✔ truncates first line even with newlines in text (0.141041ms)
✔ TruncatedText component (3.402458ms)
▶ TUI overlay compositing
✔ should not leak styles when a trailing reset sits beyond the last visible column (no overlay) (6.724416ms)
✔ should not leak styles when overlay slicing drops trailing SGR resets (3.806834ms)
✔ TUI overlay compositing (11.0975ms)
▶ TUI differential rendering
✔ tracks cursor correctly when content shrinks with unchanged remaining lines (9.843416ms)
✔ renders correctly when only a middle line changes (spinner case) (7.995375ms)
✔ resets styles after each rendered line (0.984792ms)
✔ renders correctly when first line changes but rest stays same (2.0085ms)
✔ renders correctly when last line changes but rest stays same (3.265208ms)
✔ renders correctly when multiple non-adjacent lines change (3.204875ms)
✔ handles transition from content to empty and back to content (4.3885ms)
✔ TUI differential rendering (32.515667ms)
▶ wrapTextWithAnsi
▶ underline styling
✔ should not apply underline style before the styled text (2.190542ms)
✔ should not have whitespace before underline reset code (0.173042ms)
✔ should not bleed underline to padding - each line should end with reset for underline only (0.323959ms)
✔ underline styling (3.063125ms)
▶ background color preservation
✔ should preserve background color across wrapped lines without full reset (0.225333ms)
✔ should reset underline but preserve background when wrapping underlined text inside background (0.270042ms)
✔ background color preservation (0.565875ms)
▶ basic wrapping
✔ should wrap plain text correctly (0.115416ms)
✔ should truncate trailing whitespace that exceeds width (0.071333ms)
✔ should preserve color codes across wraps (0.130334ms)
✔ basic wrapping (0.430125ms)
✔ wrapTextWithAnsi (4.400584ms)
ℹ tests 233
ℹ suites 60
ℹ pass 233
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 674.709584
Command exited with code 1I see - the pi-tui workspace ran tests and passed all 233 of them. The other workspaces failed because they couldn't find the test file (it's in a different package path). The test actually ran and passed via the tui workspace which ran all tests.
Let me run the specific test from the coding-agent package correctly.
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && npx vitest --run test/session-selector-path-delete.test.ts 2>&1RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/session-selector-path-delete.test.ts (5 tests) 10ms
Test Files 1 passed (1)
Tests 5 passed (5)
Start at 20:25:21
Duration 565ms (transform 147ms, setup 0ms, collect 307ms, tests 10ms, environment 0ms, prepare 43ms)All tests pass. Let me summarize the status for the user.
Conflict resolved. Here's the summary:
Conflict: Just the CHANGELOG.md, where both main and the PR added entries under ### Added. Resolved by keeping both entries, with proper attribution added for the external contribution.
Current state:
pr-816-resume-additions is rebased on origin/mainChanges made by us:
DynamicBorder above the headerkeyHint()/rawKeyHint() for consistent styling([#816](https://github.com/badlogic/pi-mono/pull/816) by [@w-winter](https://github.com/w-winter))Ready for you to test interactively with /resume.
test complete, all good. commit our minor change, merge into main, push
The user wants me to:
Let me do this step by step, being careful to only add the specific file I modified.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/modes/interactive/components/session-selector.ts && git commit -m "style(coding-agent): move border above header, use keyHint for consistency"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 408 files in 240ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.0 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[pr-816-resume-additions 298af5c1] style(coding-agent): move border above header, use keyHint for consistency
1 file changed, 13 insertions(+), 6 deletions(-)Good, now merge into main and push.
>cd /Users/badlogic/workspaces/pi-mono && git checkout main && git pull --rebase && git merge pr-816-resume-additions && git pushSwitched to branch 'main'
Your branch is up to date with 'origin/main'.
Already up to date.
Updating d43930c8..298af5c1
Fast-forward
packages/coding-agent/CHANGELOG.md | 1 +
packages/coding-agent/README.md | 4 +
packages/coding-agent/docs/session.md | 6 +
.../interactive/components/session-selector.ts | 390 ++++++++++++++++++---
.../src/modes/interactive/interactive-mode.ts | 1 +
.../test/session-selector-path-delete.test.ts | 185 ++++++++++
6 files changed, 534 insertions(+), 53 deletions(-)
create mode 100644 packages/coding-agent/test/session-selector-path-delete.test.ts
To https://github.com/badlogic/pi-mono.git
d43930c8..298af5c1 main -> mainNow I should close the PR and leave a comment. Let me do that.
>cd /Users/badlogic/workspaces/pi-mono && gh pr close 816 --comment "Merged into main with minor adjustments: moved the border above the header and updated hint text to use the keyHint/rawKeyHint system for consistent styling. Thanks @w-winter!" && git branch -d pr-816-resume-additions✓ Closed pull request badlogic/pi-mono#816 (feat(coding-agent): add session path toggle and deletion to /resume)
Deleted branch pr-816-resume-additions (was 298af5c1).Done! All tasks complete. Let me summarize.
Done.
npm run check # verify it compilesnpm test -- packages/coding-agent/test/session-selector-path-delete.test.ts # run the new teststhis.addChild(new Spacer(1));this.addChild(this.header);this.addChild(new Spacer(1));this.addChild(new DynamicBorder());this.addChild(new Spacer(1));this.addChild(new Spacer(1));this.addChild(new DynamicBorder());this.addChild(new Spacer(1));this.addChild(this.header);this.addChild(new Spacer(1));───────────────────────────────────────────────────────────────────────────────────────────────────────────
Resume Session (Current Folder) ◉ Current Folder | ○ All Sort: FuzzyTab: scope · re:<pattern> for regex · "phrase" for exact phraseCtrl+R: sort · Ctrl+D: delete · Ctrl+P: path (off)
>rawKeyHint("↑↓", "navigate") + " " + keyHint("selectConfirm", "select") + " " + keyHint("selectCancel", "cancel")rawKeyHint("↑↓", "navigate") + " " + keyHint("selectConfirm", "select") + " " + keyHint("selectCancel", "cancel")tab scope · re:<pattern> regex · "phrase" exactctrl+r sort · ctrl+d delete · ctrl+p path (off)