- 原子写: 新增 atomicWriteFile,缓存/收藏/forks 改为临时文件+rename,避免崩溃损坏 - fork 关系: executeFork 记录待绑定项,扫描后按 cwd+时间窗口匹配新会话并持久化 - 摘要重试: 去掉 needDetail 反复刷新逻辑,仅对未生成摘要的会话触发,避免浪费 claude -p 调用 - 脚本转义: pwsh/bash 的 Prompt 回显段转义双引号与 $,防止注入和语法错误 - 输入限制: 工作空间数字输入限制 4 位,避免无限增长 - 品牌统一: 底部版本号文案 绝尘 → U-TABS
153 lines
3.8 KiB
Go
153 lines
3.8 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
)
|
|
|
|
// Model 主模型
|
|
type Model struct {
|
|
activeGroup int
|
|
cursor int
|
|
selected map[int]bool
|
|
inputBuf string
|
|
width int
|
|
height int
|
|
launched string
|
|
history HistoryState
|
|
pendingCmd string
|
|
update UpdateState
|
|
wsFocus int // workspace 焦点列: 0=tabs 1=list
|
|
wsTabCur int // tabs 列光标 (0..len(Groups), 最后一个是历史入口)
|
|
forkMode bool // 分叉输入模式
|
|
forkBuf string // 分叉方向提示输入
|
|
|
|
// 性能缓存
|
|
allSessionsCache []*Session // "全部" tab 会话切片缓存
|
|
allSessionsDirty bool // 缓存是否需要重建
|
|
cacheDirty bool // session-cache.json 是否需要写盘
|
|
summaryCount int // 本次会话已生成的摘要数
|
|
pendingForks []pendingFork // executeFork 留下的待绑定项,扫描后解析
|
|
}
|
|
|
|
func NewModel() *Model {
|
|
return &Model{
|
|
activeGroup: len(Groups), // 默认打开历史对话
|
|
selected: make(map[int]bool),
|
|
}
|
|
}
|
|
|
|
func (m *Model) Init() tea.Cmd {
|
|
m.update.Checking = true
|
|
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) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyPressMsg:
|
|
return m.handleKey(msg.String())
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
case ScanCompleteMsg:
|
|
m.history.Projects = msg.Projects
|
|
m.history.Cache = msg.Cache
|
|
m.history.Loaded = true
|
|
m.history.Scanning = false
|
|
m.allSessionsDirty = true
|
|
m.cacheDirty = true
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 处理 executeFork 留下的待绑定 fork
|
|
if len(m.pendingForks) > 0 {
|
|
resolvePendingForks(m.pendingForks, m.history.Projects)
|
|
m.pendingForks = nil
|
|
m.cacheDirty = true
|
|
}
|
|
total := 0
|
|
for _, pd := range msg.Projects {
|
|
total += len(pd.Sessions)
|
|
}
|
|
m.launched = fmt.Sprintf("refreshed: %d projects, %d sessions", len(msg.Projects), total)
|
|
if len(msg.UpdatedIDs) > 0 {
|
|
return m, m.nextSummaryCmd()
|
|
}
|
|
return m, nil
|
|
case SummaryResultMsg:
|
|
m.applySummary(msg.SessionID, msg.Summary, msg.Completed, msg.Pending)
|
|
m.cacheDirty = true
|
|
cmds := []tea.Cmd{m.nextSummaryCmd(), cacheFlushTick()}
|
|
return m, tea.Batch(cmds...)
|
|
case UpdateAvailableMsg:
|
|
m.update.Checking = false
|
|
m.update.Available = true
|
|
m.update.NewVersion = msg.NewVersion
|
|
m.update.Changelog = msg.Changelog
|
|
m.update.DownloadURL = msg.DownloadURL
|
|
m.update.SHA256 = msg.SHA256
|
|
m.update.FileSize = msg.FileSize
|
|
case UpdateCompleteMsg:
|
|
m.update.Updating = false
|
|
m.update.Done = true
|
|
m.update.Available = false
|
|
case UpdateErrorMsg:
|
|
m.update.Updating = false
|
|
m.update.Error = msg.Err
|
|
m.update.Checking = false
|
|
case cacheFlushMsg:
|
|
m.flushCache()
|
|
// 只在仍有脏数据时继续调度,避免空转
|
|
if m.cacheDirty {
|
|
return m, cacheFlushTick()
|
|
}
|
|
return m, nil
|
|
case tea.QuitMsg:
|
|
// 最终落盘:扫描未完成时 Cache 可能为 nil,此时无数据可写,
|
|
// 已生成的摘要会在下次启动扫描时由 cache 维护
|
|
m.flushCache()
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *Model) GetPendingCmd() string {
|
|
return m.pendingCmd
|
|
}
|
|
|
|
func (m *Model) flushCache() {
|
|
if !m.cacheDirty || m.history.Cache == nil {
|
|
return
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return
|
|
}
|
|
saveCache(home, m.history.Cache)
|
|
m.cacheDirty = false
|
|
}
|
|
|
|
func cacheFlushTick() tea.Cmd {
|
|
return tea.Tick(3*time.Second, func(_ time.Time) tea.Msg {
|
|
return cacheFlushMsg{}
|
|
})
|
|
}
|