summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-02-17 15:06:53 +0000
committerDavid Hill <[email protected]>2026-02-17 15:16:07 +0000
commit57a5d5fd342b6451384d7549b00189b6891116bf (patch)
tree952a4c95cd3b4334feb75d4c83b22a9246630e20 /packages/ui/src
parent14684d8e75bfc9113657de6678dec7c03aeba7a1 (diff)
downloadopencode-57a5d5fd342b6451384d7549b00189b6891116bf.tar.gz
opencode-57a5d5fd342b6451384d7549b00189b6891116bf.zip
tweak(ui): show assistant response meta on hover
Adds hover-only metadata after the assistant copy icon showing agent, provider, model, and response duration.
Diffstat (limited to 'packages/ui/src')
-rw-r--r--packages/ui/src/components/message-part.css5
-rw-r--r--packages/ui/src/components/message-part.tsx51
2 files changed, 51 insertions, 5 deletions
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index cd15117a0..5b0a3cc17 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -168,6 +168,7 @@
display: flex;
align-items: center;
justify-content: flex-start;
+ gap: 10px;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
@@ -179,6 +180,10 @@
}
}
+ [data-slot="text-part-meta"] {
+ user-select: none;
+ }
+
[data-slot="text-part-copy-wrapper"][data-interrupted] {
width: 100%;
justify-content: flex-end;
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index bdd323b71..8e74b14ba 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -902,6 +902,47 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
() =>
props.message.role === "assistant" && (props.message as AssistantMessage).error?.name === "MessageAbortedError",
)
+
+ const provider = createMemo(() => {
+ if (props.message.role !== "assistant") return ""
+ const id = (props.message as AssistantMessage).providerID
+ const match = data.store.provider?.all?.find((p) => p.id === id)
+ return match?.name ?? id
+ })
+
+ const model = createMemo(() => {
+ if (props.message.role !== "assistant") return ""
+ const message = props.message as AssistantMessage
+ const match = data.store.provider?.all?.find((p) => p.id === message.providerID)
+ return match?.models?.[message.modelID]?.name ?? message.modelID
+ })
+
+ const duration = createMemo(() => {
+ if (props.message.role !== "assistant") return ""
+ const message = props.message as AssistantMessage
+ const completed = message.time.completed
+ if (typeof completed !== "number") return ""
+ const ms = completed - message.time.created
+ if (!(ms >= 0)) return ""
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`
+ const minutes = Math.floor(ms / 60_000)
+ const seconds = Math.round((ms - minutes * 60_000) / 1000)
+ return `${minutes}m ${seconds}s`
+ })
+
+ const meta = createMemo(() => {
+ if (props.message.role !== "assistant") return ""
+ const agent = (props.message as AssistantMessage).agent
+ const items = [
+ agent ? agent[0]?.toUpperCase() + agent.slice(1) : "",
+ provider(),
+ model(),
+ duration(),
+ interrupted() ? i18n.t("ui.message.interrupted") : "",
+ ]
+ return items.filter((x) => !!x).join(" \u00B7 ")
+ })
+
const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory)
const throttledText = createThrottledValue(displayText)
const isLastTextPart = createMemo(() => {
@@ -934,11 +975,6 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
</div>
<Show when={showCopy()}>
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
- <Show when={interrupted()}>
- <span data-slot="text-part-interrupted" class="text-13-regular text-text-weak cursor-default">
- {i18n.t("ui.message.interrupted")}
- </span>
- </Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
placement="top"
@@ -953,6 +989,11 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
/>
</Tooltip>
+ <Show when={meta()}>
+ <span data-slot="text-part-meta" class="text-12-regular text-text-weak cursor-default">
+ {meta()}
+ </span>
+ </Show>
</div>
</Show>
</div>