11
This commit is contained in:
commit
e6f1776d4f
264 files changed
+54215
No files matched your search
@@ -0,0 +1,283 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"superwork-tui/internal/auth"
|
||||
"superwork-tui/internal/cc"
|
||||
"superwork-tui/internal/config"
|
||||
"superwork-tui/internal/git"
|
||||
"superwork-tui/internal/gitea"
|
||||
"superwork-tui/internal/issue"
|
||||
)
|
||||
|
||||
// watchBrainstormFn is injectable for tests; production code uses the real watcher.
|
||||
var watchBrainstormFn = func(projectsDir string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
sid, err := cc.WatchForNewSession(ctx, cc.SessionWatchOpts{
|
||||
ProjectsDir: projectsDir,
|
||||
Timeout: 120 * time.Second,
|
||||
})
|
||||
return sid, err
|
||||
}
|
||||
|
||||
type reviewResultMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type testResultMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type testSessionCapturedMsg struct {
|
||||
issueNumber int
|
||||
sessionID string
|
||||
}
|
||||
|
||||
// reviewCmd opens/focuses the review session using codex resume.
|
||||
func reviewCmd(iss issue.Issue, sm *SessionManager) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if iss.ReviewSessionID == "" {
|
||||
return reviewResultMsg{err: fmt.Errorf("issue #%d has no reviewSessionId", iss.Number)}
|
||||
}
|
||||
|
||||
workspaceRoot, err := workspaceRootFn()
|
||||
if err != nil {
|
||||
return reviewResultMsg{err: fmt.Errorf("workspace root: %w", err)}
|
||||
}
|
||||
|
||||
if iss.WorktreePath == "" {
|
||||
return reviewResultMsg{err: fmt.Errorf("审查会话无法恢复 #%d: worktree 不存在", iss.Number)}
|
||||
}
|
||||
abs := filepath.Join(workspaceRoot, iss.WorktreePath)
|
||||
if _, err := os.Stat(abs); err != nil {
|
||||
return reviewResultMsg{err: fmt.Errorf("审查会话无法恢复 #%d: worktree 不存在", iss.Number)}
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("codex resume -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox %s", shellQuote(iss.ReviewSessionID))
|
||||
|
||||
if err := sm.OpenOrFocus(context.Background(), PhaseReview, iss, abs, cmd); err != nil {
|
||||
return reviewResultMsg{err: fmt.Errorf("open review session: %w", err)}
|
||||
}
|
||||
return reviewResultMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
// resolveTestSessionCwd returns worktree abs path if it exists on disk, else workspaceRoot.
|
||||
func resolveTestSessionCwd(workspaceRoot string, iss issue.Issue) string {
|
||||
if iss.WorktreePath != "" {
|
||||
abs := filepath.Join(workspaceRoot, iss.WorktreePath)
|
||||
if _, err := os.Stat(abs); err == nil {
|
||||
return abs
|
||||
}
|
||||
}
|
||||
return workspaceRoot
|
||||
}
|
||||
|
||||
// testCmd starts or resumes the test session.
|
||||
func testCmd(iss issue.Issue, sm *SessionManager) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
workspaceRoot, err := workspaceRootFn()
|
||||
if err != nil {
|
||||
return testResultMsg{err: fmt.Errorf("workspace root: %w", err)}
|
||||
}
|
||||
|
||||
cwd := resolveTestSessionCwd(workspaceRoot, iss)
|
||||
|
||||
settings, err := config.Load()
|
||||
if err != nil {
|
||||
settings = config.DefaultSettings()
|
||||
}
|
||||
|
||||
profilePath := iss.TestProfilePath
|
||||
if profilePath == "" {
|
||||
profilePath = iss.ProfilePath
|
||||
}
|
||||
|
||||
var command string
|
||||
if iss.TestSessionID != "" {
|
||||
command = claudeCmd(claudeCmdOpts{
|
||||
ResumeSessionID: iss.TestSessionID,
|
||||
ProfilePath: profilePath,
|
||||
SystemPromptCommand: settings.SystemPromptCommand,
|
||||
})
|
||||
} else {
|
||||
inWorktree := cwd != workspaceRoot
|
||||
if !inWorktree && iss.PR == "" {
|
||||
return testResultMsg{err: fmt.Errorf("issue #%d: no worktree and no PR for test session", iss.Number)}
|
||||
}
|
||||
var prompt string
|
||||
if inWorktree {
|
||||
prompt = "当前工单的改动代码就在这个 worktree 工作目录里,无需用 tea 拉取,直接开始:怎么在本地测试以确保本次改动符合预期?"
|
||||
} else {
|
||||
prompt = fmt.Sprintf("我已经合并了#%s号pr,你先通过tea熟悉本次pr改动的代码,然后,怎么在本地测试以确保改动符合预期?", iss.PR)
|
||||
}
|
||||
command = claudeCmd(claudeCmdOpts{
|
||||
Prompt: prompt,
|
||||
ProfilePath: profilePath,
|
||||
SystemPromptCommand: settings.SystemPromptCommand,
|
||||
})
|
||||
}
|
||||
|
||||
if err := sm.OpenOrFocus(context.Background(), PhaseTest, iss, cwd, command); err != nil {
|
||||
return testResultMsg{err: fmt.Errorf("open test session: %w", err)}
|
||||
}
|
||||
return testResultMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
// watchTestSessionCmd watches for a new claude session in the test cwd's projects dir.
|
||||
func watchTestSessionCmd(issueNumber int, cwd string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return testSessionCapturedMsg{issueNumber: issueNumber}
|
||||
}
|
||||
projectsDir := cc.ClaudeProjectsDir(home, cwd)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
sid, _ := cc.WatchForNewSession(ctx, cc.SessionWatchOpts{
|
||||
ProjectsDir: projectsDir,
|
||||
Timeout: 120 * time.Second,
|
||||
})
|
||||
return testSessionCapturedMsg{issueNumber: issueNumber, sessionID: sid}
|
||||
}
|
||||
}
|
||||
|
||||
// writeTestSessionCmd persists the captured testSessionId to Gitea state JSON.
|
||||
func writeTestSessionCmd(issueNumber int, sessionID string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
root, err := workspaceRootFn()
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
remote, err := git.DetectRepo(root)
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
token, err := auth.ResolveGiteaToken(remote.Host)
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
client := gitea.New(remote.Host, token)
|
||||
_ = issue.MergeStateJSON(ctx, client, remote.Owner, remote.Repo, issueNumber, map[string]any{
|
||||
"testSessionId": sessionID,
|
||||
})
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
// InjectFeedback tries impl window first, falls back to test window.
|
||||
// Returns true if injection succeeded.
|
||||
func InjectFeedback(sm *SessionManager, iss issue.Issue, text string) bool {
|
||||
ctx := context.Background()
|
||||
session := resolveSessionName()
|
||||
|
||||
implWin := windowName(iss.Number, PhaseImpl)
|
||||
exists, err := sm.windowExists(session, implWin)
|
||||
if err == nil && exists {
|
||||
if injectErr := sm.Inject(ctx, PhaseImpl, iss, text); injectErr == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
testWin := windowName(iss.Number, PhaseTest)
|
||||
exists, err = sm.windowExists(session, testWin)
|
||||
if err == nil && exists {
|
||||
if injectErr := sm.Inject(ctx, PhaseTest, iss, text); injectErr == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// brainstormStartCmd starts a new brainstorm session when no SessionID exists.
|
||||
// brainstormStartedMsg is returned by brainstormStartCmd on successful window open.
|
||||
// The model uses it to start the session-capture watcher.
|
||||
type brainstormStartedMsg struct {
|
||||
issueNumber int
|
||||
workspaceRoot string
|
||||
}
|
||||
|
||||
// brainstormSessionCapturedMsg carries the captured sessionId for a brainstorm session.
|
||||
type brainstormSessionCapturedMsg struct {
|
||||
issueNumber int
|
||||
sessionID string
|
||||
}
|
||||
|
||||
// brainstormStartCmd starts a fresh brainstorm session (no existing SessionID).
|
||||
// On success it returns brainstormStartedMsg so the model can launch the watcher.
|
||||
func brainstormStartCmd(iss issue.Issue, sm *SessionManager) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
workspaceRoot, err := workspaceRootFn()
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{err: fmt.Errorf("workspace root: %w", err)}
|
||||
}
|
||||
|
||||
settings, err := config.Load()
|
||||
if err != nil {
|
||||
settings = config.DefaultSettings()
|
||||
}
|
||||
|
||||
prompt := cc.BrainstormContinuePrompt(settings, struct{ IssueNumber string }{
|
||||
IssueNumber: strconv.Itoa(iss.Number),
|
||||
})
|
||||
|
||||
command := claudeCmd(claudeCmdOpts{
|
||||
Prompt: prompt,
|
||||
ProfilePath: iss.ProfilePath,
|
||||
SystemPromptCommand: settings.SystemPromptCommand,
|
||||
})
|
||||
|
||||
if err := sm.OpenOrFocus(context.Background(), PhaseBrain, iss, workspaceRoot, command); err != nil {
|
||||
return sessionActionResultMsg{err: fmt.Errorf("open brainstorm: %w", err)}
|
||||
}
|
||||
return brainstormStartedMsg{issueNumber: iss.Number, workspaceRoot: workspaceRoot}
|
||||
}
|
||||
}
|
||||
|
||||
// watchBrainstormSessionCmd watches for a new claude session in the workspace projects dir.
|
||||
func watchBrainstormSessionCmd(issueNumber int, workspaceRoot string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return brainstormSessionCapturedMsg{issueNumber: issueNumber}
|
||||
}
|
||||
projectsDir := cc.ClaudeProjectsDir(home, workspaceRoot)
|
||||
sid, _ := watchBrainstormFn(projectsDir)
|
||||
return brainstormSessionCapturedMsg{issueNumber: issueNumber, sessionID: sid}
|
||||
}
|
||||
}
|
||||
|
||||
// writeBrainstormSessionCmd persists the captured sessionId to Gitea state JSON.
|
||||
func writeBrainstormSessionCmd(issueNumber int, sessionID string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
root, err := workspaceRootFn()
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
remote, err := git.DetectRepo(root)
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
token, err := auth.ResolveGiteaToken(remote.Host)
|
||||
if err != nil {
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
client := gitea.New(remote.Host, token)
|
||||
_ = issue.MergeStateJSON(ctx, client, remote.Owner, remote.Repo, issueNumber, map[string]any{
|
||||
"sessionId": sessionID,
|
||||
})
|
||||
return sessionActionResultMsg{}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user