163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
import type { Component } from "@mariozechner/pi-tui";
|
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
import type { KeybindingsManager, Theme } from "@mariozechner/pi-coding-agent";
|
|
import type { SessionInfo } from "../types.js";
|
|
|
|
function middleTruncate(text: string, maxWidth: number): string {
|
|
if (visibleWidth(text) <= maxWidth) {
|
|
return text;
|
|
}
|
|
if (maxWidth <= 3) {
|
|
return truncateToWidth(text, maxWidth, "");
|
|
}
|
|
|
|
const chars = [...text];
|
|
const targetSideWidth = Math.max(1, Math.floor((maxWidth - 1) / 2));
|
|
|
|
let left = "";
|
|
for (const char of chars) {
|
|
if (visibleWidth(left + char) > targetSideWidth) break;
|
|
left += char;
|
|
}
|
|
|
|
let right = "";
|
|
for (const char of chars.slice().reverse()) {
|
|
if (visibleWidth(char + right) > targetSideWidth) break;
|
|
right = char + right;
|
|
}
|
|
|
|
return truncateToWidth(`${left}…${right}`, maxWidth, "");
|
|
}
|
|
|
|
function shortSessionId(sessionId: string): string {
|
|
return sessionId.slice(0, 8);
|
|
}
|
|
|
|
function sessionTitle(session: SessionInfo, options?: { self?: boolean; sameCwd?: boolean }): string {
|
|
const name = session.name || "Unnamed session";
|
|
const tags = [options?.self ? "self" : undefined, options?.sameCwd ? "same cwd" : undefined]
|
|
.filter((tag): tag is string => Boolean(tag));
|
|
const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
return `${name} (${shortSessionId(session.id)})${suffix}`;
|
|
}
|
|
|
|
export class SessionListOverlay implements Component {
|
|
private theme: Theme;
|
|
private keybindings: KeybindingsManager;
|
|
private currentSession: SessionInfo;
|
|
private done: (result: SessionInfo | undefined) => void;
|
|
private sessions: SessionInfo[];
|
|
private selectedIndex = 0;
|
|
private maxVisible = 8;
|
|
|
|
constructor(
|
|
theme: Theme,
|
|
keybindings: KeybindingsManager,
|
|
currentSession: SessionInfo,
|
|
sessions: SessionInfo[],
|
|
done: (result: SessionInfo | undefined) => void,
|
|
) {
|
|
this.theme = theme;
|
|
this.keybindings = keybindings;
|
|
this.currentSession = currentSession;
|
|
this.sessions = sessions;
|
|
this.done = done;
|
|
}
|
|
|
|
private onSessionSelect(sessionId: string): void {
|
|
const session = this.sessions.find(s => s.id === sessionId);
|
|
if (!session) return;
|
|
this.done(session);
|
|
}
|
|
|
|
invalidate(): void {}
|
|
|
|
handleInput(data: string): void {
|
|
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
this.done(undefined);
|
|
return;
|
|
}
|
|
|
|
if (this.sessions.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
this.selectedIndex = this.selectedIndex === 0 ? this.sessions.length - 1 : this.selectedIndex - 1;
|
|
return;
|
|
}
|
|
|
|
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
this.selectedIndex = this.selectedIndex === this.sessions.length - 1 ? 0 : this.selectedIndex + 1;
|
|
return;
|
|
}
|
|
|
|
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
const session = this.sessions[this.selectedIndex];
|
|
if (session) {
|
|
this.onSessionSelect(session.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
render(width: number): string[] {
|
|
const innerWidth = Math.max(36, Math.min(width - 2, 88));
|
|
const contentWidth = Math.max(1, innerWidth - 2);
|
|
const footer = `${this.keybindings.getKeys("tui.select.confirm").join("/")}: Message • ${this.keybindings.getKeys("tui.select.cancel").join("/")}: Close`;
|
|
const border = (text: string) => this.theme.fg("accent", text);
|
|
const row = (text = "") => {
|
|
const clipped = truncateToWidth(text, contentWidth, "", true);
|
|
return `${border("│")}${clipped}${" ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)))}${border("│")}`;
|
|
};
|
|
|
|
const lines: string[] = [];
|
|
lines.push(border(`╭${"─".repeat(contentWidth)}╮`));
|
|
lines.push(row(this.theme.bold(" Current Session")));
|
|
lines.push(border(`├${"─".repeat(contentWidth)}┤`));
|
|
lines.push(row());
|
|
lines.push(row(` ${this.theme.fg("dim", sessionTitle(this.currentSession, { self: true }))}`));
|
|
lines.push(row(` ${this.theme.fg("dim", `${middleTruncate(this.currentSession.cwd, Math.max(8, contentWidth - 4))} • ${this.currentSession.model}`)}`));
|
|
lines.push(row());
|
|
lines.push(border(`├${"─".repeat(contentWidth)}┤`));
|
|
lines.push(row(this.theme.bold(" Other Sessions")));
|
|
lines.push(row());
|
|
|
|
if (this.sessions.length === 0) {
|
|
lines.push(row(this.theme.fg("dim", " No other intercom-connected sessions")));
|
|
} else {
|
|
const startIndex = Math.max(
|
|
0,
|
|
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.sessions.length - this.maxVisible),
|
|
);
|
|
const endIndex = Math.min(startIndex + this.maxVisible, this.sessions.length);
|
|
|
|
for (let index = startIndex; index < endIndex; index += 1) {
|
|
const session = this.sessions[index];
|
|
const isSelected = index === this.selectedIndex;
|
|
const sameCwd = session.cwd === this.currentSession.cwd;
|
|
const prefix = isSelected ? this.theme.fg("accent", "→ ") : " ";
|
|
const title = sessionTitle(session, { sameCwd });
|
|
const pathText = `${middleTruncate(session.cwd, Math.max(8, contentWidth - 4))} • ${session.model}`;
|
|
|
|
lines.push(row(`${prefix}${isSelected ? this.theme.fg("accent", title) : title}`));
|
|
lines.push(row(` ${this.theme.fg("dim", pathText)}`));
|
|
if (index < endIndex - 1) {
|
|
lines.push(row());
|
|
}
|
|
}
|
|
|
|
if (startIndex > 0 || endIndex < this.sessions.length) {
|
|
lines.push(row());
|
|
lines.push(row(this.theme.fg("dim", ` ${this.selectedIndex + 1}/${this.sessions.length}`)));
|
|
}
|
|
}
|
|
|
|
lines.push(row());
|
|
lines.push(border(`├${"─".repeat(contentWidth)}┤`));
|
|
lines.push(row(this.theme.fg("dim", ` ${footer}`)));
|
|
lines.push(border(`╰${"─".repeat(contentWidth)}╯`));
|
|
|
|
return lines;
|
|
}
|
|
}
|