Skip to content

Commit 8c332f9

Browse files
committed
feat(tui): add progressive message loading UI
Adds TUI integration for loading older messages in sessions with 100+ messages. Implementation: - loadConversationHistory(): Loads messages up to next compaction summary - loadFullSessionHistory(): Loads entire remaining session history UI Integration: - Displays 'Load more messages' when 100+ messages present - Two clickable options for conversation vs full history - Toast notifications show count of messages loaded - Uses synthetic message pattern for clean positioning Depends on the ts_before and breakpoint API parameters added in the previous commit.
1 parent 2b891d1 commit 8c332f9

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
462462
)
463463
fullSyncedSessions.add(sessionID)
464464
},
465+
async loadConversationHistory(sessionID: string) {
466+
const messages = store.message[sessionID]
467+
if (!messages || messages.length === 0) return
468+
469+
const earliest = messages[0]
470+
const result = await sdk.client.session.messages({
471+
sessionID,
472+
ts_before: earliest.time.created,
473+
breakpoint: true,
474+
})
475+
476+
if (!result.data || result.data.length === 0) {
477+
return 0
478+
}
479+
480+
setStore(
481+
produce((draft) => {
482+
const existing = draft.message[sessionID] ?? []
483+
draft.message[sessionID] = [...result.data!.map((x) => x.info), ...existing]
484+
for (const message of result.data!) {
485+
draft.part[message.info.id] = message.parts
486+
}
487+
}),
488+
)
489+
return result.data.length
490+
},
491+
async loadFullSessionHistory(sessionID: string) {
492+
const messages = store.message[sessionID]
493+
if (!messages || messages.length === 0) return
494+
495+
const earliest = messages[0]
496+
const result = await sdk.client.session.messages({
497+
sessionID,
498+
ts_before: earliest.time.created,
499+
})
500+
501+
if (!result.data || result.data.length === 0) {
502+
return 0
503+
}
504+
505+
setStore(
506+
produce((draft) => {
507+
const existing = draft.message[sessionID] ?? []
508+
draft.message[sessionID] = [...result.data!.map((x) => x.info), ...existing]
509+
for (const message of result.data!) {
510+
draft.part[message.info.id] = message.parts
511+
}
512+
}),
513+
)
514+
return result.data.length
515+
},
465516
},
466517
bootstrap,
467518
}

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ export function Session() {
123123
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
124124
})
125125
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
126+
127+
const messagesDisplay = createMemo(() => {
128+
const msgs = messages()
129+
if (msgs.length >= 100) {
130+
const synthetic = {
131+
id: "__load_more__",
132+
sessionID: route.sessionID,
133+
role: "system" as const,
134+
time: { created: 0, updated: 0, completed: null },
135+
_synthetic: true,
136+
} as any
137+
return [synthetic, ...msgs]
138+
}
139+
return msgs
140+
})
141+
126142
const permissions = createMemo(() => {
127143
if (session()?.parentID) return []
128144
return children().flatMap((x) => sync.data.permission[x.id] ?? [])
@@ -979,9 +995,78 @@ export function Session() {
979995
flexGrow={1}
980996
scrollAcceleration={scrollAcceleration()}
981997
>
982-
<For each={messages()}>
998+
<For each={messagesDisplay()}>
983999
{(message, index) => (
9841000
<Switch>
1001+
<Match when={message.id === "__load_more__"}>
1002+
{(function () {
1003+
const [hoveredButton, setHoveredButton] = createSignal<"conversation" | "full" | null>(null)
1004+
const [loading, setLoading] = createSignal(false)
1005+
1006+
const handleLoadConversation = async () => {
1007+
if (loading()) return
1008+
setLoading(true)
1009+
try {
1010+
const count = await sync.session.loadConversationHistory(route.sessionID)
1011+
if (count === 0) {
1012+
toast.show({ message: "No more messages loaded", variant: "info" })
1013+
} else {
1014+
toast.show({ message: `History loaded (${count} messages)`, variant: "success" })
1015+
}
1016+
} finally {
1017+
setLoading(false)
1018+
}
1019+
}
1020+
1021+
const handleLoadFull = async () => {
1022+
if (loading()) return
1023+
setLoading(true)
1024+
try {
1025+
const count = await sync.session.loadFullSessionHistory(route.sessionID)
1026+
if (count === 0) {
1027+
toast.show({ message: "No more messages loaded", variant: "info" })
1028+
} else {
1029+
toast.show({ message: `History loaded (${count} messages)`, variant: "success" })
1030+
}
1031+
} finally {
1032+
setLoading(false)
1033+
}
1034+
}
1035+
1036+
return (
1037+
<box
1038+
paddingLeft={2}
1039+
paddingRight={2}
1040+
paddingTop={1}
1041+
paddingBottom={1}
1042+
marginBottom={1}
1043+
flexDirection="row"
1044+
backgroundColor={theme.backgroundPanel}
1045+
>
1046+
<text fg={theme.textMuted}>Load more messages: </text>
1047+
<box
1048+
onMouseOver={() => setHoveredButton("conversation")}
1049+
onMouseOut={() => setHoveredButton(null)}
1050+
onMouseUp={handleLoadConversation}
1051+
>
1052+
<text fg={hoveredButton() === "conversation" ? theme.accent : theme.text}>
1053+
load conversation history
1054+
</text>
1055+
</box>
1056+
<text fg={theme.textMuted}> or </text>
1057+
<box
1058+
onMouseOver={() => setHoveredButton("full")}
1059+
onMouseOut={() => setHoveredButton(null)}
1060+
onMouseUp={handleLoadFull}
1061+
>
1062+
<text fg={hoveredButton() === "full" ? theme.accent : theme.text}>
1063+
load full session history
1064+
</text>
1065+
</box>
1066+
</box>
1067+
)
1068+
})()}
1069+
</Match>
9851070
<Match when={message.id === revert()?.messageID}>
9861071
{(function () {
9871072
const command = useCommandDialog()

0 commit comments

Comments
 (0)