summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src/components
diff options
context:
space:
mode:
authorFilip <[email protected]>2026-03-02 08:57:34 +0100
committerGitHub <[email protected]>2026-03-02 13:27:34 +0530
commitbf2cc3aa2f0f0576317d048852d83d45a4724c46 (patch)
tree19fb1349fca81579f6c09d6d20294da54cee2048 /packages/ui/src/components
parent4b9e19f72f8ebbf05f6b951fd96cf68ba0b23957 (diff)
downloadopencode-bf2cc3aa2f0f0576317d048852d83d45a4724c46.tar.gz
opencode-bf2cc3aa2f0f0576317d048852d83d45a4724c46.zip
feat(app): show which messages are queued (#15587)
Diffstat (limited to 'packages/ui/src/components')
-rw-r--r--packages/ui/src/components/message-part.css22
-rw-r--r--packages/ui/src/components/message-part.tsx17
-rw-r--r--packages/ui/src/components/session-turn.tsx27
3 files changed, 60 insertions, 6 deletions
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index 6727bb22f..c23a16ee1 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -46,12 +46,18 @@
overflow: hidden;
background: var(--surface-weak);
border: 1px solid var(--border-weak-base);
- transition: border-color 0.15s ease;
+ transition:
+ border-color 0.15s ease,
+ opacity 0.3s ease;
&:hover {
border-color: var(--border-strong-base);
}
+ &[data-queued] {
+ opacity: 0.6;
+ }
+
&[data-type="image"] {
width: 48px;
height: 48px;
@@ -101,6 +107,11 @@
border: 1px solid var(--border-weak-base);
padding: 8px 12px;
border-radius: 6px;
+ transition: opacity 0.3s ease;
+
+ &[data-queued] {
+ opacity: 0.6;
+ }
[data-highlight="file"] {
color: var(--syntax-property);
@@ -113,6 +124,14 @@
max-width: 100%;
}
+ [data-slot="user-message-queued-indicator"] {
+ margin-top: 6px;
+ margin-right: 2px;
+ font-size: var(--font-size-small);
+ color: var(--text-weak);
+ user-select: none;
+ }
+
[data-slot="user-message-copy-wrapper"] {
min-height: 24px;
margin-top: 4px;
@@ -149,6 +168,7 @@
align-items: center;
justify-content: flex-end;
overflow: hidden;
+ gap: 6px;
}
[data-slot="user-message-meta-tail"] {
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 02a99f9dd..39a2b4c23 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -92,6 +92,7 @@ export interface MessageProps {
parts: PartType[]
showAssistantCopyPartID?: string | null
interrupted?: boolean
+ queued?: boolean
showReasoningSummaries?: boolean
}
@@ -500,6 +501,7 @@ export function Message(props: MessageProps) {
message={userMessage() as UserMessage}
parts={props.parts}
interrupted={props.interrupted}
+ queued={props.queued}
/>
)}
</Match>
@@ -679,7 +681,12 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
)
}
-export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[]; interrupted?: boolean }) {
+export function UserMessageDisplay(props: {
+ message: UserMessage
+ parts: PartType[]
+ interrupted?: boolean
+ queued?: boolean
+}) {
const data = useData()
const dialog = useDialog()
const i18n = useI18n()
@@ -759,6 +766,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
<div
data-slot="user-message-attachment"
data-type={file.mime.startsWith("image/") ? "image" : "file"}
+ data-queued={props.queued ? "" : undefined}
onClick={() => {
if (file.mime.startsWith("image/") && file.url) {
openImagePreview(file.url, file.filename)
@@ -787,9 +795,14 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
<Show when={text()}>
<>
<div data-slot="user-message-body">
- <div data-slot="user-message-text">
+ <div data-slot="user-message-text" data-queued={props.queued ? "" : undefined}>
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
</div>
+ <Show when={props.queued}>
+ <div data-slot="user-message-queued-indicator">
+ <TextShimmer text={i18n.t("ui.message.queued")} />
+ </div>
+ </Show>
</div>
<div data-slot="user-message-copy-wrapper" data-interrupted={props.interrupted ? "" : undefined}>
<Show when={metaHead() || metaTail()}>
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index e329b1170..c441bcf61 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -192,11 +192,31 @@ export function SessionTurn(
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
)
})
+
+ const pendingUser = createMemo(() => {
+ const item = pending()
+ if (!item?.parentID) return
+ const messages = allMessages() ?? emptyMessages
+ const result = Binary.search(messages, item.parentID, (m) => m.id)
+ const msg = result.found ? messages[result.index] : messages.find((m) => m.id === item.parentID)
+ if (!msg || msg.role !== "user") return
+ return msg
+ })
+
const active = createMemo(() => {
const msg = message()
+ const parent = pendingUser()
+ if (!msg || !parent) return false
+ return parent.id === msg.id
+ })
+
+ const queued = createMemo(() => {
+ const id = message()?.id
+ if (!id) return false
+ if (!pendingUser()) return false
const item = pending()
- if (!msg || !item) return false
- return item.parentID === msg.id
+ if (!item) return false
+ return id > item.id
})
const parts = createMemo(() => {
@@ -334,6 +354,7 @@ export function SessionTurn(
)
const showThinking = createMemo(() => {
if (!working() || !!error()) return false
+ if (queued()) return false
if (status().type === "retry") return false
if (showReasoningSummaries()) return assistantVisible() === 0
if (assistantTailVisible() === "text") return false
@@ -364,7 +385,7 @@ export function SessionTurn(
class={props.classes?.container}
>
<div data-slot="session-turn-message-content" aria-live="off">
- <Message message={msg()} parts={parts()} interrupted={interrupted()} />
+ <Message message={msg()} parts={parts()} interrupted={interrupted()} queued={queued()} />
</div>
<Show when={compaction()}>
{(part) => (