Skip to content

Commit c526caa

Browse files
authored
fix: show model display name in message footer and transcript (#20539)
1 parent b1c0748 commit c526caa

4 files changed

Lines changed: 170 additions & 13 deletions

File tree

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ import { Spinner } from "@tui/component/spinner"
2121
import { selectedForeground, useTheme } from "@tui/context/theme"
2222
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
2323
import { Prompt, type PromptRef } from "@tui/component/prompt"
24-
import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk/v2"
24+
import type {
25+
AssistantMessage,
26+
Part,
27+
Provider,
28+
ToolPart,
29+
UserMessage,
30+
TextPart,
31+
ReasoningPart,
32+
} from "@opencode-ai/sdk/v2"
2533
import { useLocal } from "@tui/context/local"
2634
import { Locale } from "@/util/locale"
2735
import type { Tool } from "@/tool/tool"
@@ -69,6 +77,7 @@ import { Global } from "@/global"
6977
import { PermissionPrompt } from "./permission"
7078
import { QuestionPrompt } from "./question"
7179
import { DialogExportOptions } from "../../ui/dialog-export-options"
80+
import * as Model from "../../util/model"
7281
import { formatTranscript } from "../../util/transcript"
7382
import { UI } from "@/cli/ui.ts"
7483
import { useTuiConfig } from "../../context/tui-config"
@@ -85,6 +94,7 @@ const context = createContext<{
8594
showDetails: () => boolean
8695
showGenericToolOutput: () => boolean
8796
diffWrapMode: () => "word" | "none"
97+
providers: () => ReadonlyMap<string, Provider>
8898
sync: ReturnType<typeof useSync>
8999
tui: ReturnType<typeof useTuiConfig>
90100
}>()
@@ -150,6 +160,7 @@ export function Session() {
150160
})
151161
const showTimestamps = createMemo(() => timestamps() === "show")
152162
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
163+
const providers = createMemo(() => Model.index(sync.data.provider))
153164

154165
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
155166

@@ -814,6 +825,7 @@ export function Session() {
814825
thinking: showThinking(),
815826
toolDetails: showDetails(),
816827
assistantMetadata: showAssistantMetadata(),
828+
providers: sync.data.provider,
817829
},
818830
)
819831
await Clipboard.copy(transcript)
@@ -858,6 +870,7 @@ export function Session() {
858870
thinking: options.thinking,
859871
toolDetails: options.toolDetails,
860872
assistantMetadata: options.assistantMetadata,
873+
providers: sync.data.provider,
861874
},
862875
)
863876

@@ -1003,6 +1016,7 @@ export function Session() {
10031016
showDetails,
10041017
showGenericToolOutput,
10051018
diffWrapMode,
1019+
providers,
10061020
sync,
10071021
tui: tuiConfig,
10081022
}}
@@ -1287,10 +1301,12 @@ function UserMessage(props: {
12871301
}
12881302

12891303
function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
1304+
const ctx = use()
12901305
const local = useLocal()
12911306
const { theme } = useTheme()
12921307
const sync = useSync()
12931308
const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])
1309+
const model = createMemo(() => Model.name(ctx.providers(), props.message.providerID, props.message.modelID))
12941310

12951311
const final = createMemo(() => {
12961312
return props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)
@@ -1360,7 +1376,7 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
13601376
{" "}
13611377
</span>{" "}
13621378
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.mode)}</span>
1363-
<span style={{ fg: theme.textMuted }}> · {props.message.modelID}</span>
1379+
<span style={{ fg: theme.textMuted }}> · {model()}</span>
13641380
<Show when={duration()}>
13651381
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
13661382
</Show>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Provider } from "@opencode-ai/sdk/v2"
2+
3+
export function index(list: Provider[] | undefined) {
4+
return new Map((list ?? []).map((item) => [item.id, item] as const))
5+
}
6+
7+
export function get(list: Provider[] | ReadonlyMap<string, Provider> | undefined, providerID: string, modelID: string) {
8+
const provider =
9+
list instanceof Map
10+
? list.get(providerID)
11+
: Array.isArray(list)
12+
? list.find((item) => item.id === providerID)
13+
: undefined
14+
return provider?.models[modelID]
15+
}
16+
17+
export function name(
18+
list: Provider[] | ReadonlyMap<string, Provider> | undefined,
19+
providerID: string,
20+
modelID: string,
21+
) {
22+
return get(list, providerID, modelID)?.name ?? modelID
23+
}

