@@ -390,6 +390,8 @@ type AppModel struct {
390390 prevStatuses map [int64 ]string
391391 // Track tasks with active input notifications (for UI highlighting)
392392 tasksNeedingInput map [int64 ]bool
393+ // Track which tasks have question prompts (vs permission prompts)
394+ questionPrompts map [int64 ]bool
393395 // Cached executor prompt messages for blocked tasks (from DB hook logs)
394396 executorPrompts map [int64 ]string
395397 // Track tasks the user closed manually (suppress notification for these)
@@ -610,6 +612,7 @@ func NewAppModel(database *db.DB, exec *executor.Executor, workingDir string, ve
610612 loading : true ,
611613 prevStatuses : make (map [int64 ]string ),
612614 tasksNeedingInput : make (map [int64 ]bool ),
615+ questionPrompts : make (map [int64 ]bool ),
613616 lastViewedAt : make (map [int64 ]time.Time ),
614617 executorPrompts : make (map [int64 ]string ),
615618 userClosedTaskIDs : make (map [int64 ]bool ),
@@ -827,9 +830,12 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
827830 // Handles external approval (e.g. from tmux) where PreToolUse
828831 // logs "Agent resumed working" and transitions to processing.
829832 if m .tasksNeedingInput [t .ID ] {
830- if m .latestChoicePrompt (t .ID ) == "" {
833+ if prompt , isQ := m .latestChoicePrompt (t .ID ); prompt == "" {
831834 delete (m .tasksNeedingInput , t .ID )
835+ delete (m .questionPrompts , t .ID )
832836 delete (m .executorPrompts , t .ID )
837+ } else {
838+ m .questionPrompts [t .ID ] = isQ
833839 }
834840 }
835841 }
@@ -851,20 +857,25 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
851857 for _ , t := range m .tasks {
852858 if t .Status == db .StatusDone || t .Status == db .StatusBacklog {
853859 delete (m .tasksNeedingInput , t .ID )
860+ delete (m .questionPrompts , t .ID )
854861 delete (m .executorPrompts , t .ID )
855862 continue
856863 }
857864 if m .tasksNeedingInput [t .ID ] {
858865 // Re-validate: if task is no longer blocked, the user provided input
859866 // (e.g., from the detail view tmux pane). Also re-check permission prompts.
860- if t . Status != db .StatusBlocked && m . latestChoicePrompt ( t . ID ) == "" {
867+ if prompt , isQ := m . latestChoicePrompt ( t . ID ); t . Status != db .StatusBlocked && prompt == "" {
861868 delete (m .tasksNeedingInput , t .ID )
869+ delete (m .questionPrompts , t .ID )
862870 delete (m .executorPrompts , t .ID )
871+ } else {
872+ m .questionPrompts [t .ID ] = isQ
863873 }
864874 continue
865875 }
866- if prompt := m .latestChoicePrompt (t .ID ); prompt != "" {
876+ if prompt , isQ := m .latestChoicePrompt (t .ID ); prompt != "" {
867877 m .tasksNeedingInput [t .ID ] = true
878+ m .questionPrompts [t .ID ] = isQ
868879 // Capture the tmux pane content for richer display of the prompt.
869880 // This shows the actual executor output (including multiple choice options)
870881 // rather than just the hook log summary.
@@ -1179,6 +1190,7 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
11791190 // still blocked (e.g. another prompt queued), the latestChoicePrompt
11801191 // catch-up loop will re-detect it on the next poll cycle.
11811192 delete (m .tasksNeedingInput , msg .taskID )
1193+ delete (m .questionPrompts , msg .taskID )
11821194 delete (m .executorPrompts , msg .taskID )
11831195 }
11841196 cmds = append (cmds , m .loadTasks ())
@@ -1219,12 +1231,16 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
12191231 // newly-blocked tasks so the user can approve/deny immediately
12201232 // without waiting for the next loadTasks poll.
12211233 if m .tasksNeedingInput [event .TaskID ] {
1222- if m .latestChoicePrompt (event .TaskID ) == "" {
1234+ if prompt , isQ := m .latestChoicePrompt (event .TaskID ); prompt == "" {
12231235 delete (m .tasksNeedingInput , event .TaskID )
1236+ delete (m .questionPrompts , event .TaskID )
12241237 delete (m .executorPrompts , event .TaskID )
1238+ } else {
1239+ m .questionPrompts [event .TaskID ] = isQ
12251240 }
1226- } else if prompt := m .latestChoicePrompt (event .TaskID ); prompt != "" {
1241+ } else if prompt , isQ := m .latestChoicePrompt (event .TaskID ); prompt != "" {
12271242 m .tasksNeedingInput [event .TaskID ] = true
1243+ m .questionPrompts [event .TaskID ] = isQ
12281244 paneContent := executor .CapturePaneContent (event .TaskID , 15 )
12291245 if paneContent != "" {
12301246 m .executorPrompts [event .TaskID ] = paneContent
@@ -1739,7 +1755,12 @@ func (m *AppModel) renderExecutorPromptPreview(task *db.Task) string {
17391755 Width (m .width ).
17401756 Padding (0 , 1 )
17411757
1742- hints := hintStyle .Render ("y approve N deny tab input enter detail" )
1758+ var hints string
1759+ if m .questionPrompts [task .ID ] {
1760+ hints = hintStyle .Render ("tab reply enter detail" )
1761+ } else {
1762+ hints = hintStyle .Render ("y approve N deny tab input enter detail" )
1763+ }
17431764
17441765 prompt := m .executorPrompts [task .ID ]
17451766 promptLines := extractPromptLines (prompt , m .width - 10 )
@@ -3486,6 +3507,7 @@ func (m *AppModel) updateRetry(msg tea.Msg) (tea.Model, tea.Cmd) {
34863507 taskID := m .retryView .task .ID
34873508 // Clear kanban notification immediately for instant UI feedback
34883509 delete (m .tasksNeedingInput , taskID )
3510+ delete (m .questionPrompts , taskID )
34893511 delete (m .executorPrompts , taskID )
34903512 m .kanban .SetTasksNeedingInput (m .tasksNeedingInput )
34913513 // Clear the notification banner if it's for this task
@@ -4156,29 +4178,43 @@ func (m *AppModel) sendTextToExecutor(taskID int64, text string) tea.Cmd {
41564178// latestChoicePrompt checks whether a task has a pending permission/choice prompt
41574179// by reading recent DB logs written by the notification hook. Returns the prompt
41584180// message if still pending, or "" if resolved (e.g. "Agent resumed working",
4159- // user approved/denied). This is status-agnostic — works for any active task.
4160- // Only matches "Waiting for permission" entries (actual choice prompts), NOT
4181+ // user approved/denied). The second return value isQuestion is true when the
4182+ // prompt is a question (from MCP taskyou_needs_input) rather than a permission
4183+ // prompt. Question prompts have different resolution semantics: they are NOT
4184+ // cleared by "Agent resumed working" or tool logs, only by explicit user reply.
4185+ // Only matches "Waiting for permission" and "question" entries, NOT
41614186// "Waiting for user input" (generic idle/end_turn scenarios).
4162- func (m * AppModel ) latestChoicePrompt (taskID int64 ) string {
4187+ func (m * AppModel ) latestChoicePrompt (taskID int64 ) ( string , bool ) {
41634188 logs , err := m .db .GetTaskLogs (taskID , 10 )
41644189 if err != nil {
4165- return ""
4190+ return "" , false
41664191 }
41674192 // Logs are in DESC order (most recent first).
41684193 // A pending prompt is only valid if no subsequent log indicates resolution.
4194+ // For permission prompts: "Agent resumed working", tool logs, and user
4195+ // approve/deny all resolve the prompt.
4196+ // For question prompts: only an explicit user reply resolves it.
4197+ permissionResolved := false
41694198 for _ , l := range logs {
41704199 switch {
4200+ case l .LineType == "question" :
4201+ // Question prompts are only resolved by explicit user reply,
4202+ // not by "Agent resumed working" or tool logs.
4203+ return l .Content , true
41714204 case l .LineType == "system" && strings .HasPrefix (l .Content , "Waiting for permission" ):
4172- return l .Content
4205+ if permissionResolved {
4206+ return "" , false
4207+ }
4208+ return l .Content , false
41734209 case l .LineType == "system" && (l .Content == "Agent resumed working" || l .Content == "Claude resumed working" ):
4174- return "" // prompt was resolved
4210+ permissionResolved = true
41754211 case l .LineType == "user" && (strings .HasPrefix (l .Content , "Approved" ) || strings .HasPrefix (l .Content , "Denied" ) || strings .HasPrefix (l .Content , "Replied" )):
4176- return "" // user already responded
4212+ return "" , false // user already responded (resolves both types)
41774213 case l .LineType == "tool" :
4178- return "" // a tool ran after the permission prompt, so it was resolved
4214+ permissionResolved = true
41794215 }
41804216 }
4181- return ""
4217+ return "" , false
41824218}
41834219
41844220// detectPermissionPrompt does a live DB check for a pending permission prompt
@@ -4187,11 +4223,12 @@ func (m *AppModel) latestChoicePrompt(taskID int64) string {
41874223// poll-based detection hasn't caught up yet (e.g. a real-time event showed the
41884224// task as blocked before loadTasks detected the permission prompt).
41894225func (m * AppModel ) detectPermissionPrompt (taskID int64 ) bool {
4190- prompt := m .latestChoicePrompt (taskID )
4226+ prompt , isQ := m .latestChoicePrompt (taskID )
41914227 if prompt == "" {
41924228 return false
41934229 }
41944230 m .tasksNeedingInput [taskID ] = true
4231+ m .questionPrompts [taskID ] = isQ
41954232 paneContent := executor .CapturePaneContent (taskID , 15 )
41964233 if paneContent != "" {
41974234 m .executorPrompts [taskID ] = paneContent
0 commit comments