summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components/message-part.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-11 16:34:48 -0500
committerGitHub <[email protected]>2026-03-11 21:34:48 +0000
commitfbd9b7cf4fb27433a066a10a197c7edb420336d1 (patch)
tree39697e059b99369f370682a0f68da037adfe1016 /packages/ui/src/components/message-part.tsx
parent58f45ae22b0faeba4a98910b0d3b42146846462b (diff)
downloadopencode-fbd9b7cf4fb27433a066a10a197c7edb420336d1.tar.gz
opencode-fbd9b7cf4fb27433a066a10a197c7edb420336d1.zip
feat(app): restore to message and fork session (#17092)
Diffstat (limited to 'packages/ui/src/components/message-part.tsx')
-rw-r--r--packages/ui/src/components/message-part.tsx59
1 files changed, 59 insertions, 0 deletions
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 65b6229d6..1a50ed48c 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -129,12 +129,20 @@ function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element {
export interface MessageProps {
message: MessageType
parts: PartType[]
+ actions?: UserActions
showAssistantCopyPartID?: string | null
interrupted?: boolean
queued?: boolean
showReasoningSummaries?: boolean
}
+export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
+
+export type UserActions = {
+ fork?: SessionAction
+ revert?: SessionAction
+}
+
export interface MessagePartProps {
part: PartType
message: MessageType
@@ -676,6 +684,7 @@ export function Message(props: MessageProps) {
<UserMessageDisplay
message={userMessage() as UserMessage}
parts={props.parts}
+ actions={props.actions}
interrupted={props.interrupted}
queued={props.queued}
/>
@@ -872,6 +881,7 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
export function UserMessageDisplay(props: {
message: UserMessage
parts: PartType[]
+ actions?: UserActions
interrupted?: boolean
queued?: boolean
}) {
@@ -879,6 +889,7 @@ export function UserMessageDisplay(props: {
const dialog = useDialog()
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
+ const [busy, setBusy] = createSignal<"fork" | "revert" | undefined>()
const textPart = createMemo(
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
@@ -945,6 +956,22 @@ export function UserMessageDisplay(props: {
setTimeout(() => setCopied(false), 2000)
}
+ const run = (kind: "fork" | "revert") => {
+ const act = kind === "fork" ? props.actions?.fork : props.actions?.revert
+ if (!act || busy()) return
+ setBusy(kind)
+ void Promise.resolve()
+ .then(() =>
+ act({
+ sessionID: props.message.sessionID,
+ messageID: props.message.id,
+ }),
+ )
+ .finally(() => {
+ if (busy() === kind) setBusy(undefined)
+ })
+ }
+
return (
<div data-component="user-message" data-interrupted={props.interrupted ? "" : undefined}>
<Show when={attachments().length > 0}>
@@ -1012,6 +1039,38 @@ export function UserMessageDisplay(props: {
</Show>
</span>
</Show>
+ <Show when={props.actions?.fork}>
+ <Tooltip value={i18n.t("ui.message.forkMessage")} placement="top" gutter={4}>
+ <IconButton
+ icon="fork"
+ size="normal"
+ variant="ghost"
+ disabled={!!busy()}
+ onMouseDown={(e) => e.preventDefault()}
+ onClick={(event) => {
+ event.stopPropagation()
+ run("fork")
+ }}
+ aria-label={i18n.t("ui.message.forkMessage")}
+ />
+ </Tooltip>
+ </Show>
+ <Show when={props.actions?.revert}>
+ <Tooltip value={i18n.t("ui.message.revertMessage")} placement="top" gutter={4}>
+ <IconButton
+ icon="reset"
+ size="normal"
+ variant="ghost"
+ disabled={!!busy()}
+ onMouseDown={(e) => e.preventDefault()}
+ onClick={(event) => {
+ event.stopPropagation()
+ run("revert")
+ }}
+ aria-label={i18n.t("ui.message.revertMessage")}
+ />
+ </Tooltip>
+ </Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
placement="top"