mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
message dialog
This commit is contained in:
parent
5d57607801
commit
256dead643
3 changed files with 153 additions and 68 deletions
|
|
@ -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 (
|
||||
<DialogSelect
|
||||
title="Message"
|
||||
options={[
|
||||
{
|
||||
title: "Revert",
|
||||
value: "session.revert",
|
||||
description: "rollback everything after this message",
|
||||
onSelect: (dialog) => {
|
||||
sdk.session.revert({
|
||||
path: {
|
||||
id: props.sessionID,
|
||||
},
|
||||
body: {
|
||||
messageID: message()!.id,
|
||||
},
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Fork",
|
||||
value: "session.fork",
|
||||
description: "create a new session",
|
||||
onSelect: () => {},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexGrow={1}>
|
||||
|
|
@ -224,23 +232,23 @@ export function Session() {
|
|||
<text fg={Theme.textMuted}>
|
||||
<span style={{ fg: Theme.text }}>{keybind.print("messages_redo")}</span> or /redo to restore
|
||||
</text>
|
||||
<Show when={revert()!.diffFiles?.length}>
|
||||
<box marginTop={1}>
|
||||
<For each={revert()!.diffFiles}>
|
||||
{(file) => (
|
||||
<text>
|
||||
{file.filename}
|
||||
<Show when={file.additions > 0}>
|
||||
<span style={{ fg: Theme.diffAdded }}> +{file.additions}</span>
|
||||
</Show>
|
||||
<Show when={file.deletions > 0}>
|
||||
<span style={{ fg: Theme.diffRemoved }}> -{file.deletions}</span>
|
||||
</Show>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={revert()!.diffFiles?.length}>
|
||||
<box marginTop={1}>
|
||||
<For each={revert()!.diffFiles}>
|
||||
{(file) => (
|
||||
<text>
|
||||
{file.filename}
|
||||
<Show when={file.additions > 0}>
|
||||
<span style={{ fg: Theme.diffAdded }}> +{file.additions}</span>
|
||||
</Show>
|
||||
<Show when={file.deletions > 0}>
|
||||
<span style={{ fg: Theme.diffRemoved }}> -{file.deletions}</span>
|
||||
</Show>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</Match>
|
||||
|
|
@ -248,7 +256,13 @@ export function Session() {
|
|||
<></>
|
||||
</Match>
|
||||
<Match when={message.role === "user"}>
|
||||
<UserMessage message={message as UserMessage} parts={sync.data.part[message.id] ?? []} />
|
||||
<UserMessage
|
||||
onMouseDown={() =>
|
||||
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
|
||||
}
|
||||
message={message as UserMessage}
|
||||
parts={sync.data.part[message.id] ?? []}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={message.role === "assistant"}>
|
||||
<AssistantMessage
|
||||
|
|
@ -294,18 +308,27 @@ const MIME_BADGE: Record<string, string> = {
|
|||
"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 (
|
||||
<box
|
||||
onMouseOver={() => {
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,20 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
|
||||
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<T>(props: DialogSelectProps<T>) {
|
|||
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<T>(props: DialogSelectProps<T>) {
|
|||
</Show>
|
||||
<For each={options}>
|
||||
{(option) => {
|
||||
const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
|
||||
return (
|
||||
<Option
|
||||
<box
|
||||
id={JSON.stringify(option.value)}
|
||||
title={option.title}
|
||||
footer={option.footer ?? (option.keybind ? keybind.print(option.keybind as any) : undefined)}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={isDeepEqual(option.value, selected()?.value)}
|
||||
current={isDeepEqual(option.value, props.current)}
|
||||
/>
|
||||
flexDirection="row"
|
||||
onMouseUp={() => {
|
||||
option.onSelect?.(dialog)
|
||||
props.onSelect?.(option)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
|
||||
if (index === -1) return
|
||||
moveTo(index)
|
||||
}}
|
||||
backgroundColor={active() ? Theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<Option
|
||||
title={option.title}
|
||||
footer={option.footer ?? (option.keybind ? keybind.print(option.keybind as any) : undefined)}
|
||||
description={option.description !== category ? option.description : undefined}
|
||||
active={active()}
|
||||
current={isDeepEqual(option.value, props.current)}
|
||||
/>
|
||||
</box>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
|
@ -181,21 +209,15 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|||
}
|
||||
|
||||
function Option(props: {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
active?: boolean
|
||||
current?: boolean
|
||||
footer?: string
|
||||
onMouseOver?: () => void
|
||||
}) {
|
||||
return (
|
||||
<box
|
||||
id={props.id}
|
||||
flexDirection="row"
|
||||
backgroundColor={props.active ? Theme.primary : RGBA.fromInts(0, 0, 0, 0)}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<>
|
||||
<box flexGrow={1} flexShrink={0} flexDirection="row">
|
||||
<text
|
||||
fg={props.active ? Theme.background : props.current ? Theme.primary : Theme.text}
|
||||
|
|
@ -208,6 +230,6 @@ function Option(props: {
|
|||
<Show when={props.footer}>
|
||||
<text fg={props.active ? Theme.background : Theme.textMuted}>{props.footer}</text>
|
||||
</Show>
|
||||
</box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue