优化: 界面布局对齐与时区修复

This commit is contained in:
2026-05-18 17:28:40 +08:00
parent 52fa702e66
commit 401713fc5a
3 changed files with 482 additions and 206 deletions

View File

@@ -7,7 +7,9 @@ import (
"log"
"math/big"
"os/exec"
"runtime"
"strconv"
"path/filepath"
"strings"
"unicode/utf16"
"unicode/utf8"
@@ -27,6 +29,7 @@ type Model struct {
height int
launched string
history HistoryState
pendingCmd string // Linux: 退出后执行的命令
}
func NewModel() *Model {
@@ -56,9 +59,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.history.Projects = msg.Projects
m.history.Loaded = true
m.history.Scanning = false
return m, m.nextSummaryCmd()
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)
m.applySummary(msg.SessionID, msg.Summary, msg.Completed, msg.Pending)
return m, m.nextSummaryCmd()
}
return m, nil
@@ -168,6 +179,11 @@ func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
} else {
return m.resumeSelected()
}
case "n":
return m.newSessionFromHistory()
case "r", "f5":
m.history.Scanning = true
return m, ScanSessionsCmd()
}
return m, nil
}
@@ -177,7 +193,7 @@ func (m *Model) moveHistoryDir(dir int) {
return
}
m.history.DirCursor += dir
maxDir := len(m.history.Projects) // 含"全部"
maxDir := len(m.history.Projects) + 1 // 含"全部"
if m.history.DirCursor < 0 {
m.history.DirCursor = maxDir - 1
}
@@ -233,15 +249,47 @@ func (m *Model) resumeSelected() (*Model, tea.Cmd) {
if s == nil {
return m, nil
}
go resumeSession(s)
if runtime.GOOS == "windows" {
go resumeSession(s)
} else {
cwd := s.Cwd
title := s.CustomTitle
if title == "" {
title = s.ID[:8]
}
m.pendingCmd = fmt.Sprintf(`printf '\033]0;resume: %s\007' && echo "=== Resuming: %s ===" && cd "%s" && claude -r %s`, title, title, cwd, s.ID)
}
m.launched = "resume: " + s.CustomTitle
return m, nil
}
func (m *Model) newSessionFromHistory() (*Model, tea.Cmd) {
s := m.currentSession()
if s == nil {
return m, nil
}
cwd := s.Cwd
if runtime.GOOS == "windows" {
cwd = strings.ReplaceAll(cwd, "/", "\\")
}
title := filepath.Base(cwd) + " - new"
if runtime.GOOS == "windows" {
script := fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "%s"
Write-Host "=== New Session @ %s ===" -ForegroundColor Cyan
cd "%s"
claude --permission-mode bypassPermissions`, title, cwd, cwd)
launchInWtWithTitle(cwd, title, script)
} else {
m.pendingCmd = fmt.Sprintf(`printf '\033]0;%s\007' && echo "=== New Session @ %s ===" && cd "%s" && claude --permission-mode bypassPermissions`, title, cwd, cwd)
}
m.launched = "new: " + filepath.Base(cwd)
return m, nil
}
// --- AI 摘要 ---
func (m *Model) applySummary(sessionID, summary string) {
func (m *Model) applySummary(sessionID, summary string, completed, pending []string) {
if summary == "" {
return
}
@@ -249,7 +297,9 @@ func (m *Model) applySummary(sessionID, summary string) {
for _, s := range pd.Sessions {
if s.ID == sessionID {
s.AISummary = summary
saveSummaryToCache(sessionID, summary)
s.Completed = completed
s.Pending = pending
saveSummaryToCache(sessionID, summary, completed, pending)
return
}
}
@@ -259,7 +309,9 @@ func (m *Model) applySummary(sessionID, summary string) {
func (m *Model) nextSummaryCmd() tea.Cmd {
for _, pd := range m.history.Projects {
for _, s := range pd.Sessions {
if s.AISummary == "" && s.FilePath != "" && s.FirstMsg != "" {
needSummary := s.AISummary == "" && s.FirstMsg != ""
needDetail := s.AISummary != "" && len(s.Completed) == 0 && len(s.Pending) == 0
if (needSummary || needDetail) && s.FilePath != "" {
return generateSummaryCmd(s.FilePath, s.ID)
}
}
@@ -299,7 +351,7 @@ func (m *Model) launchSelected() (*Model, tea.Cmd) {
var launched []string
for idx := range m.selected {
ws := &AllWorkspaces[idx]
go launchWorkspace(*ws)
m.LaunchWorkspace(*ws)
launched = append(launched, ws.Title)
}
m.launched = strings.Join(launched, ", ")
@@ -309,7 +361,7 @@ func (m *Model) launchSelected() (*Model, tea.Cmd) {
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
if m.cursor < len(svcs) {
ws := svcs[m.cursor]
go launchWorkspace(ws)
m.LaunchWorkspace(ws)
m.launched = ws.Title
}
return m, nil
@@ -323,7 +375,7 @@ func (m *Model) launchByInput() (*Model, tea.Cmd) {
}
ws := FindByNumber(num)
if ws != nil {
go launchWorkspace(*ws)
m.LaunchWorkspace(*ws)
m.launched = ws.Title
}
m.inputBuf = ""
@@ -362,7 +414,7 @@ func (m *Model) render() string {
if ok {
b.WriteString(lipgloss.NewStyle().
Bold(true).Background(style.BgPanel).Foreground(gs.GetForeground()).
Padding(0, 1).Render(label))
Render(label))
} else {
b.WriteString(style.TabActiveStyle.Render(label))
}
@@ -378,7 +430,7 @@ func (m *Model) render() string {
gs := style.GroupStyles["HISTORY"]
b.WriteString(lipgloss.NewStyle().
Bold(true).Background(style.BgPanel).Foreground(gs.GetForeground()).
Padding(0, 1).Render(histLabel))
Render(histLabel))
} else {
b.WriteString(style.TabInactiveStyle.Render(histLabel))
}
@@ -420,7 +472,9 @@ func (m *Model) renderHelp() []string {
m.fmtHelp("j/k", "sel"),
m.fmtHelp("Tab", "→panel"),
m.fmtHelp("Enter", "resume"),
m.fmtHelp("1-"+fmt.Sprintf("%d", m.totalTabs()), "tab"),
m.fmtHelp("n", "new"),
m.fmtHelp("1-"+fmt.Sprintf("%d", m.totalTabs()), "tab"),
m.fmtHelp("r/F5", "refresh"),
m.fmtHelp("q", "quit"),
}
}
@@ -446,10 +500,24 @@ func (m *Model) renderWorkspace() string {
return style.SubtitleStyle.Render(" empty")
}
// 预留 header(2) + footer(2),面板边框+标题占 3 行
availH := max(3, m.height-4-3)
start, end := viewport(m.cursor, len(svcs), availH)
// 左栏: 列表
var left strings.Builder
innerW := listW - 4
for i, ws := range svcs {
// 计算 title 最大宽度用于对齐
maxTitleW := 0
for _, ws := range svcs {
if tw := stringWidth(ws.Title); tw > maxTitleW {
maxTitleW = tw
}
}
for i := start; i < end; i++ {
ws := svcs[i]
cur := " "
if i == m.cursor {
cur = "▸"
@@ -459,11 +527,13 @@ func (m *Model) renderWorkspace() string {
mark = "✓"
}
paddedTitle := padRightByWidth(ws.Title, maxTitleW)
if i == m.cursor {
// 选中行:纯文本,让 SelStyle 统一着色
prefix := cur + " " + mark + " " + fmt.Sprintf("%02d", ws.N) + " "
remainW := max(10, innerW-len(prefix))
text := truncateByWidth(ws.Title+" "+ws.Prompt, remainW)
text := truncateByWidth(paddedTitle+" "+ws.Prompt, remainW)
left.WriteString(style.SelStyle.Width(innerW).Render(prefix + text))
} else {
// 非选中行:子样式着色
@@ -474,7 +544,7 @@ func (m *Model) renderWorkspace() string {
num := style.NumStyle.Render(fmt.Sprintf("%02d", ws.N))
prefix := cur + " " + markStr + " " + num + " "
remainW := max(10, innerW-lipgloss.Width(prefix))
text := truncateByWidth(ws.Title+" "+ws.Prompt, remainW)
text := truncateByWidth(paddedTitle+" "+ws.Prompt, remainW)
left.WriteString(style.NormStyle.Render(prefix + text))
}
left.WriteString("\n")
@@ -519,7 +589,11 @@ func (m *Model) renderWorkspace() string {
right.WriteString("\n")
right.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(" " + strings.Repeat("─", detailW-6)))
right.WriteString("\n")
right.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Italic(true).Render(" $ wt → CC @" + ws.Title))
hintCmd := "wt"
if runtime.GOOS != "windows" {
hintCmd = "bash"
}
right.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Italic(true).Render(" $ "+hintCmd+" → CC @"+ws.Title))
right.WriteString("\n")
right.WriteString(lipgloss.NewStyle().Foreground(style.Dim).Render(" [Enter]start [Space]multi"))
} else {
@@ -539,174 +613,224 @@ func (m *Model) renderWorkspace() string {
// --- HISTORY 三栏渲染 ---
func (m *Model) renderHistory() string {
if m.history.Scanning {
return style.ScanningStyle.Render(" scanning sessions...")
}
if !m.history.Loaded {
if !m.history.Loaded && !m.history.Scanning {
return style.ScanningStyle.Render(" press Tab to load sessions...")
}
if len(m.history.Projects) == 0 && m.history.Scanning {
return style.ScanningStyle.Render(" scanning sessions...")
}
if len(m.history.Projects) == 0 {
return style.SubtitleStyle.Render(" no sessions found")
}
// 三栏宽度分配
avail := m.width - 4 // borders + gaps
dirW := max(20, avail*20/100)
sessW := max(30, avail*40/100)
detailW := max(20, avail-dirW-sessW)
// 三个独立面板Width() 含边框,内容宽度 = Width - 2
avail := m.width - 8
dirW := max(18, avail*20/100)
sessW := max(28, avail*40/100)
detailW := avail - dirW - sessW
listH := max(3, m.height-7)
// 左栏: 目录列表
dirBox := m.renderDirPanel(dirW)
// 中栏: 会话列表
sessBox := m.renderSessPanel(sessW)
// 右栏: 详情
detailBox := m.renderHistoryDetail(detailW)
dirCw := dirW - 2
sessCw := sessW - 2
detailCw := detailW - 2
return lipgloss.JoinHorizontal(lipgloss.Left, dirBox, " ", sessBox, " ", detailBox)
dirLines := m.dirColumnLines(dirCw, listH-1)
sessLines := m.sessColumnLines(sessCw, listH-1)
detailLines := m.detailColumnLines(detailCw, listH-1)
// 统一高度:补空行到相同行数
maxRows := min(listH-1, max(max(len(dirLines), len(sessLines)), len(detailLines)))
padLines := func(lines []string, w int) string {
for len(lines) < maxRows {
lines = append(lines, strings.Repeat(" ", w))
}
return strings.Join(lines, "\n")
}
panelSty := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(style.BgPanel)
dirBox := panelSty.Width(dirW).Render(
style.DetailTitle.Render("dirs") + "\n" + padLines(dirLines, dirCw))
sessBox := panelSty.Width(sessW).Render(
style.DetailTitle.Render("sessions") + "\n" + padLines(sessLines, sessCw))
detailBox := panelSty.Width(detailW).Render(
style.DetailTitle.Render("detail") + "\n" + padLines(detailLines, detailCw))
return lipgloss.JoinHorizontal(lipgloss.Top, dirBox, " ", sessBox, " ", detailBox)
}
func (m *Model) renderDirPanel(w int) string {
var b strings.Builder
innerW := w - 4
func (m *Model) dirColumnLines(w, listH int) []string {
var lines []string
total := len(m.history.Projects) + 1
start, end := viewport(m.history.DirCursor, total, listH)
b.WriteString(style.DetailTitle.Render(" dirs "))
b.WriteString("\n")
prefixW := 3 // " " + cur(1) + " "
// "全部" 行 (index 0)
totalSess := 0
for _, pd := range m.history.Projects {
totalSess += len(pd.Sessions)
buildDirLine := func(cur, name, cnt string, selected bool) string {
cntW := stringWidth(cnt)
nameW := max(2, w-prefixW-cntW)
name = truncateByWidth(name, nameW)
line := " " + cur + " " + padRightByWidth(name, nameW) + cnt
sty := style.NormStyle
if selected {
sty = style.SelStyle
}
return sty.Render(padRightByWidth(truncateByWidth(line, w), w))
}
cur := " "
if m.history.DirCursor == 0 {
cur = "▸"
}
if m.history.DirCursor == 0 && m.history.FocusPanel == 0 {
line := fmt.Sprintf("%s 全部 (%d)", cur, totalSess)
b.WriteString(style.SelStyle.Width(innerW).Render(line))
} else {
cnt := style.DirCountStyle.Render(fmt.Sprintf("(%d)", totalSess))
line := fmt.Sprintf("%s 全部 %s", cur, cnt)
b.WriteString(style.NormStyle.Render(line))
}
b.WriteString("`n")
for i, pd := range m.history.Projects {
if start == 0 {
totalSess := 0
for _, pd := range m.history.Projects {
totalSess += len(pd.Sessions)
}
cur := " "
if m.history.DirCursor == 0 {
cur = "▸"
}
lines = append(lines, buildDirLine(cur, "全部", fmt.Sprintf("(%d)", totalSess),
m.history.DirCursor == 0 && m.history.FocusPanel == 0))
}
for i := 0; i < len(m.history.Projects); i++ {
if i+1 < start || i+1 >= end {
continue
}
pd := m.history.Projects[i]
cur := " "
if m.history.DirCursor == i+1 {
cur = "▸"
}
name := truncateByWidth(pd.DirShort, max(8, innerW-8))
if m.history.DirCursor == i+1 && m.history.FocusPanel == 0 {
line := fmt.Sprintf("%s %s (%d)", cur, name, len(pd.Sessions))
b.WriteString(style.SelStyle.Width(innerW).Render(line))
} else {
cnt := style.DirCountStyle.Render(fmt.Sprintf("(%d)", len(pd.Sessions)))
line := fmt.Sprintf("%s %s %s", cur, name, cnt)
b.WriteString(style.NormStyle.Render(line))
}
b.WriteString("`n")
lines = append(lines, buildDirLine(cur, pd.DirShort, fmt.Sprintf("(%d)", len(pd.Sessions)),
m.history.DirCursor == i+1 && m.history.FocusPanel == 0))
}
return lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(style.BgPanel).
Width(w).
Padding(0, 1).
Render(b.String())
return lines
}
func (m *Model) renderSessPanel(w int) string {
var b strings.Builder
innerW := w - 4
b.WriteString(style.DetailTitle.Render(" sessions "))
b.WriteString("\n")
func (m *Model) sessColumnLines(w, listH int) []string {
sessions := m.currentSessions()
if len(sessions) == 0 {
b.WriteString(style.SubtitleStyle.Render(" no sessions"))
} else {
for i, s := range sessions {
cur := " "
if i == m.history.SessCursor {
cur = "▸"
}
timeStr := s.StartTime.Format("01-02 15:04")
title := s.DisplayTitle()
remainW := max(10, innerW-10)
text := truncateByWidth(title, remainW)
if i == m.history.SessCursor && m.history.FocusPanel == 1 {
line := fmt.Sprintf("%s %s %s", cur, timeStr, text)
b.WriteString(style.SelStyle.Width(innerW).Render(line))
} else {
line := fmt.Sprintf("%s %s %s", cur, style.SessionTimeStyle.Render(timeStr), text)
b.WriteString(style.NormStyle.Render(line))
}
b.WriteString("\n")
}
return []string{style.SubtitleStyle.Render(fitWidth(" no sessions", w))}
}
return lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(style.BgPanel).
Width(w).
Padding(0, 1).
Render(b.String())
prefixW := 15 // " " + cur(2) + timeStr(11) + " "
textW := max(8, w-prefixW)
// 预渲染每个 session 为 1~N 行
type sessBlock struct {
lines []string
}
var blocks []sessBlock
totalLines := 0
curLineOffset := 0
for i, s := range sessions {
cur := " "
if i == m.history.SessCursor {
cur = "▸ "
}
timeStr := s.StartTime.Local().Format("01-02 15:04")
title := s.DisplayTitle()
wrapped := wrapByWidth(title, textW)
if len(wrapped) == 0 {
wrapped = []string{""}
}
sty := style.NormStyle
if i == m.history.SessCursor && m.history.FocusPanel == 1 {
sty = style.SelStyle
}
var blockLines []string
for li, part := range wrapped {
var line string
if li == 0 {
line = fmt.Sprintf(" %s%s %s", cur, timeStr, part)
} else {
line = strings.Repeat(" ", prefixW) + part
}
blockLines = append(blockLines, sty.Render(padRightByWidth(truncateByWidth(line, w), w)))
}
if i == m.history.SessCursor {
curLineOffset = totalLines
}
totalLines += len(blockLines)
blocks = append(blocks, sessBlock{lines: blockLines})
}
// 按总行数做 viewport
startLine, endLine := viewport(curLineOffset, totalLines, listH)
var allLines []string
for _, b := range blocks {
allLines = append(allLines, b.lines...)
}
if endLine > len(allLines) {
endLine = len(allLines)
}
return allLines[startLine:endLine]
}
func (m *Model) renderHistoryDetail(w int) string {
var b strings.Builder
func (m *Model) detailColumnLines(w, listH int) []string {
s := m.currentSession()
if s == nil {
b.WriteString(lipgloss.NewStyle().Foreground(style.Dim).Render(" ← select to view"))
} else {
b.WriteString(style.DetailTitle.Render(" detail "))
b.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(strings.Repeat("─", w-12)))
b.WriteString("\n\n")
rows := []struct {
key string
val string
sty lipgloss.Style
}{
{"title", s.CustomTitle, style.ValStyle},
{"dir", s.Cwd, style.ValStyle},
{"time", fmt.Sprintf("%s ~ %s", s.StartTime.Format("01-02 15:04"), s.EndTime.Format("15:04")), style.NumStyle},
{"msgs", fmt.Sprintf("%d", s.MsgCount), style.SessionMsgCntStyle},
}
for _, r := range rows {
b.WriteString(" ")
b.WriteString(style.KeyStyle.Render(r.key))
b.WriteString(" ")
b.WriteString(r.sty.Render(r.val))
b.WriteString("\n")
}
// 摘要
summary := s.DisplaySummary()
if summary != "" {
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(" " + strings.Repeat("─", w-6)))
b.WriteString("\n")
b.WriteString(style.SessionSummaryStyle.Render(" " + truncateByWidth(summary, w-6)))
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(lipgloss.NewStyle().Foreground(style.Dim).Render(" [Enter]resume"))
return []string{lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" ← select to view", w))}
}
return lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(style.BgPanel).
Width(w).
Padding(0, 1).
Render(b.String())
sepSty := lipgloss.NewStyle().Foreground(style.BgPanel)
var lines []string
// Title
lines = append(lines, style.DetailTitle.Render(fitWidth(" "+s.DisplayTitle(), w)))
// Separator
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
// Key-value rows
kvRows := []struct{ k, v string }{
{"dir", s.Cwd},
{"time", fmt.Sprintf("%s ~ %s", s.StartTime.Local().Format("01-02 15:04"), s.EndTime.Local().Format("15:04"))},
{"msgs", fmt.Sprintf("%d", s.MsgCount)},
}
for _, r := range kvRows {
line := fmt.Sprintf(" %-4s %s", r.k, r.v)
lines = append(lines, style.NormStyle.Render(fitWidth(line, w)))
}
// Summary
if summary := s.DisplaySummary(); summary != "" {
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
lines = append(lines, style.SessionSummaryStyle.Render(fitWidth(" "+summary, w)))
}
// Completed items
if len(s.Completed) > 0 {
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
lines = append(lines, lipgloss.NewStyle().Foreground(style.Success).Bold(true).Render(fitWidth(" ✓ 已完成", w)))
for _, item := range s.Completed {
lines = append(lines, style.NormStyle.Render(fitWidth(" · "+cleanText(item), w)))
}
}
// Pending items
if len(s.Pending) > 0 {
lines = append(lines, sepSty.Render(fitWidth(strings.Repeat("─", w), w)))
lines = append(lines, lipgloss.NewStyle().Foreground(style.Warning).Bold(true).Render(fitWidth(" ○ 待办", w)))
for _, item := range s.Pending {
lines = append(lines, style.NormStyle.Render(fitWidth(" · "+cleanText(item), w)))
}
}
// Hint
lines = append(lines, lipgloss.NewStyle().Foreground(style.Dim).Render(fitWidth(" [Enter]resume", w)))
if len(lines) > listH {
lines = lines[:listH]
}
return lines
}
// --- 工具 ---
@@ -716,12 +840,69 @@ func (m *Model) fmtHelp(key, desc string) string {
lipgloss.NewStyle().Foreground(style.Dim).Render(":" + desc)
}
// viewport 返回 [start, end) 区间,保证 cursor 在可见范围内
func viewport(cursor, total, height int) (int, int) {
if total <= height {
return 0, total
}
half := height / 2
start := cursor - half
if start < 0 {
start = 0
}
end := start + height
if end > total {
end = total
start = end - height
if start < 0 {
start = 0
}
}
return start, end
}
// stringWidth 返回字符串的终端显示宽度
func stringWidth(s string) int {
w := 0
for _, r := range s {
w += runeWidth(r)
}
return w
}
// padRightByWidth 补空格到指定视觉宽度
func padRightByWidth(s string, targetW int) string {
w := stringWidth(s)
if w >= targetW {
return s
}
return s + strings.Repeat(" ", targetW-w)
}
// wrapByWidth 按显示宽度换行,返回多行文本
func wrapByWidth(s string, maxW int) []string {
if maxW <= 0 || s == "" {
return []string{s}
}
var lines []string
for s != "" {
cut := truncateByWidth(s, maxW)
lines = append(lines, cut)
s = s[len(cut):]
}
return lines
}
// fitWidth 截断并填充到精确视觉宽度
func fitWidth(s string, w int) string {
return padRightByWidth(truncateByWidth(s, w), w)
}
// truncateByWidth 按显示宽度截断字符串,不切断多字节字符
func truncateByWidth(s string, maxW int) string {
w := 0
for i := 0; i < len(s); {
_, size := utf8.DecodeRuneInString(s[i:])
r := rune(s[i])
r, size := utf8.DecodeRuneInString(s[i:])
rw := runeWidth(r)
if w+rw > maxW {
return s[:i]
@@ -750,45 +931,67 @@ func runeWidth(r rune) int {
return 1
}
// --- 启动 ---
func launchInWt(dir, script string) {
encoded := encodePSCommand(script)
color := fmt.Sprintf("#%02X%02X%02X", randRange(80, 255), randRange(80, 255), randRange(80, 255))
cmd := exec.Command("wt.exe", "-w", "0",
"-d", dir,
"--tabColor", color,
"pwsh", "-NoExit", "-EncodedCommand", encoded,
)
if err := cmd.Start(); err != nil {
log.Printf("[u-tabs] launch fail: %v", err)
func launchInWtWithTitle(dir, tabTitle, script string) {
if runtime.GOOS == "windows" {
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)
}
args = append(args, "pwsh", "-NoExit", "-EncodedCommand", encoded)
cmd := exec.Command("wt.exe", args...)
if err := cmd.Start(); err != nil {
log.Printf("[u-tabs] launch fail: %v", err)
}
}
}
func launchWorkspace(ws Workspace) {
func (m *Model) LaunchWorkspace(ws Workspace) {
script := buildLaunchScript(ws)
launchInWt(ws.Dir, script)
if runtime.GOOS != "windows" {
m.pendingCmd = "cd '" + ws.Dir + "' && " + script
return
}
launchInWtWithTitle(ws.Dir, "", script)
}
// GetPendingCmd 返回退出后需执行的命令Linux 用)
func (m *Model) GetPendingCmd() string {
return m.pendingCmd
}
func resumeSession(s *Session) {
cwd := strings.ReplaceAll(s.Cwd, "/", "\\")
cwd := s.Cwd
if runtime.GOOS == "windows" {
cwd = strings.ReplaceAll(s.Cwd, "/", "\\")
}
title := s.CustomTitle
if title == "" {
title = s.ID[:8]
}
script := fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "resume: %s"
var script string
if runtime.GOOS == "windows" {
script = fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "resume: %s"
Write-Host "=== Resuming: %s ===" -ForegroundColor Cyan
cd "%s"
claude -r %s`, title, title, cwd, s.ID)
launchInWt(cwd, script)
claude -r %s --permission-mode bypassPermissions`, title, title, cwd, s.ID)
} else {
script = fmt.Sprintf(`printf '\033]0;resume: %s\007' && echo "=== Resuming: %s ===" && cd "%s" && claude -r %s`, title, title, cwd, s.ID)
}
launchInWtWithTitle(cwd, "", script)
}
func buildLaunchScript(ws Workspace) string {
return fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "%s"
if runtime.GOOS == "windows" {
return fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "%s"
Write-Host "=== %s ===" -ForegroundColor Cyan
Write-Host "Prompt: %s" -ForegroundColor Yellow
cd "%s"
claude --name "%s" --permission-mode bypassPermissions`,
ws.Title, ws.Title, ws.Prompt, ws.Dir, ws.Title)
}
return fmt.Sprintf(`printf '\033]0;%s\007' && echo "=== %s ===" && echo "Prompt: %s" && cd "%s" && claude --name "%s"`,
ws.Title, ws.Title, ws.Prompt, ws.Dir, ws.Title)
}
@@ -806,3 +1009,4 @@ func randRange(min, max int) int {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min)))
return min + int(n.Int64())
}