Rename active note mention to active

This commit is contained in:
murashit 2026-07-05 22:01:35 +09:00
parent c0a954d4ab
commit 4dd222ab2c
16 changed files with 63 additions and 51 deletions

View file

@ -44,7 +44,7 @@ You can also open the plugin page directly: <https://community.obsidian.md/plugi
Each panel can keep a separate conversation, so related work can stay open side by side. Start fresh from the panel, reopen recent threads from the picker or panel history, or use the Threads view to see active work across the vault.
The composer treats Obsidian content as prompt context. It suggests vault files and recent notes for wikilinks, keeps links readable while attaching resolved files, and opens file links from Codex replies back in Obsidian. Use `@active-note` or `@selection` to include the active note or current Markdown selection without pasting it manually, or enable **Reference active note on send** to reference the current active note with each composer send. Paste or drop files to save them in the attachment folder and reference them from the same prompt.
The composer treats Obsidian content as prompt context. It suggests vault files and recent notes for wikilinks, keeps links readable while attaching resolved files, and opens file links from Codex replies back in Obsidian. Use `@active` or `@selection` to include the active file or current Markdown selection without pasting it manually, or enable **Reference active file on send** to reference the current active file with each composer send. Paste or drop files to save them in the attachment folder and reference them from the same prompt.
For context outside the current composer, use `/refer <thread> <message>` to send a message with recent turns from another Codex thread, or `/clip <url> [message]` to save a readable Markdown clipping from a web page and send a wikilink to the saved note.

View file

@ -3,7 +3,7 @@ export interface RequestMention {
path: string;
}
export const ACTIVE_NOTE_MENTION_NAME = "<active note>";
export const ACTIVE_FILE_MENTION_NAME = "<active>";
export interface RequestAdditionalContext {
key: string;

View file

@ -130,9 +130,9 @@ function activeContextReferenceSuggestions(
const query = rawQuery.toLowerCase();
const start = beforeCursor.length - rawQuery.length - 1;
const suggestions: ComposerSuggestion[] = [];
if (references?.activeNote && "active-note".startsWith(query) && query !== "active-note") {
if (references?.activeNote && "active".startsWith(query)) {
suggestions.push({
display: "Active note",
display: "Active file",
detail: references.activeNote.path,
replacement: activeNoteContextReferenceMarker(references.activeNote),
start,

View file

@ -1,7 +1,7 @@
import { parseLinktext } from "obsidian";
import type { SkillMetadata } from "../../../../domain/catalog/metadata";
import {
ACTIVE_NOTE_MENTION_NAME,
ACTIVE_FILE_MENTION_NAME,
type CodexInput,
codexTextInputWithMentions,
type RequestAdditionalContext,
@ -83,7 +83,7 @@ export function preparedUserInputWithWikiLinkMentionsSkillsAndContext(
}
const attachedActiveNote = options.referenceActiveNoteOnSend ? contextReferences.activeNote : null;
if (attachedActiveNote) mentions.push({ name: ACTIVE_NOTE_MENTION_NAME, path: attachedActiveNote.path });
if (attachedActiveNote) mentions.push({ name: ACTIVE_FILE_MENTION_NAME, path: attachedActiveNote.path });
const skillByName = firstEnabledSkillByName(skills);
const resolvedSkills: RequestMention[] = [];
@ -188,7 +188,7 @@ function selectionBodyLines(selections: readonly SelectionContextReference[]): s
}
function activeNoteContextLines(activeNote: ActiveNoteContextReference): string[] {
return ["Attached active note:", `- ${ACTIVE_NOTE_MENTION_NAME} -> ${activeNote.path}`];
return ["Referenced active file:", `- ${ACTIVE_FILE_MENTION_NAME} -> ${activeNote.path}`];
}
function parsedSkillReferences(text: string): string[] {

View file

@ -1,7 +1,7 @@
import { ACTIVE_NOTE_MENTION_NAME, type CodexInputItem } from "../../../../../domain/chat/input";
import { ACTIVE_FILE_MENTION_NAME, type CodexInputItem } from "../../../../../domain/chat/input";
import type { MessageStreamFileMention } from "../items";
const ACTIVE_NOTE_DISPLAY_NAME = "Active note";
const ACTIVE_FILE_DISPLAY_NAME = "Active file";
export function fileMentionsFromInput(input: readonly CodexInputItem[]): MessageStreamFileMention[] {
const seenFilePaths = new Set<string>();
@ -9,11 +9,11 @@ export function fileMentionsFromInput(input: readonly CodexInputItem[]): Message
const mentions: MessageStreamFileMention[] = [];
for (const item of input) {
if (item.type !== "mention") continue;
const activeNoteMention = item.name === ACTIVE_NOTE_MENTION_NAME;
const activeNoteMention = item.name === ACTIVE_FILE_MENTION_NAME;
const seen = activeNoteMention ? seenActiveNotePaths : seenFilePaths;
if (seen.has(item.path)) continue;
seen.add(item.path);
mentions.push({ name: activeNoteMention ? ACTIVE_NOTE_DISPLAY_NAME : item.name, path: item.path });
mentions.push({ name: activeNoteMention ? ACTIVE_FILE_DISPLAY_NAME : item.name, path: item.path });
}
return mentions;
}

View file

@ -121,8 +121,8 @@ function ComposerSettingsSection({ panel, actions }: { panel: SettingsTabPanelSt
<ObsidianToggle checked={panel.scrollThreadFromComposerEdges} onChange={actions.setScrollThreadFromComposerEdges} />
</SettingRow>
<SettingRow
name="Reference active note on send"
desc="When enabled, each composer send references the current active note as context without changing the prompt text."
name="Reference active file on send"
desc="When enabled, each composer send references the current active file as context without changing the prompt text."
>
<ObsidianToggle checked={panel.referenceActiveNoteOnSend} onChange={actions.setReferenceActiveNoteOnSend} />
</SettingRow>

View file

@ -121,7 +121,7 @@ describe("turn item conversion preserves app-server semantics", () => {
});
});
it("keeps active note mentions visible even when a wikilink resolves to the same path", () => {
it("keeps active file mentions visible even when a wikilink resolves to the same path", () => {
const userMessage: TurnItem = {
type: "userMessage",
id: "u1",
@ -130,14 +130,14 @@ describe("turn item conversion preserves app-server semantics", () => {
{ type: "text", text: "Read [[Alpha]].", text_elements: [] },
{ type: "mention", name: "Alpha", path: "thoughts/Alpha.md" },
{ type: "mention", name: "Alpha duplicate", path: "thoughts/Alpha.md" },
{ type: "mention", name: "<active note>", path: "thoughts/Alpha.md" },
{ type: "mention", name: "<active>", path: "thoughts/Alpha.md" },
],
};
expect(messageStreamItemFromTurnItem(userMessage)).toMatchObject({
mentionedFiles: [
{ name: "Alpha", path: "thoughts/Alpha.md" },
{ name: "Active note", path: "thoughts/Alpha.md" },
{ name: "Active file", path: "thoughts/Alpha.md" },
],
});
});

View file

@ -36,14 +36,14 @@ describe("chat app-server transports", () => {
});
});
it("starts turns with wikilink and active note context in one app-server payload", async () => {
it("starts turns with wikilink and active file context in one app-server payload", async () => {
const request = vi.fn().mockResolvedValue({ turn: { id: "turn-1" } });
const client = { request } as unknown as AppServerClient;
const transport = createTestGateway({
currentClient: () => client,
connectedClient: vi.fn().mockResolvedValue(client),
}).turn;
const text = "Compare [[Alpha]] with the active note.";
const text = "Compare [[Alpha]] with the active file.";
const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
text,
(target) => (target === "Alpha" ? { name: "Alpha", path: "notes/Alpha.md" } : null),
@ -67,13 +67,13 @@ describe("chat app-server transports", () => {
input: [
{ type: "text", text, text_elements: [] },
{ type: "mention", name: "Alpha", path: "notes/Alpha.md" },
{ type: "mention", name: "<active note>", path: "notes/Alpha.md" },
{ type: "mention", name: "<active>", path: "notes/Alpha.md" },
],
additionalContext: {
codex_panel_obsidian_context: {
kind: "untrusted",
value:
"Obsidian context for the current user input:\nResolved wikilinks:\n- [[Alpha]] -> notes/Alpha.md\n\nAttached active note:\n- <active note> -> notes/Alpha.md",
"Obsidian context for the current user input:\nResolved wikilinks:\n- [[Alpha]] -> notes/Alpha.md\n\nReferenced active file:\n- <active> -> notes/Alpha.md",
},
},
clientUserMessageId: "local-user",

View file

@ -229,13 +229,25 @@ describe("composer suggestions", () => {
it("uses one active suggestion family at a time", () => {
expect(activeComposerSuggestions("[[bet", notes, [])[0]?.replacement).toBe("[[Beta Note]]");
expect(
activeComposerSuggestions("@act", notes, [], [], [], null, {
activeComposerSuggestions("@active", notes, [], [], [], null, {
contextReferences: {
activeNote: { name: "Beta Note", path: "topics/Beta Note.md", linktext: "Beta Note" },
selection: null,
},
})[0]?.replacement,
).toBe("[[Beta Note]]");
})[0],
).toMatchObject({
display: "Active file",
detail: "topics/Beta Note.md",
replacement: "[[Beta Note]]",
});
expect(
activeComposerSuggestions("@active-note", notes, [], [], [], null, {
contextReferences: {
activeNote: { name: "Beta Note", path: "topics/Beta Note.md", linktext: "Beta Note" },
selection: null,
},
}),
).toEqual([]);
expect(
activeComposerSuggestions("@sel", notes, [], [], [], null, {
contextReferences: {

View file

@ -167,7 +167,7 @@ describe("wikilink context", () => {
});
it("leaves bare context references as raw text", () => {
const text = "整理して @active-note and @selection";
const text = "整理して @active and @selection";
const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
text,
(target) => (target === "notes/Alpha" ? { name: "Alpha", path: "notes/Alpha.md" } : null),
@ -189,7 +189,7 @@ describe("wikilink context", () => {
expect(prepared.input).toEqual([{ type: "text", text }]);
});
it("resolves completed active-note snapshots without depending on the current link context", () => {
it("resolves completed active snapshots without depending on the current link context", () => {
const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
"整理して [[Alpha]]",
() => null,
@ -205,7 +205,7 @@ describe("wikilink context", () => {
expect(prepared.input).toContainEqual({ type: "mention", name: "Alpha", path: "notes/Alpha.md" });
});
it("references the active note on send when enabled without changing visible text", () => {
it("references the active file on send when enabled without changing visible text", () => {
const text = "Rewrite the introduction.";
const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
text,
@ -221,18 +221,18 @@ describe("wikilink context", () => {
expect(prepared.text).toBe(text);
expect(prepared.input).toEqual([
{ type: "text", text },
{ type: "mention", name: "<active note>", path: "notes/Alpha.md" },
{ type: "mention", name: "<active>", path: "notes/Alpha.md" },
{
type: "additionalContext",
key: "codex_panel_obsidian_context",
kind: "untrusted",
value: "Obsidian context for the current user input:\nAttached active note:\n- <active note> -> notes/Alpha.md",
value: "Obsidian context for the current user input:\nReferenced active file:\n- <active> -> notes/Alpha.md",
},
]);
});
it("keeps wikilinks and active note in one Obsidian context when both are present", () => {
const text = "Compare [[Beta]] with the current note.";
it("keeps wikilinks and active file in one Obsidian context when both are present", () => {
const text = "Compare [[Beta]] with the active file.";
const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
text,
(target) => (target === "Beta" ? { name: "Beta", path: "notes/Beta.md" } : null),
@ -248,18 +248,18 @@ describe("wikilink context", () => {
expect(prepared.input).toEqual([
{ type: "text", text },
{ type: "mention", name: "Beta", path: "notes/Beta.md" },
{ type: "mention", name: "<active note>", path: "notes/Alpha.md" },
{ type: "mention", name: "<active>", path: "notes/Alpha.md" },
{
type: "additionalContext",
key: "codex_panel_obsidian_context",
kind: "untrusted",
value:
"Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n\nAttached active note:\n- <active note> -> notes/Alpha.md",
"Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n\nReferenced active file:\n- <active> -> notes/Alpha.md",
},
]);
});
it("keeps wikilinks, selections, and active note in one Obsidian context", () => {
it("keeps wikilinks, selections, and active file in one Obsidian context", () => {
const text = "Compare [[Beta]] with [[Gamma]] (L2:C1-L2:C6).";
const prepared = preparedUserInputWithWikiLinkMentionsSkillsAndContext(
text,
@ -289,13 +289,13 @@ describe("wikilink context", () => {
{ type: "text", text },
{ type: "mention", name: "Beta", path: "notes/Beta.md" },
{ type: "mention", name: "Gamma", path: "notes/Gamma.md" },
{ type: "mention", name: "<active note>", path: "notes/Alpha.md" },
{ type: "mention", name: "<active>", path: "notes/Alpha.md" },
{
type: "additionalContext",
key: "codex_panel_obsidian_context",
kind: "untrusted",
value:
"Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n- [[Gamma]] -> notes/Gamma.md\n\nReferenced selections:\n- [[Gamma]] (L2:C1-L2:C6) -> notes/Gamma.md L2:C1-L2:C6\n\n[[Gamma]] (L2:C1-L2:C6):\nselected gamma\n\nAttached active note:\n- <active note> -> notes/Alpha.md",
"Obsidian context for the current user input:\nResolved wikilinks:\n- [[Beta]] -> notes/Beta.md\n- [[Gamma]] -> notes/Gamma.md\n\nReferenced selections:\n- [[Gamma]] (L2:C1-L2:C6) -> notes/Gamma.md L2:C1-L2:C6\n\n[[Gamma]] (L2:C1-L2:C6):\nselected gamma\n\nReferenced active file:\n- <active> -> notes/Alpha.md",
},
]);
});

View file

@ -52,7 +52,7 @@ function resumeThread(stateStore: ReturnType<typeof createChatStateStore>, items
}
describe("createConversationTurnActions", () => {
it("does not attach composer-only active note context when implementing a plan", async () => {
it("does not attach composer-only active file context when implementing a plan", async () => {
const stateStore = createChatStateStore(createChatState());
const plan = planItem("plan");
resumeThread(stateStore, [plan]);

View file

@ -55,19 +55,19 @@ describe("optimistic turn start helpers", () => {
});
});
it("keeps active note mentions visible even when the same file is mentioned explicitly", () => {
it("keeps active file mentions visible even when the same file is mentioned explicitly", () => {
const text = "Read [[Note]].";
const input = [
{ type: "text" as const, text },
{ type: "mention" as const, name: "Note", path: "Note.md" },
{ type: "mention" as const, name: "Note duplicate", path: "Note.md" },
{ type: "mention" as const, name: "<active note>", path: "Note.md" },
{ type: "mention" as const, name: "<active>", path: "Note.md" },
];
expect(localUserMessageItemFromInput({ id: "local-user", text, codexInput: input })).toMatchObject({
mentionedFiles: [
{ name: "Note", path: "Note.md" },
{ name: "Active note", path: "Note.md" },
{ name: "Active file", path: "Note.md" },
],
});
});

View file

@ -147,7 +147,7 @@ describe("VaultNoteCandidateProvider", () => {
expect(provider.resolveMention("Missing", "")).toBeNull();
});
it("uses the active note only as Obsidian link-resolution context", () => {
it("uses the active file only as Obsidian link-resolution context", () => {
const linked = tFile("notes/Project.md", "Project");
const getFirstLinkpathDest = vi.fn(() => linked);
const app = appFixture({
@ -171,7 +171,7 @@ describe("VaultNoteCandidateProvider", () => {
expect(getFirstLinkpathDest).toHaveBeenCalledWith("Bases/Projects.base", "Daily/Today.md");
});
it("returns active note and selection references from the last markdown view", () => {
it("returns active file and selection references from the last markdown view", () => {
const file = tFile("notes/Alpha.md", "Alpha");
const app = appFixture({
activeView: markdownView(file, {

View file

@ -431,7 +431,7 @@ describe("ChatComposerController", () => {
]);
});
it("freezes active note context when inserting the active-note suggestion", async () => {
it("freezes active file context when inserting the active suggestion", async () => {
const stateStore = createChatStateStore();
const parent = document.createElement("div");
let references: ComposerContextReferences = {
@ -488,7 +488,7 @@ describe("ChatComposerController", () => {
});
});
it("uses the captured active note when slash commands prepare input asynchronously", () => {
it("uses the captured active file when slash commands prepare input asynchronously", () => {
const stateStore = createChatStateStore();
let references: ComposerContextReferences = {
activeNote: { name: "Alpha", path: "notes/Alpha.md", linktext: "Alpha" },
@ -522,12 +522,12 @@ describe("ChatComposerController", () => {
expect(controller.preparedInput("Rewrite intro", snapshot).input).toEqual([
{ type: "text", text: "Rewrite intro" },
{ type: "mention", name: "<active note>", path: "notes/Alpha.md" },
{ type: "mention", name: "<active>", path: "notes/Alpha.md" },
{
type: "additionalContext",
key: "codex_panel_obsidian_context",
kind: "untrusted",
value: "Obsidian context for the current user input:\nAttached active note:\n- <active note> -> notes/Alpha.md",
value: "Obsidian context for the current user input:\nReferenced active file:\n- <active> -> notes/Alpha.md",
},
]);
});

View file

@ -69,7 +69,7 @@ describe("settings tab", () => {
"Composer",
"Send shortcut",
"Scroll thread from composer line edges",
"Reference active note on send",
"Reference active file on send",
"Attachment folder",
"Web clipping",
"Clipped note folder",
@ -142,13 +142,13 @@ describe("settings tab", () => {
expect(settingDesc(tab, "Scroll thread from composer line edges")).toContain("Up/Ctrl+P");
});
it("saves the active note reference setting", async () => {
it("saves the active file reference setting", async () => {
const saveSettings = vi.fn().mockResolvedValue(undefined);
const tab = newSettingsTab({ saveSettings });
tab.display();
const toggle = inputForSetting(tab, "Reference active note on send");
if (!toggle) throw new Error("Missing active note reference toggle");
const toggle = inputForSetting(tab, "Reference active file on send");
if (!toggle) throw new Error("Missing active file reference toggle");
expect(toggle.parentElement?.classList.contains("checkbox-container")).toBe(true);
toggle.checked = true;
@ -156,7 +156,7 @@ describe("settings tab", () => {
await flushPromises();
expect(saveSettings).toHaveBeenCalledOnce();
expect(settingDesc(tab, "Reference active note on send")).toContain("current active note");
expect(settingDesc(tab, "Reference active file on send")).toContain("current active file");
});
it("saves the attachment folder setting", async () => {

View file

@ -132,7 +132,7 @@ describe("settings", () => {
);
});
it("normalizes active note reference on send", () => {
it("normalizes active file reference on send", () => {
expect(normalizeSettings({}).referenceActiveNoteOnSend).toBe(false);
expect(normalizeSettings({ referenceActiveNoteOnSend: true }).referenceActiveNoteOnSend).toBe(true);
expect(normalizeSettings({ referenceActiveNoteOnSend: "yes" }).referenceActiveNoteOnSend).toBe(