mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Resolve relative daily-note references in the composer
This commit is contained in:
parent
dec80ff6cf
commit
0c587f72cd
15 changed files with 260 additions and 14 deletions
|
|
@ -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` 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.
|
||||
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. When Obsidian Daily Notes or the daily section of Periodic Notes is enabled, `@today`, `@tomorrow`, and `@yesterday` insert wikilinks using that daily-note folder and date format. 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.
|
||||
|
||||
|
|
|
|||
34
package-lock.json
generated
34
package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
|||
"@tanstack/query-core": "^5.101.2",
|
||||
"defuddle": "^0.19.1",
|
||||
"micromark": "^4.0.2",
|
||||
"obsidian-daily-notes-interface": "^0.9.5",
|
||||
"preact": "^10.29.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
"eslint-plugin-obsidianmd": "^0.4.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"knip": "^6.24.0",
|
||||
"moment": "^2.29.4",
|
||||
"obsidian": "~1.12.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
|
|
@ -276,7 +278,6 @@
|
|||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
|
@ -287,7 +288,6 @@
|
|||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
|
@ -1271,7 +1271,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
|
|
@ -2366,7 +2365,6 @@
|
|||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
|
|
@ -2410,7 +2408,6 @@
|
|||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
|
|
@ -2447,7 +2444,6 @@
|
|||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
|
|
@ -3356,7 +3352,6 @@
|
|||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
|
|
@ -7051,7 +7046,6 @@
|
|||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
|
|
@ -7258,7 +7252,6 @@
|
|||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
|
|
@ -7269,6 +7262,27 @@
|
|||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian-daily-notes-interface": {
|
||||
"version": "0.9.5",
|
||||
"resolved": "https://registry.npmjs.org/obsidian-daily-notes-interface/-/obsidian-daily-notes-interface-0.9.5.tgz",
|
||||
"integrity": "sha512-Jk1hFagCcMAC1B/LGHCeSLRWIvNCX4qJ6a5HDpUZqs8ckmdszzDKNTdnJZgCVzsUEQKdATJmoNW2kjMzk7+qhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"obsidian": ">=1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian-daily-notes-interface/node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
|
|
@ -8219,7 +8233,6 @@
|
|||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
|
|
@ -8837,7 +8850,6 @@
|
|||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
"eslint-plugin-obsidianmd": "^0.4.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"knip": "^6.24.0",
|
||||
"moment": "^2.29.4",
|
||||
"obsidian": "~1.12.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
"@tanstack/query-core": "^5.101.2",
|
||||
"defuddle": "^0.19.1",
|
||||
"micromark": "^4.0.2",
|
||||
"obsidian-daily-notes-interface": "^0.9.5",
|
||||
"preact": "^10.29.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
export interface DailyNoteReferenceCandidate {
|
||||
keyword: "today" | "tomorrow" | "yesterday";
|
||||
display: string;
|
||||
path: string;
|
||||
linktext: string;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { DailyNoteReferenceCandidate } from "./daily-note-references";
|
||||
import type { NoteCandidate } from "./suggestions";
|
||||
|
||||
export interface WikiLinkMention {
|
||||
|
|
@ -7,6 +8,7 @@ export interface WikiLinkMention {
|
|||
|
||||
export interface NoteCandidateProvider {
|
||||
candidates(sourcePath: string): readonly NoteCandidate[];
|
||||
dailyNoteReferences(sourcePath: string): readonly DailyNoteReferenceCandidate[];
|
||||
tags(): readonly string[];
|
||||
resolveMention(target: string, sourcePath: string): WikiLinkMention | null;
|
||||
dispose(): void;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
type SelectionContextReference,
|
||||
selectionContextReferenceMarker,
|
||||
} from "./context-references";
|
||||
import type { DailyNoteReferenceCandidate } from "./daily-note-references";
|
||||
import { SLASH_COMMANDS, type SlashCommandName, slashCommandSubcommands } from "./slash-commands";
|
||||
|
||||
export interface ComposerSuggestion {
|
||||
|
|
@ -35,6 +36,7 @@ export interface ComposerSuggestion {
|
|||
export interface ComposerSuggestionOptions {
|
||||
activeThreadId?: string | null;
|
||||
contextReferences?: ComposerContextReferences;
|
||||
dailyNoteReferences?: readonly DailyNoteReferenceCandidate[] | (() => readonly DailyNoteReferenceCandidate[]);
|
||||
permissionProfiles?: readonly RuntimePermissionProfileSummary[];
|
||||
tagCandidates?: readonly string[] | (() => readonly string[]);
|
||||
}
|
||||
|
|
@ -112,7 +114,7 @@ export function activeComposerSuggestions(
|
|||
): ComposerSuggestion[] {
|
||||
return (
|
||||
activeWikiLinkSuggestions(beforeCursor, notes) ??
|
||||
activeContextReferenceSuggestions(beforeCursor, options.contextReferences) ??
|
||||
activeContextReferenceSuggestions(beforeCursor, options.contextReferences, options.dailyNoteReferences) ??
|
||||
activeTagSuggestions(beforeCursor, options.tagCandidates ?? []) ??
|
||||
activeSlashSubcommandSuggestions(beforeCursor) ??
|
||||
activeThreadCommandSuggestions(beforeCursor, threads, options.activeThreadId ?? null) ??
|
||||
|
|
@ -128,6 +130,7 @@ export function activeComposerSuggestions(
|
|||
function activeContextReferenceSuggestions(
|
||||
beforeCursor: string,
|
||||
references: ComposerContextReferences | undefined,
|
||||
dailyNoteReferences: readonly DailyNoteReferenceCandidate[] | (() => readonly DailyNoteReferenceCandidate[]) | undefined,
|
||||
): ComposerSuggestion[] | null {
|
||||
const match = /(^|[\s([{])@([A-Za-z-]{0,120})$/.exec(beforeCursor);
|
||||
if (!match) return null;
|
||||
|
|
@ -155,9 +158,25 @@ function activeContextReferenceSuggestions(
|
|||
selectionContext: references.selection,
|
||||
});
|
||||
}
|
||||
const matchingDailyNotes = dailyNoteReferenceList(dailyNoteReferences).filter((candidate) => candidate.keyword.startsWith(query));
|
||||
suggestions.push(
|
||||
...matchingDailyNotes.map((candidate) => ({
|
||||
display: candidate.display,
|
||||
detail: candidate.path,
|
||||
replacement: `[[${candidate.linktext}]]`,
|
||||
start,
|
||||
})),
|
||||
);
|
||||
return suggestions.slice(0, 8);
|
||||
}
|
||||
|
||||
function dailyNoteReferenceList(
|
||||
references: readonly DailyNoteReferenceCandidate[] | (() => readonly DailyNoteReferenceCandidate[]) | undefined,
|
||||
): readonly DailyNoteReferenceCandidate[] {
|
||||
if (!references) return [];
|
||||
return typeof references === "function" ? references() : references;
|
||||
}
|
||||
|
||||
export function applyComposerSuggestionInsertion(
|
||||
value: string,
|
||||
cursor: number,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import type { Moment, MomentInput } from "moment";
|
||||
import { type App, moment, normalizePath, TFile } from "obsidian";
|
||||
import { appHasDailyNotesPluginLoaded, getDailyNoteSettings, type IPeriodicNoteSettings } from "obsidian-daily-notes-interface";
|
||||
|
||||
import type { DailyNoteReferenceCandidate } from "../../application/composer/daily-note-references";
|
||||
import { linktextForFile } from "./vault-note-links.obsidian";
|
||||
|
||||
const RELATIVE_DAILY_NOTES = [
|
||||
{ keyword: "today", display: "Today", dayOffset: 0 },
|
||||
{ keyword: "tomorrow", display: "Tomorrow", dayOffset: 1 },
|
||||
{ keyword: "yesterday", display: "Yesterday", dayOffset: -1 },
|
||||
] as const;
|
||||
|
||||
export function configuredDailyNoteReferences(app: App, sourcePath: string): readonly DailyNoteReferenceCandidate[] {
|
||||
try {
|
||||
if (!appHasDailyNotesPluginLoaded()) return [];
|
||||
const settings = getDailyNoteSettings() as IPeriodicNoteSettings | undefined;
|
||||
if (!settings?.format) return [];
|
||||
return dailyNoteReferencesFromSettings(app, sourcePath, settings, new Date());
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function dailyNoteReferencesFromSettings(
|
||||
app: App,
|
||||
sourcePath: string,
|
||||
settings: IPeriodicNoteSettings,
|
||||
referenceDate: Date,
|
||||
): readonly DailyNoteReferenceCandidate[] {
|
||||
const format = settings.format;
|
||||
if (!format) return [];
|
||||
const createMoment = moment as unknown as (input?: MomentInput) => Moment;
|
||||
return RELATIVE_DAILY_NOTES.map(({ keyword, display, dayOffset }) => {
|
||||
const filename = createMoment(referenceDate).add(dayOffset, "day").format(format);
|
||||
const path = dailyNotePath(settings.folder ?? "", filename);
|
||||
const existingFile = app.vault.getAbstractFileByPath(path);
|
||||
return {
|
||||
keyword,
|
||||
display,
|
||||
path,
|
||||
linktext: existingFile instanceof TFile ? linktextForFile(app, existingFile, sourcePath) : path.replace(/\.md$/i, ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function dailyNotePath(folder: string, filename: string): string {
|
||||
const markdownFilename = filename.toLowerCase().endsWith(".md") ? filename : `${filename}.md`;
|
||||
return normalizePath(folder ? `${folder}/${markdownFilename}` : markdownFilename);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { stripHeadingForLink, TFile } from "obsidian";
|
|||
|
||||
import type { NoteCandidateProvider, WikiLinkMention } from "../../application/composer/note-context";
|
||||
import type { NoteCandidate } from "../../application/composer/suggestions";
|
||||
import { configuredDailyNoteReferences } from "./vault-daily-note-references.obsidian";
|
||||
import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian";
|
||||
|
||||
interface FileCandidate {
|
||||
|
|
@ -48,6 +49,10 @@ export class VaultNoteCandidateProvider implements NoteCandidateProvider {
|
|||
return this.shared.catalog.candidates(sourcePath);
|
||||
}
|
||||
|
||||
dailyNoteReferences(sourcePath: string): ReturnType<NoteCandidateProvider["dailyNoteReferences"]> {
|
||||
return configuredDailyNoteReferences(this.app, sourcePath);
|
||||
}
|
||||
|
||||
tags(): readonly string[] {
|
||||
return this.shared.catalog.tags();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ export class ChatComposerController {
|
|||
{
|
||||
activeThreadId: state.activeThread.id,
|
||||
contextReferences: this.contextReferences(),
|
||||
dailyNoteReferences: () => this.options.noteCandidateProvider.dailyNoteReferences(this.options.sourcePath()),
|
||||
permissionProfiles: state.connection.availablePermissionProfiles,
|
||||
tagCandidates: () => this.options.noteCandidateProvider.tags(),
|
||||
},
|
||||
|
|
@ -299,6 +300,7 @@ export class ChatComposerController {
|
|||
{
|
||||
activeThreadId: state.activeThread.id,
|
||||
contextReferences: this.contextReferences(),
|
||||
dailyNoteReferences: () => this.options.noteCandidateProvider.dailyNoteReferences(this.options.sourcePath()),
|
||||
permissionProfiles: state.connection.availablePermissionProfiles,
|
||||
tagCandidates: () => this.options.noteCandidateProvider.tags(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -281,6 +281,55 @@ describe("composer suggestions", () => {
|
|||
expect(activeComposerSuggestions("/help", notes, [])).toEqual([]);
|
||||
});
|
||||
|
||||
it("resolves relative daily-note references to configured wikilinks", () => {
|
||||
const dailyNoteReferences = [
|
||||
{
|
||||
keyword: "today",
|
||||
display: "Today",
|
||||
path: "Journal/2026/07/2026-07-10.md",
|
||||
linktext: "Journal/2026/07/2026-07-10",
|
||||
},
|
||||
{
|
||||
keyword: "tomorrow",
|
||||
display: "Tomorrow",
|
||||
path: "Journal/2026/07/2026-07-11.md",
|
||||
linktext: "Journal/2026/07/2026-07-11",
|
||||
},
|
||||
{
|
||||
keyword: "yesterday",
|
||||
display: "Yesterday",
|
||||
path: "Journal/2026/07/2026-07-09.md",
|
||||
linktext: "Journal/2026/07/2026-07-09",
|
||||
},
|
||||
] as const;
|
||||
|
||||
expect(
|
||||
activeComposerSuggestions("@to", notes, [], [], [], null, {
|
||||
dailyNoteReferences,
|
||||
}),
|
||||
).toMatchObject([
|
||||
{
|
||||
display: "Today",
|
||||
detail: "Journal/2026/07/2026-07-10.md",
|
||||
replacement: "[[Journal/2026/07/2026-07-10]]",
|
||||
},
|
||||
{
|
||||
display: "Tomorrow",
|
||||
detail: "Journal/2026/07/2026-07-11.md",
|
||||
replacement: "[[Journal/2026/07/2026-07-11]]",
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
activeComposerSuggestions("Please inspect @yesterday", notes, [], [], [], null, {
|
||||
dailyNoteReferences,
|
||||
})[0],
|
||||
).toMatchObject({
|
||||
display: "Yesterday",
|
||||
detail: "Journal/2026/07/2026-07-09.md",
|
||||
replacement: "[[Journal/2026/07/2026-07-09]]",
|
||||
});
|
||||
});
|
||||
|
||||
it("suggests Obsidian tags after a hash trigger", () => {
|
||||
const options = {
|
||||
tagCandidates: ["project/codex", "project/obsidian", "daily-note", "web"],
|
||||
|
|
|
|||
|
|
@ -1,10 +1,72 @@
|
|||
import { type App, type EventRef, TFile } from "obsidian";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const dailyNotesInterface = vi.hoisted(() => ({
|
||||
appHasDailyNotesPluginLoaded: vi.fn<() => boolean>(),
|
||||
getDailyNoteSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("obsidian-daily-notes-interface", () => dailyNotesInterface);
|
||||
|
||||
import { VaultComposerContextReferenceProvider } from "../../../../src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian";
|
||||
import {
|
||||
configuredDailyNoteReferences,
|
||||
dailyNoteReferencesFromSettings,
|
||||
} from "../../../../src/features/chat/host/obsidian/vault-daily-note-references.obsidian";
|
||||
import { VaultNoteCandidateProvider } from "../../../../src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian";
|
||||
|
||||
describe("VaultNoteCandidateProvider", () => {
|
||||
beforeEach(() => {
|
||||
dailyNotesInterface.appHasDailyNotesPluginLoaded.mockReset().mockReturnValue(false);
|
||||
dailyNotesInterface.getDailyNoteSettings.mockReset().mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("keeps optional daily-note integration failures out of composer suggestions", () => {
|
||||
dailyNotesInterface.appHasDailyNotesPluginLoaded.mockImplementation(() => {
|
||||
throw new Error("Daily Notes API unavailable");
|
||||
});
|
||||
|
||||
expect(configuredDailyNoteReferences(appFixture(), "Inbox.md")).toEqual([]);
|
||||
});
|
||||
|
||||
it("builds relative references from the configured daily-note folder and format", () => {
|
||||
const existingToday = tFile("Journal/2026/07/2026-07-10.md", "2026-07-10");
|
||||
const fileToLinktext = vi.fn((file: TFile) => (file === existingToday ? "2026-07-10" : file.path));
|
||||
const app = appFixture({
|
||||
abstractFiles: new Map([[existingToday.path, existingToday]]),
|
||||
fileToLinktext,
|
||||
});
|
||||
|
||||
expect(
|
||||
dailyNoteReferencesFromSettings(
|
||||
app,
|
||||
"Projects/Codex.md",
|
||||
{ folder: "Journal", format: "YYYY/MM/YYYY-MM-DD", template: "" },
|
||||
new Date(2026, 6, 10, 12),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
keyword: "today",
|
||||
display: "Today",
|
||||
path: "Journal/2026/07/2026-07-10.md",
|
||||
linktext: "2026-07-10",
|
||||
},
|
||||
{
|
||||
keyword: "tomorrow",
|
||||
display: "Tomorrow",
|
||||
path: "Journal/2026/07/2026-07-11.md",
|
||||
linktext: "Journal/2026/07/2026-07-11",
|
||||
},
|
||||
{
|
||||
keyword: "yesterday",
|
||||
display: "Yesterday",
|
||||
path: "Journal/2026/07/2026-07-09.md",
|
||||
linktext: "Journal/2026/07/2026-07-09",
|
||||
},
|
||||
]);
|
||||
expect(fileToLinktext).toHaveBeenCalledWith(existingToday, "Projects/Codex.md", true);
|
||||
});
|
||||
|
||||
it("builds note candidates from markdown files", () => {
|
||||
const app = appFixture({
|
||||
files: [
|
||||
|
|
|
|||
|
|
@ -173,6 +173,32 @@ describe("ChatComposerController", () => {
|
|||
expect(tags).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inserts configured relative daily-note references as wikilinks", () => {
|
||||
const dailyNoteReferences = vi.fn(() => [
|
||||
{
|
||||
keyword: "today" as const,
|
||||
display: "Today",
|
||||
path: "Journal/2026-07-10.md",
|
||||
linktext: "Journal/2026-07-10",
|
||||
},
|
||||
]);
|
||||
const { controller, parent, stateStore } = composerControllerFixture({
|
||||
controller: {
|
||||
noteCandidateProvider: noteProvider({ dailyNoteReferences }),
|
||||
sourcePath: () => "Inbox.md",
|
||||
},
|
||||
});
|
||||
|
||||
renderComposerController(parent, controller, stateStore);
|
||||
setTextAreaValue(composer(parent), "@today");
|
||||
composer(parent).setSelectionRange(6, 6);
|
||||
composer(parent).dispatchEvent(new Event("input", { bubbles: true }));
|
||||
composer(parent).dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
|
||||
|
||||
expect(composer(parent).value).toBe("[[Journal/2026-07-10]]");
|
||||
expect(dailyNoteReferences).toHaveBeenCalledWith("Inbox.md");
|
||||
});
|
||||
|
||||
it("keeps Tab wikilink insertion before closing brackets while Enter lands after them", () => {
|
||||
const stateStore = createChatStateStore();
|
||||
const parent = document.createElement("div");
|
||||
|
|
@ -919,6 +945,7 @@ describe("ChatComposerController", () => {
|
|||
function noteProvider(overrides: Partial<NoteCandidateProvider> = {}): NoteCandidateProvider {
|
||||
return {
|
||||
candidates: () => [],
|
||||
dailyNoteReferences: () => [],
|
||||
tags: () => [],
|
||||
resolveMention: () => null,
|
||||
dispose: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -501,6 +501,7 @@ function setTextAreaValue(textarea: HTMLTextAreaElement, value: string): void {
|
|||
function noteProvider(overrides: Partial<NoteCandidateProvider> = {}): NoteCandidateProvider {
|
||||
return {
|
||||
candidates: () => [],
|
||||
dailyNoteReferences: () => [],
|
||||
tags: () => [],
|
||||
resolveMention: () => null,
|
||||
dispose: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import Moment from "moment";
|
||||
|
||||
import { installObsidianElementHelpers } from "../support/obsidian-dom";
|
||||
|
||||
export type App = Record<string, never>;
|
||||
|
||||
export const notices: string[] = [];
|
||||
|
||||
export const moment = Moment;
|
||||
|
||||
export const Platform = {
|
||||
isDesktop: true,
|
||||
isMobile: false,
|
||||
|
|
|
|||
|
|
@ -10,5 +10,10 @@ export default defineConfig({
|
|||
test: {
|
||||
environment: "node",
|
||||
globals: false,
|
||||
server: {
|
||||
deps: {
|
||||
inline: ["obsidian-daily-notes-interface"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue