新增: HISTORY 历史会话功能
三栏布局(目录/会话/详情), 扫描 ~/.claude/projects JSONL, 支持 resume 会话, 缓存增量更新, 代码审查问题修复
This commit is contained in:
524
internal/app.go
524
internal/app.go
@@ -26,6 +26,7 @@ type Model struct {
|
||||
width int
|
||||
height int
|
||||
launched string
|
||||
history HistoryState
|
||||
}
|
||||
|
||||
func NewModel() *Model {
|
||||
@@ -39,51 +40,190 @@ func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// totalTabs 包含 HISTORY 的总 Tab 数
|
||||
func (m *Model) totalTabs() int {
|
||||
return len(Groups) + 1
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
s := msg.String()
|
||||
switch s {
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "tab", "right", "l":
|
||||
m.activeGroup = (m.activeGroup + 1) % len(Groups)
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
case "shift+tab", "left", "h":
|
||||
m.activeGroup = (m.activeGroup - 1 + len(Groups)) % len(Groups)
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
case "1", "2", "3", "4":
|
||||
idx, _ := strconv.Atoi(s)
|
||||
if idx <= len(Groups) {
|
||||
m.activeGroup = idx - 1
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
}
|
||||
case "up", "k":
|
||||
m.moveCursor(-1)
|
||||
case "down", "j":
|
||||
m.moveCursor(1)
|
||||
case " ":
|
||||
m.toggleMultiSelect()
|
||||
case "enter":
|
||||
if m.inputBuf != "" {
|
||||
return m.launchByInput()
|
||||
}
|
||||
return m.launchSelected()
|
||||
default:
|
||||
if len(s) == 1 && s[0] >= '0' && s[0] <= '9' {
|
||||
m.inputBuf += s
|
||||
}
|
||||
}
|
||||
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.Loaded = true
|
||||
m.history.Scanning = false
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// --- 按键分发 ---
|
||||
|
||||
func (m *Model) handleKey(s string) (*Model, tea.Cmd) {
|
||||
// 全局按键
|
||||
switch s {
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
// 数字键跳转 Tab
|
||||
if len(s) == 1 && s[0] >= '1' && s[0] <= '9' {
|
||||
idx, _ := strconv.Atoi(s)
|
||||
if idx <= m.totalTabs() {
|
||||
m.activeGroup = idx - 1
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
return m, m.onTabSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
// HISTORY Tab: tab/left/right 用于面板切换
|
||||
if m.IsHistoryTab() {
|
||||
return m.handleHistoryKey(s)
|
||||
}
|
||||
|
||||
// Workspace Tab 按键
|
||||
switch s {
|
||||
case "tab", "right", "l":
|
||||
m.activeGroup = (m.activeGroup + 1) % m.totalTabs()
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
return m, m.onTabSwitch()
|
||||
case "shift+tab", "left", "h":
|
||||
m.activeGroup = (m.activeGroup - 1 + m.totalTabs()) % m.totalTabs()
|
||||
m.cursor = 0
|
||||
m.inputBuf = ""
|
||||
return m, m.onTabSwitch()
|
||||
}
|
||||
|
||||
return m.handleWorkspaceKey(s)
|
||||
}
|
||||
|
||||
func (m *Model) onTabSwitch() tea.Cmd {
|
||||
if m.IsHistoryTab() && !m.history.Loaded && !m.history.Scanning {
|
||||
m.history.Scanning = true
|
||||
return ScanSessionsCmd()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Workspace Tab 按键 ---
|
||||
|
||||
func (m *Model) handleWorkspaceKey(s string) (*Model, tea.Cmd) {
|
||||
switch s {
|
||||
case "up", "k":
|
||||
m.moveCursor(-1)
|
||||
case "down", "j":
|
||||
m.moveCursor(1)
|
||||
case " ":
|
||||
m.toggleMultiSelect()
|
||||
case "enter":
|
||||
if m.inputBuf != "" {
|
||||
return m.launchByInput()
|
||||
}
|
||||
return m.launchSelected()
|
||||
default:
|
||||
if len(s) == 1 && s[0] >= '0' && s[0] <= '9' {
|
||||
m.inputBuf += s
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// --- HISTORY Tab 按键 ---
|
||||
|
||||
func (m *Model) handleHistoryKey(s string) (*Model, tea.Cmd) {
|
||||
switch s {
|
||||
case "up", "k":
|
||||
if m.history.FocusPanel == 0 {
|
||||
m.moveHistoryDir(-1)
|
||||
} else {
|
||||
m.moveHistorySess(-1)
|
||||
}
|
||||
case "down", "j":
|
||||
if m.history.FocusPanel == 0 {
|
||||
m.moveHistoryDir(1)
|
||||
} else {
|
||||
m.moveHistorySess(1)
|
||||
}
|
||||
case "tab", "right", "l":
|
||||
if m.history.FocusPanel < 1 {
|
||||
m.history.FocusPanel = 1
|
||||
}
|
||||
case "shift+tab", "left", "h":
|
||||
if m.history.FocusPanel > 0 {
|
||||
m.history.FocusPanel = 0
|
||||
}
|
||||
case "enter":
|
||||
if m.history.FocusPanel == 0 {
|
||||
// 在左栏按 enter 切到中栏
|
||||
m.history.FocusPanel = 1
|
||||
} else {
|
||||
return m.resumeSelected()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) moveHistoryDir(dir int) {
|
||||
if len(m.history.Projects) == 0 {
|
||||
return
|
||||
}
|
||||
m.history.DirCursor += dir
|
||||
if m.history.DirCursor < 0 {
|
||||
m.history.DirCursor = len(m.history.Projects) - 1
|
||||
}
|
||||
if m.history.DirCursor >= len(m.history.Projects) {
|
||||
m.history.DirCursor = 0
|
||||
}
|
||||
m.history.SessCursor = 0
|
||||
}
|
||||
|
||||
func (m *Model) moveHistorySess(dir int) {
|
||||
sessions := m.currentSessions()
|
||||
if len(sessions) == 0 {
|
||||
return
|
||||
}
|
||||
m.history.SessCursor += dir
|
||||
if m.history.SessCursor < 0 {
|
||||
m.history.SessCursor = len(sessions) - 1
|
||||
}
|
||||
if m.history.SessCursor >= len(sessions) {
|
||||
m.history.SessCursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) currentSessions() []*Session {
|
||||
if m.history.DirCursor < len(m.history.Projects) {
|
||||
return m.history.Projects[m.history.DirCursor].Sessions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) currentSession() *Session {
|
||||
sessions := m.currentSessions()
|
||||
if len(sessions) == 0 || m.history.SessCursor >= len(sessions) {
|
||||
return nil
|
||||
}
|
||||
return sessions[m.history.SessCursor]
|
||||
}
|
||||
|
||||
func (m *Model) resumeSelected() (*Model, tea.Cmd) {
|
||||
s := m.currentSession()
|
||||
if s == nil {
|
||||
return m, nil
|
||||
}
|
||||
go resumeSession(s)
|
||||
m.launched = "resume: " + s.CustomTitle
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// --- Workspace 光标 ---
|
||||
|
||||
func (m *Model) moveCursor(dir int) {
|
||||
svcs := WorkspacesByGroup(Groups[m.activeGroup].Label)
|
||||
if len(svcs) == 0 {
|
||||
@@ -146,6 +286,7 @@ func (m *Model) launchByInput() (*Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// --- View ---
|
||||
|
||||
func (m *Model) View() tea.View {
|
||||
v := tea.NewView(m.render())
|
||||
@@ -162,13 +303,11 @@ func (m *Model) render() string {
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
// layout widths
|
||||
listW := max(42, min(65, m.width*55/100))
|
||||
detailW := max(30, m.width-listW-3)
|
||||
|
||||
// ── header: title + tabs ──
|
||||
b.WriteString(style.TitleStyle.Render(" u-tabs "))
|
||||
sep := style.TabSep.Render(" | ")
|
||||
|
||||
// Workspace Tabs
|
||||
for i, g := range Groups {
|
||||
if i > 0 {
|
||||
b.WriteString(sep)
|
||||
@@ -187,19 +326,83 @@ func (m *Model) render() string {
|
||||
b.WriteString(style.TabInactiveStyle.Render(label))
|
||||
}
|
||||
}
|
||||
|
||||
// HISTORY Tab
|
||||
b.WriteString(sep)
|
||||
histLabel := " HISTORY 历史 "
|
||||
if m.IsHistoryTab() {
|
||||
gs := style.GroupStyles["HISTORY"]
|
||||
b.WriteString(lipgloss.NewStyle().
|
||||
Bold(true).Background(style.BgPanel).Foreground(gs.GetForeground()).
|
||||
Padding(0, 1).Render(histLabel))
|
||||
} else {
|
||||
b.WriteString(style.TabInactiveStyle.Render(histLabel))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(style.BgPanel).Render(strings.Repeat("─", m.width)))
|
||||
b.WriteString("\n")
|
||||
|
||||
// ── 内容区 ──
|
||||
if m.IsHistoryTab() {
|
||||
b.WriteString(m.renderHistory())
|
||||
} else {
|
||||
b.WriteString(m.renderWorkspace())
|
||||
}
|
||||
|
||||
// ── footer ──
|
||||
b.WriteString("\n")
|
||||
if m.inputBuf != "" {
|
||||
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ▶ num:%s [Enter]go [Esc]cancel", m.inputBuf)))
|
||||
}
|
||||
if m.launched != "" {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Render(fmt.Sprintf(" ✓ %s", m.launched)))
|
||||
m.launched = ""
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
helpParts := m.renderHelp()
|
||||
b.WriteString(" " + strings.Join(helpParts, " "))
|
||||
|
||||
if hint := ConfigHint(); hint != "" {
|
||||
b.WriteString("\n" + hint)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m *Model) renderHelp() []string {
|
||||
if m.IsHistoryTab() {
|
||||
return []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("q", "quit"),
|
||||
}
|
||||
}
|
||||
return []string{
|
||||
m.fmtHelp("j/k", "sel"),
|
||||
m.fmtHelp("Enter", "run"),
|
||||
m.fmtHelp("Space", "multi"),
|
||||
m.fmtHelp("Tab", "group"),
|
||||
m.fmtHelp("q", "quit"),
|
||||
}
|
||||
}
|
||||
|
||||
// --- Workspace 渲染 ---
|
||||
|
||||
func (m *Model) renderWorkspace() string {
|
||||
listW := max(42, min(65, m.width*55/100))
|
||||
detailW := max(30, m.width-listW-3)
|
||||
|
||||
g := Groups[m.activeGroup]
|
||||
gs, _ := style.GroupStyles[g.Label]
|
||||
svcs := WorkspacesByGroup(g.Label)
|
||||
if len(svcs) == 0 {
|
||||
b.WriteString(style.SubtitleStyle.Render(" empty"))
|
||||
return b.String()
|
||||
return style.SubtitleStyle.Render(" empty")
|
||||
}
|
||||
|
||||
// ═══ left: list ═══
|
||||
// 左栏: 列表
|
||||
var left strings.Builder
|
||||
innerW := listW - 4
|
||||
for i, ws := range svcs {
|
||||
@@ -209,19 +412,26 @@ func (m *Model) render() string {
|
||||
}
|
||||
mark := " "
|
||||
if m.selected[ws.Index] {
|
||||
mark = style.MarkStyle.Render("✓")
|
||||
mark = "✓"
|
||||
}
|
||||
num := style.NumStyle.Render(fmt.Sprintf("%02d", ws.N))
|
||||
|
||||
prefix := cur + " " + mark + " " + num + " "
|
||||
remainW := max(10, innerW-lipgloss.Width(prefix))
|
||||
text := truncateByWidth(ws.Title+" "+ws.Prompt, remainW)
|
||||
|
||||
line := prefix + text
|
||||
if i == m.cursor {
|
||||
left.WriteString(style.SelStyle.Width(innerW).Render(line))
|
||||
// 选中行:纯文本,让 SelStyle 统一着色
|
||||
prefix := cur + " " + mark + " " + fmt.Sprintf("%02d", ws.N) + " "
|
||||
remainW := max(10, innerW-len(prefix))
|
||||
text := truncateByWidth(ws.Title+" "+ws.Prompt, remainW)
|
||||
left.WriteString(style.SelStyle.Width(innerW).Render(prefix + text))
|
||||
} else {
|
||||
left.WriteString(style.NormStyle.Render(line))
|
||||
// 非选中行:子样式着色
|
||||
markStr := " "
|
||||
if m.selected[ws.Index] {
|
||||
markStr = style.MarkStyle.Render("✓")
|
||||
}
|
||||
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)
|
||||
left.WriteString(style.NormStyle.Render(prefix + text))
|
||||
}
|
||||
left.WriteString("\n")
|
||||
}
|
||||
@@ -234,7 +444,7 @@ func (m *Model) render() string {
|
||||
Padding(0, 1).
|
||||
Render(groupHeader + "\n" + left.String())
|
||||
|
||||
// ═══ right: detail ═══
|
||||
// 右栏: 详情
|
||||
var right strings.Builder
|
||||
if m.cursor < len(svcs) {
|
||||
ws := svcs[m.cursor]
|
||||
@@ -279,34 +489,165 @@ func (m *Model) render() string {
|
||||
Padding(0, 1).
|
||||
Render(right.String())
|
||||
|
||||
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Left, listBox, " ", detailBox))
|
||||
|
||||
// ── footer ──
|
||||
b.WriteString("\n")
|
||||
if m.inputBuf != "" {
|
||||
b.WriteString(style.InputStyle.Render(fmt.Sprintf(" ▶ num:%s [Enter]go [Esc]cancel", m.inputBuf)))
|
||||
}
|
||||
if m.launched != "" {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(style.Accent).Render(fmt.Sprintf(" ✓ %s", m.launched)))
|
||||
m.launched = ""
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
helpParts := []string{
|
||||
m.fmtHelp("j/k", "sel"),
|
||||
m.fmtHelp("Enter", "run"),
|
||||
m.fmtHelp("Space", "multi"),
|
||||
m.fmtHelp("Tab", "group"),
|
||||
m.fmtHelp("q", "quit"),
|
||||
}
|
||||
b.WriteString(" " + strings.Join(helpParts, " "))
|
||||
|
||||
if hint := ConfigHint(); hint != "" {
|
||||
b.WriteString("\n" + hint)
|
||||
}
|
||||
return b.String()
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, listBox, " ", detailBox)
|
||||
}
|
||||
|
||||
// --- HISTORY 三栏渲染 ---
|
||||
|
||||
func (m *Model) renderHistory() string {
|
||||
if m.history.Scanning {
|
||||
return style.ScanningStyle.Render(" scanning sessions...")
|
||||
}
|
||||
if !m.history.Loaded {
|
||||
return style.ScanningStyle.Render(" press Tab to load 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)
|
||||
|
||||
// 左栏: 目录列表
|
||||
dirBox := m.renderDirPanel(dirW)
|
||||
// 中栏: 会话列表
|
||||
sessBox := m.renderSessPanel(sessW)
|
||||
// 右栏: 详情
|
||||
detailBox := m.renderHistoryDetail(detailW)
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, dirBox, " ", sessBox, " ", detailBox)
|
||||
}
|
||||
|
||||
func (m *Model) renderDirPanel(w int) string {
|
||||
var b strings.Builder
|
||||
innerW := w - 4
|
||||
|
||||
b.WriteString(style.DetailTitle.Render(" dirs "))
|
||||
b.WriteString("\n")
|
||||
|
||||
for i, pd := range m.history.Projects {
|
||||
cur := " "
|
||||
if i == m.history.DirCursor {
|
||||
cur = "▸"
|
||||
}
|
||||
name := truncateByWidth(pd.DirShort, max(8, innerW-8))
|
||||
|
||||
if i == m.history.DirCursor && 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")
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(style.BgPanel).
|
||||
Width(w).
|
||||
Padding(0, 1).
|
||||
Render(b.String())
|
||||
}
|
||||
|
||||
func (m *Model) renderSessPanel(w int) string {
|
||||
var b strings.Builder
|
||||
innerW := w - 4
|
||||
|
||||
b.WriteString(style.DetailTitle.Render(" sessions "))
|
||||
b.WriteString("\n")
|
||||
|
||||
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 lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(style.BgPanel).
|
||||
Width(w).
|
||||
Padding(0, 1).
|
||||
Render(b.String())
|
||||
}
|
||||
|
||||
func (m *Model) renderHistoryDetail(w int) string {
|
||||
var b strings.Builder
|
||||
|
||||
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 lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(style.BgPanel).
|
||||
Width(w).
|
||||
Padding(0, 1).
|
||||
Render(b.String())
|
||||
}
|
||||
|
||||
// --- 工具 ---
|
||||
|
||||
func (m *Model) fmtHelp(key, desc string) string {
|
||||
return lipgloss.NewStyle().Foreground(style.Accent).Render(key) +
|
||||
lipgloss.NewStyle().Foreground(style.Dim).Render(":" + desc)
|
||||
@@ -346,22 +687,39 @@ func runeWidth(r rune) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// --- launch ---
|
||||
// --- 启动 ---
|
||||
|
||||
func launchWorkspace(ws Workspace) {
|
||||
script := buildLaunchScript(ws)
|
||||
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", ws.Dir,
|
||||
"-d", dir,
|
||||
"--tabColor", color,
|
||||
"pwsh", "-NoExit", "-EncodedCommand", encoded,
|
||||
)
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[u-tabs] launch fail %s(%d): %v", ws.Title, ws.N, err)
|
||||
log.Printf("[u-tabs] launch fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func launchWorkspace(ws Workspace) {
|
||||
script := buildLaunchScript(ws)
|
||||
launchInWt(ws.Dir, script)
|
||||
}
|
||||
|
||||
func resumeSession(s *Session) {
|
||||
cwd := strings.ReplaceAll(s.Cwd, "/", "\\")
|
||||
title := s.CustomTitle
|
||||
if title == "" {
|
||||
title = s.ID[:8]
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func buildLaunchScript(ws Workspace) string {
|
||||
return fmt.Sprintf(`$Host.UI.RawUI.WindowTitle = "%s"
|
||||
Write-Host "=== %s ===" -ForegroundColor Cyan
|
||||
|
||||
Reference in New Issue
Block a user