@@ -67,6 +67,7 @@ export class Agent {
6767 this . _progressSessionCounter = 0 ;
6868 this . conversationIds = new Map ( ) ; // tabId -> stable conversationId (regenerated on clearConversation)
6969 this . conversationModes = new Map ( ) ; // tabId -> 'ask' | 'act' | 'dev'
70+ this . plannerFollowUpSkipTabs = new Set ( ) ; // tabIds allowed one short follow-up after an approved try-mode plan
7071 this . hydratedTabs = new Set ( ) ; // tabIds we've already pulled from storage
7172 this . persistTimers = new Map ( ) ; // tabId -> debounce handle
7273 this . abortFlags = new Map ( ) ; // tabId -> boolean
@@ -281,6 +282,7 @@ export class Agent {
281282 }
282283
283284 clearScratchpad ( tabId ) {
285+ this . plannerFollowUpSkipTabs . delete ( tabId ) ;
284286 const messages = this . conversations . get ( tabId ) ;
285287 if ( ! messages ) {
286288 return { success : true , existed : false , note : 'scratchpad already empty' } ;
@@ -3174,6 +3176,44 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
31743176 return this . _plannerMode ( ) !== 'off' ;
31753177 }
31763178
3179+ _messageHasPlannerFollowUpAttachmentBlocks ( message ) {
3180+ const content = message ?. content ?? message ;
3181+ if ( ! Array . isArray ( content ) ) return false ;
3182+ return content . some ( block => {
3183+ if ( ! block || typeof block !== 'object' ) return false ;
3184+ if ( block . type !== 'text' ) return true ;
3185+ const text = String ( block . text || '' ) ;
3186+ return text . startsWith ( '[UNTRUSTED USER ATTACHMENTS' ) || text . startsWith ( '[UNTRUSTED DOCUMENT' ) ;
3187+ } ) ;
3188+ }
3189+
3190+ _plannerFollowUpText ( message ) {
3191+ const text = this . _stripInjectedTaskContext ( userMessageToText ( message ) ) ;
3192+ return text . replace ( / \s + / g, ' ' ) . trim ( ) ;
3193+ }
3194+
3195+ _plannerFollowUpHasExplicitUrl ( text ) {
3196+ return / (?: h t t p s ? : \/ \/ | w w w \. ) \S + | \b (?: [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] { 0 , 61 } [ a - z 0 - 9 ] ) ? \. ) + [ a - z ] { 2 , 63 } (?: \/ [ ^ \s ] * ) ? / i. test ( String ( text || '' ) ) ;
3197+ }
3198+
3199+ _hasApprovedPlannerHandoff ( messages ) {
3200+ const idx = this . _findScratchpadIndex ( messages || [ ] ) ;
3201+ if ( idx < 0 ) return false ;
3202+ const body = this . _extractScratchpadBody ( messages [ idx ] . content ) ;
3203+ return / \[ A p p r o v e d p l a n \b [ ^ \] ] * p i n n e d b y p l a n n e r \] / . test ( body ) ;
3204+ }
3205+
3206+ _shouldSkipPlannerForShortFollowUp ( tabId , priorMessages , enriched , plannerMode ) {
3207+ if ( plannerMode !== 'try' ) return false ;
3208+ if ( this . _normalizePlanReviewMode ( this . planReviewMode ) === 'always' ) return false ;
3209+ if ( ! this . plannerFollowUpSkipTabs . has ( tabId ) ) return false ;
3210+ if ( ! this . _hasApprovedPlannerHandoff ( priorMessages ) ) return false ;
3211+ if ( this . _messageHasPlannerFollowUpAttachmentBlocks ( enriched ) ) return false ;
3212+ const text = this . _plannerFollowUpText ( enriched ) ;
3213+ if ( ! text || text . length > 100 ) return false ;
3214+ return ! this . _plannerFollowUpHasExplicitUrl ( text ) ;
3215+ }
3216+
31773217 async _maybeRunPlannerGate ( tabId , messages , enriched , onUpdate , mode , costState , runId , tabInfo = null ) {
31783218 const plannerMode = this . _isActionMode ( mode ) ? this . _plannerMode ( ) : 'off' ;
31793219 const runPlanner = plannerMode !== 'off' ;
@@ -3185,7 +3225,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
31853225 const priorMessages = runPlanner ? messages . slice ( ) : null ;
31863226 messages . push ( enriched ) ;
31873227 this . _persist ( tabId ) ;
3188- if ( ! runPlanner ) return { proceed : true } ;
3228+ if ( ! runPlanner ) {
3229+ this . plannerFollowUpSkipTabs . delete ( tabId ) ;
3230+ return { proceed : true } ;
3231+ }
3232+ if ( this . _shouldSkipPlannerForShortFollowUp ( tabId , priorMessages , enriched , plannerMode ) ) {
3233+ this . plannerFollowUpSkipTabs . delete ( tabId ) ;
3234+ return { proceed : true } ;
3235+ }
3236+ this . plannerFollowUpSkipTabs . delete ( tabId ) ;
31893237
31903238 const historyDigest = this . _buildPlannerHistoryDigest ( priorMessages ) ;
31913239 const gate = await this . _runPlannerGate ( tabId , enriched , onUpdate , costState , runId , historyDigest , tabInfo , plannerMode ) ;
@@ -3209,6 +3257,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
32093257 // Note: the "Plan approved — running…" confirmation is rendered locally
32103258 // by submitPlanReview in the sidepanel, so there's no plan_approved
32113259 // agent_update to emit here (no handler consumed it).
3260+ if ( plannerMode === 'try' ) this . plannerFollowUpSkipTabs . add ( tabId ) ;
32123261 }
32133262 this . _persist ( tabId ) ;
32143263 }
@@ -4071,6 +4120,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
40714120 this . _cancelClarifications ( tabId , 'conversation cleared' ) ;
40724121 this . _cancelPendingPlans ( tabId , 'conversation cleared' ) ;
40734122 this . conversations . delete ( tabId ) ;
4123+ this . plannerFollowUpSkipTabs . delete ( tabId ) ;
40744124 this . progressLedgers . delete ( tabId ) ;
40754125 this . progressPageScopes . delete ( tabId ) ;
40764126 this . progressSessions . delete ( tabId ) ;
0 commit comments