Skip to content

Commit ee0d096

Browse files
committed
refactor(SessionList): 优化会话列表支持动态窗口大小和分页导航
- 引入 useWindowSize 以适配终端尺寸变化 - 基于窗口高度动态计算可见会话条目数 - 实现滚动偏移保障选中项始终可见 - 增加分页键(PageUp/PageDown)、Home、End 操作支持快速导航 - 限定选中索引在有效范围内避免越界错误 - 改进 UI 布局,添加边框和滚动提示信息 - 优化会话渲染逻辑,支持动态渲染当前可见会话列表 - 美化选中与非选中会话条目的显示样式 - 维护旧版显示逻辑以供替代使用
1 parent d743d20 commit ee0d096

2 files changed

Lines changed: 106 additions & 28 deletions

File tree

src/ui/SessionList.tsx

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,52 @@
1-
import React, { useState } from "react";
2-
import { Box, Text, useInput } from "ink";
3-
import type { SessionEntry } from "../session";
1+
import React, {useState, useMemo} from "react";
2+
import {Box, Text, useInput, useWindowSize} from "ink";
3+
import type {SessionEntry} from "../session";
44

55
type Props = {
66
sessions: SessionEntry[];
77
onSelect: (sessionId: string) => void;
88
onCancel: () => void;
99
};
1010

