Skip to content

Commit 18558c2

Browse files
committed
core: expose v2 session messages in tui
1 parent 64baf51 commit 18558c2

15 files changed

Lines changed: 682 additions & 40 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { useEvent } from "@tui/context/event"
2828
import { SDKProvider, useSDK } from "@tui/context/sdk"
2929
import { StartupLoading } from "@tui/component/startup-loading"
3030
import { SyncProvider, useSync } from "@tui/context/sync"
31+
import { SyncProviderV2 } from "@tui/context/sync-v2"
3132
import { LocalProvider, useLocal } from "@tui/context/local"
3233
import { DialogModel } from "@tui/component/dialog-model"
3334
import { useConnected } from "@tui/component/use-connected"
@@ -166,27 +167,29 @@ export function tui(input: {
166167
>
167168
<ProjectProvider>
168169
<SyncProvider>
169-
<ThemeProvider mode={mode}>
170-
<LocalProvider>
171-
<KeybindProvider>
172-
<PromptStashProvider>
173-
<DialogProvider>
174-
<CommandProvider>
175-
<FrecencyProvider>
176-
<PromptHistoryProvider>
177-
<PromptRefProvider>
178-
<EditorContextProvider>
179-
<App onSnapshot={input.onSnapshot} />
180-
</EditorContextProvider>
181-
</PromptRefProvider>
182-
</PromptHistoryProvider>
183-
</FrecencyProvider>
184-
</CommandProvider>
185-
</DialogProvider>
186-
</PromptStashProvider>
187-
</KeybindProvider>
188-
</LocalProvider>
189-
</ThemeProvider>
170+
<SyncProviderV2>
171+
<ThemeProvider mode={mode}>
172+
<LocalProvider>
173+
<KeybindProvider>
174+
<PromptStashProvider>
175+
<DialogProvider>
176+
<CommandProvider>
177+
<FrecencyProvider>
178+
<PromptHistoryProvider>
179+
<PromptRefProvider>
180+
<EditorContextProvider>
181+
<App onSnapshot={input.onSnapshot} />
182+
</EditorContextProvider>
183+
</PromptRefProvider>
184+
</PromptHistoryProvider>
185+
</FrecencyProvider>
186+
</CommandProvider>
187+
</DialogProvider>
188+
</PromptStashProvider>
189+
</KeybindProvider>
190+
</LocalProvider>
191+
</ThemeProvider>
192+
</SyncProviderV2>
190193
</SyncProvider>
191194
</ProjectProvider>
192195
</SDKProvider>
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { useEvent } from "@tui/context/event"
2+
import type {
3+
SessionMessage,
4+
SessionMessageAssistant,
5+
SessionMessageAssistantReasoning,
6+
SessionMessageAssistantText,
7+
SessionMessageAssistantTool,
8+
} from "@opencode-ai/sdk/v2"
9+
import { createStore, produce, reconcile } from "solid-js/store"
10+
import { createSimpleContext } from "./helper"
11+
import { useSDK } from "./sdk"
12+
13+
function activeAssistant(messages: SessionMessage[]) {
14+
const index = messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed)
15+
if (index < 0) return
16+
const assistant = messages[index]
17+
return assistant?.type === "assistant" ? assistant : undefined
18+
}
19+
20+
function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
21+
return assistant?.content.findLast(
22+
(item): item is SessionMessageAssistantTool =>
23+
item.type === "tool" && (callID === undefined || item.callID === callID),
24+
)
25+
}
26+
27+
function latestText(assistant: SessionMessageAssistant | undefined) {
28+
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text")
29+
}
30+
31+
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
32+
return assistant?.content.findLast(
33+
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.reasoningID === reasoningID,
34+
)
35+
}
36+
37+
export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({
38+
name: "SyncV2",
39+
init: () => {
40+
const [store, setStore] = createStore<{
41+
messages: {
42+
[sessionID: string]: SessionMessage[]
43+
}
44+
}>({
45+
messages: {},
46+
})
47+
48+
const event = useEvent()
49+
const sdk = useSDK()
50+
51+
function update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
52+
setStore(
53+
"messages",
54+
produce((draft) => {
55+
fn((draft[sessionID] ??= []))
56+
}),
57+
)
58+
}
59+
60+
event.subscribe((event) => {
61+
switch (event.type) {
62+
case "session.next.prompted": {
63+
update(event.properties.sessionID, (draft) => {
64+
draft.push({
65+
id: event.properties.id,
66+
type: "user",
67+
text: event.properties.prompt.text,
68+
files: event.properties.prompt.files,
69+
agents: event.properties.prompt.agents,
70+
time: { created: event.properties.timestamp },
71+
})
72+
})
73+
break
74+
}
75+
case "session.next.synthetic":
76+
update(event.properties.sessionID, (draft) => {
77+
draft.push({
78+
id: event.properties.id,
79+
type: "synthetic",
80+
sessionID: event.properties.sessionID,
81+
text: event.properties.text,
82+
time: { created: event.properties.timestamp },
83+
})
84+
})
85+
break
86+
case "session.next.step.started":
87+
update(event.properties.sessionID, (draft) => {
88+
const currentAssistant = activeAssistant(draft)
89+
if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp
90+
draft.push({
91+
id: event.properties.id,
92+
type: "assistant",
93+
content: [],
94+
snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined,
95+
time: { created: event.properties.timestamp },
96+
})
97+
})
98+
break
99+
case "session.next.step.ended":
100+
update(event.properties.sessionID, (draft) => {
101+
const currentAssistant = activeAssistant(draft)
102+
if (!currentAssistant) return
103+
currentAssistant.time.completed = event.properties.timestamp
104+
currentAssistant.cost = event.properties.cost
105+
currentAssistant.tokens = event.properties.tokens
106+
if (event.properties.snapshot)
107+
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot }
108+
})
109+
break
110+
case "session.next.text.started":
111+
update(event.properties.sessionID, (draft) => {
112+
activeAssistant(draft)?.content.push({ type: "text", text: "" })
113+
})
114+
break
115+
case "session.next.text.delta":
116+
update(event.properties.sessionID, (draft) => {
117+
const match = latestText(activeAssistant(draft))
118+
if (match) match.text += event.properties.delta
119+
})
120+
break
121+
case "session.next.text.ended":
122+
update(event.properties.sessionID, (draft) => {
123+
const match = latestText(activeAssistant(draft))
124+
if (match) match.text = event.properties.text
125+
})
126+
break
127+
case "session.next.tool.input.started":
128+
update(event.properties.sessionID, (draft) => {
129+
activeAssistant(draft)?.content.push({
130+
type: "tool",
131+
callID: event.properties.callID,
132+
name: event.properties.name,
133+
time: { created: event.properties.timestamp },
134+
state: { status: "pending", input: "" },
135+
})
136+
})
137+
break
138+
case "session.next.tool.input.delta":
139+
update(event.properties.sessionID, (draft) => {
140+
const match = latestTool(activeAssistant(draft), event.properties.callID)
141+
if (match?.state.status === "pending") match.state.input += event.properties.delta
142+
})
143+
break
144+
case "session.next.tool.input.ended":
145+
break
146+
case "session.next.tool.called":
147+
update(event.properties.sessionID, (draft) => {
148+
const match = latestTool(activeAssistant(draft), event.properties.callID)
149+
if (!match) return
150+
match.time.ran = event.properties.timestamp
151+
match.state = { status: "running", input: event.properties.input, structured: {}, content: [] }
152+
})
153+
break
154+
case "session.next.tool.progress":
155+
update(event.properties.sessionID, (draft) => {
156+
const match = latestTool(activeAssistant(draft), event.properties.callID)
157+
if (match?.state.status !== "running") return
158+
match.state.structured = event.properties.structured
159+
match.state.content = [...event.properties.content]
160+
})
161+
break
162+
case "session.next.tool.success":
163+
update(event.properties.sessionID, (draft) => {
164+
const match = latestTool(activeAssistant(draft), event.properties.callID)
165+
if (match?.state.status !== "running") return
166+
match.state = {
167+
status: "completed",
168+
input: match.state.input,
169+
structured: event.properties.structured,
170+
content: [...event.properties.content],
171+
}
172+
})
173+
break
174+
case "session.next.tool.error":
175+
update(event.properties.sessionID, (draft) => {
176+
const match = latestTool(activeAssistant(draft), event.properties.callID)
177+
if (match?.state.status !== "running") return
178+
match.state = {
179+
status: "error",
180+
error: event.properties.error,
181+
input: match.state.input,
182+
structured: match.state.structured,
183+
content: match.state.content,
184+
}
185+
})
186+
break
187+
case "session.next.reasoning.started":
188+
update(event.properties.sessionID, (draft) => {
189+
activeAssistant(draft)?.content.push({
190+
type: "reasoning",
191+
reasoningID: event.properties.reasoningID,
192+
text: "",
193+
})
194+
})
195+
break
196+
case "session.next.reasoning.delta":
197+
update(event.properties.sessionID, (draft) => {
198+
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
199+
if (match) match.text += event.properties.delta
200+
})
201+
break
202+
case "session.next.reasoning.ended":
203+
update(event.properties.sessionID, (draft) => {
204+
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
205+
if (match) match.text = event.properties.text
206+
})
207+
break
208+
case "session.next.retried":
209+
break
210+
case "session.next.compacted":
211+
update(event.properties.sessionID, (draft) => {
212+
draft.push({
213+
id: event.properties.id,
214+
type: "compaction",
215+
sessionID: event.properties.sessionID,
216+
auto: event.properties.auto,
217+
overflow: event.properties.overflow,
218+
time: { created: event.properties.timestamp },
219+
})
220+
})
221+
break
222+
}
223+
})
224+
225+
const result = {
226+
data: store,
227+
session: {
228+
message: {
229+
async sync(sessionID: string) {
230+
const response = await sdk.client.v2.session.messages({
231+
sessionID,
232+
})
233+
setStore("messages", sessionID, reconcile(response.data ?? []))
234+
},
235+
fromSession(sessionID: string) {
236+
const messages = store.messages[sessionID]
237+
if (!messages) return []
238+
return messages
239+
},
240+
},
241+
},
242+
}
243+
244+
return result
245+
},
246+
})

0 commit comments

Comments
 (0)