mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 10:02:23 +00:00
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
This commit is contained in:
commit
6e4508f129
4 changed files with 171 additions and 53 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
- 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))
|
- 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))
|
||||||
- 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))
|
- 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))
|
||||||
|
- `/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))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,8 @@ export interface SessionInfo {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
/** User-defined display name from session_info entries. */
|
/** User-defined display name from session_info entries. */
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/** Path to the parent session (if this session was forked). */
|
||||||
|
parentSessionPath?: string;
|
||||||
created: Date;
|
created: Date;
|
||||||
modified: Date;
|
modified: Date;
|
||||||
messageCount: number;
|
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 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);
|
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,
|
id: (header as SessionHeader).id,
|
||||||
cwd,
|
cwd,
|
||||||
name,
|
name,
|
||||||
|
parentSessionPath,
|
||||||
created: new Date((header as SessionHeader).timestamp),
|
created: new Date((header as SessionHeader).timestamp),
|
||||||
modified,
|
modified,
|
||||||
messageCount,
|
messageCount,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { fuzzyMatch } from "@mariozechner/pi-tui";
|
import { fuzzyMatch } from "@mariozechner/pi-tui";
|
||||||
import type { SessionInfo } from "../../../core/session-manager.js";
|
import type { SessionInfo } from "../../../core/session-manager.js";
|
||||||
|
|
||||||
export type SortMode = "recent" | "relevance";
|
export type SortMode = "threaded" | "recent" | "relevance";
|
||||||
|
|
||||||
export interface ParsedSearchQuery {
|
export interface ParsedSearchQuery {
|
||||||
mode: "tokens" | "regex";
|
mode: "tokens" | "regex";
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ function formatSessionDate(date: Date): string {
|
||||||
const diffHours = Math.floor(diffMs / 3600000);
|
const diffHours = Math.floor(diffMs / 3600000);
|
||||||
const diffDays = Math.floor(diffMs / 86400000);
|
const diffDays = Math.floor(diffMs / 86400000);
|
||||||
|
|
||||||
if (diffMins < 1) return "just now";
|
if (diffMins < 1) return "now";
|
||||||
if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`;
|
if (diffMins < 60) return `${diffMins}m`;
|
||||||
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
|
if (diffHours < 24) return `${diffHours}h`;
|
||||||
if (diffDays === 1) return "1 day ago";
|
if (diffDays < 7) return `${diffDays}d`;
|
||||||
if (diffDays < 7) return `${diffDays} days ago`;
|
if (diffDays < 30) return `${Math.floor(diffDays / 7)}w`;
|
||||||
|
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo`;
|
||||||
return date.toLocaleDateString();
|
return `${Math.floor(diffDays / 365)}y`;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SessionSelectorHeader implements Component {
|
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 title = this.scope === "current" ? "Resume Session (Current Folder)" : "Resume Session (All)";
|
||||||
const leftText = theme.bold(title);
|
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);
|
const sortText = theme.fg("muted", "Sort: ") + theme.fg("accent", sortLabel);
|
||||||
|
|
||||||
let scopeText: string;
|
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
|
* Custom session list component with multi-line items and search
|
||||||
*/
|
*/
|
||||||
class SessionList implements Component, Focusable {
|
class SessionList implements Component, Focusable {
|
||||||
public getSelectedSessionPath(): string | undefined {
|
public getSelectedSessionPath(): string | undefined {
|
||||||
const selected = this.filteredSessions[this.selectedIndex];
|
const selected = this.filteredSessions[this.selectedIndex];
|
||||||
return selected?.path;
|
return selected?.session.path;
|
||||||
}
|
}
|
||||||
private allSessions: SessionInfo[] = [];
|
private allSessions: SessionInfo[] = [];
|
||||||
private filteredSessions: SessionInfo[] = [];
|
private filteredSessions: FlatSessionNode[] = [];
|
||||||
private selectedIndex: number = 0;
|
private selectedIndex: number = 0;
|
||||||
private searchInput: Input;
|
private searchInput: Input;
|
||||||
private showCwd = false;
|
private showCwd = false;
|
||||||
private sortMode: SortMode = "relevance";
|
private sortMode: SortMode = "threaded";
|
||||||
private showPath = false;
|
private showPath = false;
|
||||||
private confirmingDeletePath: string | null = null;
|
private confirmingDeletePath: string | null = null;
|
||||||
private currentSessionFilePath?: string;
|
private currentSessionFilePath?: string;
|
||||||
|
|
@ -196,7 +271,7 @@ class SessionList implements Component, Focusable {
|
||||||
public onDeleteSession?: (sessionPath: string) => Promise<void>;
|
public onDeleteSession?: (sessionPath: string) => Promise<void>;
|
||||||
public onRenameSession?: (sessionPath: string) => void;
|
public onRenameSession?: (sessionPath: string) => void;
|
||||||
public onError?: (message: 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
|
// Focusable implementation - propagate to searchInput for IME cursor positioning
|
||||||
private _focused = false;
|
private _focused = false;
|
||||||
|
|
@ -210,18 +285,19 @@ class SessionList implements Component, Focusable {
|
||||||
|
|
||||||
constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {
|
constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode, currentSessionFilePath?: string) {
|
||||||
this.allSessions = sessions;
|
this.allSessions = sessions;
|
||||||
this.filteredSessions = sessions;
|
this.filteredSessions = [];
|
||||||
this.searchInput = new Input();
|
this.searchInput = new Input();
|
||||||
this.showCwd = showCwd;
|
this.showCwd = showCwd;
|
||||||
this.sortMode = sortMode;
|
this.sortMode = sortMode;
|
||||||
this.currentSessionFilePath = currentSessionFilePath;
|
this.currentSessionFilePath = currentSessionFilePath;
|
||||||
|
this.filterSessions("");
|
||||||
|
|
||||||
// Handle Enter in search input - select current item
|
// Handle Enter in search input - select current item
|
||||||
this.searchInput.onSubmit = () => {
|
this.searchInput.onSubmit = () => {
|
||||||
if (this.filteredSessions[this.selectedIndex]) {
|
if (this.filteredSessions[this.selectedIndex]) {
|
||||||
const selected = this.filteredSessions[this.selectedIndex];
|
const selected = this.filteredSessions[this.selectedIndex];
|
||||||
if (this.onSelect) {
|
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 {
|
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));
|
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;
|
if (!selected) return;
|
||||||
|
|
||||||
// Prevent deleting current session
|
// 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");
|
this.onError?.("Cannot delete the currently active session");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setConfirmingDeletePath(selected.path);
|
this.setConfirmingDeletePath(selected.session.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidate(): void {}
|
invalidate(): void {}
|
||||||
|
|
@ -293,25 +384,49 @@ class SessionList implements Component, Focusable {
|
||||||
);
|
);
|
||||||
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
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++) {
|
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 isSelected = i === this.selectedIndex;
|
||||||
const isConfirmingDelete = session.path === this.confirmingDeletePath;
|
const isConfirmingDelete = session.path === this.confirmingDeletePath;
|
||||||
|
const isCurrent = this.currentSessionFilePath === session.path;
|
||||||
|
|
||||||
// Use session name if set, otherwise first message
|
// Build tree prefix
|
||||||
|
const prefix = this.buildTreePrefix(node);
|
||||||
|
|
||||||
|
// Session display text (name or first message)
|
||||||
const hasName = !!session.name;
|
const hasName = !!session.name;
|
||||||
const displayText = session.name ?? session.firstMessage;
|
const displayText = session.name ?? session.firstMessage;
|
||||||
const normalizedMessage = displayText.replace(/\n/g, " ").trim();
|
const normalizedMessage = displayText.replace(/\n/g, " ").trim();
|
||||||
|
|
||||||
// First line: cursor + message (truncate to visible width)
|
// Right side: message count and age
|
||||||
// Use warning color for custom names to distinguish from first message
|
const age = formatSessionDate(session.modified);
|
||||||
|
const msgCount = String(session.messageCount);
|
||||||
|
let rightPart = `${msgCount} ${age}`;
|
||||||
|
if (this.showCwd && session.cwd) {
|
||||||
|
rightPart = `${shortenPath(session.cwd)} ${rightPart}`;
|
||||||
|
}
|
||||||
|
if (this.showPath) {
|
||||||
|
rightPart = `${shortenPath(session.path)} ${rightPart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor
|
||||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||||
const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
|
|
||||||
const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
|
// Calculate available width for message
|
||||||
let messageColor: "error" | "warning" | null = null;
|
const prefixWidth = visibleWidth(prefix);
|
||||||
|
const rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing
|
||||||
|
const availableForMsg = width - 2 - prefixWidth - rightWidth; // -2 for cursor
|
||||||
|
|
||||||
|
const truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), "…");
|
||||||
|
|
||||||
|
// Style message
|
||||||
|
let messageColor: "error" | "warning" | "accent" | null = null;
|
||||||
if (isConfirmingDelete) {
|
if (isConfirmingDelete) {
|
||||||
messageColor = "error";
|
messageColor = "error";
|
||||||
|
} else if (isCurrent) {
|
||||||
|
messageColor = "accent";
|
||||||
} else if (hasName) {
|
} else if (hasName) {
|
||||||
messageColor = "warning";
|
messageColor = "warning";
|
||||||
}
|
}
|
||||||
|
|
@ -319,31 +434,18 @@ class SessionList implements Component, Focusable {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
styledMsg = theme.bold(styledMsg);
|
styledMsg = theme.bold(styledMsg);
|
||||||
}
|
}
|
||||||
const messageLine = cursor + styledMsg;
|
|
||||||
|
|
||||||
// Second line: metadata (dimmed) - also truncate for safety
|
// Build line
|
||||||
const modified = formatSessionDate(session.modified);
|
const leftPart = cursor + theme.fg("dim", prefix) + styledMsg;
|
||||||
const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
|
const leftWidth = visibleWidth(leftPart);
|
||||||
const metadataParts = [modified, msgCount];
|
const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));
|
||||||
if (this.showCwd && session.cwd) {
|
const styledRight = theme.fg(isConfirmingDelete ? "error" : "dim", rightPart);
|
||||||
metadataParts.push(shortenPath(session.cwd));
|
|
||||||
|
let line = leftPart + " ".repeat(spacing) + styledRight;
|
||||||
|
if (isSelected) {
|
||||||
|
line = theme.bg("selectedBg", line);
|
||||||
}
|
}
|
||||||
const metadata = ` ${metadataParts.join(" · ")}`;
|
lines.push(truncateToWidth(line, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add scroll indicator if needed
|
// Add scroll indicator if needed
|
||||||
|
|
@ -356,6 +458,16 @@ class SessionList implements Component, Focusable {
|
||||||
return lines;
|
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 {
|
handleInput(keyData: string): void {
|
||||||
const kb = getEditorKeybindings();
|
const kb = getEditorKeybindings();
|
||||||
|
|
||||||
|
|
@ -405,7 +517,7 @@ class SessionList implements Component, Focusable {
|
||||||
if (matchesKey(keyData, "ctrl+r")) {
|
if (matchesKey(keyData, "ctrl+r")) {
|
||||||
const selected = this.filteredSessions[this.selectedIndex];
|
const selected = this.filteredSessions[this.selectedIndex];
|
||||||
if (selected) {
|
if (selected) {
|
||||||
this.onRenameSession?.(selected.path);
|
this.onRenameSession?.(selected.session.path);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +555,7 @@ class SessionList implements Component, Focusable {
|
||||||
else if (kb.matches(keyData, "selectConfirm")) {
|
else if (kb.matches(keyData, "selectConfirm")) {
|
||||||
const selected = this.filteredSessions[this.selectedIndex];
|
const selected = this.filteredSessions[this.selectedIndex];
|
||||||
if (selected && this.onSelect) {
|
if (selected && this.onSelect) {
|
||||||
this.onSelect(selected.path);
|
this.onSelect(selected.session.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Escape - cancel
|
// Escape - cancel
|
||||||
|
|
@ -524,7 +636,7 @@ export class SessionSelectorComponent extends Container implements Focusable {
|
||||||
private sessionList: SessionList;
|
private sessionList: SessionList;
|
||||||
private header: SessionSelectorHeader;
|
private header: SessionSelectorHeader;
|
||||||
private scope: SessionScope = "current";
|
private scope: SessionScope = "current";
|
||||||
private sortMode: SortMode = "relevance";
|
private sortMode: SortMode = "threaded";
|
||||||
private currentSessions: SessionInfo[] | null = null;
|
private currentSessions: SessionInfo[] | null = null;
|
||||||
private allSessions: SessionInfo[] | null = null;
|
private allSessions: SessionInfo[] | null = null;
|
||||||
private currentSessionsLoader: SessionsLoader;
|
private currentSessionsLoader: SessionsLoader;
|
||||||
|
|
@ -793,7 +905,8 @@ export class SessionSelectorComponent extends Container implements Focusable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleSortMode(): void {
|
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.header.setSortMode(this.sortMode);
|
||||||
this.sessionList.setSortMode(this.sortMode);
|
this.sessionList.setSortMode(this.sortMode);
|
||||||
this.requestRender();
|
this.requestRender();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue