- 会话分叉: 按 c 键从历史会话分叉,支持带方向提示 - 启动自动加载历史记录 - 增量扫描: 缓存内存化、目录 modTime 跳过、已删除条目裁剪 - 刷新按钮复用 onTabSwitch 单一入口
251 lines
5.8 KiB
Go
251 lines
5.8 KiB
Go
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
|
||
}
|