fix(composer): unify suggestion state construction

This commit is contained in:
murashit 2026-07-17 06:07:31 +09:00
parent cb6e89b1ae
commit edf2b70902
2 changed files with 62 additions and 28 deletions

View file

@ -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 {

View file

@ -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: {