package tui import ( "context" "fmt" "os" "strings" "time" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "superwork-tui/internal/cc" "superwork-tui/internal/config" "superwork-tui/internal/logging" "superwork-tui/internal/store" ) type managedSessionsMode int const ( sessionsModeCreate managedSessionsMode = iota sessionsModeRename ) // ── message types ───────────────────────────────────────────────────────────── type managedSessionsLoadedMsg struct { data store.ManagedSessionsData err error } type managedSessionsSavedMsg struct { err error } type managedSessionCapturedMsg struct { name string profilePath string createdAt int64 sessionID string } var watchForNewSessionFn = func(ctx context.Context, opts cc.SessionWatchOpts) (string, error) { return cc.WatchForNewSession(ctx, opts) } // ── commands ────────────────────────────────────────────────────────────────── func loadManagedSessionsCmd() tea.Cmd { return func() tea.Msg { root, err := workspaceRootFn() if err != nil { return managedSessionsLoadedMsg{err: err} } data, err := store.ReadManagedSessions(root) return managedSessionsLoadedMsg{data: data, err: err} } } func saveManagedSessionsCmd(data store.ManagedSessionsData) tea.Cmd { return func() tea.Msg { root, err := workspaceRootFn() if err != nil { return managedSessionsSavedMsg{err: err} } err = store.WriteManagedSessions(root, data) return managedSessionsSavedMsg{err: err} } } func resumeManagedSessionCmd(sess store.ManagedSession, settings *config.Settings, mgr *SessionManager) tea.Cmd { return func() tea.Msg { if sess.ID == "" { return sessionActionResultMsg{err: fmt.Errorf("会话 %q 尚无 ID,无法 resume", sess.Name)} } sysCmd := "" if settings != nil { sysCmd = settings.SystemPromptCommand } cmd := claudeResumeCmd(sess.ID, sess.ProfilePath, sysCmd) cwd, _ := workspaceRootFn() winName := "ms-" + sess.ID[:min(8, len(sess.ID))] logging.Default.Info("sessions", fmt.Sprintf("resume 会话 %s (%s)", sess.Name, sess.ID)) err := mgr.RunInWindow(context.Background(), winName, cwd, cmd) return sessionActionResultMsg{err: err} } } func launchAndWatchManagedSessionCmd(name, profilePath string, createdAt int64, settings *config.Settings, mgr *SessionManager) tea.Cmd { return func() tea.Msg { cwd, err := workspaceRootFn() if err != nil { return managedSessionCapturedMsg{name: name, profilePath: profilePath, createdAt: createdAt} } sysCmd := "" if settings != nil { sysCmd = settings.SystemPromptCommand } cmd := claudeCmd(claudeCmdOpts{ ProfilePath: profilePath, SystemPromptCommand: sysCmd, }) winName := "ms-new-" + name if len(winName) > 20 { winName = winName[:20] } if err := mgr.RunInWindow(context.Background(), winName, cwd, cmd); err != nil { logging.Default.Warn("sessions", fmt.Sprintf("managed session launch failed: %v", err)) } home, err := os.UserHomeDir() if err != nil { return managedSessionCapturedMsg{name: name, profilePath: profilePath, createdAt: createdAt} } projectsDir := cc.ClaudeProjectsDir(home, cwd) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() sid, _ := watchForNewSessionFn(ctx, cc.SessionWatchOpts{ ProjectsDir: projectsDir, Timeout: 120 * time.Second, }) if sid == "" { logging.Default.Warn("sessions", "managed session watch timeout (120s)") return managedSessionCapturedMsg{name: name, profilePath: profilePath, createdAt: createdAt} } logging.Default.Info("sessions", fmt.Sprintf("managed session captured: %s", sid)) return managedSessionCapturedMsg{name: name, profilePath: profilePath, createdAt: createdAt, sessionID: sid} } } // ── open ────────────────────────────────────────────────────────────────────── func (m Model) openManagedSessions() (Model, tea.Cmd) { m.state = stateManagedSessions m.managedSessionsCursor = 0 m.managedSessionsInputActive = false m.managedSessionsDeleteID = "" return m, loadManagedSessionsCmd() } // ── key handling ────────────────────────────────────────────────────────────── func (m Model) handleManagedSessionsKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.managedSessionsDeleteID != "" { switch msg.String() { case "y": id := m.managedSessionsDeleteID m.managedSessionsDeleteID = "" newData := store.DeleteSession(m.managedSessions, id) m.managedSessions = newData if m.managedSessionsCursor >= len(m.managedSessions.Sessions) && m.managedSessionsCursor > 0 { m.managedSessionsCursor-- } logging.Default.Info("sessions", fmt.Sprintf("已删除会话 %s", id)) return m, saveManagedSessionsCmd(newData) case "n", "esc": m.managedSessionsDeleteID = "" } return m, nil } if m.managedSessionsInputActive { switch msg.String() { case "enter": name := strings.TrimSpace(m.managedSessionsInput.Value()) m.managedSessionsInputActive = false if name == "" { return m, nil } switch m.managedSessionsInputMode { case sessionsModeCreate: logging.Default.Info("sessions", fmt.Sprintf("新建会话 %q", name)) return m, launchAndWatchManagedSessionCmd(name, "", time.Now().Unix(), m.settings, m.sessionMgr) case sessionsModeRename: if m.managedSessionsCursor < len(m.managedSessions.Sessions) { id := m.managedSessions.Sessions[m.managedSessionsCursor].ID newData := store.RenameSession(m.managedSessions, id, name) m.managedSessions = newData logging.Default.Info("sessions", fmt.Sprintf("重命名会话 %s → %q", id, name)) return m, saveManagedSessionsCmd(newData) } } case "esc": m.managedSessionsInputActive = false default: var cmd tea.Cmd m.managedSessionsInput, cmd = m.managedSessionsInput.Update(msg) return m, cmd } return m, nil } switch msg.String() { case "esc": m.state = stateLoaded case "up", "k": if m.managedSessionsCursor > 0 { m.managedSessionsCursor-- } case "down", "j": if m.managedSessionsCursor < len(m.managedSessions.Sessions)-1 { m.managedSessionsCursor++ } case "a": ti := textinput.New() ti.Placeholder = "会话名称" ti.Focus() m.managedSessionsInput = ti m.managedSessionsInputMode = sessionsModeCreate m.managedSessionsInputActive = true case "r": if m.managedSessionsCursor < len(m.managedSessions.Sessions) { cur := m.managedSessions.Sessions[m.managedSessionsCursor] ti := textinput.New() ti.SetValue(cur.Name) ti.Focus() m.managedSessionsInput = ti m.managedSessionsInputMode = sessionsModeRename m.managedSessionsInputActive = true } case "d": if m.managedSessionsCursor < len(m.managedSessions.Sessions) { m.managedSessionsDeleteID = m.managedSessions.Sessions[m.managedSessionsCursor].ID } case "enter": if m.managedSessionsCursor < len(m.managedSessions.Sessions) { sess := m.managedSessions.Sessions[m.managedSessionsCursor] return m, resumeManagedSessionCmd(sess, m.settings, m.sessionMgr) } } return m, nil } // ── view ────────────────────────────────────────────────────────────────────── func (m Model) managedSessionsView() string { var sb strings.Builder sb.WriteString(helpStyle.Render("受管理 Claude 会话") + "\n") sb.WriteString(strings.Repeat("─", 70) + "\n") if len(m.managedSessions.Sessions) == 0 { sb.WriteString(" (无会话)\n") } else { header := fmt.Sprintf(" %-20s %-20s %-15s %s", "名称", "ID", "ProfilePath", "创建时间") sb.WriteString(helpStyle.Render(header) + "\n") for i, s := range m.managedSessions.Sessions { ts := time.Unix(s.CreatedAt, 0).Format("2006-01-02 15:04") idShort := s.ID if len(idShort) > 20 { idShort = idShort[:20] } line := fmt.Sprintf(" %-20s %-20s %-15s %s", s.Name, idShort, s.ProfilePath, ts) if i == m.managedSessionsCursor { sb.WriteString(selectedCardStyle.Render(line) + "\n") } else { sb.WriteString(normalCardStyle.Render(line) + "\n") } } } sb.WriteString("\n") if m.managedSessionsDeleteID != "" { deleteName := m.managedSessionsDeleteID for _, s := range m.managedSessions.Sessions { if s.ID == m.managedSessionsDeleteID { deleteName = s.Name break } } sb.WriteString(confirmStyle.Render(fmt.Sprintf("确认删除会话 %q? (y/n)", deleteName)) + "\n") } else if m.managedSessionsInputActive { prompt := "新建名称: " if m.managedSessionsInputMode == sessionsModeRename { prompt = "新名称: " } sb.WriteString(confirmStyle.Render(prompt) + m.managedSessionsInput.View() + "\n") } else { sb.WriteString(helpStyle.Render("a new r rename d delete enter resume esc back")) } return sb.String() }