新增: 历史会话标题自定义编辑与摘要质量修复
- 标题自定义编辑:HISTORY 详情按 e 编辑标题,UserTitle 持久化到缓存且最高优先级 - AI 摘要质量修复:困惑回复检测词表收紧,消息提取过滤噪音,修复 JSON 包裹困惑回复漏网 - 数据维护命令:--fix-titles 清理低质量摘要,--regen-summaries 并发生成 - 修复代码审查 9 个问题:退格 UTF-8 损坏、中文输入、flush 调度、误杀合法摘要、 按键全局拦截、UserTitle 未同步窗口标题、wt 启动回落、cmd 注入防护、前缀误隐藏会话 - 启动链 wt.exe 回退与 cmd start 标题转义 - gofmt 规范化,新增 launch_test.go
This commit is contained in:
+51
-18
@@ -3,6 +3,7 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
@@ -10,20 +11,22 @@ import (
|
|||||||
|
|
||||||
// Model 主模型
|
// Model 主模型
|
||||||
type Model struct {
|
type Model struct {
|
||||||
activeGroup int
|
activeGroup int
|
||||||
cursor int
|
cursor int
|
||||||
selected map[int]bool
|
selected map[int]bool
|
||||||
inputBuf string
|
inputBuf string
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
launched string
|
launched string
|
||||||
history HistoryState
|
history HistoryState
|
||||||
pendingCmd string
|
pendingCmd string
|
||||||
update UpdateState
|
update UpdateState
|
||||||
wsFocus int // workspace 焦点列: 0=tabs 1=list
|
wsFocus int // workspace 焦点列: 0=tabs 1=list
|
||||||
wsTabCur int // tabs 列光标 (0..len(Groups), 最后一个是历史入口)
|
wsTabCur int // tabs 列光标 (0..len(Groups), 最后一个是历史入口)
|
||||||
forkMode bool // 分叉输入模式
|
forkMode bool // 分叉输入模式
|
||||||
forkBuf string // 分叉方向提示输入
|
forkBuf string // 分叉方向提示输入
|
||||||
|
editTitleMode bool // 编辑标题模式
|
||||||
|
editTitleBuf string // 标题编辑缓冲区
|
||||||
|
|
||||||
// 性能缓存
|
// 性能缓存
|
||||||
allSessionsCache []*Session // "全部" tab 会话切片缓存
|
allSessionsCache []*Session // "全部" tab 会话切片缓存
|
||||||
@@ -88,10 +91,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
total += len(pd.Sessions)
|
total += len(pd.Sessions)
|
||||||
}
|
}
|
||||||
m.launched = fmt.Sprintf("refreshed: %d projects, %d sessions", len(msg.Projects), total)
|
m.launched = fmt.Sprintf("refreshed: %d projects, %d sessions", len(msg.Projects), total)
|
||||||
if len(msg.UpdatedIDs) > 0 {
|
// 无论本次是否有文件更新,都尝试补齐缺失的摘要
|
||||||
return m, m.nextSummaryCmd()
|
return m, m.nextSummaryCmd()
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case SummaryResultMsg:
|
case SummaryResultMsg:
|
||||||
m.applySummary(msg.SessionID, msg.Summary, msg.Completed, msg.Pending)
|
m.applySummary(msg.SessionID, msg.Summary, msg.Completed, msg.Pending)
|
||||||
m.cacheDirty = true
|
m.cacheDirty = true
|
||||||
@@ -133,6 +134,38 @@ func (m *Model) GetPendingCmd() string {
|
|||||||
return m.pendingCmd
|
return m.pendingCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) confirmEditTitle() (*Model, tea.Cmd) {
|
||||||
|
s := m.currentSession()
|
||||||
|
title := strings.TrimSpace(m.editTitleBuf)
|
||||||
|
m.editTitleMode = false
|
||||||
|
m.editTitleBuf = ""
|
||||||
|
if s == nil || title == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
cmd := m.setUserTitle(s.ID, title)
|
||||||
|
m.launched = "rename: " + title
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) setUserTitle(sessionID, title string) tea.Cmd {
|
||||||
|
for _, pd := range m.history.Projects {
|
||||||
|
for _, s := range pd.Sessions {
|
||||||
|
if s.ID == sessionID {
|
||||||
|
s.UserTitle = title
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.history.Cache != nil {
|
||||||
|
if entry, ok := m.history.Cache[sessionID]; ok {
|
||||||
|
entry.UserTitle = title
|
||||||
|
m.cacheDirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 调度落盘,避免改名只存在内存、被磁盘旧缓存覆盖
|
||||||
|
return cacheFlushTick()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) flushCache() {
|
func (m *Model) flushCache() {
|
||||||
if !m.cacheDirty || m.history.Cache == nil {
|
if !m.cacheDirty || m.history.Cache == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
+1252
-76
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,11 @@ import tea "charm.land/bubbletea/v2"
|
|||||||
// --- 按键分发 ---
|
// --- 按键分发 ---
|
||||||
|
|
||||||
func (m *Model) handleKey(s string) (*Model, tea.Cmd) {
|
func (m *Model) handleKey(s string) (*Model, tea.Cmd) {
|
||||||
|
// 编辑标题时优先走输入处理,避免 q/1/2/u 等被全局拦截
|
||||||
|
if m.editTitleMode {
|
||||||
|
return m.handleHistoryKey(s)
|
||||||
|
}
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case "q", "ctrl+c":
|
case "q", "ctrl+c":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
@@ -17,6 +22,8 @@ func (m *Model) handleKey(s string) (*Model, tea.Cmd) {
|
|||||||
|
|
||||||
// Tab 快捷切换: 1=工作空间 2=历史对话
|
// Tab 快捷切换: 1=工作空间 2=历史对话
|
||||||
if s == "1" {
|
if s == "1" {
|
||||||
|
m.editTitleMode = false
|
||||||
|
m.editTitleBuf = ""
|
||||||
if m.IsHistoryTab() {
|
if m.IsHistoryTab() {
|
||||||
m.activeGroup = 0
|
m.activeGroup = 0
|
||||||
m.resetCursor()
|
m.resetCursor()
|
||||||
@@ -117,6 +124,9 @@ func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
|
|||||||
if m.forkMode {
|
if m.forkMode {
|
||||||
return m.handleForkInput(s)
|
return m.handleForkInput(s)
|
||||||
}
|
}
|
||||||
|
if m.editTitleMode {
|
||||||
|
return m.handleEditTitleInput(s)
|
||||||
|
}
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
@@ -159,6 +169,13 @@ func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case "f":
|
case "f":
|
||||||
m.toggleFavorite()
|
m.toggleFavorite()
|
||||||
|
case "e", "E":
|
||||||
|
if m.currentSession() != nil && !m.forkMode {
|
||||||
|
s := m.currentSession()
|
||||||
|
m.editTitleMode = true
|
||||||
|
m.editTitleBuf = s.DisplayTitle()
|
||||||
|
m.history.FocusPanel = 1 // 确保右栏可见
|
||||||
|
}
|
||||||
case "c":
|
case "c":
|
||||||
if m.currentSession() != nil && m.history.FocusPanel == 1 {
|
if m.currentSession() != nil && m.history.FocusPanel == 1 {
|
||||||
m.forkMode = true
|
m.forkMode = true
|
||||||
@@ -168,6 +185,32 @@ func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) handleEditTitleInput(s string) (*Model, tea.Cmd) {
|
||||||
|
switch s {
|
||||||
|
case "enter":
|
||||||
|
return m.confirmEditTitle()
|
||||||
|
case "esc":
|
||||||
|
m.editTitleMode = false
|
||||||
|
m.editTitleBuf = ""
|
||||||
|
case "backspace":
|
||||||
|
r := []rune(m.editTitleBuf)
|
||||||
|
if len(r) > 0 {
|
||||||
|
m.editTitleBuf = string(r[:len(r)-1])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// 编辑状态下忽略导航/功能键,避免 up/down/tab 等被当作字符输入
|
||||||
|
switch s {
|
||||||
|
case "up", "down", "left", "right", "tab", "shift+tab":
|
||||||
|
// 不处理
|
||||||
|
default:
|
||||||
|
if s != "" && s[0] >= 32 {
|
||||||
|
m.editTitleBuf += s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Model) handleForkInput(s string) (*Model, tea.Cmd) {
|
func (m *Model) handleForkInput(s string) (*Model, tea.Cmd) {
|
||||||
switch s {
|
switch s {
|
||||||
case "enter":
|
case "enter":
|
||||||
|
|||||||
+60
-23
@@ -3,6 +3,7 @@ package internal
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -114,42 +115,82 @@ func powershellExe() string {
|
|||||||
return "powershell"
|
return "powershell"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wtExe 返回 Windows Terminal 可执行文件路径;未安装/找不到时返回空串。
|
||||||
|
// 查找顺序:PATH 中的 wt.exe → 应用执行别名标准路径
|
||||||
|
// %LOCALAPPDATA%\Microsoft\WindowsApps\wt.exe(WindowsApps 常不在 PATH)。
|
||||||
|
// 找不到的常见原因:未安装 Windows Terminal(Win10 需 Store 安装)、
|
||||||
|
// 或 WindowsApps 执行别名被禁用。调用方据此回退到独立窗口启动。
|
||||||
|
func wtExe() string {
|
||||||
|
if p, err := exec.LookPath("wt.exe"); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
if local := os.Getenv("LOCALAPPDATA"); local != "" {
|
||||||
|
p := filepath.Join(local, "Microsoft", "WindowsApps", "wt.exe")
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func launchInWtWithTitle(dir, tabTitle, script string) {
|
func launchInWtWithTitle(dir, tabTitle, script string) {
|
||||||
encoded := encodePSCommand(script)
|
encoded := encodePSCommand(script)
|
||||||
color := fmt.Sprintf("#%02X%02X%02X", randRange(80, 255), randRange(80, 255), randRange(80, 255))
|
ps := powershellExe()
|
||||||
args := []string{"-w", "0", "-d", dir, "--tabColor", color}
|
|
||||||
if tabTitle != "" {
|
// 优先 Windows Terminal:在已有窗口(-w 0)里开新标签页
|
||||||
args = append(args, "--title", tabTitle)
|
if wt := wtExe(); wt != "" {
|
||||||
|
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, ps, "-NoExit", "-EncodedCommand", encoded)
|
||||||
|
if err := exec.Command(wt, args...).Start(); err == nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Printf("[u-tabs] launch fail (wt): %v, fallback to cmd start", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
args = append(args, powershellExe(), "-NoExit", "-EncodedCommand", encoded)
|
|
||||||
cmd := exec.Command("wt.exe", args...)
|
// 回退:未安装 Windows Terminal → cmd start 开独立 PowerShell 窗口
|
||||||
|
// start 语法:START ["title"] [options] command [args]
|
||||||
|
// 首个参数被引号包裹时才视为窗口标题,否则视为命令。
|
||||||
|
// Go 的 exec.Command 只在参数含空格时自动添加引号,
|
||||||
|
// 所以给 tabTitle 追加空格(如尚不含空格)来确保被引号包裹。
|
||||||
|
titleArg := tabTitle
|
||||||
|
// 转义标题中的 cmd 特殊字符,防止命令注入:
|
||||||
|
// 双引号全部移除;& | < > ^ 前加 ^(顺序:先 ^ 再其余)
|
||||||
|
titleArg = strings.ReplaceAll(titleArg, `"`, "")
|
||||||
|
titleArg = strings.ReplaceAll(titleArg, "^", "^^")
|
||||||
|
titleArg = strings.ReplaceAll(titleArg, "&", "^&")
|
||||||
|
titleArg = strings.ReplaceAll(titleArg, "|", "^|")
|
||||||
|
titleArg = strings.ReplaceAll(titleArg, "<", "^<")
|
||||||
|
titleArg = strings.ReplaceAll(titleArg, ">", "^>")
|
||||||
|
if titleArg != "" && !strings.ContainsAny(titleArg, " \t") {
|
||||||
|
titleArg += " " // 迫使 Go 自动加引号,窗口标题尾部空格不影响使用
|
||||||
|
}
|
||||||
|
args := []string{"/c", "start", titleArg, ps, "-NoExit", "-EncodedCommand", encoded}
|
||||||
|
cmd := exec.Command("cmd.exe", args...)
|
||||||
|
cmd.Dir = dir
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Printf("[u-tabs] launch fail: %v", err)
|
log.Printf("[u-tabs] launch fail (cmd): %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildResumeAction 构建恢复会话动作
|
// buildResumeAction 构建恢复会话动作
|
||||||
func buildResumeAction(s *Session) SessionAction {
|
func buildResumeAction(s *Session) SessionAction {
|
||||||
title := s.CustomTitle
|
|
||||||
if title == "" {
|
|
||||||
title = s.ID[:8]
|
|
||||||
}
|
|
||||||
return SessionAction{
|
return SessionAction{
|
||||||
Cwd: s.Cwd,
|
Cwd: s.Cwd,
|
||||||
Title: "resume: " + title,
|
Title: "resume: " + s.DisplayTitle(),
|
||||||
SessionID: s.ID,
|
SessionID: s.ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildForkAction 构建分叉会话动作
|
// buildForkAction 构建分叉会话动作
|
||||||
func buildForkAction(s *Session, prompt string) SessionAction {
|
func buildForkAction(s *Session, prompt string) SessionAction {
|
||||||
title := s.CustomTitle
|
|
||||||
if title == "" {
|
|
||||||
title = s.ID[:8]
|
|
||||||
}
|
|
||||||
return SessionAction{
|
return SessionAction{
|
||||||
Cwd: s.Cwd,
|
Cwd: s.Cwd,
|
||||||
Title: "fork: " + title,
|
Title: "fork: " + s.DisplayTitle(),
|
||||||
SessionID: s.ID,
|
SessionID: s.ID,
|
||||||
Fork: true,
|
Fork: true,
|
||||||
Prompt: prompt,
|
Prompt: prompt,
|
||||||
@@ -184,7 +225,7 @@ func (m *Model) resumeSelected() (*Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
action := buildResumeAction(s)
|
action := buildResumeAction(s)
|
||||||
action.Execute(m)
|
action.Execute(m)
|
||||||
m.launched = "resume: " + s.CustomTitle
|
m.launched = "resume: " + s.DisplayTitle()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +266,7 @@ func (m *Model) executeFork() (*Model, tea.Cmd) {
|
|||||||
At: time.Now(),
|
At: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
title := s.CustomTitle
|
m.launched = "fork: " + s.DisplayTitle()
|
||||||
if title == "" {
|
|
||||||
title = s.ID[:8]
|
|
||||||
}
|
|
||||||
m.launched = "fork: " + title
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestWtExeNeverReturnsBareName 锁定 wtExe 的契约:要么返回真实路径,要么返回空串,
|
||||||
|
// 绝不能返回裸 "wt.exe"。
|
||||||
|
// 裸 "wt.exe" 在未安装 Windows Terminal 的机器上会让 exec.Command 启动报
|
||||||
|
// `exec: "wt.exe": executable file not found in %PATH%`(v0.2.2 的崩溃根因)。
|
||||||
|
// 空串触发 launchInWtWithTitle 的 cmd /c start 独立窗口回退。
|
||||||
|
func TestWtExeNeverReturnsBareName(t *testing.T) {
|
||||||
|
got := wtExe()
|
||||||
|
if got == "wt.exe" {
|
||||||
|
t.Fatalf("wtExe() returned bare %q; must return full path or empty string", got)
|
||||||
|
}
|
||||||
|
if got != "" && !strings.HasSuffix(got, "wt.exe") {
|
||||||
|
t.Fatalf("wtExe() returned unexpected path %q", got)
|
||||||
|
}
|
||||||
|
t.Logf("wtExe() = %q (empty ⇒ wt absent → cmd fallback)", got)
|
||||||
|
}
|
||||||
@@ -19,12 +19,12 @@ var (
|
|||||||
Red = lipgloss.Color("#f7768e") // red
|
Red = lipgloss.Color("#f7768e") // red
|
||||||
|
|
||||||
// ── component styles ──
|
// ── component styles ──
|
||||||
TitleStyle = lipgloss.NewStyle().Bold(true).Foreground(Accent)
|
TitleStyle = lipgloss.NewStyle().Bold(true).Foreground(Accent)
|
||||||
SubtitleStyle = lipgloss.NewStyle().Foreground(Dim)
|
SubtitleStyle = lipgloss.NewStyle().Foreground(Dim)
|
||||||
|
|
||||||
TabActiveStyle = lipgloss.NewStyle().Bold(true).Background(BgPanel).Foreground(Bright).Padding(0, 1)
|
TabActiveStyle = lipgloss.NewStyle().Bold(true).Background(BgPanel).Foreground(Bright).Padding(0, 1)
|
||||||
TabInactiveStyle = lipgloss.NewStyle().Foreground(Dim).Padding(0, 1)
|
TabInactiveStyle = lipgloss.NewStyle().Foreground(Dim).Padding(0, 1)
|
||||||
TabSep = lipgloss.NewStyle().Foreground(BgPanel)
|
TabSep = lipgloss.NewStyle().Foreground(BgPanel)
|
||||||
|
|
||||||
SelStyle = lipgloss.NewStyle().Foreground(BgDark).Background(Purple).Bold(true)
|
SelStyle = lipgloss.NewStyle().Foreground(BgDark).Background(Purple).Bold(true)
|
||||||
NormStyle = lipgloss.NewStyle().Foreground(Fg)
|
NormStyle = lipgloss.NewStyle().Foreground(Fg)
|
||||||
|
|||||||
+5
-5
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Version 当前版本,发布时更新
|
// Version 当前版本,发布时更新
|
||||||
const Version = "0.2.2"
|
const Version = "0.2.3"
|
||||||
|
|
||||||
// --- 远程 JSON 结构 ---
|
// --- 远程 JSON 结构 ---
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@ type platformInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type versionInfo struct {
|
type versionInfo struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ReleaseDate string `json:"release_date"`
|
ReleaseDate string `json:"release_date"`
|
||||||
Changelog string `json:"changelog"`
|
Changelog string `json:"changelog"`
|
||||||
Platforms map[string]platformInfo `json:"platforms"`
|
Platforms map[string]platformInfo `json:"platforms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- bubbletea 消息类型 ---
|
// --- bubbletea 消息类型 ---
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ func (m *Model) renderHelp() []string {
|
|||||||
m.fmtHelp("←→", "panel"),
|
m.fmtHelp("←→", "panel"),
|
||||||
m.fmtHelp("Enter", "resume"),
|
m.fmtHelp("Enter", "resume"),
|
||||||
m.fmtHelp("f", "star"),
|
m.fmtHelp("f", "star"),
|
||||||
|
m.fmtHelp("e", "rename"),
|
||||||
m.fmtHelp("n", "new"),
|
m.fmtHelp("n", "new"),
|
||||||
m.fmtHelp("c", "fork"),
|
m.fmtHelp("c", "fork"),
|
||||||
m.fmtHelp("1", "workspace"),
|
m.fmtHelp("1", "workspace"),
|
||||||
|
|||||||
+12
-1
@@ -198,7 +198,18 @@ func (m *Model) detailColumnLines(w, listH int) []string {
|
|||||||
if m.history.Favorites[s.ID] {
|
if m.history.Favorites[s.ID] {
|
||||||
favPrefix = "★ "
|
favPrefix = "★ "
|
||||||
}
|
}
|
||||||
lines = append(lines, style.DetailTitle.Render(fitWidth(favPrefix+s.DisplayTitle(), w)))
|
// 标题行:编辑模式显示输入框,否则显示普通标题
|
||||||
|
if m.editTitleMode {
|
||||||
|
editLine := " ✏ " + m.editTitleBuf
|
||||||
|
editW := w - 4
|
||||||
|
if lipgloss.Width(editLine) > editW {
|
||||||
|
editLine = truncateByWidth(editLine, editW-2) + "…"
|
||||||
|
}
|
||||||
|
lines = append(lines, style.InputStyle.Render(fitWidth(editLine, w)))
|
||||||
|
lines = append(lines, style.DimFg.Render(fitWidth(" [Enter]save [Esc]cancel", w)))
|
||||||
|
} else {
|
||||||
|
lines = append(lines, style.DetailTitle.Render(fitWidth(favPrefix+s.DisplayTitle(), w)))
|
||||||
|
}
|
||||||
lines = append(lines, style.SepFg.Render(fitWidth(sepRunes(w), w)))
|
lines = append(lines, style.SepFg.Render(fitWidth(sepRunes(w), w)))
|
||||||
|
|
||||||
fav := "-"
|
fav := "-"
|
||||||
|
|||||||
@@ -11,6 +11,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// --fix-titles: 清理缓存中的低质量 AI 摘要(Claude 困惑回复/噪音/过短),
|
||||||
|
// 下次启动会重新扫描并重新生成。一次性数据维护命令,不进入 TUI。
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "--fix-titles" {
|
||||||
|
n := internal.CleanupBadTitles()
|
||||||
|
fmt.Printf("已清理 %d 个低质量摘要,重启 u-tabs 后将自动重新生成\n", n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --regen-summaries: 立即并发生成所有缺失的 AI 摘要(AISummary 为空的会话)。
|
||||||
|
// 通常在 --fix-titles 之后运行,把清理掉的标题重新解析回来。不进入 TUI。
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "--regen-summaries" {
|
||||||
|
ok, fail := internal.RegenerateSummaries(4)
|
||||||
|
fmt.Printf("摘要重新生成完成: 成功 %d, 降级 %d\n", ok, fail)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
internal.InitData()
|
internal.InitData()
|
||||||
m := internal.NewModel()
|
m := internal.NewModel()
|
||||||
p := tea.NewProgram(m, tea.WithFPS(0))
|
p := tea.NewProgram(m, tea.WithFPS(0))
|
||||||
|
|||||||
Reference in New Issue
Block a user