wip: append prompt

This commit is contained in:
adamdotdevin 2025-07-23 08:56:48 -05:00
parent b7b0cdbd7c
commit 7e4c6095ec
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
22 changed files with 225 additions and 103 deletions

View file

@ -720,12 +720,7 @@ export namespace Server {
},
},
}),
zValidator(
"json",
z.object({
text: z.string(),
}),
),
zValidator("json", Session.ChatInput.pick({ parts: true })),
async (c) => c.json(await callTui(c)),
)
.post(

View file

@ -330,14 +330,8 @@ export namespace Session {
return part
}
export const ChatInput = z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: z.array(
export const PartsInput = z
.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
messageID: true,
@ -360,7 +354,19 @@ export namespace Session {
ref: "FilePartInput",
}),
]),
),
)
.openapi({
ref: "PartsInput",
})
export const ChatInput = z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
mode: z.string().optional(),
tools: z.record(z.boolean()).optional(),
parts: PartsInput,
})
export type ChatInput = z.infer<typeof ChatInput>

View file

@ -1,4 +1,4 @@
configured_endpoints: 24
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-9574184bd9e916aa69eae8e26e0679556038d3fcfb4009a445c97c6cc3e4f3ee.yml
openapi_spec_hash: 93ba1215ab0dc853a1691b049cc47d75
config_hash: 09e4835d57ec7ed0b2d316c6815bcf0a
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-305a02cb514ed54609aa9daae92410230435b7f886f04f790b1db828cd20e524.yml
openapi_spec_hash: b207cc749da156457a2b48c9337aa690
config_hash: 14ba54419d8428bd5440fd20eba04f99

View file

@ -92,6 +92,7 @@ Types:
- <code><a href="./src/resources/session.ts">FileSource</a></code>
- <code><a href="./src/resources/session.ts">Message</a></code>
- <code><a href="./src/resources/session.ts">Part</a></code>
- <code><a href="./src/resources/session.ts">PartsInput</a></code>
- <code><a href="./src/resources/session.ts">Session</a></code>
- <code><a href="./src/resources/session.ts">SnapshotPart</a></code>
- <code><a href="./src/resources/session.ts">StepFinishPart</a></code>

View file

@ -58,6 +58,7 @@ import {
FileSource,
Message,
Part,
PartsInput,
Session,
SessionAbortResponse,
SessionChatParams,
@ -825,6 +826,7 @@ export declare namespace Opencode {
type FileSource as FileSource,
type Message as Message,
type Part as Part,
type PartsInput as PartsInput,
type Session as Session,
type SnapshotPart as SnapshotPart,
type StepFinishPart as StepFinishPart,

View file

@ -147,7 +147,7 @@ export namespace Config {
npm?: string;
options?: { [key: string]: unknown };
options?: Provider.Options;
}
export namespace Provider {
@ -190,6 +190,14 @@ export namespace Config {
output: number;
}
}
export interface Options {
apiKey?: string;
baseURL?: string;
[k: string]: unknown;
}
}
}

View file

