修复: 缓存原子写与 fork 关系持久化,优化摘要重试
- 原子写: 新增 atomicWriteFile,缓存/收藏/forks 改为临时文件+rename,避免崩溃损坏 - fork 关系: executeFork 记录待绑定项,扫描后按 cwd+时间窗口匹配新会话并持久化 - 摘要重试: 去掉 needDetail 反复刷新逻辑,仅对未生成摘要的会话触发,避免浪费 claude -p 调用 - 脚本转义: pwsh/bash 的 Prompt 回显段转义双引号与 $,防止注入和语法错误 - 输入限制: 工作空间数字输入限制 4 位,避免无限增长 - 品牌统一: 底部版本号文案 绝尘 → U-TABS
This commit is contained in:
+81
-12
@@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"charm.land/bubbletea/v2"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
// Session 单个会话的元数据
|
||||
@@ -414,9 +414,9 @@ func findStringEnd(data []byte) int {
|
||||
const cacheVersion = 2
|
||||
|
||||
type cacheFile struct {
|
||||
Version int `json:"version"`
|
||||
Sessions map[string]*cacheEntry `json:"sessions"`
|
||||
ProjectTimes map[string]time.Time `json:"projectTimes"` // 项目目录 modTime
|
||||
Version int `json:"version"`
|
||||
Sessions map[string]*cacheEntry `json:"sessions"`
|
||||
ProjectTimes map[string]time.Time `json:"projectTimes"` // 项目目录 modTime
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
@@ -497,7 +497,7 @@ func saveCacheWithTimes(home string, m map[string]*cacheEntry, projTimes map[str
|
||||
log.Printf("[u-tabs] cache mkdir fail: %v", err)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(cachePath(home), data, 0o644); err != nil {
|
||||
if err := atomicWriteFile(cachePath(home), data, 0o644); err != nil {
|
||||
log.Printf("[u-tabs] cache write fail: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -666,7 +666,7 @@ func saveFavorites(favs map[string]bool) {
|
||||
log.Printf("[u-tabs] favorites mkdir fail: %v", err)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(favoritesPath(home), data, 0o644); err != nil {
|
||||
if err := atomicWriteFile(favoritesPath(home), data, 0o644); err != nil {
|
||||
log.Printf("[u-tabs] favorites write fail: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -701,6 +701,14 @@ func forksPath(home string) string {
|
||||
return filepath.Join(home, ".u-tabs", "forks.json")
|
||||
}
|
||||
|
||||
// pendingFork 记录一次待绑定的 fork:启动 fork 后新会话 ID 未知,
|
||||
// 等下次扫描时按 cwd + 时间窗口匹配出新建会话再建立关系。
|
||||
type pendingFork struct {
|
||||
SourceID string
|
||||
Cwd string
|
||||
At time.Time
|
||||
}
|
||||
|
||||
func loadForks() map[string]string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -728,9 +736,70 @@ func recordFork(targetID, sourceID string) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(home, ".u-tabs")
|
||||
os.MkdirAll(dir, 0o755)
|
||||
os.WriteFile(forksPath(home), data, 0o644)
|
||||
if err := atomicWriteFile(forksPath(home), data, 0o644); err != nil {
|
||||
log.Printf("[u-tabs] forks write fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePendingForks 处理 executeFork 留下的待绑定项:
|
||||
// 在最新扫描结果中,找出 cwd 匹配且 StartTime 晚于记录时刻的新会话,
|
||||
// 将其作为 fork 目标记录关系到 forks.json。
|
||||
// 匹配窗口 10 分钟,避免时钟偏差;同一 cwd 只匹配最早的一条新会话。
|
||||
func resolvePendingForks(pending []pendingFork, projects []*ProjectDir) {
|
||||
if len(pending) == 0 {
|
||||
return
|
||||
}
|
||||
forks := loadForks()
|
||||
changed := false
|
||||
for _, p := range pending {
|
||||
// 已绑过则跳过
|
||||
matched := ""
|
||||
for _, pd := range projects {
|
||||
if normalizeCwd(pd.Dir) != normalizeCwd(p.Cwd) {
|
||||
continue
|
||||
}
|
||||
for _, s := range pd.Sessions {
|
||||
if s.StartTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
// 新会话起始时间晚于 fork 时刻(容许 2 秒时钟偏差)
|
||||
if !s.StartTime.Before(p.At.Add(-2 * time.Second)) {
|
||||
if s.ID == p.SourceID {
|
||||
continue
|
||||
}
|
||||
if matched == "" || s.StartTime.Before(findSessionStart(projects, matched)) {
|
||||
matched = s.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if matched != "" {
|
||||
forks[matched] = p.SourceID
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
data, _ := json.Marshal(forks)
|
||||
atomicWriteFile(forksPath(home), data, 0o644)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCwd(s string) string {
|
||||
return strings.ReplaceAll(s, "\\", "/")
|
||||
}
|
||||
|
||||
func findSessionStart(projects []*ProjectDir, id string) time.Time {
|
||||
for _, pd := range projects {
|
||||
for _, s := range pd.Sessions {
|
||||
if s.ID == id {
|
||||
return s.StartTime
|
||||
}
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// --- AI 摘要与收藏操作 ---
|
||||
@@ -766,9 +835,9 @@ func (m *Model) nextSummaryCmd() tea.Cmd {
|
||||
}
|
||||
for _, pd := range m.history.Projects {
|
||||
for _, s := range pd.Sessions {
|
||||
needSummary := s.AISummary == "" && s.FirstMsg != ""
|
||||
needDetail := s.AISummary != "" && len(s.Completed) == 0 && len(s.Pending) == 0
|
||||
if (needSummary || needDetail) && s.FilePath != "" {
|
||||
// 仅对从未生成过摘要的会话生成,避免对已有摘要的会话反复重试
|
||||
// (fallback 路径已用 FirstMsg/ID 充充 AISummary,标记为已生成)
|
||||
if s.AISummary == "" && s.FirstMsg != "" && s.FilePath != "" {
|
||||
m.summaryCount++
|
||||
return generateSummaryCmd(s.FilePath, s.ID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user