Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 4 additions & 169 deletions packages/tui/src/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Match,
on,
onCleanup,
onMount,
Show,
Switch,
useContext,
Expand All @@ -21,10 +20,10 @@ import { createStore } from "solid-js/store"
import { useProject } from "../../context/project"
import { useData } from "../../context/data"
import { SplitBorder } from "../../ui/border"
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
import { useTuiTerminalEnvironment } from "../../context/runtime"
import { Spinner, SPINNER_FRAMES } from "../../component/spinner"
import { createSyntaxStyleMemo, generateSubtleSyntax, useTheme } from "../../context/theme"
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
import { ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
import { Prompt, type PromptRef } from "../../component/prompt"
import type {
ModelInfo,
Expand All @@ -42,7 +41,6 @@ import { webSearchProviderLabel } from "../../util/tool-display"
import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
import { useSDK } from "../../context/sdk"
import { useEditorContext } from "../../context/editor"
import { openEditor } from "../../editor"
import { useDialog } from "../../ui/dialog"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { DialogMessage } from "./dialog-message"
Expand Down Expand Up @@ -153,7 +151,6 @@ export function Session() {
const { navigate } = useRoute()
const data = useData()
const project = useProject()
const paths = useTuiPaths()
const tuiConfig = useTuiConfig()
const kv = useKV()
const { theme } = useTheme()
Expand Down Expand Up @@ -196,16 +193,6 @@ export function Session() {
})
const disabled = createMemo(() => permissions().length > 0 || forms().length > 0)

const pending = createMemo(() => {
const completed = messages().findLast((x) => x.type === "assistant" && x.time.completed)?.id
return messages().findLast((x) => x.type === "assistant" && !x.time.completed && (!completed || x.id > completed))
?.id
})

const lastAssistant = createMemo(() => {
return messages().findLast((x) => x.type === "assistant")
})

const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = kv.signal<"auto" | "hide">("sidebar", "auto")
const [sidebarOpen, setSidebarOpen] = createSignal(false)
Expand Down Expand Up @@ -314,7 +301,7 @@ export function Session() {
const messagesList = messages()
const scrollTop = scroll.y

// Get visible messages sorted by position, filtering for valid non-synthetic, non-ignored content
// Find rendered V2 message boundaries that contain visible user or assistant text.
const visibleMessages = children
.filter((c) => {
if (!c.id) return false
Expand Down Expand Up @@ -653,7 +640,7 @@ export function Session() {
const messages = sessionMessages()
if (!messages || !messages.length) return

// Find the most recent user message with non-ignored, non-synthetic text parts
// Find the most recent rendered V2 user message with visible text.
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]
if (!message || message.type !== "user" || !message.text.trim()) continue
Expand Down Expand Up @@ -1562,122 +1549,6 @@ function UserMessage(props: { message: SessionMessageUser }) {
)
}

function AssistantMessage(props: { message: SessionMessageAssistant; last: boolean }) {
const ctx = use()
const local = useLocal()
const { theme } = useTheme()
const model = createMemo(
() =>
ctx
.models()
.find((model) => model.providerID === props.message.model.providerID && model.id === props.message.model.id)
?.name ?? `${props.message.model.providerID}/${props.message.model.id}`,
)

const final = createMemo(() => {
return props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)
})

const duration = createMemo(() => {
if (!final()) return 0
if (!props.message.time.completed) return 0
return props.message.time.completed - props.message.time.created
})

const exploration = createMemo(() => {
const grouped = new Map<string, { first: boolean; parts: SessionMessageAssistantTool[]; active: boolean }>()
if (!ctx.groupExploration()) return grouped
const runs = props.message.content
.map((part) =>
part.type === "tool" &&
["read", "glob", "grep"].includes(toolDisplay(part.name)) &&
part.state.status !== "streaming"
? part
: undefined,
)
.reduce<SessionMessageAssistantTool[][]>(
(runs, part) => {
if (part) runs[runs.length - 1].push(part)
if (!part && runs[runs.length - 1].length) runs.push([])
return runs
},
[[]],
)
.filter((run) => run.length > 0)
for (const run of runs) {
const summary = {
parts: run,
active: false,
}
run.forEach((part, index) => grouped.set(part.id, { ...summary, first: index === 0 }))
}
return grouped
})

return (
<>
<For each={props.message.content}>
{(content, index) => (
<Switch>
<Match when={content.type === "text"}>
<TextPart
part={content as SessionMessageAssistantText}
last={index() === props.message.content.length - 1}
/>
</Match>
<Match when={content.type === "reasoning"}>
<ReasoningPart
part={content as SessionMessageAssistantReasoning}
message={props.message}
last={index() === props.message.content.length - 1}
/>
</Match>
<Match when={content.type === "tool"}>
<Show when={exploration().get((content as SessionMessageAssistantTool).id)?.first !== false}>
<Show
when={exploration().get((content as SessionMessageAssistantTool).id)}
fallback={<ToolPart part={content as SessionMessageAssistantTool} />}
>
{(summary) => <ExplorationSummary {...summary()} />}
</Show>
</Show>
</Match>
</Switch>
)}
</For>
<Show when={props.message.error}>
<box
border={["left"]}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
backgroundColor={theme.backgroundPanel}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.error}
>
<text fg={theme.textMuted}>{errorMessage(props.message.error)}</text>
</box>
</Show>
<AssistantRetry retry={props.message.retry} />
<Switch>
<Match when={props.last || final() || props.message.error}>
<box paddingLeft={3}>
<text>
<span style={{ fg: props.message.error ? theme.textMuted : local.agent.color(props.message.agent) }}>
{Locale.titlecase(props.message.agent)}
</span>
<span style={{ fg: theme.textMuted }}> · {model()}</span>
<Show when={duration()}>
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
</Show>
</text>
</box>
</Match>
</Switch>
</>
)
}

function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
const { theme } = useTheme()
return (
Expand All @@ -1693,40 +1564,6 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
)
}

function ExplorationSummary(props: { parts: SessionMessageAssistantTool[]; active: boolean }) {
const { theme } = useTheme()
const pathFormatter = usePathFormatter()
const label = (part: SessionMessageAssistantTool) => {
const input = typeof part.state.input === "string" ? {} : part.state.input
const tool = toolDisplay(part.name)
if (tool === "read") return `Read ${pathFormatter.format(stringValue(input.path))}`
if (tool === "glob") return `Glob "${stringValue(input.pattern)}"`
return `Grep "${stringValue(input.pattern)}"`
}
return (
<box flexDirection="column">
<InlineToolRow
icon="✱"
color={theme.textMuted}
complete={!props.active}
pending="Exploring"
spinner={props.active}
>
{props.active ? "Exploring" : "Explored"}
</InlineToolRow>
<For each={props.parts}>
{(part, index) => (
<box paddingLeft={5}>
<text fg={part.state.status === "error" ? theme.error : theme.textMuted}>
{index() === props.parts.length - 1 ? "└" : "├"} {label(part)}
</text>
</box>
)}
</For>
</box>
)
}

const INLINE_TOOL_ICON_WIDTH = 2

function ReasoningPart(props: {
Expand Down Expand Up @@ -1856,8 +1693,6 @@ function TextPart(props: { last: boolean; part: SessionMessageAssistantText }) {
)
}

// Pending messages moved to individual tool pending functions

function ToolPart(props: { part: SessionMessageAssistantTool }) {
const ctx = use()
const data = useData()
Expand Down
Loading