|
@@ -5,7 +5,7 @@ import { Spinner } from "@tui/component/spinner"
|
|
|
import { useTheme } from "@tui/context/theme"
|
|
import { useTheme } from "@tui/context/theme"
|
|
|
import { useLocal } from "@tui/context/local"
|
|
import { useLocal } from "@tui/context/local"
|
|
|
import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
|
import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
|
|
-import type { SyntaxStyle } from "@opentui/core"
|
|
|
|
|
|
|
+import { TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core"
|
|
|
import { Locale } from "@/util/locale"
|
|
import { Locale } from "@/util/locale"
|
|
|
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
|
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
|
|
import path from "path"
|
|
import path from "path"
|
|
@@ -44,6 +44,10 @@ function View(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
const messages = createMemo(() => sync.data.messages[props.sessionID] ?? [])
|
|
const messages = createMemo(() => sync.data.messages[props.sessionID] ?? [])
|
|
|
const renderedMessages = createMemo(() => messages().toReversed())
|
|
const renderedMessages = createMemo(() => messages().toReversed())
|
|
|
const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant"))
|
|
const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant"))
|
|
|
|
|
+ const lastUserCreated = (index: number) =>
|
|
|
|
|
+ renderedMessages()
|
|
|
|
|
+ .slice(0, index)
|
|
|
|
|
+ .findLast((message) => message.type === "user")?.time.created
|
|
|
|
|
|
|
|
createEffect(() => {
|
|
createEffect(() => {
|
|
|
void sync.session.message.sync(props.sessionID)
|
|
void sync.session.message.sync(props.sessionID)
|
|
@@ -83,10 +87,11 @@ function View(props: { api: TuiPluginApi; sessionID: string }) {
|
|
|
last={lastAssistant()?.id === message.id}
|
|
last={lastAssistant()?.id === message.id}
|
|
|
syntax={syntax()}
|
|
syntax={syntax()}
|
|
|
subtleSyntax={subtleSyntax()}
|
|
subtleSyntax={subtleSyntax()}
|
|
|
|
|
+ start={lastUserCreated(index())}
|
|
|
/>
|
|
/>
|
|
|
</Match>
|
|
</Match>
|
|
|
<Match when={message.type === "synthetic"}>
|
|
<Match when={message.type === "synthetic"}>
|
|
|
- <SyntheticMessage message={message as SessionMessageSynthetic} index={index()} />
|
|
|
|
|
|
|
+ <></>
|
|
|
</Match>
|
|
</Match>
|
|
|
<Match when={message.type === "shell"}>
|
|
<Match when={message.type === "shell"}>
|
|
|
<ShellMessage message={message as SessionMessageShell} />
|
|
<ShellMessage message={message as SessionMessageShell} />
|
|
@@ -146,63 +151,36 @@ function UserMessage(props: { message: SessionMessageUser; index: number }) {
|
|
|
<box
|
|
<box
|
|
|
id={props.message.id}
|
|
id={props.message.id}
|
|
|
border={["left"]}
|
|
border={["left"]}
|
|
|
- borderColor={theme.primary}
|
|
|
|
|
|
|
+ borderColor={theme.secondary}
|
|
|
customBorderChars={SplitBorder.customBorderChars}
|
|
customBorderChars={SplitBorder.customBorderChars}
|
|
|
marginTop={props.index === 0 ? 0 : 1}
|
|
marginTop={props.index === 0 ? 0 : 1}
|
|
|
flexShrink={0}
|
|
flexShrink={0}
|
|
|
- >
|
|
|
|
|
- <box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
|
|
|
|
|
- <Show
|
|
|
|
|
- when={props.message.text.trim()}
|
|
|
|
|
- fallback={
|
|
|
|
|
- <MissingData label="User message text" detail={`Message ${props.message.id} has no text field content.`} />
|
|
|
|
|
- }
|
|
|
|
|
- >
|
|
|
|
|
- <text fg={theme.text}>{props.message.text}</text>
|
|
|
|
|
- </Show>
|
|
|
|
|
- <Show when={attachments().length}>
|
|
|
|
|
- <box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
|
|
|
|
- <For each={props.message.files ?? []}>
|
|
|
|
|
- {(file) => (
|
|
|
|
|
- <text fg={theme.text}>
|
|
|
|
|
- <span style={{ bg: theme.secondary, fg: theme.background }}> {file.mime} </span>
|
|
|
|
|
- <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.name ?? file.uri} </span>
|
|
|
|
|
- </text>
|
|
|
|
|
- )}
|
|
|
|
|
- </For>
|
|
|
|
|
- <For each={props.message.agents ?? []}>
|
|
|
|
|
- {(agent) => (
|
|
|
|
|
- <text fg={theme.text}>
|
|
|
|
|
- <span style={{ bg: theme.accent, fg: theme.background }}> agent </span>
|
|
|
|
|
- <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {agent.name} </span>
|
|
|
|
|
- </text>
|
|
|
|
|
- )}
|
|
|
|
|
- </For>
|
|
|
|
|
- </box>
|
|
|
|
|
- </Show>
|
|
|
|
|
- <text fg={theme.textMuted}>{Locale.todayTimeOrDateTime(props.message.time.created)}</text>
|
|
|
|
|
- </box>
|
|
|
|
|
- </box>
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function SyntheticMessage(props: { message: SessionMessageSynthetic; index: number }) {
|
|
|
|
|
- const { theme } = useTheme()
|
|
|
|
|
- return (
|
|
|
|
|
- <box
|
|
|
|
|
- id={props.message.id}
|
|
|
|
|
- border={["left"]}
|
|
|
|
|
- borderColor={theme.backgroundElement}
|
|
|
|
|
- customBorderChars={SplitBorder.customBorderChars}
|
|
|
|
|
- marginTop={props.index === 0 ? 0 : 1}
|
|
|
|
|
- paddingLeft={2}
|
|
|
|
|
paddingTop={1}
|
|
paddingTop={1}
|
|
|
paddingBottom={1}
|
|
paddingBottom={1}
|
|
|
|
|
+ paddingLeft={2}
|
|
|
backgroundColor={theme.backgroundPanel}
|
|
backgroundColor={theme.backgroundPanel}
|
|
|
- flexShrink={0}
|
|
|
|
|
>
|
|
>
|
|
|
- <text fg={theme.textMuted}>Synthetic</text>
|
|
|
|
|
<text fg={theme.text}>{props.message.text}</text>
|
|
<text fg={theme.text}>{props.message.text}</text>
|
|
|
|
|
+ <Show when={attachments().length}>
|
|
|
|
|
+ <box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
|
|
|
|
+ <For each={props.message.files ?? []}>
|
|
|
|
|
+ {(file) => (
|
|
|
|
|
+ <text fg={theme.text}>
|
|
|
|
|
+ <span style={{ bg: theme.secondary, fg: theme.background }}> {file.mime} </span>
|
|
|
|
|
+ <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.name ?? file.uri} </span>
|
|
|
|
|
+ </text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </For>
|
|
|
|
|
+ <For each={props.message.agents ?? []}>
|
|
|
|
|
+ {(agent) => (
|
|
|
|
|
+ <text fg={theme.text}>
|
|
|
|
|
+ <span style={{ bg: theme.accent, fg: theme.background }}> agent </span>
|
|
|
|
|
+ <span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {agent.name} </span>
|
|
|
|
|
+ </text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </For>
|
|
|
|
|
+ </box>
|
|
|
|
|
+ </Show>
|
|
|
</box>
|
|
</box>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
@@ -237,7 +215,7 @@ function ShellMessage(props: { message: SessionMessageShell }) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function CompactionMessage(props: { message: SessionMessageCompaction }) {
|
|
function CompactionMessage(props: { message: SessionMessageCompaction }) {
|
|
|
- const { theme } = useTheme()
|
|
|
|
|
|
|
+ const { theme, syntax } = useTheme()
|
|
|
return (
|
|
return (
|
|
|
<box
|
|
<box
|
|
|
marginTop={1}
|
|
marginTop={1}
|
|
@@ -248,7 +226,19 @@ function CompactionMessage(props: { message: SessionMessageCompaction }) {
|
|
|
flexShrink={0}
|
|
flexShrink={0}
|
|
|
>
|
|
>
|
|
|
<Show when={props.message.summary}>
|
|
<Show when={props.message.summary}>
|
|
|
- <text fg={theme.textMuted}>{props.message.summary}</text>
|
|
|
|
|
|
|
+ {(summary) => (
|
|
|
|
|
+ <box paddingLeft={3} paddingTop={1}>
|
|
|
|
|
+ <code
|
|
|
|
|
+ filetype="markdown"
|
|
|
|
|
+ drawUnstyledText={false}
|
|
|
|
|
+ streaming={false}
|
|
|
|
|
+ syntaxStyle={syntax()}
|
|
|
|
|
+ content={summary().trim()}
|
|
|
|
|
+ conceal={true}
|
|
|
|
|
+ fg={theme.text}
|
|
|
|
|
+ />
|
|
|
|
|
+ </box>
|
|
|
|
|
+ )}
|
|
|
</Show>
|
|
</Show>
|
|
|
</box>
|
|
</box>
|
|
|
)
|
|
)
|
|
@@ -294,12 +284,13 @@ function AssistantMessage(props: {
|
|
|
last: boolean
|
|
last: boolean
|
|
|
syntax: SyntaxStyle
|
|
syntax: SyntaxStyle
|
|
|
subtleSyntax: SyntaxStyle
|
|
subtleSyntax: SyntaxStyle
|
|
|
|
|
+ start?: number
|
|
|
}) {
|
|
}) {
|
|
|
const { theme } = useTheme()
|
|
const { theme } = useTheme()
|
|
|
const local = useLocal()
|
|
const local = useLocal()
|
|
|
const duration = createMemo(() => {
|
|
const duration = createMemo(() => {
|
|
|
if (!props.message.time.completed) return 0
|
|
if (!props.message.time.completed) return 0
|
|
|
- return props.message.time.completed - props.message.time.created
|
|
|
|
|
|
|
+ return props.message.time.completed - (props.start ?? props.message.time.created)
|
|
|
})
|
|
})
|
|
|
const model = createMemo(() => {
|
|
const model = createMemo(() => {
|
|
|
const variant = props.message.model.variant ? `/${props.message.model.variant}` : ""
|
|
const variant = props.message.model.variant ? `/${props.message.model.variant}` : ""
|
|
@@ -361,7 +352,7 @@ function AssistantText(props: { part: SessionMessageAssistantText; syntax: Synta
|
|
|
const { theme } = useTheme()
|
|
const { theme } = useTheme()
|
|
|
return (
|
|
return (
|
|
|
<Show when={props.part.text.trim()}>
|
|
<Show when={props.part.text.trim()}>
|
|
|
- <box paddingLeft={3} marginTop={1} flexShrink={0}>
|
|
|
|
|
|
|
+ <box paddingLeft={3} marginTop={1} flexShrink={0} id="text">
|
|
|
<code
|
|
<code
|
|
|
filetype="markdown"
|
|
filetype="markdown"
|
|
|
drawUnstyledText={false}
|
|
drawUnstyledText={false}
|
|
@@ -521,33 +512,93 @@ function InlineTool(props: {
|
|
|
part: SessionMessageAssistantTool
|
|
part: SessionMessageAssistantTool
|
|
|
}) {
|
|
}) {
|
|
|
const { theme } = useTheme()
|
|
const { theme } = useTheme()
|
|
|
|
|
+ const renderer = useRenderer()
|
|
|
|
|
+ const [margin, setMargin] = createSignal(0)
|
|
|
|
|
+ const [hover, setHover] = createSignal(false)
|
|
|
|
|
+ const [showError, setShowError] = createSignal(false)
|
|
|
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined))
|
|
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined))
|
|
|
|
|
+ const complete = createMemo(() => !!props.complete)
|
|
|
const denied = createMemo(() => {
|
|
const denied = createMemo(() => {
|
|
|
const message = error()
|
|
const message = error()
|
|
|
if (!message) return false
|
|
if (!message) return false
|
|
|
return (
|
|
return (
|
|
|
message.includes("QuestionRejectedError") ||
|
|
message.includes("QuestionRejectedError") ||
|
|
|
message.includes("rejected permission") ||
|
|
message.includes("rejected permission") ||
|
|
|
|
|
+ message.includes("specified a rule") ||
|
|
|
message.includes("user dismissed")
|
|
message.includes("user dismissed")
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
+ const fg = createMemo(() => {
|
|
|
|
|
+ if (error()) return theme.error
|
|
|
|
|
+ if (complete()) return theme.textMuted
|
|
|
|
|
+ return theme.text
|
|
|
|
|
+ })
|
|
|
|
|
+ const attributes = createMemo(() => (denied() ? TextAttributes.STRIKETHROUGH : undefined))
|
|
|
return (
|
|
return (
|
|
|
- <box marginTop={1} paddingLeft={3} flexShrink={0}>
|
|
|
|
|
- <Switch>
|
|
|
|
|
- <Match when={props.spinner}>
|
|
|
|
|
- <Spinner color={theme.text}>{props.children}</Spinner>
|
|
|
|
|
- </Match>
|
|
|
|
|
- <Match when={true}>
|
|
|
|
|
- <text paddingLeft={3} fg={props.complete ? theme.textMuted : theme.text}>
|
|
|
|
|
- <Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
|
|
|
|
- {props.icon} {props.children}
|
|
|
|
|
- </Show>
|
|
|
|
|
- </text>
|
|
|
|
|
- </Match>
|
|
|
|
|
- </Switch>
|
|
|
|
|
- <Show when={error() && !denied()}>
|
|
|
|
|
- <text fg={theme.error}>{error()}</text>
|
|
|
|
|
- </Show>
|
|
|
|
|
|
|
+ <box
|
|
|
|
|
+ marginTop={margin()}
|
|
|
|
|
+ paddingLeft={3}
|
|
|
|
|
+ flexShrink={0}
|
|
|
|
|
+ flexDirection="row"
|
|
|
|
|
+ gap={1}
|
|
|
|
|
+ backgroundColor={hover() && error() ? theme.backgroundMenu : undefined}
|
|
|
|
|
+ onMouseOver={() => error() && setHover(true)}
|
|
|
|
|
+ onMouseOut={() => setHover(false)}
|
|
|
|
|
+ onMouseUp={() => {
|
|
|
|
|
+ if (!error()) return
|
|
|
|
|
+ if (renderer.getSelection()?.getSelectedText()) return
|
|
|
|
|
+ setShowError((prev) => !prev)
|
|
|
|
|
+ }}
|
|
|
|
|
+ renderBefore={function () {
|
|
|
|
|
+ const el = this as BoxRenderable
|
|
|
|
|
+ const parent = el.parent
|
|
|
|
|
+ if (!parent) return
|
|
|
|
|
+ const previous = parent.getChildren()[parent.getChildren().indexOf(el) - 1]
|
|
|
|
|
+ if (!previous) {
|
|
|
|
|
+ setMargin(0)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (previous.id.startsWith("text")) setMargin(1)
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <box flexShrink={0}>
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match when={props.spinner}>
|
|
|
|
|
+ <Spinner color={theme.text} />
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={complete()}>
|
|
|
|
|
+ <text fg={fg()} attributes={attributes()}>
|
|
|
|
|
+ {props.icon}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={true}>
|
|
|
|
|
+ <text fg={fg()} attributes={attributes()}>
|
|
|
|
|
+ ~
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ </box>
|
|
|
|
|
+ <box flexGrow={1}>
|
|
|
|
|
+ <box>
|
|
|
|
|
+ <Switch>
|
|
|
|
|
+ <Match when={complete()}>
|
|
|
|
|
+ <text fg={fg()} attributes={attributes()}>
|
|
|
|
|
+ {props.children}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ <Match when={true}>
|
|
|
|
|
+ <text fg={fg()} attributes={attributes()}>
|
|
|
|
|
+ {props.pending}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </Match>
|
|
|
|
|
+ </Switch>
|
|
|
|
|
+ </box>
|
|
|
|
|
+ <Show when={showError() && error()}>
|
|
|
|
|
+ <box>
|
|
|
|
|
+ <text fg={theme.error}>{error()}</text>
|
|
|
|
|
+ </box>
|
|
|
|
|
+ </Show>
|
|
|
|
|
+ </box>
|
|
|
</box>
|
|
</box>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|