package internal import ( "fmt" "log" "os/exec" "path/filepath" "strconv" "strings" "charm.land/bubbletea/v2" ) // SessionAction 描述一个 Claude Code 会话启动动作 type SessionAction struct { Cwd string // 工作目录 Title string // 窗口标题 SessionID string // resume/fork 目标(空=新建) Fork bool // 是否分叉 Prompt string // 初始提示(fork 方向或 workspace prompt) Name string // --name 参数(workspace 启动用) } // Execute 执行启动动作 func (a SessionAction) Execute(m *Model) { if isWindows { cwd := toWinPath(a.Cwd) launchInWtWithTitle(cwd, a.Title, a.buildScript()) } else { m.pendingCmd = a.buildScript() } } // buildScript 根据平台构建启动脚本 func (a SessionAction) buildScript() string { if isWindows { return a.pwshScript() } return a.bashScript() } func (a SessionAction) pwshScript() string { var b strings.Builder b.WriteString(fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "%s"`, a.Title)) b.WriteString(fmt.Sprintf("\n\t\tWrite-Host \"=== %s ===\" -ForegroundColor Cyan", a.Title)) if a.Prompt != "" && a.Name == "" { // workspace prompt 显示 b.WriteString(fmt.Sprintf("\n\t\tWrite-Host \"Prompt: %s\" -ForegroundColor Yellow", a.Prompt)) } b.WriteString(fmt.Sprintf("\n\t\tcd \"%s\"", toWinPath(a.Cwd))) b.WriteString("\n\t\tclaude") if a.SessionID != "" { b.WriteString(fmt.Sprintf(" -r %s", a.SessionID)) if a.Fork { b.WriteString(" --fork-session") } } if a.Prompt != "" && a.SessionID != "" { escaped := strings.ReplaceAll(a.Prompt, `"`, "`\"") b.WriteString(fmt.Sprintf(` "%s"`, escaped)) } if a.Name != "" { b.WriteString(fmt.Sprintf(` --name "%s"`, a.Name)) } b.WriteString(" --permission-mode bypassPermissions") return b.String() } func (a SessionAction) bashScript() string { var b strings.Builder b.WriteString(fmt.Sprintf(`printf '\033]0;%s\007' && echo "=== %s ==="`, a.Title, a.Title)) if a.Prompt != "" && a.Name == "" { b.WriteString(fmt.Sprintf(` && echo "Prompt: %s"`, a.Prompt)) } b.WriteString(fmt.Sprintf(` && cd "%s"`, a.Cwd)) b.WriteString(" && claude") if a.SessionID != "" { b.WriteString(fmt.Sprintf(" -r %s", a.SessionID)) if a.Fork { b.WriteString(" --fork-session") } } if a.Prompt != "" && a.SessionID != "" { escaped := strings.ReplaceAll(a.Prompt, "'", "'\\''") b.WriteString(fmt.Sprintf(` '%s'`, escaped)) } if a.Name != "" { b.WriteString(fmt.Sprintf(` --name "%s"`, a.Name)) } b.WriteString(" --permission-mode bypassPermissions") return b.String() } // --- 启动辅助 --- func launchInWtWithTitle(dir, tabTitle, script string) { encoded := encodePSCommand(script) color := fmt.Sprintf("#%02X%02X%02X", randRange(80, 255), randRange(80, 255), randRange(80, 255)) args := []string{"-w", "0", "-d", dir, "--tabColor", color} if tabTitle != "" { args = append(args, "--title", tabTitle) } args = append(args, "pwsh", "-NoExit", "-EncodedCommand", encoded) cmd := exec.Command("wt.exe", args...) if err := cmd.Start(); err != nil { log.Printf("[u-tabs] launch fail: %v", err) } } // buildResumeAction 构建恢复会话动作 func buildResumeAction(s *Session) SessionAction { title := s.CustomTitle if title == "" { title = s.ID[:8] } return SessionAction{ Cwd: s.Cwd, Title: "resume: " + title, SessionID: s.ID, } } // buildForkAction 构建分叉会话动作 func buildForkAction(s *Session, prompt string) SessionAction { title := s.CustomTitle if title == "" { title = s.ID[:8] } return SessionAction{ Cwd: s.Cwd, Title: "fork: " + title, SessionID: s.ID, Fork: true, Prompt: prompt, } } // buildNewAction 构建新建会话动作 func buildNewAction(cwd string) SessionAction { title := filepath.Base(cwd) + " - new" return SessionAction{ Cwd: cwd, Title: title, } } // buildWorkspaceAction 构建工作空间启动动作 func buildWorkspaceAction(ws Workspace) SessionAction { return SessionAction{ Cwd: ws.Dir, Title: ws.Title, Prompt: ws.Prompt, Name: ws.Title, } } // --- 会话操作 --- func (m *Model) resumeSelected() (*Model, tea.Cmd) { s := m.currentSession() if s == nil { return m, nil } action := buildResumeAction(s) action.Execute(m) m.launched = "resume: " + s.CustomTitle return m, nil } func (m *Model) newSessionFromHistory() (*Model, tea.Cmd) { s := m.currentSession() cwd := "" if s != nil { cwd = s.Cwd } else { cwd = m.currentDirCwd() } if cwd == "" { return m, nil } action := buildNewAction(cwd) action.Execute(m) m.launched = "new: " + filepath.Base(cwd) return m, nil } func (m *Model) executeFork() (*Model, tea.Cmd) { s := m.currentSession() if s == nil { m.forkMode = false return m, nil } m.forkMode = false action := buildForkAction(s, m.forkBuf) m.forkBuf = "" action.Execute(m) title := s.CustomTitle if title == "" { title = s.ID[:8] } m.launched = "fork: " + title return m, nil } func (m *Model) launchSelected() (*Model, tea.Cmd) { if len(m.selected) > 0 { var launched []string for idx := range m.selected { ws := &AllWorkspaces[idx] action := buildWorkspaceAction(*ws) action.Execute(m) launched = append(launched, ws.Title) } m.launched = strings.Join(launched, ", ") m.selected = make(map[int]bool) return m, nil } svcs := WorkspacesByGroup(Groups[m.activeGroup].Label) if m.cursor < len(svcs) { ws := svcs[m.cursor] action := buildWorkspaceAction(ws) action.Execute(m) m.launched = ws.Title } return m, nil } func (m *Model) launchByInput() (*Model, tea.Cmd) { num, err := strconv.Atoi(m.inputBuf) if err != nil { m.inputBuf = "" return m, nil } ws := FindByNumber(num) if ws != nil { action := buildWorkspaceAction(*ws) action.Execute(m) m.launched = ws.Title } m.inputBuf = "" return m, nil }