summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/provider/transform.ts15
-rw-r--r--packages/opencode/src/session/index.ts30
-rw-r--r--packages/opencode/src/session/message-v2.ts16
-rw-r--r--packages/sdk/go/.stats.yml6
-rw-r--r--packages/sdk/go/api.md1
-rw-r--r--packages/sdk/go/config.go30
-rwxr-xr-xpackages/sdk/go/scripts/mock4
-rwxr-xr-xpackages/sdk/go/scripts/test2
-rw-r--r--packages/sdk/go/session.go148
-rw-r--r--packages/sdk/stainless/stainless.yml1
-rw-r--r--packages/tui/internal/components/chat/message.go20
-rw-r--r--packages/tui/internal/components/chat/messages.go32
-rw-r--r--packages/tui/internal/tui/tui.go4
-rw-r--r--packages/web/src/components/Share.tsx12
-rw-r--r--packages/web/src/components/icons/custom.tsx23
-rw-r--r--packages/web/src/components/share/part.module.css23
-rw-r--r--packages/web/src/components/share/part.tsx12
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<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":
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<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,
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:
- <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>
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/[email protected] -- prism mock "$URL" &> .prism.log &
+ npm exec --package=@stainless-api/[email protected] -- 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/[email protected] -- prism mock "$URL"
+ npm exec --package=@stainless-api/[email protected] -- 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/[email protected] -- 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()
}
@@ -1048,6 +1053,11 @@ func init() {
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ReasoningPart{}),
+ DiscriminatorValue: "reasoning",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
Type: reflect.TypeOf(FilePart{}),
DiscriminatorValue: "file",
},
@@ -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<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 (
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<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>
)
}
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) => <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>