Skip to content

Commit 6085e29

Browse files
committed
Guard the @-mention lookup against a shell disposed mid-flight
Quitting while an @-mention filesystem lookup is in flight let the resolved promise write suggestions into a torn-down shell, touching the freed renderer/TextBuffer. Check shell.disposed after each await in openAtMentionSuggestions and bail before any further write.
1 parent cf3bb84 commit 6085e29

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

src/tui-opentui/mention-popup.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,39 @@ describe("@ popup narrows as you type", () => {
123123
})
124124
})
125125

126+
test("quitting mid-lookup does not write into the disposed shell", async () => {
127+
await withTestRenderer(
128+
async (h) => {
129+
const shell = createAppShell(h.renderer, {
130+
terminal: { columns: 80, rows: 24 },
131+
wireKeys: false,
132+
run: "idle",
133+
})
134+
let resolveLookup: (entries: readonly string[]) => void = () => {}
135+
setMentionSuggestionSource(
136+
shell,
137+
() =>
138+
new Promise<readonly string[]>((resolve) => {
139+
resolveLookup = resolve
140+
}),
141+
)
142+
143+
shell.prompt.value = "read @"
144+
shell.prompt.cursorOffset = shell.prompt.value.length
145+
const pending = openAtMentionSuggestions(shell)
146+
147+
// The operator quits before the filesystem lookup answers.
148+
shell.dispose()
149+
resolveLookup(["AGENTS.md", "README.md"])
150+
151+
await expect(pending).resolves.toBe(false)
152+
expect(shell.overlayKind).toBeNull()
153+
expect(isMentionPopupOpen(shell)).toBe(false)
154+
},
155+
{ width: 80, height: 24 },
156+
)
157+
})
158+
126159
test("no match closes the popup and leaves the typed text", async () => {
127160
await withShell(async (shell) => {
128161
await openAt(shell, "@")

src/tui-opentui/shell.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3931,11 +3931,15 @@ export async function openAtMentionSuggestions(shell: AppShell): Promise<boolean
39313931
await source(token.dir),
39323932
token.fragment,
39333933
)
3934+
// Quitting mid-lookup tears down the renderer/TextBuffer this function
3935+
// writes into below; a resolved-but-stale lookup must not touch them.
3936+
if (shell.disposed) return false
39343937
// The source caps how many entries it returns per directory, so a large
39353938
// directory can cap out before the interior match appears. Asking it to do
39363939
// its own prefix filter puts that cap after the narrowing instead of before.
39373940
if (suggestions.length === 0 && token.fragment.length > 0) {
39383941
suggestions = await source(at.prefix)
3942+
if (shell.disposed) return false
39393943
}
39403944
if (mentionGenerations.get(shell) !== generation) return false
39413945

0 commit comments

Comments
 (0)