66 "context"
77 "fmt"
88 "log"
9+ "strings"
910 "sync"
1011
1112 wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
@@ -84,14 +85,20 @@ func (ar *AgentRunner) bufferEvent(taskID string, event claude.TaskStreamEvent)
8485
8586// RunTaskOptions configures a RunTask invocation.
8687type RunTaskOptions struct {
87- SessionID string // Claude session ID for --resume (empty = new session)
88- Prompt string // Override task prompt (used for follow-ups)
89- OnSessionID func (sessionID string ) // Callback when Claude session_id is received
88+ SessionID string // Claude session ID for --resume (empty = new session)
89+ Prompt string // Override task prompt (used for follow-ups)
90+ OnSessionID func (sessionID string ) // Callback when Claude session_id is received
91+ }
92+
93+ // RunResult carries information about how the task run completed.
94+ type RunResult struct {
95+ NeedsInput bool // true if the agent's output indicates it needs user input
96+ LastText string // the last text output from the agent (for displaying in the UI)
9097}
9198
9299// RunTask starts a Claude Code process for a task with the given agent configuration.
93100// It streams events to the frontend and returns when the process completes.
94- func (ar * AgentRunner ) RunTask (ctx context.Context , task * models.Task , agent * models.Agent , workDir string , opts ... RunTaskOptions ) error {
101+ func (ar * AgentRunner ) RunTask (ctx context.Context , task * models.Task , agent * models.Agent , workDir string , opts ... RunTaskOptions ) ( * RunResult , error ) {
95102 var runOpts RunTaskOptions
96103 if len (opts ) > 0 {
97104 runOpts = opts [0 ]
@@ -113,7 +120,7 @@ func (ar *AgentRunner) RunTask(ctx context.Context, task *models.Task, agent *mo
113120 SessionID : runOpts .SessionID ,
114121 })
115122 if err != nil {
116- return fmt .Errorf ("start claude (%s): %w" , ar .cliPath , err )
123+ return nil , fmt .Errorf ("start claude (%s): %w" , ar .cliPath , err )
117124 }
118125
119126 ar .mu .Lock ()
@@ -126,8 +133,9 @@ func (ar *AgentRunner) RunTask(ctx context.Context, task *models.Task, agent *mo
126133 ar .mu .Unlock ()
127134 }()
128135
129- // Stream events to frontend
136+ // Stream events to frontend, track last text for question detection
130137 eventCount := 0
138+ var lastText string
131139 for event := range proc .Events () {
132140 eventCount ++
133141 if eventCount <= 3 || eventCount % 10 == 0 {
@@ -141,9 +149,20 @@ func (ar *AgentRunner) RunTask(ctx context.Context, task *models.Task, agent *mo
141149
142150 ar .emitTaskEvent (task .ID , event )
143151
152+ // Track last text content for question detection
153+ if event .Type == "assistant" {
154+ text := claude .ExtractTextContent (event )
155+ if text != "" {
156+ lastText = text
157+ }
158+ }
159+
144160 // Capture result text
145161 if event .Type == "result" {
146162 task .ResultText = event .Result
163+ if event .Result != "" {
164+ lastText = event .Result
165+ }
147166 }
148167 }
149168 log .Printf ("[runner] task %s: stream ended after %d events" , task .ID [:8 ], eventCount )
@@ -165,11 +184,60 @@ func (ar *AgentRunner) RunTask(ctx context.Context, task *models.Task, agent *mo
165184 if proc .Err () != nil {
166185 stderrOutput := proc .Stderr ()
167186 if stderrOutput != "" {
168- return fmt .Errorf ("claude process: %w\n stderr: %s" , proc .Err (), stderrOutput )
187+ return nil , fmt .Errorf ("claude process: %w\n stderr: %s" , proc .Err (), stderrOutput )
188+ }
189+ return nil , fmt .Errorf ("claude process: %w" , proc .Err ())
190+ }
191+
192+ // Detect if the agent's output indicates it needs user input
193+ result := & RunResult {
194+ LastText : lastText ,
195+ NeedsInput : detectNeedsInput (lastText ),
196+ }
197+ return result , nil
198+ }
199+
200+ // detectNeedsInput checks if the agent's last output looks like it's asking for user input.
201+ // Only checks the last paragraph to avoid false positives from questions in the middle of output.
202+ func detectNeedsInput (text string ) bool {
203+ if text == "" {
204+ return false
205+ }
206+ trimmed := strings .TrimSpace (text )
207+
208+ // Only look at the last paragraph (after last double newline) to reduce false positives.
209+ // Agents often have questions mid-output but the final paragraph is what matters.
210+ if idx := strings .LastIndex (trimmed , "\n \n " ); idx >= 0 {
211+ trimmed = strings .TrimSpace (trimmed [idx :])
212+ }
213+
214+ // Check if the last paragraph ends with a question mark
215+ if strings .HasSuffix (trimmed , "?" ) {
216+ return true
217+ }
218+
219+ lower := strings .ToLower (trimmed )
220+
221+ // Common patterns indicating the agent wants approval/input (checked in last paragraph only)
222+ inputPatterns := []string {
223+ "approve" , "approval" , "onay" ,
224+ "ready for review" , "ready to proceed" ,
225+ "which option" , "hangi seçenek" , "hangisini" ,
226+ "should i proceed" , "devam edeyim mi" ,
227+ "waiting for" , "bekliyor" ,
228+ "please confirm" , "lütfen onaylayın" ,
229+ "let me know" , "bana bildirin" ,
230+ "what do you think" , "ne düşünüyorsun" ,
231+ "do you want" , "ister misin" ,
232+ "select one" , "birini seç" ,
233+ }
234+ for _ , pattern := range inputPatterns {
235+ if strings .Contains (lower , pattern ) {
236+ return true
169237 }
170- return fmt .Errorf ("claude process: %w" , proc .Err ())
171238 }
172- return nil
239+
240+ return false
173241}
174242
175243// StopTask kills the Claude process for a specific task.
0 commit comments