Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
"chrono-node": "^2.8.3",
"fuse.js": "7.1.0",
"js-tiktoken": "^1.0.20",
"xstate": "^5.28.0",
"zustand": "^5.0.6"
},
"repository": {
Expand Down
306 changes: 0 additions & 306 deletions plans/issue-1130-fsm-modal-reload.md

This file was deleted.

16 changes: 3 additions & 13 deletions src/gui/AIAssistantProvidersModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import GenericInputPrompt from "./GenericInputPrompt/GenericInputPrompt";
import { ProviderPickerModal } from "./ProviderPickerModal";
import GenericYesNoPrompt from "./GenericYesNoPrompt/GenericYesNoPrompt";
import type { IconType } from "src/types/IconType";
import { ModalReloadController } from "./utils/modalReloadMachine";

export class AIAssistantProvidersModal extends Modal {
public waitForClose: Promise<AIProvider[]>;
Expand All @@ -23,7 +22,6 @@ export class AIAssistantProvidersModal extends Modal {
private selectedProvider: AIProvider | null;

private _selectedProviderClone: AIProvider | null;
private readonly reloadController: ModalReloadController;

constructor(providers: AIProvider[], app: App) {
super(app);
Expand All @@ -34,14 +32,6 @@ export class AIAssistantProvidersModal extends Modal {
this.rejectPromise = reject;
this.resolvePromise = resolve;
});
this.reloadController = new ModalReloadController({
modalEl: this.modalEl,
contentEl: this.contentEl,
render: () => {
this.contentEl.empty();
this.display();
},
});

this.open();
this.display();
Expand All @@ -66,7 +56,9 @@ export class AIAssistantProvidersModal extends Modal {
}

private reload(): void {
this.reloadController.requestReload("ai-assistant-providers:reload");
this.contentEl.empty();

this.display();
}

addProvidersSetting(container: HTMLElement) {
Expand Down Expand Up @@ -385,10 +377,8 @@ export class AIAssistantProvidersModal extends Modal {
this.selectedProvider = null;
this.reload();
this.open();
return;
}

this.reloadController.destroy();
this.resolvePromise(this.providers);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Return immediately after reopening provider list

When onClose() is triggered while editing a provider (this.selectedProvider is set), the method now falls through to resolvePromise/super.onClose after calling this.open(). This closes the modal instead of returning to the provider list and can persist in-progress edits that users expected to discard (e.g., pressing Esc/X or Cancel-with-changes path), which is a behavior regression from the previous flow.

Useful? React with 👍 / 👎.

super.onClose();
Comment on lines 377 to 383
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Missing early return in AIAssistantProvidersModal.onClose() causes premature promise resolution and modal teardown

The revert removed the return statement from the if (this.selectedProvider) branch in onClose(). Previously (src/gui/AIAssistantProvidersModal.ts:388), when a user closed the modal while editing a provider, the code would set selectedProvider = null, reload to show the providers list, reopen the modal, and return — preventing resolvePromise and super.onClose() from executing. Now, after the reopen, execution falls through to this.resolvePromise(this.providers) and super.onClose(), which prematurely resolves the waitForClose promise (signaling the caller that the user is done) and tears down the modal. The caller at src/gui/AIAssistantSettingsModal.ts:68-72 will process the returned providers and reload its own UI while the providers modal is still supposed to be open.

(Refers to lines 374-383)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
Expand Down
15 changes: 3 additions & 12 deletions src/gui/AIAssistantSettingsModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import QuickAdd from "src/main";
import { FormatDisplayFormatter } from "src/formatters/formatDisplayFormatter";
import { AIAssistantProvidersModal } from "./AIAssistantProvidersModal";
import { getModelNames } from "src/ai/aiHelpers";
import { ModalReloadController } from "./utils/modalReloadMachine";

type AIAssistantSettings = QuickAddSettings["ai"];

Expand All @@ -17,7 +16,6 @@ export class AIAssistantSettingsModal extends Modal {
private rejectPromise: (reason?: unknown) => void;

private settings: AIAssistantSettings;
private readonly reloadController: ModalReloadController;

constructor(app: App, settings: AIAssistantSettings) {
super(app);
Expand All @@ -30,14 +28,6 @@ export class AIAssistantSettingsModal extends Modal {
this.resolvePromise = resolve;
}
);
this.reloadController = new ModalReloadController({
modalEl: this.modalEl,
contentEl: this.contentEl,
render: () => {
this.contentEl.empty();
this.display();
},
});

this.open();
this.display();
Expand All @@ -63,7 +53,9 @@ export class AIAssistantSettingsModal extends Modal {
}

private reload(): void {
this.reloadController.requestReload("ai-assistant-settings:reload");
this.contentEl.empty();

this.display();
}

addProvidersSetting(container: HTMLElement) {
Expand Down Expand Up @@ -164,7 +156,6 @@ export class AIAssistantSettingsModal extends Modal {
}

onClose(): void {
this.reloadController.destroy();
this.resolvePromise(this.settings);
super.onClose();
}
Expand Down
6 changes: 5 additions & 1 deletion src/gui/ChoiceBuilder/captureChoiceBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export class CaptureChoiceBuilder extends ChoiceBuilder {
// Behavior
new Setting(this.contentEl).setName("Behavior").setHeading();
if (!this.choice.captureToActiveFile) {
this.addOpenFileSetting("Open the captured file.", "captured");
this.addOpenFileSetting("Open the captured file.");

if (this.choice.openFile) {
this.addFileOpeningSetting("captured");
}
}
this.addSelectionAsValueSetting();
this.addTemplaterAfterCaptureSetting();
Expand Down
77 changes: 13 additions & 64 deletions src/gui/ChoiceBuilder/choiceBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ import {
} from "../../utils/fileOpeningDefaults";
import { GenericTextSuggester } from "../suggesters/genericTextSuggester";
import { promptRenameChoice } from "../choiceRename";
import { ModalReloadController } from "../utils/modalReloadMachine";

export abstract class ChoiceBuilder extends Modal {
private resolvePromise: (input: IChoice) => void;
public waitForClose: Promise<IChoice>;
abstract choice: IChoice;
protected svelteElements: SvelteComponent[] = [];
private readonly reloadController: ModalReloadController;
private fileOpeningSectionContainer: HTMLElement | null = null;

protected constructor(app: App) {
super(app);
Expand All @@ -27,20 +24,13 @@ export abstract class ChoiceBuilder extends Modal {

this.containerEl.addClass("quickAddModal");
this.open();
this.reloadController = new ModalReloadController({
modalEl: this.modalEl,
contentEl: this.contentEl,
render: () => {
this.contentEl.empty();
this.display();
},
});
}

protected abstract display(): unknown;

protected reload() {
this.reloadController.requestReload("choice-builder:reload");
this.contentEl.empty();
this.display();
}

protected addOnePageOverrideSetting(choice: IChoice): void {
Expand Down Expand Up @@ -112,49 +102,21 @@ export abstract class ChoiceBuilder extends Modal {
* Adds a toggle that controls whether the resulting file should be opened.
* @param description Description explaining what file will be opened (e.g. "Open the file that is captured to.")
*/
protected addOpenFileSetting(
description: string,
contextLabel?: string,
): void {
protected addOpenFileSetting(description: string): void {
// We intentionally cast to `any` because not all IChoice implementations have openFile.
const choice: any = this.choice as any;
if (choice.openFile === undefined) return; // Guard: nothing to configure

new Setting(this.contentEl)
.setName("Open")
.setDesc(description)
.addToggle((toggle) => {
toggle.setValue(choice.openFile);
toggle.onChange((value) => {
choice.openFile = value;
if (!contextLabel) {
this.reload();
return;
}

this.renderFileOpeningSection(contextLabel);
});
.addToggle((toggle) => {
toggle.setValue(choice.openFile);
toggle.onChange((value) => {
choice.openFile = value;
this.reload();
});

if (!contextLabel) return;

this.fileOpeningSectionContainer = this.contentEl.createDiv(
"choiceBuilder__fileOpeningSection",
);
this.renderFileOpeningSection(contextLabel);
}

private renderFileOpeningSection(contextLabel: string): void {
if (!this.fileOpeningSectionContainer) return;

this.fileOpeningSectionContainer.empty();
const choice: any = this.choice as any;
if (!choice.openFile) return;

this.addFileOpeningSettingsToContainer(
this.fileOpeningSectionContainer,
contextLabel,
);
});
}

/**
Expand All @@ -164,20 +126,13 @@ export abstract class ChoiceBuilder extends Modal {
* @param contextLabel Text to use in descriptions (e.g. "captured" | "created")
*/
protected addFileOpeningSetting(contextLabel: string): void {
this.addFileOpeningSettingsToContainer(this.contentEl, contextLabel);
}

private addFileOpeningSettingsToContainer(
container: HTMLElement,
contextLabel: string,
): void {
const choice: any = this.choice as any;
choice.fileOpening = normalizeFileOpening(choice.fileOpening);

const fileOpening = choice.fileOpening as FileOpeningSettings;

// Location selector
new Setting(container)
new Setting(this.contentEl)
.setName("File Opening Location")
.setDesc(`Where to open the ${contextLabel} file`)
.addDropdown((dropdown) => {
Expand All @@ -192,18 +147,13 @@ export abstract class ChoiceBuilder extends Modal {
dropdown.setValue(fileOpening.location);
dropdown.onChange((value: any) => {
fileOpening.location = value as OpenLocation;
if (this.fileOpeningSectionContainer) {
this.renderFileOpeningSection(contextLabel);
return;
}

this.reload();
});
});

// Split direction – only if location === "split"
if (fileOpening.location === "split") {
new Setting(container)
new Setting(this.contentEl)
.setName("Split Direction")
.setDesc("How to arrange the new pane relative to the current one")
.addDropdown((dropdown) => {
Expand All @@ -219,7 +169,7 @@ export abstract class ChoiceBuilder extends Modal {
}

// View mode selector
new Setting(container)
new Setting(this.contentEl)
.setName("View Mode")
.setDesc("How to display the opened file")
.addDropdown((dropdown) => {
Expand All @@ -241,7 +191,7 @@ export abstract class ChoiceBuilder extends Modal {

// Focus toggle – only show for non-reuse locations
if (fileOpening.location !== "reuse") {
new Setting(container)
new Setting(this.contentEl)
.setName("Focus new pane")
.setDesc("Focus the opened tab immediately after opening")
.addToggle((toggle) =>
Expand All @@ -253,7 +203,6 @@ export abstract class ChoiceBuilder extends Modal {
}

onClose() {
this.reloadController.destroy();
super.onClose();
this.svelteElements.forEach((el) => {
if (el && el.$destroy) el.$destroy();
Expand Down
5 changes: 4 additions & 1 deletion src/gui/ChoiceBuilder/templateChoiceBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export class TemplateChoiceBuilder extends ChoiceBuilder {
// Behavior
new Setting(this.contentEl).setName("Behavior").setHeading();
this.addFileAlreadyExistsSetting();
this.addOpenFileSetting("Open the created file.", "created");
this.addOpenFileSetting("Open the created file.");
if (this.choice.openFile) {
this.addFileOpeningSetting("created");
}
this.addOnePageOverrideSetting(this.choice);
}

Expand Down
Loading