refactor(schema): apply session review decisions (#35793)

This commit is contained in:
Kit Langton 2026-07-07 22:10:11 -04:00 committed by GitHub
commit ed6ad272ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
142 changed files with 4239 additions and 3174 deletions

View file

@ -26,14 +26,14 @@ import { createSyntaxStyleMemo, generateSubtleSyntax, useTheme } from "../../con
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
import { Prompt, type PromptRef } from "../../component/prompt"
import type {
ModelV2Info,
SessionMessage,
ModelInfo,
SessionMessageInfo,
SessionMessageAssistant,
SessionMessageAssistantReasoning,
SessionMessageAssistantText,
SessionMessageAssistantTool,
SessionMessageUser,
SessionV2Info,
SessionInfo,
} from "@opencode-ai/sdk/v2"
import { useLocal } from "../../context/local"
import { Locale } from "../../util/locale"
@ -130,7 +130,7 @@ const context = createContext<{
showGenericToolOutput: () => boolean
groupExploration: () => boolean
diffWrapMode: () => "word" | "none"
models: () => ModelV2Info[]
models: () => ModelInfo[]
tui: ReturnType<typeof useTuiConfig>
}>()
@ -172,16 +172,6 @@ export function Session() {
})
onCleanup(() => setEpilogue())
const messages = sessionMessages
const transientCompaction = createMemo(() => {
if (
messages().some(
(message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"),
)
)
return
const text = data.session.compaction(route.sessionID)
return text === undefined ? undefined : { text }
})
const descendantSessionIDs = createMemo(() => {
if (session()?.parentID) return []
return data.session.family(route.sessionID).filter((id) => id !== route.sessionID)
@ -936,9 +926,6 @@ export function Session() {
/>
)}
</For>
<Show when={transientCompaction()}>
{(compaction) => <CompactionMessage status="running" text={compaction().text} />}
</Show>
<BackgroundToolHint messages={messages()} />
<Show when={session()?.revert?.messageID}>
<RevertMessage
@ -1024,7 +1011,7 @@ export function Session() {
)
}
function SessionRowView(props: { row: SessionRow; message: (messageID: string) => SessionMessage | undefined }) {
function SessionRowView(props: { row: SessionRow; message: (messageID: string) => SessionMessageInfo | undefined }) {
return (
<box marginTop={1} flexShrink={0}>
<Switch>
@ -1062,7 +1049,7 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) =
)
}
function BackgroundToolHint(props: { messages: SessionMessage[] }) {
function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
const { theme } = useTheme()
const shortcut = useCommandShortcut("session.background")
const visible = createMemo(() => {
@ -1090,14 +1077,14 @@ function BackgroundToolHint(props: { messages: SessionMessage[] }) {
)
}
function SessionMessageView(props: { message: SessionMessage }) {
function SessionMessageView(props: { message: SessionMessageInfo }) {
return (
<Switch>
<Match when={props.message.type === "user"}>
<UserMessage message={props.message as SessionMessageUser} />
</Match>
<Match when={props.message.type === "shell"}>
<ShellMessage message={props.message as Extract<SessionMessage, { type: "shell" }>} />
<ShellMessage message={props.message as Extract<SessionMessageInfo, { type: "shell" }>} />
</Match>
<Match when={props.message.type === "agent-switched" || props.message.type === "model-switched"}>
<SessionSwitchMessageV2 message={props.message} />
@ -1106,17 +1093,17 @@ function SessionMessageView(props: { message: SessionMessage }) {
when={props.message.type === "system" || props.message.type === "synthetic" || props.message.type === "skill"}
>
<Show when={props.message.type === "skill"} fallback={<SessionNoticeMessageV2 message={props.message} />}>
<SessionSkillMessage message={props.message as Extract<SessionMessage, { type: "skill" }>} />
<SessionSkillMessage message={props.message as Extract<SessionMessageInfo, { type: "skill" }>} />
</Show>
</Match>
<Match when={props.message.type === "compaction"}>
<CompactionMessage message={props.message as Extract<SessionMessage, { type: "compaction" }>} />
<CompactionMessage message={props.message as Extract<SessionMessageInfo, { type: "compaction" }>} />
</Match>
</Switch>
)
}
function SessionPartView(props: { partRef: PartRef; message: (messageID: string) => SessionMessage | undefined }) {
function SessionPartView(props: { partRef: PartRef; message: (messageID: string) => SessionMessageInfo | undefined }) {
const message = createMemo(() => props.message(props.partRef.messageID))
const part = createMemo(() => {
const item = message()
@ -1150,7 +1137,7 @@ function SessionGroupView(props: {
refs: PartRef[]
pending: PartRef[]
completed: boolean
message: (messageID: string) => SessionMessage | undefined
message: (messageID: string) => SessionMessageInfo | undefined
}) {
const { theme } = useTheme()
const ctx = use()
@ -1260,7 +1247,7 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
)
}
function SessionSwitchMessageV2(props: { message: SessionMessage }) {
function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
const ctx = use()
const { theme } = useTheme()
const text = () => {
@ -1276,7 +1263,7 @@ function SessionSwitchMessageV2(props: { message: SessionMessage }) {
)
}
function SessionNoticeMessageV2(props: { message: SessionMessage }) {
function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
const { theme } = useTheme()
const text = () => {
if (props.message.type === "system") return "Instructions updated"
@ -1290,7 +1277,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessage }) {
)
}
function SessionSkillMessage(props: { message: Extract<SessionMessage, { type: "skill" }> }) {
function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { type: "skill" }> }) {
const { theme } = useTheme()
return (
<InlineToolRow icon="→" color={theme.textMuted} pending="Skill" complete={true}>
@ -1300,7 +1287,7 @@ function SessionSkillMessage(props: { message: Extract<SessionMessage, { type: "
}
function CompactionMessage(props: {
message?: Extract<SessionMessage, { type: "compaction" }>
message?: Extract<SessionMessageInfo, { type: "compaction" }>
status?: "running"
text?: string
}) {
@ -1308,9 +1295,10 @@ function CompactionMessage(props: {
const kv = useKV()
const { theme, syntax } = useTheme()
const status = () => props.message?.status ?? props.status
const text = () => props.message?.summary ?? props.text ?? ""
const text = () =>
props.message?.status === "failed" ? props.message.error.message : (props.message?.summary ?? props.text ?? "")
const color = () => (status() === "failed" ? theme.error : status() === "completed" ? theme.success : theme.textMuted)
const border = () => (status() === "queued" ? theme.border : color())
const border = color
return (
<box>
<box flexDirection="row" alignItems="center">
@ -1328,11 +1316,8 @@ function CompactionMessage(props: {
<Match when={status() === "failed"}>
<text fg={color()}></text>
</Match>
<Match when={status() === "queued"}>
<text fg={color()}></text>
</Match>
</Switch>
<text fg={color()}>{status() === "queued" ? "Compaction queued" : "Compaction"}</text>
<text fg={color()}>Compaction</text>
</box>
<box border={["top"]} borderColor={border()} flexGrow={1} />
</box>
@ -1363,7 +1348,7 @@ function statusLabel(status: "added" | "modified" | "deleted") {
function RevertMessage(props: {
count: number
files: ReadonlyArray<{
readonly path: string
readonly file: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
@ -1412,7 +1397,7 @@ function RevertMessage(props: {
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={theme.textMuted}>{statusLabel(file.status)}</text>
<text fg={theme.text} wrapMode="none">
{Locale.truncateLeft(file.path, 60)}
{Locale.truncateLeft(file.file, 60)}
</text>
<Show when={file.additions > 0}>
<text fg={theme.diffAdded}>+{file.additions}</text>
@ -1433,7 +1418,7 @@ function RevertMessage(props: {
)
}
function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell" }> }) {
function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "shell" }> }) {
const { theme } = useTheme()
const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
@ -1448,7 +1433,7 @@ function ShellMessage(props: { message: Extract<SessionMessage, { type: "shell"
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background}
>
<text fg={theme.text}>$ {props.message.shell.command}</text>
<text fg={theme.text}>$ {props.message.command}</text>
<Show when={output()}>
<text fg={theme.textMuted}>{output()}</text>
</Show>
@ -1560,7 +1545,7 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole
.map((part) =>
part.type === "tool" &&
["read", "glob", "grep"].includes(toolDisplay(part.name)) &&
part.state.status !== "pending"
part.state.status !== "streaming"
? part
: undefined,
)
@ -1832,7 +1817,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
const data = useData()
const display = createMemo(() => toolDisplay(props.part.name))
const activeBackgroundWork = createMemo(() => {
if (props.part.state.status === "pending") return false
if (props.part.state.status === "streaming") return false
if (display() === "shell") {
const shellID = stringValue(props.part.state.structured.shellID)
return Boolean(shellID && data.shell.get(shellID))
@ -1856,13 +1841,13 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) {
const toolprops = {
get metadata() {
return props.part.state.status === "pending" ? {} : props.part.state.structured
return props.part.state.status === "streaming" ? {} : props.part.state.structured
},
get input() {
return typeof props.part.state.input === "string" ? {} : props.part.state.input
},
get output() {
if (props.part.state.status === "pending") return undefined
if (props.part.state.status === "streaming") return undefined
return props.part.state.content
.flatMap((content) => (content.type === "text" ? [content.text] : [content.name ?? content.uri]))
.join("\n")
@ -2187,7 +2172,7 @@ function Shell(props: ToolProps) {
const isRunning = createMemo(() => props.part.state.status === "running" || backgroundRunning())
const command = createMemo(() => stringValue(props.input.command))
const output = createMemo(() => {
if (props.part.state.status === "pending") return ""
if (props.part.state.status === "streaming") return ""
if (shellID()) return ""
const content = props.part.state.content[0]
return stripAnsi(content?.type === "text" ? content.text.trim() : "")
@ -2209,7 +2194,7 @@ function Shell(props: ToolProps) {
<Show
when={command()}
fallback={
isRunning() || props.part.state.status === "pending" ? (
isRunning() || props.part.state.status === "streaming" ? (
<Spinner color={color()}>Writing command...</Spinner>
) : (
<text fg={theme.textMuted}>Writing command...</text>
@ -2418,7 +2403,7 @@ function executeCalls(value: unknown): ExecuteCall[] {
function Execute(props: ToolProps) {
const ctx = use()
const { theme } = useTheme()
const isLoading = createMemo(() => props.part.state.status === "pending" || props.part.state.status === "running")
const isLoading = createMemo(() => props.part.state.status === "streaming" || props.part.state.status === "running")
const calls = createMemo(() => executeCalls(props.metadata.toolCalls))
const output = createMemo(() => stripAnsi(props.output?.trim() ?? ""))
const hasRuntimeError = createMemo(() => props.metadata.error === true)
@ -2514,7 +2499,7 @@ function Edit(props: ToolProps) {
: "# Preparing edit..."
}
part={props.part}
spinner={props.part.state.status === "pending"}
spinner={props.part.state.status === "streaming"}
/>
</Match>
</Switch>
@ -2606,7 +2591,7 @@ function ApplyPatch(props: ToolProps) {
: "# Preparing patch..."
}
part={props.part}
spinner={props.part.state.status === "pending"}
spinner={props.part.state.status === "streaming"}
/>
</Match>
</Switch>
@ -2756,15 +2741,15 @@ function recordValue(value: unknown): Record<string, unknown> | undefined {
}
function formatSessionTranscript(
session: SessionV2Info,
messages: SessionMessage[],
session: SessionInfo,
messages: SessionMessageInfo[],
thinking: boolean,
toolDetails: boolean,
) {
const body = messages.flatMap((message) => {
if (message.type === "user") return [`## User\n\n${message.text}`]
if (message.type === "shell")
return [`## Shell\n\n\`\`\`\n$ ${message.shell.command}\n${message.output?.output ?? ""}\n\`\`\``]
return [`## Shell\n\n\`\`\`\n$ ${message.command}\n${message.output?.output ?? ""}\n\`\`\``]
if (message.type !== "assistant") return []
const content = message.content.flatMap((item) => {
if (item.type === "text") return [item.text]
@ -2774,7 +2759,7 @@ function formatSessionTranscript(
const output =
item.state.status === "error"
? item.state.error.message
: item.state.status === "pending"
: item.state.status === "streaming"
? ""
: item.state.content
.flatMap((entry) => (entry.type === "text" ? [entry.text] : [entry.name ?? entry.uri]))

View file

@ -148,7 +148,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
const message = data.session.message.get(props.request.sessionID, tool.messageID)
if (message?.type !== "assistant") return {}
const part = message.content.find((part) => part.type === "tool" && part.id === tool.callID)
if (part?.type === "tool" && part.state.status !== "pending") return part.state.input
if (part?.type === "tool" && part.state.status !== "streaming") return part.state.input
return {}
})

View file

@ -1,4 +1,4 @@
import type { SessionMessage, SessionMessageAssistant } from "@opencode-ai/sdk/v2"
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2"
import { createEffect, on, onCleanup, type Accessor } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { useData } from "../../context/data"
@ -88,7 +88,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
{
id: message.id,
created: message.time.created,
input: message.status === "queued" || message.status === "running",
input: message.status === "running",
},
]
: [],
@ -157,7 +157,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
const isPending = (messageID: string) => {
const message = data.session.message.get(sessionID(), messageID)
if (message?.type === "user") return data.session.input.has(sessionID(), messageID)
return message?.type === "compaction" && (message.status === "queued" || message.status === "running")
return message?.type === "compaction" && message.status === "running"
}
const queuedStart = (rows: SessionRow[]) => {
@ -173,7 +173,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
}
const subscriptions = [
data.on("session.prompt.admitted", input),
data.on("session.compaction.admitted", input),
data.on("session.compaction.started", message),
data.on("session.instructions.updated", message),
data.on("session.synthetic", (event) => {
if (event.data.sessionID === sessionID() && event.data.description?.trim())
@ -224,11 +224,9 @@ export function createSessionRows(sessionID: Accessor<string>) {
return rows
}
export function reduceSessionRows(messages: SessionMessage[], inputs = new Set<string>()) {
const isInput = (message: SessionMessage) => inputs.has(message.id)
const pendingCompactions = messages.filter(
(message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"),
)
export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set<string>()) {
const isInput = (message: SessionMessageInfo) => inputs.has(message.id)
const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running")
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
return [
...messages.filter((message) => !pending.has(message.id)),