package tui import ( "context" "fmt" "path/filepath" "strings" tea "charm.land/bubbletea/v2" "superwork-tui/internal/auth" "superwork-tui/internal/cc" "superwork-tui/internal/config" "superwork-tui/internal/files" "superwork-tui/internal/git" "superwork-tui/internal/gitea" "superwork-tui/internal/issue" ) type branchSyncStatusMsg struct { status git.BranchSyncStatus err error } type branchSyncDoneMsg struct { devBranch string autoBuildBranch string err error } type envLockDoneMsg struct { locked bool result files.EnvLockResult err error } type prDiffResultMsg struct { issueNumber int prDiffFile string err error } // branchSyncCheckCmd checks branch sync status and returns branchSyncStatusMsg. func branchSyncCheckCmd() tea.Cmd { return func() tea.Msg { ctx := context.Background() root, err := workspaceRootFn() if err != nil { return branchSyncStatusMsg{err: err} } settings, err := config.Load() if err != nil { return branchSyncStatusMsg{err: fmt.Errorf("load settings: %w", err)} } // Empty autoBuildBranch means "follow devBranch"; resolving here makes // the check fall into the same-branch (Unavailable) path, matching TS. autoBuildBranch := settings.AutoBuildBranch if autoBuildBranch == "" { autoBuildBranch = settings.DevBranch } status, err := git.CheckBranchSync(ctx, git.BranchSyncOpts{ WorkspaceRoot: root, DevBranch: settings.DevBranch, AutoBuildBranch: autoBuildBranch, }) return branchSyncStatusMsg{status: status, err: err} } } // branchSyncRunCmd pushes devBranch to autoBuildBranch, resolving workspace root internally. func branchSyncRunCmd(devBranch, autoBuildBranch string) tea.Cmd { return func() tea.Msg { ctx := context.Background() root, err := workspaceRootFn() if err != nil { return branchSyncDoneMsg{devBranch: devBranch, autoBuildBranch: autoBuildBranch, err: err} } err = git.RunBranchSync(ctx, git.BranchSyncOpts{ WorkspaceRoot: root, DevBranch: devBranch, AutoBuildBranch: autoBuildBranch, }) return branchSyncDoneMsg{devBranch: devBranch, autoBuildBranch: autoBuildBranch, err: err} } } // envLockToggleCmd locks or unlocks .env files based on current state. func envLockToggleCmd(currentlyLocked bool) tea.Cmd { return func() tea.Msg { root, err := workspaceRootFn() if err != nil { return envLockDoneMsg{err: err} } var result files.EnvLockResult if currentlyLocked { result, err = files.UnlockEnvFiles(root) return envLockDoneMsg{locked: false, result: result, err: err} } result, err = files.LockEnvFiles(root) return envLockDoneMsg{locked: true, result: result, err: err} } } // prDiffSummaryCmd asks Claude to summarize the PR diff and write it to docs/pr-diff/.md. func prDiffSummaryCmd(iss issue.Issue) tea.Cmd { return func() tea.Msg { ctx := context.Background() root, err := workspaceRootFn() if err != nil { return prDiffResultMsg{issueNumber: iss.Number, err: err} } safeBranch := strings.ReplaceAll(iss.Branch, "/", "-") outFile := filepath.Join("docs", "pr-diff", safeBranch+".md") prompt := fmt.Sprintf( "请阅读 PR #%s(分支 %s)的变更,并将综合摘要写入 %s。"+ "摘要应包括:改动内容、改动原因、关键技术决策以及测试方案。", iss.PR, iss.Branch, outFile, ) _, spawnErr := cc.SpawnClaude(ctx, cc.Opts{ Prompt: prompt, Cwd: root, }) if spawnErr != nil { return prDiffResultMsg{issueNumber: iss.Number, err: spawnErr} } // Persist prDiffFile to Gitea state JSON (best-effort). remote, rErr := git.DetectRepo(root) if rErr == nil { token, tErr := auth.ResolveGiteaToken(remote.Host) if tErr == nil { client := gitea.New(remote.Host, token) _ = issue.MergeStateJSON(ctx, client, remote.Owner, remote.Repo, iss.Number, map[string]any{ "prDiffFile": outFile, }) } } return prDiffResultMsg{issueNumber: iss.Number, prDiffFile: outFile} } }