11-
export function SessionList({ sessions, onSelect, onCancel }: Props): React.ReactElement {
11+
export function SessionList({sessions, onSelect, onCancel}: Props): React.ReactElement {
1212
const [index, setIndex] = useState(0);
13+
const {columns, rows} = useWindowSize();
14+
15+
// 根据终端高度动态计算可见的会话数量
16+
const maxVisibleSessions = useMemo(() => {
17+
// 减去边框、标题、页脚、滚动指示器等占用的空间
18+
// 外层容器 height=rows-1,外边框2 + header1 + 内边框2 + footer1 + 滚动指示器1 = 8
19+
const reservedLines = 8;
20+
const linesPerSession = 3; // height=2 + marginBottom=1
21+
const availableLines = Math.max(0, rows - reservedLines);
22+
return Math.max(1, Math.floor(availableLines / linesPerSession));
23+
}, [rows]);
24+
25+
// 确保index在有效范围内
26+
const safeIndex = useMemo(() => {
27+
if (sessions.length === 0) return 0;
28+
return Math.max(0, Math.min(index, sessions.length - 1));
29+
}, [index, sessions.length]);
30+
31+
// 计算滚动偏移量,确保选中的项目始终可见
32+
const scrollOffset = useMemo(() => {
33+
if (safeIndex < maxVisibleSessions) return 0;
34+
return safeIndex - maxVisibleSessions + 1;
35+
}, [safeIndex, maxVisibleSessions]);
36+
37+
// 获取当前可见的会话列表
38+
const visibleSessions = useMemo(() => {
39+
return sessions.slice(scrollOffset, scrollOffset + maxVisibleSessions);
40+
}, [sessions, scrollOffset, maxVisibleSessions]);
1341

1442
useInput((input, key) => {
1543
if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
1644
onCancel();
1745
return;
1846
}
47+
if (sessions.length === 0) {
48+
return;
49+
}
1950
if (key.upArrow) {
2051
setIndex((i) => Math.max(0, i - 1));
2152
return;
@@ -24,8 +55,24 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
2455
setIndex((i) => Math.min(sessions.length - 1, i + 1));
2556
return;
2657
}
58+
if (key.pageUp) {
59+
setIndex((i) => Math.max(0, i - maxVisibleSessions));
60+
return;
61+
}
62+
if (key.pageDown) {
63+
setIndex((i) => Math.min(sessions.length - 1, i + maxVisibleSessions));
64+
return;
65+
}
66+
if (key.home) {
67+
setIndex(0);
68+
return;
69+
}
70+
if (key.end) {
71+
setIndex(sessions.length - 1);
72+
return;
73+
}
2774
if (key.return) {
28-
const session = sessions[index];
75+
const session = sessions[safeIndex];
2976
if (session) {
3077
onSelect(session.id);
3178
}
@@ -42,19 +89,54 @@ export function SessionList({ sessions, onSelect, onCancel }: Props): React.Reac
4289
}
4390

4491
return (
45-
<Box flexDirection="column">
46-
<Text bold color="cyanBright">Resume a session</Text>
47-
{sessions.slice(0, 30).map((session, i) => (
48-
<Text key={session.id} color={i === index ? "cyanBright" : undefined}>
49-
{i === index ? "› " : " "}
50-
<Text dimColor>{formatTimestamp(session.updateTime)} </Text>
51-
<Text>{formatSessionTitle(session.summary || "Untitled")}</Text>
52-
<Text dimColor> ({session.status})</Text>
53-
</Text>
54-
))}
55-
{sessions.length > 30 ? <Text dimColor>{sessions.length - 30} older sessions hidden.</Text> : null}
56-
<Box marginTop={1}>
57-
<Text dimColor>↑/↓ to navigate · Enter to select · Esc to cancel</Text>
92+
<Box
93+
flexDirection="column"
94+
width={columns - 6}
95+
height={rows - 1}
96+
overflow="hidden"
97+
paddingX={1}
98+
marginTop={1}
99+
>
100+
<Box flexDirection="column" borderStyle='round' borderDimColor flexGrow={1} overflow="hidden">
101+
{/* Header row */}
102+
<Box paddingX={1}><Text bold color="#229ac3">Resume a session ({sessions.length} total)</Text></Box>
103+
{/* Session list */}
104+
<Box borderTop={true} borderBottom={true} borderLeft={false} borderRight={false} borderStyle='round'
105+
borderDimColor flexDirection='column' flexGrow={1} paddingX={1} overflow="hidden">
106+
{visibleSessions.map((session, i) => {
107+
const actualIndex = scrollOffset + i;
108+
return (
109+
<Box key={session.id} height={2} marginBottom={1}>
110+
<Box>
111+
<Text color='#229ac3'>
112+
{actualIndex === safeIndex ? "› " : " "}
113+
</Text>
114+
</Box>
115+
<Box flexDirection='column' flexGrow={1}>
116+
<Box width={'100%'}>
117+
<Text {...(actualIndex === safeIndex ? {bold: true} : {})} color={actualIndex === safeIndex ? "#229ac3" : undefined}>
118+
{formatSessionTitle(session.summary || "Untitled")}
119+
</Text>
120+
<Text dimColor> ({session.status})</Text>
121+
</Box>
122+
<Box width='100%'>
123+
<Text dimColor>{formatTimestamp(session.updateTime)} </Text>
124+
</Box>
125+
</Box>
126+
</Box>
127+
);
128+
})}
129+
{(scrollOffset > 0 || scrollOffset + maxVisibleSessions < sessions.length) ? (
130+
<Box marginTop={1}>
131+
{scrollOffset > 0 ? <Text dimColor>{scrollOffset} newer sessions above. </Text> : null}
132+
{scrollOffset + maxVisibleSessions < sessions.length ? <Text dimColor>{sessions.length - scrollOffset - maxVisibleSessions} older sessions below.</Text> : null}
133+
</Box>
134+
) : null}
135+
</Box>
136+
{/* Footer */}
137+
<Box>
138+
<Text dimColor>↑/↓ navigate · PgUp/PgDn page · Enter select · Esc cancel</Text>
139+
</Box>
58140
</Box>
59141
</Box>
60142
);

src/ui/WelcomeScreen.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import React, { useMemo, useState} from "react";
2-
import { Box, Text } from "ink";
1+
import React, {useMemo, useState} from "react";
2+
import {Box, Text} from "ink";
33
import * as os from "node:os";
44
import path from 'node:path';
5-
import type { SkillInfo } from "../session";
6-
import type { ResolvedDeepcodingSettings } from "../settings";
7-
import {
8-
BUILTIN_SLASH_COMMANDS,
9-
buildSlashCommands,
10-
formatSlashCommandDescription
11-
} from "./slashCommands";
12-
import { ThemedGradient } from "./ThemedGradient";
5+
import type {SkillInfo} from "../session";
6+
import type {ResolvedDeepcodingSettings} from "../settings";
7+
import {buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription} from "./slashCommands";
8+
import {ThemedGradient} from "./ThemedGradient";
139

1410
type WelcomeScreenProps = {
1511
projectRoot: string;

0 commit comments

Comments
 (0)