Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions extensions/cli/src/ui/SessionPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { ChatHistoryItem } from "core/index.js";
import { Box, Text } from "ink";
import React, { useMemo } from "react";

import { useTerminalSize } from "./hooks/useTerminalSize.js";
import { defaultBoxStyles } from "./styles.js";

interface SessionPreviewProps {
chatHistory: ChatHistoryItem[];
sessionTitle: string;
}

function formatMessageContent(content: string | any): string {
if (typeof content === "string") {
return content;
} else if (Array.isArray(content)) {
// For array content, find the first text part
const textPart = content.find((part: any) => part.type === "text");
return textPart && "text" in textPart
? textPart.text
: "(multimodal message)";
}
return "(unknown content type)";
}

export function SessionPreview({
chatHistory,
sessionTitle,
}: SessionPreviewProps) {
const { rows: terminalHeight } = useTerminalSize();

// Filter and format messages for preview
const previewMessages = useMemo(() => {
return chatHistory
.filter((item) => {
// Skip system messages
if (item.message.role === "system") return false;

// Skip empty assistant messages
if (item.message.role === "assistant") {
const content = formatMessageContent(item.message.content);
if (!content || content.trim() === "") return false;
}

return true;
})
.map((item) => ({
role: item.message.role,
content: formatMessageContent(item.message.content),
}));
}, [chatHistory]);

// Calculate how many messages we can display based on terminal height
const maxMessages = useMemo(() => {
// Account for:
// - Box border (top + bottom): 2 lines
// - Box padding (top + bottom): 2 lines
// - Title line: 1 line
// - Empty line after title: 1 line
// - "... and X more messages" line: 1 line
// - Each message: ~4 lines (role + content + marginBottom)
const OVERHEAD = 7;
const LINES_PER_MESSAGE = 4;
const availableHeight = Math.max(1, terminalHeight - OVERHEAD);
return Math.max(1, Math.floor(availableHeight / LINES_PER_MESSAGE));
}, [terminalHeight]);

if (previewMessages.length === 0) {
return (
<Box {...defaultBoxStyles("blue")} flexDirection="column" width="100%">
<Text color="blue" bold>
Preview
</Text>
<Text color="gray">(no messages)</Text>
</Box>
);
}

return (
<Box {...defaultBoxStyles("blue")} flexDirection="column" width="100%">
<Text color="blue" bold>
{sessionTitle}
</Text>
<Text> </Text>
{previewMessages.slice(0, maxMessages).map((msg, index) => {
const roleColor = msg.role === "user" ? "green" : "cyan";
const roleLabel = msg.role === "user" ? "You" : "Assistant";
// Truncate long messages for preview
const truncatedContent =
msg.content.length > 150
? msg.content.substring(0, 150) + "..."
: msg.content;

return (
<Box key={index} flexDirection="column" marginBottom={1}>
<Text color={roleColor} bold>
{roleLabel}:
</Text>
<Text wrap="truncate-end">{truncatedContent}</Text>
</Box>
);
})}
{previewMessages.length > maxMessages && (
<Text color="gray" italic>
... and {previewMessages.length - maxMessages} more messages
</Text>
)}
</Box>
);
}
138 changes: 94 additions & 44 deletions extensions/cli/src/ui/SessionSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Session } from "core/index.js";
import { format, isThisWeek, isThisYear, isToday, isYesterday } from "date-fns";
import { Box, Text, useInput } from "ink";
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";

import { ExtendedSessionMetadata } from "../session.js";
import { ExtendedSessionMetadata, loadSessionById } from "../session.js";

import { useTerminalSize } from "./hooks/useTerminalSize.js";
import { SessionPreview } from "./SessionPreview.js";
import { defaultBoxStyles } from "./styles.js";

