mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-20 17:24:22 +01:00
feat: add session rename functionality to TUI modal (#1821)
Co-authored-by: opencode <noreply@opencode.ai> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com>
This commit is contained in:
committed by
GitHub
parent
81583cddbd
commit
47c327641b
@@ -296,6 +296,47 @@ export namespace Server {
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.patch(
|
||||
"/session/:id",
|
||||
describeRoute({
|
||||
description: "Update session properties",
|
||||
operationId: "session.update",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully updated session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"param",
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const updates = c.req.valid("json")
|
||||
|
||||
const updatedSession = await Session.update(sessionID, (session) => {
|
||||
if (updates.title !== undefined) {
|
||||
session.title = updates.title
|
||||
}
|
||||
})
|
||||
|
||||
return c.json(updatedSession)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session/:id/init",
|
||||
describeRoute({
|
||||
|
||||
@@ -66,6 +66,18 @@ func (r *SessionService) Delete(ctx context.Context, id string, opts ...option.R
|
||||
return
|
||||
}
|
||||
|
||||
// Update session properties
|
||||
func (r *SessionService) Update(ctx context.Context, id string, body SessionUpdateParams, 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", id)
|
||||
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...)
|
||||
return
|
||||
}
|
||||
|
||||
// Abort a session
|
||||
func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) {
|
||||
opts = append(r.Options[:], opts...)
|
||||
@@ -2356,3 +2368,11 @@ type SessionSummarizeParams struct {
|
||||
func (r SessionSummarizeParams) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
type SessionUpdateParams struct {
|
||||
Title param.Field[string] `json:"title"`
|
||||
}
|
||||
|
||||
func (r SessionUpdateParams) MarshalJSON() (data []byte, err error) {
|
||||
return apijson.MarshalRoot(r)
|
||||
}
|
||||
|
||||
@@ -760,6 +760,17 @@ func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateSession(ctx context.Context, sessionID string, title string) error {
|
||||
_, err := a.Client.Session.Update(ctx, sessionID, opencode.SessionUpdateParams{
|
||||
Title: opencode.F(title),
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to update session", "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
|
||||
response, err := a.Client.Session.Messages(ctx, sessionId)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode-sdk-go"
|
||||
@@ -110,6 +111,9 @@ type sessionDialog struct {
|
||||
list list.List[sessionItem]
|
||||
app *app.App
|
||||
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
||||
renameMode bool
|
||||
renameInput textinput.Model
|
||||
renameIndex int // index of session being renamed
|
||||
}
|
||||
|
||||
func (s *sessionDialog) Init() tea.Cmd {
|
||||
@@ -123,6 +127,39 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
s.height = msg.Height
|
||||
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||
case tea.KeyPressMsg:
|
||||
if s.renameMode {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) && idx == s.renameIndex {
|
||||
newTitle := s.renameInput.Value()
|
||||
if strings.TrimSpace(newTitle) != "" {
|
||||
sessionToUpdate := s.sessions[idx]
|
||||
return s, tea.Sequence(
|
||||
func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
err := s.app.UpdateSession(ctx, sessionToUpdate.ID, newTitle)
|
||||
if err != nil {
|
||||
return toast.NewErrorToast("Failed to rename session: " + err.Error())()
|
||||
}
|
||||
s.sessions[idx].Title = newTitle
|
||||
s.renameMode = false
|
||||
s.modal.SetTitle("Switch Session")
|
||||
s.updateListItems()
|
||||
return toast.NewSuccessToast("Session renamed successfully")()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
s.renameMode = false
|
||||
s.modal.SetTitle("Switch Session")
|
||||
s.updateListItems()
|
||||
return s, nil
|
||||
default:
|
||||
var cmd tea.Cmd
|
||||
s.renameInput, cmd = s.renameInput.Update(msg)
|
||||
return s, cmd
|
||||
}
|
||||
} else {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if s.deleteConfirmation >= 0 {
|
||||
@@ -142,6 +179,15 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(app.SessionClearedMsg{}),
|
||||
)
|
||||
case "r":
|
||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||
s.renameMode = true
|
||||
s.renameIndex = idx
|
||||
s.setupRenameInput(s.sessions[idx].Title)
|
||||
s.modal.SetTitle("Rename Session")
|
||||
s.updateListItems()
|
||||
return s, textinput.Blink
|
||||
}
|
||||
case "x", "delete", "backspace":
|
||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||
if s.deleteConfirmation == idx {
|
||||
@@ -171,21 +217,38 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.renameMode {
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := s.list.Update(msg)
|
||||
s.list = listModel.(list.List[sessionItem])
|
||||
return s, cmd
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *sessionDialog) Render(background string) string {
|
||||
if s.renameMode {
|
||||
// Show rename input instead of list
|
||||
t := theme.CurrentTheme()
|
||||
renameView := s.renameInput.View()
|
||||
|
||||
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
||||
helpText := mutedStyle("Enter to confirm, Esc to cancel")
|
||||
helpText = styles.NewStyle().PaddingLeft(1).PaddingTop(1).Render(helpText)
|
||||
|
||||
content := strings.Join([]string{renameView, helpText}, "\n")
|
||||
return s.modal.Render(content, background)
|
||||
}
|
||||
|
||||
listView := s.list.View()
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
keyStyle := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundPanel()).Render
|
||||
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
||||
|
||||
leftHelp := keyStyle("n") + mutedStyle(" new session")
|
||||
leftHelp := keyStyle("n") + mutedStyle(" new session") + " " + keyStyle("r") + mutedStyle(" rename")
|
||||
rightHelp := keyStyle("x/del") + mutedStyle(" delete session")
|
||||
|
||||
bgColor := t.BackgroundPanel()
|
||||
@@ -203,6 +266,39 @@ func (s *sessionDialog) Render(background string) string {
|
||||
return s.modal.Render(content, background)
|
||||
}
|
||||
|
||||
func (s *sessionDialog) setupRenameInput(currentTitle string) {
|
||||
t := theme.CurrentTheme()
|
||||
bgColor := t.BackgroundPanel()
|
||||
textColor := t.Text()
|
||||
textMutedColor := t.TextMuted()
|
||||
|
||||
s.renameInput = textinput.New()
|
||||
s.renameInput.SetValue(currentTitle)
|
||||
s.renameInput.Focus()
|
||||
s.renameInput.CharLimit = 100
|
||||
s.renameInput.SetWidth(layout.Current.Container.Width - 20)
|
||||
|
||||
s.renameInput.Styles.Blurred.Placeholder = styles.NewStyle().
|
||||
Foreground(textMutedColor).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
s.renameInput.Styles.Blurred.Text = styles.NewStyle().
|
||||
Foreground(textColor).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
s.renameInput.Styles.Focused.Placeholder = styles.NewStyle().
|
||||
Foreground(textMutedColor).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
s.renameInput.Styles.Focused.Text = styles.NewStyle().
|
||||
Foreground(textColor).
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
s.renameInput.Styles.Focused.Prompt = styles.NewStyle().
|
||||
Background(bgColor).
|
||||
Lipgloss()
|
||||
}
|
||||
|
||||
func (s *sessionDialog) updateListItems() {
|
||||
_, currentIdx := s.list.GetSelectedItem()
|
||||
|
||||
@@ -229,7 +325,22 @@ func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// ReopenSessionModalMsg is emitted when the session modal should be reopened
|
||||
type ReopenSessionModalMsg struct{}
|
||||
|
||||
func (s *sessionDialog) Close() tea.Cmd {
|
||||
if s.renameMode {
|
||||
// If in rename mode, exit rename mode and return a command to reopen the modal
|
||||
s.renameMode = false
|
||||
s.modal.SetTitle("Switch Session")
|
||||
s.updateListItems()
|
||||
|
||||
// Return a command that will reopen the session modal
|
||||
return func() tea.Msg {
|
||||
return ReopenSessionModalMsg{}
|
||||
}
|
||||
}
|
||||
// Normal close behavior
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -272,6 +383,8 @@ func NewSessionDialog(app *app.App) SessionDialog {
|
||||
list: listComponent,
|
||||
app: app,
|
||||
deleteConfirmation: -1,
|
||||
renameMode: false,
|
||||
renameIndex: -1,
|
||||
modal: modal.New(
|
||||
modal.WithTitle("Switch Session"),
|
||||
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
||||
|
||||
@@ -357,6 +357,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
a.modal = nil
|
||||
return a, cmd
|
||||
case dialog.ReopenSessionModalMsg:
|
||||
// Reopen the session modal (used when exiting rename mode)
|
||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||
a.modal = sessionDialog
|
||||
return a, nil
|
||||
case commands.ExecuteCommandMsg:
|
||||
updated, cmd := a.executeCommand(commands.Command(msg))
|
||||
return updated, cmd
|
||||
|
||||
Reference in New Issue
Block a user