feat(app): interruption state

This commit is contained in:
Adam 2026-03-12 19:07:19 -05:00
parent 268855dc5a
commit c173988aaa
No known key found for this signature in database
GPG Key ID: 9CB48779AF150E75
3 changed files with 21 additions and 34 deletions

View File

@ -23,10 +23,6 @@
max-width: 100%; max-width: 100%;
gap: 0; gap: 0;
&[data-interrupted] {
color: var(--text-weak);
}
[data-slot="user-message-attachments"] { [data-slot="user-message-attachments"] {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -165,10 +161,6 @@
text-align: right; text-align: right;
} }
[data-slot="user-message-copy-wrapper"][data-interrupted] {
gap: 12px;
}
&:hover [data-slot="user-message-copy-wrapper"], &:hover [data-slot="user-message-copy-wrapper"],
&:focus-within [data-slot="user-message-copy-wrapper"] { &:focus-within [data-slot="user-message-copy-wrapper"] {
opacity: 1; opacity: 1;

View File

@ -131,7 +131,6 @@ export interface MessageProps {
parts: PartType[] parts: PartType[]
actions?: UserActions actions?: UserActions
showAssistantCopyPartID?: string | null showAssistantCopyPartID?: string | null
interrupted?: boolean
showReasoningSummaries?: boolean showReasoningSummaries?: boolean
} }
@ -691,12 +690,7 @@ export function Message(props: MessageProps) {
<Switch> <Switch>
<Match when={props.message.role === "user" && props.message}> <Match when={props.message.role === "user" && props.message}>
{(userMessage) => ( {(userMessage) => (
<UserMessageDisplay <UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} actions={props.actions} />
message={userMessage() as UserMessage}
parts={props.parts}
actions={props.actions}
interrupted={props.interrupted}
/>
)} )}
</Match> </Match>
<Match when={props.message.role === "assistant" && props.message}> <Match when={props.message.role === "assistant" && props.message}>
@ -887,12 +881,7 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
) )
} }
export function UserMessageDisplay(props: { export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[]; actions?: UserActions }) {
message: UserMessage
parts: PartType[]
actions?: UserActions
interrupted?: boolean
}) {
const data = useData() const data = useData()
const dialog = useDialog() const dialog = useDialog()
const i18n = useI18n() const i18n = useI18n()
@ -947,10 +936,7 @@ export function UserMessageDisplay(props: {
return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0") return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0")
}) })
const metaTail = createMemo(() => { const metaTail = stamp
const items = [stamp(), props.interrupted ? i18n.t("ui.message.interrupted") : ""]
return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0")
})
const openImagePreview = (url: string, alt?: string) => { const openImagePreview = (url: string, alt?: string) => {
dialog.show(() => <ImagePreview src={url} alt={alt} />) dialog.show(() => <ImagePreview src={url} alt={alt} />)
@ -981,7 +967,7 @@ export function UserMessageDisplay(props: {
} }
return ( return (
<div data-component="user-message" data-interrupted={props.interrupted ? "" : undefined}> <div data-component="user-message">
<Show when={attachments().length > 0}> <Show when={attachments().length > 0}>
<div data-slot="user-message-attachments"> <div data-slot="user-message-attachments">
<For each={attachments()}> <For each={attachments()}>
@ -1021,7 +1007,7 @@ export function UserMessageDisplay(props: {
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} /> <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
</div> </div>
</div> </div>
<div data-slot="user-message-copy-wrapper" data-interrupted={props.interrupted ? "" : undefined}> <div data-slot="user-message-copy-wrapper">
<Show when={metaHead() || metaTail()}> <Show when={metaHead() || metaTail()}>
<span data-slot="user-message-meta-wrap"> <span data-slot="user-message-meta-wrap">
<Show when={metaHead()}> <Show when={metaHead()}>
@ -1305,14 +1291,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
) )
} }
PART_MAPPING["compaction"] = function CompactionPartDisplay() { export function MessageDivider(props: { label: string }) {
const i18n = useI18n()
return ( return (
<div data-component="compaction-part"> <div data-component="compaction-part">
<div data-slot="compaction-part-divider"> <div data-slot="compaction-part-divider">
<span data-slot="compaction-part-line" /> <span data-slot="compaction-part-line" />
<span data-slot="compaction-part-label" class="text-12-regular text-text-weak"> <span data-slot="compaction-part-label" class="text-12-regular text-text-weak">
{i18n.t("ui.messagePart.compaction")} {props.label}
</span> </span>
<span data-slot="compaction-part-line" /> <span data-slot="compaction-part-line" />
</div> </div>
@ -1320,6 +1305,11 @@ PART_MAPPING["compaction"] = function CompactionPartDisplay() {
) )
} }
PART_MAPPING["compaction"] = function CompactionPartDisplay() {
const i18n = useI18n()
return <MessageDivider label={i18n.t("ui.messagePart.compaction")} />
}
PART_MAPPING["text"] = function TextPartDisplay(props) { PART_MAPPING["text"] = function TextPartDisplay(props) {
const data = useData() const data = useData()
const i18n = useI18n() const i18n = useI18n()

View File

@ -7,7 +7,7 @@ import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { AssistantParts, Message, Part, PART_MAPPING, type UserActions } from "./message-part" import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part"
import { Card } from "./card" import { Card } from "./card"
import { Accordion } from "./accordion" import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header" import { StickyAccordionHeader } from "./sticky-accordion-header"
@ -276,6 +276,11 @@ export function SessionTurn(
) )
const interrupted = createMemo(() => assistantMessages().some((m) => m.error?.name === "MessageAbortedError")) const interrupted = createMemo(() => assistantMessages().some((m) => m.error?.name === "MessageAbortedError"))
const divider = createMemo(() => {
if (compaction()) return i18n.t("ui.messagePart.compaction")
if (interrupted()) return i18n.t("ui.message.interrupted")
return ""
})
const error = createMemo( const error = createMemo(
() => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error, () => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error,
) )
@ -384,11 +389,11 @@ export function SessionTurn(
class={props.classes?.container} class={props.classes?.container}
> >
<div data-slot="session-turn-message-content" aria-live="off"> <div data-slot="session-turn-message-content" aria-live="off">
<Message message={message()!} parts={parts()} actions={props.actions} interrupted={interrupted()} /> <Message message={message()!} parts={parts()} actions={props.actions} />
</div> </div>
<Show when={compaction()}> <Show when={divider()}>
<div data-slot="session-turn-compaction"> <div data-slot="session-turn-compaction">
<Part part={compaction()!} message={message()!} hideDetails /> <MessageDivider label={divider()} />
</div> </div>
</Show> </Show>
<Show when={assistantMessages().length > 0}> <Show when={assistantMessages().length > 0}>