diff --git a/src/features/chat/panel/composer-controller.ts b/src/features/chat/panel/composer-controller.ts index 7423a5d0..08209fd1 100644 --- a/src/features/chat/panel/composer-controller.ts +++ b/src/features/chat/panel/composer-controller.ts @@ -159,10 +159,6 @@ export class ChatComposerController { this.options.contextReferenceProvider.dispose(); } - refreshSuggestions(): void { - this.updateSuggestions(); - } - captureInputSnapshot(): ComposerInputSnapshot { const sourcePath = this.options.sourcePath(); return { @@ -245,23 +241,7 @@ export class ChatComposerController { this.dispatchSuggestions({ type: "composer/suggestions-set", suggestions: [] }); return; } - const suggestions = activeComposerSuggestions( - beforeCursor, - this.noteCandidates(), - state.connection.availableSkills, - state.threadList.listedThreads, - state.connection.availableModels, - this.options.currentModelForSuggestions(), - { - activeThreadId: activeThreadState(state)?.id ?? null, - activeThreadEphemeral: activeThreadState(state)?.lifetime?.kind === "ephemeral", - activeThreadSubagent: activeThreadState(state)?.provenance?.kind === "subagent", - contextReferences: this.contextReferences(), - dailyNoteReferences: () => this.options.noteCandidateProvider.dailyNoteReferences(this.options.sourcePath()), - permissionProfiles: state.connection.availablePermissionProfiles, - tagCandidates: () => this.options.noteCandidateProvider.tags(), - }, - ); + const suggestions = this.activeSuggestions(beforeCursor, state); this.dispatchSuggestions({ type: "composer/suggestions-set", @@ -298,7 +278,17 @@ export class ChatComposerController { } const beforeCursor = composerTextBeforeCursor(this.composer); if (beforeCursor === null) return { suggestions: [], selected: 0, dismissedSignature: null }; - const suggestions = activeComposerSuggestions( + const suggestions = this.activeSuggestions(beforeCursor, state); + return { + suggestions, + selected: state.composer.suggestSelected >= suggestions.length ? 0 : state.composer.suggestSelected, + dismissedSignature: null, + }; + } + + private activeSuggestions(beforeCursor: string, state: ChatState): readonly ComposerSuggestion[] { + const activeThread = activeThreadState(state); + return activeComposerSuggestions( beforeCursor, this.noteCandidates(), state.connection.availableSkills, @@ -306,18 +296,15 @@ export class ChatComposerController { state.connection.availableModels, this.options.currentModelForSuggestions(), { - activeThreadId: activeThreadState(state)?.id ?? null, + activeThreadId: activeThread?.id ?? null, + activeThreadEphemeral: activeThread?.lifetime?.kind === "ephemeral", + activeThreadSubagent: activeThread?.provenance?.kind === "subagent", contextReferences: this.contextReferences(), dailyNoteReferences: () => this.options.noteCandidateProvider.dailyNoteReferences(this.options.sourcePath()), permissionProfiles: state.connection.availablePermissionProfiles, tagCandidates: () => this.options.noteCandidateProvider.tags(), }, ); - return { - suggestions, - selected: state.composer.suggestSelected >= suggestions.length ? 0 : state.composer.suggestSelected, - dismissedSignature: null, - }; } private selectSuggestion(index: number): void { diff --git a/tests/features/chat/panel/composer-controller.test.ts b/tests/features/chat/panel/composer-controller.test.ts index 2b152344..9c1ab37a 100644 --- a/tests/features/chat/panel/composer-controller.test.ts +++ b/tests/features/chat/panel/composer-controller.test.ts @@ -213,6 +213,53 @@ describe("ChatComposerController", () => { expect(parent.querySelector(".codex-panel__composer-suggestion")?.textContent).toContain("/"); }); + it("applies ephemeral-thread suggestion restrictions on input without waiting for keyup", () => { + const stateStore = createChatStateStore( + chatStateFixture({ + activeThread: { + id: "side-thread", + lifetime: { kind: "ephemeral", sourceThreadId: "source-thread", sourceThreadTitle: "Source" }, + }, + }), + ); + const { controller, parent } = composerControllerFixture({ stateStore }); + + renderComposerController(parent, controller, stateStore); + setTextAreaValue(composer(parent), "/f"); + composer(parent).setSelectionRange(2, 2); + composer(parent).dispatchEvent(new Event("input", { bubbles: true })); + + expect(stateStore.getState().composer.suggestions.map((suggestion) => suggestion.replacement)).not.toContain("/fork"); + expect(stateStore.getState().composer.suggestions.map((suggestion) => suggestion.replacement)).toContain("/fast"); + }); + + it("omits subagent slash suggestions on input without waiting for keyup", () => { + const stateStore = createChatStateStore( + chatStateFixture({ + activeThread: { + id: "child-thread", + provenance: { + kind: "subagent", + subagentKind: "thread-spawn", + parentThreadId: "parent-thread", + sessionId: "session", + depth: 1, + agentNickname: "Scout", + agentRole: "explorer", + }, + }, + }), + ); + const { controller, parent } = composerControllerFixture({ stateStore }); + + renderComposerController(parent, controller, stateStore); + setTextAreaValue(composer(parent), "/"); + composer(parent).setSelectionRange(1, 1); + composer(parent).dispatchEvent(new Event("input", { bubbles: true })); + + expect(stateStore.getState().composer.suggestions).toEqual([]); + }); + it("updates Obsidian tag suggestions when the input changes", () => { const { controller, parent, stateStore } = composerControllerFixture({ controller: {