@@ -24,6 +24,10 @@ let output: vscode.OutputChannel;
2424/** The progress reporter of an in-flight draft, so worker progress notifications can update it. */
2525let activeProgress : vscode . Progress < { message ?: string } > | undefined ;
2626
27+ /** Guards against re-entrant drafts: one generation at a time keeps the single worker from wedging
28+ * (a cancelled draft's provider call still occupies the worker) and the progress reporter unshared. */
29+ let draftInFlight = false ;
30+
2731/** A read-only scheme backing the apply-to-file diff preview. */
2832const DRAFT_SCHEME = "gmat-copilot-draft" ;
2933const draftContents = new Map < string , string > ( ) ;
@@ -90,6 +94,12 @@ async function draftCommand(source: "input" | "selection"): Promise<void> {
9094 if ( ! activeWorker ) {
9195 return ;
9296 }
97+ if ( draftInFlight ) {
98+ vscode . window . showInformationMessage (
99+ "GMAT Copilot: a draft is already in progress — finish or dismiss it before starting another." ,
100+ ) ;
101+ return ;
102+ }
93103 const editor = vscode . window . activeTextEditor ;
94104 const intent = await resolveIntent ( source , editor ) ;
95105 if ( ! intent ) {
@@ -108,36 +118,36 @@ async function draftCommand(source: "input" | "selection"): Promise<void> {
108118 dryRun : config . get < boolean > ( "dryRun" , false ) ,
109119 } ;
110120
111- let result : DraftResult ;
121+ draftInFlight = true ;
112122 try {
113- result = await vscode . window . withProgress (
114- { location : vscode . ProgressLocation . Notification , cancellable : true , title : "GMAT Copilot" } ,
115- async ( progress , token ) => {
116- activeProgress = progress ;
117- progress . report ( { message : "Generating the mission script…" } ) ;
118- try {
119- return await activeWorker . draft ( params , token ) ;
120- } finally {
121- activeProgress = undefined ;
122- }
123- } ,
124- ) ;
125- } catch ( err ) {
126- if ( ! isCancellation ( err ) ) {
127- showWorkerError ( err ) ;
123+ let result : DraftResult ;
124+ try {
125+ result = await vscode . window . withProgress (
126+ {
127+ location : vscode . ProgressLocation . Notification ,
128+ cancellable : true ,
129+ title : "GMAT Copilot" ,
130+ } ,
131+ async ( progress , token ) => {
132+ activeProgress = progress ;
133+ progress . report ( { message : "Generating the mission script…" } ) ;
134+ try {
135+ return await activeWorker . draft ( params , token ) ;
136+ } finally {
137+ activeProgress = undefined ;
138+ }
139+ } ,
140+ ) ;
141+ } catch ( err ) {
142+ if ( ! isCancellation ( err ) ) {
143+ showWorkerError ( err ) ;
144+ }
145+ return ;
128146 }
129- return ;
130- }
131-
132- applyDiagnostics ( editor ?. document , result . diagnostics ) ;
133- if ( result . rejected ) {
134- vscode . window . showWarningMessage (
135- "GMAT Copilot: the draft did not validate clean and was not applied. See the Problems panel — " +
136- "switch to permissive mode or refine the prompt." ,
137- ) ;
138- return ;
147+ await presentResult ( editor , result ) ;
148+ } finally {
149+ draftInFlight = false ;
139150 }
140- await reviewAndApply ( editor , result ) ;
141151}
142152
143153async function resolveIntent (
@@ -162,20 +172,53 @@ async function resolveIntent(
162172}
163173
164174// -------------------------------------------------------------------- apply-to-current-file UX
165- async function reviewAndApply (
175+ // The worker's diagnostics index into the *generated script*, so they are attached to a document
176+ // that actually holds that script — the applied target or a scratch document — never the pre-apply
177+ // active buffer (whose content is still the user's old text, so the squiggles would land on the
178+ // wrong lines), and never dropped when the draft opens in a fresh document.
179+ async function presentResult (
166180 editor : vscode . TextEditor | undefined ,
167181 result : DraftResult ,
168182) : Promise < void > {
169- const script = result . script ;
183+ if ( result . rejected ) {
184+ // Strict rejected the draft, so the user's file is left untouched. Open the best-effort draft in
185+ // a scratch document so its findings line up with the text they point at, in the Problems panel.
186+ const doc = await openDraft ( result . script ) ;
187+ applyDiagnostics ( doc , result . diagnostics ) ;
188+ vscode . window . showWarningMessage ( rejectionMessage ( result ) ) ;
189+ return ;
190+ }
170191 if ( ! editor ) {
171- const doc = await vscode . workspace . openTextDocument ( { language : "gmat" , content : script } ) ;
172- await vscode . window . showTextDocument ( doc ) ;
192+ const doc = await openDraft ( result . script ) ;
193+ applyDiagnostics ( doc , result . diagnostics ) ;
173194 vscode . window . showInformationMessage (
174195 "GMAT Copilot: no active editor to apply to — opened the draft in a new document for review." ,
175196 ) ;
176197 return ;
177198 }
199+ await reviewAndApply ( editor , result ) ;
200+ }
178201
202+ /** Open the generated script in a fresh editor document for review (no active file to apply to). */
203+ async function openDraft ( script : string ) : Promise < vscode . TextDocument > {
204+ const doc = await vscode . workspace . openTextDocument ( { language : "gmat" , content : script } ) ;
205+ await vscode . window . showTextDocument ( doc ) ;
206+ return doc ;
207+ }
208+
209+ /** Why a strict draft was rejected — a lint failure or, with the dry-run on, a runtime failure. */
210+ function rejectionMessage ( result : DraftResult ) : string {
211+ const dryRun = result . dryRun ;
212+ const why =
213+ dryRun && ! dryRun . ok ? `did not pass the ${ dryRun . tier } -tier dry-run` : "did not lint clean" ;
214+ return (
215+ `GMAT Copilot: the draft ${ why } and was not applied. It is open for review with the findings ` +
216+ "in the Problems panel — switch to permissive mode or refine the prompt."
217+ ) ;
218+ }
219+
220+ async function reviewAndApply ( editor : vscode . TextEditor , result : DraftResult ) : Promise < void > {
221+ const script = result . script ;
179222 const target = editor . document ;
180223 const label = target . isUntitled ? "untitled" : path . basename ( target . fileName ) ;
181224 const draftUri = vscode . Uri . parse ( `${ DRAFT_SCHEME } :/${ draftCounter ++ } /${ label } ` ) ;
@@ -194,6 +237,7 @@ async function reviewAndApply(
194237 "Discard" ,
195238 ) ;
196239 if ( choice !== "Apply" ) {
240+ // Discarded: leave the user's file and any existing diagnostics on it untouched.
197241 return ;
198242 }
199243 const edit = new vscode . WorkspaceEdit ( ) ;
@@ -203,6 +247,10 @@ async function reviewAndApply(
203247 ) ;
204248 edit . replace ( target . uri , fullRange , script ) ;
205249 const applied = await vscode . workspace . applyEdit ( edit ) ;
250+ if ( applied ) {
251+ // The target now holds the generated script, so the diagnostics line up with its content.
252+ applyDiagnostics ( target , result . diagnostics ) ;
253+ }
206254 vscode . window . showInformationMessage (
207255 applied
208256 ? "GMAT Copilot: draft applied to the active file."
0 commit comments