From b8d2aebf09313d451cec873b3d0807b818015b32 Mon Sep 17 00:00:00 2001 From: adamdotdevin <2363879+adamdottv@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:24:16 -0500 Subject: [PATCH] feat: thinking blocks rendered in tui and share page --- packages/opencode/src/provider/transform.ts | 15 ++ packages/opencode/src/session/index.ts | 30 ++++ packages/opencode/src/session/message-v2.ts | 16 ++ packages/sdk/go/.stats.yml | 6 +- packages/sdk/go/api.md | 1 + packages/sdk/go/config.go | 30 +++- packages/sdk/go/scripts/mock | 4 +- packages/sdk/go/scripts/test | 2 +- packages/sdk/go/session.go | 148 ++++++++++++++---- packages/sdk/stainless/stainless.yml | 1 + .../tui/internal/components/chat/message.go | 20 ++- .../tui/internal/components/chat/messages.go | 32 ++++ packages/tui/internal/tui/tui.go | 4 + packages/web/src/components/Share.tsx | 12 +- packages/web/src/components/icons/custom.tsx | 23 ++- .../web/src/components/share/part.module.css | 23 +++ packages/web/src/components/share/part.tsx | 12 +- 17 files changed, 324 insertions(+), 55 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 91a2777b8..933df3dd4 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -87,7 +87,22 @@ export namespace ProviderTransform { return { reasoningEffort: "minimal", textVerbosity: "low", + // reasoningSummary: "auto", + // include: ["reasoning.encrypted_content"], } } + // if (modelID.includes("claude")) { + // return { + // thinking: { + // type: "enabled", + // budgetTokens: 32000, + // }, + // } + // } + // if (_providerID === "bedrock") { + // return { + // reasoningConfig: { type: "enabled", budgetTokens: 32000 }, + // } + // } } } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 218f7ba88..583f3df7f 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -1006,6 +1006,7 @@ export namespace Session { async process(stream: StreamTextResult, never>) { try { let currentText: MessageV2.TextPart | undefined + let reasoningMap: Record = {} for await (const value of stream.fullStream) { log.info("part", { @@ -1016,12 +1017,41 @@ export namespace Session { break case "reasoning-start": + if (value.id in reasoningMap) { + continue + } + reasoningMap[value.id] = { + id: Identifier.ascending("part"), + messageID: assistantMsg.id, + sessionID: assistantMsg.sessionID, + type: "reasoning", + text: "", + time: { + start: Date.now(), + }, + } break case "reasoning-delta": + if (value.id in reasoningMap) { + const part = reasoningMap[value.id] + part.text += value.text + if (part.text) await updatePart(part) + } break case "reasoning-end": + if (value.id in reasoningMap) { + const part = reasoningMap[value.id] + part.text = part.text.trimEnd() + part.providerMetadata = value.providerMetadata + part.time = { + start: Date.now(), + end: Date.now(), + } + await updatePart(part) + delete reasoningMap[value.id] + } break case "tool-input-start": diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index f12526e79..a1daf9b9a 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -118,6 +118,21 @@ export namespace MessageV2 { }) export type TextPart = z.infer + export const ReasoningPart = PartBase.extend({ + type: z.literal("reasoning"), + text: z.string(), + providerMetadata: z.record(z.any()).optional(), + time: z + .object({ + start: z.number(), + end: z.number().optional(), + }) + .optional(), + }).openapi({ + ref: "ReasoningPart", + }) + export type ReasoningPart = z.infer + export const ToolPart = PartBase.extend({ type: z.literal("tool"), callID: z.string(), @@ -229,6 +244,7 @@ export namespace MessageV2 { export const Part = z .discriminatedUnion("type", [ TextPart, + ReasoningPart, FilePart, ToolPart, StepStartPart, diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index a2023f0e4..4db6fb4c0 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 34 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-da1c340135c3dd6b1edb4e00e7039d2ac54d59271683a8b6ed528e51137ce41a.yml -openapi_spec_hash: 0cdd9b6273d72f5a6f484a2999ff0632 -config_hash: 7581d5948150d4ef7dd7b13d0845dbeb +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-b86cf7bb8df4f60ebe8b8f51e281c8076cfdccc8554178c1b78beca4b025f0ff.yml +openapi_spec_hash: 47633b7481d91708643ea7b43fffffe6 +config_hash: bd7f6435ed0c0005f373b5526c07a055 diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md index d71951994..eeef10331 100644 --- a/packages/sdk/go/api.md +++ b/packages/sdk/go/api.md @@ -92,6 +92,7 @@ Response Types: - opencode.FileSource - opencode.Message - opencode.Part +- opencode.ReasoningPart - opencode.Session - opencode.SnapshotPart - opencode.StepFinishPart diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index a7b75a1bf..ed3e3c5ab 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -74,9 +74,10 @@ type Config struct { // Control sharing behavior:'manual' allows manual sharing via commands, 'auto' // enables automatic sharing, 'disabled' disables all sharing Share ConfigShare `json:"share"` - // Small model to use for tasks like title generation in the - // format of provider/model + // Small model to use for tasks like title generation in the format of + // provider/model SmallModel string `json:"small_model"` + Snapshot bool `json:"snapshot"` // Theme name to use for the interface Theme string `json:"theme"` // Custom username to display in conversations instead of system username @@ -105,6 +106,7 @@ type configJSON struct { Provider apijson.Field Share apijson.Field SmallModel apijson.Field + Snapshot apijson.Field Theme apijson.Field Username apijson.Field raw string @@ -780,9 +782,10 @@ func (r ConfigModePlanMode) IsKnown() bool { } type ConfigPermission struct { - Bash ConfigPermissionBashUnion `json:"bash"` - Edit ConfigPermissionEdit `json:"edit"` - JSON configPermissionJSON `json:"-"` + Bash ConfigPermissionBashUnion `json:"bash"` + Edit ConfigPermissionEdit `json:"edit"` + Webfetch ConfigPermissionWebfetch `json:"webfetch"` + JSON configPermissionJSON `json:"-"` } // configPermissionJSON contains the JSON metadata for the struct @@ -790,6 +793,7 @@ type ConfigPermission struct { type configPermissionJSON struct { Bash apijson.Field Edit apijson.Field + Webfetch apijson.Field raw string ExtraFields map[string]apijson.Field } @@ -876,6 +880,22 @@ func (r ConfigPermissionEdit) IsKnown() bool { return false } +type ConfigPermissionWebfetch string + +const ( + ConfigPermissionWebfetchAsk ConfigPermissionWebfetch = "ask" + ConfigPermissionWebfetchAllow ConfigPermissionWebfetch = "allow" + ConfigPermissionWebfetchDeny ConfigPermissionWebfetch = "deny" +) + +func (r ConfigPermissionWebfetch) IsKnown() bool { + switch r { + case ConfigPermissionWebfetchAsk, ConfigPermissionWebfetchAllow, ConfigPermissionWebfetchDeny: + return true + } + return false +} + type ConfigProvider struct { Models map[string]ConfigProviderModel `json:"models,required"` ID string `json:"id"` diff --git a/packages/sdk/go/scripts/mock b/packages/sdk/go/scripts/mock index d2814ae6a..0b28f6ea2 100755 --- a/packages/sdk/go/scripts/mock +++ b/packages/sdk/go/scripts/mock @@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}" # Run prism mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & # Wait for server to come online echo -n "Waiting for server" @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" fi diff --git a/packages/sdk/go/scripts/test b/packages/sdk/go/scripts/test index efebceaee..c26b12228 100755 --- a/packages/sdk/go/scripts/test +++ b/packages/sdk/go/scripts/test @@ -43,7 +43,7 @@ elif ! prism_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the prism command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" echo exit 1 diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index bae17cf2a..bb5cecf57 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -962,18 +962,20 @@ type Part struct { Cost float64 `json:"cost"` Filename string `json:"filename"` // This field can have the runtime type of [[]string]. - Files interface{} `json:"files"` - Hash string `json:"hash"` - Mime string `json:"mime"` - Name string `json:"name"` - Snapshot string `json:"snapshot"` + Files interface{} `json:"files"` + Hash string `json:"hash"` + Mime string `json:"mime"` + Name string `json:"name"` + // This field can have the runtime type of [map[string]interface{}]. + ProviderMetadata interface{} `json:"providerMetadata"` + Snapshot string `json:"snapshot"` // This field can have the runtime type of [FilePartSource], [AgentPartSource]. Source interface{} `json:"source"` // This field can have the runtime type of [ToolPartState]. State interface{} `json:"state"` Synthetic bool `json:"synthetic"` Text string `json:"text"` - // This field can have the runtime type of [TextPartTime]. + // This field can have the runtime type of [TextPartTime], [ReasoningPartTime]. Time interface{} `json:"time"` // This field can have the runtime type of [StepFinishPartTokens]. Tokens interface{} `json:"tokens"` @@ -985,28 +987,29 @@ type Part struct { // partJSON contains the JSON metadata for the struct [Part] type partJSON struct { - ID apijson.Field - MessageID apijson.Field - SessionID apijson.Field - Type apijson.Field - CallID apijson.Field - Cost apijson.Field - Filename apijson.Field - Files apijson.Field - Hash apijson.Field - Mime apijson.Field - Name apijson.Field - Snapshot apijson.Field - Source apijson.Field - State apijson.Field - Synthetic apijson.Field - Text apijson.Field - Time apijson.Field - Tokens apijson.Field - Tool apijson.Field - URL apijson.Field - raw string - ExtraFields map[string]apijson.Field + ID apijson.Field + MessageID apijson.Field + SessionID apijson.Field + Type apijson.Field + CallID apijson.Field + Cost apijson.Field + Filename apijson.Field + Files apijson.Field + Hash apijson.Field + Mime apijson.Field + Name apijson.Field + ProviderMetadata apijson.Field + Snapshot apijson.Field + Source apijson.Field + State apijson.Field + Synthetic apijson.Field + Text apijson.Field + Time apijson.Field + Tokens apijson.Field + Tool apijson.Field + URL apijson.Field + raw string + ExtraFields map[string]apijson.Field } func (r partJSON) RawJSON() string { @@ -1025,14 +1028,16 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) { // AsUnion returns a [PartUnion] interface which you can cast to the specific types // for more type safety. // -// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart], -// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], [AgentPart]. +// Possible runtime types of the union are [TextPart], [ReasoningPart], [FilePart], +// [ToolPart], [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], +// [AgentPart]. func (r Part) AsUnion() PartUnion { return r.union } -// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart], -// [StepFinishPart], [SnapshotPart], [PartPatchPart] or [AgentPart]. +// Union satisfied by [TextPart], [ReasoningPart], [FilePart], [ToolPart], +// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart] or +// [AgentPart]. type PartUnion interface { implementsPart() } @@ -1046,6 +1051,11 @@ func init() { Type: reflect.TypeOf(TextPart{}), DiscriminatorValue: "text", }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ReasoningPart{}), + DiscriminatorValue: "reasoning", + }, apijson.UnionVariant{ TypeFilter: gjson.JSON, Type: reflect.TypeOf(FilePart{}), @@ -1134,6 +1144,7 @@ type PartType string const ( PartTypeText PartType = "text" + PartTypeReasoning PartType = "reasoning" PartTypeFile PartType = "file" PartTypeTool PartType = "tool" PartTypeStepStart PartType = "step-start" @@ -1145,12 +1156,83 @@ const ( func (r PartType) IsKnown() bool { switch r { - case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent: + case PartTypeText, PartTypeReasoning, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent: return true } return false } +type ReasoningPart struct { + ID string `json:"id,required"` + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + Text string `json:"text,required"` + Type ReasoningPartType `json:"type,required"` + ProviderMetadata map[string]interface{} `json:"providerMetadata"` + Time ReasoningPartTime `json:"time"` + JSON reasoningPartJSON `json:"-"` +} + +// reasoningPartJSON contains the JSON metadata for the struct [ReasoningPart] +type reasoningPartJSON struct { + ID apijson.Field + MessageID apijson.Field + SessionID apijson.Field + Text apijson.Field + Type apijson.Field + ProviderMetadata apijson.Field + Time apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ReasoningPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r reasoningPartJSON) RawJSON() string { + return r.raw +} + +func (r ReasoningPart) implementsPart() {} + +type ReasoningPartType string + +const ( + ReasoningPartTypeReasoning ReasoningPartType = "reasoning" +) + +func (r ReasoningPartType) IsKnown() bool { + switch r { + case ReasoningPartTypeReasoning: + return true + } + return false +} + +type ReasoningPartTime struct { + Start float64 `json:"start,required"` + End float64 `json:"end"` + JSON reasoningPartTimeJSON `json:"-"` +} + +// reasoningPartTimeJSON contains the JSON metadata for the struct +// [ReasoningPartTime] +type reasoningPartTimeJSON struct { + Start apijson.Field + End apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ReasoningPartTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r reasoningPartTimeJSON) RawJSON() string { + return r.raw +} + type Session struct { ID string `json:"id,required"` Time SessionTime `json:"time,required"` diff --git a/packages/sdk/stainless/stainless.yml b/packages/sdk/stainless/stainless.yml index 3a8da4061..0c85a2cef 100644 --- a/packages/sdk/stainless/stainless.yml +++ b/packages/sdk/stainless/stainless.yml @@ -101,6 +101,7 @@ resources: toolPart: ToolPart agentPart: AgentPart agentPartInput: AgentPartInput + reasoningPart: ReasoningPart stepStartPart: StepStartPart stepFinishPart: StepFinishPart snapshotPart: SnapshotPart diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go index 5f4dc9612..66f8d728e 100644 --- a/packages/tui/internal/components/chat/message.go +++ b/packages/tui/internal/components/chat/message.go @@ -208,6 +208,7 @@ func renderText( showToolDetails bool, width int, extra string, + isThinking bool, fileParts []opencode.FilePart, agentParts []opencode.AgentPart, toolCalls ...opencode.ToolPart, @@ -219,8 +220,15 @@ func renderText( var content string switch casted := message.(type) { case opencode.AssistantMessage: + bg := t.Background() + if isThinking { + bg = t.BackgroundPanel() + } ts = time.UnixMilli(int64(casted.Time.Created)) - content = util.ToMarkdown(text, width, t.Background()) + content = util.ToMarkdown(text, width, bg) + if isThinking { + content = styles.NewStyle().Background(bg).Foreground(t.TextMuted()).Render("Thinking") + "\n\n" + content + } case opencode.UserMessage: ts = time.UnixMilli(int64(casted.Time.Created)) base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor) @@ -385,6 +393,16 @@ func renderText( WithBorderColor(t.Secondary()), ) case opencode.AssistantMessage: + if isThinking { + return renderContentBlock( + app, + content, + width, + WithTextColor(t.Text()), + WithBackgroundColor(t.BackgroundPanel()), + WithBorderColor(t.BackgroundPanel()), + ) + } return renderContentBlock( app, content, diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 22cb97fb5..ff279821f 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -369,6 +369,7 @@ func (m *messagesComponent) renderView() tea.Cmd { m.showToolDetails, width, files, + false, fileParts, agentParts, ) @@ -448,6 +449,7 @@ func (m *messagesComponent) renderView() tea.Cmd { m.showToolDetails, width, "", + false, []opencode.FilePart{}, []opencode.AgentPart{}, toolCallParts..., @@ -469,6 +471,7 @@ func (m *messagesComponent) renderView() tea.Cmd { m.showToolDetails, width, "", + false, []opencode.FilePart{}, []opencode.AgentPart{}, toolCallParts..., @@ -546,6 +549,35 @@ func (m *messagesComponent) renderView() tea.Cmd { lineCount += lipgloss.Height(content) + 1 blocks = append(blocks, content) } + case opencode.ReasoningPart: + if reverted { + continue + } + text := "..." + if part.Text != "" { + text = part.Text + } + content = renderText( + m.app, + message.Info, + text, + casted.ModelID, + m.showToolDetails, + width, + "", + true, + []opencode.FilePart{}, + []opencode.AgentPart{}, + ) + content = lipgloss.PlaceHorizontal( + m.width, + lipgloss.Center, + content, + styles.WhitespaceStyle(t.Background()), + ) + partCount++ + lineCount += lipgloss.Height(content) + 1 + blocks = append(blocks, content) } } } diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 5f178e15a..639d15d04 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -423,6 +423,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch casted := p.(type) { case opencode.TextPart: return casted.ID == msg.Properties.Part.ID + case opencode.ReasoningPart: + return casted.ID == msg.Properties.Part.ID case opencode.FilePart: return casted.ID == msg.Properties.Part.ID case opencode.ToolPart: @@ -461,6 +463,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch casted := p.(type) { case opencode.TextPart: return casted.ID == msg.Properties.PartID + case opencode.ReasoningPart: + return casted.ID == msg.Properties.PartID case opencode.FilePart: return casted.ID == msg.Properties.PartID case opencode.ToolPart: diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index 4a75f737a..47632492d 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -61,7 +61,7 @@ export default function Share(props: { const [store, setStore] = createStore<{ info?: Session.Info messages: Record - }>({ info: props.info, messages: mapValues(props.messages, (x: any) => "metadata" in x ? fromV1(x) : x) }) + }>({ info: props.info, messages: mapValues(props.messages, (x: any) => ("metadata" in x ? fromV1(x) : x)) }) const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id))) const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"]) createEffect(() => { @@ -128,12 +128,10 @@ export default function Share(props: { setStore("messages", messageID, reconcile(d.content)) } if (type === "part") { - setStore("messages", d.content.messageID, "parts", arr => { + setStore("messages", d.content.messageID, "parts", (arr) => { const index = arr.findIndex((x) => x.id === d.content.id) - if (index === -1) - arr.push(d.content) - if (index > -1) - arr[index] = d.content + if (index === -1) arr.push(d.content) + if (index > -1) arr[index] = d.content return [...arr] }) } @@ -350,7 +348,7 @@ export default function Share(props: { if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running")) return false return true - }) + }), ) return ( diff --git a/packages/web/src/components/icons/custom.tsx b/packages/web/src/components/icons/custom.tsx index ba06ddfb3..8023032e5 100644 --- a/packages/web/src/components/icons/custom.tsx +++ b/packages/web/src/components/icons/custom.tsx @@ -54,7 +54,10 @@ export function IconOpencode(props: JSX.SvgSVGAttributes) { export function IconMeta(props: JSX.SvgSVGAttributes) { return ( - + ) } @@ -63,6 +66,22 @@ export function IconMeta(props: JSX.SvgSVGAttributes) { export function IconRobot(props: JSX.SvgSVGAttributes) { return ( - + + + ) +} + +// https://icones.js.org/collection/ri?s=brain&icon=ri:brain-2-line +export function IconBrain(props: JSX.SvgSVGAttributes) { + return ( + + + ) } diff --git a/packages/web/src/components/share/part.module.css b/packages/web/src/components/share/part.module.css index ffae0c3b7..3dd321425 100644 --- a/packages/web/src/components/share/part.module.css +++ b/packages/web/src/components/share/part.module.css @@ -128,6 +128,29 @@ max-width: var(--md-tool-width); } + [data-component="assistant-reasoning"] { + min-width: 0; + display: flex; + flex-direction: column; + gap: 1rem; + flex-grow: 1; + max-width: var(--md-tool-width); + + & > [data-component="assistant-reasoning-markdown"] { + align-self: flex-start; + font-size: 0.875rem; + border: 1px solid var(--sl-color-blue-high); + padding: 0.5rem calc(0.5rem + 3px); + border-radius: 0.25rem; + position: relative; + + [data-component="copy-button"] { + top: 0.5rem; + right: calc(0.5rem - 1px); + } + } + } + [data-component="assistant-text"] { min-width: 0; display: flex; diff --git a/packages/web/src/components/share/part.tsx b/packages/web/src/components/share/part.tsx index 772a80dc6..30f927fc4 100644 --- a/packages/web/src/components/share/part.tsx +++ b/packages/web/src/components/share/part.tsx @@ -19,7 +19,7 @@ import { IconMagnifyingGlass, IconDocumentMagnifyingGlass, } from "../icons" -import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic } from "../icons/custom" +import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic, IconBrain } from "../icons/custom" import { ContentCode } from "./content-code" import { ContentDiff } from "./content-diff" import { ContentText } from "./content-text" @@ -83,6 +83,9 @@ export function Part(props: PartProps) { > {(model) => } + + + @@ -157,6 +160,13 @@ export function Part(props: PartProps) { )} )} + {props.message.role === "assistant" && props.part.type === "reasoning" && ( +
+
+ +
+
+ )} {props.message.role === "user" && props.part.type === "file" && (
Attachment