feat: thinking blocks rendered in tui and share page

This commit is contained in:
adamdotdevin 2025-08-10 19:24:16 -05:00
parent 20e818ad05
commit b8d2aebf09
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
17 changed files with 324 additions and 55 deletions

View file

@ -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 },
// }
// }
}
}

View file

@ -1006,6 +1006,7 @@ export namespace Session {
async process(stream: StreamTextResult<Record<string, AITool>, never>) {
try {
let currentText: MessageV2.TextPart | undefined
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
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":

View file

@ -118,6 +118,21 @@ export namespace MessageV2 {
})
export type TextPart = z.infer<typeof TextPart>
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<typeof ReasoningPart>
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,

View file

@ -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

View file

@ -92,6 +92,7 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileSource">FileSource</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Part">Part</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ReasoningPart">ReasoningPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SnapshotPart">SnapshotPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPart">StepFinishPart</a>

View file

@ -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"`

View file

@ -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

View file

@ -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

View file

@ -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"`

View file

@ -101,6 +101,7 @@ resources:
toolPart: ToolPart
agentPart: AgentPart
agentPartInput: AgentPartInput
reasoningPart: ReasoningPart
stepStartPart: StepStartPart
stepFinishPart: StepFinishPart
snapshotPart: SnapshotPart

View file

@ -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,

View file

@ -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)
}
}
}

View file

@ -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:

View file

@ -61,7 +61,7 @@ export default function Share(props: {
const [store, setStore] = createStore<{
info?: Session.Info
messages: Record<string, MessageWithParts>
}>({ 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 (

View file

@ -54,7 +54,10 @@ export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M16.92 4.5c-1.851 0-3.298 1.394-4.608 3.165C10.512 5.373 9.007 4.5 7.206 4.5C3.534 4.5.72 9.28.72 14.338c0 3.165 1.531 5.162 4.096 5.162c1.846 0 3.174-.87 5.535-4.997c0 0 .984-1.737 1.66-2.934q.356.574.75 1.238l1.107 1.862c2.156 3.608 3.358 4.831 5.534 4.831c2.5 0 3.89-2.024 3.89-5.255c0-5.297-2.877-9.745-6.372-9.745m-8.37 8.886c-1.913 3-2.575 3.673-3.64 3.673c-1.097 0-1.749-.963-1.749-2.68c0-3.672 1.831-7.427 4.014-7.427c1.182 0 2.17.682 3.683 2.848c-1.437 2.204-2.307 3.586-2.307 3.586m7.224-.377L14.45 10.8a45 45 0 0 0-1.032-1.608c1.193-1.841 2.176-2.759 3.347-2.759c2.43 0 4.375 3.58 4.375 7.976c0 1.676-.549 2.649-1.686 2.649c-1.09 0-1.61-.72-3.68-4.05" />
<path
fill="currentColor"
d="M16.92 4.5c-1.851 0-3.298 1.394-4.608 3.165C10.512 5.373 9.007 4.5 7.206 4.5C3.534 4.5.72 9.28.72 14.338c0 3.165 1.531 5.162 4.096 5.162c1.846 0 3.174-.87 5.535-4.997c0 0 .984-1.737 1.66-2.934q.356.574.75 1.238l1.107 1.862c2.156 3.608 3.358 4.831 5.534 4.831c2.5 0 3.89-2.024 3.89-5.255c0-5.297-2.877-9.745-6.372-9.745m-8.37 8.886c-1.913 3-2.575 3.673-3.64 3.673c-1.097 0-1.749-.963-1.749-2.68c0-3.672 1.831-7.427 4.014-7.427c1.182 0 2.17.682 3.683 2.848c-1.437 2.204-2.307 3.586-2.307 3.586m7.224-.377L14.45 10.8a45 45 0 0 0-1.032-1.608c1.193-1.841 2.176-2.759 3.347-2.759c2.43 0 4.375 3.58 4.375 7.976c0 1.676-.549 2.649-1.686 2.649c-1.09 0-1.61-.72-3.68-4.05"
/>
</svg>
)
}
@ -63,6 +66,22 @@ export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconRobot(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2M6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1zm-4 3H0v6h2zm20 0h2v6h-2zM9 14.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6 0a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3" /></svg>
<path
fill="currentColor"
d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2M6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1zm-4 3H0v6h2zm20 0h2v6h-2zM9 14.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6 0a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"
/>
</svg>
)
}
// https://icones.js.org/collection/ri?s=brain&icon=ri:brain-2-line
export function IconBrain(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M7 6q.001.357.115.67a1 1 0 0 1-1 1.333L6 8a2 2 0 0 0-1.491 3.333a1 1 0 0 1 0 1.334a2 2 0 0 0 .864 3.233a1 1 0 0 1 .67 1.135a2.5 2.5 0 1 0 4.932.824q.009-.063.025-.123V6a2 2 0 1 0-4 0m6 11.736q.016.06.025.122a2.5 2.5 0 1 0 4.932-.823a1 1 0 0 1 .67-1.135a2 2 0 0 0 .864-3.233a1 1 0 0 1 0-1.334a2 2 0 0 0-1.607-3.33a1 1 0 0 1-.999-1.333q.113-.313.115-.67a2 2 0 1 0-4 0zM9 2a4 4 0 0 1 3 1.354a4 4 0 0 1 6.998 2.771A4.002 4.002 0 0 1 21.465 12A3.997 3.997 0 0 1 20 17.465v.035a4.5 4.5 0 0 1-8 2.828A4.5 4.5 0 0 1 4 17.5v-.035A3.997 3.997 0 0 1 2.535 12a4.002 4.002 0 0 1 2.467-5.874L5 6a4 4 0 0 1 4-4"
/>
</svg>
)
}

View file

@ -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;

View file

@ -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) => <ProviderIcon model={model()} size={18} />}
</Match>
<Match when={props.part.type === "reasoning" && props.message.role === "assistant"}>
<IconBrain width={18} height={18} />
</Match>
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
<IconQueueList width={18} height={18} />
</Match>
@ -157,6 +160,13 @@ export function Part(props: PartProps) {
)}
</div>
)}
{props.message.role === "assistant" && props.part.type === "reasoning" && (
<div data-component="assistant-reasoning">
<div data-component="assistant-reasoning-markdown">
<ContentMarkdown expand={props.last} text={props.part.text || "Thinking..."} />
</div>
</div>
)}
{props.message.role === "user" && props.part.type === "file" && (
<div data-component="attachment">
<div data-slot="copy">Attachment</div>