|
|
|
@@ -24,7 +24,7 @@ import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
|
|
|
|
import { Spinner, SPINNER_FRAMES } from "../../component/spinner"
|
|
|
|
|
import { PatchDiff } from "../../component/patch-diff"
|
|
|
|
|
import { createSyntaxStyleMemo, ThemeContextProvider, useTheme, useThemes } from "../../context/theme"
|
|
|
|
|
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
|
|
|
|
|
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA, MouseEvent } from "@opentui/core"
|
|
|
|
|
import { Prompt, type PromptRef } from "../../component/prompt"
|
|
|
|
|
import type {
|
|
|
|
|
ModelInfo,
|
|
|
|
@@ -54,6 +54,7 @@ import { openEditor } from "../../editor"
|
|
|
|
|
import { useDialog } from "../../ui/dialog"
|
|
|
|
|
import { DialogSelect } from "../../ui/dialog-select"
|
|
|
|
|
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
|
|
|
|
import { DialogImagePreview } from "../../component/dialog-image-preview"
|
|
|
|
|
import { DialogMessage } from "./dialog-message"
|
|
|
|
|
import { DialogFork } from "./dialog-fork"
|
|
|
|
|
import { DialogTimeline } from "./dialog-timeline"
|
|
|
|
@@ -1592,8 +1593,9 @@ function SessionGroupView(props: {
|
|
|
|
|
</InlineToolRow>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={expanded() && grouped().length > 0}>
|
|
|
|
|
<For each={grouped()}>{(part) => <ToolPart part={part} />}</For>
|
|
|
|
|
<For each={grouped()}>{(part) => <ToolPart part={part} images={false} />}</For>
|
|
|
|
|
</Show>
|
|
|
|
|
<ToolImages parts={grouped()} />
|
|
|
|
|
<For each={pending()}>{(part) => <ToolPart part={part} />}</For>
|
|
|
|
|
</Show>
|
|
|
|
|
</Show>
|
|
|
|
@@ -1897,6 +1899,11 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
|
|
|
|
const local = useLocal()
|
|
|
|
|
const files = createMemo(() => props.message.files ?? [])
|
|
|
|
|
const skills = createMemo(() => props.message.skills ?? [])
|
|
|
|
|
const images = createMemo(() =>
|
|
|
|
|
files().flatMap((file) =>
|
|
|
|
|
file.mime.startsWith("image/") ? [{ uri: `data:${file.mime};base64,${file.data}` }] : [],
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
const themes = useThemes()
|
|
|
|
|
const theme = useTheme("elevated")
|
|
|
|
|
const mode = themes.mode
|
|
|
|
@@ -1918,6 +1925,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
|
|
|
|
|
borderColor={delivery() ? theme.border.default : color()}
|
|
|
|
|
customBorderChars={SplitBorder.customBorderChars}
|
|
|
|
|
>
|
|
|
|
|
<SessionImages images={images()} paddingLeft={2} />
|
|
|
|
|
<box
|
|
|
|
|
onMouseOver={() => {
|
|
|
|
|
setHover(true)
|
|
|
|
@@ -2210,7 +2218,7 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
|
|
|
|
|
|
|
|
|
|
// Pending messages moved to individual tool pending functions
|
|
|
|
|
|
|
|
|
|
function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|
|
|
|
function ToolPart(props: { part: SessionMessageAssistantTool; images?: boolean }) {
|
|
|
|
|
const display = createMemo(() => toolDisplay(props.part.name))
|
|
|
|
|
|
|
|
|
|
const toolprops = {
|
|
|
|
@@ -2234,7 +2242,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
const content = (
|
|
|
|
|
<Switch>
|
|
|
|
|
<Match when={display() === "shell"}>
|
|
|
|
|
<Shell {...toolprops} />
|
|
|
|
@@ -2280,6 +2288,87 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
|
|
|
|
|
</Match>
|
|
|
|
|
</Switch>
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
content,
|
|
|
|
|
<Show when={props.images !== false}>
|
|
|
|
|
<ToolImages parts={[props.part]} />
|
|
|
|
|
</Show>,
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ToolImages(props: { parts: readonly SessionMessageAssistantTool[] }) {
|
|
|
|
|
const images = createMemo(() => props.parts.flatMap(inlineToolImages))
|
|
|
|
|
return <SessionImages images={images()} />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SessionImages(props: { images: readonly { uri: string }[]; paddingLeft?: number }) {
|
|
|
|
|
const ctx = use()
|
|
|
|
|
const dialog = useDialog()
|
|
|
|
|
const dimensions = useTerminalDimensions()
|
|
|
|
|
const images = createMemo(() => (ctx.config.session?.image_preview ? props.images : []))
|
|
|
|
|
const height = createMemo(() => Math.max(4, Math.min(8, Math.floor(dimensions().height / 4))))
|
|
|
|
|
const visible = createMemo(() => images().slice(0, 3))
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Show when={visible().length > 0}>
|
|
|
|
|
<box
|
|
|
|
|
flexDirection="row"
|
|
|
|
|
flexShrink={0}
|
|
|
|
|
paddingTop={1}
|
|
|
|
|
paddingLeft={props.paddingLeft ?? 3}
|
|
|
|
|
paddingRight={2}
|
|
|
|
|
paddingBottom={1}
|
|
|
|
|
gap={1}
|
|
|
|
|
>
|
|
|
|
|
<For each={visible()}>
|
|
|
|
|
{(image, index) => {
|
|
|
|
|
const [failed, setFailed] = createSignal(false)
|
|
|
|
|
return (
|
|
|
|
|
<box
|
|
|
|
|
width={height() * 2}
|
|
|
|
|
height={height()}
|
|
|
|
|
flexBasis={height() * 2}
|
|
|
|
|
flexShrink={1}
|
|
|
|
|
alignItems="center"
|
|
|
|
|
justifyContent="center"
|
|
|
|
|
onMouseUp={(event: MouseEvent) => {
|
|
|
|
|
if (event.button !== 0) return
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
dialog.replace(() => <DialogImagePreview images={images()} initial={index()} />)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Show when={!failed()} fallback={<text>No preview</text>}>
|
|
|
|
|
<image
|
|
|
|
|
source={image.uri}
|
|
|
|
|
fit="cover"
|
|
|
|
|
protocol="auto"
|
|
|
|
|
width="100%"
|
|
|
|
|
height="100%"
|
|
|
|
|
onError={() => setFailed(true)}
|
|
|
|
|
/>
|
|
|
|
|
</Show>
|
|
|
|
|
</box>
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
</For>
|
|
|
|
|
<Show when={images().length > visible().length}>
|
|
|
|
|
<box width={8} height={height()} flexShrink={1} alignItems="center" justifyContent="center">
|
|
|
|
|
<text wrapMode="none" truncate>
|
|
|
|
|
+{images().length - visible().length} more
|
|
|
|
|
</text>
|
|
|
|
|
</box>
|
|
|
|
|
</Show>
|
|
|
|
|
</box>
|
|
|
|
|
</Show>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function inlineToolImages(part: SessionMessageAssistantTool) {
|
|
|
|
|
return toolDisplayContent(part.state).flatMap((content) =>
|
|
|
|
|
content.type === "file" && content.mime.startsWith("image/") && content.uri.startsWith("data:image/")
|
|
|
|
|
? [{ uri: content.uri }]
|
|
|
|
|
: [],
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ToolProps = {
|
|
|
|
|