|
|
@@ -3,6 +3,7 @@ import { useData } from "../context"
|
|
|
import { useDiffComponent } from "../context/diff"
|
|
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
|
|
import { checksum } from "@opencode-ai/util/encode"
|
|
|
+import { Binary } from "@opencode-ai/util/binary"
|
|
|
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
|
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
|
import { DiffChanges } from "./diff-changes"
|
|
|
@@ -62,24 +63,29 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined {
|
|
|
|
|
|
function AssistantMessageItem(props: {
|
|
|
message: AssistantMessage
|
|
|
- summary: string | undefined
|
|
|
- response: string | undefined
|
|
|
- lastTextPartId: string | undefined
|
|
|
- working: boolean
|
|
|
+ responsePartId: string | undefined
|
|
|
+ hideResponsePart: boolean
|
|
|
}) {
|
|
|
const data = useData()
|
|
|
const msgParts = createMemo(() => data.store.part[props.message.id] ?? [])
|
|
|
- const lastTextPart = createMemo(() =>
|
|
|
- msgParts()
|
|
|
- .filter((p) => p?.type === "text")
|
|
|
- .at(-1),
|
|
|
- )
|
|
|
+ const lastTextPart = createMemo(() => {
|
|
|
+ const parts = msgParts()
|
|
|
+ for (let i = parts.length - 1; i >= 0; i--) {
|
|
|
+ const part = parts[i]
|
|
|
+ if (part?.type === "text") return part as TextPart
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
|
|
|
const filteredParts = createMemo(() => {
|
|
|
- if (!props.working && !props.summary && props.response && props.lastTextPartId === lastTextPart()?.id) {
|
|
|
- return msgParts().filter((p) => p?.id !== lastTextPart()?.id)
|
|
|
- }
|
|
|
- return msgParts()
|
|
|
+ const parts = msgParts()
|
|
|
+ if (!props.hideResponsePart) return parts
|
|
|
+
|
|
|
+ const responsePartId = props.responsePartId
|
|
|
+ if (!responsePartId) return parts
|
|
|
+ if (responsePartId !== lastTextPart()?.id) return parts
|
|
|
+
|
|
|
+ return parts.filter((part) => part?.id !== responsePartId)
|
|
|
})
|
|
|
|
|
|
return <Message message={props.message} parts={filteredParts()} />
|
|
|
@@ -89,6 +95,7 @@ export function SessionTurn(
|
|
|
props: ParentProps<{
|
|
|
sessionID: string
|
|
|
messageID: string
|
|
|
+ lastUserMessageID?: string
|
|
|
stepsExpanded?: boolean
|
|
|
onStepsExpandedToggle?: () => void
|
|
|
onUserInteracted?: () => void
|
|
|
@@ -103,14 +110,30 @@ export function SessionTurn(
|
|
|
const diffComponent = useDiffComponent()
|
|
|
|
|
|
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? [])
|
|
|
- const userMessages = createMemo(() =>
|
|
|
- allMessages()
|
|
|
- .filter((m) => m.role === "user")
|
|
|
- .sort((a, b) => a.id.localeCompare(b.id)),
|
|
|
- )
|
|
|
|
|
|
- const message = createMemo(() => userMessages().find((m) => m.id === props.messageID))
|
|
|
- const isLastUserMessage = createMemo(() => message()?.id === userMessages().at(-1)?.id)
|
|
|
+ const message = createMemo(() => {
|
|
|
+ const messages = allMessages()
|
|
|
+ const result = Binary.search(messages, props.messageID, (m) => m.id)
|
|
|
+ if (!result.found) return undefined
|
|
|
+
|
|
|
+ const msg = messages[result.index]
|
|
|
+ if (msg.role !== "user") return undefined
|
|
|
+
|
|
|
+ return msg
|
|
|
+ })
|
|
|
+
|
|
|
+ const lastUserMessageID = createMemo(() => {
|
|
|
+ if (props.lastUserMessageID) return props.lastUserMessageID
|
|
|
+
|
|
|
+ const messages = allMessages()
|
|
|
+ for (let i = messages.length - 1; i >= 0; i--) {
|
|
|
+ const msg = messages[i]
|
|
|
+ if (msg?.role === "user") return msg.id
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+ })
|
|
|
+
|
|
|
+ const isLastUserMessage = createMemo(() => props.messageID === lastUserMessageID())
|
|
|
|
|
|
const parts = createMemo(() => {
|
|
|
const msg = message()
|
|
|
@@ -118,10 +141,29 @@ export function SessionTurn(
|
|
|
return data.store.part[msg.id] ?? []
|
|
|
})
|
|
|
|
|
|
+ const messageIndex = createMemo(() => {
|
|
|
+ const messages = allMessages()
|
|
|
+ const result = Binary.search(messages, props.messageID, (m) => m.id)
|
|
|
+ if (!result.found) return -1
|
|
|
+ return result.index
|
|
|
+ })
|
|
|
+
|
|
|
const assistantMessages = createMemo(() => {
|
|
|
const msg = message()
|
|
|
if (!msg) return [] as AssistantMessage[]
|
|
|
- return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[]
|
|
|
+
|
|
|
+ const messages = allMessages()
|
|
|
+ const index = messageIndex()
|
|
|
+ if (index < 0) return [] as AssistantMessage[]
|
|
|
+
|
|
|
+ const result: AssistantMessage[] = []
|
|
|
+ for (let i = index + 1; i < messages.length; i++) {
|
|
|
+ const item = messages[i]
|
|
|
+ if (!item) continue
|
|
|
+ if (item.role === "user") break
|
|
|
+ if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
|
|
|
+ }
|
|
|
+ return result
|
|
|
})
|
|
|
|
|
|
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
|
|
|
@@ -152,16 +194,18 @@ export function SessionTurn(
|
|
|
})
|
|
|
|
|
|
const permissionParts = createMemo(() => {
|
|
|
- const result: { part: ToolPart; message: AssistantMessage }[] = []
|
|
|
const permissions = data.store.permission?.[props.sessionID] ?? []
|
|
|
- if (!permissions.length) return result
|
|
|
+ if (!permissions.length) return [] as { part: ToolPart; message: AssistantMessage }[]
|
|
|
|
|
|
- for (const m of assistantMessages()) {
|
|
|
- const msgParts = data.store.part[m.id] ?? []
|
|
|
- for (const p of msgParts) {
|
|
|
- if (p?.type === "tool" && permissions.some((perm) => perm.callID === (p as ToolPart).callID)) {
|
|
|
- result.push({ part: p as ToolPart, message: m })
|
|
|
- }
|
|
|
+ const ids = new Set(permissions.map((perm) => perm.callID))
|
|
|
+ const result: { part: ToolPart; message: AssistantMessage }[] = []
|
|
|
+
|
|
|
+ for (const message of assistantMessages()) {
|
|
|
+ const parts = data.store.part[message.id] ?? []
|
|
|
+ for (const part of parts) {
|
|
|
+ if (part?.type !== "tool") continue
|
|
|
+ const tool = part as ToolPart
|
|
|
+ if (ids.has(tool.callID)) result.push({ part: tool, message })
|
|
|
}
|
|
|
}
|
|
|
return result
|
|
|
@@ -245,12 +289,11 @@ export function SessionTurn(
|
|
|
return s
|
|
|
})
|
|
|
|
|
|
- const summary = () => message()?.summary?.body
|
|
|
- const response = () => {
|
|
|
- const part = lastTextPart()
|
|
|
- return part?.type === "text" ? (part as TextPart).text : undefined
|
|
|
- }
|
|
|
- const hasDiffs = () => message()?.summary?.diffs?.length
|
|
|
+ const summary = createMemo(() => message()?.summary?.body)
|
|
|
+ const response = createMemo(() => lastTextPart()?.text)
|
|
|
+ const responsePartId = createMemo(() => lastTextPart()?.id)
|
|
|
+ const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
|
|
|
+ const hideResponsePart = createMemo(() => !working() && !summary() && !!responsePartId())
|
|
|
|
|
|
function duration() {
|
|
|
const msg = message()
|
|
|
@@ -477,10 +520,8 @@ export function SessionTurn(
|
|
|
{(assistantMessage) => (
|
|
|
<AssistantMessageItem
|
|
|
message={assistantMessage}
|
|
|
- summary={summary()}
|
|
|
- response={response()}
|
|
|
- lastTextPartId={lastTextPart()?.id}
|
|
|
- working={working()}
|
|
|
+ responsePartId={responsePartId()}
|
|
|
+ hideResponsePart={hideResponsePart()}
|
|
|
/>
|
|
|
)}
|
|
|
</For>
|