interface SessionSelectorProps {
Expand Down Expand Up @@ -40,7 +42,19 @@ export function SessionSelector({
onExit,
}: SessionSelectorProps) {
const [selectedIndex, setSelectedIndex] = useState(0);
const { rows: terminalHeight } = useTerminalSize();
const [previewSession, setPreviewSession] = useState<Session | null>(null);
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();

// Load the selected session for preview
useEffect(() => {
const selectedSession = sessions[selectedIndex];
if (selectedSession && !selectedSession.isRemote) {
const session = loadSessionById(selectedSession.sessionId);
setPreviewSession(session);
} else {
setPreviewSession(null);
}
}, [selectedIndex, sessions]);

// Calculate how many sessions we can display based on terminal height and scrolling
const { displaySessions, scrollOffset } = useMemo(() => {
Expand Down Expand Up @@ -104,54 +118,90 @@ export function SessionSelector({
const hasMoreAbove = scrollOffset > 0;
const hasMoreBelow = scrollOffset + displaySessions.length < sessions.length;

// Determine if we should show preview (only if terminal is wide enough)
const showPreview = terminalWidth > 100;
const listWidth = showPreview
? Math.floor(terminalWidth * 0.3)
: terminalWidth;

return (
<Box {...defaultBoxStyles("blue")}>
<Text color="blue" bold>
Recent Sessions{" "}
{sessions.length > displaySessions.length &&
`(${selectedIndex + 1}/${sessions.length})`}
</Text>
<Text color="gray">↑/↓ to navigate, Enter to select, Esc to exit</Text>
<Text> </Text>

{hasMoreAbove && (
<Text color="gray" italic>
⬆ {scrollOffset} more sessions above...
<Box flexDirection="row" width={terminalWidth}>
{/* Left side: Session list */}
<Box {...defaultBoxStyles("blue")} width={listWidth}>
<Text color="blue" bold>
Recent Sessions{" "}
{sessions.length > displaySessions.length &&
`(${selectedIndex + 1}/${sessions.length})`}
</Text>
)}
<Text color="gray">↑/↓ to navigate, Enter to select, Esc to exit</Text>
<Text> </Text>

{hasMoreAbove && (
<Text color="gray" italic>
⬆ {scrollOffset} more sessions above...
</Text>
)}

{displaySessions.map((session, index) => {
const globalIndex = index + scrollOffset;
const isSelected = globalIndex === selectedIndex;
const indicator = isSelected ? "➤ " : " ";
const color = isSelected ? "blue" : "white";

return (
<Box key={session.sessionId} flexDirection="column">
<Box paddingRight={3}>
<Text bold={isSelected} color={color} wrap="truncate-end">
{indicator}
{formatMessage(session.title)}
</Text>
</Box>
<Box marginLeft={2}>
<Text color="gray">
{formatTimestamp(new Date(session.dateCreated))}
{session.isRemote ? " (remote)" : " (local)"}
</Text>
</Box>
{index < displaySessions.length - 1 && (
<Text key={`spacer-${session.sessionId}`}> </Text>
)}
</Box>
);
})}

{hasMoreBelow && (
<Text color="gray" italic>
⬇ {sessions.length - scrollOffset - displaySessions.length} more
sessions below...
</Text>
)}
</Box>

{displaySessions.map((session, index) => {
const globalIndex = index + scrollOffset;
const isSelected = globalIndex === selectedIndex;
const indicator = isSelected ? "➤ " : " ";
const color = isSelected ? "blue" : "white";

return (
<Box key={session.sessionId} flexDirection="column">
<Box paddingRight={3}>
<Text bold={isSelected} color={color} wrap="truncate-end">
{indicator}
{formatMessage(session.title)}
{/* Right side: Preview panel */}
{showPreview && (
<Box marginLeft={1} flexGrow={1} width="100%">
{previewSession ? (
<SessionPreview
chatHistory={previewSession.history}
sessionTitle={previewSession.title}
/>
) : (
<Box
{...defaultBoxStyles("blue")}
flexDirection="column"
width="100%"
>
<Text color="blue" bold>
Preview
</Text>
</Box>
<Box marginLeft={2}>
<Text color="gray">
{formatTimestamp(new Date(session.dateCreated))}
{session.isRemote ? " (remote)" : " (local)"}
{sessions[selectedIndex]?.isRemote
? "(remote session preview not available)"
: "(loading...)"}
</Text>
</Box>
{index < displaySessions.length - 1 && (
<Text key={`spacer-${session.sessionId}`}> </Text>
)}
</Box>
);
})}

{hasMoreBelow && (
<Text color="gray" italic>
⬇ {sessions.length - scrollOffset - displaySessions.length} more
sessions below...
</Text>
)}
</Box>
)}
</Box>
);
Expand Down
Loading