packages/opencode/src/cli/cmd/tui/util/transcript.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import type { AssistantMessage, Part, UserMessage } from "@opencode-ai/sdk/v2"
1+
import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2"
22
import { Locale } from "@/util/locale"
3+
import * as Model from "./model"
34

45
export type TranscriptOptions = {
56
thinking: boolean
67
toolDetails: boolean
78
assistantMetadata: boolean
9+
providers?: Provider[]
810
}
911

1012
export type SessionInfo = {
@@ -26,27 +28,33 @@ export function formatTranscript(
2628
messages: MessageWithParts[],
2729
options: TranscriptOptions,
2830
): string {
31+
const providers = Model.index(options.providers)
2932
let transcript = `# ${session.title}\n\n`
3033
transcript += `**Session ID:** ${session.id}\n`
3134
transcript += `**Created:** ${new Date(session.time.created).toLocaleString()}\n`
3235
transcript += `**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n`
3336
transcript += `---\n\n`
3437

3538
for (const msg of messages) {
36-
transcript += formatMessage(msg.info, msg.parts, options)
39+
transcript += formatMessage(msg.info, msg.parts, options, providers)
3740
transcript += `---\n\n`
3841
}
3942

4043
return transcript
4144
}
4245

43-
export function formatMessage(msg: UserMessage | AssistantMessage, parts: Part[], options: TranscriptOptions): string {
46+
export function formatMessage(
47+
msg: UserMessage | AssistantMessage,
48+
parts: Part[],
49+
options: TranscriptOptions,
50+
providers?: Provider[] | ReadonlyMap<string, Provider>,
51+
): string {
4452
let result = ""
4553

4654
if (msg.role === "user") {
4755
result += `## User\n\n`
4856
} else {
49-
result += formatAssistantHeader(msg, options.assistantMetadata)
57+
result += formatAssistantHeader(msg, options.assistantMetadata, providers ?? options.providers)
5058
}
5159

5260
for (const part of parts) {
@@ -56,15 +64,21 @@ export function formatMessage(msg: UserMessage | AssistantMessage, parts: Part[]
5664
return result
5765
}
5866

59-
export function formatAssistantHeader(msg: AssistantMessage, includeMetadata: boolean): string {
67+
export function formatAssistantHeader(
68+
msg: AssistantMessage,
69+
includeMetadata: boolean,
70+
providers?: Provider[] | ReadonlyMap<string, Provider>,
71+
): string {
6072
if (!includeMetadata) {
6173
return `## Assistant\n\n`
6274
}
6375

6476
const duration =
6577
msg.time.completed && msg.time.created ? ((msg.time.completed - msg.time.created) / 1000).toFixed(1) + "s" : ""
6678

67-
return `## Assistant (${Locale.titlecase(msg.agent)} · ${msg.modelID}${duration ? ` · ${duration}` : ""})\n\n`
79+
const modelName = Model.name(providers, msg.providerID, msg.modelID)
80+
81+
return `## Assistant (${Locale.titlecase(msg.agent)} · ${modelName}${duration ? ` · ${duration}` : ""})\n\n`
6882
}
6983

7084
export function formatPart(part: Part, options: TranscriptOptions): string {

packages/opencode/test/cli/tui/transcript.test.ts

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,66 @@ import {
55
formatPart,
66
formatTranscript,
77
} from "../../../src/cli/cmd/tui/util/transcript"
8-
import type { AssistantMessage, Part, UserMessage } from "@opencode-ai/sdk/v2"
8+
import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2"
9+
10+
const providers: Provider[] = [
11+
{
12+
id: "anthropic",
13+
name: "Anthropic",
14+
source: "api",
15+
env: [],
16+
options: {},
17+
models: {
18+
"claude-sonnet-4-20250514": {
19+
id: "claude-sonnet-4-20250514",
20+
providerID: "anthropic",
21+
api: {
22+
id: "claude-sonnet-4-20250514",
23+
url: "https://example.com/claude-sonnet-4-20250514",
24+
npm: "@ai-sdk/anthropic",
25+
},
26+
name: "Claude Sonnet 4",
27+
capabilities: {
28+
temperature: true,
29+
reasoning: true,
30+
attachment: true,
31+
toolcall: true,
32+
input: {
33+
text: true,
34+
audio: false,
35+
image: true,
36+
video: false,
37+
pdf: true,
38+
},
39+
output: {
40+
text: true,
41+
audio: false,
42+
image: false,
43+
video: false,
44+
pdf: false,
45+
},
46+
interleaved: false,
47+
},
48+
cost: {
49+
input: 0,
50+
output: 0,
51+
cache: {
52+
read: 0,
53+
write: 0,
54+
},
55+
},
56+
limit: {
57+
context: 200_000,
58+
output: 8_192,
59+
},
60+
status: "active",
61+
options: {},
62+
headers: {},
63+
release_date: "2025-05-14",
64+
},
65+
},
66+
},
67+
]
968

1069
describe("transcript", () => {
1170
describe("formatAssistantHeader", () => {
@@ -29,6 +88,11 @@ describe("transcript", () => {
2988
expect(result).toBe("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)\n\n")
3089
})
3190

91+
test("uses model display name when available", () => {
92+
const result = formatAssistantHeader(baseMsg, true, providers)
93+
expect(result).toBe("## Assistant (Build · Claude Sonnet 4 · 5.4s)\n\n")
94+
})
95+
3296
test("excludes metadata when disabled", () => {
3397
const result = formatAssistantHeader(baseMsg, false)
3498
expect(result).toBe("## Assistant\n\n")
@@ -196,7 +260,7 @@ describe("transcript", () => {
196260
})
197261

198262
describe("formatMessage", () => {
199-
const options = { thinking: true, toolDetails: true, assistantMetadata: true }
263+
const options = { thinking: true, toolDetails: true, assistantMetadata: true, providers }
200264

201265
test("formats user message", () => {
202266
const msg: UserMessage = {
@@ -230,7 +294,7 @@ describe("transcript", () => {
230294
}
231295
const parts: Part[] = [{ id: "p1", sessionID: "ses_123", messageID: "msg_123", type: "text", text: "Hi there" }]
232296
const result = formatMessage(msg, parts, options)
233-
expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 5.4s)")
297+
expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 5.4s)")
234298
expect(result).toContain("Hi there")
235299
})
236300
})
@@ -272,19 +336,59 @@ describe("transcript", () => {
272336
parts: [{ id: "p2", sessionID: "ses_abc123", messageID: "msg_2", type: "text" as const, text: "Hi!" }],
273337
},
274338
]
275-
const options = { thinking: false, toolDetails: false, assistantMetadata: true }
339+
const options = {
340+
thinking: false,
341+
toolDetails: false,
342+
assistantMetadata: true,
343+
providers,
344+
}
276345

277346
const result = formatTranscript(session, messages, options)
278347

279348
expect(result).toContain("# Test Session")
280349
expect(result).toContain("**Session ID:** ses_abc123")
281350
expect(result).toContain("## User")
282351
expect(result).toContain("Hello")
283-
expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
352+
expect(result).toContain("## Assistant (Build · Claude Sonnet 4 · 0.5s)")
284353
expect(result).toContain("Hi!")
285354
expect(result).toContain("---")
286355
})
287356

357+
test("falls back to raw model id when provider data is missing", () => {
358+
const session = {
359+
id: "ses_abc123",
360+
title: "Test Session",
361+
time: { created: 1000000000000, updated: 1000000001000 },
362+
}
363+
const messages = [
364+
{
365+
info: {
366+
id: "msg_1",
367+
sessionID: "ses_abc123",
368+
role: "assistant" as const,
369+
agent: "build",
370+
modelID: "claude-sonnet-4-20250514",
371+
providerID: "anthropic",
372+
mode: "",
373+
parentID: "msg_0",
374+
path: { cwd: "/test", root: "/test" },
375+
cost: 0.001,
376+
tokens: { input: 100, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
377+
time: { created: 1000000000100, completed: 1000000000600 },
378+
},
379+
parts: [{ id: "p1", sessionID: "ses_abc123", messageID: "msg_1", type: "text" as const, text: "Response" }],
380+
},
381+
]
382+
383+
const result = formatTranscript(session, messages, {
384+
thinking: false,
385+
toolDetails: false,
386+
assistantMetadata: true,
387+
})
388+
389+
expect(result).toContain("## Assistant (Build · claude-sonnet-4-20250514 · 0.5s)")
390+
})
391+
288392
test("formats transcript without assistant metadata", () => {
289393
const session = {
290394
id: "ses_abc123",

0 commit comments

Comments
 (0)