新增: 会话分叉功能,优化增量扫描缓存
- 会话分叉: 按 c 键从历史会话分叉,支持带方向提示 - 启动自动加载历史记录 - 增量扫描: 缓存内存化、目录 modTime 跳过、已删除条目裁剪 - 刷新按钮复用 onTabSwitch 单一入口
This commit is contained in:
250
internal/launch.go
Normal file
250
internal/launch.go
Normal file
@@ -0,0 +1,250 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user