新增: 历史会话标题自定义编辑与摘要质量修复
- 标题自定义编辑: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:
+60
-23
@@ -3,6 +3,7 @@ package internal
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -114,42 +115,82 @@ func powershellExe() string {
|
||||
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) {
|
||||
encoded := encodePSCommand(script)
|
||||
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)
|
||||
ps := powershellExe()
|
||||
|
||||
// 优先 Windows Terminal:在已有窗口(-w 0)里开新标签页
|
||||
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 {
|
||||
log.Printf("[u-tabs] launch fail: %v", err)
|
||||
log.Printf("[u-tabs] launch fail (cmd): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildResumeAction 构建恢复会话动作
|
||||
func buildResumeAction(s *Session) SessionAction {
|
||||
title := s.CustomTitle
|
||||
if title == "" {
|
||||
title = s.ID[:8]
|
||||
}
|
||||
return SessionAction{
|
||||
Cwd: s.Cwd,
|
||||
Title: "resume: " + title,
|
||||
Title: "resume: " + s.DisplayTitle(),
|
||||
SessionID: s.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// buildForkAction 构建分叉会话动作
|
||||
func buildForkAction(s *Session, prompt string) SessionAction {
|
||||
title := s.CustomTitle
|
||||
if title == "" {
|
||||
title = s.ID[:8]
|
||||
}
|
||||
return SessionAction{
|
||||
Cwd: s.Cwd,
|
||||
Title: "fork: " + title,
|
||||
Title: "fork: " + s.DisplayTitle(),
|
||||
SessionID: s.ID,
|
||||
Fork: true,
|
||||
Prompt: prompt,
|
||||
@@ -184,7 +225,7 @@ func (m *Model) resumeSelected() (*Model, tea.Cmd) {
|
||||
}
|
||||
action := buildResumeAction(s)
|
||||
action.Execute(m)
|
||||
m.launched = "resume: " + s.CustomTitle
|
||||
m.launched = "resume: " + s.DisplayTitle()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -225,11 +266,7 @@ func (m *Model) executeFork() (*Model, tea.Cmd) {
|
||||
At: time.Now(),
|
||||
})
|
||||
|
||||
title := s.CustomTitle
|
||||
if title == "" {
|
||||
title = s.ID[:8]
|
||||
}
|
||||
m.launched = "fork: " + title
|
||||
m.launched = "fork: " + s.DisplayTitle()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user