@ -50,6 +50,7 @@ export {
type FileSource,
type Message,
type Part,
type PartsInput,
type Session,
type SnapshotPart,
type StepFinishPart,

View file

@ -205,6 +205,8 @@ export type Message = UserMessage | AssistantMessage;
export type Part = TextPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart;
export type PartsInput = Array<TextPartInput | FilePartInput>;
export interface Session {
id: string;
@ -494,7 +496,7 @@ export type SessionSummarizeResponse = boolean;
export interface SessionChatParams {
modelID: string;
parts: Array<TextPartInput | FilePartInput>;
parts: PartsInput;
providerID: string;
@ -529,6 +531,7 @@ export declare namespace SessionResource {
type FileSource as FileSource,
type Message as Message,
type Part as Part,
type PartsInput as PartsInput,
type Session as Session,
type SnapshotPart as SnapshotPart,
type StepFinishPart as StepFinishPart,

View file

@ -1,6 +1,7 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
import { APIResource } from '../core/resource';
import * as SessionAPI from './session';
import { APIPromise } from '../core/api-promise';
import { RequestOptions } from '../internal/request-options';
@ -25,7 +26,7 @@ export type TuiAppendPromptResponse = boolean;
export type TuiOpenHelpResponse = boolean;
export interface TuiAppendPromptParams {
text: string;
parts: SessionAPI.PartsInput;
}
export declare namespace Tui {

View file

@ -7,7 +7,7 @@ const client = new Opencode({ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http
describe('resource tui', () => {
// skipped: tests are disabled for the time being
test.skip('appendPrompt: only required params', async () => {
const responsePromise = client.tui.appendPrompt({ text: 'text' });
const responsePromise = client.tui.appendPrompt({ parts: [{ text: 'text', type: 'text' }] });
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
@ -19,7 +19,9 @@ describe('resource tui', () => {
// skipped: tests are disabled for the time being
test.skip('appendPrompt: required and optional params', async () => {
const response = await client.tui.appendPrompt({ text: 'text' });
const response = await client.tui.appendPrompt({
parts: [{ text: 'text', type: 'text', id: 'id', synthetic: true, time: { start: 0, end: 0 } }],
});
});
// skipped: tests are disabled for the time being

View file

@ -1,8 +1,10 @@
package app
import (
"strings"
"time"
"github.com/google/uuid"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/attachment"
"github.com/sst/opencode/internal/id"
@ -109,8 +111,8 @@ func (p Prompt) ToMessage(
}
}
func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
parts := []opencode.SessionChatParamsPartUnion{}
func (m Message) ToSessionChatParams() opencode.PartsInputParam {
parts := []opencode.PartsInputItemUnionParam{}
for _, part := range m.Parts {
switch p := part.(type) {
case opencode.TextPart:
@ -173,8 +175,8 @@ func (m Message) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
return parts
}
func (p Prompt) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
parts := []opencode.SessionChatParamsPartUnion{
func (p Prompt) ToSessionChatParams() opencode.PartsInputParam {
parts := []opencode.PartsInputItemUnionParam{
opencode.TextPartInputParam{
Type: opencode.F(opencode.TextPartInputTypeText),
Text: opencode.F(p.Text),
@ -233,3 +235,62 @@ func (p Prompt) ToSessionChatParams() []opencode.SessionChatParamsPartUnion {
}
return parts
}
func NewPromptFromParts(parts opencode.PartsInputParam) Prompt {
texts := []string{}
attachments := []*attachment.Attachment{}
for _, part := range parts {
switch p := part.(type) {
case opencode.TextPartInputParam:
texts = append(texts, p.Text.Value)
case opencode.FilePartInputParam:
switch source := p.Source.Value.(type) {
case opencode.FileSourceParam:
display := source.Text.Value.Value.Value
texts = append(texts, display)
attachments = append(attachments, &attachment.Attachment{
ID: uuid.NewString(),
Type: "file",
Display: display,
URL: p.URL.Value,
Filename: p.Filename.Value,
MediaType: p.Mime.Value,
StartIndex: int(source.Text.Value.Start.Value),
EndIndex: int(source.Text.Value.End.Value),
Source: attachment.FileSource{Path: source.Path.Value, Mime: p.Mime.Value},
})
case opencode.SymbolSourceParam:
display := source.Text.Value.Value.Value
texts = append(texts, display)
attachments = append(attachments, &attachment.Attachment{
ID: uuid.NewString(),
Type: "symbol",
Display: display,
URL: p.URL.Value,
Filename: p.Filename.Value,
MediaType: p.Mime.Value,
StartIndex: int(source.Text.Value.Start.Value),
EndIndex: int(source.Text.Value.End.Value),
Source: attachment.SymbolSource{
Path: source.Path.Value,
Name: source.Name.Value,
Kind: int(source.Kind.Value),
Range: attachment.SymbolRange{
Start: attachment.Position{
Line: int(source.Range.Value.Start.Value.Line.Value),
Char: int(source.Range.Value.Start.Value.Character.Value),
},
End: attachment.Position{
Line: int(source.Range.Value.End.Value.Line.Value),
Char: int(source.Range.Value.End.Value.Character.Value),
},
}},
})
}
}
}
return Prompt{
Text: strings.Join(texts, " "),
Attachments: attachments,
}
}

View file

@ -45,6 +45,7 @@ type EditorComponent interface {
SetInterruptKeyInDebounce(inDebounce bool)
SetExitKeyInDebounce(inDebounce bool)
RestoreFromHistory(index int)
AppendPrompt(prompt app.Prompt)
}
type editorComponent struct {
@ -630,21 +631,15 @@ func NewEditorComponent(app *app.App) EditorComponent {
return m
}
// RestoreFromHistory restores a message from history at the given index
func (m *editorComponent) RestoreFromHistory(index int) {
if index < 0 || index >= len(m.app.State.MessageHistory) {
return
}
entry := m.app.State.MessageHistory[index]
m.textarea.Reset()
m.textarea.SetValue(entry.Text)
func (m *editorComponent) AppendPrompt(prompt app.Prompt) {
length := m.Length()
m.textarea.MoveToEnd()
m.textarea.InsertRunesFromUserInput([]rune(prompt.Text))
// Sort attachments by start index in reverse order (process from end to beginning)
// This prevents index shifting issues
attachmentsCopy := make([]*attachment.Attachment, len(entry.Attachments))
copy(attachmentsCopy, entry.Attachments)
attachmentsCopy := make([]*attachment.Attachment, len(prompt.Attachments))
copy(attachmentsCopy, prompt.Attachments)
for i := 0; i < len(attachmentsCopy)-1; i++ {
for j := i + 1; j < len(attachmentsCopy); j++ {
@ -655,12 +650,27 @@ func (m *editorComponent) RestoreFromHistory(index int) {
}
for _, att := range attachmentsCopy {
m.textarea.SetCursorColumn(att.StartIndex)
m.textarea.ReplaceRange(att.StartIndex, att.EndIndex, "")
m.textarea.SetCursorColumn(length + att.StartIndex)
m.textarea.ReplaceRange(length+att.StartIndex, length+att.EndIndex, "")
m.textarea.InsertAttachment(att)
}
}
func (m *editorComponent) RestoreFromPrompt(prompt app.Prompt) {
m.textarea.Reset()
m.textarea.MoveToBegin()
m.AppendPrompt(prompt)
}
// RestoreFromHistory restores a message from history at the given index
func (m *editorComponent) RestoreFromHistory(index int) {
if index < 0 || index >= len(m.app.State.MessageHistory) {
return
}
entry := m.app.State.MessageHistory[index]
m.RestoreFromPrompt(entry)
}
func getMediaTypeFromExtension(ext string) string {
switch strings.ToLower(ext) {
case ".jpg":

View file

@ -511,15 +511,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.modal = helpDialog
case "/tui/append-prompt":
var body struct {
Text string `json:"text"`
Parts opencode.PartsInputParam `json:"parts"`
}
json.Unmarshal((msg.Body), &body)
existing := a.editor.Value()
text := body.Text
if existing != "" && !strings.HasSuffix(existing, " ") {
text = " " + text
}
a.editor.SetValueWithAttachments(existing + text + " ")
prompt := app.NewPromptFromParts(body.Parts)
a.editor.AppendPrompt(prompt)
default:
break
}

View file

@ -1,4 +1,4 @@
configured_endpoints: 24
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-9574184bd9e916aa69eae8e26e0679556038d3fcfb4009a445c97c6cc3e4f3ee.yml
openapi_spec_hash: 93ba1215ab0dc853a1691b049cc47d75
config_hash: 09e4835d57ec7ed0b2d316c6815bcf0a
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-305a02cb514ed54609aa9daae92410230435b7f886f04f790b1db828cd20e524.yml
openapi_spec_hash: b207cc749da156457a2b48c9337aa690
config_hash: 14ba54419d8428bd5440fd20eba04f99

View file

@ -79,6 +79,7 @@ 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#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>
- <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#FileSourceParam">FileSourceParam</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#PartsInputParam">PartsInputParam</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#SymbolSourceParam">SymbolSourceParam</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>

View file

@ -333,7 +333,7 @@ type ConfigProvider struct {
Env []string `json:"env"`
Name string `json:"name"`
Npm string `json:"npm"`
Options map[string]interface{} `json:"options"`
Options ConfigProviderOptions `json:"options"`
JSON configProviderJSON `json:"-"`
}
@ -447,6 +447,30 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
return r.raw
}
type ConfigProviderOptions struct {
APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"`
ExtraFields map[string]interface{} `json:"-,extras"`
JSON configProviderOptionsJSON `json:"-"`
}
// configProviderOptionsJSON contains the JSON metadata for the struct
// [ConfigProviderOptions]
type configProviderOptionsJSON struct {
APIKey apijson.Field
BaseURL apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigProviderOptions) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configProviderOptionsJSON) RawJSON() string {
return r.raw
}
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
// enables automatic sharing, 'disabled' disables all sharing
type ConfigShare string

View file

@ -496,7 +496,7 @@ func (r FilePartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r FilePartInputParam) implementsSessionChatParamsPartUnion() {}
func (r FilePartInputParam) implementsPartsInputItemUnionParam() {}
type FilePartInputType string
@ -932,6 +932,46 @@ func (r PartType) IsKnown() bool {
return false
}
type PartsInputParam []PartsInputItemUnionParam
type PartsInputItemParam struct {
Type param.Field[PartsInputItemType] `json:"type,required"`
ID param.Field[string] `json:"id"`
Filename param.Field[string] `json:"filename"`
Mime param.Field[string] `json:"mime"`
Source param.Field[FilePartSourceUnionParam] `json:"source"`
Synthetic param.Field[bool] `json:"synthetic"`
Text param.Field[string] `json:"text"`
Time param.Field[interface{}] `json:"time"`
URL param.Field[string] `json:"url"`
}
func (r PartsInputItemParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r PartsInputItemParam) implementsPartsInputItemUnionParam() {}
// Satisfied by [TextPartInputParam], [FilePartInputParam], [PartsInputItemParam].
type PartsInputItemUnionParam interface {
implementsPartsInputItemUnionParam()
}
type PartsInputItemType string
const (
PartsInputItemTypeText PartsInputItemType = "text"
PartsInputItemTypeFile PartsInputItemType = "file"
)
func (r PartsInputItemType) IsKnown() bool {
switch r {
case PartsInputItemTypeText, PartsInputItemTypeFile:
return true
}
return false
}
type Session struct {
ID string `json:"id,required"`
Time SessionTime `json:"time,required"`
@ -1451,7 +1491,7 @@ func (r TextPartInputParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r TextPartInputParam) implementsSessionChatParamsPartUnion() {}
func (r TextPartInputParam) implementsPartsInputItemUnionParam() {}
type TextPartInputType string
@ -1949,57 +1989,18 @@ func (r sessionMessagesResponseJSON) RawJSON() string {
}
type SessionChatParams struct {
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"`
Tools param.Field[map[string]bool] `json:"tools"`
ModelID param.Field[string] `json:"modelID,required"`
Parts param.Field[PartsInputParam] `json:"parts,required"`
ProviderID param.Field[string] `json:"providerID,required"`
MessageID param.Field[string] `json:"messageID"`
Mode param.Field[string] `json:"mode"`
Tools param.Field[map[string]bool] `json:"tools"`
}
func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
type SessionChatParamsPart struct {
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"`
Source param.Field[FilePartSourceUnionParam] `json:"source"`
Synthetic param.Field[bool] `json:"synthetic"`
Text param.Field[string] `json:"text"`
Time param.Field[interface{}] `json:"time"`
URL param.Field[string] `json:"url"`
}
func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
// Satisfied by [TextPartInputParam], [FilePartInputParam],
// [SessionChatParamsPart].
type SessionChatParamsPartUnion interface {
implementsSessionChatParamsPartUnion()
}
type SessionChatParamsPartsType string
const (
SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
)
func (r SessionChatParamsPartsType) IsKnown() bool {
switch r {
case SessionChatParamsPartsTypeText, SessionChatParamsPartsTypeFile:
return true
}
return false
}
type SessionInitParams struct {
MessageID param.Field[string] `json:"messageID,required"`
ModelID param.Field[string] `json:"modelID,required"`

View file

@ -118,7 +118,7 @@ func TestSessionChatWithOptionalParams(t *testing.T) {
"id",
opencode.SessionChatParams{
ModelID: opencode.F("modelID"),
Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.TextPartInputParam{
Parts: opencode.F(opencode.PartsInputParam{opencode.TextPartInputParam{
Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartInputTypeText),
ID: opencode.F("id"),

View file

@ -48,7 +48,7 @@ func (r *TuiService) OpenHelp(ctx context.Context, opts ...option.RequestOption)
}
type TuiAppendPromptParams struct {
Text param.Field[string] `json:"text,required"`
Parts param.Field[PartsInputParam] `json:"parts,required"`
}
func (r TuiAppendPromptParams) MarshalJSON() (data []byte, err error) {

View file

@ -26,7 +26,16 @@ func TestTuiAppendPrompt(t *testing.T) {
option.WithBaseURL(baseURL),
)
_, err := client.Tui.AppendPrompt(context.TODO(), opencode.TuiAppendPromptParams{
Text: opencode.F("text"),
Parts: opencode.F(opencode.PartsInputParam{opencode.TextPartInputParam{
Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartInputTypeText),
ID: opencode.F("id"),
Synthetic: opencode.F(true),
Time: opencode.F(opencode.TextPartInputTimeParam{
Start: opencode.F(0.000000),
End: opencode.F(0.000000),
}),
}}),
})
if err != nil {
var apierr *opencode.Error

View file

@ -77,7 +77,7 @@ async function appendPrompt(port: number, text: string) {
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text }),
body: JSON.stringify({ parts: [{ type: "text", text }] }),
})
}

View file

@ -90,6 +90,7 @@ resources:
session: Session
message: Message
part: Part
partsInput: PartsInput
textPart: TextPart
textPartInput: TextPartInput
filePart: FilePart