mirror of
https://github.com/sst/opencode.git
synced 2025-08-30 01:44:11 +00:00
feat: thinking blocks rendered in tui and share page
This commit is contained in:
parent
20e818ad05
commit
b8d2aebf09
17 changed files with 324 additions and 55 deletions
|
@ -87,7 +87,22 @@ export namespace ProviderTransform {
|
||||||
return {
|
return {
|
||||||
reasoningEffort: "minimal",
|
reasoningEffort: "minimal",
|
||||||
textVerbosity: "low",
|
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 },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1006,6 +1006,7 @@ export namespace Session {
|
||||||
async process(stream: StreamTextResult<Record<string, AITool>, never>) {
|
async process(stream: StreamTextResult<Record<string, AITool>, never>) {
|
||||||
try {
|
try {
|
||||||
let currentText: MessageV2.TextPart | undefined
|
let currentText: MessageV2.TextPart | undefined
|
||||||
|
let reasoningMap: Record<string, MessageV2.ReasoningPart> = {}
|
||||||
|
|
||||||
for await (const value of stream.fullStream) {
|
for await (const value of stream.fullStream) {
|
||||||
log.info("part", {
|
log.info("part", {
|
||||||
|
@ -1016,12 +1017,41 @@ export namespace Session {
|
||||||
break
|
break
|
||||||
|
|
||||||
case "reasoning-start":
|
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
|
break
|
||||||
|
|
||||||
case "reasoning-delta":
|
case "reasoning-delta":
|
||||||
|
if (value.id in reasoningMap) {
|
||||||
|
const part = reasoningMap[value.id]
|
||||||
|
part.text += value.text
|
||||||
|
if (part.text) await updatePart(part)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case "reasoning-end":
|
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
|
break
|
||||||
|
|
||||||
case "tool-input-start":
|
case "tool-input-start":
|
||||||
|
|
|
@ -118,6 +118,21 @@ export namespace MessageV2 {
|
||||||
})
|
})
|
||||||
export type TextPart = z.infer<typeof TextPart>
|
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({
|
export const ToolPart = PartBase.extend({
|
||||||
type: z.literal("tool"),
|
type: z.literal("tool"),
|
||||||
callID: z.string(),
|
callID: z.string(),
|
||||||
|
@ -229,6 +244,7 @@ export namespace MessageV2 {
|
||||||
export const Part = z
|
export const Part = z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
TextPart,
|
TextPart,
|
||||||
|
ReasoningPart,
|
||||||
FilePart,
|
FilePart,
|
||||||
ToolPart,
|
ToolPart,
|
||||||
StepStartPart,
|
StepStartPart,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
configured_endpoints: 34
|
configured_endpoints: 34
|
||||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-da1c340135c3dd6b1edb4e00e7039d2ac54d59271683a8b6ed528e51137ce41a.yml
|
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-b86cf7bb8df4f60ebe8b8f51e281c8076cfdccc8554178c1b78beca4b025f0ff.yml
|
||||||
openapi_spec_hash: 0cdd9b6273d72f5a6f484a2999ff0632
|
openapi_spec_hash: 47633b7481d91708643ea7b43fffffe6
|
||||||
config_hash: 7581d5948150d4ef7dd7b13d0845dbeb
|
config_hash: bd7f6435ed0c0005f373b5526c07a055
|
||||||
|
|
|
@ -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#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#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#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#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#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>
|
- <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>
|
||||||
|
|
|
@ -74,9 +74,10 @@ type Config struct {
|
||||||
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
|
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
|
||||||
// enables automatic sharing, 'disabled' disables all sharing
|
// enables automatic sharing, 'disabled' disables all sharing
|
||||||
Share ConfigShare `json:"share"`
|
Share ConfigShare `json:"share"`
|
||||||
// Small model to use for tasks like title generation in the
|
// Small model to use for tasks like title generation in the format of
|
||||||
// format of provider/model
|
// provider/model
|
||||||
SmallModel string `json:"small_model"`
|
SmallModel string `json:"small_model"`
|
||||||
|
Snapshot bool `json:"snapshot"`
|
||||||
// Theme name to use for the interface
|
// Theme name to use for the interface
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
// Custom username to display in conversations instead of system username
|
// Custom username to display in conversations instead of system username
|
||||||
|
@ -105,6 +106,7 @@ type configJSON struct {
|
||||||
Provider apijson.Field
|
Provider apijson.Field
|
||||||
Share apijson.Field
|
Share apijson.Field
|
||||||
SmallModel apijson.Field
|
SmallModel apijson.Field
|
||||||
|
Snapshot apijson.Field
|
||||||
Theme apijson.Field
|
Theme apijson.Field
|
||||||
Username apijson.Field
|
Username apijson.Field
|
||||||
raw string
|
raw string
|
||||||
|
@ -780,9 +782,10 @@ func (r ConfigModePlanMode) IsKnown() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigPermission struct {
|
type ConfigPermission struct {
|
||||||
Bash ConfigPermissionBashUnion `json:"bash"`
|
Bash ConfigPermissionBashUnion `json:"bash"`
|
||||||
Edit ConfigPermissionEdit `json:"edit"`
|
Edit ConfigPermissionEdit `json:"edit"`
|
||||||
JSON configPermissionJSON `json:"-"`
|
Webfetch ConfigPermissionWebfetch `json:"webfetch"`
|
||||||
|
JSON configPermissionJSON `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// configPermissionJSON contains the JSON metadata for the struct
|
// configPermissionJSON contains the JSON metadata for the struct
|
||||||
|
@ -790,6 +793,7 @@ type ConfigPermission struct {
|
||||||
type configPermissionJSON struct {
|
type configPermissionJSON struct {
|
||||||
Bash apijson.Field
|
Bash apijson.Field
|
||||||
Edit apijson.Field
|
Edit apijson.Field
|
||||||
|
Webfetch apijson.Field
|
||||||
raw string
|
raw string
|
||||||
ExtraFields map[string]apijson.Field
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
@ -876,6 +880,22 @@ func (r ConfigPermissionEdit) IsKnown() bool {
|
||||||
return false
|
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 {
|
type ConfigProvider struct {
|
||||||
Models map[string]ConfigProviderModel `json:"models,required"`
|
Models map[string]ConfigProviderModel `json:"models,required"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
|
@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}"
|
||||||
|
|
||||||
# Run prism mock on the given spec
|
# Run prism mock on the given spec
|
||||||
if [ "$1" == "--daemon" ]; then
|
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
|
# Wait for server to come online
|
||||||
echo -n "Waiting for server"
|
echo -n "Waiting for server"
|
||||||
|
@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then
|
||||||
|
|
||||||
echo
|
echo
|
||||||
else
|
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
|
fi
|
||||||
|
|
|
@ -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 "To run the server, pass in the path or url of your OpenAPI"
|
||||||
echo -e "spec to the prism command:"
|
echo -e "spec to the prism command:"
|
||||||
echo
|
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
|
echo
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
@ -962,18 +962,20 @@ type Part struct {
|
||||||
Cost float64 `json:"cost"`
|
Cost float64 `json:"cost"`
|
||||||
Filename string `json:"filename"`
|
Filename string `json:"filename"`
|
||||||
// This field can have the runtime type of [[]string].
|
// This field can have the runtime type of [[]string].
|
||||||
Files interface{} `json:"files"`
|
Files interface{} `json:"files"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
Mime string `json:"mime"`
|
Mime string `json:"mime"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Snapshot string `json:"snapshot"`
|
// 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].
|
// This field can have the runtime type of [FilePartSource], [AgentPartSource].
|
||||||
Source interface{} `json:"source"`
|
Source interface{} `json:"source"`
|
||||||
// This field can have the runtime type of [ToolPartState].
|
// This field can have the runtime type of [ToolPartState].
|
||||||
State interface{} `json:"state"`
|
State interface{} `json:"state"`
|
||||||
Synthetic bool `json:"synthetic"`
|
Synthetic bool `json:"synthetic"`
|
||||||
Text string `json:"text"`
|
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"`
|
Time interface{} `json:"time"`
|
||||||
// This field can have the runtime type of [StepFinishPartTokens].
|
// This field can have the runtime type of [StepFinishPartTokens].
|
||||||
Tokens interface{} `json:"tokens"`
|
Tokens interface{} `json:"tokens"`
|
||||||
|
@ -985,28 +987,29 @@ type Part struct {
|
||||||
|
|
||||||
// partJSON contains the JSON metadata for the struct [Part]
|
// partJSON contains the JSON metadata for the struct [Part]
|
||||||
type partJSON struct {
|
type partJSON struct {
|
||||||
ID apijson.Field
|
ID apijson.Field
|
||||||
MessageID apijson.Field
|
MessageID apijson.Field
|
||||||
SessionID apijson.Field
|
SessionID apijson.Field
|
||||||
Type apijson.Field
|
Type apijson.Field
|
||||||
CallID apijson.Field
|
CallID apijson.Field
|
||||||
Cost apijson.Field
|
Cost apijson.Field
|
||||||
Filename apijson.Field
|
Filename apijson.Field
|
||||||
Files apijson.Field
|
Files apijson.Field
|
||||||
Hash apijson.Field
|
Hash apijson.Field
|
||||||
Mime apijson.Field
|
Mime apijson.Field
|
||||||
Name apijson.Field
|
Name apijson.Field
|
||||||
Snapshot apijson.Field
|
ProviderMetadata apijson.Field
|
||||||
Source apijson.Field
|
Snapshot apijson.Field
|
||||||
State apijson.Field
|
Source apijson.Field
|
||||||
Synthetic apijson.Field
|
State apijson.Field
|
||||||
Text apijson.Field
|
Synthetic apijson.Field
|
||||||
Time apijson.Field
|
Text apijson.Field
|
||||||
Tokens apijson.Field
|
Time apijson.Field
|
||||||
Tool apijson.Field
|
Tokens apijson.Field
|
||||||
URL apijson.Field
|
Tool apijson.Field
|
||||||
raw string
|
URL apijson.Field
|
||||||
ExtraFields map[string]apijson.Field
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r partJSON) RawJSON() string {
|
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
|
// AsUnion returns a [PartUnion] interface which you can cast to the specific types
|
||||||
// for more type safety.
|
// for more type safety.
|
||||||
//
|
//
|
||||||
// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
|
// Possible runtime types of the union are [TextPart], [ReasoningPart], [FilePart],
|
||||||
// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], [AgentPart].
|
// [ToolPart], [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart],
|
||||||
|
// [AgentPart].
|
||||||
func (r Part) AsUnion() PartUnion {
|
func (r Part) AsUnion() PartUnion {
|
||||||
return r.union
|
return r.union
|
||||||
}
|
}
|
||||||
|
|
||||||
// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
|
// Union satisfied by [TextPart], [ReasoningPart], [FilePart], [ToolPart],
|
||||||
// [StepFinishPart], [SnapshotPart], [PartPatchPart] or [AgentPart].
|
// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart] or
|
||||||
|
// [AgentPart].
|
||||||
type PartUnion interface {
|
type PartUnion interface {
|
||||||
implementsPart()
|
implementsPart()
|
||||||
}
|
}
|
||||||
|
@ -1046,6 +1051,11 @@ func init() {
|
||||||
Type: reflect.TypeOf(TextPart{}),
|
Type: reflect.TypeOf(TextPart{}),
|
||||||
DiscriminatorValue: "text",
|
DiscriminatorValue: "text",
|
||||||
},
|
},
|
||||||
|
apijson.UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(ReasoningPart{}),
|
||||||
|
DiscriminatorValue: "reasoning",
|
||||||
|
},
|
||||||
apijson.UnionVariant{
|
apijson.UnionVariant{
|
||||||
TypeFilter: gjson.JSON,
|
TypeFilter: gjson.JSON,
|
||||||
Type: reflect.TypeOf(FilePart{}),
|
Type: reflect.TypeOf(FilePart{}),
|
||||||
|
@ -1134,6 +1144,7 @@ type PartType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PartTypeText PartType = "text"
|
PartTypeText PartType = "text"
|
||||||
|
PartTypeReasoning PartType = "reasoning"
|
||||||
PartTypeFile PartType = "file"
|
PartTypeFile PartType = "file"
|
||||||
PartTypeTool PartType = "tool"
|
PartTypeTool PartType = "tool"
|
||||||
PartTypeStepStart PartType = "step-start"
|
PartTypeStepStart PartType = "step-start"
|
||||||
|
@ -1145,12 +1156,83 @@ const (
|
||||||
|
|
||||||
func (r PartType) IsKnown() bool {
|
func (r PartType) IsKnown() bool {
|
||||||
switch r {
|
switch r {
|
||||||
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent:
|
case PartTypeText, PartTypeReasoning, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
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 {
|
type Session struct {
|
||||||
ID string `json:"id,required"`
|
ID string `json:"id,required"`
|
||||||
Time SessionTime `json:"time,required"`
|
Time SessionTime `json:"time,required"`
|
||||||
|
|
|
@ -101,6 +101,7 @@ resources:
|
||||||
toolPart: ToolPart
|
toolPart: ToolPart
|
||||||
agentPart: AgentPart
|
agentPart: AgentPart
|
||||||
agentPartInput: AgentPartInput
|
agentPartInput: AgentPartInput
|
||||||
|
reasoningPart: ReasoningPart
|
||||||
stepStartPart: StepStartPart
|
stepStartPart: StepStartPart
|
||||||
stepFinishPart: StepFinishPart
|
stepFinishPart: StepFinishPart
|
||||||
snapshotPart: SnapshotPart
|
snapshotPart: SnapshotPart
|
||||||
|
|
|
@ -208,6 +208,7 @@ func renderText(
|
||||||
showToolDetails bool,
|
showToolDetails bool,
|
||||||
width int,
|
width int,
|
||||||
extra string,
|
extra string,
|
||||||
|
isThinking bool,
|
||||||
fileParts []opencode.FilePart,
|
fileParts []opencode.FilePart,
|
||||||
agentParts []opencode.AgentPart,
|
agentParts []opencode.AgentPart,
|
||||||
toolCalls ...opencode.ToolPart,
|
toolCalls ...opencode.ToolPart,
|
||||||
|
@ -219,8 +220,15 @@ func renderText(
|
||||||
var content string
|
var content string
|
||||||
switch casted := message.(type) {
|
switch casted := message.(type) {
|
||||||
case opencode.AssistantMessage:
|
case opencode.AssistantMessage:
|
||||||
|
bg := t.Background()
|
||||||
|
if isThinking {
|
||||||
|
bg = t.BackgroundPanel()
|
||||||
|
}
|
||||||
ts = time.UnixMilli(int64(casted.Time.Created))
|
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:
|
case opencode.UserMessage:
|
||||||
ts = time.UnixMilli(int64(casted.Time.Created))
|
ts = time.UnixMilli(int64(casted.Time.Created))
|
||||||
base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor)
|
base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor)
|
||||||
|
@ -385,6 +393,16 @@ func renderText(
|
||||||
WithBorderColor(t.Secondary()),
|
WithBorderColor(t.Secondary()),
|
||||||
)
|
)
|
||||||
case opencode.AssistantMessage:
|
case opencode.AssistantMessage:
|
||||||
|
if isThinking {
|
||||||
|
return renderContentBlock(
|
||||||
|
app,
|
||||||
|
content,
|
||||||
|
width,
|
||||||
|
WithTextColor(t.Text()),
|
||||||
|
WithBackgroundColor(t.BackgroundPanel()),
|
||||||
|
WithBorderColor(t.BackgroundPanel()),
|
||||||
|
)
|
||||||
|
}
|
||||||
return renderContentBlock(
|
return renderContentBlock(
|
||||||
app,
|
app,
|
||||||
content,
|
content,
|
||||||
|
|
|
@ -369,6 +369,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||||
m.showToolDetails,
|
m.showToolDetails,
|
||||||
width,
|
width,
|
||||||
files,
|
files,
|
||||||
|
false,
|
||||||
fileParts,
|
fileParts,
|
||||||
agentParts,
|
agentParts,
|
||||||
)
|
)
|
||||||
|
@ -448,6 +449,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||||
m.showToolDetails,
|
m.showToolDetails,
|
||||||
width,
|
width,
|
||||||
"",
|
"",
|
||||||
|
false,
|
||||||
[]opencode.FilePart{},
|
[]opencode.FilePart{},
|
||||||
[]opencode.AgentPart{},
|
[]opencode.AgentPart{},
|
||||||
toolCallParts...,
|
toolCallParts...,
|
||||||
|
@ -469,6 +471,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||||
m.showToolDetails,
|
m.showToolDetails,
|
||||||
width,
|
width,
|
||||||
"",
|
"",
|
||||||
|
false,
|
||||||
[]opencode.FilePart{},
|
[]opencode.FilePart{},
|
||||||
[]opencode.AgentPart{},
|
[]opencode.AgentPart{},
|
||||||
toolCallParts...,
|
toolCallParts...,
|
||||||
|
@ -546,6 +549,35 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
||||||
lineCount += lipgloss.Height(content) + 1
|
lineCount += lipgloss.Height(content) + 1
|
||||||
blocks = append(blocks, content)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -423,6 +423,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch casted := p.(type) {
|
switch casted := p.(type) {
|
||||||
case opencode.TextPart:
|
case opencode.TextPart:
|
||||||
return casted.ID == msg.Properties.Part.ID
|
return casted.ID == msg.Properties.Part.ID
|
||||||
|
case opencode.ReasoningPart:
|
||||||
|
return casted.ID == msg.Properties.Part.ID
|
||||||
case opencode.FilePart:
|
case opencode.FilePart:
|
||||||
return casted.ID == msg.Properties.Part.ID
|
return casted.ID == msg.Properties.Part.ID
|
||||||
case opencode.ToolPart:
|
case opencode.ToolPart:
|
||||||
|
@ -461,6 +463,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch casted := p.(type) {
|
switch casted := p.(type) {
|
||||||
case opencode.TextPart:
|
case opencode.TextPart:
|
||||||
return casted.ID == msg.Properties.PartID
|
return casted.ID == msg.Properties.PartID
|
||||||
|
case opencode.ReasoningPart:
|
||||||
|
return casted.ID == msg.Properties.PartID
|
||||||
case opencode.FilePart:
|
case opencode.FilePart:
|
||||||
return casted.ID == msg.Properties.PartID
|
return casted.ID == msg.Properties.PartID
|
||||||
case opencode.ToolPart:
|
case opencode.ToolPart:
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default function Share(props: {
|
||||||
const [store, setStore] = createStore<{
|
const [store, setStore] = createStore<{
|
||||||
info?: Session.Info
|
info?: Session.Info
|
||||||
messages: Record<string, MessageWithParts>
|
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 messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
|
||||||
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
|
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -128,12 +128,10 @@ export default function Share(props: {
|
||||||
setStore("messages", messageID, reconcile(d.content))
|
setStore("messages", messageID, reconcile(d.content))
|
||||||
}
|
}
|
||||||
if (type === "part") {
|
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)
|
const index = arr.findIndex((x) => x.id === d.content.id)
|
||||||
if (index === -1)
|
if (index === -1) arr.push(d.content)
|
||||||
arr.push(d.content)
|
if (index > -1) arr[index] = d.content
|
||||||
if (index > -1)
|
|
||||||
arr[index] = d.content
|
|
||||||
return [...arr]
|
return [...arr]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -350,7 +348,7 @@ export default function Share(props: {
|
||||||
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
|
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -54,7 +54,10 @@ export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -63,6 +66,22 @@ export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
export function IconRobot(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconRobot(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,29 @@
|
||||||
max-width: var(--md-tool-width);
|
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"] {
|
[data-component="assistant-text"] {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
IconMagnifyingGlass,
|
IconMagnifyingGlass,
|
||||||
IconDocumentMagnifyingGlass,
|
IconDocumentMagnifyingGlass,
|
||||||
} from "../icons"
|
} 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 { ContentCode } from "./content-code"
|
||||||
import { ContentDiff } from "./content-diff"
|
import { ContentDiff } from "./content-diff"
|
||||||
import { ContentText } from "./content-text"
|
import { ContentText } from "./content-text"
|
||||||
|
@ -83,6 +83,9 @@ export function Part(props: PartProps) {
|
||||||
>
|
>
|
||||||
{(model) => <ProviderIcon model={model()} size={18} />}
|
{(model) => <ProviderIcon model={model()} size={18} />}
|
||||||
</Match>
|
</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"}>
|
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
|
||||||
<IconQueueList width={18} height={18} />
|
<IconQueueList width={18} height={18} />
|
||||||
</Match>
|
</Match>
|
||||||
|
@ -157,6 +160,13 @@ export function Part(props: PartProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</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" && (
|
{props.message.role === "user" && props.part.type === "file" && (
|
||||||
<div data-component="attachment">
|
<div data-component="attachment">
|
||||||
<div data-slot="copy">Attachment</div>
|
<div data-slot="copy">Attachment</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue