@@ -75,6 +75,7 @@ export class Agent {
7575 this._progressSessionCounter = 0;
7676 this.conversationModes = new Map(); // tabId -> 'ask' | 'act' | 'dev'
7777 this.conversationIds = new Map(); // tabId -> stable conversationId (regenerated on clearConversation)
78+ this.plannerFollowUpSkipTabs = new Set(); // tabIds allowed one short follow-up after an approved try-mode plan
7879 this.hydratedTabs = new Set(); // tabIds we've already pulled from storage
7980 this.persistTimers = new Map(); // tabId -> debounce handle
8081 this.abortFlags = new Map(); // tabId -> boolean
@@ -287,6 +288,7 @@ export class Agent {
287288 }
288289
289290 clearScratchpad(tabId) {
291+ this.plannerFollowUpSkipTabs.delete(tabId);
290292 const messages = this.conversations.get(tabId);
291293 if (!messages) {
292294 return { success: true, existed: false, note: 'scratchpad already empty' };
@@ -3896,6 +3898,44 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
38963898 return this._plannerMode() !== 'off';
38973899 }
38983900
3901+ _messageHasPlannerFollowUpAttachmentBlocks(message) {
3902+ const content = message?.content ?? message;
3903+ if (!Array.isArray(content)) return false;
3904+ return content.some(block => {
3905+ if (!block || typeof block !== 'object') return false;
3906+ if (block.type !== 'text') return true;
3907+ const text = String(block.text || '');
3908+ return text.startsWith('[UNTRUSTED USER ATTACHMENTS') || text.startsWith('[UNTRUSTED DOCUMENT');
3909+ });
3910+ }
3911+
3912+ _plannerFollowUpText(message) {
3913+ const text = this._stripInjectedTaskContext(userMessageToText(message));
3914+ return text.replace(/\s+/g, ' ').trim();
3915+ }
3916+
3917+ _plannerFollowUpHasExplicitUrl(text) {
3918+ return /(?:https?:\/\/|www\.)\S+|\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}(?:\/[^\s]*)?/i.test(String(text || ''));
3919+ }
3920+
3921+ _hasApprovedPlannerHandoff(messages) {
3922+ const idx = this._findScratchpadIndex(messages || []);
3923+ if (idx < 0) return false;
3924+ const body = this._extractScratchpadBody(messages[idx].content);
3925+ return /\[Approved plan\b[^\]]*pinned by planner\]/.test(body);
3926+ }
3927+
3928+ _shouldSkipPlannerForShortFollowUp(tabId, priorMessages, enriched, plannerMode) {
3929+ if (plannerMode !== 'try') return false;
3930+ if (this._normalizePlanReviewMode(this.planReviewMode) === 'always') return false;
3931+ if (!this.plannerFollowUpSkipTabs.has(tabId)) return false;
3932+ if (!this._hasApprovedPlannerHandoff(priorMessages)) return false;
3933+ if (this._messageHasPlannerFollowUpAttachmentBlocks(enriched)) return false;
3934+ const text = this._plannerFollowUpText(enriched);
3935+ if (!text || text.length > 100) return false;
3936+ return !this._plannerFollowUpHasExplicitUrl(text);
3937+ }
3938+
38993939 /**
39003940 * Plan-before-Act gate: push user message, pin approved plan after it, or stop early.
39013941 */
@@ -3910,7 +3950,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
39103950 const priorMessages = runPlanner ? messages.slice() : null;
39113951 messages.push(enriched);
39123952 this._persist(tabId);
3913- if (!runPlanner) return { proceed: true };
3953+ if (!runPlanner) {
3954+ this.plannerFollowUpSkipTabs.delete(tabId);
3955+ return { proceed: true };
3956+ }
3957+ if (this._shouldSkipPlannerForShortFollowUp(tabId, priorMessages, enriched, plannerMode)) {
3958+ this.plannerFollowUpSkipTabs.delete(tabId);
3959+ return { proceed: true };
3960+ }
3961+ this.plannerFollowUpSkipTabs.delete(tabId);
39143962
39153963 const historyDigest = this._buildPlannerHistoryDigest(priorMessages);
39163964 const gate = await this._runPlannerGate(tabId, enriched, onUpdate, costState, runId, historyDigest, tabInfo, plannerMode);
@@ -3935,6 +3983,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
39353983 // Note: the "Plan approved — running…" confirmation is rendered locally
39363984 // by submitPlanReview in the sidepanel, so there's no plan_approved
39373985 // agent_update to emit here (no handler consumed it).
3986+ if (plannerMode === 'try') this.plannerFollowUpSkipTabs.add(tabId);
39383987 }
39393988 this._persist(tabId);
39403989 }
@@ -4962,6 +5011,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
49625011 this._cancelClarifications(tabId, 'conversation cleared');
49635012 this._cancelPendingPlans(tabId, 'conversation cleared');
49645013 this.conversations.delete(tabId);
5014+ this.plannerFollowUpSkipTabs.delete(tabId);
49655015 this.progressLedgers.delete(tabId);
49665016 this.progressPageScopes.delete(tabId);
49675017 this.progressSessions.delete(tabId);
0 commit comments