From 0994cbcff723ff79bbae4f9f17f517edb33bf290 Mon Sep 17 00:00:00 2001 From: Pengfei Yan Date: Thu, 19 Mar 2026 07:46:28 +0000 Subject: [PATCH 1/2] add a button 'load previous messages' when it got its limitation, then users can see more history messages in session --- packages/app/src/i18n/en.ts | 2 + packages/app/src/i18n/zh.ts | 2 + packages/app/src/i18n/zht.ts | 3 +- packages/app/src/pages/session.tsx | 5 ++ .../src/pages/session/message-timeline.tsx | 17 ++++++- .../opencode/src/cli/cmd/tui/context/sync.tsx | 48 ++++++++++++++++++- packages/opencode/src/cli/cmd/tui/event.ts | 1 + .../src/cli/cmd/tui/routes/session/index.tsx | 35 ++++++++++++++ packages/opencode/src/config/config.ts | 1 + packages/opencode/src/server/routes/tui.ts | 1 + 10 files changed, 112 insertions(+), 3 deletions(-) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 72caed40ad9..5225c35fe8e 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -544,6 +544,8 @@ export const dict = { "session.messages.loadEarlier": "Load earlier messages", "session.messages.loading": "Loading messages...", "session.messages.jumpToLatest": "Jump to latest", + "session.messages.showingOf": "Showing {{showing}} of {{total}} messages", + "session.messages.showingOfMore": "Showing {{showing}} of {{total}}+ messages", "session.context.addToContext": "Add {{selection}} to context", "session.todo.title": "Todos", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index cf64ca9b2c5..41163082c96 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -517,6 +517,8 @@ export const dict = { "session.messages.loadEarlier": "加载更早的消息", "session.messages.loading": "正在加载消息...", "session.messages.jumpToLatest": "跳转到最新", + "session.messages.showingOf": "显示 {{showing}} / {{total}} 条消息", + "session.messages.showingOfMore": "显示 {{showing}} / {{total}}+ 条消息", "session.context.addToContext": "将 {{selection}} 添加到上下文", "session.todo.title": "待办事项", "session.todo.collapse": "折叠", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 02c00d17a22..eb85d873c20 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -511,7 +511,8 @@ export const dict = { "session.messages.loadingEarlier": "正在載入更早的訊息...", "session.messages.loadEarlier": "載入更早的訊息", "session.messages.loading": "正在載入訊息...", - + "session.messages.showingOf": "顯示 {{showing}} / {{total}} 則訊息", + "session.messages.showingOfMore": "顯示 {{showing}} / {{total}}+ 則訊息", "session.messages.jumpToLatest": "跳到最新", "session.context.addToContext": "將 {{selection}} 新增到上下文", "session.todo.title": "待辦事項", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 6d29170081a..764679e5dbc 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -288,10 +288,13 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) { ), ) + const totalVisible = createMemo(() => input.visibleUserMessages().length) + return { turnStart, setTurnStart, renderedUserMessages, + totalVisible, loadAndReveal, onScrollerScroll, } @@ -1725,6 +1728,8 @@ export default function Page() { if (root) scheduleScrollState(root) }} turnStart={historyWindow.turnStart()} + totalVisible={historyWindow.totalVisible()} + totalLoaded={messages().length} historyMore={historyMore()} historyLoading={historyLoading()} onLoadEarlier={() => { diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 74f2e8c2c12..8d7c33fa643 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -211,6 +211,8 @@ export function MessageTimeline(props: { centered: boolean setContentRef: (el: HTMLDivElement) => void turnStart: number + totalVisible: number + totalLoaded: number historyMore: boolean historyLoading: boolean onLoadEarlier: () => void @@ -927,7 +929,7 @@ export function MessageTimeline(props: { }} > 0 || props.historyMore}> -
+
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 3b296a927aa..5721fbf3eb8 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -64,6 +64,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ part: { [messageID: string]: Part[] } + history_cursor: { + [sessionID: string]: string | undefined + } + history_complete: { + [sessionID: string]: boolean | undefined + } + history_loading: { + [sessionID: string]: boolean | undefined + } lsp: LspStatus[] mcp: { [key: string]: McpStatus @@ -96,6 +105,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ todo: {}, message: {}, part: {}, + history_cursor: {}, + history_complete: {}, + history_loading: {}, lsp: [], mcp: {}, mcp_resource: {}, @@ -253,7 +265,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }), ) const updated = store.message[event.properties.info.sessionID] - if (updated.length > 100) { + if (updated.length > 500) { const oldest = updated[0] batch(() => { setStore( @@ -475,6 +487,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.session.todo({ sessionID }), sdk.client.session.diff({ sessionID }), ]) + const cursor = messages.response?.headers?.get("X-Next-Cursor") ?? undefined setStore( produce((draft) => { const match = Binary.search(draft.session, sessionID, (s) => s.id) @@ -486,10 +499,43 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ draft.part[message.info.id] = message.parts } draft.session_diff[sessionID] = diff.data ?? [] + draft.history_cursor[sessionID] = cursor + draft.history_complete[sessionID] = !cursor }), ) fullSyncedSessions.add(sessionID) }, + history: { + more(sessionID: string) { + return !!store.history_cursor[sessionID] && !store.history_complete[sessionID] + }, + loading(sessionID: string) { + return !!store.history_loading[sessionID] + }, + async loadMore(sessionID: string) { + const cur = store.history_cursor[sessionID] + if (!cur || store.history_complete[sessionID] || store.history_loading[sessionID]) return + setStore("history_loading", sessionID, true) + try { + const result = await sdk.client.session.messages({ sessionID, limit: 100, before: cur }) + const next = result.response?.headers?.get("X-Next-Cursor") ?? undefined + setStore( + produce((draft) => { + const older = (result.data ?? []).map((x) => x.info) + const existing = draft.message[sessionID] ?? [] + draft.message[sessionID] = [...older, ...existing] + for (const msg of result.data ?? []) { + draft.part[msg.info.id] = msg.parts + } + draft.history_cursor[sessionID] = next + draft.history_complete[sessionID] = !next + }), + ) + } finally { + setStore("history_loading", sessionID, false) + } + }, + }, }, workspace: { get(workspaceID: string) { diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index b2e4b92c551..cab8dd787e2 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -23,6 +23,7 @@ export const TuiEvent = { "session.half.page.down", "session.first", "session.last", + "session.load.earlier", "prompt.clear", "prompt.submit", "agent.cycle", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 7456742cdf3..10edb9090d3 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -319,6 +319,15 @@ export function Session() { }, 50) } + async function loadEarlier() { + if (!sync.session.history.more(route.sessionID)) return + const before = scroll?.scrollHeight ?? 0 + await sync.session.history.loadMore(route.sessionID) + if (!scroll || scroll.isDestroyed) return + const delta = scroll.scrollHeight - before + if (delta > 0) scroll.scrollBy(delta) + } + const local = useLocal() function moveFirstChild() { @@ -742,6 +751,16 @@ export function Session() { dialog.clear() }, }, + { + title: "Load previous messages", + value: "session.load.earlier", + keybind: "messages_load_earlier", + category: "Session", + onSelect: (dialog) => { + void loadEarlier() + dialog.clear() + }, + }, { title: "Jump to last user message", value: "session.messages_last_user", @@ -1069,6 +1088,22 @@ export function Session() { flexGrow={1} scrollAcceleration={scrollAcceleration()} > + + + Loading previous messages...} + > + void loadEarlier()}> + ▲ Load previous messages + + + {messages().length} messages loaded + {messages().length >= 200 ? " · high memory usage" : ""} + + + + {(message, index) => ( diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 47afdfd7d0f..c92eace77c1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -841,6 +841,7 @@ export namespace Config { messages_next: z.string().optional().default("none").describe("Navigate to next message"), messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), + messages_load_earlier: z.string().optional().default("e").describe("Load earlier messages"), messages_copy: z.string().optional().default("y").describe("Copy message"), messages_undo: z.string().optional().default("u").describe("Undo message"), messages_redo: z.string().optional().default("r").describe("Redo message"), diff --git a/packages/opencode/src/server/routes/tui.ts b/packages/opencode/src/server/routes/tui.ts index 8650a0cccf7..86e2a4eb3b2 100644 --- a/packages/opencode/src/server/routes/tui.ts +++ b/packages/opencode/src/server/routes/tui.ts @@ -281,6 +281,7 @@ export const TuiRoutes = lazy(() => messages_half_page_down: "session.half.page.down", messages_first: "session.first", messages_last: "session.last", + messages_load_earlier: "session.load.earlier", agent_cycle: "agent.cycle", }[command], }) From 0c326dfb7c5c7518f0982967f0466ea4d16a81ee Mon Sep 17 00:00:00 2001 From: Pengfei Yan Date: Thu, 19 Mar 2026 08:41:59 +0000 Subject: [PATCH 2/2] resolve the shortcut key conflict --- packages/opencode/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index c92eace77c1..6590852acd0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -841,7 +841,7 @@ export namespace Config { messages_next: z.string().optional().default("none").describe("Navigate to next message"), messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), - messages_load_earlier: z.string().optional().default("e").describe("Load earlier messages"), + messages_load_earlier: z.string().optional().default("p").describe("Load previous messages"), messages_copy: z.string().optional().default("y").describe("Copy message"), messages_undo: z.string().optional().default("u").describe("Undo message"), messages_redo: z.string().optional().default("r").describe("Redo message"),