Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export namespace Config {
.optional()
.default("ctrl+left")
.describe("Cycle to previous child session"),
scratchpad_open: z.string().optional().default("<leader>w").describe("Open session scratchpad"),
messages_page_up: z.string().optional().default("pgup").describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().default("pgdown").describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
Expand Down
67 changes: 67 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,73 @@ export namespace Server {
return c.json(true)
},
)
.get(
"/session/:id/scratchpad",
describeRoute({
description: "Get session system scratchpad content",
operationId: "session.scratchpad.get",
responses: {
200: {
description: "System scratch content",
content: {
"application/json": {
schema: resolver(z.object({ scratchpad: z.string() })),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
async (c) => {
const sessionID = c.req.valid("param").id
const session = await Session.get(sessionID)
return c.json({ scratchpad: session?.scratchpad || "" })
},
)
.put(
"/session/:id/scratchpad",
describeRoute({
description: "Update session system scratchpad content",
operationId: "session.scratchpad.update",
responses: {
200: {
description: "System scratch updated successfully",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"param",
z.object({
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator(
"json",
z.object({
scratchpad: z.string(),
}),
),
async (c) => {
const sessionID = c.req.valid("param").id
const { scratchpad } = c.req.valid("json")

const updatedSession = await Session.update(sessionID, (session) => {
session.scratchpad = scratchpad
})

return c.json(updatedSession)
},
)
.get(
"/config/providers",
describeRoute({
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export namespace Session {
diff: z.string().optional(),
})
.optional(),
scratchpad: z.string().optional(),
})
.openapi({
ref: "Session",
Expand Down
72 changes: 64 additions & 8 deletions packages/sdk/go/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,30 @@ func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.
return
}

// Get session scratchpad content
func (r *SessionService) ScratchpadGet(ctx context.Context, id string, opts ...option.RequestOption) (res *SessionScratchpadGetResponse, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("session/%s/scratchpad", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}

// Update session system scratch content
func (r *SessionService) ScratchpadUpdate(ctx context.Context, id string, body SessionScratchpadUpdateParams, opts ...option.RequestOption) (res *Session, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
return
}
path := fmt.Sprintf("session/%s/scratchpad", id)
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, &res, opts...)
return
}

type AgentPart struct {
ID string `json:"id,required"`
MessageID string `json:"messageID,required"`
Expand Down Expand Up @@ -1282,14 +1306,15 @@ func (r ReasoningPartType) IsKnown() bool {
}

type Session struct {
ID string `json:"id,required"`
Time SessionTime `json:"time,required"`
Title string `json:"title,required"`
Version string `json:"version,required"`
ParentID string `json:"parentID"`
Revert SessionRevert `json:"revert"`
Share SessionShare `json:"share"`
JSON sessionJSON `json:"-"`
ID string `json:"id,required"`
Time SessionTime `json:"time,required"`
Title string `json:"title,required"`
Version string `json:"version,required"`
Scratchpad string `json:"scratchpad"`
ParentID string `json:"parentID"`
Revert SessionRevert `json:"revert"`
Share SessionShare `json:"share"`
JSON sessionJSON `json:"-"`
}

// sessionJSON contains the JSON metadata for the struct [Session]
Expand Down Expand Up @@ -2432,3 +2457,34 @@ type SessionSummarizeParams struct {
func (r SessionSummarizeParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}

type SessionScratchpadGetResponse struct {
Scratchpad string `json:"scratchpad,required"`
JSON sessionScratchpadGetResponseJSON `json:"-"`
}

type sessionScratchpadGetResponseJSON struct {
Scratchpad apijson.Field
raw string
ExtraFields map[string]apijson.Field
}

func (r sessionScratchpadGetResponseJSON) RawJSON() string {
return r.raw
}

func (r *SessionScratchpadGetResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}

func (r sessionScratchpadGetResponseJSON) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}

type SessionScratchpadUpdateParams struct {
Scratchpad param.Field[string] `json:"scratchpad,required"`
}

func (r SessionScratchpadUpdateParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
78 changes: 75 additions & 3 deletions packages/tui/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,10 +727,23 @@ func (a *App) MarkProjectInitialized(ctx context.Context) error {
}

func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
// If there is homescreen system scratch content, transfer it to the new session after creation

session, err := a.Client.Session.New(ctx, opencode.SessionNewParams{})
if err != nil {
return nil, err
}
// Transfer homescreen system scratch content if present
if a.State.HomescreenScratchpad != "" {
_, err := a.Client.Session.ScratchpadUpdate(ctx, session.ID, opencode.SessionScratchpadUpdateParams{
Scratchpad: opencode.F(a.State.HomescreenScratchpad),
})
if err == nil {
session.Scratchpad = a.State.HomescreenScratchpad
a.State.HomescreenScratchpad = ""
a.SaveState()
}
}
return session, nil
}

Expand All @@ -746,18 +759,34 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) {
}

messageID := id.Ascending(id.Message)
message := prompt.ToMessage(messageID, a.Session.ID)

// Create user message (scratchpad content is sent as system message if present)
message := prompt.ToMessage(messageID, a.Session.ID)
a.Messages = append(a.Messages, message)

// Prepare system message from scratchpad content if present
var systemMessage *string
scratchpadContent := a.GetSessionScratchpad()
if strings.TrimSpace(scratchpadContent) != "" {
systemPrompt := "The user has shared a scratchpad for this session. If it contains a task list, you MUST use the `todowrite` tool to track its items.\n\nUser's scratchpad content:\n" + scratchpadContent + "\n\n---"
systemMessage = &systemPrompt
}

cmds = append(cmds, func() tea.Msg {
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
chatParams := opencode.SessionChatParams{
ProviderID: opencode.F(a.Provider.ID),
ModelID: opencode.F(a.Model.ID),
Agent: opencode.F(a.Agent().Name),
MessageID: opencode.F(messageID),
Parts: opencode.F(message.ToSessionChatParams()),
})
}

// Add system message if scratchpad content exists
if systemMessage != nil {
chatParams.System = opencode.F(*systemMessage)
}

_, err := a.Client.Session.Chat(ctx, a.Session.ID, chatParams)
if err != nil {
errormsg := fmt.Sprintf("failed to send message: %v", err)
slog.Error(errormsg)
Expand Down Expand Up @@ -850,6 +879,49 @@ func (a *App) UpdateSession(ctx context.Context, sessionID string, title string)
return nil
}

func (a *App) GetSessionScratchpad() string {
if a.Session == nil {
return ""
}
return a.Session.Scratchpad
}


func (a *App) SaveSessionScratchpad(content string) error {
if a.Session == nil {
return fmt.Errorf("no active session")
}

ctx := context.Background()
_, err := a.Client.Session.ScratchpadUpdate(ctx, a.Session.ID, opencode.SessionScratchpadUpdateParams{
Scratchpad: opencode.F(content),
})
if err != nil {
slog.Error("Failed to save system scratch", "error", err)
return err
}

// Update local session
a.Session.Scratchpad = content
return nil
}

func (a *App) LoadSessionScratchpad(ctx context.Context) error {
if a.Session == nil {
return fmt.Errorf("no active session")
}

response, err := a.Client.Session.ScratchpadGet(ctx, a.Session.ID)
if err != nil {
slog.Error("Failed to load system scratch", "error", err)
return err
}

// Update local session
a.Session.Scratchpad = response.Scratchpad
return nil
}

func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
response, err := a.Client.Session.Messages(ctx, sessionId)
if err != nil {
Expand Down
21 changes: 11 additions & 10 deletions packages/tui/internal/app/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ type AgentModel struct {
}

type State struct {
Theme string `toml:"theme"`
AgentModel map[string]AgentModel `toml:"agent_model"`
Provider string `toml:"provider"`
Model string `toml:"model"`
Agent string `toml:"agent"`
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
RecentlyUsedAgents []AgentUsage `toml:"recently_used_agents"`
MessageHistory []Prompt `toml:"message_history"`
ShowToolDetails *bool `toml:"show_tool_details"`
ShowThinkingBlocks *bool `toml:"show_thinking_blocks"`
HomescreenScratchpad string `toml:"homescreen_scratchpad"` // persists system scratch when no session is active
Theme string `toml:"theme"`
AgentModel map[string]AgentModel `toml:"agent_model"`
Provider string `toml:"provider"`
Model string `toml:"model"`
Agent string `toml:"agent"`
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
RecentlyUsedAgents []AgentUsage `toml:"recently_used_agents"`
MessageHistory []Prompt `toml:"message_history"`
ShowToolDetails *bool `toml:"show_tool_details"`
ShowThinkingBlocks *bool `toml:"show_thinking_blocks"`
}

func NewState() *State {
Expand Down
Loading