Files
superwork/tui/internal/tui/profiles_panel.go
T
2026-06-23 05:02:15 +08:00

323 lines
9.2 KiB
Go

package tui
import (
"context"
"fmt"
"strings"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"superwork-tui/internal/auth"
"superwork-tui/internal/cc"
"superwork-tui/internal/git"
"superwork-tui/internal/gitea"
"superwork-tui/internal/issue"
"superwork-tui/internal/logging"
"superwork-tui/internal/store"
)
// ── message types ─────────────────────────────────────────────────────────────
type profilePickerLoadedMsg struct {
profiles []cc.ClaudeProfile
err error
}
type profilePathSavedMsg struct {
forTest bool
err error
}
type profileGridLoadedMsg struct {
data store.ProfilesData
err error
}
type profileGridSavedMsg struct {
err error
}
// ── profile picker (f / F in detail) ─────────────────────────────────────────
func loadProfilePickerCmd(dir string) tea.Cmd {
return func() tea.Msg {
profiles, err := cc.ListClaudeProfiles(dir)
return profilePickerLoadedMsg{profiles: profiles, err: err}
}
}
func (m Model) openProfilePicker(forTest bool) (Model, tea.Cmd) {
if m.settings == nil || m.settings.ProfilesDir == "" {
m.statusMsg = "未配置 profiles 目录"
return m, nil
}
m.profilePickerForTest = forTest
m.profilePickerCursor = 0
m.profilePickerPrevState = m.state
m.state = stateProfilePicker
return m, loadProfilePickerCmd(m.settings.ProfilesDir)
}
func (m Model) handleProfilePickerKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc":
m.state = m.profilePickerPrevState
case "up", "k":
if m.profilePickerCursor > 0 {
m.profilePickerCursor--
}
case "down", "j":
if m.profilePickerCursor < len(m.profilePickerProfiles)-1 {
m.profilePickerCursor++
}
case "enter":
if len(m.profilePickerProfiles) == 0 {
m.statusMsg = "无可用 profile"
m.state = m.profilePickerPrevState
return m, nil
}
chosen := m.profilePickerProfiles[m.profilePickerCursor]
m.state = m.profilePickerPrevState
if m.detailIss != nil {
if m.profilePickerForTest {
m.detailIss.TestProfilePath = chosen.Path
logging.Default.Info("tui", fmt.Sprintf("#%d testProfilePath=%s", m.detailIss.Number, chosen.Path))
return m, saveProfilePathCmd(m.detailIss.Number, chosen.Path, true)
}
m.detailIss.ProfilePath = chosen.Path
logging.Default.Info("tui", fmt.Sprintf("#%d profilePath=%s", m.detailIss.Number, chosen.Path))
return m, saveProfilePathCmd(m.detailIss.Number, chosen.Path, false)
}
}
return m, nil
}
func saveProfilePathCmd(issueNumber int, path string, forTest bool) tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
root, err := workspaceRootFn()
if err != nil {
return profilePathSavedMsg{forTest: forTest, err: err}
}
remote, err := git.DetectRepo(root)
if err != nil {
return profilePathSavedMsg{forTest: forTest, err: err}
}
token, err := auth.ResolveGiteaToken(remote.Host)
if err != nil {
return profilePathSavedMsg{forTest: forTest, err: err}
}
client := gitea.New(remote.Host, token)
field := "profilePath"
if forTest {
field = "testProfilePath"
}
err = issue.MergeStateJSON(ctx, client, remote.Owner, remote.Repo, issueNumber, map[string]any{field: path})
if err != nil {
logging.Default.Error("tui", fmt.Sprintf("持久化 %s 失败 #%d: %v", field, issueNumber, err))
}
return profilePathSavedMsg{forTest: forTest, err: err}
}
}
func (m Model) profilePickerView() string {
var sb strings.Builder
sb.WriteString(helpStyle.Render("选择 Claude Profile") + "\n")
sb.WriteString(strings.Repeat("─", 50) + "\n")
if len(m.profilePickerProfiles) == 0 {
sb.WriteString(" (无可用 profile — 请检查 profiles 目录)\n")
} else {
for i, p := range m.profilePickerProfiles {
line := fmt.Sprintf(" %-30s %s", p.Name, p.Path)
if i == m.profilePickerCursor {
sb.WriteString(selectedCardStyle.Render(line) + "\n")
} else {
sb.WriteString(normalCardStyle.Render(line) + "\n")
}
}
}
sb.WriteString("\n")
sb.WriteString(helpStyle.Render("↑↓/jk select enter confirm esc cancel"))
return sb.String()
}
// ── profile grid (G) ──────────────────────────────────────────────────────────
func loadProfileGridCmd() tea.Cmd {
return func() tea.Msg {
root, err := workspaceRootFn()
if err != nil {
return profileGridLoadedMsg{err: err}
}
data, err := store.ReadProfiles(root)
return profileGridLoadedMsg{data: data, err: err}
}
}
func (m Model) openProfileGrid() (Model, tea.Cmd) {
m.state = stateProfileGrid
m.profileGridRow = 0
m.profileGridCol = 0
m.profileGridEditing = false
m.profileGridAddingKey = false
return m, loadProfileGridCmd()
}
func saveProfileGridCmd(data store.ProfilesData) tea.Cmd {
return func() tea.Msg {
root, err := workspaceRootFn()
if err != nil {
return profileGridSavedMsg{err: err}
}
err = store.WriteProfiles(root, data)
if err != nil {
logging.Default.Error("tui", fmt.Sprintf("保存 profiles 失败: %v", err))
} else {
logging.Default.Info("tui", "profiles 已保存")
}
return profileGridSavedMsg{err: err}
}
}
func (m Model) handleProfileGridKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.profileGridAddingKey {
switch msg.String() {
case "enter":
key := strings.TrimSpace(m.profileGridNewKey.Value())
m.profileGridAddingKey = false
if key != "" {
vals := make(map[string]string, len(m.profileGridData.Profiles))
for _, p := range m.profileGridData.Profiles {
vals[p] = ""
}
m.profileGridData.Rows = append(m.profileGridData.Rows, store.ProfileRow{Key: key, Values: vals})
m.profileGridRow = len(m.profileGridData.Rows) - 1
m.profileGridCol = 0
}
case "esc":
m.profileGridAddingKey = false
default:
var cmd tea.Cmd
m.profileGridNewKey, cmd = m.profileGridNewKey.Update(msg)
return m, cmd
}
return m, nil
}
if m.profileGridEditing {
switch msg.String() {
case "enter":
val := m.profileGridEdit.Value()
row := m.profileGridRow
if row < len(m.profileGridData.Rows) && m.profileGridCol >= 1 && m.profileGridCol <= len(m.profileGridData.Profiles) {
prof := m.profileGridData.Profiles[m.profileGridCol-1]
if m.profileGridData.Rows[row].Values == nil {
m.profileGridData.Rows[row].Values = map[string]string{}
}
m.profileGridData.Rows[row].Values[prof] = val
}
m.profileGridEditing = false
case "esc":
m.profileGridEditing = false
default:
var cmd tea.Cmd
m.profileGridEdit, cmd = m.profileGridEdit.Update(msg)
return m, cmd
}
return m, nil
}
switch msg.String() {
case "esc":
m.state = stateLoaded
case "up", "k":
if m.profileGridRow > 0 {
m.profileGridRow--
}
case "down", "j":
if m.profileGridRow < len(m.profileGridData.Rows)-1 {
m.profileGridRow++
}
case "left", "h":
if m.profileGridCol > 0 {
m.profileGridCol--
}
case "right", "l":
if m.profileGridCol < len(m.profileGridData.Profiles) {
m.profileGridCol++
}
case "enter":
if m.profileGridCol >= 1 && m.profileGridCol <= len(m.profileGridData.Profiles) &&
m.profileGridRow >= 0 && m.profileGridRow < len(m.profileGridData.Rows) {
prof := m.profileGridData.Profiles[m.profileGridCol-1]
curVal := m.profileGridData.Rows[m.profileGridRow].Values[prof]
ti := textinput.New()
ti.SetValue(curVal)
ti.Focus()
m.profileGridEdit = ti
m.profileGridEditing = true
}
case "a":
ti := textinput.New()
ti.Placeholder = "输入行 key"
ti.Focus()
m.profileGridNewKey = ti
m.profileGridAddingKey = true
case "ctrl+s":
return m, saveProfileGridCmd(m.profileGridData)
}
return m, nil
}
func (m Model) profileGridView() string {
var sb strings.Builder
sb.WriteString(helpStyle.Render("Profile Grid") + "\n")
sb.WriteString(strings.Repeat("─", 60) + "\n")
if len(m.profileGridData.Profiles) == 0 {
sb.WriteString(" (无 profile 列)\n")
} else {
header := fmt.Sprintf(" %-20s", "key")
for _, p := range m.profileGridData.Profiles {
header += fmt.Sprintf(" %-20s", p)
}
sb.WriteString(helpStyle.Render(header) + "\n")
sb.WriteString(" " + strings.Repeat("─", 20+22*len(m.profileGridData.Profiles)) + "\n")
for ri, row := range m.profileGridData.Rows {
line := ""
keyCell := fmt.Sprintf("%-20s", row.Key)
if ri == m.profileGridRow && m.profileGridCol == 0 {
keyCell = selectedCardStyle.Render(keyCell)
}
line += " " + keyCell
for ci, p := range m.profileGridData.Profiles {
val := row.Values[p]
cellStr := fmt.Sprintf("%-20s", val)
colIdx := ci + 1
if ri == m.profileGridRow && colIdx == m.profileGridCol {
if m.profileGridEditing {
cellStr = m.profileGridEdit.View()
} else {
cellStr = selectedCardStyle.Render(cellStr)
}
}
line += " " + cellStr
}
sb.WriteString(line + "\n")
}
}
if m.profileGridAddingKey {
sb.WriteString("\n新行 key: " + m.profileGridNewKey.View() + "\n")
}
sb.WriteString("\n")
sb.WriteString(helpStyle.Render("↑↓/jk ←→/hl navigate enter edit cell a add row ctrl+s save esc back"))
if m.statusMsg != "" {
sb.WriteString("\n" + helpStyle.Render(m.statusMsg))
}
return sb.String()
}