Dax 7 месяцев назад
Родитель
Сommit
d56dec4ba7

+ 0 - 2
packages/opencode/src/cli/cmd/run.ts

@@ -172,8 +172,6 @@ export const RunCommand = cmd({
         parts: [
           {
             id: Identifier.ascending("part"),
-            sessionID: session.id,
-            messageID: messageID,
             type: "text",
             text: message,
           },

+ 1 - 10
packages/opencode/src/server/server.ts

@@ -451,16 +451,7 @@ export namespace Server {
             id: z.string().openapi({ description: "Session ID" }),
           }),
         ),
-        zValidator(
-          "json",
-          z.object({
-            messageID: z.string(),
-            providerID: z.string(),
-            modelID: z.string(),
-            mode: z.string(),
-            parts: z.union([MessageV2.FilePart, MessageV2.TextPart]).array(),
-          }),
-        ),
+        zValidator("json", Session.ChatInput.omit({ sessionID: true })),
         async (c) => {
           const sessionID = c.req.valid("param").id
           const body = c.req.valid("json")

+ 42 - 12
packages/opencode/src/session/index.ts

@@ -319,14 +319,39 @@ export namespace Session {
     return part
   }
 
-  export async function chat(input: {
-    sessionID: string
-    messageID: string
-    providerID: string
-    modelID: string
-    mode?: string
-    parts: (MessageV2.TextPart | MessageV2.FilePart)[]
-  }) {
+  export const ChatInput = z.object({
+    sessionID: Identifier.schema("session"),
+    messageID: Identifier.schema("message").optional(),
+    providerID: z.string(),
+    modelID: z.string(),
+    mode: z.string().optional(),
+    parts: z.array(
+      z.discriminatedUnion("type", [
+        MessageV2.TextPart.omit({
+          messageID: true,
+          sessionID: true,
+        })
+          .partial({
+            id: true,
+          })
+          .openapi({
+            ref: "TextPartInput",
+          }),
+        MessageV2.FilePart.omit({
+          messageID: true,
+          sessionID: true,
+        })
+          .partial({
+            id: true,
+          })
+          .openapi({
+            ref: "FilePartInput",
+          }),
+      ]),
+    ),
+  })
+
+  export async function chat(input: z.infer<typeof ChatInput>) {
     const l = log.clone().tag("session", input.sessionID)
     l.info("chatting")
 
@@ -384,7 +409,7 @@ export namespace Session {
     if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
 
     const userMsg: MessageV2.Info = {
-      id: input.messageID,
+      id: input.messageID ?? Identifier.ascending("message"),
       role: "user",
       sessionID: input.sessionID,
       time: {
@@ -490,7 +515,14 @@ export namespace Session {
               ]
           }
         }
-        return [part]
+        return [
+          {
+            id: Identifier.ascending("part"),
+            ...part,
+            messageID: userMsg.id,
+            sessionID: input.sessionID,
+          },
+        ]
       }),
     ).then((x) => x.flat())
 
@@ -1104,8 +1136,6 @@ export namespace Session {
       parts: [
         {
           id: Identifier.ascending("part"),
-          sessionID: input.sessionID,
-          messageID: input.messageID,
           type: "text",
           text: PROMPT_INITIALIZE.replace("${path}", app.path.root),
         },

+ 0 - 2
packages/opencode/src/tool/task.ts

@@ -44,8 +44,6 @@ export const TaskTool = Tool.define({
       parts: [
         {
           id: Identifier.ascending("part"),
-          messageID,
-          sessionID: session.id,
           type: "text",
           text: params.prompt,
         },

+ 12 - 16
packages/tui/internal/app/app.go

@@ -63,7 +63,7 @@ type SessionClearedMsg struct{}
 type CompactSessionMsg struct{}
 type SendMsg struct {
 	Text        string
-	Attachments []opencode.FilePartParam
+	Attachments []opencode.FilePartInputParam
 }
 type SetEditorContentMsg struct {
 	Text string
@@ -462,7 +462,7 @@ func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
 func (a *App) SendChatMessage(
 	ctx context.Context,
 	text string,
-	attachments []opencode.FilePartParam,
+	attachments []opencode.FilePartInputParam,
 ) (*App, tea.Cmd) {
 	var cmds []tea.Cmd
 	if a.Session.ID == "" {
@@ -511,22 +511,18 @@ func (a *App) SendChatMessage(
 		for _, part := range parts {
 			switch casted := part.(type) {
 			case opencode.TextPart:
-				partsParam = append(partsParam, opencode.TextPartParam{
-					ID:        opencode.F(casted.ID),
-					MessageID: opencode.F(casted.MessageID),
-					SessionID: opencode.F(casted.SessionID),
-					Type:      opencode.F(casted.Type),
-					Text:      opencode.F(casted.Text),
+				partsParam = append(partsParam, opencode.TextPartInputParam{
+					ID:   opencode.F(casted.ID),
+					Type: opencode.F(opencode.TextPartInputType(casted.Type)),
+					Text: opencode.F(casted.Text),
 				})
 			case opencode.FilePart:
-				partsParam = append(partsParam, opencode.FilePartParam{
-					ID:        opencode.F(casted.ID),
-					Mime:      opencode.F(casted.Mime),
-					MessageID: opencode.F(casted.MessageID),
-					SessionID: opencode.F(casted.SessionID),
-					Type:      opencode.F(casted.Type),
-					URL:       opencode.F(casted.URL),
-					Filename:  opencode.F(casted.Filename),
+				partsParam = append(partsParam, opencode.FilePartInputParam{
+					ID:       opencode.F(casted.ID),
+					Mime:     opencode.F(casted.Mime),
+					Type:     opencode.F(opencode.FilePartInputType(casted.Type)),
+					URL:      opencode.F(casted.URL),
+					Filename: opencode.F(casted.Filename),
 				})
 			}
 		}

+ 3 - 3
packages/tui/internal/components/chat/editor.go

@@ -306,10 +306,10 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 
 	attachments := m.textarea.GetAttachments()
-	fileParts := make([]opencode.FilePartParam, 0)
+	fileParts := make([]opencode.FilePartInputParam, 0)
 	for _, attachment := range attachments {
-		fileParts = append(fileParts, opencode.FilePartParam{
-			Type:     opencode.F(opencode.FilePartTypeFile),
+		fileParts = append(fileParts, opencode.FilePartInputParam{
+			Type:     opencode.F(opencode.FilePartInputTypeFile),
 			Mime:     opencode.F(attachment.MediaType),
 			URL:      opencode.F(attachment.URL),
 			Filename: opencode.F(attachment.Filename),

+ 3 - 3
packages/tui/sdk/.stats.yml

@@ -1,4 +1,4 @@
 configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-8792f91dd070f7b4ee671fc86e8a03976dc7fb6ee49f8c99ad989e1597003774.yml
-openapi_spec_hash: fe9dc3a074be560de0b97df9b5af2c1b
-config_hash: b7f3d9742335715c458494988498b183
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d34620b462127c45497743c97fd3569f9e629d9fbd97c0707087eeddbd4b3de1.yml
+openapi_spec_hash: 23864c98d555350fe56f1d0e56f205c5
+config_hash: a8441af7cb2db855d79fd372ee3b9fb1

+ 2 - 2
packages/tui/sdk/api.md

@@ -77,8 +77,8 @@ 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#FilePartParam">FilePartParam</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#TextPartParam">TextPartParam</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#TextPartInputParam">TextPartInputParam</a>
 
 Response Types:
 

+ 15 - 8
packages/tui/sdk/config.go

@@ -67,8 +67,8 @@ type Config struct {
 	Model string `json:"model"`
 	// Custom provider configurations and model overrides
 	Provider map[string]ConfigProvider `json:"provider"`
-	// Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables
-	// all sharing
+	// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
+	// enables automatic sharing, 'disabled' disables all sharing
 	Share ConfigShare `json:"share"`
 	// Theme name to use for the interface
 	Theme string `json:"theme"`
@@ -206,6 +206,8 @@ type ConfigMcp struct {
 	Enabled bool `json:"enabled"`
 	// This field can have the runtime type of [map[string]string].
 	Environment interface{} `json:"environment"`
+	// This field can have the runtime type of [map[string]string].
+	Headers interface{} `json:"headers"`
 	// URL of the remote MCP server
 	URL   string        `json:"url"`
 	JSON  configMcpJSON `json:"-"`
@@ -218,6 +220,7 @@ type configMcpJSON struct {
 	Command     apijson.Field
 	Enabled     apijson.Field
 	Environment apijson.Field
+	Headers     apijson.Field
 	URL         apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
@@ -427,18 +430,19 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
 	return r.raw
 }
 
-// Control sharing behavior: 'auto' enables automatic sharing, 'disabled' disables
-// all sharing
+// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
+// enables automatic sharing, 'disabled' disables all sharing
 type ConfigShare string
 
 const (
+	ConfigShareManual   ConfigShare = "manual"
 	ConfigShareAuto     ConfigShare = "auto"
 	ConfigShareDisabled ConfigShare = "disabled"
 )
 
 func (r ConfigShare) IsKnown() bool {
 	switch r {
-	case ConfigShareAuto, ConfigShareDisabled:
+	case ConfigShareManual, ConfigShareAuto, ConfigShareDisabled:
 		return true
 	}
 	return false
@@ -509,9 +513,9 @@ type KeybindsConfig struct {
 	SessionShare string `json:"session_share,required"`
 	// Unshare current session
 	SessionUnshare string `json:"session_unshare,required"`
-	// Switch mode
+	// Next mode
 	SwitchMode string `json:"switch_mode,required"`
-	// Switch mode reverse
+	// Previous Mode
 	SwitchModeReverse string `json:"switch_mode_reverse,required"`
 	// List available themes
 	ThemeList string `json:"theme_list,required"`
@@ -638,7 +642,9 @@ type McpRemoteConfig struct {
 	// URL of the remote MCP server
 	URL string `json:"url,required"`
 	// Enable or disable the MCP server on startup
-	Enabled bool                `json:"enabled"`
+	Enabled bool `json:"enabled"`
+	// Headers to send with the request
+	Headers map[string]string   `json:"headers"`
 	JSON    mcpRemoteConfigJSON `json:"-"`
 }
 
@@ -647,6 +653,7 @@ type mcpRemoteConfigJSON struct {
 	Type        apijson.Field
 	URL         apijson.Field
 	Enabled     apijson.Field
+	Headers     apijson.Field
 	raw         string
 	ExtraFields map[string]apijson.Field
 }

+ 53 - 30
packages/tui/sdk/session.go

@@ -481,21 +481,33 @@ func (r FilePartType) IsKnown() bool {
 	return false
 }
 
-type FilePartParam struct {
-	ID        param.Field[string]       `json:"id,required"`
-	MessageID param.Field[string]       `json:"messageID,required"`
-	Mime      param.Field[string]       `json:"mime,required"`
-	SessionID param.Field[string]       `json:"sessionID,required"`
-	Type      param.Field[FilePartType] `json:"type,required"`
-	URL       param.Field[string]       `json:"url,required"`
-	Filename  param.Field[string]       `json:"filename"`
+type FilePartInputParam struct {
+	Mime     param.Field[string]            `json:"mime,required"`
+	Type     param.Field[FilePartInputType] `json:"type,required"`
+	URL      param.Field[string]            `json:"url,required"`
+	ID       param.Field[string]            `json:"id"`
+	Filename param.Field[string]            `json:"filename"`
 }
 
-func (r FilePartParam) MarshalJSON() (data []byte, err error) {
+func (r FilePartInputParam) MarshalJSON() (data []byte, err error) {
 	return apijson.MarshalRoot(r)
 }
 
-func (r FilePartParam) implementsSessionChatParamsPartUnion() {}
+func (r FilePartInputParam) implementsSessionChatParamsPartUnion() {}
+
+type FilePartInputType string
+
+const (
+	FilePartInputTypeFile FilePartInputType = "file"
+)
+
+func (r FilePartInputType) IsKnown() bool {
+	switch r {
+	case FilePartInputTypeFile:
+		return true
+	}
+	return false
+}
 
 type Message struct {
 	ID        string      `json:"id,required"`
@@ -1076,28 +1088,40 @@ func (r textPartTimeJSON) RawJSON() string {
 	return r.raw
 }
 
-type TextPartParam struct {
-	ID        param.Field[string]            `json:"id,required"`
-	MessageID param.Field[string]            `json:"messageID,required"`
-	SessionID param.Field[string]            `json:"sessionID,required"`
-	Text      param.Field[string]            `json:"text,required"`
-	Type      param.Field[TextPartType]      `json:"type,required"`
-	Synthetic param.Field[bool]              `json:"synthetic"`
-	Time      param.Field[TextPartTimeParam] `json:"time"`
+type TextPartInputParam struct {
+	Text      param.Field[string]                 `json:"text,required"`
+	Type      param.Field[TextPartInputType]      `json:"type,required"`
+	ID        param.Field[string]                 `json:"id"`
+	Synthetic param.Field[bool]                   `json:"synthetic"`
+	Time      param.Field[TextPartInputTimeParam] `json:"time"`
 }
 
-func (r TextPartParam) MarshalJSON() (data []byte, err error) {
+func (r TextPartInputParam) MarshalJSON() (data []byte, err error) {
 	return apijson.MarshalRoot(r)
 }
 
-func (r TextPartParam) implementsSessionChatParamsPartUnion() {}
+func (r TextPartInputParam) implementsSessionChatParamsPartUnion() {}
+
+type TextPartInputType string
 
-type TextPartTimeParam struct {
+const (
+	TextPartInputTypeText TextPartInputType = "text"
+)
+
+func (r TextPartInputType) IsKnown() bool {
+	switch r {
+	case TextPartInputTypeText:
+		return true
+	}
+	return false
+}
+
+type TextPartInputTimeParam struct {
 	Start param.Field[float64] `json:"start,required"`
 	End   param.Field[float64] `json:"end"`
 }
 
-func (r TextPartTimeParam) MarshalJSON() (data []byte, err error) {
+func (r TextPartInputTimeParam) MarshalJSON() (data []byte, err error) {
 	return apijson.MarshalRoot(r)
 }
 
@@ -1574,11 +1598,11 @@ func (r sessionMessagesResponseJSON) RawJSON() string {
 }
 
 type SessionChatParams struct {
-	MessageID  param.Field[string]                       `json:"messageID,required"`
-	Mode       param.Field[string]                       `json:"mode,required"`
 	ModelID    param.Field[string]                       `json:"modelID,required"`
 	Parts      param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
 	ProviderID param.Field[string]                       `json:"providerID,required"`
+	MessageID  param.Field[string]                       `json:"messageID"`
+	Mode       param.Field[string]                       `json:"mode"`
 }
 
 func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
@@ -1586,10 +1610,8 @@ func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
 }
 
 type SessionChatParamsPart struct {
-	ID        param.Field[string]                     `json:"id,required"`
-	MessageID param.Field[string]                     `json:"messageID,required"`
-	SessionID param.Field[string]                     `json:"sessionID,required"`
 	Type      param.Field[SessionChatParamsPartsType] `json:"type,required"`
+	ID        param.Field[string]                     `json:"id"`
 	Filename  param.Field[string]                     `json:"filename"`
 	Mime      param.Field[string]                     `json:"mime"`
 	Synthetic param.Field[bool]                       `json:"synthetic"`
@@ -1604,7 +1626,8 @@ func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
 
 func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
 
-// Satisfied by [FilePartParam], [TextPartParam], [SessionChatParamsPart].
+// Satisfied by [TextPartInputParam], [FilePartInputParam],
+// [SessionChatParamsPart].
 type SessionChatParamsPartUnion interface {
 	implementsSessionChatParamsPartUnion()
 }
@@ -1612,13 +1635,13 @@ type SessionChatParamsPartUnion interface {
 type SessionChatParamsPartsType string
 
 const (
-	SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
 	SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
+	SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
 )
 
 func (r SessionChatParamsPartsType) IsKnown() bool {
 	switch r {
-	case SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeText:
+	case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile:
 		return true
 	}
 	return false

+ 12 - 11
packages/tui/sdk/session_test.go

@@ -101,7 +101,7 @@ func TestSessionAbort(t *testing.T) {
 	}
 }
 
-func TestSessionChat(t *testing.T) {
+func TestSessionChatWithOptionalParams(t *testing.T) {
 	t.Skip("skipped: tests are disabled for the time being")
 	baseURL := "http://localhost:4010"
 	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
@@ -117,19 +117,20 @@ func TestSessionChat(t *testing.T) {
 		context.TODO(),
 		"id",
 		opencode.SessionChatParams{
-			MessageID: opencode.F("messageID"),
-			Mode:      opencode.F("mode"),
-			ModelID:   opencode.F("modelID"),
-			Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.FilePartParam{
+			ModelID: opencode.F("modelID"),
+			Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.TextPartInputParam{
+				Text:      opencode.F("text"),
+				Type:      opencode.F(opencode.TextPartInputTypeText),
 				ID:        opencode.F("id"),
-				MessageID: opencode.F("messageID"),
-				Mime:      opencode.F("mime"),
-				SessionID: opencode.F("sessionID"),
-				Type:      opencode.F(opencode.FilePartTypeFile),
-				URL:       opencode.F("url"),
-				Filename:  opencode.F("filename"),
+				Synthetic: opencode.F(true),
+				Time: opencode.F(opencode.TextPartInputTimeParam{
+					Start: opencode.F(0.000000),
+					End:   opencode.F(0.000000),
+				}),
 			}}),
 			ProviderID: opencode.F("providerID"),
+			MessageID:  opencode.F("msg"),
+			Mode:       opencode.F("mode"),
 		},
 	)
 	if err != nil {

+ 2 - 0
stainless.yml

@@ -92,7 +92,9 @@ resources:
       message: Message
       part: Part
       textPart: TextPart
+      textPartInput: TextPartInput
       filePart: FilePart
+      filePartInput: FilePartInput
       toolPart: ToolPart
       stepStartPart: StepStartPart
       stepFinishPart: StepFinishPart