Skip to content

Commit 1c06790

Browse files
continue[bot]sestinjContinueUsernametingwai
authored
feat: Add preview panel to session selector (cn ls / /resume) (#8231)
* feat: Add preview panel to session selector for chat history - Created SessionPreview component to display chat history - Updated SessionSelector to show two-column layout with preview on right - Preview shows first 10 messages with truncation for long content - Preview only displays when terminal width > 100 columns - Automatically loads and displays session history as cursor moves Fixes CON-3892 Generated with Continue(https://continue.dev) Co-Authored-By: Continue <[email protected]> Co-authored-by: Username <[email protected]> * chore: Adjust session selector layout to 30/70 split - Changed list width from 45% to 30% of terminal width - Preview panel now takes up 70% of the right side - Provides more space for chat history preview Co-Authored-By: Continue <[email protected]> Co-authored-by: Username <[email protected]> * fix lint * preview pane takes full width * check terminal height and skip empty messages --------- Co-authored-by: Continue Agent <[email protected]> Co-authored-by: Continue <[email protected]> Co-authored-by: Username <[email protected]> Co-authored-by: Ting-Wai To <[email protected]>
1 parent 3dad415 commit 1c06790

File tree

2 files changed

+204
-44
lines changed

2 files changed

+204
-44
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { ChatHistoryItem } from "core/index.js";
2+
import { Box, Text } from "ink";
3+
import React, { useMemo } from "react";
4+
5+
import { useTerminalSize } from "./hooks/useTerminalSize.js";
6+
import { defaultBoxStyles } from "./styles.js";
7+
8+
interface SessionPreviewProps {
9+
chatHistory: ChatHistoryItem[];
10+
sessionTitle: string;
11+
}
12+
13+
function formatMessageContent(content: string | any): string {
14+
if (typeof content === "string") {
15+
return content;
16+
} else if (Array.isArray(content)) {
17+
// For array content, find the first text part
18+
const textPart = content.find((part: any) => part.type === "text");
19+
return textPart && "text" in textPart
20+
? textPart.text
21+
: "(multimodal message)";
22+
}
23+
return "(unknown content type)";
24+
}
25+
26+
export function SessionPreview({
27+
chatHistory,
28+
sessionTitle,
29+
}: SessionPreviewProps) {
30+
const { rows: terminalHeight } = useTerminalSize();
31+
32+
// Filter and format messages for preview
33+
const previewMessages = useMemo(() => {
34+
return chatHistory
35+
.filter((item) => {
36+
// Skip system messages
37+
if (item.message.role === "system") return false;
38+
39+
// Skip empty assistant messages
40+
if (item.message.role === "assistant") {
41+
const content = formatMessageContent(item.message.content);
42+
if (!content || content.trim() === "") return false;
43+
}
44+
45+
return true;
46+
})
47+
.map((item) => ({
48+
role: item.message.role,
49+
content: formatMessageContent(item.message.content),
50+
}));
51+
}, [chatHistory]);
52+
53+
// Calculate how many messages we can display based on terminal height
54+
const maxMessages = useMemo(() => {
55+
// Account for:
56+
// - Box border (top + bottom): 2 lines
57+
// - Box padding (top + bottom): 2 lines
58+
// - Title line: 1 line
59+
// - Empty line after title: 1 line
60+
// - "... and X more messages" line: 1 line
61+
// - Each message: ~4 lines (role + content + marginBottom)
62+
const OVERHEAD = 7;
63+
const LINES_PER_MESSAGE = 4;
64+
const availableHeight = Math.max(1, terminalHeight - OVERHEAD);
65+
return Math.max(1, Math.floor(availableHeight / LINES_PER_MESSAGE));
66+
}, [terminalHeight]);
67+
68+
if (previewMessages.length === 0) {
69+
return (
70+
<Box {...defaultBoxStyles("blue")} flexDirection="column" width="100%">
71+
<Text color="blue" bold>
72+
Preview
73+
</Text>
74+
<Text color="gray">(no messages)</Text>
75+
</Box>
76+
);
77+
}
78+
79+
return (
80+
<Box {...defaultBoxStyles("blue")} flexDirection="column" width="100%">
81+
<Text color="blue" bold>
82+
{sessionTitle}
83+
</Text>
84+
<Text> </Text>
85+
{previewMessages.slice(0, maxMessages).map((msg, index) => {
86+
const roleColor = msg.role === "user" ? "green" : "cyan";
87+
const roleLabel = msg.role === "user" ? "You" : "Assistant";
88+
// Truncate long messages for preview
89+
const truncatedContent =
90+
msg.content.length > 150
91+
? msg.content.substring(0, 150) + "..."
92+
: msg.content;
93+
94+
return (
95+
<Box key={index} flexDirection="column" marginBottom={1}>
96+
<Text color={roleColor} bold>
97+
{roleLabel}:
98+
</Text>
99+
<Text wrap="truncate-end">{truncatedContent}</Text>
100+
</Box>
101+
);
102+
})}
103+
{previewMessages.length > maxMessages && (
104+
<Text color="gray" italic>
105+
... and {previewMessages.length - maxMessages} more messages
106+
</Text>
107+
)}
108+
</Box>
109+
);
110+
}

extensions/cli/src/ui/SessionSelector.tsx

Lines changed: 94 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import type { Session } from "core/index.js";
12
import { format, isThisWeek, isThisYear, isToday, isYesterday } from "date-fns";
23
import { Box, Text, useInput } from "ink";
3-
import React, { useMemo, useState } from "react";
4+
import React, { useEffect, useMemo, useState } from "react";
45

5-
import { ExtendedSessionMetadata } from "../session.js";
6+
import { ExtendedSessionMetadata, loadSessionById } from "../session.js";
67

78
import { useTerminalSize } from "./hooks/useTerminalSize.js";
9+
import { SessionPreview } from "./SessionPreview.js";
810
import { defaultBoxStyles } from "./styles.js";
911

1012
interface SessionSelectorProps {
@@ -40,7 +42,19 @@ export function SessionSelector({
4042
onExit,
4143
}: SessionSelectorProps) {
4244
const [selectedIndex, setSelectedIndex] = useState(0);
43-
const { rows: terminalHeight } = useTerminalSize();
45+
const [previewSession, setPreviewSession] = useState<Session | null>(null);
46+
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
47+
48+
// Load the selected session for preview
49+
useEffect(() => {
50+
const selectedSession = sessions[selectedIndex];
51+
if (selectedSession && !selectedSession.isRemote) {
52+
const session = loadSessionById(selectedSession.sessionId);
53+
setPreviewSession(session);
54+
} else {
55+
setPreviewSession(null);
56+
}
57+
}, [selectedIndex, sessions]);
4458

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

121+
// Determine if we should show preview (only if terminal is wide enough)
122+
const showPreview = terminalWidth > 100;
123+
const listWidth = showPreview
124+
? Math.floor(terminalWidth * 0.3)
125+
: terminalWidth;
126+
107127
return (
108-
<Box {...defaultBoxStyles("blue")}>
109-
<Text color="blue" bold>
110-
Recent Sessions{" "}
111-
{sessions.length > displaySessions.length &&
112-
`(${selectedIndex + 1}/${sessions.length})`}
113-
</Text>
114-
<Text color="gray">↑/↓ to navigate, Enter to select, Esc to exit</Text>
115-
<Text> </Text>
116-
117-
{hasMoreAbove && (
118-
<Text color="gray" italic>
119-
{scrollOffset} more sessions above...
128+
<Box flexDirection="row" width={terminalWidth}>
129+
{/* Left side: Session list */}
130+
<Box {...defaultBoxStyles("blue")} width={listWidth}>
131+
<Text color="blue" bold>
132+
Recent Sessions{" "}
133+
{sessions.length > displaySessions.length &&
134+
`(${selectedIndex + 1}/${sessions.length})`}
120135
</Text>
121-
)}
136+
<Text color="gray">↑/↓ to navigate, Enter to select, Esc to exit</Text>
137+
<Text> </Text>
138+
139+
{hasMoreAbove && (
140+
<Text color="gray" italic>
141+
{scrollOffset} more sessions above...
142+
</Text>
143+
)}
144+
145+
{displaySessions.map((session, index) => {
146+
const globalIndex = index + scrollOffset;
147+
const isSelected = globalIndex === selectedIndex;
148+
const indicator = isSelected ? "➤ " : " ";
149+
const color = isSelected ? "blue" : "white";
150+
151+
return (
152+
<Box key={session.sessionId} flexDirection="column">
153+
<Box paddingRight={3}>
154+
<Text bold={isSelected} color={color} wrap="truncate-end">
155+
{indicator}
156+
{formatMessage(session.title)}
157+
</Text>
158+
</Box>
159+
<Box marginLeft={2}>
160+
<Text color="gray">
161+
{formatTimestamp(new Date(session.dateCreated))}
162+
{session.isRemote ? " (remote)" : " (local)"}
163+
</Text>
164+
</Box>
165+
{index < displaySessions.length - 1 && (
166+
<Text key={`spacer-${session.sessionId}`}> </Text>
167+
)}
168+
</Box>
169+
);
170+
})}
171+
172+
{hasMoreBelow && (
173+
<Text color="gray" italic>
174+
{sessions.length - scrollOffset - displaySessions.length} more
175+
sessions below...
176+
</Text>
177+
)}
178+
</Box>
122179

123-
{displaySessions.map((session, index) => {
124-
const globalIndex = index + scrollOffset;
125-
const isSelected = globalIndex === selectedIndex;
126-
const indicator = isSelected ? "➤ " : " ";
127-
const color = isSelected ? "blue" : "white";
128-
129-
return (
130-
<Box key={session.sessionId} flexDirection="column">
131-
<Box paddingRight={3}>
132-
<Text bold={isSelected} color={color} wrap="truncate-end">
133-
{indicator}
134-
{formatMessage(session.title)}
180+
{/* Right side: Preview panel */}
181+
{showPreview && (
182+
<Box marginLeft={1} flexGrow={1} width="100%">
183+
{previewSession ? (
184+
<SessionPreview
185+
chatHistory={previewSession.history}
186+
sessionTitle={previewSession.title}
187+
/>
188+
) : (
189+
<Box
190+
{...defaultBoxStyles("blue")}
191+
flexDirection="column"
192+
width="100%"
193+
>
194+
<Text color="blue" bold>
195+
Preview
135196
</Text>
136-
</Box>
137-
<Box marginLeft={2}>
138197
<Text color="gray">
139-
{formatTimestamp(new Date(session.dateCreated))}
140-
{session.isRemote ? " (remote)" : " (local)"}
198+
{sessions[selectedIndex]?.isRemote
199+
? "(remote session preview not available)"
200+
: "(loading...)"}
141201
</Text>
142202
</Box>
143-
{index < displaySessions.length - 1 && (
144-
<Text key={`spacer-${session.sessionId}`}> </Text>
145-
)}
146-
</Box>
147-
);
148-
})}
149-
150-
{hasMoreBelow && (
151-
<Text color="gray" italic>
152-
{sessions.length - scrollOffset - displaySessions.length} more
153-
sessions below...
154-
</Text>
203+
)}
204+
</Box>
155205
)}
156206
</Box>
157207
);

0 commit comments

Comments
 (0)