From 256dead643a59dd621203b399b6439d9772dc87b Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 28 Sep 2025 05:01:00 -0400 Subject: [PATCH] message dialog --- .../cmd/tui/routes/session/dialog-message.tsx | 40 ++++++ .../src/cli/cmd/tui/routes/session/index.tsx | 125 +++++++++++------- .../src/cli/cmd/tui/ui/dialog-select.tsx | 56 +++++--- 3 files changed, 153 insertions(+), 68 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx new file mode 100644 index 000000000..839157fc8 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -0,0 +1,40 @@ +import { createMemo } from "solid-js" +import { useSync } from "../../context/sync" +import { DialogSelect } from "../../ui/dialog-select" +import { useSDK } from "../../context/sdk" + +export function DialogMessage(props: { messageID: string; sessionID: string }) { + const sync = useSync() + const sdk = useSDK() + const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID)) + + return ( + { + sdk.session.revert({ + path: { + id: props.sessionID, + }, + body: { + messageID: message()!.id, + }, + }) + dialog.clear() + }, + }, + { + title: "Fork", + value: "session.fork", + description: "create a new session", + onSelect: () => {}, + }, + ]} + /> + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 89aae10b3..5afbe4340 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, For, Match, Show, Switch, type Component } from "solid-js" +import { createEffect, createMemo, createSignal, For, Match, Show, Switch, type Component } from "solid-js" import { Dynamic } from "solid-js/web" import path from "path" import { useRouteData } from "@tui/context/route" @@ -29,6 +29,8 @@ import { Shimmer } from "@tui/ui/shimmer" import { useKeybind } from "@tui/context/keybind" import { Header } from "./header" import { parsePatch } from "diff" +import { useDialog } from "../../ui/dialog" +import { DialogMessage } from "./dialog-message" export function Session() { const route = useRouteData("session") @@ -162,38 +164,44 @@ export function Session() { }, ]) - const revert = createMemo(() => { - const s = session() - if (!s) return - const messageID = s.revert?.messageID - if (!messageID) return - const reverted = messages().filter((x) => x.id >= messageID && x.role === "user") + const revert = createMemo(() => { + const s = session() + if (!s) return + const messageID = s.revert?.messageID + if (!messageID) return + const reverted = messages().filter((x) => x.id >= messageID && x.role === "user") - const diffFiles = (() => { - const diffText = s.revert?.diff || "" - if (!diffText) return [] - - const patches = parsePatch(diffText) - return patches.map(patch => { - const filename = patch.newFileName || patch.oldFileName || 'unknown' - const cleanFilename = filename.replace(/^[ab]\//, '') - return { - filename: cleanFilename, - additions: patch.hunks.reduce((sum, hunk) => - sum + hunk.lines.filter(line => line.startsWith('+')).length, 0), - deletions: patch.hunks.reduce((sum, hunk) => - sum + hunk.lines.filter(line => line.startsWith('-')).length, 0) - } - }) - })() + const diffFiles = (() => { + const diffText = s.revert?.diff || "" + if (!diffText) return [] - return { - messageID, - reverted, - diff: s.revert!.diff, - diffFiles, - } - }) + const patches = parsePatch(diffText) + return patches.map((patch) => { + const filename = patch.newFileName || patch.oldFileName || "unknown" + const cleanFilename = filename.replace(/^[ab]\//, "") + return { + filename: cleanFilename, + additions: patch.hunks.reduce( + (sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("+")).length, + 0, + ), + deletions: patch.hunks.reduce( + (sum, hunk) => sum + hunk.lines.filter((line) => line.startsWith("-")).length, + 0, + ), + } + }) + })() + + return { + messageID, + reverted, + diff: s.revert!.diff, + diffFiles, + } + }) + + const dialog = useDialog() return ( @@ -224,23 +232,23 @@ export function Session() { {keybind.print("messages_redo")} or /redo to restore - - - - {(file) => ( - - {file.filename} - 0}> - +{file.additions} - - 0}> - -{file.deletions} - - - )} - - - + + + + {(file) => ( + + {file.filename} + 0}> + +{file.additions} + + 0}> + -{file.deletions} + + + )} + + + @@ -248,7 +256,13 @@ export function Session() { <> - + + dialog.replace(() => ) + } + message={message as UserMessage} + parts={sync.data.part[message.id] ?? []} + /> = { "application/pdf": "pdf", } -function UserMessage(props: { message: UserMessage; parts: Part[] }) { +function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseDown: () => void }) { const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0]) const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : []))) const sync = useSync() + const [hover, setHover] = createSignal(false) + return ( { + setHover(true) + }} + onMouseOut={() => { + setHover(false) + }} + onMouseDown={props.onMouseDown} border={["left"]} paddingTop={1} paddingBottom={1} paddingLeft={2} marginTop={1} - backgroundColor={Theme.backgroundPanel} + backgroundColor={hover() ? Theme.backgroundElement : Theme.backgroundPanel} customBorderChars={SplitBorder.customBorderChars} borderColor={Theme.secondary} flexShrink={0} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index dbe1a7136..89549c879 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -39,13 +39,20 @@ export function DialogSelect(props: DialogSelectProps) { let input: InputRenderable - const grouped = createMemo(() => { + const filtered = createMemo(() => { const needle = store.filter.toLowerCase() const result = pipe( props.options, filter((x) => x.disabled !== false), take(props.limit ?? Infinity), (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)), + ) + return result + }) + + const grouped = createMemo(() => { + const result = pipe( + filtered(), groupBy((x) => x.category ?? ""), // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))), entries(), @@ -71,6 +78,10 @@ export function DialogSelect(props: DialogSelectProps) { let next = store.selected + direction if (next < 0) next = flat().length - 1 if (next >= flat().length) next = 0 + moveTo(next) + } + + function moveTo(next: number) { setStore("selected", next) const target = scroll.getChildren().find((child) => { return child.id === JSON.stringify(selected()?.value) @@ -148,15 +159,32 @@ export function DialogSelect(props: DialogSelectProps) { {(option) => { + const active = createMemo(() => isDeepEqual(option.value, selected()?.value)) return ( - ) }} @@ -181,21 +209,15 @@ export function DialogSelect(props: DialogSelectProps) { } function Option(props: { - id: string title: string description?: string active?: boolean current?: boolean footer?: string + onMouseOver?: () => void }) { return ( - + <> {props.footer} - + ) }