Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "折叠",
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/i18n/zht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "待辦事項",
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,13 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
),
)

const totalVisible = createMemo(() => input.visibleUserMessages().length)

return {
turnStart,
setTurnStart,
renderedUserMessages,
totalVisible,
loadAndReveal,
onScrollerScroll,
}
Expand Down Expand Up @@ -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={() => {
Expand Down
17 changes: 16 additions & 1 deletion packages/app/src/pages/session/message-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -927,7 +929,7 @@ export function MessageTimeline(props: {
}}
>
<Show when={props.turnStart > 0 || props.historyMore}>
<div class="w-full flex justify-center">
<div class="w-full flex flex-col items-center gap-1 py-2">
<Button
variant="ghost"
size="large"
Expand All @@ -939,6 +941,19 @@ export function MessageTimeline(props: {
? language.t("session.messages.loadingEarlier")
: language.t("session.messages.loadEarlier")}
</Button>
<Show when={props.totalVisible > 0}>
<span class="text-11-regular text-text-weak opacity-50 select-none">
{props.historyMore
? language.t("session.messages.showingOfMore", {
showing: props.renderedUserMessages.length,
total: props.totalVisible,
})
: language.t("session.messages.showingOf", {
showing: props.renderedUserMessages.length,
total: props.totalVisible,
})}
</span>
</Show>
</div>
</Show>
<For each={rendered()}>
Expand Down
48 changes: 47 additions & 1 deletion packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -96,6 +105,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
todo: {},
message: {},
part: {},
history_cursor: {},
history_complete: {},
history_loading: {},
lsp: [],
mcp: {},
mcp_resource: {},
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const TuiEvent = {
"session.half.page.down",
"session.first",
"session.last",
"session.load.earlier",
"prompt.clear",
"prompt.submit",
"agent.cycle",
Expand Down
35 changes: 35 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1069,6 +1088,22 @@ export function Session() {
flexGrow={1}
scrollAcceleration={scrollAcceleration()}
>
<Show when={sync.session.history.more(route.sessionID) || sync.session.history.loading(route.sessionID)}>
<box flexShrink={0} justifyContent="center" paddingTop={1} paddingBottom={1}>
<Show
when={!sync.session.history.loading(route.sessionID)}
fallback={<text fg={theme.textMuted}>Loading previous messages...</text>}
>
<text fg={theme.accent} onMouseUp={() => void loadEarlier()}>
▲ Load previous messages
</text>
<text fg={theme.textMuted}>
{messages().length} messages loaded
{messages().length >= 200 ? " · high memory usage" : ""}
</text>
</Show>
</box>
</Show>
<For each={messages()}>
{(message, index) => (
<Switch>
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<leader>p").describe("Load previous messages"),
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/server/routes/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
})
Expand Down
Loading