新增: 会话分叉功能,优化增量扫描缓存
- 新增 c 键触发会话分叉,支持带方向提示的分叉输入 - 使用 claude --fork-session 原生分叉,不污染原会话 - Init() 启动时自动触发历史扫描,无需手动切 tab - 缓存提升到 HistoryState 内存持有,避免重复 I/O - 新增项目目录 modTime 增量扫描,未变目录跳过遍历 - 扫描后裁剪缓存,删除磁盘已不存在的条目 - updateSummaryInCache 直接操作内存缓存
This commit is contained in:
129
internal/app.go
129
internal/app.go
@@ -35,6 +35,8 @@ type Model struct {
|
||||
update UpdateState
|
||||
wsFocus int // workspace 焦点列: 0=tabs 1=list
|
||||
wsTabCur int // tabs 列光标 (0..len(Groups), 最后一个是历史入口)
|
||||
forkMode bool // 分叉输入模式
|
||||
forkBuf string // 分叉方向提示输入
|
||||
}
|
||||
|
||||
func NewModel() *Model {
|
||||
@@ -46,7 +48,11 @@ func NewModel() *Model {
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
m.update.Checking = true
|
||||
return CheckUpdateCmd()
|
||||
cmds := []tea.Cmd{CheckUpdateCmd()}
|
||||
if cmd := m.onTabSwitch(); cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -58,11 +64,23 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.height = msg.Height
|
||||
case ScanCompleteMsg:
|
||||
m.history.Projects = msg.Projects
|
||||
m.history.Cache = msg.Cache
|
||||
m.history.Loaded = true
|
||||
m.history.Scanning = false
|
||||
if m.history.Favorites == nil {
|
||||
m.history.Favorites = loadFavorites()
|
||||
}
|
||||
// 填充 ForkFrom
|
||||
forks := loadForks()
|
||||
if len(forks) > 0 {
|
||||
for _, pd := range m.history.Projects {
|
||||
for _, s := range pd.Sessions {
|
||||
if src, ok := forks[s.ID]; ok {
|
||||
s.ForkFrom = src
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
total := 0
|
||||
for _, pd := range msg.Projects {
|
||||
total += len(pd.Sessions)
|
||||
@@ -134,11 +152,11 @@ func (m *Model) handleKey(s string) (*Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *Model) onTabSwitch() tea.Cmd {
|
||||
if m.IsHistoryTab() && !m.history.Loaded && !m.history.Scanning {
|
||||
m.history.Scanning = true
|
||||
return ScanSessionsCmd()
|
||||
if !m.IsHistoryTab() || m.history.Scanning {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
m.history.Scanning = true
|
||||
return ScanSessionsCmd()
|
||||
}
|
||||
|
||||
// --- Workspace 按键 ---
|
||||
@@ -206,6 +224,11 @@ func (m *Model) moveGroupCursor(dir int) {
|
||||
// --- HISTORY 按键 ---
|
||||
|
||||
func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
|
||||
// fork 输入模式拦截
|
||||
if m.forkMode {
|
||||
return m.handleForkInput(s)
|
||||
}
|
||||
|
||||
switch s {
|
||||
case "up", "k":
|
||||
if m.history.FocusPanel == 0 {
|
||||
@@ -243,10 +266,37 @@ func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
|
||||
case "n":
|
||||
return m.newSessionFromHistory()
|
||||
case "r", "f5":
|
||||
m.history.Scanning = true
|
||||
return m, ScanSessionsCmd()
|
||||
if cmd := m.onTabSwitch(); cmd != nil {
|
||||
return m, cmd
|
||||
}
|
||||
case "f":
|
||||
m.toggleFavorite()
|
||||
case "c":
|
||||
if m.currentSession() != nil && m.history.FocusPanel == 1 {
|
||||
m.forkMode = true
|
||||
m.forkBuf = ""
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// handleForkInput 处理分叉方向提示输入
|
||||
func (m *Model) handleForkInput(s string) (*Model, tea.Cmd) {
|
||||
switch s {
|
||||
case "enter":
|
||||
return m.executeFork()
|
||||
case "esc":
|
||||
m.forkMode = false
|
||||
m.forkBuf = ""
|
||||
case "backspace":
|
||||
if len(m.forkBuf) > 0 {
|
||||
m.forkBuf = m.forkBuf[:len(m.forkBuf)-1]
|
||||
}
|
||||
default:
|
||||
// 只接受可打印字符
|
||||
if len(s) == 1 && s[0] >= 32 && s[0] < 127 {
|
||||
m.forkBuf += s
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -365,6 +415,57 @@ func (m *Model) newSessionFromHistory() (*Model, tea.Cmd) {
|
||||
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
|
||||
|
||||
cwd := s.Cwd
|
||||
if isWindows {
|
||||
cwd = strings.ReplaceAll(cwd, "/", "\\")
|
||||
}
|
||||
title := s.CustomTitle
|
||||
if title == "" {
|
||||
title = s.ID[:8]
|
||||
}
|
||||
label := "fork: " + title
|
||||
|
||||
// 构建 claude -r <id> --fork-session [prompt]
|
||||
if m.forkBuf != "" {
|
||||
// 用户输入了方向提示
|
||||
prompt := m.forkBuf
|
||||
m.forkBuf = ""
|
||||
if isWindows {
|
||||
escaped := strings.ReplaceAll(prompt, `"`, "`\"")
|
||||
script := fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "fork: %s"
|
||||
Write-Host "=== Fork from %s ===" -ForegroundColor Cyan
|
||||
cd "%s"
|
||||
claude -r %s --fork-session "%s" --permission-mode bypassPermissions`, title, title, cwd, s.ID, escaped)
|
||||
launchInWtWithTitle(cwd, label, script)
|
||||
} else {
|
||||
escaped := strings.ReplaceAll(prompt, "'", "'\\''")
|
||||
m.pendingCmd = fmt.Sprintf(`printf '\033]0;fork: %s\007' && echo "=== Fork from %s ===" && cd "%s" && claude -r %s --fork-session '%s' --permission-mode bypassPermissions`, title, title, cwd, s.ID, escaped)
|
||||
}
|
||||
} else {
|
||||
// 纯分叉,不带方向提示
|
||||
if isWindows {
|
||||
script := fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "fork: %s"
|
||||
Write-Host "=== Fork from %s ===" -ForegroundColor Cyan
|
||||
cd "%s"
|
||||
claude -r %s --fork-session --permission-mode bypassPermissions`, title, title, cwd, s.ID)
|
||||
launchInWtWithTitle(cwd, label, script)
|
||||
} else {
|
||||
m.pendingCmd = fmt.Sprintf(`printf '\033]0;fork: %s\007' && echo "=== Fork from %s ===" && cd "%s" && claude -r %s --fork-session --permission-mode bypassPermissions`, title, title, cwd, s.ID)
|
||||
}
|
||||
}
|
||||
|
||||
m.launched = label
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// --- AI 摘要 ---
|
||||
|
||||
func (m *Model) applySummary(sessionID, summary string, completed, pending []string) {
|
||||
@@ -377,7 +478,7 @@ func (m *Model) applySummary(sessionID, summary string, completed, pending []str
|
||||
s.AISummary = summary
|
||||
s.Completed = completed
|
||||
s.Pending = pending
|
||||
saveSummaryToCache(sessionID, summary, completed, pending)
|
||||
updateSummaryInCache(m.history.Cache, sessionID, summary, completed, pending)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -542,6 +643,9 @@ func (m *Model) render() string {
|
||||
if m.inputBuf != "" {
|
||||
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ▶ num:%s [Enter]go [Esc]cancel", m.inputBuf)))
|
||||
}
|
||||
if m.forkMode {
|
||||
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ⎇ fork:%s [Enter]confirm [Esc]cancel", m.forkBuf)))
|
||||
}
|
||||
if m.launched != "" {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Render(fmt.Sprintf(" ✓ %s", m.launched)))
|
||||
m.launched = ""
|
||||
@@ -585,6 +689,7 @@ func (m *Model) renderHelp() []string {
|
||||
m.fmtHelp("Enter", "resume"),
|
||||
m.fmtHelp("f", "star"),
|
||||
m.fmtHelp("n", "new"),
|
||||
m.fmtHelp("c", "fork"),
|
||||
m.fmtHelp("1", "workspace"),
|
||||
m.fmtHelp("r/F5", "refresh"),
|
||||
m.fmtHelp("q", "quit"),
|
||||
@@ -1012,6 +1117,14 @@ func (m *Model) detailColumnLines(w, listH int) []string {
|
||||
|
||||
lines = append(lines, lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" [Enter]resume", w)))
|
||||
|
||||
if s.ForkFrom != "" {
|
||||
forkID := s.ForkFrom
|
||||
if len(forkID) > 8 {
|
||||
forkID = forkID[:8]
|
||||
}
|
||||
lines = append(lines, lipgloss.NewStyle().Foreground(style.Cyan).Render(fitWidth(fmt.Sprintf(" ⎇ fork from %s", forkID), w)))
|
||||
}
|
||||
|
||||
if len(lines) > listH {
|
||||
lines = lines[:listH]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user