返回 DeepSeek-Reasonix
cli_resume_start.go
根目录 / internal / cli / cli_resume_start.go
1 package cli
2
3 import (
4 "errors"
5 "fmt"
6 "os"
7 "strings"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/control"
11 "reasonix/internal/i18n"
12 "reasonix/internal/session"
13 )
14
15 // headlessResumeTarget resolves --resume/--continue for the non-interactive
16 // agent path, before any heavy assembly, so --copy and the session lease are
17 // settled first. --resume takes precedence over --continue, matching the
18 // Resume call downstream, and accepts file paths, branch IDs, preview text,
19 // opaque machine session IDs (#7429) and final-format session identities.
20 // The int is a process exit code: 0 means the (possibly empty) target is
21 // usable, anything else is the code the command must exit with.
22 func headlessResumeTarget(resume string, cont, copySession bool) (cliResumeTarget, int) {
23 target := cliResumeTarget{}
24 if strings.TrimSpace(resume) != "" {
25 resolved, err := resolveSessionQuery(resolveCLISessionDir(), strings.TrimSpace(resume))
26 if err != nil {
27 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
28 return cliResumeTarget{}, 1
29 }
30 target = resolved
31 }
32 if target.empty() && cont {
33 continued, rc := continueResumeTarget()
34 if rc != 0 {
35 return cliResumeTarget{}, rc
36 }
37 target = continued
38 }
39 return target, copySessionExitCode(copySession, target)
40 }
41
42 // interactiveResumeTarget resolves the same selection for the TUI, where the
43 // picker sentinel opens the chooser instead of failing. resumeValue must
44 // already be normalized by normalizedResumeFlag.
45 func interactiveResumeTarget(resumeValue string, cont, copySession bool) (cliResumeTarget, int) {
46 var target cliResumeTarget
47 switch {
48 case resumeValue == resumePickerSentinel:
49 picked, rc := pickSessionToResume()
50 if rc != 0 {
51 return cliResumeTarget{}, rc
52 }
53 target = picked
54 case resumeValue != "":
55 resolved, err := resolveSessionQuery(resolveCLISessionDir(), resumeValue)
56 if err != nil {
57 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
58 return cliResumeTarget{}, 1
59 }
60 target = resolved
61 case cont:
62 continued, rc := continueResumeTarget()
63 if rc != 0 {
64 return cliResumeTarget{}, rc
65 }
66 target = continued
67 }
68 return target, copySessionExitCode(copySession, target)
69 }
70
71 // normalizedResumeFlag maps the bare-flag spellings of --resume onto the
72 // values the resolver understands. The result also decides the telemetry
73 // session mode, so it is computed once and shared.
74 func normalizedResumeFlag(resume string) string {
75 value := strings.TrimSpace(resume)
76 switch strings.ToLower(value) {
77 case "true":
78 return resumePickerSentinel
79 case "false":
80 return ""
81 }
82 return value
83 }
84
85 // continueResumeTarget implements --continue: sweep the recovery branches this
86 // workspace left behind, then take the newest conversation.
87 func continueResumeTarget() (cliResumeTarget, int) {
88 sessionDir := resolveCLISessionDir()
89 reclaimCLIRecoveryBranches(sessionDir)
90 target, ok := newestResumeTarget(sessionDir)
91 if !ok {
92 fmt.Fprintln(os.Stderr, i18n.M.NoSessionToResume)
93 return cliResumeTarget{}, 1
94 }
95 return target, 0
96 }
97
98 // copySessionExitCode validates --copy against the resolved target: forking
99 // needs something to fork, and a final-format session has no transcript file
100 // to copy yet.
101 func copySessionExitCode(copySession bool, target cliResumeTarget) int {
102 switch {
103 case !copySession:
104 return 0
105 case target.empty():
106 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, "--copy requires --resume or --continue")
107 case target.canonical():
108 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, "--copy does not support final-format sessions yet")
109 default:
110 return 0
111 }
112 return 2
113 }
114
115 // commitStartupResume binds the resolved target to ctrl. A final-format
116 // session whose writer is held elsewhere is taken over only when approve
117 // agrees, which is the sole difference between --takeover and the TUI prompt.
118 func commitStartupResume(binding *cliTakeoverBinding, manager *cliTakeoverManager, ctrl *control.Controller,
119 resumed *agent.Session, target cliResumeTarget, approve func(error) bool) error {
120 err := commitResumedSession(binding, manager, ctrl, resumed, target)
121 if err == nil || !target.canonical() || !errors.Is(err, session.ErrWriterOwned) || !approve(err) {
122 return err
123 }
124 return cliStartupCanonicalTakeover(ctrl, manager, target)
125 }
126
127 // flagTakeoverApproval answers for --takeover, which commits before the
128 // conflict is known.
129 func flagTakeoverApproval(enabled bool) func(error) bool {
130 return func(error) bool { return enabled }
131 }
132
133 // promptTakeoverApproval asks on the terminal; a non-interactive start declines.
134 func promptTakeoverApproval(err error) bool {
135 return isInteractive() && promptSessionTakeover(err)
136 }
137
137 lines GO