优化: 界面布局对齐与时区修复
This commit is contained in:
@@ -27,6 +27,8 @@ type Session struct {
|
||||
FirstMsg string // 首条用户消息 (截断)
|
||||
AwaySummary string // away_summary 系统摘要
|
||||
AISummary string // AI 生成的摘要 (缓存)
|
||||
Completed []string // AI 提取的已完成项
|
||||
Pending []string // AI 提取的待办项
|
||||
FilePath string // JSONL 文件完整路径
|
||||
}
|
||||
|
||||
@@ -39,6 +41,10 @@ type ProjectDir struct {
|
||||
|
||||
// DisplayTitle 返回会话显示标题
|
||||
func (s *Session) DisplayTitle() string {
|
||||
return cleanText(s.displayTitleRaw())
|
||||
}
|
||||
|
||||
func (s *Session) displayTitleRaw() string {
|
||||
if s.AISummary != "" {
|
||||
return s.AISummary
|
||||
}
|
||||
@@ -53,12 +59,27 @@ func (s *Session) DisplayTitle() string {
|
||||
|
||||
// DisplaySummary 返回会话摘要
|
||||
func (s *Session) DisplaySummary() string {
|
||||
return cleanText(s.displaySummaryRaw())
|
||||
}
|
||||
|
||||
func (s *Session) displaySummaryRaw() string {
|
||||
if s.AwaySummary != "" {
|
||||
return s.AwaySummary
|
||||
}
|
||||
return s.FirstMsg
|
||||
}
|
||||
|
||||
// cleanText 清理显示文本:多空格保留一个,换行转空格
|
||||
func cleanText(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
s = strings.ReplaceAll(s, "\t", " ")
|
||||
for strings.Contains(s, " ") {
|
||||
s = strings.ReplaceAll(s, " ", " ")
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// HistoryState HISTORY Tab 视图状态
|
||||
type HistoryState struct {
|
||||
Projects []*ProjectDir
|
||||
@@ -77,28 +98,31 @@ func (m *Model) IsHistoryTab() bool {
|
||||
// --- 扫描 ---
|
||||
|
||||
type ScanCompleteMsg struct {
|
||||
Projects []*ProjectDir
|
||||
Projects []*ProjectDir
|
||||
UpdatedIDs map[string]bool // modTime 变化的会话,需要重新生成摘要
|
||||
}
|
||||
|
||||
func ScanSessionsCmd() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return ScanCompleteMsg{Projects: scanAllProjects()}
|
||||
projects, updated := scanAllProjects()
|
||||
return ScanCompleteMsg{Projects: projects, UpdatedIDs: updated}
|
||||
}
|
||||
}
|
||||
|
||||
func scanAllProjects() []*ProjectDir {
|
||||
func scanAllProjects() ([]*ProjectDir, map[string]bool) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
projectsDir := filepath.Join(home, ".claude", "projects")
|
||||
entries, err := os.ReadDir(projectsDir)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cache := loadCache(home)
|
||||
dirMap := make(map[string]*ProjectDir)
|
||||
updatedIDs := make(map[string]bool)
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
@@ -129,6 +153,11 @@ func scanAllProjects() []*ProjectDir {
|
||||
if session == nil || session.Cwd == "" {
|
||||
continue
|
||||
}
|
||||
// 清除旧摘要,标记需要重新生成
|
||||
session.AISummary = ""
|
||||
session.Completed = nil
|
||||
session.Pending = nil
|
||||
updatedIDs[sessionID] = true
|
||||
addSessionToDir(dirMap, session)
|
||||
cache[sessionID] = cacheEntryFrom(session, info.ModTime())
|
||||
}
|
||||
@@ -146,7 +175,7 @@ func scanAllProjects() []*ProjectDir {
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Dir < result[j].Dir
|
||||
})
|
||||
return result
|
||||
return result, updatedIDs
|
||||
}
|
||||
|
||||
func addSessionToDir(dirMap map[string]*ProjectDir, s *Session) {
|
||||
@@ -174,18 +203,22 @@ func scanSessionFile(path string, sessionID string) *Session {
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
|
||||
|
||||
lineCount := 0
|
||||
firstMsgFound := false
|
||||
scannedLines := 0 // 有效行(含 type 的)
|
||||
|
||||
for scanner.Scan() {
|
||||
lineCount++
|
||||
line := scanner.Bytes()
|
||||
|
||||
// 前 20 行内出现 queue-operation → claude -p 会话,跳过
|
||||
if lineCount <= 20 && matchType(line, "queue-operation") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !hasType(line) {
|
||||
continue
|
||||
}
|
||||
scannedLines++
|
||||
|
||||
// cwd — 统一为正斜杠
|
||||
if s.Cwd == "" {
|
||||
@@ -230,13 +263,6 @@ func scanSessionFile(path string, sessionID string) *Session {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 按 scannedLines/lineCount 比例估算总消息数
|
||||
if lineCount > scanLineLimit && scannedLines > 0 {
|
||||
ratio := float64(lineCount) / float64(scanLineLimit)
|
||||
s.MsgCount = int(float64(s.MsgCount) * ratio)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -287,7 +313,7 @@ func extractUserMsg(line []byte) string {
|
||||
region := line[searchStart:]
|
||||
if tidx := bytes.Index(region, []byte(`"text":"`)); tidx >= 0 {
|
||||
var text string
|
||||
if extractJSONString(region[tidx+6:], &text) && text != "" &&
|
||||
if extractJSONString(region[tidx+7:], &text) && text != "" &&
|
||||
!strings.HasPrefix(text, "Base directory") {
|
||||
return text
|
||||
}
|
||||
@@ -331,7 +357,7 @@ func findStringEnd(data []byte) int {
|
||||
|
||||
// --- 缓存 ---
|
||||
|
||||
const cacheVersion = 1
|
||||
const cacheVersion = 2
|
||||
|
||||
type cacheFile struct {
|
||||
Version int `json:"version"`
|
||||
@@ -348,6 +374,8 @@ type cacheEntry struct {
|
||||
FirstMsg string `json:"firstMsg"`
|
||||
AwaySummary string `json:"awaySummary"`
|
||||
AISummary string `json:"aiSummary"`
|
||||
Completed []string `json:"completed"`
|
||||
Pending []string `json:"pending"`
|
||||
FilePath string `json:"filePath"`
|
||||
}
|
||||
|
||||
@@ -357,6 +385,7 @@ func (e *cacheEntry) ToSession(id string) *Session {
|
||||
StartTime: e.StartTime, EndTime: e.EndTime,
|
||||
MsgCount: e.MsgCount, FirstMsg: e.FirstMsg,
|
||||
AwaySummary: e.AwaySummary, AISummary: e.AISummary,
|
||||
Completed: e.Completed, Pending: e.Pending,
|
||||
FilePath: e.FilePath,
|
||||
}
|
||||
}
|
||||
@@ -367,6 +396,7 @@ func cacheEntryFrom(s *Session, modTime time.Time) *cacheEntry {
|
||||
StartTime: s.StartTime, EndTime: s.EndTime,
|
||||
MsgCount: s.MsgCount, FirstMsg: s.FirstMsg,
|
||||
AwaySummary: s.AwaySummary, AISummary: s.AISummary,
|
||||
Completed: s.Completed, Pending: s.Pending,
|
||||
FilePath: s.FilePath,
|
||||
}
|
||||
}
|
||||
@@ -416,39 +446,63 @@ func truncStr(s string, maxRunes int) string {
|
||||
type SummaryResultMsg struct {
|
||||
SessionID string
|
||||
Summary string
|
||||
Completed []string
|
||||
Pending []string
|
||||
}
|
||||
|
||||
func generateSummaryCmd(filePath, sessionID string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
summary := generateAISummary(filePath)
|
||||
return SummaryResultMsg{SessionID: sessionID, Summary: summary}
|
||||
sum, done, todo := generateAISummary(filePath)
|
||||
return SummaryResultMsg{SessionID: sessionID, Summary: sum, Completed: done, Pending: todo}
|
||||
}
|
||||
}
|
||||
|
||||
func generateAISummary(filePath string) string {
|
||||
type aiSummaryResult struct {
|
||||
Summary string `json:"summary"`
|
||||
Completed []string `json:"completed"`
|
||||
Pending []string `json:"pending"`
|
||||
}
|
||||
|
||||
func generateAISummary(filePath string) (string, []string, []string) {
|
||||
messages := extractMessagesForSummary(filePath)
|
||||
if messages == "" {
|
||||
return ""
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
prompt := "请用20字以内中文概括以下对话的主题,只输出概括文字,不加引号、标点或markdown格式:\n\n" + messages
|
||||
prompt := `分析以下Claude Code对话,用JSON输出:
|
||||
{"summary":"20字内中文概括","completed":["已完成项1","已完成项2"],"pending":["待办项1"]}
|
||||
要求:summary 20字内概括;completed 已完成工作(每项≤30字);pending 未完成/待办(每项≤30字)。只输出JSON。` + "\n\n" + messages
|
||||
|
||||
cmd := exec.CommandContext(ctx, "claude", "-p", prompt)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
summary := strings.TrimSpace(string(output))
|
||||
summary = strings.Trim(summary, "\"'`*")
|
||||
summary = strings.TrimPrefix(summary, "概括:")
|
||||
summary = strings.TrimPrefix(summary, "主题:")
|
||||
if len(summary) > 60 {
|
||||
summary = truncStr(summary, 60)
|
||||
raw := strings.TrimSpace(string(output))
|
||||
raw = strings.Trim(raw, "`")
|
||||
raw = strings.TrimPrefix(raw, "json")
|
||||
raw = strings.TrimSpace(raw)
|
||||
|
||||
var result aiSummaryResult
|
||||
if json.Unmarshal([]byte(raw), &result) != nil {
|
||||
// Fallback: treat whole output as summary
|
||||
s := strings.Trim(raw, "\"'`*")
|
||||
s = strings.TrimPrefix(s, "概括:")
|
||||
s = strings.TrimPrefix(s, "主题:")
|
||||
if len(s) > 60 {
|
||||
s = truncStr(s, 60)
|
||||
}
|
||||
return s, nil, nil
|
||||
}
|
||||
return summary
|
||||
|
||||
if len(result.Summary) > 60 {
|
||||
result.Summary = truncStr(result.Summary, 60)
|
||||
}
|
||||
return result.Summary, result.Completed, result.Pending
|
||||
}
|
||||
|
||||
func extractMessagesForSummary(path string) string {
|
||||
@@ -482,7 +536,7 @@ func extractMessagesForSummary(path string) string {
|
||||
return strings.Join(msgs, "\n")
|
||||
}
|
||||
|
||||
func saveSummaryToCache(sessionID, summary string) {
|
||||
func saveSummaryToCache(sessionID, summary string, completed, pending []string) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -493,5 +547,7 @@ func saveSummaryToCache(sessionID, summary string) {
|
||||
return
|
||||
}
|
||||
entry.AISummary = summary
|
||||
entry.Completed = completed
|
||||
entry.Pending = pending
|
||||
saveCache(home, cache)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user