mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
wip: @<agent>
This commit is contained in:
parent
a89c3a83b1
commit
05746eaacc
16 changed files with 434 additions and 109 deletions
|
|
@ -382,6 +382,16 @@ export namespace Session {
|
|||
.openapi({
|
||||
ref: "FilePartInput",
|
||||
}),
|
||||
MessageV2.AgentPart.omit({
|
||||
messageID: true,
|
||||
sessionID: true,
|
||||
})
|
||||
.partial({
|
||||
id: true,
|
||||
})
|
||||
.openapi({
|
||||
ref: "AgentPartInput",
|
||||
}),
|
||||
]),
|
||||
),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -172,6 +172,21 @@ export namespace MessageV2 {
|
|||
})
|
||||
export type FilePart = z.infer<typeof FilePart>
|
||||
|
||||
export const AgentPart = PartBase.extend({
|
||||
type: z.literal("agent"),
|
||||
name: z.string(),
|
||||
source: z
|
||||
.object({
|
||||
value: z.string(),
|
||||
start: z.number().int(),
|
||||
end: z.number().int(),
|
||||
})
|
||||
.optional(),
|
||||
}).openapi({
|
||||
ref: "AgentPart",
|
||||
})
|
||||
export type AgentPart = z.infer<typeof AgentPart>
|
||||
|
||||
export const StepStartPart = PartBase.extend({
|
||||
type: z.literal("step-start"),
|
||||
}).openapi({
|
||||
|
|
@ -212,7 +227,16 @@ export namespace MessageV2 {
|
|||
export type User = z.infer<typeof User>
|
||||
|
||||
export const Part = z
|
||||
.discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart])
|
||||
.discriminatedUnion("type", [
|
||||
TextPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
SnapshotPart,
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
])
|
||||
.openapi({
|
||||
ref: "Part",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
configured_endpoints: 34
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-7d370d30ae7aba41bef18ec75399d84abbeea6ed1adb6ceff7221dfd27fe06dc.yml
|
||||
openapi_spec_hash: bb266bf0d43d6ac11b14838977c8c7a5
|
||||
config_hash: 92d12e018bb9e4f5a6f9ea9e277d403e
|
||||
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-7d3c87c714e166d1e388805f43e483e46ddf5ed05185bd092263ea0cd9b3e130.yml
|
||||
openapi_spec_hash: 333cd3fe1348a5124a904bc6224a3a8e
|
||||
config_hash: 7581d5948150d4ef7dd7b13d0845dbeb
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ Methods:
|
|||
|
||||
Params 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#AgentPartInputParam">AgentPartInputParam</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#FilePartInputParam">FilePartInputParam</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#FilePartSourceUnionParam">FilePartSourceUnionParam</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#FilePartSourceTextParam">FilePartSourceTextParam</a>
|
||||
|
|
@ -83,6 +84,7 @@ Params Types:
|
|||
|
||||
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#AgentPart">AgentPart</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#AssistantMessage">AssistantMessage</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#FilePart">FilePart</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#FilePartSource">FilePartSource</a>
|
||||
|
|
|
|||
|
|
@ -827,11 +827,12 @@ type ConfigPermissionBashString string
|
|||
const (
|
||||
ConfigPermissionBashStringAsk ConfigPermissionBashString = "ask"
|
||||
ConfigPermissionBashStringAllow ConfigPermissionBashString = "allow"
|
||||
ConfigPermissionBashStringDeny ConfigPermissionBashString = "deny"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionBashString) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow:
|
||||
case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow, ConfigPermissionBashStringDeny:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -848,11 +849,12 @@ type ConfigPermissionBashMapItem string
|
|||
const (
|
||||
ConfigPermissionBashMapAsk ConfigPermissionBashMapItem = "ask"
|
||||
ConfigPermissionBashMapAllow ConfigPermissionBashMapItem = "allow"
|
||||
ConfigPermissionBashMapDeny ConfigPermissionBashMapItem = "deny"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionBashMapItem) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow:
|
||||
case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow, ConfigPermissionBashMapDeny:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -863,11 +865,12 @@ type ConfigPermissionEdit string
|
|||
const (
|
||||
ConfigPermissionEditAsk ConfigPermissionEdit = "ask"
|
||||
ConfigPermissionEditAllow ConfigPermissionEdit = "allow"
|
||||
ConfigPermissionEditDeny ConfigPermissionEdit = "deny"
|
||||
)
|
||||
|
||||
func (r ConfigPermissionEdit) IsKnown() bool {
|
||||
switch r {
|
||||
case ConfigPermissionEditAsk, ConfigPermissionEditAllow:
|
||||
case ConfigPermissionEditAsk, ConfigPermissionEditAllow, ConfigPermissionEditDeny:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -133,11 +133,13 @@ func NewRequestConfig(ctx context.Context, method string, u string, body interfa
|
|||
// Fallback to json serialization if none of the serialization functions that we expect
|
||||
// to see is present.
|
||||
if body != nil && !hasSerializationFunc {
|
||||
content, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(true)
|
||||
if err := enc.Encode(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = bytes.NewBuffer(content)
|
||||
reader = buf
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, u, nil)
|
||||
|
|
|
|||
|
|
@ -190,6 +190,113 @@ func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.
|
|||
return
|
||||
}
|
||||
|
||||
type AgentPart struct {
|
||||
ID string `json:"id,required"`
|
||||
MessageID string `json:"messageID,required"`
|
||||
Name string `json:"name,required"`
|
||||
SessionID string `json:"sessionID,required"`
|
||||
Type AgentPartType `json:"type,required"`
|
||||
Source AgentPartSource `json:"source"`
|
||||
JSON agentPartJSON `json:"-"`
|
||||
}
|
||||
|
||||
// agentPartJSON contains the JSON metadata for the struct [AgentPart]
|
||||
type agentPartJSON struct {
|
||||
ID apijson.Field
|
||||
MessageID apijson.Field
|
||||
Name apijson.Field
|
||||
SessionID apijson.Field
|
||||
Type apijson.Field
|
||||
Source apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AgentPart) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r agentPartJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
func (r AgentPart) implementsPart() {}
|
||||
|
||||
type AgentPartType string
|
||||
|
||||
const (
|
||||
AgentPartTypeAgent AgentPartType = "agent"
|
||||
)
|
||||
|
||||
func (r AgentPartType) IsKnown() bool {
|
||||
switch r {
|
||||
case AgentPartTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AgentPartSource struct {
|
||||
End int64 `json:"end,required"`
|
||||
Start int64 `json:"start,required"`
|
||||
Value string `json:"value,required"`
|
||||
JSON agentPartSourceJSON `json:"-"`
|
||||
}
|
||||
|
||||
// agentPartSourceJSON contains the JSON metadata for the struct [AgentPartSource]
|
||||
type agentPartSourceJSON struct {
|
||||
End apijson.Field
|
||||
Start apijson.Field
|
||||
Value apijson.Field
|
||||
raw string
|
||||
ExtraFields map[string]apijson.Field
|
||||
}
|
||||
|
||||
func (r *AgentPartSource) UnmarshalJSON(data []byte) (err error) {
|
||||
return apijson.UnmarshalRoot(data, r)
|
||||
}
|
||||
|
||||
func (r agentPartSourceJSON) RawJSON() string {
|
||||
return r.raw
|
||||
}
|
||||
|
||||
type AgentPartInputParam struct {
|
||||
Name param.Field[string] `json:"name,required"`
|
||||
Type param.Field[AgentPartInputType] `json:"type,required"`
|
||||
ID param.Field[string] `json:"id"`
|
||||
Source param.Field[AgentPartInputSourceParam] `json:"source"`
|
||||
}
|
||||
|
||||
func (r AgentPartInputParam) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
func (r AgentPartInputParam) implementsSessionChatParamsPartUnion() {}
|
||||
|
||||
type AgentPartInputType string
|
||||
|
||||
const (
|
||||
AgentPartInputTypeAgent AgentPartInputType = "agent"
|
||||
)
|
||||
|
||||
func (r AgentPartInputType) IsKnown() bool {
|
||||
switch r {
|
||||
case AgentPartInputTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AgentPartInputSourceParam struct {
|
||||
End param.Field[int64] `json:"end,required"`
|
||||
Start param.Field[int64] `json:"start,required"`
|
||||
Value param.Field[string] `json:"value,required"`
|
||||
}
|
||||
|
||||
func (r AgentPartInputSourceParam) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
type AssistantMessage struct {
|
||||
ID string `json:"id,required"`
|
||||
Cost float64 `json:"cost,required"`
|
||||
|
|
@ -855,11 +962,13 @@ 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"`
|
||||
Snapshot string `json:"snapshot"`
|
||||
Source FilePartSource `json:"source"`
|
||||
Files interface{} `json:"files"`
|
||||
Hash string `json:"hash"`
|
||||
Mime string `json:"mime"`
|
||||
Name string `json:"name"`
|
||||
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"`
|
||||
|
|
@ -886,6 +995,7 @@ type partJSON struct {
|
|||
Files apijson.Field
|
||||
Hash apijson.Field
|
||||
Mime apijson.Field
|
||||
Name apijson.Field
|
||||
Snapshot apijson.Field
|
||||
Source apijson.Field
|
||||
State apijson.Field
|
||||
|
|
@ -916,13 +1026,13 @@ func (r *Part) UnmarshalJSON(data []byte) (err error) {
|
|||
// for more type safety.
|
||||
//
|
||||
// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
|
||||
// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart].
|
||||
// [StepStartPart], [StepFinishPart], [SnapshotPart], [PartPatchPart], [AgentPart].
|
||||
func (r Part) AsUnion() PartUnion {
|
||||
return r.union
|
||||
}
|
||||
|
||||
// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart],
|
||||
// [StepFinishPart], [SnapshotPart] or [PartPatchPart].
|
||||
// [StepFinishPart], [SnapshotPart], [PartPatchPart] or [AgentPart].
|
||||
type PartUnion interface {
|
||||
implementsPart()
|
||||
}
|
||||
|
|
@ -966,6 +1076,11 @@ func init() {
|
|||
Type: reflect.TypeOf(PartPatchPart{}),
|
||||
DiscriminatorValue: "patch",
|
||||
},
|
||||
apijson.UnionVariant{
|
||||
TypeFilter: gjson.JSON,
|
||||
Type: reflect.TypeOf(AgentPart{}),
|
||||
DiscriminatorValue: "agent",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1025,11 +1140,12 @@ const (
|
|||
PartTypeStepFinish PartType = "step-finish"
|
||||
PartTypeSnapshot PartType = "snapshot"
|
||||
PartTypePatch PartType = "patch"
|
||||
PartTypeAgent PartType = "agent"
|
||||
)
|
||||
|
||||
func (r PartType) IsKnown() bool {
|
||||
switch r {
|
||||
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch:
|
||||
case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish, PartTypeSnapshot, PartTypePatch, PartTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
@ -2095,7 +2211,8 @@ type SessionChatParamsPart struct {
|
|||
ID param.Field[string] `json:"id"`
|
||||
Filename param.Field[string] `json:"filename"`
|
||||
Mime param.Field[string] `json:"mime"`
|
||||
Source param.Field[FilePartSourceUnionParam] `json:"source"`
|
||||
Name param.Field[string] `json:"name"`
|
||||
Source param.Field[interface{}] `json:"source"`
|
||||
Synthetic param.Field[bool] `json:"synthetic"`
|
||||
Text param.Field[string] `json:"text"`
|
||||
Time param.Field[interface{}] `json:"time"`
|
||||
|
|
@ -2108,7 +2225,7 @@ func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
|
|||
|
||||
func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
|
||||
|
||||
// Satisfied by [TextPartInputParam], [FilePartInputParam],
|
||||
// Satisfied by [TextPartInputParam], [FilePartInputParam], [AgentPartInputParam],
|
||||
// [SessionChatParamsPart].
|
||||
type SessionChatParamsPartUnion interface {
|
||||
implementsSessionChatParamsPartUnion()
|
||||
|
|
@ -2117,13 +2234,14 @@ type SessionChatParamsPartUnion interface {
|
|||
type SessionChatParamsPartsType string
|
||||
|
||||
const (
|
||||
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
|
||||
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
|
||||
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
|
||||
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
|
||||
SessionChatParamsPartsTypeAgent SessionChatParamsPartsType = "agent"
|
||||
)
|
||||
|
||||
func (r SessionChatParamsPartsType) IsKnown() bool {
|
||||
switch r {
|
||||
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile:
|
||||
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeAgent:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ resources:
|
|||
fileSource: FileSource
|
||||
symbolSource: SymbolSource
|
||||
toolPart: ToolPart
|
||||
agentPart: AgentPart
|
||||
agentPartInput: AgentPartInput
|
||||
stepStartPart: StepStartPart
|
||||
stepFinishPart: StepFinishPart
|
||||
snapshotPart: SnapshotPart
|
||||
|
|
|
|||
|
|
@ -55,6 +55,22 @@ func (p Prompt) ToMessage(
|
|||
Text: text,
|
||||
}}
|
||||
for _, attachment := range p.Attachments {
|
||||
if attachment.Type == "agent" {
|
||||
source, _ := attachment.GetAgentSource()
|
||||
parts = append(parts, opencode.AgentPart{
|
||||
ID: id.Ascending(id.Part),
|
||||
MessageID: messageID,
|
||||
SessionID: sessionID,
|
||||
Name: source.Name,
|
||||
Source: opencode.AgentPartSource{
|
||||
Value: attachment.Display,
|
||||
Start: int64(attachment.StartIndex),
|
||||
End: int64(attachment.EndIndex),
|
||||
},
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
text := opencode.FilePartSourceText{
|
||||
Start: int64(attachment.StartIndex),
|
||||
End: int64(attachment.EndIndex),
|
||||
|
|
@ -122,6 +138,17 @@ func (m Message) ToPrompt() (*Prompt, error) {
|
|||
continue
|
||||
}
|
||||
text += p.Text + " "
|
||||
case opencode.AgentPart:
|
||||
attachments = append(attachments, &attachment.Attachment{
|
||||
ID: p.ID,
|
||||
Type: "agent",
|
||||
Display: p.Source.Value,
|
||||
StartIndex: int(p.Source.Start),
|
||||
EndIndex: int(p.Source.End),
|
||||
Source: &attachment.AgentSource{
|
||||
Name: p.Name,
|
||||
},
|
||||
})
|
||||
case opencode.FilePart:
|
||||
switch p.Source.Type {
|
||||
case "file":
|
||||
|
|
@ -236,68 +263,18 @@ func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
|
|||
Filename: opencode.F(p.Filename),
|
||||
Source: opencode.F(source),
|
||||
})
|
||||
case opencode.AgentPart:
|
||||
parts = append(parts, opencode.AgentPartInputParam{
|
||||
ID: opencode.F(p.ID),
|
||||
Type: opencode.F(opencode.AgentPartInputTypeAgent),
|
||||
Name: opencode.F(p.Name),
|
||||
Source: opencode.F(opencode.AgentPartInputSourceParam{
|
||||
Value: opencode.F(p.Source.Value),
|
||||
Start: opencode.F(p.Source.Start),
|
||||
End: opencode.F(p.Source.End),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func (p Prompt) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
|
||||
parts := []opencode.SessionChatParamsPartUnion{
|
||||
opencode.TextPartInputParam{
|
||||
Type: opencode.F(opencode.TextPartInputTypeText),
|
||||
Text: opencode.F(p.Text),
|
||||
},
|
||||
}
|
||||
for _, att := range p.Attachments {
|
||||
filePart := opencode.FilePartInputParam{
|
||||
Type: opencode.F(opencode.FilePartInputTypeFile),
|
||||
Mime: opencode.F(att.MediaType),
|
||||
URL: opencode.F(att.URL),
|
||||
Filename: opencode.F(att.Filename),
|
||||
}
|
||||
switch att.Type {
|
||||
case "file":
|
||||
if fs, ok := att.GetFileSource(); ok {
|
||||
filePart.Source = opencode.F(
|
||||
opencode.FilePartSourceUnionParam(opencode.FileSourceParam{
|
||||
Type: opencode.F(opencode.FileSourceTypeFile),
|
||||
Path: opencode.F(fs.Path),
|
||||
Text: opencode.F(opencode.FilePartSourceTextParam{
|
||||
Start: opencode.F(int64(att.StartIndex)),
|
||||
End: opencode.F(int64(att.EndIndex)),
|
||||
Value: opencode.F(att.Display),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
case "symbol":
|
||||
if ss, ok := att.GetSymbolSource(); ok {
|
||||
filePart.Source = opencode.F(
|
||||
opencode.FilePartSourceUnionParam(opencode.SymbolSourceParam{
|
||||
Type: opencode.F(opencode.SymbolSourceTypeSymbol),
|
||||
Path: opencode.F(ss.Path),
|
||||
Name: opencode.F(ss.Name),
|
||||
Kind: opencode.F(int64(ss.Kind)),
|
||||
Range: opencode.F(opencode.SymbolSourceRangeParam{
|
||||
Start: opencode.F(opencode.SymbolSourceRangeStartParam{
|
||||
Line: opencode.F(float64(ss.Range.Start.Line)),
|
||||
Character: opencode.F(float64(ss.Range.Start.Char)),
|
||||
}),
|
||||
End: opencode.F(opencode.SymbolSourceRangeEndParam{
|
||||
Line: opencode.F(float64(ss.Range.End.Line)),
|
||||
Character: opencode.F(float64(ss.Range.End.Char)),
|
||||
}),
|
||||
}),
|
||||
Text: opencode.F(opencode.FilePartSourceTextParam{
|
||||
Start: opencode.F(int64(att.StartIndex)),
|
||||
End: opencode.F(int64(att.EndIndex)),
|
||||
Value: opencode.F(att.Display),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
parts = append(parts, filePart)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ type SymbolRange struct {
|
|||
End Position `toml:"end"`
|
||||
}
|
||||
|
||||
type AgentSource struct {
|
||||
Name string `toml:"name"`
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Line int `toml:"line"`
|
||||
Char int `toml:"char"`
|
||||
|
|
@ -76,6 +80,15 @@ func (a *Attachment) GetSymbolSource() (*SymbolSource, bool) {
|
|||
return ss, ok
|
||||
}
|
||||
|
||||
// GetAgentSource returns the source as AgentSource if the attachment is an agent type
|
||||
func (a *Attachment) GetAgentSource() (*AgentSource, bool) {
|
||||
if a.Type != "agent" {
|
||||
return nil, false
|
||||
}
|
||||
as, ok := a.Source.(*AgentSource)
|
||||
return as, ok
|
||||
}
|
||||
|
||||
// FromMap creates a TextSource from a map[string]any
|
||||
func (ts *TextSource) FromMap(sourceMap map[string]any) {
|
||||
if value, ok := sourceMap["value"].(string); ok {
|
||||
|
|
@ -128,6 +141,13 @@ func (ss *SymbolSource) FromMap(sourceMap map[string]any) {
|
|||
}
|
||||
}
|
||||
|
||||
// FromMap creates an AgentSource from a map[string]any
|
||||
func (as *AgentSource) FromMap(sourceMap map[string]any) {
|
||||
if name, ok := sourceMap["name"].(string); ok {
|
||||
as.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreSourceType converts a map[string]any source back to the proper type
|
||||
func (a *Attachment) RestoreSourceType() {
|
||||
if a.Source == nil {
|
||||
|
|
@ -149,6 +169,10 @@ func (a *Attachment) RestoreSourceType() {
|
|||
ss := &SymbolSource{}
|
||||
ss.FromMap(sourceMap)
|
||||
a.Source = ss
|
||||
case "agent":
|
||||
as := &AgentSource{}
|
||||
as.FromMap(sourceMap)
|
||||
a.Source = as
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
packages/tui/internal/completions/agents.go
Normal file
70
packages/tui/internal/completions/agents.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package completions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type agentsContextGroup struct {
|
||||
app *app.App
|
||||
}
|
||||
|
||||
func (cg *agentsContextGroup) GetId() string {
|
||||
return "agents"
|
||||
}
|
||||
|
||||
func (cg *agentsContextGroup) GetEmptyMessage() string {
|
||||
return "no matching agents"
|
||||
}
|
||||
|
||||
func (cg *agentsContextGroup) GetChildEntries(
|
||||
query string,
|
||||
) ([]CompletionSuggestion, error) {
|
||||
items := make([]CompletionSuggestion, 0)
|
||||
|
||||
query = strings.TrimSpace(query)
|
||||
|
||||
agents, err := cg.app.Client.App.Agents(
|
||||
context.Background(),
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get agent list", "error", err)
|
||||
return items, err
|
||||
}
|
||||
if agents == nil {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
for _, agent := range *agents {
|
||||
if query != "" && !strings.Contains(strings.ToLower(agent.Name), strings.ToLower(query)) {
|
||||
continue
|
||||
}
|
||||
|
||||
displayFunc := func(s styles.Style) string {
|
||||
t := theme.CurrentTheme()
|
||||
muted := s.Foreground(t.TextMuted()).Render
|
||||
return s.Render(agent.Name) + muted(" (agent)")
|
||||
}
|
||||
|
||||
item := CompletionSuggestion{
|
||||
Display: displayFunc,
|
||||
Value: agent.Name,
|
||||
ProviderID: cg.GetId(),
|
||||
RawData: agent,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func NewAgentsContextGroup(app *app.App) CompletionProvider {
|
||||
return &agentsContextGroup{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
|
@ -288,6 +288,31 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
return m, nil
|
||||
case "agents":
|
||||
atIndex := m.textarea.LastRuneIndex('@')
|
||||
if atIndex == -1 {
|
||||
// Should not happen, but as a fallback, just insert.
|
||||
m.textarea.InsertString(msg.Item.Value + " ")
|
||||
return m, nil
|
||||
}
|
||||
|
||||
cursorCol := m.textarea.CursorColumn()
|
||||
m.textarea.ReplaceRange(atIndex, cursorCol, "")
|
||||
|
||||
name := msg.Item.Value
|
||||
attachment := &attachment.Attachment{
|
||||
ID: uuid.NewString(),
|
||||
Type: "agent",
|
||||
Display: "@" + name,
|
||||
Source: &attachment.AgentSource{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
|
||||
m.textarea.InsertAttachment(attachment)
|
||||
m.textarea.InsertString(" ")
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
slog.Debug("Unknown provider", "provider", msg.Item.ProviderID)
|
||||
return m, nil
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ func renderText(
|
|||
width int,
|
||||
extra string,
|
||||
fileParts []opencode.FilePart,
|
||||
agentParts []opencode.AgentPart,
|
||||
toolCalls ...opencode.ToolPart,
|
||||
) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
|
@ -229,9 +230,47 @@ func renderText(
|
|||
|
||||
// Apply highlighting to filenames and base style to rest of text BEFORE wrapping
|
||||
textLen := int64(len(text))
|
||||
|
||||
// Collect all parts to highlight (both file and agent parts)
|
||||
type highlightPart struct {
|
||||
start int64
|
||||
end int64
|
||||
color compat.AdaptiveColor
|
||||
}
|
||||
var highlights []highlightPart
|
||||
|
||||
// Add file parts with secondary color
|
||||
for _, filePart := range fileParts {
|
||||
highlight := base.Foreground(t.Secondary())
|
||||
start, end := filePart.Source.Text.Start, filePart.Source.Text.End
|
||||
highlights = append(highlights, highlightPart{
|
||||
start: filePart.Source.Text.Start,
|
||||
end: filePart.Source.Text.End,
|
||||
color: t.Secondary(),
|
||||
})
|
||||
}
|
||||
|
||||
// Add agent parts with secondary color (same as file parts)
|
||||
for _, agentPart := range agentParts {
|
||||
highlights = append(highlights, highlightPart{
|
||||
start: agentPart.Source.Start,
|
||||
end: agentPart.Source.End,
|
||||
color: t.Secondary(),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort highlights by start position
|
||||
slices.SortFunc(highlights, func(a, b highlightPart) int {
|
||||
if a.start < b.start {
|
||||
return -1
|
||||
}
|
||||
if a.start > b.start {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for _, part := range highlights {
|
||||
highlight := base.Foreground(part.color)
|
||||
start, end := part.start, part.end
|
||||
|
||||
if end > textLen {
|
||||
end = textLen
|
||||
|
|
|
|||
|
|
@ -300,12 +300,17 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||
}
|
||||
remainingParts := message.Parts[partIndex+1:]
|
||||
fileParts := make([]opencode.FilePart, 0)
|
||||
agentParts := make([]opencode.AgentPart, 0)
|
||||
for _, part := range remainingParts {
|
||||
switch part := part.(type) {
|
||||
case opencode.FilePart:
|
||||
if part.Source.Text.Start >= 0 && part.Source.Text.End >= part.Source.Text.Start {
|
||||
fileParts = append(fileParts, part)
|
||||
}
|
||||
case opencode.AgentPart:
|
||||
if part.Source.Start >= 0 && part.Source.End >= part.Source.Start {
|
||||
agentParts = append(agentParts, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
flexItems := []layout.FlexItem{}
|
||||
|
|
@ -355,6 +360,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||
width,
|
||||
files,
|
||||
fileParts,
|
||||
agentParts,
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
|
|
@ -433,6 +439,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||
width,
|
||||
"",
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
toolCallParts...,
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
|
|
@ -453,6 +460,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
|
|||
width,
|
||||
"",
|
||||
[]opencode.FilePart{},
|
||||
[]opencode.AgentPart{},
|
||||
toolCallParts...,
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
|
|
|
|||
|
|
@ -66,11 +66,16 @@ func (c *completionDialogComponent) Init() tea.Cmd {
|
|||
|
||||
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
allItems := make([]completions.CompletionSuggestion, 0)
|
||||
// Collect results from all providers and preserve provider order
|
||||
type providerItems struct {
|
||||
idx int
|
||||
items []completions.CompletionSuggestion
|
||||
}
|
||||
|
||||
itemsByProvider := make([]providerItems, 0, len(c.providers))
|
||||
providersWithResults := 0
|
||||
|
||||
// Collect results from all providers
|
||||
for _, provider := range c.providers {
|
||||
for idx, provider := range c.providers {
|
||||
items, err := provider.GetChildEntries(query)
|
||||
if err != nil {
|
||||
slog.Error(
|
||||
|
|
@ -84,33 +89,46 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
|
|||
}
|
||||
if len(items) > 0 {
|
||||
providersWithResults++
|
||||
allItems = append(allItems, items...)
|
||||
itemsByProvider = append(itemsByProvider, providerItems{idx: idx, items: items})
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a query, use fuzzy ranking to sort results
|
||||
if query != "" && providersWithResults > 1 {
|
||||
// If there's a query, fuzzy-rank within each provider, then concatenate by provider order
|
||||
if query != "" && providersWithResults > 0 {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
|
||||
// Create a slice of display values for fuzzy matching
|
||||
displayValues := make([]string, len(allItems))
|
||||
for i, item := range allItems {
|
||||
displayValues[i] = item.Display(baseStyle)
|
||||
|
||||
// Ensure stable provider order just in case
|
||||
sort.SliceStable(itemsByProvider, func(i, j int) bool { return itemsByProvider[i].idx < itemsByProvider[j].idx })
|
||||
|
||||
final := make([]completions.CompletionSuggestion, 0)
|
||||
for _, entry := range itemsByProvider {
|
||||
// Build display values for fuzzy matching within this provider
|
||||
displayValues := make([]string, len(entry.items))
|
||||
for i, item := range entry.items {
|
||||
displayValues[i] = item.Display(baseStyle)
|
||||
}
|
||||
|
||||
matches := fuzzy.RankFindFold(query, displayValues)
|
||||
sort.Sort(matches)
|
||||
|
||||
// Reorder items for this provider based on fuzzy ranking
|
||||
ranked := make([]completions.CompletionSuggestion, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
ranked = append(ranked, entry.items[m.OriginalIndex])
|
||||
}
|
||||
final = append(final, ranked...)
|
||||
}
|
||||
|
||||
matches := fuzzy.RankFindFold(query, displayValues)
|
||||
sort.Sort(matches)
|
||||
|
||||
// Reorder items based on fuzzy ranking
|
||||
rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
rankedItems = append(rankedItems, allItems[match.OriginalIndex])
|
||||
}
|
||||
|
||||
return rankedItems
|
||||
return final
|
||||
}
|
||||
|
||||
return allItems
|
||||
// No query or no results: just concatenate in provider order
|
||||
all := make([]completions.CompletionSuggestion, 0)
|
||||
for _, entry := range itemsByProvider {
|
||||
all = append(all, entry.items...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
}
|
||||
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ type Model struct {
|
|||
commandProvider completions.CompletionProvider
|
||||
fileProvider completions.CompletionProvider
|
||||
symbolsProvider completions.CompletionProvider
|
||||
agentsProvider completions.CompletionProvider
|
||||
showCompletionDialog bool
|
||||
leaderBinding *key.Binding
|
||||
toastManager *toast.ToastManager
|
||||
|
|
@ -211,8 +212,8 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Set both file and symbols providers for @ completion
|
||||
a.completions = dialog.NewCompletionDialogComponent("@", a.fileProvider, a.symbolsProvider)
|
||||
// Set file, symbols, and agents providers for @ completion
|
||||
a.completions = dialog.NewCompletionDialogComponent("@", a.agentsProvider, a.fileProvider, a.symbolsProvider)
|
||||
updated, cmd = a.completions.Update(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
|
@ -1220,6 +1221,7 @@ func NewModel(app *app.App) tea.Model {
|
|||
commandProvider := completions.NewCommandCompletionProvider(app)
|
||||
fileProvider := completions.NewFileContextGroup(app)
|
||||
symbolsProvider := completions.NewSymbolsContextGroup(app)
|
||||
agentsProvider := completions.NewAgentsContextGroup(app)
|
||||
|
||||
messages := chat.NewMessagesComponent(app)
|
||||
editor := chat.NewEditorComponent(app)
|
||||
|
|
@ -1240,6 +1242,7 @@ func NewModel(app *app.App) tea.Model {
|
|||
commandProvider: commandProvider,
|
||||
fileProvider: fileProvider,
|
||||
symbolsProvider: symbolsProvider,
|
||||
agentsProvider: agentsProvider,
|
||||
leaderBinding: leaderBinding,
|
||||
showCompletionDialog: false,
|
||||
toastManager: toast.NewToastManager(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue