Files
u-tabs/internal/launch.go
绝尘 36aeef4bb7 新增: 会话分叉功能,优化增量扫描缓存
- 会话分叉: 按 c 键从历史会话分叉,支持带方向提示
- 启动自动加载历史记录
- 增量扫描: 缓存内存化、目录 modTime 跳过、已删除条目裁剪
- 刷新按钮复用 onTabSwitch 单一入口
2026-05-31 21:40:20 +08:00

251 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}