Read PR description, comments, commits, and changed files.
UnderstandReviewed linked issues and comments.
UnderstandAnalyzed PR diff and relevant code files.
UnderstandChecked changelog and documentation for updates.
UnderstandMerged PR, updated changelog, and added comment.
EditYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/1124 ive already pulled the pr down into the active local branch
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.
>gh pr view 1124 --repo badlogic/pi-mono --json title,body,comments,commits,files,author,labels,state,headRefName,baseRefName{"author":{"id":"MDQ6VXNlcjE4NDM5","is_bot":false,"login":"pasky","name":"Petr Baudis"},"baseRefName":"main","body":"Adds a \"Threaded\" sort mode to the /resume session selector that displays sessions in a tree structure based on parent-child (fork) relationships.\r\n\r\n### Changes\r\n\r\n**New \"Threaded\" sort mode (now default)**\r\n- Sessions displayed in tree structure based on parentSession relationships\r\n- Tree sorted by root session's modified date (most recent first)\r\n- Sort mode cycles: Threaded → Recent → Fuzzy → Threaded\r\n\r\n**Compact one-line format**\r\n- One line per session instead of multi-line (message + metadata + blank line)\r\n- Age shown in compact format: now, 5m, 2h, 3d, 2w, 1mo, 1y\r\n- Message count displayed at the beginning as `(X)` before the session label\r\n- Current session highlighted in accent color\r\n- Selected row has background highlight\r\n- Visible sessions increased from 5 to 10\r\n\r\n**Implementation notes**\r\n- Added parentSessionPath to SessionInfo (reads from session header's parentSession field)\r\n- Tree structure only shown in \"Threaded\" mode without search; other modes and search results show flat list\r\n\r\nThis is how it looks now:\r\n<img width=\"1512\" height=\"495\" alt=\"image\" src=\"https://github.com/user-attachments/assets/84905b1b-aadf-45a7-b76b-51672f6a077e\" />\r\n\r\nCloses #1108","comments":[],"commits":[{"authoredDate":"2026-01-31T00:46:06Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjE4NDM5","login":"pasky","name":"Petr Baudis"}],"committedDate":"2026-01-31T22:17:05Z","messageBody":"Adds 'Threaded' as a new sort mode (now default) that displays sessions\nin a tree structure based on parent-child relationships. The tree is\nsorted by root session's modified date. Other sort modes (Recent, Fuzzy)\ncontinue to show flat lists.\n\nSort mode cycles: Threaded -> Recent -> Fuzzy -> Threaded","messageHeadline":"feat(coding-agent): add threaded sort mode to /resume with tree display","oid":"42d54e0d1c4f5378068b0eacf2074b449c4465d3"},{"authoredDate":"2026-01-31T03:26:34Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjE4NDM5","login":"pasky","name":"Petr Baudis"}],"committedDate":"2026-01-31T22:40:02Z","messageBody":"- One line per session instead of multi-line (message + metadata + blank)\n- Age shown in compact format (now, 5m, 2h, 3d, 2w, 1mo, 1y)\n- Message count on the left side\n- Current session highlighted in accent color\n- Selected row has background highlight\n- Increased maxVisible from 5 to 10 sessions","messageHeadline":"feat(coding-agent): compact one-line format for /resume session list","oid":"178e1e11c125ce98b32807da1e7ef4fb68779021"}],"files":[{"path":"packages/coding-agent/src/core/session-manager.ts","additions":4,"deletions":0},{"path":"packages/coding-agent/src/modes/interactive/components/session-selector-search.ts","additions":1,"deletions":1},{"path":"packages/coding-agent/src/modes/interactive/components/session-selector.ts","additions":166,"deletions":51}],"headRefName":"feat/resume-threaded-tree","labels":[],"state":"OPEN","title":"feat(coding-agent): threaded sort mode and compact one-line format for /resume"}>gh issue view 1108 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"### What do you want to change?\n\nTo /resume, add a new sort mode \"threaded\" (new default) that is like \"recent\" but shows child threads linked under their parents instead. (This approach makes the behavior similar to e.g. `mutt`.)\n\nWhile at it, let's make /resume compact like /tree is, with single line per session. (At least it would be my strong aesthetic preference.)\n\n### Why?\n\nCurrently, `/fork` records a link to parent session. Extensions such as `/handoff` can do the same. However, these links aren't presented anywhere. For example AmpCode presents their threads as a tree and it's really convenient when working on a bigger project across multiple sesssions forked / handed off from each other. I find myself often switching between various branches as I make followups to different parts of a bigger functionality.\n\nAlternative minimalistic approach: this session tree could be in an extension, but not as is - in that case, I'd open a PR that makes /resume take a session.jsonl path as an optional argument, so that we can resume arbitrary sessions programmatically. (If so, it would be more consistent to keep /fork in an extension too, though.)\n\n### How? (optional)\n\nmy clanker's summary of a PR i have here if this lgty:\n\n---\n\n feat(coding-agent): add threaded sort mode to /resume with tree display\n\n ### Summary\n\n This PR adds a \"Threaded\" sort mode to the /resume session selector that displays sessions in a tree structure based on parent-child (fork) relationships. The tree view makes it easier\n to navigate related sessions when using /handoff to fork work.\n\n ### Changes\n\n New \"Threaded\" sort mode (now default)\n - Sessions are displayed in a tree structure based on parentSession relationships\n - Tree sorted by root session's modified date (most recent first)\n - Sort mode cycles: Threaded → Recent → Fuzzy → Threaded\n\n Compact one-line format\n - One line per session instead of multi-line (message + metadata + blank line)\n - Age shown in compact format: now, 5m, 2h, 3d, 2w, 1mo, 1y\n - Message count displayed on the right (e.g., 3m for 3 messages)\n - (this is a bit confusing in the end, maybe instead we should show (3) at the _beginning_ of the line)\n - Current session highlighted in accent color\n - Selected row has background highlight\n - Visible sessions increased from 5 to 10\n\n Implementation notes\n - Added parentSessionPath to SessionInfo (reads from session header's parentSession field)\n - Tree structure only shown in \"Threaded\" mode without search; other modes and search results show flat list\n - Minimal changes to existing code structure\n\n ### Screenshots\n\n ```\n session about feature X 2h 5m\n └─ followup on feature X 1h 3m\n unrelated session 3d 12m\n ├─ fork 1 of unrelated 2d 8m\n └─ fork 2 of unrelated 1d 4m\n ```\n\n<img width=\"1512\" height=\"348\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/7375582b-454c-41fc-80d6-c7c85ed074f8\" />","comments":[{"id":"IC_kwDOPbFNk87kO0Aq","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"lgtm please swnd a PR.\n\nI need to know the number of messages for each session for debugging purposes. Please add that in if possible.","createdAt":"2026-01-31T19:08:07Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/1108#issuecomment-3829088298","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87kO0KT","author":{"login":"github-actions"},"authorAssociation":"CONTRIBUTOR","body":"@pasky has been added to the approved contributors list. You can now submit PRs. Thanks for contributing!","createdAt":"2026-01-31T19:08:19Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/1108#issuecomment-3829088915","viewerDidAuthor":false}],"labels":[],"state":"CLOSED","title":"Support browsing session trees (based on fork parent links)"}>gh pr diff 1124 --repo badlogic/pi-monodiff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts
index dbb54f89e..b8c74c693 100644
--- a/packages/coding-agent/src/core/session-manager.ts
+++ b/packages/coding-agent/src/core/session-manager.ts
@@ -168,6 +168,8 @@ export interface SessionInfo {
cwd: string;
/** User-defined display name from session_info entries. */
name?: string;
+ /** Path to the parent session (if this session was forked). */
+ parentSessionPath?: string;
created: Date;
modified: Date;
messageCount: number;
@@ -587,6 +589,7 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
}
const cwd = typeof (header as SessionHeader).cwd === "string" ? (header as SessionHeader).cwd : "";
+ const parentSessionPath = (header as SessionHeader).parentSession;
const modified = getSessionModifiedDate(entries, header as SessionHeader, stats.mtime);
@@ -595,6 +598,7 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
id: (header as SessionHeader).id,
cwd,
name,
+ parentSessionPath,
created: new Date((header as SessionHeader).timestamp),
modified,
messageCount,
diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts b/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts
index b77d2f882..1ec8e0dde 100644
--- a/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts
+++ b/packages/coding-agent/src/modes/interactive/components/session-selector-search.ts
@@ -1,7 +1,7 @@
import { fuzzyMatch } from "@mariozechner/pi-tui";
import type { SessionInfo } from "../../../core/session-manager.js";
-export type SortMode = "recent" | "relevance";
+export type SortMode = "threaded" | "recent" | "relevance";
export interface ParsedSearchQuery {
mode: "tokens" | "regex";
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 49f6c8a04..becfdfe77 100644
--- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts
+++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts
@@ -38,13 +38,13 @@ function formatSessionDate(date: Date): string {
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
- if (diffMins < 1) return "just now";
- if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
- if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
- if (diffDays === 1) return "1 day ago";
- if (diffDays < 7) return `${diffDays} days ago`;
-
- return date.toLocaleDateString();
+ if (diffMins < 1) return "now";
+ if (diffMins < 60) return `${diffMins}m`;
+ if (diffHours < 24) return `${diffHours}h`;
+ if (diffDays < 7) return `${diffDays}d`;
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)}w`;
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo`;
+ return `${Math.floor(diffDays / 365)}y`;
}
class SessionSelectorHeader implements Component {
@@ -119,7 +119,7 @@ class SessionSelectorHeader implements Component {
const title = this.scope === "current" ? "Resume Session (Current Folder)" : "Resume Session (All)";
const leftText = theme.bold(title);
- const sortLabel = this.sortMode === "recent" ? "Recent" : "Fuzzy";
+ const sortLabel = this.sortMode === "threaded" ? "Threaded" : this.sortMode === "recent" ? "Recent" : "Fuzzy";
const sortText = theme.fg("muted", "Sort: ") + theme.fg("accent", sortLabel);
let scopeText: string;
@@ -169,20 +169,95 @@ class SessionSelectorHeader implements Component {
}
}
+/** A session tree node for hierarchical display */
+interface SessionTreeNode {
+ session: SessionInfo;
+ children: SessionTreeNode[];
+}
+
+/** Flattened node for display with tree structure info */
+interface FlatSessionNode {
+ session: SessionInfo;
+ depth: number;
+ isLast: boolean;
+ /** For each ancestor level, whether there are more siblings after it */
+ ancestorContinues: boolean[];
+}
+
+/**
+ * Build a tree structure from sessions based on parentSessionPath.
+ * Returns root nodes sorted by modified date (descending).
+ */
+function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] {
+ const byPath = new Map<string, SessionTreeNode>();
+
+ for (const session of sessions) {
+ byPath.set(session.path, { session, children: [] });
+ }
+
+ const roots: SessionTreeNode[] = [];
+
+ for (const session of sessions) {
+ const node = byPath.get(session.path)!;
+ const parentPath = session.parentSessionPath;
+
+ if (parentPath && byPath.has(parentPath)) {
+ byPath.get(parentPath)!.children.push(node);
+ } else {
+ roots.push(node);
+ }
+ }
+
+ // Sort children and roots by modified date (descending)
+ const sortNodes = (nodes: SessionTreeNode[]): void => {
+ nodes.sort((a, b) => b.session.modified.getTime() - a.session.modified.getTime());
+ for (const node of nodes) {
+ sortNodes(node.children);
+ }
+ };
+ sortNodes(roots);
+
+ return roots;
+}
+
+/**
+ * Flatten tree into display list with tree structure metadata.
+ */
+function flattenSessionTree(roots: SessionTreeNode[]): FlatSessionNode[] {
+ const result: FlatSessionNode[] = [];
+
+ const walk = (node: SessionTreeNode, depth: number, ancestorContinues: boolean[], isLast: boolean): void => {
+ result.push({ session: node.session, depth, isLast, ancestorContinues });
+
+ for (let i = 0; i < node.children.length; i++) {
+ const childIsLast = i === node.children.length - 1;
+ // Only show continuation line for non-root ancestors
+ const continues = depth > 0 ? !isLast : false;
+ walk(node.children[i]!, depth + 1, [...ancestorContinues, continues], childIsLast);
+ }
+ };
+
+ for (let i = 0; i < roots.length; i++) {
+ walk(roots[i]!, 0, [], i === roots.length - 1);
+ }
+
+ return result;
+}
+
/**
* Custom session list component with multi-line items and search
*/
class SessionList implements Component, Focusable {
public getSelectedSessionPath(): string | undefined {
const selected = this.filteredSessions[this.selectedIndex];
- return selected?.path;
+ return selected?.session.path;
}
private allSessions: SessionInfo[] = [];
- private filteredSessions: SessionInfo[] = [];
+ private filteredSessions: FlatSessionNode[] = [];
private selectedIndex: number = 0;
private searchInput: Input;
private showCwd = false;
- private sortMode: SortMode = "relevance";
+ private sortMode: SortMode = "threaded";
private showPath = false;
private confirmingDeletePath: string | null = null;
private currentSessionFilePath?: string;
@@ -196,7 +271,7 @@ class SessionList implements Component, Focusable {
public onDeleteSession?: (sessionPath: string) => Promise<void>;
public onRenameSession?: (sessionPath: string) => void;
public onError?: (message: string) => void;
- private maxVisible: number = 5; // Max sessions visible (each session: message + metadata + optional path + blank)
+ private maxVisible: number = 10; // Max sessions visible (one line each)
// Focusable implementation - propagate to searchInput for IME cursor positioning
private _focused = false;
@@ -210,18 +285,19 @@ class SessionList implements Component, Focusable {
constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {
this.allSessions = sessions;
- this.filteredSessions = sessions;
+ this.filteredSessions = [];
this.searchInput = new Input();
this.showCwd = showCwd;
this.sortMode = sortMode;
this.currentSessionFilePath = currentSessionFilePath;
+ this.filterSessions("");
// Handle Enter in search input - select current item
this.searchInput.onSubmit = () => {
if (this.filteredSessions[this.selectedIndex]) {
const selected = this.filteredSessions[this.selectedIndex];
if (this.onSelect) {
- this.onSelect(selected.path);
+ this.onSelect(selected.session.path);
}
}
};
@@ -239,7 +315,22 @@ class SessionList implements Component, Focusable {
}
private filterSessions(query: string): void {
- this.filteredSessions = filterAndSortSessions(this.allSessions, query, this.sortMode);
+ const trimmed = query.trim();
+
+ if (this.sortMode === "threaded" && !trimmed) {
+ // Threaded mode without search: show tree structure
+ const roots = buildSessionTree(this.allSessions);
+ this.filteredSessions = flattenSessionTree(roots);
+ } else {
+ // Other modes or with search: flat list
+ const filtered = trimmed ? filterAndSortSessions(this.allSessions, query, this.sortMode) : this.allSessions;
+ this.filteredSessions = filtered.map((session) => ({
+ session,
+ depth: 0,
+ isLast: true,
+ ancestorContinues: [],
+ }));
+ }
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
}
@@ -253,12 +344,12 @@ class SessionList implements Component, Focusable {
if (!selected) return;
// Prevent deleting current session
- if (this.currentSessionFilePath && selected.path === this.currentSessionFilePath) {
+ if (this.currentSessionFilePath && selected.session.path === this.currentSessionFilePath) {
this.onError?.("Cannot delete the currently active session");
return;
}
- this.setConfirmingDeletePath(selected.path);
+ this.setConfirmingDeletePath(selected.session.path);
}
invalidate(): void {}
@@ -293,25 +384,50 @@ class SessionList implements Component, Focusable {
);
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
- // Render visible sessions (message + metadata + optional path + blank line)
+ // Render visible sessions (one line each with tree structure)
for (let i = startIndex; i < endIndex; i++) {
- const session = this.filteredSessions[i];
+ const node = this.filteredSessions[i]!;
+ const session = node.session;
const isSelected = i === this.selectedIndex;
const isConfirmingDelete = session.path === this.confirmingDeletePath;
+ const isCurrent = this.currentSessionFilePath === session.path;
+
+ // Build tree prefix
+ const prefix = this.buildTreePrefix(node);
- // Use session name if set, otherwise first message
+ // Session display text (name or first message)
const hasName = !!session.name;
const displayText = session.name ?? session.firstMessage;
const normalizedMessage = displayText.replace(/\n/g, " ").trim();
- // First line: cursor + message (truncate to visible width)
- // Use warning color for custom names to distinguish from first message
+ // Right side: age
+ const age = formatSessionDate(session.modified);
+ let rightPart = age;
+ if (this.showCwd && session.cwd) {
+ rightPart = `${shortenPath(session.cwd)} ${rightPart}`;
+ }
+ if (this.showPath) {
+ rightPart = `${shortenPath(session.path)} ${rightPart}`;
+ }
+
+ // Cursor and message count prefix
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
- const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
- const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
- let messageColor: "error" | "warning" | null = null;
+ const msgCountPrefix = `(${session.messageCount}) `;
+
+ // Calculate available width for message
+ const prefixWidth = visibleWidth(prefix);
+ const msgCountWidth = visibleWidth(msgCountPrefix);
+ const rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing
+ const availableForMsg = width - 2 - prefixWidth - msgCountWidth - rightWidth; // -2 for cursor
+
+ const truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), "…");
+
+ // Style message
+ let messageColor: "error" | "warning" | "accent" | null = null;
if (isConfirmingDelete) {
messageColor = "error";
+ } else if (isCurrent) {
+ messageColor = "accent";
} else if (hasName) {
messageColor = "warning";
}
@@ -319,31 +435,19 @@ class SessionList implements Component, Focusable {
if (isSelected) {
styledMsg = theme.bold(styledMsg);
}
- const messageLine = cursor + styledMsg;
- // Second line: metadata (dimmed) - also truncate for safety
- const modified = formatSessionDate(session.modified);
- const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
- const metadataParts = [modified, msgCount];
- if (this.showCwd && session.cwd) {
- metadataParts.push(shortenPath(session.cwd));
- }
- const metadata = ` ${metadataParts.join(" · ")}`;
- const truncatedMetadata = truncateToWidth(metadata, width, "");
- const metadataLine = theme.fg(isConfirmingDelete ? "error" : "dim", truncatedMetadata);
-
- lines.push(messageLine);
- lines.push(metadataLine);
+ // Build line
+ const styledMsgCount = theme.fg("dim", msgCountPrefix);
+ const leftPart = cursor + theme.fg("dim", prefix) + styledMsgCount + styledMsg;
+ const leftWidth = visibleWidth(leftPart);
+ const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));
+ const styledRight = theme.fg(isConfirmingDelete ? "error" : "dim", rightPart);
- // 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);
+ let line = leftPart + " ".repeat(spacing) + styledRight;
+ if (isSelected) {
+ line = theme.bg("selectedBg", line);
}
-
- lines.push(""); // Blank line between sessions
+ lines.push(truncateToWidth(line, width));
}
// Add scroll indicator if needed
@@ -356,6 +460,16 @@ class SessionList implements Component, Focusable {
return lines;
}
+ private buildTreePrefix(node: FlatSessionNode): string {
+ if (node.depth === 0) {
+ return "";
+ }
+
+ const parts = node.ancestorContinues.map((continues) => (continues ? "│ " : " "));
+ const branch = node.isLast ? "└─ " : "├─ ";
+ return parts.join("") + branch;
+ }
+
handleInput(keyData: string): void {
const kb = getEditorKeybindings();
@@ -405,7 +519,7 @@ class SessionList implements Component, Focusable {
if (matchesKey(keyData, "ctrl+r")) {
const selected = this.filteredSessions[this.selectedIndex];
if (selected) {
- this.onRenameSession?.(selected.path);
+ this.onRenameSession?.(selected.session.path);
}
return;
}
@@ -443,7 +557,7 @@ class SessionList implements Component, Focusable {
else if (kb.matches(keyData, "selectConfirm")) {
const selected = this.filteredSessions[this.selectedIndex];
if (selected && this.onSelect) {
- this.onSelect(selected.path);
+ this.onSelect(selected.session.path);
}
}
// Escape - cancel
@@ -524,7 +638,7 @@ export class SessionSelectorComponent extends Container implements Focusable {
private sessionList: SessionList;
private header: SessionSelectorHeader;
private scope: SessionScope = "current";
- private sortMode: SortMode = "relevance";
+ private sortMode: SortMode = "threaded";
private currentSessions: SessionInfo[] | null = null;
private allSessions: SessionInfo[] | null = null;
private currentSessionsLoader: SessionsLoader;
@@ -793,7 +907,8 @@ export class SessionSelectorComponent extends Container implements Focusable {
}
private toggleSortMode(): void {
- this.sortMode = this.sortMode === "recent" ? "relevance" : "recent";
+ // Cycle: threaded -> recent -> relevance -> threaded
+ this.sortMode = this.sortMode === "threaded" ? "recent" : this.sortMode === "recent" ? "relevance" : "threaded";
this.header.setSortMode(this.sortMode);
this.sessionList.setSortMode(this.sortMode);
this.requestRender();Now let me read the relevant files in full to validate the changes:
Now let me check the changelog:
Let me check the docs directory for any documentation that might need updating:
>ls -la /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/total 552
drwxr-xr-x@ 24 badlogic staff 768 Feb 1 00:58 .
drwxr-xr-x@ 18 badlogic staff 576 Feb 1 00:58 ..
-rw-r--r--@ 1 badlogic staff 15999 Jan 26 01:23 compaction.md
-rw-r--r--@ 1 badlogic staff 16373 Jan 25 03:33 custom-provider.md
-rw-r--r--@ 1 badlogic staff 1327 Jan 26 02:06 development.md
-rw-r--r--@ 1 badlogic staff 56719 Jan 30 21:42 extensions.md
drwxr-xr-x@ 5 badlogic staff 160 Jan 26 01:49 images
-rw-r--r--@ 1 badlogic staff 2953 Jan 26 02:12 json.md
-rw-r--r--@ 1 badlogic staff 5734 Jan 31 23:09 keybindings.md
-rw-r--r--@ 1 badlogic staff 5784 Jan 30 02:25 models.md
-rw-r--r--@ 1 badlogic staff 5719 Jan 27 00:12 packages.md
-rw-r--r--@ 1 badlogic staff 1855 Jan 25 20:56 prompt-templates.md
-rw-r--r--@ 1 badlogic staff 4368 Jan 30 21:42 providers.md
-rw-r--r--@ 1 badlogic staff 26191 Jan 30 02:25 rpc.md
-rw-r--r--@ 1 badlogic staff 27799 Jan 25 03:33 sdk.md
-rw-r--r--@ 1 badlogic staff 14289 Jan 26 01:22 session.md
-rw-r--r--@ 1 badlogic staff 6356 Feb 1 00:58 settings.md
-rw-r--r--@ 1 badlogic staff 356 Jan 25 22:11 shell-aliases.md
-rw-r--r--@ 1 badlogic staff 6121 Jan 26 12:37 skills.md
-rw-r--r--@ 1 badlogic staff 1520 Jan 25 21:33 terminal-setup.md
-rw-r--r--@ 1 badlogic staff 7934 Jan 25 20:48 themes.md
-rw-r--r--@ 1 badlogic staff 6645 Jan 16 21:49 tree.md
-rw-r--r--@ 1 badlogic staff 27867 Jan 26 22:01 tui.md
-rw-r--r--@ 1 badlogic staff 394 Jan 25 22:11 windows.mdLet me check the keybindings.md and settings.md for any relevant sort mode references:
>grep -n -i "resume\|session.*sort\|recent\|fuzzy\|threaded" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/keybindings.md /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/settings.md 2>/dev/null/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/keybindings.md:58:| `yank` | `ctrl+y` | Paste most recently deleted text |
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/keybindings.md:126:| `toggleSessionSort` | `ctrl+s` | Toggle sort mode |
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/settings.md:55:| `compaction.keepRecentTokens` | number | `20000` | Recent tokens to keep (not summarized) |
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/settings.md:62: "keepRecentTokens": 20000
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/settings.md:185: "keepRecentTokens": 20000Now let me verify the current state of the local branch to ensure I have the PR changes:
>cd /Users/badlogic/workspaces/pi-mono && git log --oneline -5 && echo "---" && git branch --show-current178e1e11 feat(coding-agent): compact one-line format for /resume session list
42d54e0d feat(coding-agent): add threaded sort mode to /resume with tree display
6f2d0663 feat(coding-agent): add keybinding actions for /tree, /fork, and /new
d075291b feat(tui): add sticky column for vertical cursor navigation (#1120)
675136f0 chore: approve contributor pasky
---
pr-1124/Users/badlogic/Desktop/Screenshot\ 2026-02-01\ at\ 01.00.13.png
works as intended, but the message number messes with indentation. propose a better layout for the message number, title, and time
I see the issue. The message count (60), (34), (245), etc. has variable width which causes the session titles to start at different horizontal positions. This creates a jagged, hard-to-scan appearance.
Layout Options:
Option 1: Right-align message count in a fixed-width column
(60) have a look at this commit: https://github... 1d (34) pull this into a local branch 1d (245) Issue: JSON serialization failure in tool... 1d (119) read @packages/tui/src/tui.ts then this... 1dPad the count to a fixed width (e.g., 5 chars for (xxx)) so titles align.
Option 2: Move message count to the right side with the age
have a look at this commit: https://github... 60 1d pull this into a local branch 34 1d Issue: JSON serialization failure in tool... 245 1dAge and count become a right-aligned metadata block. Titles all start at the same column.
Option 3: Compact message count after the title (before age)
have a look at this commit: https://github... (60) 1d pull this into a local branch (34) 1d Issue: JSON serialization failure in tool... (245) 1dMessage count is part of the right-side metadata, keeping left alignment clean.
Recommendation: Option 2 or 3. Both keep titles aligned at the left edge, which is easier to scan. Option 2 is cleaner if you drop the parentheses; Option 3 keeps the visual distinction with parentheses.
The tree indentation for child sessions would stay consistent since the prefix (└─, ├─) comes before the title, not the count.
2 is good
I'll implement Option 2: move the message count to the right side alongside the age.
Now I'll make the edit to move the message count to the right side:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 450 files in 325ms. No fixes applied.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 40ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.The check passes. The infos are pre-existing in unrelated files. Let me verify the change looks correct:
The change is complete. Now sessions will display like:
have a look at this commit: https://github... 60 1d pull this into a local branch 34 1d Issue: JSON serialization failure in tool... 245 1dThe message count is now on the right side before the age, and all session titles align at the same column.
ok, love it, commit, merge into main, close pr with a ocmment in my tone describing what we changed and thanking the contributor. make sure changelog has an entry
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch pr-1124
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/CHANGELOG.md
modified: packages/coding-agent/src/modes/interactive/components/session-selector.ts
no changes added to commit (use "git add" and/or "git commit -a")>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/CHANGELOG.md packages/coding-agent/src/modes/interactive/components/session-selector.ts && git commit -m "feat(coding-agent): threaded sort mode and compact format for /resume
Adds 'Threaded' as a new sort mode (now default) that displays sessions
in a tree structure based on parent-child relationships. Compact one-line
format with message count and age right-aligned for clean title alignment.
Closes #1108"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 450 files in 292ms. No fixes applied.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 36ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.
✅ All pre-commit checks passed!
[pr-1124 2a631a24] feat(coding-agent): threaded sort mode and compact format for /resume
2 files changed, 7 insertions(+), 8 deletions(-)>cd /Users/badlogic/workspaces/pi-mono && git checkout main && git pull --rebase && git merge pr-1124 --no-ff -m "feat(coding-agent): threaded sort mode and compact format for /resume (#1124)
Adds 'Threaded' as a new sort mode (now default) that displays sessions
in a tree structure based on parent-child relationships. Compact one-line
format with message count and age right-aligned for clean title alignment.
Closes #1108"Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Already up to date.
Auto-merging packages/coding-agent/CHANGELOG.md
CONFLICT (content): Merge conflict in packages/coding-agent/CHANGELOG.md
Automatic merge failed; fix conflicts and then commit the result.
Command exited with code 1>cd /Users/badlogic/workspaces/pi-mono && cat packages/coding-agent/CHANGELOG.md | head -20# Changelog
## [Unreleased]
### Added
- Added `newSession`, `tree`, and `fork` keybinding actions for `/new`, `/tree`, and `/fork` commands. All unbound by default. ([#1114](https://github.com/badlogic/pi-mono/pull/1114) by [@juanibiapina](https://github.com/juanibiapina))
<<<<<<< HEAD
- Added `retry.maxDelayMs` setting to cap maximum server-requested retry delay. When a provider requests a longer delay (e.g., Google's "quota will reset after 5h"), the request fails immediately with an informative error instead of waiting silently. Default: 60000ms (60 seconds). ([#1123](https://github.com/badlogic/pi-mono/issues/1123))
### Fixed
- Fixed `switchSession()` appending spurious `thinking_level_change` entry to session log on resume. `setThinkingLevel()` is now idempotent. ([#1118](https://github.com/badlogic/pi-mono/issues/1118))
- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))
=======
- `/resume` session picker: new "Threaded" sort mode (now default) displays sessions in a tree structure based on fork relationships. Compact one-line format with message count and age on the right. ([#1124](https://github.com/badlogic/pi-mono/pull/1124) by [@pasky](https://github.com/pasky))
>>>>>>> pr-1124
## [0.50.7] - 2026-01-31>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/CHANGELOG.md && git commit -m "feat(coding-agent): threaded sort mode and compact format for /resume (#1124)
Adds 'Threaded' as a new sort mode (now default) that displays sessions
in a tree structure based on parent-child relationships. Compact one-line
format with message count and age right-aligned for clean title alignment.
Closes #1108"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write --error-on-warnings . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 451 files in 295ms. No fixes applied.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write --error-on-warnings . && tsc --noEmit && cd example && biome check --write --error-on-warnings . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[main 6e4508f1] feat(coding-agent): threaded sort mode and compact format for /resume (#1124)>cd /Users/badlogic/workspaces/pi-mono && git push origin mainTo https://github.com/badlogic/pi-mono.git
030a61d8..6e4508f1 main -> main>gh pr close 1124 --repo badlogic/pi-mono --comment "Merged with a small tweak: moved the message count to the right side next to the age so session titles align cleanly. Looks great now.
Thanks @pasky for the contribution! The threaded view is exactly what I needed for navigating forked sessions."X Pull request badlogic/pi-mono#1124 (feat(coding-agent): threaded sort mode and compact one-line format for /resume) can't be closed because it was already merged
Command exited with code 1The PR was auto-closed when the commit referencing Closes #1108 was pushed. Let me add the comment:
>gh pr comment 1124 --repo badlogic/pi-mono --body "Merged with a small tweak: moved the message count to the right side next to the age so session titles align cleanly. Looks great now.
Thanks @pasky for the contribution! The threaded view is exactly what I needed for navigating forked sessions."https://github.com/badlogic/pi-mono/pull/1124#issuecomment-3829755499>cd /Users/badlogic/workspaces/pi-mono && git branch -d pr-1124Deleted branch pr-1124 (was 2a631a24).Done. Merged PR #1124 with the layout fix (message count moved to right side), added changelog entry, pushed to main, and left a comment thanking @pasky.
(60) have a look at this commit: https://github... 1d (34) pull this into a local branch 1d (245) Issue: JSON serialization failure in tool... 1d (119) read @packages/tui/src/tui.ts then this... 1d have a look at this commit: https://github... 60 1d pull this into a local branch 34 1d Issue: JSON serialization failure in tool... 245 1d have a look at this commit: https://github... (60) 1d pull this into a local branch (34) 1d Issue: JSON serialization failure in tool... (245) 1d have a look at this commit: https://github... 60 1d pull this into a local branch 34 1d Issue: JSON serialization failure in tool... 245 1d