Skip to content

feat(copilotcli):Implement updating plan file in exit plan mode handling#309454

Draft
DonJayamanne wants to merge 1 commit intomainfrom
don/super-herring
Draft

feat(copilotcli):Implement updating plan file in exit plan mode handling#309454
DonJayamanne wants to merge 1 commit intomainfrom
don/super-herring

Conversation

@DonJayamanne
Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI review requested due to automatic review settings April 13, 2026 11:50
@DonJayamanne DonJayamanne self-assigned this Apr 13, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extracts Copilot CLI “exit plan mode” handling into a dedicated handleExitPlanMode helper and adds a new unit test suite, with the goal of supporting updating the SDK session plan when the user edits plan.md during exit-plan approval.

Changes:

  • Introduces exitPlanModeHandler.ts to centralize autopilot/interactive exit-plan decisions and plan syncing logic.
  • Updates CopilotCLISession to delegate exit_plan_mode.requested handling to the new helper.
  • Adds exitPlanModeHandler.spec.ts with coverage for autopilot/interactive flows and plan file monitoring.
Show a summary per file
File Description
extensions/copilot/src/extension/chatSessions/copilotcli/node/exitPlanModeHandler.ts New handler + plan file monitor implementation.
extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts Wires the SDK event to the new handler.
extensions/copilot/src/extension/chatSessions/copilotcli/node/test/exitPlanModeHandler.spec.ts New unit tests for handler behavior and monitoring.

Copilot's findings

Comments suppressed due to low confidence (4)

extensions/copilot/src/extension/chatSessions/copilotcli/node/exitPlanModeHandler.ts:85

  • ExitPlanModeEventData models actions/recommendedAction as required strings, but existing callers/tests show these fields can be omitted (e.g. actions undefined). As written, resolveInteractive will throw when it does event.actions.map(...) if actions is missing. Make these fields optional (or default them defensively in handleExitPlanMode) and ensure all usages tolerate undefined/empty values.
export interface ExitPlanModeEventData {
	readonly requestId: string;
	readonly actions: string[];
	readonly recommendedAction: string;
}

extensions/copilot/src/extension/chatSessions/copilotcli/node/exitPlanModeHandler.ts:59

  • Plan syncing currently depends on onDidChangeTextDocument events and only writes when doc.isDirty === false. Saving a document typically does not fire a change event, so if the user saves after the delayer fires (or without a format-on-save edit), the SDK session will never be updated. Consider listening to an explicit save event (preferred) or re-triggering/polling until the document becomes clean while the question is open.
		this.add(workspaceService.onDidChangeTextDocument(e => {
			if (e.contentChanges.length === 0 || !isEqual(e.document.uri, planUri)) {
				return;
			}
			this._lastChangedDocument = e.document;
			this._delayer.trigger(() => this._syncIfSaved());
		}));
	}

	private _syncIfSaved(): void {
		const doc = this._lastChangedDocument;
		if (!doc || doc.isDirty) {
			return;
		}

extensions/copilot/src/extension/chatSessions/copilotcli/node/exitPlanModeHandler.ts:65

  • _pendingWrite is replaced on each sync, allowing multiple writePlan calls to overlap and potentially complete out-of-order (older write finishing last). Chain writes onto the previous promise (serialize) so the final plan content is deterministic, and flush() truly waits for all queued writes.
		const content = doc.getText();
		this._logService.trace('[ExitPlanModeHandler] Plan file saved by user, syncing to SDK session');
		this._pendingWrite = this._session.writePlan(content).catch(err => {
			this._logService.error(err, '[ExitPlanModeHandler] Failed to write plan changes to SDK session');
		});
	}

extensions/copilot/src/extension/chatSessions/copilotcli/node/exitPlanModeHandler.ts:186

  • Interactive resolution assumes answer.selected[0] exists whenever freeText is empty. If the UI returns an answer with no selection (or skipped: true), this will respond approved: true with an invalid/undefined selectedAction. Handle answer.skipped / empty selections by returning { approved: false } (or falling back to a safe default from event.actions).
		if (!answer) {
			return { approved: false };
		}
		if (answer.freeText) {
			return { approved: false, feedback: answer.freeText };
		}

		let selectedAction: ExitPlanModeActionType = answer.selected[0] as ExitPlanModeActionType;
		for (const [action, desc] of Object.entries(actionDescriptions)) {
			if (desc.label === selectedAction) {
				selectedAction = action as ExitPlanModeActionType;
				break;
			}
		}
		const autoApproveEdits = permissionLevel === 'autoApprove' ? true : undefined;
		return { approved: true, selectedAction, autoApproveEdits };
  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment on lines +20 to +23
'autopilot': { label: 'Autopilot', description: l10n.t('Auto-approve all tool calls and continue until the task is done') },
'interactive': { label: 'Interactive', description: l10n.t('Let the agent continue in interactive mode, asking for input and approval for each action.') },
'exit_only': { label: 'Approve and exit', description: l10n.t('Exit planning, but do not execute the plan. I will execute the plan myself.') },
'autopilot_fleet': { label: 'Autopilot Fleet', description: l10n.t('Auto-approve all tool calls, including fleet management actions, and continue until the task is done.') },
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The option labels in actionDescriptions are user-visible but are not localized. Wrap these labels with l10n.t(...) (as you already do for descriptions) so the question UI can be translated consistently.

This issue also appears in the following locations of the same file:

  • line 46
  • line 60
  • line 81
  • line 171
Suggested change
'autopilot': { label: 'Autopilot', description: l10n.t('Auto-approve all tool calls and continue until the task is done') },
'interactive': { label: 'Interactive', description: l10n.t('Let the agent continue in interactive mode, asking for input and approval for each action.') },
'exit_only': { label: 'Approve and exit', description: l10n.t('Exit planning, but do not execute the plan. I will execute the plan myself.') },
'autopilot_fleet': { label: 'Autopilot Fleet', description: l10n.t('Auto-approve all tool calls, including fleet management actions, and continue until the task is done.') },
'autopilot': { label: l10n.t('Autopilot'), description: l10n.t('Auto-approve all tool calls and continue until the task is done') },
'interactive': { label: l10n.t('Interactive'), description: l10n.t('Let the agent continue in interactive mode, asking for input and approval for each action.') },
'exit_only': { label: l10n.t('Approve and exit'), description: l10n.t('Exit planning, but do not execute the plan. I will execute the plan myself.') },
'autopilot_fleet': { label: l10n.t('Autopilot Fleet'), description: l10n.t('Auto-approve all tool calls, including fleet management actions, and continue until the task is done.') },

Copilot uses AI. Check for mistakes.
Comment on lines +250 to +252
// Allow debouncer to fire
await new Promise(r => setTimeout(r, 400));

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests wait ~400ms using real timers to let the Delayer fire, which slows the suite and can be flaky under load. Prefer vi.useFakeTimers() and vi.advanceTimersByTime(...) (or runAllTimersAsync) to make the debounce behavior deterministic and faster.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants