返回 DeepSeek-Reasonix
submit_route.go
根目录 / internal / control / submit_route.go
1 package control
2
3 import (
4 "strings"
5
6 "reasonix/internal/skill"
7 )
8
9 var managementNoticeCommands = map[string]struct{}{
10 "/model": {}, "/provider": {}, "/memory": {}, "/migrate": {}, "/migration": {},
11 "/skill": {}, "/skills": {}, "/plugin": {}, "/plugins": {}, "/reload-cmd": {},
12 "/hooks": {}, "/mcp": {},
13 }
14
15 // SubmitDisposition describes whether a raw submit is expected to create a
16 // durable conversational turn. Management commands can still mutate session
17 // state or emit notices, but they must not be presented as turn admission.
18 type SubmitDisposition string
19
20 const (
21 SubmitTurnStarted SubmitDisposition = "turn_started"
22 SubmitManagementHandled SubmitDisposition = "management_handled"
23 )
24
25 // ClassifySubmitRoute is the single controller-owned route classifier shared
26 // by desktop's turn receipt and the normal Submit path. Keep conditional
27 // commands here so a management-only form never gets mistaken for a turn.
28 func (c *Controller) ClassifySubmitRoute(input string) SubmitDisposition {
29 trimmed := strings.TrimSpace(input)
30 if isSimpleManagementInput(trimmed) {
31 return SubmitManagementHandled
32 }
33 return c.classifySlashSubmit(trimmed)
34 }
35
36 func isSimpleManagementInput(trimmed string) bool {
37 if trimmed == "" {
38 return false
39 }
40 if _, ok := MemoryQuickAddNote(trimmed); ok {
41 return true
42 }
43 if _, ok := RememberCommandNote(trimmed); ok {
44 return true
45 }
46 if goal, ok := ParseGoalCommand(trimmed); ok {
47 return goal.Action != GoalCommandSet
48 }
49 if trimmed == "/reload" || trimmed == "/effort" || strings.HasPrefix(trimmed, "/effort ") {
50 return true
51 }
52 // Focus-guided "/compact <focus>" runs the same asynchronous compaction as
53 // the bare form and never admits a turn (see Submit).
54 if strings.HasPrefix(trimmed, "/compact ") {
55 return true
56 }
57 switch trimmed {
58 case "/compact", "/context", "/new", "/clear":
59 return true
60 default:
61 return false
62 }
63 }
64
65 func (c *Controller) classifySlashSubmit(trimmed string) SubmitDisposition {
66 if _, ok := ParseFinalReadinessRecoveryCommand(trimmed); ok || !strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "!") {
67 return SubmitTurnStarted
68 }
69 if strings.HasPrefix(trimmed, "/mcp__") || c.isPathSubmit(trimmed) {
70 return SubmitTurnStarted
71 }
72 fields := strings.Fields(trimmed)
73 if len(fields) == 0 {
74 return SubmitTurnStarted
75 }
76 switch fields[0] {
77 case "/tree", "/branch", "/switch", "/rewind":
78 return SubmitManagementHandled
79 case "/plan-exec":
80 return c.classifyPlanExecSubmit()
81 case "/prometheus":
82 return classifyPrometheusSubmit(trimmed, fields[0])
83 }
84 if _, ok := managementNoticeCommands[fields[0]]; ok {
85 return SubmitManagementHandled
86 }
87 if IsBuiltinDocsSlash(fields[0], c.Commands(), c.SlashSkills()) {
88 return classifyDocsSubmit(trimmed, fields[0])
89 }
90 if sk, task, ok := c.resolveSkillInvocation(trimmed); ok && sk.RunAs == skill.RunSubagent && strings.TrimSpace(task) == "" {
91 return SubmitManagementHandled
92 }
93 return SubmitTurnStarted
94 }
95
96 func (c *Controller) isPathSubmit(trimmed string) bool {
97 if _, ok := FileRefLine(trimmed); ok {
98 return true
99 }
100 if _, ok := SlashPathLineRef(trimmed, c.workspaceRoot); ok {
101 return true
102 }
103 return SlashPathLikeLine(trimmed)
104 }
105
106 func (c *Controller) classifyPlanExecSubmit() SubmitDisposition {
107 return SubmitManagementHandled
108 }
109
110 func classifyPrometheusSubmit(trimmed, command string) SubmitDisposition {
111 args := strings.TrimSpace(strings.TrimPrefix(trimmed, command))
112 if args == "" || args == "--strict" {
113 return SubmitManagementHandled
114 }
115 if strings.HasPrefix(args, "--strict ") && strings.TrimSpace(strings.TrimPrefix(args, "--strict ")) == "" {
116 return SubmitManagementHandled
117 }
118 return SubmitTurnStarted
119 }
120
121 func classifyDocsSubmit(trimmed, command string) SubmitDisposition {
122 if strings.TrimSpace(strings.TrimPrefix(trimmed, command)) == "" {
123 return SubmitManagementHandled
124 }
125 return SubmitTurnStarted
126 }
127
128 // SubmitResult is returned by the result-capable desktop submit path. The
129 // legacy Submit* methods intentionally keep their void/error contracts.
130 type SubmitResult struct {
131 Disposition SubmitDisposition `json:"disposition"`
132 }
133
134 // SubmitDisplayWithResult preserves the existing asynchronous submit behavior
135 // while exposing the route disposition to hosts that need an admission receipt.
136 func (c *Controller) SubmitDisplayWithResult(display, input string) SubmitResult {
137 disposition := c.ClassifySubmitRoute(input)
138 c.SubmitDisplay(display, input)
139 return SubmitResult{Disposition: disposition}
140 }
141
141 lines GO